From 4b65d4da066accdd5d45d2e39968428bd8ec487d Mon Sep 17 00:00:00 2001 From: huangyuhui Date: Tue, 9 Jan 2018 00:03:34 +0800 Subject: [PATCH] Make background transparent? --- .../jackhuang/hmcl/game/AccountHelper.java | 144 +++++ .../jackhuang/hmcl/game/HMCLGameLauncher.java | 51 ++ .../hmcl/game/HMCLGameRepository.java | 209 +++++++ .../jackhuang/hmcl/game/LauncherHelper.java | 253 ++++++++ .../org/jackhuang/hmcl/game/LoadingState.java | 26 + .../org/jackhuang/hmcl/setting/Profile.java | 184 ++++++ .../hmcl/setting/VersionSetting.java | 591 ++++++++++++++++++ .../java/org/jackhuang/hmcl/ui/LogWindow.java | 235 +++++++ .../main/kotlin/org/jackhuang/hmcl/Events.kt | 5 +- .../main/kotlin/org/jackhuang/hmcl/Main.kt | 8 +- .../org/jackhuang/hmcl/game/AccountHelper.kt | 87 --- .../jackhuang/hmcl/game/HMCLGameRepository.kt | 154 ----- ...ncher.kt => HMCLMultiCharacterSelector.kt} | 19 +- .../org/jackhuang/hmcl/game/LauncherHelper.kt | 227 ------- .../org/jackhuang/hmcl/game/ModpackHelper.kt | 8 +- .../org/jackhuang/hmcl/setting/Profile.kt | 123 ---- .../org/jackhuang/hmcl/setting/Settings.kt | 4 +- .../jackhuang/hmcl/setting/VersionSetting.kt | 336 ---------- .../org/jackhuang/hmcl/ui/AccountsPage.kt | 4 +- .../org/jackhuang/hmcl/ui/Controllers.kt | 2 +- .../kotlin/org/jackhuang/hmcl/ui/FXUtils.kt | 2 +- .../jackhuang/hmcl/ui/LeftPaneController.kt | 4 +- .../kotlin/org/jackhuang/hmcl/ui/LogWindow.kt | 156 ----- .../kotlin/org/jackhuang/hmcl/ui/MainPage.kt | 50 +- .../org/jackhuang/hmcl/ui/ProfilePage.kt | 2 +- .../org/jackhuang/hmcl/ui/VersionItem.kt | 16 +- .../hmcl/ui/VersionSettingsController.kt | 68 +- .../hmcl/ui/YggdrasilAccountLoginPane.kt | 3 +- .../kotlin/org/jackhuang/hmcl/util/Lang.kt | 13 +- .../assets/css/jfoenix-main-demo.css | 10 + .../main/resources/assets/fxml/decorator.fxml | 9 +- HMCL/src/main/resources/assets/fxml/main.fxml | 8 +- .../resources/assets/fxml/version-item.fxml | 6 +- .../assets/fxml/version/version-settings.fxml | 4 +- .../java/org/jackhuang/hmcl/auth/Account.java | 6 +- .../hmcl/auth/MultiCharacterSelector.java | 39 ++ .../hmcl/auth/NoCharacterException.java | 34 + .../auth/NoSelectedCharacterException.java | 41 ++ .../jackhuang/hmcl/auth/OfflineAccount.java | 2 +- .../hmcl/auth/yggdrasil/GameProfile.java | 5 +- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 25 +- .../forge/{Install.java => ForgeInstall.java} | 6 +- ...lProfile.java => ForgeInstallProfile.java} | 8 +- .../hmcl/download/forge/ForgeInstallTask.java | 2 +- .../hmcl/download/game/GameLibrariesTask.java | 15 +- .../download/game/GameRemoteVersions.java | 2 +- .../game/VersionJsonDownloadTask.java | 2 +- .../download/liteloader/LiteLoaderBranch.java | 2 +- .../liteloader/LiteLoaderInstallTask.java | 4 +- .../optifine/OptiFineInstallTask.java | 2 +- .../optifine/OptiFineVersionList.java | 5 +- .../java/org/jackhuang/hmcl/event/Event.java | 2 +- .../org/jackhuang/hmcl/event/EventBus.java | 9 +- .../jackhuang/hmcl/event/EventManager.java | 33 +- .../org/jackhuang/hmcl/event/FailedEvent.java | 2 +- .../hmcl/event/GameJsonParseFailedEvent.java | 52 ++ .../hmcl/event/JVMLaunchFailedEvent.java | 12 +- .../hmcl/event/LoadedOneVersionEvent.java | 25 +- .../event/ProcessExitedAbnormallyEvent.java | 12 +- .../hmcl/event/ProcessStoppedEvent.java | 12 +- .../hmcl/event/RefreshedVersionsEvent.java | 11 +- .../hmcl/event/RefreshingVersionsEvent.java | 11 +- .../org/jackhuang/hmcl/game/Arguments.java | 6 +- .../org/jackhuang/hmcl/game/AssetIndex.java | 2 +- .../hmcl/game/DefaultGameRepository.java | 39 +- .../java/org/jackhuang/hmcl/game/Library.java | 4 +- .../jackhuang/hmcl/game/RuledArgument.java | 2 +- .../java/org/jackhuang/hmcl/game/Version.java | 7 +- .../hmcl/launch/DefaultLauncher.java | 23 +- .../jackhuang/hmcl/launch/Log4jHandler.java | 21 +- .../jackhuang/hmcl/mod/CurseInstallTask.java | 2 +- .../org/jackhuang/hmcl/mod/CurseManifest.java | 4 +- .../mod/MultiMCInstanceConfiguration.java | 3 +- .../org/jackhuang/hmcl/task/SimpleTask.java | 9 +- .../java/org/jackhuang/hmcl/task/Task.java | 12 +- .../org/jackhuang/hmcl/task/TaskEvent.java | 4 +- .../hmcl/util/ExceptionalConsumer.java | 25 + .../java/org/jackhuang/hmcl/util/Lang.java | 32 +- .../jackhuang/hmcl/util/OperatingSystem.java | 8 +- .../jackhuang/hmcl/util/VersionNumber.java | 2 +- 80 files changed, 2201 insertions(+), 1406 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/LoadingState.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/game/AccountHelper.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLGameRepository.kt rename HMCL/src/main/kotlin/org/jackhuang/hmcl/game/{HMCLGameLauncher.kt => HMCLMultiCharacterSelector.kt} (54%) delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/game/LauncherHelper.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/Profile.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/VersionSetting.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/LogWindow.kt create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/auth/MultiCharacterSelector.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoCharacterException.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/{Install.java => ForgeInstall.java} (90%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/{InstallProfile.java => ForgeInstallProfile.java} (88%) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/event/GameJsonParseFailedEvent.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/ExceptionalConsumer.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java new file mode 100644 index 000000000..e225bd1d7 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java @@ -0,0 +1,144 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.game; + +import javafx.geometry.Rectangle2D; +import javafx.scene.image.Image; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.Scheduler; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.DialogController; +import org.jackhuang.hmcl.ui.FXUtilsKt; +import org.jackhuang.hmcl.util.NetworkUtils; + +import java.io.File; +import java.net.Proxy; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +public final class AccountHelper { + public static final AccountHelper INSTANCE = new AccountHelper(); + private AccountHelper() {} + + public static final File SKIN_DIR = new File(Main.APPDATA, "skins"); + + public static void loadSkins() { + loadSkins(Proxy.NO_PROXY); + } + + public static void loadSkins(Proxy proxy) { + for (Account account : Settings.INSTANCE.getAccounts().values()) { + if (account instanceof YggdrasilAccount) { + new SkinLoadTask((YggdrasilAccount) account, proxy, false).start(); + } + } + } + public static Task loadSkinAsync(YggdrasilAccount account) { + return loadSkinAsync(account, Settings.INSTANCE.getProxy()); + } + + public static Task loadSkinAsync(YggdrasilAccount account, Proxy proxy) { + return new SkinLoadTask(account, proxy, false); + } + + public static Task refreshSkinAsync(YggdrasilAccount account) { + return refreshSkinAsync(account, Settings.INSTANCE.getProxy()); + } + + public static Task refreshSkinAsync(YggdrasilAccount account, Proxy proxy) { + return new SkinLoadTask(account, proxy, true); + } + + private static File getSkinFile(String name) { + return new File(SKIN_DIR, name + ".png"); + } + + public static Image getSkin(YggdrasilAccount account) { + return getSkin(account, 1); + } + + public static Image getSkin(YggdrasilAccount account, double scaleRatio) { + if (account.getSelectedProfile() == null) return FXUtilsKt.DEFAULT_ICON; + String name = account.getSelectedProfile().getName(); + if (name == null) return FXUtilsKt.DEFAULT_ICON; + File file = getSkinFile(name); + if (file.exists()) { + Image original = new Image("file:" + file.getAbsolutePath()); + return new Image("file:" + file.getAbsolutePath(), + original.getWidth() * scaleRatio, + original.getHeight() * scaleRatio, + false, false); + } + return FXUtilsKt.DEFAULT_ICON; + } + + public static Rectangle2D getViewport(double scaleRatio) { + double size = 8.0 * scaleRatio; + return new Rectangle2D(size, size, size, size); + } + + private static class SkinLoadTask extends Task { + private final YggdrasilAccount account; + private final Proxy proxy; + private final boolean refresh; + private final List dependencies = new LinkedList<>(); + + public SkinLoadTask(YggdrasilAccount account, Proxy proxy) { + this(account, proxy, false); + } + + public SkinLoadTask(YggdrasilAccount account, Proxy proxy, boolean refresh) { + this.account = account; + this.proxy = proxy; + this.refresh = refresh; + } + + @Override + public Scheduler getScheduler() { + return Schedulers.io(); + } + + @Override + public Collection getDependencies() { + return dependencies; + } + + @Override + public void execute() throws Exception { + if (account.canLogIn() && (account.getSelectedProfile() == null || refresh)) + DialogController.INSTANCE.logIn(account); + + GameProfile profile = account.getSelectedProfile(); + if (profile == null) return; + String name = profile.getName(); + if (name == null) return; + String url = "http://skins.minecraft.net/MinecraftSkins/" + name + ".png"; + File file = getSkinFile(name); + if (!refresh && file.exists()) + return; + dependencies.add(new FileDownloadTask(NetworkUtils.toURL(url), file, proxy)); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java new file mode 100644 index 000000000..26826f4d4 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java @@ -0,0 +1,51 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.game; + +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.auth.AuthInfo; +import org.jackhuang.hmcl.launch.DefaultLauncher; +import org.jackhuang.hmcl.launch.ProcessListener; + +import java.util.List; + +/** + * @author huangyuhui + */ +public final class HMCLGameLauncher extends DefaultLauncher { + + public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options) { + this(repository, versionId, authInfo, options, null); + } + + public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) { + this(repository, versionId, authInfo, options, listener, true); + } + + public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) { + super(repository, versionId, authInfo, options, listener, daemon); + } + + @Override + protected void appendJvmArgs(List result) { + super.appendJvmArgs(result); + + result.add("-Dminecraft.launcher.version=" + Main.VERSION); + result.add("-Dminecraft.launcher.brand=" + Main.NAME); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java new file mode 100644 index 000000000..b70637481 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -0,0 +1,209 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.game; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.jackhuang.hmcl.event.EventBus; +import org.jackhuang.hmcl.event.RefreshedVersionsEvent; +import org.jackhuang.hmcl.event.RefreshingVersionsEvent; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.setting.VersionSetting; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.util.FileUtils; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Logging; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; + +public class HMCLGameRepository extends DefaultGameRepository { + private final Profile profile; + private final Map versionSettings = new HashMap<>(); + private final Set beingModpackVersions = new HashSet<>(); + + public boolean checkedModpack = false, checkingModpack = false; + + public HMCLGameRepository(Profile profile, File baseDirectory) { + super(baseDirectory); + this.profile = profile; + } + + private boolean useSelf(String version, String assetId) { + VersionSetting vs = profile.getVersionSetting(version); + return new File(getBaseDirectory(), "assets/indexes/" + assetId + ".json").exists() || vs.isNoCommon(); + } + + @Override + public File getAssetDirectory(String version, String assetId) { + if (useSelf(version, assetId)) + return super.getAssetDirectory(version, assetId); + else + return new File(Settings.INSTANCE.getCommonPath(), "assets"); + } + + @Override + public File getRunDirectory(String id) { + if (beingModpackVersions.contains(id)) + return getVersionRoot(id); + else { + VersionSetting vs = profile.getVersionSetting(id); + switch (vs.getGameDirType()) { + case VERSION_FOLDER: return getVersionRoot(id); + case ROOT_FOLDER: return super.getRunDirectory(id); + case CUSTOM: return new File(vs.getGameDir()); + default: throw new Error(); + } + } + } + + @Override + public File getLibraryFile(Version version, Library lib) { + VersionSetting vs = profile.getVersionSetting(version.getId()); + File self = super.getLibraryFile(version, lib); + if (self.exists() || vs.isNoCommon()) + return self; + else + return new File(Settings.INSTANCE.getCommonPath(), "libraries/" + lib.getPath()); + } + + + @Override + protected void refreshVersionsImpl() { + versionSettings.clear(); + super.refreshVersionsImpl(); + versions.keySet().forEach(this::loadVersionSetting); + + checkModpack(); + + try { + File file = new File(getBaseDirectory(), "launcher_profiles.json"); + if (!file.exists() && !versions.isEmpty()) + FileUtils.writeText(file, PROFILE); + } catch (IOException ex) { + Logging.LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex); + } + } + + @Override + public void refreshVersions() { + EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)); + Schedulers.newThread().schedule(() -> { + refreshVersionsImpl(); + EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this)); + }); + } + + public void changeDirectory(File newDirectory) { + setBaseDirectory(newDirectory); + refreshVersions(); + } + + private void checkModpack() { + + if (!checkingModpack) { + checkingModpack = true; + if (getVersionCount() == 0) { + File modpack = new File("modpack.zip").getAbsoluteFile(); + if (modpack.exists()) { + // TODO + } + /* + SwingUtilities.invokeLater(() -> { + if (TaskWindow.factory().execute(ModpackManager.install(MainFrame.INSTANCE, modpack, this, null))) + refreshVersions(); + checkedModpack = true; + }); + */ + } + } + } + + private File getVersionSettingFile(String id) { + return new File(getVersionRoot(id), "hmclversion.cfg"); + } + + private void loadVersionSetting(String id) { + File file = getVersionSettingFile(id); + if (file.exists()) + try { + VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class); + initVersionSetting(id, versionSetting); + } catch (Exception ex) { + // If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated. + } + } + + public VersionSetting createVersionSetting(String id) { + if (!hasVersion(id)) + return null; + if (versionSettings.containsKey(id)) + return versionSettings.get(id); + else + return initVersionSetting(id, new VersionSetting()); + } + + private VersionSetting initVersionSetting(String id, VersionSetting vs) { + vs.addPropertyChangedListener(a -> saveVersionSetting(id)); + versionSettings.put(id, vs); + return vs; + } + + /** + * Get the version setting for version id. + * + * @param id version id + * + * @return may return null if the id not exists + */ + public VersionSetting getVersionSetting(String id) { + if (!versionSettings.containsKey(id)) + loadVersionSetting(id); + return versionSettings.get(id); + } + + public File getVersionIcon(String id) { + return new File(getVersionRoot(id), "icon.png"); + } + + public void saveVersionSetting(String id) { + if (!versionSettings.containsKey(id)) + return; + Lang.invoke(() -> FileUtils.writeText(getVersionSettingFile(id), GSON.toJson(versionSettings.get(id)))); + } + + public void markVersionAsModpack(String id) { + beingModpackVersions.add(id); + } + + public void undoMark(String id) { + beingModpackVersions.remove(id); + } + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting() + .registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE) + .create(); + + private static final String PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}"; +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java new file mode 100644 index 000000000..ccc4d22e7 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -0,0 +1,253 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.game; + +import com.jfoenix.concurrency.JFXUtilities; +import javafx.application.Platform; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.AuthInfo; +import org.jackhuang.hmcl.auth.AuthenticationException; +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.launch.DefaultLauncher; +import org.jackhuang.hmcl.launch.ProcessListener; +import org.jackhuang.hmcl.mod.CurseCompletionTask; +import org.jackhuang.hmcl.setting.LauncherVisibility; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.setting.VersionSetting; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.task.TaskListener; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.DialogController; +import org.jackhuang.hmcl.ui.LaunchingStepsPane; +import org.jackhuang.hmcl.ui.LogWindow; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Log4jLevel; +import org.jackhuang.hmcl.util.ManagedProcess; +import org.jackhuang.hmcl.util.Pair; + +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +public final class LauncherHelper { + public static final LauncherHelper INSTANCE = new LauncherHelper(); + private LauncherHelper(){} + + private final LaunchingStepsPane launchingStepsPane = new LaunchingStepsPane(); + public static final Queue PROCESSES = new ConcurrentLinkedQueue<>(); + + public void launch(String selectedVersion) { + Profile profile = Settings.INSTANCE.getSelectedProfile(); + GameRepository repository = profile.getRepository(); + DefaultDependencyManager dependencyManager = profile.getDependency(); + Account account = Settings.INSTANCE.getSelectedAccount(); + if (account == null) + throw new IllegalStateException("No account"); + + Version version = repository.getVersion(selectedVersion); + VersionSetting setting = profile.getVersionSetting(selectedVersion); + + Controllers.INSTANCE.dialog(launchingStepsPane); + TaskExecutor executor = Task.of(v -> emitStatus(LoadingState.DEPENDENCIES), Schedulers.javafx()) + .then(dependencyManager.checkGameCompletionAsync(version)) + .then(Task.of(v -> emitStatus(LoadingState.MODS), Schedulers.javafx())) + .then(new CurseCompletionTask(dependencyManager, selectedVersion)) + .then(Task.of(v -> emitStatus(LoadingState.LOGIN), Schedulers.javafx())) + .then(Task.of(v -> { + try { + v.set("account", account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.getProxy())); + } catch (AuthenticationException e) { + v.set("account", DialogController.INSTANCE.logIn(account)); + JFXUtilities.runInFX(() -> Controllers.INSTANCE.dialog(launchingStepsPane)); + } + })) + .then(Task.of(v -> emitStatus(LoadingState.LAUNCHING), Schedulers.javafx())) + .then(Task.of(v -> { + v.set("launcher", new HMCLGameLauncher( + repository, selectedVersion, v.get("account"), setting.toLaunchOptions(profile.getGameDir()), new HMCLProcessListener(v.get("account"), setting) + )); + })) + .then(v -> v.get("launcher").launchAsync()) + .then(Task.of(v -> { + PROCESSES.add(v.get(DefaultLauncher.LAUNCH_ASYNC_ID)); + if (setting.getLauncherVisibility() == LauncherVisibility.CLOSE) + Main.Companion.stop(); + })) + .executor(); + + executor.setTaskListener(new TaskListener() { + AtomicInteger finished = new AtomicInteger(0); + @Override + public void onFinished(Task task) { + finished.incrementAndGet(); + Platform.runLater(() -> { + launchingStepsPane.getPgsTasks().setProgress(1.0 * finished.get() / executor.getRunningTasks()); + }); + } + + @Override + public void onTerminate() { + Platform.runLater(Controllers.INSTANCE::closeDialog); + } + }); + + executor.start(); + } + + public static void stopManagedProcesses() { + synchronized (PROCESSES) { + while (!PROCESSES.isEmpty()) + Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop); + } + } + + public void emitStatus(LoadingState state) { + if (state == LoadingState.DONE) + Controllers.INSTANCE.closeDialog(); + + launchingStepsPane.getLblCurrentState().setText(state.toString()); + launchingStepsPane.getLblSteps().setText((state.ordinal() + 1) + " / " + LoadingState.values().length); + } + + private void checkExit(LauncherVisibility v) { + switch (v) { + case HIDE_AND_REOPEN: + Platform.runLater(Controllers.INSTANCE.getStage()::show); + break; + case KEEP: + // No operations here + break; + case CLOSE: + throw new Error("Never get to here"); + case HIDE: + Platform.runLater(() -> { + // Shut down the platform when user closed log window. + Platform.setImplicitExit(true); + // If we use Main.stop(), log window will be halt immediately. + Main.Companion.stopWithoutJavaFXPlatform(); + }); + break; + } + } + + /** + * The managed process listener. + * Guarantee that one [JavaProcess], one [HMCLProcessListener]. + * Because every time we launched a game, we generates a new [HMCLProcessListener] + */ + class HMCLProcessListener implements ProcessListener { + + private final VersionSetting setting; + private final Map forbiddenTokens; + private final LauncherVisibility visibility; + private ManagedProcess process; + private boolean lwjgl; + private LogWindow logWindow; + private final LinkedList> logs; + + public HMCLProcessListener(AuthInfo authInfo, VersionSetting setting) { + this.setting = setting; + + if (authInfo == null) + forbiddenTokens = Collections.emptyMap(); + else + forbiddenTokens = Lang.mapOf( + new Pair<>(authInfo.getAuthToken(), ""), + new Pair<>(authInfo.getUserId(), ""), + new Pair<>(authInfo.getUsername(), "") + ); + + visibility = setting.getLauncherVisibility(); + logs = new LinkedList<>(); + } + + @Override + public void setProcess(ManagedProcess process) { + this.process = process; + + if (setting.isShowLogs()) + Platform.runLater(() -> { + logWindow = new LogWindow(); + logWindow.show(); + }); + } + + @Override + public void onLog(String log, Log4jLevel level) { + String newLog = log; + for (Map.Entry entry : forbiddenTokens.entrySet()) + newLog = newLog.replace(entry.getKey(), entry.getValue()); + + if (level.lessOrEqual(Log4jLevel.ERROR)) + System.err.print(log); + else + System.out.print(log); + + Platform.runLater(() -> { + logs.add(new Pair<>(log, level)); + if (logs.size() > Settings.INSTANCE.getLogLines()) + logs.removeFirst(); + if (logWindow != null) + logWindow.logLine(log, level); + }); + + if (!lwjgl && log.contains("LWJGL Version: ")) { + lwjgl = true; + switch (visibility) { + case HIDE_AND_REOPEN: + Platform.runLater(() -> { + Controllers.INSTANCE.getStage().hide(); + emitStatus(LoadingState.DONE); + }); + break; + case CLOSE: + throw new Error("Never come to here"); + case KEEP: + // No operations here + break; + case HIDE: + Platform.runLater(() -> { + Controllers.INSTANCE.getStage().close(); + emitStatus(LoadingState.DONE); + }); + break; + } + } + } + + @Override + public void onExit(int exitCode, ExitType exitType) { + if (exitType != ExitType.NORMAL && logWindow == null) + Platform.runLater(() -> { + logWindow = new LogWindow(); + logWindow.show(); + logWindow.onDone.register(() -> { + for (Map.Entry entry : logs) + logWindow.logLine(entry.getKey(), entry.getValue()); + }); + }); + + checkExit(visibility); + } + + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LoadingState.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LoadingState.java new file mode 100644 index 000000000..9cac32a47 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LoadingState.java @@ -0,0 +1,26 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.game; + +public enum LoadingState { + DEPENDENCIES, + MODS, + LOGIN, + LAUNCHING, + DONE +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java new file mode 100644 index 000000000..cef51a4ff --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java @@ -0,0 +1,184 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2013 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.setting; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.io.File; +import java.lang.reflect.Type; +import java.util.Optional; +import javafx.beans.InvalidationListener; +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.mod.ModManager; +import org.jackhuang.hmcl.util.ImmediateObjectProperty; +import org.jackhuang.hmcl.util.ImmediateStringProperty; + +/** + * + * @author huangyuhui + */ +public final class Profile { + + private final HMCLGameRepository repository; + private final ModManager modManager; + + private final ImmediateObjectProperty gameDirProperty; + + public ImmediateObjectProperty getGameDirProperty() { + return gameDirProperty; + } + + public File getGameDir() { + return gameDirProperty.get(); + } + + public void setGameDir(File gameDir) { + gameDirProperty.set(gameDir); + } + + private final ImmediateObjectProperty globalProperty = new ImmediateObjectProperty<>(this, "global", new VersionSetting()); + + public ImmediateObjectProperty globalProperty() { + return globalProperty; + } + + public VersionSetting getGlobal() { + return globalProperty.get(); + } + + public void setGlobal(VersionSetting global) { + globalProperty.set(global); + } + + private final ImmediateStringProperty nameProperty; + + public ImmediateStringProperty getNameProperty() { + return nameProperty; + } + + public String getName() { + return nameProperty.get(); + } + + public void setName(String name) { + nameProperty.set(name); + } + + public Profile() { + this("Default"); + } + + public Profile(String name) { + this(name, new File(".minecraft")); + } + + public Profile(String name, File initialGameDir) { + nameProperty = new ImmediateStringProperty(this, "name", name); + gameDirProperty = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir); + repository = new HMCLGameRepository(this, initialGameDir); + modManager = new ModManager(repository); + + gameDirProperty.addListener((a, b, newValue) -> repository.changeDirectory(newValue)); + } + + public HMCLGameRepository getRepository() { + return repository; + } + + public ModManager getModManager() { + return modManager; + } + + public DefaultDependencyManager getDependency() { + return new DefaultDependencyManager(repository, Settings.INSTANCE.getDownloadProvider(), Settings.INSTANCE.getProxy()); + } + + public VersionSetting getVersionSetting(String id) { + VersionSetting vs = repository.getVersionSetting(id); + if (vs == null || vs.isUsesGlobal()) { + getGlobal().setGlobal(true); // always keep global.isGlobal = true + return getGlobal(); + } else + return vs; + } + + public boolean isVersionGlobal(String id) { + VersionSetting vs = repository.getVersionSetting(id); + return vs == null || vs.isUsesGlobal(); + } + + public VersionSetting specializeVersionSetting(String id) { + VersionSetting vs = repository.getVersionSetting(id); + if (vs == null) + vs = repository.createVersionSetting(id); + if (vs == null) + return null; + vs.setUsesGlobal(false); + return vs; + } + + public void globalizeVersionSetting(String id) { + VersionSetting vs = repository.getVersionSetting(id); + if (vs != null) + vs.setUsesGlobal(true); + } + + public void addPropertyChangedListener(InvalidationListener listener) { + nameProperty.addListener(listener); + globalProperty.addListener(listener); + gameDirProperty.addListener(listener); + } + + public static final class Serializer implements JsonSerializer, JsonDeserializer { + public static final Serializer INSTANCE = new Serializer(); + + private Serializer() { + } + + @Override + public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) { + if (src == null) + return JsonNull.INSTANCE; + + JsonObject jsonObject = new JsonObject(); + jsonObject.add("global", context.serialize(src.getGlobal())); + jsonObject.addProperty("gameDir", src.getGameDir().getPath()); + + return jsonObject; + } + + @Override + public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json == null || json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null; + JsonObject obj = (JsonObject) json; + String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""); + + Profile profile = new Profile("Default", new File(gameDir)); + profile.setGlobal(context.deserialize(obj.get("global"), VersionSetting.class)); + return profile; + } + + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java new file mode 100644 index 000000000..13fca2e8a --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java @@ -0,0 +1,591 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2013 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.setting; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Optional; + +import com.google.gson.*; +import javafx.beans.InvalidationListener; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.game.LaunchOptions; +import org.jackhuang.hmcl.util.*; + +/** + * + * @author huangyuhui + */ +public final class VersionSetting { + + public transient String id; + + private boolean global = false; + + public boolean isGlobal() { + return global; + } + + public void setGlobal(boolean global) { + this.global = global; + } + + /** + * HMCL Version Settings have been divided into 2 parts. + * 1. Global settings. + * 2. Version settings. + * If a version claims that it uses global settings, its version setting will be disabled. + * + * Defaults false because if one version uses global first, custom version file will not be generated. + */ + private final ImmediateBooleanProperty usesGlobalProperty = new ImmediateBooleanProperty(this, "usesGlobal", false); + + public ImmediateBooleanProperty usesGlobalProperty() { + return usesGlobalProperty; + } + + public boolean isUsesGlobal() { + return usesGlobalProperty.get(); + } + + public void setUsesGlobal(boolean usesGlobal) { + usesGlobalProperty.set(usesGlobal); + } + + // java + + /** + * Java version or null if user customizes java directory. + */ + private final ImmediateStringProperty javaProperty = new ImmediateStringProperty(this, "java", ""); + + public ImmediateStringProperty javaProperty() { + return javaProperty; + } + + public String getJava() { + return javaProperty.get(); + } + + public void setJava(String java) { + javaProperty.set(java); + } + + /** + * User customized java directory or null if user uses system Java. + */ + private final ImmediateStringProperty javaDirProperty = new ImmediateStringProperty(this, "javaDir", ""); + + public ImmediateStringProperty javaDirProperty() { + return javaDirProperty; + } + + public String getJavaDir() { + return javaDirProperty.get(); + } + + public void setJavaDir(String javaDir) { + javaDirProperty.set(javaDir); + } + + /** + * The command to launch java, i.e. optirun. + */ + private final ImmediateStringProperty wrapperProperty = new ImmediateStringProperty(this, "wrapper", ""); + + public ImmediateStringProperty wrapperProperty() { + return wrapperProperty; + } + + public String getWrapper() { + return wrapperProperty.get(); + } + + public void setWrapper(String wrapper) { + wrapperProperty.set(wrapper); + } + + /** + * The permanent generation size of JVM garbage collection. + */ + private final ImmediateStringProperty permSizeProperty = new ImmediateStringProperty(this, "permSize", ""); + + public ImmediateStringProperty permSizeProperty() { + return permSizeProperty; + } + + public String getPermSize() { + return permSizeProperty.get(); + } + + public void setPermSize(String permSize) { + permSizeProperty.set(permSize); + } + + /** + * The maximum memory that JVM can allocate for heap. + */ + private final ImmediateIntegerProperty maxMemoryProperty = new ImmediateIntegerProperty(this, "maxMemory", (int) OperatingSystem.SUGGESTED_MEMORY); + + public ImmediateIntegerProperty maxMemoryProperty() { + return maxMemoryProperty; + } + + public int getMaxMemory() { + return maxMemoryProperty.get(); + } + + public void setMaxMemory(int maxMemory) { + maxMemoryProperty.set(maxMemory); + } + + /** + * The minimum memory that JVM can allocate for heap. + */ + private final ImmediateObjectProperty minMemoryProperty = new ImmediateObjectProperty<>(this, "minMemory", null); + + public ImmediateObjectProperty minMemoryProperty() { + return minMemoryProperty; + } + + public Integer getMinMemory() { + return minMemoryProperty.get(); + } + + public void setMinMemory(Integer minMemory) { + minMemoryProperty.set(minMemory); + } + + /** + * The command that will be executed before launching the Minecraft. + * Operating system relevant. + */ + private final ImmediateStringProperty preLaunchCommandProperty = new ImmediateStringProperty(this, "precalledCommand", ""); + + public ImmediateStringProperty preLaunchCommandProperty() { + return preLaunchCommandProperty; + } + + public String getPreLaunchCommand() { + return preLaunchCommandProperty.get(); + } + + public void setPreLaunchCommand(String preLaunchCommand) { + preLaunchCommandProperty.set(preLaunchCommand); + } + + // options + + /** + * The user customized arguments passed to JVM. + */ + private final ImmediateStringProperty javaArgsProperty = new ImmediateStringProperty(this, "javaArgs", ""); + + public ImmediateStringProperty javaArgsProperty() { + return javaArgsProperty; + } + + public String getJavaArgs() { + return javaArgsProperty.get(); + } + + public void setJavaArgs(String javaArgs) { + javaArgsProperty.set(javaArgs); + } + + + /** + * The user customized arguments passed to Minecraft. + */ + private final ImmediateStringProperty minecraftArgsProperty = new ImmediateStringProperty(this, "minecraftArgs", ""); + + public ImmediateStringProperty minecraftArgsProperty() { + return minecraftArgsProperty; + } + + public String getMinecraftArgs() { + return minecraftArgsProperty.get(); + } + + public void setMinecraftArgs(String minecraftArgs) { + minecraftArgsProperty.set(minecraftArgs); + } + + /** + * True if disallow HMCL use default JVM arguments. + */ + private final ImmediateBooleanProperty noJVMArgsProperty = new ImmediateBooleanProperty(this, "noJVMArgs", false); + + public ImmediateBooleanProperty noJVMArgsProperty() { + return noJVMArgsProperty; + } + + public boolean isNoJVMArgs() { + return noJVMArgsProperty.get(); + } + + public void setNoJVMArgs(boolean noJVMArgs) { + noJVMArgsProperty.set(noJVMArgs); + } + + /** + * True if HMCL does not check game's completeness. + */ + private final ImmediateBooleanProperty notCheckGameProperty = new ImmediateBooleanProperty(this, "notCheckGame", false); + + public ImmediateBooleanProperty notCheckGameProperty() { + return notCheckGameProperty; + } + + public boolean isNotCheckGame() { + return notCheckGameProperty.get(); + } + + public void setNotCheckGame(boolean notCheckGame) { + notCheckGameProperty.set(notCheckGame); + } + + + /** + * True if HMCL does not find/download libraries in/to common path. + */ + private final ImmediateBooleanProperty noCommonProperty = new ImmediateBooleanProperty(this, "noCommon", false); + + public ImmediateBooleanProperty noCommonProperty() { + return noCommonProperty; + } + + public boolean isNoCommon() { + return noCommonProperty.get(); + } + + public void setNoCommon(boolean noCommon) { + noCommonProperty.set(noCommon); + } + + /** + * True if show the logs after game launched. + */ + private final ImmediateBooleanProperty showLogsProperty = new ImmediateBooleanProperty(this, "showLogs", false); + + public ImmediateBooleanProperty showLogsProperty() { + return showLogsProperty; + } + + public boolean isShowLogs() { + return showLogsProperty.get(); + } + + public void setShowLogs(boolean showLogs) { + showLogsProperty.set(showLogs); + } + + // Minecraft settings. + + /** + * The server ip that will be entered after Minecraft successfully loaded immediately. + * + * Format: ip:port or without port. + */ + private final ImmediateStringProperty serverIpProperty = new ImmediateStringProperty(this, "serverIp", ""); + + public ImmediateStringProperty serverIpProperty() { + return serverIpProperty; + } + + public String getServerIp() { + return serverIpProperty.get(); + } + + public void setServerIp(String serverIp) { + serverIpProperty.set(serverIp); + } + + + /** + * True if Minecraft started in fullscreen mode. + */ + private final ImmediateBooleanProperty fullscreenProperty = new ImmediateBooleanProperty(this, "fullscreen", false); + + public ImmediateBooleanProperty fullscreenProperty() { + return fullscreenProperty; + } + + public boolean isFullscreen() { + return fullscreenProperty.get(); + } + + public void setFullscreen(boolean fullscreen) { + fullscreenProperty.set(fullscreen); + } + + /** + * The width of Minecraft window, defaults 800. + * + * The field saves int value. + * String type prevents unexpected value from causing JsonSyntaxException. + * We can only reset this field instead of recreating the whole setting file. + */ + private final ImmediateIntegerProperty widthProperty = new ImmediateIntegerProperty(this, "width", 854); + + public ImmediateIntegerProperty widthProperty() { + return widthProperty; + } + + public int getWidth() { + return widthProperty.get(); + } + + public void setWidth(int width) { + widthProperty.set(width); + } + + + /** + * The height of Minecraft window, defaults 480. + * + * The field saves int value. + * String type prevents unexpected value from causing JsonSyntaxException. + * We can only reset this field instead of recreating the whole setting file. + */ + private final ImmediateIntegerProperty heightProperty = new ImmediateIntegerProperty(this, "height", 480); + + public ImmediateIntegerProperty heightProperty() { + return heightProperty; + } + + public int getHeight() { + return heightProperty.get(); + } + + public void setHeight(int height) { + heightProperty.set(height); + } + + /** + * 0 - .minecraft
+ * 1 - .minecraft/versions/<version>/
+ */ + private final ImmediateObjectProperty gameDirTypeProperty = new ImmediateObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER); + + public ImmediateObjectProperty gameDirTypeProperty() { + return gameDirTypeProperty; + } + + public EnumGameDirectory getGameDirType() { + return gameDirTypeProperty.get(); + } + + public void setGameDirType(EnumGameDirectory gameDirType) { + gameDirTypeProperty.set(gameDirType); + } + + /** + * Your custom gameDir + */ + private final ImmediateStringProperty gameDirProperty = new ImmediateStringProperty(this, "gameDir", ""); + + public ImmediateStringProperty gameDirProperty() { + return gameDirProperty; + } + + public String getGameDir() { + return gameDirProperty.get(); + } + + public void setGameDir(String gameDir) { + gameDirProperty.set(gameDir); + } + + // launcher settings + + /** + * 0 - Close the launcher when the game starts.
+ * 1 - Hide the launcher when the game starts.
+ * 2 - Keep the launcher open.
+ */ + private final ImmediateObjectProperty launcherVisibilityProperty = new ImmediateObjectProperty<>(this, "launcherVisibility", LauncherVisibility.HIDE); + + public ImmediateObjectProperty launcherVisibilityProperty() { + return launcherVisibilityProperty; + } + + public LauncherVisibility getLauncherVisibility() { + return launcherVisibilityProperty.get(); + } + + public void setLauncherVisibility(LauncherVisibility launcherVisibility) { + launcherVisibilityProperty.set(launcherVisibility); + } + + public JavaVersion getJavaVersion() throws InterruptedException { + // TODO: lazy initialization may result in UI suspension. + if (StringUtils.isBlank(getJava())) + setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom"); + if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment(); + else if ("Custom".equals(getJava())) { + try { + return JavaVersion.fromExecutable(new File(getJavaDir())); + } catch (IOException e) { + return null; // Custom Java Directory not found, + } + } else if (StringUtils.isNotBlank(getJava())) { + JavaVersion c = JavaVersion.getJREs().get(getJava()); + if (c == null) { + setJava("Default"); + return JavaVersion.fromCurrentEnvironment(); + } else + return c; + } else throw new Error(); + } + + public void addPropertyChangedListener(InvalidationListener listener) { + usesGlobalProperty.addListener(listener); + javaProperty.addListener(listener); + javaDirProperty.addListener(listener); + wrapperProperty.addListener(listener); + permSizeProperty.addListener(listener); + maxMemoryProperty.addListener(listener); + minMemoryProperty.addListener(listener); + preLaunchCommandProperty.addListener(listener); + javaArgsProperty.addListener(listener); + minecraftArgsProperty.addListener(listener); + noJVMArgsProperty.addListener(listener); + notCheckGameProperty.addListener(listener); + noCommonProperty.addListener(listener); + showLogsProperty.addListener(listener); + serverIpProperty.addListener(listener); + fullscreenProperty.addListener(listener); + widthProperty.addListener(listener); + heightProperty.addListener(listener); + gameDirTypeProperty.addListener(listener); + gameDirProperty.addListener(listener); + launcherVisibilityProperty.addListener(listener); + } + + public LaunchOptions toLaunchOptions(File gameDir) throws InterruptedException { + JavaVersion javaVersion = Optional.ofNullable(getJavaVersion()).orElse(JavaVersion.fromCurrentEnvironment()); + return new LaunchOptions.Builder() + .setGameDir(gameDir) + .setJava(javaVersion) + .setVersionName(Main.TITLE) + .setProfileName(Main.TITLE) + .setMinecraftArgs(getMinecraftArgs()) + .setJavaArgs(getJavaArgs()) + .setMaxMemory(getMaxMemory()) + .setMinMemory(getMinMemory()) + .setMetaspace(StringUtils.parseInt(getPermSize())) + .setWidth(getWidth()) + .setHeight(getHeight()) + .setFullscreen(isFullscreen()) + .setServerIp(getServerIp()) + .setWrapper(getWrapper()) + .setProxyHost(Settings.INSTANCE.getProxyHost()) + .setProxyPort(Settings.INSTANCE.getProxyPort()) + .setProxyUser(Settings.INSTANCE.getProxyUser()) + .setProxyPass(Settings.INSTANCE.getProxyPass()) + .setPrecalledCommand(getPreLaunchCommand()) + .setNoGeneratedJVMArgs(isNoJVMArgs()) + .create(); + } + + public static class Serializer implements JsonSerializer, JsonDeserializer { + public static final Serializer INSTANCE = new Serializer(); + + private Serializer() { + } + + @Override + public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializationContext context) { + if (src == null) return JsonNull.INSTANCE; + JsonObject obj = new JsonObject(); + + obj.addProperty("usesGlobal", src.isUsesGlobal()); + obj.addProperty("javaArgs", src.getJavaArgs()); + obj.addProperty("minecraftArgs", src.getMinecraftArgs()); + obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? OperatingSystem.SUGGESTED_MEMORY : src.getMaxMemory()); + obj.addProperty("minMemory", src.getMinMemory()); + obj.addProperty("permSize", src.getPermSize()); + obj.addProperty("width", src.getWidth()); + obj.addProperty("height", src.getHeight()); + obj.addProperty("javaDir", src.getJavaDir()); + obj.addProperty("precalledCommand", src.getPreLaunchCommand()); + obj.addProperty("serverIp", src.getServerIp()); + obj.addProperty("java", src.getJava()); + obj.addProperty("wrapper", src.getWrapper()); + obj.addProperty("fullscreen", src.isFullscreen()); + obj.addProperty("noJVMArgs", src.isNoJVMArgs()); + obj.addProperty("notCheckGame", src.isNotCheckGame()); + obj.addProperty("noCommon", src.isNoCommon()); + obj.addProperty("showLogs", src.isShowLogs()); + obj.addProperty("gameDir", src.getGameDir()); + obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal()); + obj.addProperty("gameDirType", src.getGameDirType().ordinal()); + + return obj; + } + + @Override + public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json == null || json == JsonNull.INSTANCE || !(json instanceof JsonObject)) + return null; + JsonObject obj = (JsonObject) json; + + int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), OperatingSystem.SUGGESTED_MEMORY); + if (maxMemoryN <= 0) maxMemoryN = OperatingSystem.SUGGESTED_MEMORY; + + VersionSetting vs = new VersionSetting(); + + vs.setUsesGlobal(Optional.ofNullable(obj.get("usesGlobal")).map(JsonElement::getAsBoolean).orElse(false)); + vs.setJavaArgs(Optional.ofNullable(obj.get("javaArgs")).map(JsonElement::getAsString).orElse("")); + vs.setMinecraftArgs(Optional.ofNullable(obj.get("minecraftArgs")).map(JsonElement::getAsString).orElse("")); + vs.setMaxMemory(maxMemoryN); + vs.setMinMemory(Optional.ofNullable(obj.get("minMemory")).map(JsonElement::getAsInt).orElse(null)); + vs.setPermSize(Optional.ofNullable(obj.get("permSize")).map(JsonElement::getAsString).orElse("")); + vs.setWidth(Optional.ofNullable(obj.get("width")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0)); + vs.setHeight(Optional.ofNullable(obj.get("height")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0)); + vs.setJavaDir(Optional.ofNullable(obj.get("javaDir")).map(JsonElement::getAsString).orElse("")); + vs.setPreLaunchCommand(Optional.ofNullable(obj.get("precalledCommand")).map(JsonElement::getAsString).orElse("")); + vs.setServerIp(Optional.ofNullable(obj.get("serverIp")).map(JsonElement::getAsString).orElse("")); + vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse("")); + vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse("")); + vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("")); + vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false)); + vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false)); + vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false)); + vs.setNoCommon(Optional.ofNullable(obj.get("noCommon")).map(JsonElement::getAsBoolean).orElse(false)); + vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false)); + vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]); + vs.setGameDirType(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]); + + return vs; + } + + private int parseJsonPrimitive(JsonPrimitive primitive) { + return parseJsonPrimitive(primitive, 0); + } + + private int parseJsonPrimitive(JsonPrimitive primitive, int defaultValue) { + if (primitive.isNumber()) + return primitive.getAsInt(); + else + return Lang.parseInt(primitive.getAsString(), defaultValue); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java new file mode 100644 index 000000000..77b384299 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -0,0 +1,235 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2013 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import java.util.concurrent.CountDownLatch; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.ReadOnlyIntegerWrapper; +import javafx.concurrent.Worker; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ToggleButton; +import javafx.scene.image.Image; +import javafx.scene.layout.StackPane; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import javafx.stage.Stage; +import org.jackhuang.hmcl.MainKt; +import org.jackhuang.hmcl.event.Event; +import org.jackhuang.hmcl.event.EventManager; +import org.jackhuang.hmcl.game.LauncherHelper; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.util.IOUtils; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Log4jLevel; +import org.jackhuang.hmcl.util.StringUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * + * @author huangyuhui + */ +public final class LogWindow extends Stage { + + private final ReadOnlyIntegerWrapper fatalProperty = new ReadOnlyIntegerWrapper(0); + private final ReadOnlyIntegerWrapper errorProperty = new ReadOnlyIntegerWrapper(0); + private final ReadOnlyIntegerWrapper warnProperty = new ReadOnlyIntegerWrapper(0); + private final ReadOnlyIntegerWrapper infoProperty = new ReadOnlyIntegerWrapper(0); + private final ReadOnlyIntegerWrapper debugProperty = new ReadOnlyIntegerWrapper(0); + private final LogWindowImpl impl = new LogWindowImpl(); + private final CountDownLatch latch = new CountDownLatch(1); + public final EventManager onDone = new EventManager<>(); + + public LogWindow() { + setScene(new Scene(impl, 800, 480)); + getScene().getStylesheets().addAll(FXUtilsKt.getStylesheets()); + setTitle(MainKt.i18n("logwindow.title")); + getIcons().add(new Image("/assets/img/icon.png")); + } + + public LogWindow(String text) { + this(); + + onDone.register(() -> { + logLine(text, Log4jLevel.INFO); + }); + } + + public ReadOnlyIntegerProperty fatalProperty() { + return fatalProperty.getReadOnlyProperty(); + } + + public int getFatal() { + return fatalProperty.get(); + } + + public ReadOnlyIntegerProperty errorProperty() { + return errorProperty.getReadOnlyProperty(); + } + + public int getError() { + return errorProperty.get(); + } + + public ReadOnlyIntegerProperty warnProperty() { + return warnProperty.getReadOnlyProperty(); + } + + public int getWarn() { + return warnProperty.get(); + } + + public ReadOnlyIntegerProperty infoProperty() { + return infoProperty.getReadOnlyProperty(); + } + + public int getInfo() { + return infoProperty.get(); + } + + public ReadOnlyIntegerProperty debugProperty() { + return debugProperty.getReadOnlyProperty(); + } + + public int getDebug() { + return debugProperty.get(); + } + + public void logLine(String line, Log4jLevel level) { + Element div = impl.engine.getDocument().createElement("div"); + // a
 element to prevent multiple spaces and tabs being removed.
+        Element pre = impl.engine.getDocument().createElement("pre");
+        pre.setTextContent(line);
+        div.appendChild(pre);
+        impl.body.appendChild(div);
+        impl.engine.executeScript("checkNewLog(\"" + level.name().toLowerCase() + "\");scrollToBottom();");
+
+        switch (level) {
+            case FATAL:
+                fatalProperty.set(fatalProperty.get() + 1);
+                break;
+            case ERROR:
+                errorProperty.set(errorProperty.get() + 1);
+                break;
+            case WARN:
+                warnProperty.set(warnProperty.get() + 1);
+                break;
+            case INFO:
+                infoProperty.set(infoProperty.get() + 1);
+                break;
+            case DEBUG:
+                debugProperty.set(debugProperty.get() + 1);
+                break;
+        }
+    }
+
+    public class LogWindowImpl extends StackPane {
+
+        @FXML
+        public WebView webView;
+        @FXML
+        public ToggleButton btnFatals;
+        @FXML
+        public ToggleButton btnErrors;
+        @FXML
+        public ToggleButton btnWarns;
+        @FXML
+        public ToggleButton btnInfos;
+        @FXML
+        public ToggleButton btnDebugs;
+        @FXML
+        public ComboBox cboLines;
+
+        WebEngine engine;
+        Node body;
+        Document document;
+
+        public LogWindowImpl() {
+            FXUtilsKt.loadFXML(this, "/assets/fxml/log.fxml");
+
+            engine = webView.getEngine();
+            engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
+                    .replace("${FONT}", Settings.INSTANCE.getFont().getSize() + "px \"" + Settings.INSTANCE.getFont().getFamily() + "\""));
+            engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
+                if (newValue == Worker.State.SUCCEEDED) {
+                    document = engine.getDocument();
+                    body = document.getElementsByTagName("body").item(0);
+                    engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
+                    latch.countDown();
+                    onDone.fireEvent(new Event(LogWindow.this));
+                }
+            });
+
+            boolean flag = false;
+            for (String i : cboLines.getItems())
+                if (Integer.toString(Settings.INSTANCE.getLogLines()).equals(i)) {
+                    cboLines.getSelectionModel().select(i);
+                    flag = true;
+                }
+
+            cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
+                Settings.INSTANCE.setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
+                engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
+            });
+
+            if (!flag)
+                cboLines.getSelectionModel().select(0);
+
+            btnFatals.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(fatalProperty.get()) + " fatals", fatalProperty));
+            btnErrors.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(errorProperty.get()) + " errors", errorProperty));
+            btnWarns.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(warnProperty.get()) + " warns", warnProperty));
+            btnInfos.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(infoProperty.get()) + " infos", infoProperty));
+            btnDebugs.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(debugProperty.get()) + " debugs", debugProperty));
+
+            btnFatals.selectedProperty().addListener(o -> specificChanged());
+            btnErrors.selectedProperty().addListener(o -> specificChanged());
+            btnWarns.selectedProperty().addListener(o -> specificChanged());
+            btnInfos.selectedProperty().addListener(o -> specificChanged());
+            btnDebugs.selectedProperty().addListener(o -> specificChanged());
+        }
+
+        private void specificChanged() {
+            String res = "";
+            if (btnFatals.isSelected())
+                res += "\"fatal\", ";
+            if (btnErrors.isSelected())
+                res += "\"error\", ";
+            if (btnWarns.isSelected())
+                res += "\"warn\", ";
+            if (btnInfos.isSelected())
+                res += "\"info\", ";
+            if (btnDebugs.isSelected())
+                res += "\"debug\", ";
+            if (StringUtils.isNotBlank(res))
+                res = StringUtils.substringBeforeLast(res, ", ");
+            engine.executeScript("specific([" + res + "])");
+        }
+
+        public void onTerminateGame() {
+            LauncherHelper.stopManagedProcesses();
+        }
+
+        public void onClear() {
+            engine.executeScript("clear()");
+        }
+    }
+}
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/Events.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/Events.kt
index 9aa72f8f9..606a092a2 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/Events.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/Events.kt
@@ -17,6 +17,7 @@
  */
 package org.jackhuang.hmcl
 
+import org.jackhuang.hmcl.event.Event
 import org.jackhuang.hmcl.setting.Profile
 import java.util.*
 
@@ -30,7 +31,7 @@ import java.util.*
  * *
  * @author huangyuhui
  */
-class ProfileChangedEvent(source: Any, val value: Profile) : EventObject(source)
+class ProfileChangedEvent(source: Any, val value: Profile) : Event(source)
 
 /**
  * This event gets fired when loading profiles.
@@ -40,4 +41,4 @@ class ProfileChangedEvent(source: Any, val value: Profile) : EventObject(source)
  * *
  * @author huangyuhui
  */
-class ProfileLoadingEvent(source: Any) : EventObject(source)
+class ProfileLoadingEvent(source: Any) : Event(source)
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/Main.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/Main.kt
index cc668ec9d..04dc952bd 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/Main.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/Main.kt
@@ -57,10 +57,10 @@ class Main : Application() {
 
     companion object {
 
-        val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
-        val NAME = "HMCL"
-        val TITLE = "$NAME $VERSION"
-        val APPDATA = getWorkingDirectory("hmcl")
+        @JvmField val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
+        @JvmField val NAME = "HMCL"
+        @JvmField val TITLE = "$NAME $VERSION"
+        @JvmField val APPDATA = getWorkingDirectory("hmcl")
 
         @JvmStatic
         fun main(args: Array) {
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/AccountHelper.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/AccountHelper.kt
deleted file mode 100644
index f8e58ff8b..000000000
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/AccountHelper.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Hello Minecraft! Launcher.
- * Copyright (C) 2017  huangyuhui 
- * 
- * 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 {http://www.gnu.org/licenses/}.
- */
-package org.jackhuang.hmcl.game
-
-import javafx.geometry.Rectangle2D
-import javafx.scene.image.Image
-import org.jackhuang.hmcl.Main
-import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
-import org.jackhuang.hmcl.setting.Settings
-import org.jackhuang.hmcl.task.FileDownloadTask
-import org.jackhuang.hmcl.task.Scheduler
-import org.jackhuang.hmcl.task.Schedulers
-import org.jackhuang.hmcl.task.Task
-import org.jackhuang.hmcl.ui.DEFAULT_ICON
-import org.jackhuang.hmcl.ui.DialogController
-import org.jackhuang.hmcl.util.toURL
-import java.net.Proxy
-
-object AccountHelper {
-    val SKIN_DIR = Main.APPDATA.resolve("skins")
-
-    fun loadSkins(proxy: Proxy = Settings.proxy) {
-        for (account in Settings.getAccounts().values) {
-            if (account is YggdrasilAccount) {
-                SkinLoadTask(account, proxy, false).start()
-            }
-        }
-    }
-
-    fun loadSkinAsync(account: YggdrasilAccount, proxy: Proxy = Settings.proxy): Task =
-            SkinLoadTask(account, proxy, false)
-
-    fun refreshSkinAsync(account: YggdrasilAccount, proxy: Proxy = Settings.proxy): Task =
-            SkinLoadTask(account, proxy, true)
-
-    private class SkinLoadTask(val account: YggdrasilAccount, val proxy: Proxy, val refresh: Boolean = false): Task() {
-
-        override fun getScheduler() = Schedulers.io()
-        private val dependencies = mutableListOf()
-        override fun getDependencies() = dependencies
-
-        override fun execute() {
-            if (account.canLogIn() && (account.selectedProfile == null || refresh))
-                DialogController.logIn(account)
-            val profile = account.selectedProfile ?: return
-            val name = profile.name ?: return
-            val url = "http://skins.minecraft.net/MinecraftSkins/$name.png"
-            val file = getSkinFile(name)
-            if (!refresh && file.exists())
-                return
-            dependencies += FileDownloadTask(url.toURL(), file, proxy)
-        }
-    }
-
-    private fun getSkinFile(name: String) = SKIN_DIR.resolve("$name.png")
-
-    fun getSkin(account: YggdrasilAccount, scaleRatio: Double = 1.0): Image {
-        if (account.selectedProfile == null) return DEFAULT_ICON
-        val name = account.selectedProfile?.name ?: return DEFAULT_ICON
-        val file = getSkinFile(name)
-        if (file.exists()) {
-            val original = Image("file:" + file.absolutePath)
-            return Image("file:" + file.absolutePath, original.width * scaleRatio, original.height * scaleRatio, false, false)
-        }
-        else return DEFAULT_ICON
-    }
-
-    fun getViewport(scaleRatio: Double): Rectangle2D {
-        val size = 8.0 * scaleRatio
-        return Rectangle2D(size, size, size, size)
-    }
-}
\ No newline at end of file
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLGameRepository.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLGameRepository.kt
deleted file mode 100644
index ec0455c1a..000000000
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLGameRepository.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Hello Minecraft! Launcher.
- * Copyright (C) 2017  huangyuhui 
- * 
- * 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 {http://www.gnu.org/licenses/}.
- */
-package org.jackhuang.hmcl.game
-
-import com.google.gson.GsonBuilder
-import javafx.beans.InvalidationListener
-import org.jackhuang.hmcl.setting.EnumGameDirectory
-import org.jackhuang.hmcl.setting.Profile
-import org.jackhuang.hmcl.setting.Settings
-import org.jackhuang.hmcl.setting.VersionSetting
-import org.jackhuang.hmcl.task.Schedulers
-import org.jackhuang.hmcl.util.Logging.LOG
-import org.jackhuang.hmcl.util.fromJson
-import java.io.File
-import java.io.IOException
-import java.util.logging.Level
-
-class HMCLGameRepository(val profile: Profile, baseDirectory: File)
-    : DefaultGameRepository(baseDirectory) {
-
-    private val versionSettings = mutableMapOf()
-    private val beingModpackVersions = mutableSetOf()
-
-    private fun useSelf(version: String, assetId: String): Boolean {
-        val vs = profile.getVersionSetting(version)
-        return File(baseDirectory, "assets/indexes/$assetId.json").exists() || vs.noCommon
-    }
-
-    override fun getAssetDirectory(version: String, assetId: String): File {
-        if (useSelf(version, assetId))
-            return super.getAssetDirectory(version, assetId)
-        else
-            return File(Settings.commonPath).resolve("assets")
-    }
-
-    override fun getRunDirectory(id: String): File {
-        if (beingModpackVersions.contains(id))
-            return getVersionRoot(id)
-        else {
-            val vs = profile.getVersionSetting(id)
-            return when (vs.gameDirType) {
-                EnumGameDirectory.VERSION_FOLDER -> getVersionRoot(id)
-                EnumGameDirectory.ROOT_FOLDER -> super.getRunDirectory(id)
-                EnumGameDirectory.CUSTOM -> File(vs.gameDir)
-            }
-        }
-    }
-
-    override fun getLibraryFile(version: Version, lib: Library): File {
-        val vs = profile.getVersionSetting(version.id)
-        val self = super.getLibraryFile(version, lib);
-        if (self.exists() || vs.noCommon)
-            return self;
-        else
-            return File(Settings.commonPath).resolve("libraries/${lib.path}")
-    }
-
-    @Synchronized
-    override fun refreshVersionsImpl() {
-        Schedulers.newThread().schedule {
-            versionSettings.clear()
-
-            super.refreshVersionsImpl()
-
-            versions.keys.forEach(this::loadVersionSetting)
-
-            checkModpack()
-
-            try {
-                val file = baseDirectory.resolve("launcher_profiles.json")
-                if (!file.exists() && versions.isNotEmpty())
-                    file.writeText(PROFILE)
-            } catch (ex: IOException) {
-                LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex)
-            }
-        }
-    }
-
-    fun changeDirectory(newDir: File) {
-        baseDirectory = newDir
-        refreshVersions()
-    }
-
-    private fun checkModpack() {}
-
-    private fun getVersionSettingFile(id: String) = getVersionRoot(id).resolve("hmclversion.cfg")
-
-    private fun loadVersionSetting(id: String) {
-        val file = getVersionSettingFile(id)
-        if (file.exists()) {
-            try {
-                val versionSetting = GSON.fromJson(file.readText())!!
-                initVersionSetting(id, versionSetting)
-            } catch (ignore: Exception) {
-                // If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
-            }
-        }
-    }
-
-    private fun saveVersionSetting(id: String) {
-        if (!versionSettings.containsKey(id))
-            return
-
-        getVersionSettingFile(id).writeText(GSON.toJson(versionSettings[id]))
-    }
-
-    private fun initVersionSetting(id: String, vs: VersionSetting): VersionSetting {
-        vs.addPropertyChangedListener(InvalidationListener { saveVersionSetting(id) })
-        versionSettings[id] = vs
-        return vs
-    }
-
-    internal fun createVersionSetting(id: String): VersionSetting? {
-        if (!hasVersion(id)) return null
-        return versionSettings[id] ?: initVersionSetting(id, VersionSetting())
-    }
-
-    fun getVersionSetting(id: String): VersionSetting? {
-        if (!versionSettings.containsKey(id))
-            loadVersionSetting(id)
-        return versionSettings[id]
-    }
-
-    fun getVersionIcon(id: String): File =
-            getVersionRoot(id).resolve("icon.png")
-
-    fun markVersionAsModpack(id: String) {
-        beingModpackVersions += id
-    }
-
-    fun undoMark(id: String) {
-        beingModpackVersions -= id
-    }
-
-    companion object {
-        val PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}"
-        val GSON = GsonBuilder().registerTypeAdapter(VersionSetting::class.java, VersionSetting).setPrettyPrinting().create()
-    }
-}
\ No newline at end of file
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLGameLauncher.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLMultiCharacterSelector.kt
similarity index 54%
rename from HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLGameLauncher.kt
rename to HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLMultiCharacterSelector.kt
index 285168fa4..c2d6e4909 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLGameLauncher.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLMultiCharacterSelector.kt
@@ -17,18 +17,13 @@
  */
 package org.jackhuang.hmcl.game
 
-import org.jackhuang.hmcl.Main
-import org.jackhuang.hmcl.auth.AuthInfo
-import org.jackhuang.hmcl.launch.DefaultLauncher
-import org.jackhuang.hmcl.launch.ProcessListener
+import org.jackhuang.hmcl.auth.Account
+import org.jackhuang.hmcl.auth.MultiCharacterSelector
+import org.jackhuang.hmcl.auth.NoSelectedCharacterException
+import org.jackhuang.hmcl.auth.yggdrasil.GameProfile
 
-class HMCLGameLauncher(repository: GameRepository, versionId: String, account: AuthInfo, options: LaunchOptions, listener: ProcessListener? = null, isDaemon: Boolean = true)
-    : DefaultLauncher(repository, versionId, account, options, listener, isDaemon) {
-
-    override fun appendJvmArgs(res: MutableList) {
-        super.appendJvmArgs(res)
-
-        res.add("-Dminecraft.launcher.version=" + Main.VERSION);
-        res.add("-Dminecraft.launcher.brand=" + Main.NAME);
+object HMCLMultiCharacterSelector : MultiCharacterSelector {
+    override fun select(account: Account, names: MutableList): GameProfile {
+        return names.firstOrNull() ?: throw NoSelectedCharacterException(account)
     }
 }
\ No newline at end of file
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/LauncherHelper.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/LauncherHelper.kt
deleted file mode 100644
index 5915a8e5c..000000000
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/LauncherHelper.kt
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * Hello Minecraft! Launcher.
- * Copyright (C) 2017  huangyuhui 
- * 
- * 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 {http://www.gnu.org/licenses/}.
- */
-package org.jackhuang.hmcl.game
-
-import javafx.application.Platform
-import org.jackhuang.hmcl.Main
-import org.jackhuang.hmcl.auth.AuthInfo
-import org.jackhuang.hmcl.auth.AuthenticationException
-import org.jackhuang.hmcl.launch.DefaultLauncher
-import org.jackhuang.hmcl.launch.ProcessListener
-import org.jackhuang.hmcl.mod.CurseCompletionTask
-import org.jackhuang.hmcl.setting.LauncherVisibility
-import org.jackhuang.hmcl.setting.Settings
-import org.jackhuang.hmcl.setting.VersionSetting
-import org.jackhuang.hmcl.task.*
-import org.jackhuang.hmcl.ui.*
-import org.jackhuang.hmcl.util.Log4jLevel
-import org.jackhuang.hmcl.util.ManagedProcess
-import org.jackhuang.hmcl.util.task
-import java.util.*
-import java.util.concurrent.ConcurrentLinkedQueue
-
-object LauncherHelper {
-    private val launchingStepsPane = LaunchingStepsPane()
-    val PROCESS = ConcurrentLinkedQueue()
-
-    fun launch() {
-        val profile = Settings.selectedProfile
-        val selectedVersion = profile.selectedVersion ?: throw IllegalStateException("No version here")
-        val repository = profile.repository
-        val dependency = profile.dependency
-        val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
-        val version = repository.getVersion(selectedVersion)
-        val setting = profile.getVersionSetting(selectedVersion)
-
-        Controllers.dialog(launchingStepsPane)
-        task(Schedulers.javafx()) { emitStatus(LoadingState.DEPENDENCIES) }
-                .then(dependency.checkGameCompletionAsync(version))
-
-                .then(task(Schedulers.javafx()) { emitStatus(LoadingState.MODS) })
-                .then(CurseCompletionTask(dependency, selectedVersion))
-
-                .then(task(Schedulers.javafx()) { emitStatus(LoadingState.LOGIN) })
-                .then(task {
-                    try {
-                        it["account"] = account.logIn(Settings.proxy)
-                    } catch (e: AuthenticationException) {
-                        it["account"] = DialogController.logIn(account)
-                        runOnUiThread { Controllers.dialog(launchingStepsPane) }
-                    }
-                })
-
-                .then(task(Schedulers.javafx()) { emitStatus(LoadingState.LAUNCHING) })
-                .then(task {
-                    it["launcher"] = HMCLGameLauncher(
-                            repository = repository,
-                            versionId = selectedVersion,
-                            options = setting.toLaunchOptions(profile.gameDir),
-                            listener = HMCLProcessListener(it["account"], setting),
-                            account = it["account"]
-                    )
-                })
-                .then { it.get("launcher").launchAsync() }
-                .then(task {
-                    PROCESS.add(it[DefaultLauncher.LAUNCH_ASYNC_ID])
-                    if (setting.launcherVisibility == LauncherVisibility.CLOSE)
-                        Main.stop()
-                })
-
-                .executor()
-                .apply {
-                    taskListener = object : TaskListener() {
-                        var finished = 0
-
-                        override fun onFinished(task: Task) {
-                            ++finished
-                            runOnUiThread { launchingStepsPane.pgsTasks.progress = 1.0 * finished / runningTasks }
-                        }
-
-                        override fun onTerminate() {
-                            runOnUiThread(Controllers::closeDialog)
-                        }
-                    }
-                }.start()
-    }
-
-    fun emitStatus(state: LoadingState) {
-        if (state == LoadingState.DONE) {
-            Controllers.closeDialog()
-        }
-
-        launchingStepsPane.lblCurrentState.text = state.toString()
-        launchingStepsPane.lblSteps.text = "${state.ordinal + 1} / ${LoadingState.values().size}"
-    }
-
-    /**
-     * The managed process listener.
-     * Guarantee that one [JavaProcess], one [HMCLProcessListener].
-     * Because every time we launched a game, we generates a new [HMCLProcessListener]
-     */
-    class HMCLProcessListener(authInfo: AuthInfo?, private val setting: VersionSetting) : ProcessListener {
-        val forbiddenTokens: List> = if (authInfo == null) emptyList() else
-            listOf(
-                    authInfo.authToken to "",
-                    authInfo.userId to "",
-                    authInfo.username to ""
-            )
-        private val launcherVisibility = setting.launcherVisibility
-        private lateinit var process: ManagedProcess
-        private var lwjgl = false
-        private var logWindow: LogWindow? = null
-        private val logs = LinkedList>()
-        override fun setProcess(process: ManagedProcess) {
-            this.process = process
-
-            if (setting.showLogs) {
-                runOnUiThread { logWindow = LogWindow(); logWindow?.show() }
-            }
-        }
-
-        override fun onLog(log: String, level: Log4jLevel) {
-            var newLog = log
-            for ((original, replacement) in forbiddenTokens)
-                newLog = newLog.replace(original, replacement)
-
-            if (level.lessOrEqual(Log4jLevel.ERROR))
-                System.err.print(log)
-            else
-                System.out.print(log)
-
-            runOnUiThread {
-                logs += log to level
-                if (logs.size > Settings.logLines)
-                    logs.removeFirst()
-                logWindow?.logLine(log, level)
-            }
-
-            if (!lwjgl && log.contains("LWJGL Version: ")) {
-                lwjgl = true
-                when (launcherVisibility) {
-                    LauncherVisibility.HIDE_AND_REOPEN -> {
-                        runOnUiThread {
-                            Controllers.stage.hide()
-                            emitStatus(LoadingState.DONE)
-                        }
-                    }
-                    LauncherVisibility.CLOSE -> {
-                        throw Error("Never come to here")
-                    }
-                    LauncherVisibility.KEEP -> {
-                        // No operations here.
-                    }
-                    LauncherVisibility.HIDE -> {
-                        runOnUiThread {
-                            Controllers.stage.close()
-                            emitStatus(LoadingState.DONE)
-                        }
-                    }
-                }
-            }
-        }
-
-        override fun onExit(exitCode: Int, exitType: ProcessListener.ExitType) {
-            if (exitType != ProcessListener.ExitType.NORMAL && logWindow == null){
-                runOnUiThread {
-                    LogWindow().apply {
-                        show()
-                        Schedulers.newThread().schedule {
-                            waitForShown()
-                            runOnUiThread {
-                                for ((line, level) in logs)
-                                    logLine(line, level)
-                            }
-                        }
-                    }
-                }
-            }
-
-            checkExit(launcherVisibility)
-        }
-
-    }
-
-    private fun checkExit(launcherVisibility: LauncherVisibility) {
-        when (launcherVisibility) {
-            LauncherVisibility.HIDE_AND_REOPEN -> runOnUiThread { Controllers.stage.show() }
-            LauncherVisibility.KEEP -> { /* no operations here. */ }
-            LauncherVisibility.CLOSE -> { throw Error("Never get to here") }
-            LauncherVisibility.HIDE -> runOnUiThread {
-                // Shut down the platform when user closed log window.
-                Platform.setImplicitExit(true)
-                // If we use Main.stop(), log window will be halt immediately.
-                Main.stopWithoutJavaFXPlatform()
-            }
-        }
-    }
-
-    fun stopManagedProcesses() {
-        synchronized(PROCESS) {
-            while (PROCESS.isNotEmpty())
-                PROCESS.poll()?.stop()
-        }
-    }
-
-    enum class LoadingState {
-        DEPENDENCIES,
-        MODS,
-        LOGIN,
-        LAUNCHING,
-        DONE
-    }
-}
\ No newline at end of file
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/ModpackHelper.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/ModpackHelper.kt
index c5a7c4cbe..e6f2b5dcb 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/ModpackHelper.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/ModpackHelper.kt
@@ -51,7 +51,7 @@ fun readModpackManifest(f: File): Modpack {
 }
 
 fun MultiMCInstanceConfiguration.toVersionSetting(vs: VersionSetting) {
-    vs.usesGlobal = false
+    vs.isUsesGlobal = false
     vs.gameDirType = EnumGameDirectory.VERSION_FOLDER
 
     if (isOverrideJavaLocation) {
@@ -67,7 +67,7 @@ fun MultiMCInstanceConfiguration.toVersionSetting(vs: VersionSetting) {
 
     if (isOverrideCommands) {
         vs.wrapper = wrapperCommand.orEmpty()
-        vs.precalledCommand = preLaunchCommand.orEmpty()
+        vs.preLaunchCommand = preLaunchCommand.orEmpty()
     }
 
     if (isOverrideJavaArgs) {
@@ -75,11 +75,11 @@ fun MultiMCInstanceConfiguration.toVersionSetting(vs: VersionSetting) {
     }
 
     if (isOverrideConsole) {
-        vs.showLogs = isShowConsole
+        vs.isShowLogs = isShowConsole
     }
 
     if (isOverrideWindow) {
-        vs.fullscreen = isFullscreen
+        vs.isFullscreen = isFullscreen
         if (width != null)
             vs.width = width!!
         if (height != null)
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/Profile.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/Profile.kt
deleted file mode 100644
index e7cb40e30..000000000
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/Profile.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Hello Minecraft! Launcher.
- * Copyright (C) 2017  huangyuhui 
- * 
- * 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 {http://www.gnu.org/licenses/}.
- */
-package org.jackhuang.hmcl.setting
-
-import com.google.gson.*
-import javafx.beans.InvalidationListener
-import org.jackhuang.hmcl.download.DefaultDependencyManager
-import org.jackhuang.hmcl.event.EventBus
-import org.jackhuang.hmcl.event.RefreshedVersionsEvent
-import org.jackhuang.hmcl.game.HMCLGameRepository
-import org.jackhuang.hmcl.mod.ModManager
-import org.jackhuang.hmcl.ui.runOnUiThread
-import org.jackhuang.hmcl.util.*
-import java.io.File
-import java.lang.reflect.Type
-
-class Profile(name: String = "Default", initialGameDir: File = File(".minecraft"), initialSelectedVersion: String = "") {
-    val nameProperty = ImmediateStringProperty(this, "name", name)
-    var name: String by nameProperty
-
-    val globalProperty = ImmediateObjectProperty(this, "global", VersionSetting())
-    var global: VersionSetting by globalProperty
-
-    val selectedVersionProperty = ImmediateObjectProperty(this, "selectedVersion", initialSelectedVersion)
-    var selectedVersion: String? by selectedVersionProperty
-
-    val gameDirProperty = ImmediateObjectProperty(this, "gameDir", initialGameDir)
-    var gameDir: File by gameDirProperty
-
-    var repository = HMCLGameRepository(this, initialGameDir)
-    val dependency: DefaultDependencyManager get() = DefaultDependencyManager(repository, Settings.downloadProvider, Settings.proxy)
-    var modManager = ModManager(repository)
-
-    init {
-        gameDirProperty.onChange { repository.changeDirectory(it!!) }
-        selectedVersionProperty.onInvalidated(this::verifySelectedVersion)
-        EventBus.EVENT_BUS.channel().register { event -> if (event.source == repository) verifySelectedVersion() }
-    }
-
-    private fun verifySelectedVersion() = runOnUiThread {
-        // To prevent not loaded profile's selectedVersion being changed.
-        if (repository.isLoaded && ((selectedVersion == null && repository.getVersions().isNotEmpty()) || (selectedVersion != null && !repository.hasVersion(selectedVersion!!)))) {
-            val newVersion = repository.getVersions().firstOrNull()
-            // will cause anthor change event, we must ensure that there will not be dead recursion.
-            selectedVersion = newVersion?.id
-        }
-    }
-
-    /**
-     * @return null if the given version id does not exist.
-     */
-    fun specializeVersionSetting(id: String): VersionSetting? {
-        var vs = repository.getVersionSetting(id)
-        if (vs == null)
-            vs = repository.createVersionSetting(id) ?: return null
-        vs.usesGlobal = false
-        return vs
-    }
-
-    fun globalizeVersionSetting(id: String) {
-        repository.getVersionSetting(id)?.usesGlobal = true
-    }
-
-    fun isVersionGlobal(id: String): Boolean {
-        return repository.getVersionSetting(id)?.usesGlobal ?: true
-    }
-
-    fun getVersionSetting(id: String): VersionSetting {
-        val vs = repository.getVersionSetting(id)
-        if (vs == null || vs.usesGlobal) {
-            global.isGlobal = true // always keep global.isGlobal = true
-            return global
-        } else
-            return vs
-    }
-
-    fun getSelectedVersionSetting(): VersionSetting? = if (selectedVersion == null) null else getVersionSetting(selectedVersion!!)
-
-    fun addPropertyChangedListener(listener: InvalidationListener) {
-        nameProperty.addListener(listener)
-        globalProperty.addListener(listener)
-        selectedVersionProperty.addListener(listener)
-        gameDirProperty.addListener(listener)
-    }
-
-    companion object Serializer: JsonSerializer, JsonDeserializer {
-        override fun serialize(src: Profile?, typeOfSrc: Type?, context: JsonSerializationContext): JsonElement {
-            if (src == null) return JsonNull.INSTANCE
-            val jsonObject = JsonObject()
-            with(jsonObject) {
-                add("global", context.serialize(src.global))
-                addProperty("selectedVersion", src.selectedVersion)
-                addProperty("gameDir", src.gameDir.path)
-            }
-
-            return jsonObject
-        }
-
-        override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext): Profile? {
-            if (json == null || json == JsonNull.INSTANCE || json !is JsonObject) return null
-
-            return Profile(initialGameDir = File(json["gameDir"]?.asString ?: ""), initialSelectedVersion = json["selectedVersion"]?.asString ?: "").apply {
-                global = context.deserialize(json["global"], VersionSetting::class.java)
-            }
-        }
-
-    }
-}
\ No newline at end of file
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/Settings.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/Settings.kt
index 470f9c096..ebd2c73ca 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/Settings.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/Settings.kt
@@ -42,8 +42,8 @@ import java.util.logging.Level
 
 object Settings {
     val GSON = GsonBuilder()
-            .registerTypeAdapter(VersionSetting::class.java, VersionSetting)
-            .registerTypeAdapter(Profile::class.java, Profile)
+            .registerTypeAdapter(VersionSetting::class.java, VersionSetting.Serializer.INSTANCE)
+            .registerTypeAdapter(Profile::class.java, Profile.Serializer.INSTANCE)
             .registerTypeAdapter(File::class.java, FileTypeAdapter.INSTANCE)
             .setPrettyPrinting().create()
 
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/VersionSetting.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/VersionSetting.kt
deleted file mode 100644
index aae249f4c..000000000
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/setting/VersionSetting.kt
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * Hello Minecraft! Launcher.
- * Copyright (C) 2017  huangyuhui 
- * 
- * 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 {http://www.gnu.org/licenses/}.
- */
-package org.jackhuang.hmcl.setting
-
-import com.google.gson.*
-import javafx.beans.InvalidationListener
-import org.jackhuang.hmcl.Main
-import org.jackhuang.hmcl.game.LaunchOptions
-import org.jackhuang.hmcl.setting.Settings.proxyHost
-import org.jackhuang.hmcl.setting.Settings.proxyPass
-import org.jackhuang.hmcl.setting.Settings.proxyPort
-import org.jackhuang.hmcl.setting.Settings.proxyUser
-import org.jackhuang.hmcl.util.*
-import java.io.File
-import java.io.IOException
-import java.lang.reflect.Type
-
-class VersionSetting() {
-
-    var isGlobal: Boolean = false
-
-    /**
-     * HMCL Version Settings have been divided into 2 parts.
-     * 1. Global settings.
-     * 2. Version settings.
-     * If a version claims that it uses global settings, its version setting will be disabled.
-     *
-     * Defaults false because if one version uses global first, custom version file will not be generated.
-     */
-    val usesGlobalProperty = ImmediateBooleanProperty(this, "usesGlobal", false)
-    var usesGlobal: Boolean by usesGlobalProperty
-
-    // java
-
-    /**
-     * Java version or null if user customizes java directory.
-     */
-    val javaProperty = ImmediateStringProperty(this, "java", "")
-    var java: String by javaProperty
-
-    /**
-     * User customized java directory or null if user uses system Java.
-     */
-    val javaDirProperty = ImmediateStringProperty(this, "javaDir", "")
-    var javaDir: String by javaDirProperty
-
-    /**
-     * The command to launch java, i.e. optirun.
-     */
-    val wrapperProperty = ImmediateStringProperty(this, "wrapper", "")
-    var wrapper: String by wrapperProperty
-
-    /**
-     * The permanent generation size of JVM garbage collection.
-     */
-    val permSizeProperty = ImmediateStringProperty(this, "permSize", "")
-    var permSize: String by permSizeProperty
-
-    /**
-     * The maximum memory that JVM can allocate for heap.
-     */
-    val maxMemoryProperty = ImmediateIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY.toInt())
-    var maxMemory: Int by maxMemoryProperty
-
-    /**
-     * The minimum memory that JVM can allocate for heap.
-     */
-    val minMemoryProperty = ImmediateObjectProperty(this, "minMemory", null)
-    var minMemory: Int? by minMemoryProperty
-
-    /**
-     * The command that will be executed before launching the Minecraft.
-     * Operating system relevant.
-     */
-    val precalledCommandProperty = ImmediateStringProperty(this, "precalledCommand", "")
-    var precalledCommand: String by precalledCommandProperty
-
-    // options
-
-    /**
-     * The user customized arguments passed to JVM.
-     */
-    val javaArgsProperty = ImmediateStringProperty(this, "javaArgs", "")
-    var javaArgs: String by javaArgsProperty
-
-    /**
-     * The user customized arguments passed to Minecraft.
-     */
-    val minecraftArgsProperty = ImmediateStringProperty(this, "minecraftArgs", "")
-    var minecraftArgs: String by minecraftArgsProperty
-
-    /**
-     * True if disallow HMCL use default JVM arguments.
-     */
-    val noJVMArgsProperty = ImmediateBooleanProperty(this, "noJVMArgs", false)
-    var noJVMArgs: Boolean by noJVMArgsProperty
-
-    /**
-     * True if HMCL does not check game's completeness.
-     */
-    val notCheckGameProperty = ImmediateBooleanProperty(this, "notCheckGame", false)
-    var notCheckGame: Boolean by notCheckGameProperty
-
-    /**
-     * True if HMCL does not find/download libraries in/to common path.
-     */
-    val noCommonProperty = ImmediateBooleanProperty(this, "noCommon", false)
-    var noCommon: Boolean by noCommonProperty
-
-    /**
-     * True if show the logs after game launched.
-     */
-    val showLogsProperty = ImmediateBooleanProperty(this, "showLogs", false)
-    var showLogs: Boolean by showLogsProperty
-
-    // Minecraft settings.
-
-    /**
-     * The server ip that will be entered after Minecraft successfully loaded immediately.
-     *
-     * Format: ip:port or without port.
-     */
-    val serverIpProperty = ImmediateStringProperty(this, "serverIp", "")
-    var serverIp: String by serverIpProperty
-
-    /**
-     * True if Minecraft started in fullscreen mode.
-     */
-    val fullscreenProperty = ImmediateBooleanProperty(this, "fullscreen", false)
-    var fullscreen: Boolean by fullscreenProperty
-
-    /**
-     * The width of Minecraft window, defaults 800.
-     *
-     * The field saves int value.
-     * String type prevents unexpected value from causing JsonSyntaxException.
-     * We can only reset this field instead of recreating the whole setting file.
-     */
-    val widthProperty = ImmediateIntegerProperty(this, "width", 854)
-    var width: Int by widthProperty
-
-
-    /**
-     * The height of Minecraft window, defaults 480.
-     *
-     * The field saves int value.
-     * String type prevents unexpected value from causing JsonSyntaxException.
-     * We can only reset this field instead of recreating the whole setting file.
-     */
-    val heightProperty = ImmediateIntegerProperty(this, "height", 480)
-    var height: Int by heightProperty
-
-
-    /**
-     * 0 - .minecraft
- * 1 - .minecraft/versions/<version>/
- */ - val gameDirTypeProperty = ImmediateObjectProperty(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER) - var gameDirType: EnumGameDirectory by gameDirTypeProperty - - /** - * Your custom gameDir - */ - val gameDirProperty = ImmediateStringProperty(this, "gameDir", "") - var gameDir: String by gameDirProperty - - // launcher settings - - /** - * 0 - Close the launcher when the game starts.
- * 1 - Hide the launcher when the game starts.
- * 2 - Keep the launcher open.
- */ - val launcherVisibilityProperty = ImmediateObjectProperty(this, "launcherVisibility", LauncherVisibility.HIDE) - var launcherVisibility: LauncherVisibility by launcherVisibilityProperty - - val javaVersion: JavaVersion? get() { - // TODO: lazy initialization may result in UI suspension. - if (java.isBlank()) - java = if (javaDir.isBlank()) "Default" else "Custom" - if (java == "Default") return JavaVersion.fromCurrentEnvironment() - else if (java == "Custom") { - try { - return JavaVersion.fromExecutable(File(javaDir)) - } catch (e: IOException) { - return null // Custom Java Directory not found, - } - } else if (java.isNotBlank()) { - val c = JavaVersion.getJREs()[java] - if (c == null) { - java = "Default" - return JavaVersion.fromCurrentEnvironment() - } else - return c - } else throw Error() - } - - fun addPropertyChangedListener(listener: InvalidationListener) { - usesGlobalProperty.addListener(listener) - javaProperty.addListener(listener) - javaDirProperty.addListener(listener) - wrapperProperty.addListener(listener) - permSizeProperty.addListener(listener) - maxMemoryProperty.addListener(listener) - minMemoryProperty.addListener(listener) - precalledCommandProperty.addListener(listener) - javaArgsProperty.addListener(listener) - minecraftArgsProperty.addListener(listener) - noJVMArgsProperty.addListener(listener) - notCheckGameProperty.addListener(listener) - noCommonProperty.addListener(listener) - showLogsProperty.addListener(listener) - serverIpProperty.addListener(listener) - fullscreenProperty.addListener(listener) - widthProperty.addListener(listener) - heightProperty.addListener(listener) - gameDirTypeProperty.addListener(listener) - gameDirProperty.addListener(listener) - launcherVisibilityProperty.addListener(listener) - } - - @Throws(IOException::class) - fun toLaunchOptions(gameDir: File): LaunchOptions { - return LaunchOptions.Builder() - .setGameDir(gameDir) - .setJava(javaVersion ?: JavaVersion.fromCurrentEnvironment()) - .setVersionName(Main.TITLE) - .setProfileName(Main.TITLE) - .setMinecraftArgs(minecraftArgs) - .setJavaArgs(javaArgs) - .setMaxMemory(maxMemory) - .setMinMemory(minMemory) - .setMetaspace(permSize.toIntOrNull()) - .setWidth(width) - .setHeight(height) - .setFullscreen(fullscreen) - .setServerIp(serverIp) - .setWrapper(wrapper) - .setProxyHost(Settings.proxyHost) - .setProxyPort(Settings.proxyPort) - .setProxyUser(Settings.proxyUser) - .setProxyPass(Settings.proxyPass) - .setPrecalledCommand(precalledCommand) - .setNoGeneratedJVMArgs(noJVMArgs) - .create() - } - - companion object Serializer: JsonSerializer, JsonDeserializer { - override fun serialize(src: VersionSetting?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if (src == null) return JsonNull.INSTANCE - val jsonObject = JsonObject() - with(jsonObject) { - addProperty("usesGlobal", src.usesGlobal) - addProperty("javaArgs", src.javaArgs) - addProperty("minecraftArgs", src.minecraftArgs) - addProperty("maxMemory", if (src.maxMemory <= 0) OperatingSystem.SUGGESTED_MEMORY.toInt() else src.maxMemory) - addProperty("minMemory", src.minMemory) - addProperty("permSize", src.permSize) - addProperty("width", src.width) - addProperty("height", src.height) - addProperty("javaDir", src.javaDir) - addProperty("precalledCommand", src.precalledCommand) - addProperty("serverIp", src.serverIp) - addProperty("java", src.java) - addProperty("wrapper", src.wrapper) - addProperty("fullscreen", src.fullscreen) - addProperty("noJVMArgs", src.noJVMArgs) - addProperty("notCheckGame", src.notCheckGame) - addProperty("noCommon", src.noCommon) - addProperty("showLogs", src.showLogs) - addProperty("gameDir", src.gameDir) - addProperty("launcherVisibility", src.launcherVisibility.ordinal) - addProperty("gameDirType", src.gameDirType.ordinal) - } - - return jsonObject - } - - override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): VersionSetting? { - if (json == null || json == JsonNull.INSTANCE || json !is JsonObject) return null - - var maxMemoryN = parseJsonPrimitive(json["maxMemory"]?.asJsonPrimitive, OperatingSystem.SUGGESTED_MEMORY.toInt()) - if (maxMemoryN <= 0) maxMemoryN = OperatingSystem.SUGGESTED_MEMORY.toInt() - - return VersionSetting().apply { - usesGlobal = json["usesGlobal"]?.asBoolean ?: false - javaArgs = json["javaArgs"]?.asString ?: "" - minecraftArgs = json["minecraftArgs"]?.asString ?: "" - maxMemory = maxMemoryN - minMemory = json["minMemory"]?.asInt - permSize = json["permSize"]?.asString ?: "" - width = parseJsonPrimitive(json["width"]?.asJsonPrimitive) - height = parseJsonPrimitive(json["height"]?.asJsonPrimitive) - javaDir = json["javaDir"]?.asString ?: "" - precalledCommand = json["precalledCommand"]?.asString ?: "" - serverIp = json["serverIp"]?.asString ?: "" - java = json["java"]?.asString ?: "" - wrapper = json["wrapper"]?.asString ?: "" - gameDir = json["gameDir"]?.asString ?: "" - fullscreen = json["fullscreen"]?.asBoolean ?: false - noJVMArgs = json["noJVMArgs"]?.asBoolean ?: false - notCheckGame = json["notCheckGame"]?.asBoolean ?: false - noCommon = json["noCommon"]?.asBoolean ?: false - showLogs = json["showLogs"]?.asBoolean ?: false - launcherVisibility = LauncherVisibility.values()[json["launcherVisibility"]?.asInt ?: 1] - gameDirType = EnumGameDirectory.values()[json["gameDirType"]?.asInt ?: 0] - } - } - - fun parseJsonPrimitive(primitive: JsonPrimitive?, defaultValue: Int = 0): Int { - if (primitive != null) - if (primitive.isNumber) - return primitive.asInt - else - return primitive.asString.toIntOrNull() ?: defaultValue - else - return defaultValue - } - - } -} \ No newline at end of file diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/AccountsPage.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/AccountsPage.kt index 5245cd95a..d62d91c50 100644 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/AccountsPage.kt +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/AccountsPage.kt @@ -28,11 +28,13 @@ import javafx.scene.control.ScrollPane import javafx.scene.control.ToggleGroup import javafx.scene.layout.StackPane import org.jackhuang.hmcl.auth.Account +import org.jackhuang.hmcl.auth.MultiCharacterSelector import org.jackhuang.hmcl.auth.OfflineAccount import org.jackhuang.hmcl.auth.OfflineAccountFactory import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory +import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector import org.jackhuang.hmcl.i18n import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.task.Schedulers @@ -133,7 +135,7 @@ class AccountsPage() : StackPane(), DecoratorPage { else -> throw UnsupportedOperationException() } - account.logIn(Settings.proxy) + account.logIn(HMCLMultiCharacterSelector, Settings.proxy) account } catch (e: Exception) { e diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/Controllers.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/Controllers.kt index 431652b7b..9198c0eef 100644 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/Controllers.kt +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/Controllers.kt @@ -52,7 +52,7 @@ object Controllers { decorator.isCustomMaximize = false - scene = Scene(decorator, 800.0, 480.0) + scene = Scene(decorator, 804.0, 521.0) scene.stylesheets.addAll(*stylesheets) stage.minWidth = 800.0 stage.maxWidth = 800.0 diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/FXUtils.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/FXUtils.kt index 27da90592..c96083587 100644 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/FXUtils.kt +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/FXUtils.kt @@ -266,4 +266,4 @@ fun ImageView.limitSize(maxWidth: Double, maxHeight: Double) { } } -val DEFAULT_ICON = Image("/assets/img/icon.png") \ No newline at end of file +@JvmField val DEFAULT_ICON = Image("/assets/img/icon.png") \ No newline at end of file diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/LeftPaneController.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/LeftPaneController.kt index f640f0509..8a6a29982 100644 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/LeftPaneController.kt +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/LeftPaneController.kt @@ -97,9 +97,7 @@ class LeftPaneController(private val leftPane: AdvancedListBox) { fun onProfilesLoading() { val list = LinkedList() Settings.getProfiles().forEach { profile -> - val item = VersionListItem(profile.name).apply { - lblGameVersion.textProperty().bind(profile.selectedVersionProperty) - } + val item = VersionListItem(profile.name) val ripplerContainer = RipplerContainer(item) item.onSettingsButtonClicked { Controllers.decorator.showPage(ProfilePage(profile)) diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/LogWindow.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/LogWindow.kt deleted file mode 100644 index b3a3d6cec..000000000 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/LogWindow.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2017 huangyuhui - * - * 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 {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.ui - -import com.jfoenix.controls.JFXComboBox -import javafx.beans.Observable -import javafx.beans.binding.Bindings -import javafx.beans.property.SimpleIntegerProperty -import javafx.concurrent.Worker -import javafx.fxml.FXML -import javafx.scene.Scene -import javafx.scene.control.ToggleButton -import javafx.scene.image.Image -import javafx.scene.layout.StackPane -import javafx.scene.web.WebEngine -import javafx.scene.web.WebView -import javafx.stage.Stage -import netscape.javascript.JSObject -import org.jackhuang.hmcl.game.LauncherHelper -import org.jackhuang.hmcl.i18n -import org.jackhuang.hmcl.setting.Settings -import org.jackhuang.hmcl.util.* -import org.w3c.dom.Document -import org.w3c.dom.Node -import java.util.concurrent.Callable -import java.util.concurrent.CountDownLatch - -class LogWindow : Stage() { - val fatalProperty = SimpleIntegerProperty(0) - val errorProperty = SimpleIntegerProperty(0) - val warnProperty = SimpleIntegerProperty(0) - val infoProperty = SimpleIntegerProperty(0) - val debugProperty = SimpleIntegerProperty(0) - - val impl = LogWindowImpl() - val latch = CountDownLatch(1) - - init { - scene = Scene(impl, 800.0, 480.0) - scene.stylesheets.addAll(*stylesheets) - title = i18n("logwindow.title") - icons += Image("/assets/img/icon.png") - } - - fun logLine(line: String, level: Log4jLevel) { - impl.body.appendChild(impl.engine.document.createElement("div").apply { - // a
 element to prevent multiple spaces and tabs being removed.
-            appendChild(impl.engine.document.createElement("pre").apply {
-                textContent = line
-            })
-        })
-        impl.engine.executeScript("checkNewLog(\"${level.name.toLowerCase()}\");scrollToBottom();")
-
-        when (level) {
-            Log4jLevel.FATAL -> fatalProperty.inc()
-            Log4jLevel.ERROR -> errorProperty.inc()
-            Log4jLevel.WARN -> warnProperty.inc()
-            Log4jLevel.INFO -> infoProperty.inc()
-            Log4jLevel.DEBUG -> debugProperty.inc()
-            else -> {}
-        }
-    }
-
-    fun waitForShown() = latch.await()
-
-    inner class LogWindowImpl: StackPane() {
-        @FXML lateinit var webView: WebView
-        @FXML lateinit var btnFatals: ToggleButton
-        @FXML lateinit var btnErrors: ToggleButton
-        @FXML lateinit var btnWarns: ToggleButton
-        @FXML lateinit var btnInfos: ToggleButton
-        @FXML lateinit var btnDebugs: ToggleButton
-
-        @FXML lateinit var cboLines: JFXComboBox
-        val engine: WebEngine
-        lateinit var body: Node
-        lateinit var document: Document
-
-        init {
-            loadFXML("/assets/fxml/log.fxml")
-
-            engine = webView.engine
-            engine.loadContent(javaClass.getResourceAsStream("/assets/log-window-content.html").readFullyAsString().replace("\${FONT}", "${Settings.font.size}px \"${Settings.font.family}\""))
-            engine.loadWorker.stateProperty().onChange {
-                if (it == Worker.State.SUCCEEDED) {
-                    document = engine.document
-                    body = document.getElementsByTagName("body").item(0)
-                    engine.executeScript("limitedLogs=${Settings.logLines};")
-                    latch.countDown()
-                }
-            }
-
-            var flag = false
-            for (i in cboLines.items) {
-                if (i == Settings.logLines.toString()) {
-                    cboLines.selectionModel.select(i)
-                    flag = true
-                }
-            }
-            cboLines.selectionModel.selectedItemProperty().onChange {
-                Settings.logLines = it?.toInt() ?: 100
-                engine.executeScript("limitedLogs=${Settings.logLines};")
-            }
-            if (!flag) {
-                cboLines.selectionModel.select(0)
-            }
-
-            btnFatals.textProperty().bind(Bindings.createStringBinding(Callable { fatalProperty.get().toString() + " fatals" }, fatalProperty))
-            btnErrors.textProperty().bind(Bindings.createStringBinding(Callable { errorProperty.get().toString() + " errors" }, errorProperty))
-            btnWarns.textProperty().bind(Bindings.createStringBinding(Callable { warnProperty.get().toString() + " warns" }, warnProperty))
-            btnInfos.textProperty().bind(Bindings.createStringBinding(Callable { infoProperty.get().toString() + " infos" }, infoProperty))
-            btnDebugs.textProperty().bind(Bindings.createStringBinding(Callable { debugProperty.get().toString() + " debugs" }, debugProperty))
-
-            btnFatals.selectedProperty().onInvalidated(this::specificChanged)
-            btnErrors.selectedProperty().onInvalidated(this::specificChanged)
-            btnWarns.selectedProperty().onInvalidated(this::specificChanged)
-            btnInfos.selectedProperty().onInvalidated(this::specificChanged)
-            btnDebugs.selectedProperty().onInvalidated(this::specificChanged)
-        }
-
-        private fun specificChanged() {
-            var res = ""
-            if (btnFatals.isSelected) res += "\"fatal\", "
-            if (btnErrors.isSelected) res += "\"error\", "
-            if (btnWarns.isSelected) res += "\"warn\", "
-            if (btnInfos.isSelected) res += "\"info\", "
-            if (btnDebugs.isSelected) res += "\"debug\", "
-            if (res.isNotBlank())
-                res = res.substringBeforeLast(", ")
-            engine.executeScript("specific([$res])")
-        }
-
-        fun onTerminateGame() {
-            LauncherHelper.stopManagedProcesses()
-        }
-
-        fun onClear() {
-            engine.executeScript("clear()")
-        }
-    }
-}
\ No newline at end of file
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/MainPage.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/MainPage.kt
index aff8d3b14..3edc2ae07 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/MainPage.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/MainPage.kt
@@ -21,10 +21,8 @@ import com.jfoenix.controls.JFXButton
 import com.jfoenix.controls.JFXMasonryPane
 import javafx.application.Platform
 import javafx.beans.property.SimpleStringProperty
-import javafx.beans.property.StringProperty
 import javafx.fxml.FXML
 import javafx.scene.Node
-import javafx.scene.control.ToggleGroup
 import javafx.scene.image.Image
 import javafx.scene.layout.StackPane
 import org.jackhuang.hmcl.ProfileChangedEvent
@@ -36,11 +34,9 @@ import org.jackhuang.hmcl.game.LauncherHelper
 import org.jackhuang.hmcl.i18n
 import org.jackhuang.hmcl.setting.Profile
 import org.jackhuang.hmcl.setting.Settings
-import org.jackhuang.hmcl.ui.construct.RipplerContainer
 import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
 import org.jackhuang.hmcl.ui.wizard.DecoratorPage
 import org.jackhuang.hmcl.util.channel
-import org.jackhuang.hmcl.util.onChange
 import org.jackhuang.hmcl.util.plusAssign
 
 /**
@@ -49,7 +45,6 @@ import org.jackhuang.hmcl.util.plusAssign
 class MainPage : StackPane(), DecoratorPage {
     override val titleProperty = SimpleStringProperty(this, "title", i18n("launcher.title.main"))
 
-    @FXML lateinit var btnLaunch: JFXButton
     @FXML lateinit var btnRefresh: JFXButton
     @FXML lateinit var btnAdd: JFXButton
     @FXML lateinit var masonryPane: JFXMasonryPane
@@ -57,32 +52,24 @@ class MainPage : StackPane(), DecoratorPage {
     init {
         loadFXML("/assets/fxml/main.fxml")
 
-        btnLaunch.graphic = SVG.launch("white", 15.0, 15.0)
-        btnLaunch.limitWidth(40.0)
-        btnLaunch.limitHeight(40.0)
-
-        EventBus.EVENT_BUS.channel() += { -> loadVersions() }
+        EventBus.EVENT_BUS.channel() += { -> runOnUiThread { loadVersions() } }
         EventBus.EVENT_BUS.channel() += this::onProfilesLoading
         EventBus.EVENT_BUS.channel() += this::onProfileChanged
 
         btnAdd.setOnMouseClicked { Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game") }
         btnRefresh.setOnMouseClicked { Settings.selectedProfile.repository.refreshVersions() }
-        btnLaunch.setOnMouseClicked {
-            if (Settings.selectedAccount == null) {
-                Controllers.dialog(i18n("login.no_Player007"))
-            } else if (Settings.selectedProfile.selectedVersion == null) {
-                Controllers.dialog(i18n("minecraft.no_selected_version"))
-            } else
-                LauncherHelper.launch()
-        }
     }
 
-    private fun buildNode(i: Int, profile: Profile, version: String, game: String, group: ToggleGroup): Node {
-        return VersionItem(i, group).apply {
-            chkSelected.properties["version"] = version
-            chkSelected.isSelected = profile.selectedVersion == version
+    private fun buildNode(i: Int, profile: Profile, version: String, game: String): Node {
+        return VersionItem().apply {
             lblGameVersion.text = game
             lblVersionName.text = version
+            btnLaunch.setOnMouseClicked {
+                if (Settings.selectedAccount == null) {
+                    Controllers.dialog(i18n("login.no_Player007"))
+                } else
+                    LauncherHelper.INSTANCE.launch(version)
+            }
             btnDelete.setOnMouseClicked {
                 profile.repository.removeVersionFromDisk(version)
                 Platform.runLater { loadVersions() }
@@ -103,30 +90,15 @@ class MainPage : StackPane(), DecoratorPage {
 
     fun onProfileChanged(event: ProfileChangedEvent) = runOnUiThread {
         val profile = event.value
-        profile.selectedVersionProperty.setChangedListener {
-            versionChanged(profile.selectedVersion)
-        }
         loadVersions(profile)
     }
 
     private fun loadVersions(profile: Profile = Settings.selectedProfile) {
-        val group = ToggleGroup()
         val children = mutableListOf()
         var i = 0
-        profile.repository.getVersions().forEach { version ->
-            children += buildNode(++i, profile, version.id, minecraftVersion(profile.repository.getVersionJar(version.id)) ?: "Unknown", group)
-        }
-        group.selectedToggleProperty().onChange {
-            if (it != null)
-                profile.selectedVersion = it.properties["version"] as String
+        profile.repository.versions.forEach { version ->
+            children += buildNode(++i, profile, version.id, minecraftVersion(profile.repository.getVersionJar(version.id)) ?: "Unknown")
         }
         masonryPane.resetChildren(children)
     }
-
-    @Suppress("UNCHECKED_CAST")
-    fun versionChanged(selectedVersion: String?) {
-        masonryPane.children
-                .filter { it is RipplerContainer && it.properties["version"] is Pair<*, *> }
-                .forEach { (it as RipplerContainer).selected = (it.properties["version"] as Pair).first == selectedVersion }
-    }
 }
\ No newline at end of file
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/ProfilePage.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/ProfilePage.kt
index 80dbbf959..cb32e2703 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/ProfilePage.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/ProfilePage.kt
@@ -75,7 +75,7 @@ class ProfilePage(private val profile: Profile?): StackPane(), DecoratorPage {
             if (locationProperty.get().isNullOrBlank()) {
                 gameDir.onExplore()
             }
-            Settings.putProfile(Profile(name = txtProfileName.text, initialGameDir = File(locationProperty.get())))
+            Settings.putProfile(Profile(txtProfileName.text, File(locationProperty.get())))
         }
 
         Settings.onProfileLoading()
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionItem.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionItem.kt
index beaeaa805..6be26a00f 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionItem.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionItem.kt
@@ -32,15 +32,15 @@ import javafx.scene.layout.VBox
 import javafx.scene.paint.Color
 import java.util.concurrent.Callable
 
-class VersionItem(i: Int, group: ToggleGroup) : StackPane() {
+class VersionItem() : StackPane() {
     @FXML lateinit var icon: Pane
     @FXML lateinit var content: VBox
     @FXML lateinit var header: StackPane
     @FXML lateinit var body: StackPane
     @FXML lateinit var btnDelete: JFXButton
     @FXML lateinit var btnSettings: JFXButton
+    @FXML lateinit var btnLaunch: JFXButton
     @FXML lateinit var lblVersionName: Label
-    @FXML lateinit var chkSelected: JFXRadioButton
     @FXML lateinit var lblGameVersion: Label
     @FXML lateinit var iconView: ImageView
 
@@ -52,20 +52,20 @@ class VersionItem(i: Int, group: ToggleGroup) : StackPane() {
 
         effect = DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0)
 
-        chkSelected.toggleGroup = group
         btnSettings.graphic = SVG.gear("black", 15.0, 15.0)
         btnDelete.graphic = SVG.delete("black", 15.0, 15.0)
+        btnLaunch.graphic = SVG.launch("black", 15.0, 15.0)
 
         // create content
-        val headerColor = getDefaultColor(i % 12)
-        header.style = "-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor
+        //val headerColor = getDefaultColor(i % 12)
+        //header.style = "-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor
 
         // create image view
-        icon.translateYProperty().bind(Bindings.createDoubleBinding(Callable { header.boundsInParent.height - icon.height / 2 - 32.0 }, header.boundsInParentProperty(), icon.heightProperty()))
+        icon.translateYProperty().bind(Bindings.createDoubleBinding(Callable { header.boundsInParent.height - icon.height / 2 - 16.0 }, header.boundsInParentProperty(), icon.heightProperty()))
         iconView.limitSize(32.0, 32.0)
     }
 
-    private fun getDefaultColor(i: Int): String {
+    /*private fun getDefaultColor(i: Int): String {
         var color = "#FFFFFF"
         when (i) {
             0 -> color = "#8F3F7E"
@@ -85,5 +85,5 @@ class VersionItem(i: Int, group: ToggleGroup) : StackPane() {
             }
         }
         return color
-    }
+    }*/
 }
\ No newline at end of file
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionSettingsController.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionSettingsController.kt
index 0b5683d40..15b0f6648 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionSettingsController.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionSettingsController.kt
@@ -117,39 +117,39 @@ class VersionSettingsController {
         this.versionId = versionId
 
         lastVersionSetting?.apply {
-            widthProperty.unbind()
-            heightProperty.unbind()
-            maxMemoryProperty.unbind()
-            javaArgsProperty.unbind()
-            minecraftArgsProperty.unbind()
-            permSizeProperty.unbind()
-            wrapperProperty.unbind()
-            precalledCommandProperty.unbind()
-            serverIpProperty.unbind()
-            fullscreenProperty.unbind()
-            notCheckGameProperty.unbind()
-            noCommonProperty.unbind()
-            javaDirProperty.unbind()
-            showLogsProperty.unbind()
+            widthProperty().unbind()
+            heightProperty().unbind()
+            maxMemoryProperty().unbind()
+            javaArgsProperty().unbind()
+            minecraftArgsProperty().unbind()
+            permSizeProperty().unbind()
+            wrapperProperty().unbind()
+            preLaunchCommandProperty().unbind()
+            serverIpProperty().unbind()
+            fullscreenProperty().unbind()
+            notCheckGameProperty().unbind()
+            noCommonProperty().unbind()
+            javaDirProperty().unbind()
+            showLogsProperty().unbind()
             unbindEnum(cboLauncherVisibility)
         }
 
-        bindInt(txtWidth, version.widthProperty)
-        bindInt(txtHeight, version.heightProperty)
-        bindInt(txtMaxMemory, version.maxMemoryProperty)
-        bindString(javaItem.txtCustom, version.javaDirProperty)
-        bindString(gameDirItem.txtCustom, version.gameDirProperty)
-        bindString(txtJVMArgs, version.javaArgsProperty)
-        bindString(txtGameArgs, version.minecraftArgsProperty)
-        bindString(txtMetaspace, version.permSizeProperty)
-        bindString(txtWrapper, version.wrapperProperty)
-        bindString(txtPrecallingCommand, version.precalledCommandProperty)
-        bindString(txtServerIP, version.serverIpProperty)
-        bindEnum(cboLauncherVisibility, version.launcherVisibilityProperty)
-        bindBoolean(chkFullscreen, version.fullscreenProperty)
-        bindBoolean(chkNoGameCheck, version.notCheckGameProperty)
-        bindBoolean(chkNoCommon, version.noCommonProperty)
-        bindBoolean(chkShowLogs, version.showLogsProperty)
+        bindInt(txtWidth, version.widthProperty())
+        bindInt(txtHeight, version.heightProperty())
+        bindInt(txtMaxMemory, version.maxMemoryProperty())
+        bindString(javaItem.txtCustom, version.javaDirProperty())
+        bindString(gameDirItem.txtCustom, version.gameDirProperty())
+        bindString(txtJVMArgs, version.javaArgsProperty())
+        bindString(txtGameArgs, version.minecraftArgsProperty())
+        bindString(txtMetaspace, version.permSizeProperty())
+        bindString(txtWrapper, version.wrapperProperty())
+        bindString(txtPrecallingCommand, version.preLaunchCommandProperty())
+        bindString(txtServerIP, version.serverIpProperty())
+        bindEnum(cboLauncherVisibility, version.launcherVisibilityProperty())
+        bindBoolean(chkFullscreen, version.fullscreenProperty())
+        bindBoolean(chkNoGameCheck, version.notCheckGameProperty())
+        bindBoolean(chkNoCommon, version.noCommonProperty())
+        bindBoolean(chkShowLogs, version.showLogsProperty())
 
         val javaGroupKey = "java_group.listener"
         @Suppress("UNCHECKED_CAST")
@@ -181,8 +181,8 @@ class VersionSettingsController {
             defaultToggle?.isSelected = true
         }
 
-        version.javaDirProperty.setChangedListener { initJavaSubtitle(version) }
-        version.javaProperty.setChangedListener { initJavaSubtitle(version) }
+        version.javaDirProperty().setChangedListener { initJavaSubtitle(version) }
+        version.javaProperty().setChangedListener { initJavaSubtitle(version) }
         initJavaSubtitle(version)
 
         val gameDirKey = "game_dir.listener"
@@ -205,8 +205,8 @@ class VersionSettingsController {
         gameDirItem.group.properties[gameDirKey] = gameDirListener
         gameDirItem.group.selectedToggleProperty().addListener(gameDirListener)
 
-        version.gameDirProperty.setChangedListener { initGameDirSubtitle(version) }
-        version.gameDirTypeProperty.setChangedListener { initGameDirSubtitle(version) }
+        version.gameDirProperty().setChangedListener { initGameDirSubtitle(version) }
+        version.gameDirTypeProperty().setChangedListener { initGameDirSubtitle(version) }
         initGameDirSubtitle(version)
 
         lastVersionSetting = version
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/YggdrasilAccountLoginPane.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/YggdrasilAccountLoginPane.kt
index 9515c8bf4..e5a9e18a0 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/YggdrasilAccountLoginPane.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/YggdrasilAccountLoginPane.kt
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.auth.AuthInfo
 import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException
 import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
 import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory
+import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector
 import org.jackhuang.hmcl.i18n
 import org.jackhuang.hmcl.setting.Settings
 import org.jackhuang.hmcl.task.Schedulers
@@ -56,7 +57,7 @@ class YggdrasilAccountLoginPane(private val oldAccount: YggdrasilAccount, privat
         taskResult("login") {
             try {
                 val account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password)
-                account.logIn(Settings.proxy)
+                account.logIn(HMCLMultiCharacterSelector, Settings.proxy)
             } catch (e: Exception) {
                 e
             }
diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/util/Lang.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/util/Lang.kt
index e9cc0a01d..98cc7578b 100644
--- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/util/Lang.kt
+++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/util/Lang.kt
@@ -22,6 +22,7 @@ import com.google.gson.JsonParseException
 import com.google.gson.reflect.TypeToken
 import javafx.beans.property.Property
 import javafx.event.Event.fireEvent
+import org.jackhuang.hmcl.event.Event
 import org.jackhuang.hmcl.event.EventBus
 import org.jackhuang.hmcl.event.EventManager
 import org.jackhuang.hmcl.task.Scheduler
@@ -113,10 +114,10 @@ fun  taskResult(id: String, callable: Callable): TaskResult = Task.ofRe
 fun  taskResult(id: String, callable: (AutoTypingMap) -> V): TaskResult = Task.ofResult(id, callable)
 
 fun InputStream.readFullyAsString() = IOUtils.readFullyAsString(this)
-inline fun  EventBus.channel() = channel(T::class.java)
+inline fun  EventBus.channel() = channel(T::class.java)
 
-operator fun  EventManager.plusAssign(func: (T) -> Unit) = register(func)
-operator fun  EventManager.plusAssign(func: () -> Unit) = register(func)
-operator fun  EventManager.minusAssign(func: (T) -> Unit) = unregister(func)
-operator fun  EventManager.minusAssign(func: () -> Unit) = unregister(func)
-operator fun  EventManager.invoke(event: T) = fireEvent(event)
\ No newline at end of file
+operator fun  EventManager.plusAssign(func: (T) -> Unit) = register(func)
+operator fun  EventManager.plusAssign(func: () -> Unit) = register(func)
+operator fun  EventManager.minusAssign(func: (T) -> Unit) = unregister(func)
+operator fun  EventManager.minusAssign(func: () -> Unit) = unregister(func)
+operator fun  EventManager.invoke(event: T) = fireEvent(event)
\ No newline at end of file
diff --git a/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css b/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css
index d09624959..e153296c5 100644
--- a/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css
+++ b/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css
@@ -968,6 +968,11 @@
     -fx-background-color: -fx-decorator-color;
 }
 
+.jfx-decorator-drawer {
+    -fx-background-image: url("/assets/img/background.jpg");
+    -fx-background-size: cover;
+}
+
 .resize-border {
     -fx-border-color: #5264AE;
     -fx-border-width: 0 2 2 2;
@@ -1136,6 +1141,7 @@
  ******************************************************************************/
 
 .scroll-pane {
+    -fx-background-color: null;
     -fx-background-insets: 0;
     -fx-padding: 0;
 }
@@ -1148,6 +1154,10 @@
     -fx-background-insets: 0;
 }
 
+.scroll-pane > .viewport {
+    -fx-background-color: null;
+ }
+
 /*******************************************************************************
 *                                                                              *
 * Error Facade                                                                 *
diff --git a/HMCL/src/main/resources/assets/fxml/decorator.fxml b/HMCL/src/main/resources/assets/fxml/decorator.fxml
index a179df0cb..9318897e5 100644
--- a/HMCL/src/main/resources/assets/fxml/decorator.fxml
+++ b/HMCL/src/main/resources/assets/fxml/decorator.fxml
@@ -7,6 +7,8 @@
 
 
 
+
+
 
@@ -16,14 +18,14 @@
     
     
         
- +
- +
@@ -50,9 +52,6 @@ - - - diff --git a/HMCL/src/main/resources/assets/fxml/main.fxml b/HMCL/src/main/resources/assets/fxml/main.fxml index 65dea1695..693a75da4 100644 --- a/HMCL/src/main/resources/assets/fxml/main.fxml +++ b/HMCL/src/main/resources/assets/fxml/main.fxml @@ -5,7 +5,7 @@ @@ -24,12 +24,6 @@ - - - - - diff --git a/HMCL/src/main/resources/assets/fxml/version-item.fxml b/HMCL/src/main/resources/assets/fxml/version-item.fxml index bb95ebcd6..6b84f2dc1 100644 --- a/HMCL/src/main/resources/assets/fxml/version-item.fxml +++ b/HMCL/src/main/resources/assets/fxml/version-item.fxml @@ -12,14 +12,14 @@ xmlns:fx="http://javafx.com/fxml" type="StackPane" pickOnBounds="false"> - +
- + @@ -35,7 +35,7 @@ - + diff --git a/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml b/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml index e100b8ed5..69682218c 100644 --- a/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml +++ b/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml @@ -12,7 +12,7 @@ - + @@ -109,7 +109,7 @@ - + + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.auth; + +import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; + +import java.util.List; + +/** + * This interface is for your application to open a GUI for user to choose the character + * when a having-multi-character yggdrasil account is being logging in.. + */ +public interface MultiCharacterSelector { + + /** + * Select one of {@code names} GameProfiles to login. + * @param names available game profiles. + * @throws NoSelectedCharacterException if cannot select any character may because user close the selection window or cancel the selection. + * @return your choice of game profile. + */ + GameProfile select(Account account, List names) throws NoSelectedCharacterException; + + MultiCharacterSelector DEFAULT = (account, names) -> names.stream().findFirst().orElseThrow(() -> new NoSelectedCharacterException(account)); +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoCharacterException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoCharacterException.java new file mode 100644 index 000000000..2e2a9033e --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoCharacterException.java @@ -0,0 +1,34 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.auth; + +/** + * This exception gets threw when authenticating a yggdrasil account and there is no valid character. + * (A account may hold more than one characters.) + */ +public final class NoCharacterException extends AuthenticationException { + private final Account account; + + public NoCharacterException(Account account) { + this.account = account; + } + + public Account getAccount() { + return account; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java new file mode 100644 index 000000000..64db45335 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java @@ -0,0 +1,41 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.auth; + +/** + * This exception gets threw when a monitor of {@link MultiCharacterSelector} cannot select a + * valid character. + * + * @see org.jackhuang.hmcl.auth.MultiCharacterSelector + * @author huangyuhui + */ +public final class NoSelectedCharacterException extends AuthenticationException { + private final Account account; + + /** + * + * @param account the error yggdrasil account. + */ + public NoSelectedCharacterException(Account account) { + this.account = account; + } + + public Account getAccount() { + return account; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccount.java index 97899dd09..aa3d14974 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccount.java @@ -54,7 +54,7 @@ public class OfflineAccount extends Account { } @Override - public AuthInfo logIn(Proxy proxy) throws AuthenticationException { + public AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException { if (StringUtils.isBlank(username) || StringUtils.isBlank(uuid)) throw new AuthenticationException("Username cannot be empty"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java index 977e8500e..ac6c1b094 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java @@ -24,13 +24,16 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; +import org.jackhuang.hmcl.util.Immutable; + import java.lang.reflect.Type; import java.util.UUID; /** * - * @author huang + * @author huangyuhui */ +@Immutable public final class GameProfile { private final UUID id; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java index b054e7a35..a9f9af072 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java @@ -23,14 +23,9 @@ import com.google.gson.JsonParseException; import java.io.IOException; import java.net.Proxy; import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.auth.AuthInfo; -import org.jackhuang.hmcl.auth.AuthenticationException; -import org.jackhuang.hmcl.auth.UserType; +import java.util.*; + +import org.jackhuang.hmcl.auth.*; import org.jackhuang.hmcl.util.NetworkUtils; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.UUIDTypeAdapter; @@ -111,7 +106,7 @@ public final class YggdrasilAccount extends Account { } @Override - public AuthInfo logIn(Proxy proxy) throws AuthenticationException { + public AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException { if (canPlayOnline()) return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); else { @@ -119,11 +114,13 @@ public final class YggdrasilAccount extends Account { if (!isLoggedIn()) throw new AuthenticationException("Wrong password for account " + username); - if (selectedProfile == null) - // TODO: multi-available-profiles support - throw new UnsupportedOperationException("Do not support multi-available-profiles account yet."); - else - return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); + if (selectedProfile == null) { + if (profiles == null || profiles.length <= 0) + throw new NoCharacterException(this); + + selectedProfile = selector.select(this, Arrays.asList(profiles)); + } + return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/Install.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstall.java similarity index 90% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/Install.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstall.java index 94d185a06..4d871cb2f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/Install.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstall.java @@ -24,7 +24,7 @@ import org.jackhuang.hmcl.util.Immutable; * @author huangyuhui */ @Immutable -public final class Install { +public final class ForgeInstall { private final String profileName; private final String target; @@ -36,11 +36,11 @@ public final class Install { private final String mirrorList; private final String logo; - public Install() { + public ForgeInstall() { this(null, null, null, null, null, null, null, null, null); } - public Install(String profileName, String target, String path, String version, String filePath, String welcome, String minecraft, String mirrorList, String logo) { + public ForgeInstall(String profileName, String target, String path, String version, String filePath, String welcome, String minecraft, String mirrorList, String logo) { this.profileName = profileName; this.target = target; this.path = path; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/InstallProfile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallProfile.java similarity index 88% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/InstallProfile.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallProfile.java index 1a2958dbf..614a63cc1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/InstallProfile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallProfile.java @@ -28,20 +28,20 @@ import org.jackhuang.hmcl.util.Validation; * @author huangyuhui */ @Immutable -public final class InstallProfile implements Validation { +public final class ForgeInstallProfile implements Validation { @SerializedName("install") - private final Install install; + private final ForgeInstall install; @SerializedName("versionInfo") private final Version versionInfo; - public InstallProfile(Install install, Version versionInfo) { + public ForgeInstallProfile(ForgeInstall install, Version versionInfo) { this.install = install; this.versionInfo = versionInfo; } - public Install getInstall() { + public ForgeInstall getInstall() { return install; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java index 9a7e378ab..f91f8a8a5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java @@ -107,7 +107,7 @@ public final class ForgeInstallTask extends TaskResult { if (stream == null) throw new IOException("Malformed forge installer file, install_profile.json does not exist."); String json = IOUtils.readFullyAsString(stream); - InstallProfile installProfile = Constants.GSON.fromJson(json, InstallProfile.class); + ForgeInstallProfile installProfile = Constants.GSON.fromJson(json, ForgeInstallProfile.class); if (installProfile == null) throw new IOException("Malformed forge installer file, install_profile.json does not exist."); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java index c3c8ea45b..c58c58c03 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java @@ -57,14 +57,13 @@ public final class GameLibrariesTask extends Task { @Override public void execute() throws Exception { - for (Library library : version.getLibraries()) - if (library.appliesToCurrentEnvironment()) { - File file = dependencyManager.getGameRepository().getLibraryFile(version, library); - if (!file.exists()) - dependencies.add(new FileDownloadTask( - NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(library.getDownload().getUrl())), - file, dependencyManager.getProxy(), library.getDownload().getSha1())); - } + version.getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> { + File file = dependencyManager.getGameRepository().getLibraryFile(version, library); + if (!file.exists()) + dependencies.add(new FileDownloadTask( + NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(library.getDownload().getUrl())), + file, dependencyManager.getProxy(), library.getDownload().getSha1())); + }); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersions.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersions.java index ce5a68353..e7eb2129f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersions.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersions.java @@ -36,7 +36,7 @@ public final class GameRemoteVersions { private final GameRemoteLatestVersions latest; public GameRemoteVersions() { - this(Collections.EMPTY_LIST, null); + this(Collections.emptyList(), null); } public GameRemoteVersions(List versions, GameRemoteLatestVersions latest) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java index f8465e981..01d559356 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java @@ -61,7 +61,7 @@ public final class VersionJsonDownloadTask extends Task { @Override public void execute() throws Exception { RemoteVersion remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst() - .orElseThrow(() -> new IllegalStateException("Cannot find specific version "+gameVersion+" in remote repository")); + .orElseThrow(() -> new IllegalStateException("Cannot find specific version " + gameVersion + " in remote repository")); String jsonURL = dependencyManager.getDownloadProvider().injectURL(remoteVersion.getUrl()); dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL), Proxy.NO_PROXY, ID)); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBranch.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBranch.java index 35ad64af6..938437d35 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBranch.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBranch.java @@ -38,7 +38,7 @@ public final class LiteLoaderBranch { private final Map liteLoader; public LiteLoaderBranch() { - this(Collections.EMPTY_SET, Collections.EMPTY_MAP); + this(Collections.emptySet(), Collections.emptyMap()); } public LiteLoaderBranch(Collection libraries, Map liteLoader) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java index c96be49b6..575410199 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java @@ -96,11 +96,11 @@ public final class LiteLoaderInstallTask extends TaskResult { new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl())) ); - Version tempVersion = version.setLibraries(Lang.merge(remote.getTag().getLibraries(), Arrays.asList(library))); + Version tempVersion = version.setLibraries(Lang.merge(remote.getTag().getLibraries(), Collections.singleton(library))); setResult(version .setMainClass("net.minecraft.launchwrapper.Launch") .setLibraries(Lang.merge(tempVersion.getLibraries(), version.getLibraries())) - .setLogging(Collections.EMPTY_MAP) + .setLogging(Collections.emptyMap()) .setMinecraftArguments(version.getMinecraftArguments().orElse("") + " --tweakClass " + remote.getTag().getTweakClass()) //.setArguments(Arguments.addGameArguments(Lang.get(version.getArguments()), "--tweakClass", remote.getTag().getTweakClass())) ); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java index 807f99b42..3ae5734cd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java @@ -113,7 +113,7 @@ public final class OptiFineInstallTask extends TaskResult { hasFMLTweaker = true; if (version.getArguments().isPresent()) { List game = version.getArguments().get().getGame(); - if (game.stream().anyMatch(arg -> arg.toString(Collections.EMPTY_MAP, Collections.EMPTY_MAP).contains("FMLTweaker"))) + if (game.stream().anyMatch(arg -> arg.toString(Collections.emptyMap(), Collections.emptyMap()).contains("FMLTweaker"))) hasFMLTweaker = true; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineVersionList.java index 38c2d0fa0..e993a7042 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineVersionList.java @@ -80,10 +80,13 @@ public final class OptiFineVersionList extends VersionList { if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineFile")) version = td.getTextContent(); } + if (version == null || url == null) + continue; + Matcher matcher = PATTERN.matcher(version); while (matcher.find()) gameVersion = matcher.group(1); - if (gameVersion == null || version == null || url == null) + if (gameVersion == null) continue; versions.put(gameVersion, new RemoteVersion<>(gameVersion, version, url, null)); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/Event.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/Event.java index 4013e3f5b..9f9097601 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/Event.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/Event.java @@ -62,7 +62,7 @@ public class Event extends EventObject { return false; } - private Result result; + private Result result = Result.DEFAULT; /** * Retutns the value set as the result of this event diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.java index b5cb03298..f7c761c09 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.java @@ -17,7 +17,6 @@ */ package org.jackhuang.hmcl.event; -import java.util.EventObject; import java.util.HashMap; import org.jackhuang.hmcl.task.Schedulers; @@ -29,14 +28,14 @@ public final class EventBus { private final HashMap, EventManager> events = new HashMap<>(); - public EventManager channel(Class clazz) { + public EventManager channel(Class clazz) { if (!events.containsKey(clazz)) - events.put(clazz, new EventManager<>(Schedulers.computation())); + events.put(clazz, new EventManager<>()); return (EventManager) events.get(clazz); } - public void fireEvent(EventObject obj) { - channel((Class) obj.getClass()).fireEvent(obj); + public Event.Result fireEvent(Event obj) { + return channel((Class) obj.getClass()).fireEvent(obj); } public static final EventBus EVENT_BUS = new EventBus(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java index ee0048521..eaa4716eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.event; import java.util.EnumMap; -import java.util.EventObject; import java.util.HashSet; import java.util.function.Consumer; import org.jackhuang.hmcl.task.Scheduler; @@ -29,22 +28,13 @@ import org.jackhuang.hmcl.util.SimpleMultimap; * * @author huangyuhui */ -public final class EventManager { +public final class EventManager { - private final Scheduler scheduler; private final SimpleMultimap> handlers = new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), HashSet::new); private final SimpleMultimap handlers2 = new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), HashSet::new); - public EventManager() { - this(Schedulers.immediate()); - } - - public EventManager(Scheduler scheduler) { - this.scheduler = scheduler; - } - public void register(Consumer consumer) { register(consumer, EventPriority.NORMAL); } @@ -71,15 +61,18 @@ public final class EventManager { handlers2.removeValue(runnable); } - public void fireEvent(T event) { - scheduler.schedule(() -> { - for (EventPriority priority : EventPriority.values()) { - for (Consumer handler : handlers.get(priority)) - handler.accept(event); - for (Runnable runnable : handlers2.get(priority)) - runnable.run(); - } - }); + public Event.Result fireEvent(T event) { + for (EventPriority priority : EventPriority.values()) { + for (Consumer handler : handlers.get(priority)) + handler.accept(event); + for (Runnable runnable : handlers2.get(priority)) + runnable.run(); + } + + if (event.hasResult()) + return event.getResult(); + else + return Event.Result.DEFAULT; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/FailedEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/FailedEvent.java index 5b1df9c16..7fe1676b3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/FailedEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/FailedEvent.java @@ -23,7 +23,7 @@ import java.util.EventObject; * * @author huang */ -public class FailedEvent extends EventObject { +public class FailedEvent extends Event { private final int failedTime; private T newResult; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/GameJsonParseFailedEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/GameJsonParseFailedEvent.java new file mode 100644 index 000000000..44faf1cc7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/GameJsonParseFailedEvent.java @@ -0,0 +1,52 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.event; + +import java.io.File; + +/** + * This event gets fired when json of a game version is malformed. You can do something here. + * auto making up for the missing json, don't forget to set result to {@link Event.Result#ALLOW}. + * and even asking for removing the redundant version folder. + * + * The result ALLOW means you have corrected the json. + */ +public final class GameJsonParseFailedEvent extends Event { + private final String version; + private final File jsonFile; + + /** + * + * @param source {@link org.jackhuang.hmcl.game.DefaultGameRepository} + * @param jsonFile the minecraft.json file. + * @param version the version name + */ + public GameJsonParseFailedEvent(Object source, File jsonFile, String version) { + super(source); + this.version = version; + this.jsonFile = jsonFile; + } + + public File getJsonFile() { + return jsonFile; + } + + public String getVersion() { + return version; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/JVMLaunchFailedEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/JVMLaunchFailedEvent.java index 0e6f3ea09..5027ef5af 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/JVMLaunchFailedEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/JVMLaunchFailedEvent.java @@ -23,16 +23,20 @@ import org.jackhuang.hmcl.util.ManagedProcess; /** * This event gets fired when we launch the JVM and it got crashed. *
- * This event is fired on the [org.jackhuang.hmcl.event.EVENT_BUS] + * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS} * - * @param source [org.jackhuang.hmcl.launch.ExitWaiter] - * @param value the crashed process. * @author huangyuhui */ -public class JVMLaunchFailedEvent extends EventObject { +public class JVMLaunchFailedEvent extends Event { private final ManagedProcess process; + /** + * Constructor. + * + * @param source {@link org.jackhuang.hmcl.launch.ExitWaiter} + * @param process the crashed process. + */ public JVMLaunchFailedEvent(Object source, ManagedProcess process) { super(source); this.process = process; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/LoadedOneVersionEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/LoadedOneVersionEvent.java index bafca608a..a91c84668 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/LoadedOneVersionEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/LoadedOneVersionEvent.java @@ -17,28 +17,37 @@ */ package org.jackhuang.hmcl.event; +import org.jackhuang.hmcl.game.Version; + import java.util.EventObject; /** * This event gets fired when a minecraft version has been loaded. *
- * This event is fired on the {@link org.jackhuang.hmcl.event.EVENT_BUS} - * - * @param source {@link org.jackhuang.hmcl.game.GameRepository} - * @param version the version id. + * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS} * * @author huangyuhui */ -public final class LoadedOneVersionEvent extends EventObject { +public final class LoadedOneVersionEvent extends Event { - private final String version; + private final Version version; - public LoadedOneVersionEvent(Object source, String version) { + /** + * + * @param source {@link org.jackhuang.hmcl.game.GameRepository} + * @param version the version id. + */ + public LoadedOneVersionEvent(Object source, Version version) { super(source); this.version = version; } - public String getVersion() { + public Version getVersion() { return version; } + + @Override + public boolean hasResult() { + return true; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessExitedAbnormallyEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessExitedAbnormallyEvent.java index e0bb416e2..7f55d7289 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessExitedAbnormallyEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessExitedAbnormallyEvent.java @@ -23,16 +23,20 @@ import org.jackhuang.hmcl.util.ManagedProcess; /** * This event gets fired when a JavaProcess exited abnormally and the exit code is not zero. *

- * This event is fired on the {@link org.jackhuang.hmcl.event.EVENT_BUS} + * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS} * - * @param source {@link org.jackhuang.hmcl.launch.ExitWaiter} - * @param value The process that exited abnormally. * @author huangyuhui */ -public final class ProcessExitedAbnormallyEvent extends EventObject { +public final class ProcessExitedAbnormallyEvent extends Event { private final ManagedProcess process; + /** + * Constructor. + * + * @param source {@link org.jackhuang.hmcl.launch.ExitWaiter} + * @param process The process that exited abnormally. + */ public ProcessExitedAbnormallyEvent(Object source, ManagedProcess process) { super(source); this.process = process; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessStoppedEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessStoppedEvent.java index 1e7fbf277..c56456a1e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessStoppedEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessStoppedEvent.java @@ -23,16 +23,20 @@ import org.jackhuang.hmcl.util.ManagedProcess; /** * This event gets fired when minecraft process exited successfully and the exit code is 0. *
- * This event is fired on the {@link org.jackhuang.hmcl.event.EVENT_BUS} + * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS} * - * @param source {@link org.jackhuang.hmcl.launch.ExitWaiter} - * @param value minecraft process * @author huangyuhui */ -public class ProcessStoppedEvent extends EventObject { +public class ProcessStoppedEvent extends Event { private final ManagedProcess process; + /** + * Constructor. + * + * @param source {@link org.jackhuang.hmcl.launch.ExitWaiter} + * @param process minecraft process + */ public ProcessStoppedEvent(Object source, ManagedProcess process) { super(source); this.process = process; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshedVersionsEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshedVersionsEvent.java index b6acba78c..00bad8d3d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshedVersionsEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshedVersionsEvent.java @@ -22,14 +22,17 @@ import java.util.EventObject; /** * This event gets fired when all the versions in .minecraft folder are loaded. *
- * This event is fired on the {@link org.jackhuang.hmcl.api.HMCLApi#EVENT_BUS} - * - * @param source {@link org.jackhuang.hmcl.game.GameRepository] + * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS} * * @author huangyuhui */ -public final class RefreshedVersionsEvent extends EventObject { +public final class RefreshedVersionsEvent extends Event { + /** + * Constructor. + * + * @param source {@link org.jackhuang.hmcl.game.GameRepository] + */ public RefreshedVersionsEvent(Object source) { super(source); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshingVersionsEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshingVersionsEvent.java index b46555644..bff041e22 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshingVersionsEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshingVersionsEvent.java @@ -22,14 +22,17 @@ import java.util.EventObject; /** * This event gets fired when loading versions in a .minecraft folder. *
- * This event is fired on the {@link org.jackhuang.hmcl.event.EVENT_BUS} - * - * @param source {@link org.jackhuang.hmcl.game.GameRepository} + * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS} * * @author huangyuhui */ -public final class RefreshingVersionsEvent extends EventObject { +public final class RefreshingVersionsEvent extends Event { + /** + * Constructor. + * + * @param source {@link org.jackhuang.hmcl.game.GameRepository} + */ public RefreshingVersionsEvent(Object source) { super(source); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Arguments.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Arguments.java index 50fc7be22..f22b0d083 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Arguments.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Arguments.java @@ -50,11 +50,11 @@ public final class Arguments { } public List getGame() { - return game == null ? Collections.EMPTY_LIST : Collections.unmodifiableList(game); + return game == null ? Collections.emptyList() : Collections.unmodifiableList(game); } public List getJvm() { - return jvm == null ? Collections.EMPTY_LIST : Collections.unmodifiableList(jvm); + return jvm == null ? Collections.emptyList() : Collections.unmodifiableList(jvm); } public static Arguments addGameArguments(Arguments arguments, String... gameArguments) { @@ -83,7 +83,7 @@ public final class Arguments { } public static List parseArguments(List arguments, Map keys) { - return parseArguments(arguments, keys, Collections.EMPTY_MAP); + return parseArguments(arguments, keys, Collections.emptyMap()); } public static List parseArguments(List arguments, Map keys, Map features) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetIndex.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetIndex.java index dbc30de9d..b01c3d5c8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetIndex.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetIndex.java @@ -35,7 +35,7 @@ public final class AssetIndex { private final Map objects; public AssetIndex() { - this(false, Collections.EMPTY_MAP); + this(false, Collections.emptyMap()); } public AssetIndex(boolean virtual, Map objects) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index f2700f2dc..e4b3af5eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -21,16 +21,11 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; import java.io.File; import java.io.IOException; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.TreeMap; +import java.util.*; import java.util.logging.Level; -import org.jackhuang.hmcl.event.EventBus; -import org.jackhuang.hmcl.event.LoadedOneVersionEvent; -import org.jackhuang.hmcl.event.RefreshedVersionsEvent; -import org.jackhuang.hmcl.event.RefreshingVersionsEvent; + +import org.jackhuang.hmcl.event.*; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.Lang; @@ -94,7 +89,7 @@ public class DefaultGameRepository implements GameRepository { @Override public File getVersionJar(Version version) { Version v = version.resolve(this); - String id = Lang.nonNull(v.getJar(), v.getId()); + String id = Optional.ofNullable(v.getJar()).orElse(v.getId()); return new File(getVersionRoot(id), id + ".jar"); } @@ -184,9 +179,15 @@ public class DefaultGameRepository implements GameRepository { version = Objects.requireNonNull(readVersionJson(json)); } catch (Exception e) { // JsonSyntaxException or IOException or NullPointerException(!!) - // TODO: auto making up for the missing json - // TODO: and even asking for removing the redundant version folder. - continue; + if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW) + continue; + + try { + version = Objects.requireNonNull(readVersionJson(json)); + } catch (Exception e2) { + Logging.LOG.log(Level.SEVERE, "User corrected version json is still malformed"); + continue; + } } if (!id.equals(version.getId())) { @@ -199,18 +200,20 @@ public class DefaultGameRepository implements GameRepository { } } - versions.put(id, version); - EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, id)); + if (EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, version)) != Event.Result.DENY) + versions.put(id, version); } loaded = true; } @Override - public final void refreshVersions() { + public void refreshVersions() { EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)); - refreshVersionsImpl(); - EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this)); + Schedulers.newThread().schedule(() -> { + refreshVersionsImpl(); + EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this)); + }); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java index 6885de35d..8c12b64be 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java @@ -29,6 +29,8 @@ import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.List; import java.util.Map; +import java.util.Optional; + import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.OperatingSystem; @@ -99,7 +101,7 @@ public class Library { + (this.classifier == null ? "" : "-" + this.classifier) + ".jar"; download = new LibraryDownloadInfo(path, - Lang.nonNull(Lang.nonNull(temp != null ? temp.getUrl() : null), Lang.nonNull(url, Constants.DEFAULT_LIBRARY_URL) + path), + Optional.ofNullable(temp).map(LibraryDownloadInfo::getUrl).orElse(Optional.ofNullable(url).orElse(Constants.DEFAULT_LIBRARY_URL) + path), temp != null ? temp.getSha1() : null, temp != null ? temp.getSize() : 0 ); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/RuledArgument.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/RuledArgument.java index f97a54c2b..6636b6ba2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/RuledArgument.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/RuledArgument.java @@ -75,7 +75,7 @@ public class RuledArgument implements Argument { .map(StringArgument::new) .map(str -> str.toString(keys, features).get(0)) .collect(Collectors.toList()); - return Collections.EMPTY_LIST; + return Collections.emptyList(); } public static class Serializer implements JsonSerializer, JsonDeserializer { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java index 94b894646..b1f4e165b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java @@ -155,8 +155,6 @@ public class Version implements Comparable, Validation { /** * Resolve given version - * - * @throws CircleDependencyException */ public Version resolve(VersionProvider provider) { return resolve(provider, new HashSet<>()); @@ -231,10 +229,7 @@ public class Version implements Comparable, Validation { @Override public boolean equals(Object obj) { - if (obj instanceof Version) - return Objects.equals(id, ((Version) obj).id); - else - return false; + return obj instanceof Version && Objects.equals(id, ((Version) obj).id); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index 55cfe58be..1a8be2e79 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -22,11 +22,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; import org.jackhuang.hmcl.auth.AuthInfo; @@ -207,7 +203,7 @@ public class DefaultLauncher extends Launcher { .filter(it -> !getForbiddens().containsKey(it) || !getForbiddens().get(it).get()) .collect(Collectors.toList()); } -//http://jenkins.liteloader.com/job/LiteLoader%201.12.2/lastSuccessfulBuild/artifact/build/libs/liteloader-1.12.2-SNAPSHOT-release.jar + public Map getFeatures() { return Collections.singletonMap( "has_custom_resolution", @@ -251,14 +247,14 @@ public class DefaultLauncher extends Launcher { false); } - public Map getConfigurations() { + protected Map getConfigurations() { return Lang.mapOf( new Pair<>("${auth_player_name}", authInfo.getUsername()), new Pair<>("${auth_session}", authInfo.getAuthToken()), new Pair<>("${auth_access_token}", authInfo.getAuthToken()), new Pair<>("${auth_uuid}", authInfo.getUserId()), - new Pair<>("${version_name}", Lang.nonNull(options.getVersionName(), version.getId())), - new Pair<>("${profile_name}", Lang.nonNull(options.getProfileName(), "Minecraft")), + new Pair<>("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())), + new Pair<>("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")), new Pair<>("${version_type}", version.getType().getId()), new Pair<>("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()), new Pair<>("${user_type}", authInfo.getUserType().toString().toLowerCase()), @@ -275,11 +271,8 @@ public class DefaultLauncher extends Launcher { decompressNatives(); - if (StringUtils.isNotBlank(options.getPrecalledCommand())) { - Process process = Runtime.getRuntime().exec(options.getPrecalledCommand()); - if (process.isAlive()) - process.waitFor(); - } + if (StringUtils.isNotBlank(options.getPrecalledCommand())) + Runtime.getRuntime().exec(options.getPrecalledCommand()).waitFor(); builder.directory(repository.getRunDirectory(version.getId())) .environment().put("APPDATA", options.getGameDir().getAbsoluteFile().getParent()); @@ -372,7 +365,7 @@ public class DefaultLauncher extends Launcher { private void startMonitorsWithoutLoggingInfo(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) { processListener.setProcess(managedProcess); Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> { - processListener.onLog(it + OperatingSystem.LINE_SEPARATOR, Lang.nonNull(Log4jLevel.guessLevel(it), Log4jLevel.INFO)); + processListener.onLog(it + OperatingSystem.LINE_SEPARATOR, Optional.ofNullable(Log4jLevel.guessLevel(it)).orElse(Log4jLevel.INFO)); managedProcess.addLine(it); }), "stdout-pump", isDaemon); managedProcess.addRelatedThread(stdout); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Log4jHandler.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Log4jHandler.java index 719ee01bd..0aae2b3a4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Log4jHandler.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Log4jHandler.java @@ -17,11 +17,9 @@ */ package org.jackhuang.hmcl.launch; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; +import java.io.*; import java.util.Date; +import java.util.Optional; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; @@ -55,6 +53,7 @@ final class Log4jHandler extends Thread { public Log4jHandler(BiConsumer callback) { this.callback = callback; + newLine(""); reader = Lang.invoke(() -> XMLReaderFactory.createXMLReader()); reader.setContentHandler(new Log4jHandlerImpl()); @@ -63,7 +62,6 @@ final class Log4jHandler extends Thread { @Override public void run() { setName("log4j-handler"); - newLine(""); try { reader.parse(new InputSource(inputStream)); @@ -88,17 +86,14 @@ final class Log4jHandler extends Thread { }).get()); } - public Future newLine(String content) { + public Future newLine(String log) { return Schedulers.computation().schedule(() -> { - String log = content; - if (!log.trim().startsWith("<")) - log = "", "") + "]]>"; - outputStream.write((log + OperatingSystem.LINE_SEPARATOR) + byte[] bytes = (log + OperatingSystem.LINE_SEPARATOR) .replace("log4j:Event", "log4j_Event") .replace("log4j:Message", "log4j_Message") .replace("log4j:Throwable", "log4j_Throwable") - .getBytes() - ); + .getBytes(); + outputStream.write(bytes); outputStream.flush(); }); } @@ -155,7 +150,7 @@ final class Log4jHandler extends Thread { if (readingMessage) message.append(line).append(OperatingSystem.LINE_SEPARATOR); else - callback.accept(line, Lang.nonNull(Log4jLevel.guessLevel(line), Log4jLevel.INFO)); + callback.accept(line, Optional.ofNullable(Log4jLevel.guessLevel(line)).orElse(Log4jLevel.INFO)); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java index fa22adbf1..d511a0b82 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java @@ -49,7 +49,7 @@ public final class CurseInstallTask extends Task { * @param zipFile the CurseForge modpack file. * @param manifest The manifest content of given CurseForge modpack. * @param name the new version name - * @see readCurseForgeModpackManifest + * @see CurseManifest#readCurseForgeModpackManifest */ public CurseInstallTask(DefaultDependencyManager dependencyManager, File zipFile, CurseManifest manifest, String name) { this.dependencyManager = dependencyManager; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseManifest.java index b504938fb..893b99061 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseManifest.java @@ -23,6 +23,8 @@ import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.Optional; + import org.jackhuang.hmcl.util.CompressingUtils; import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Immutable; @@ -122,7 +124,7 @@ public final class CurseManifest { if (manifest == null) throw new JsonParseException("`manifest.json` not found. Not a valid Curse modpack."); return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(), - Lang.nonNull(CompressingUtils.readTextZipEntryQuietly(f, "modlist.html"), "No description"), manifest); + Optional.ofNullable(CompressingUtils.readTextZipEntryQuietly(f, "modlist.html")).orElse( "No description"), manifest); } public static final String MINECRAFT_MODPACK = "minecraftModpack"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCInstanceConfiguration.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCInstanceConfiguration.java index e823ece65..97172583c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCInstanceConfiguration.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCInstanceConfiguration.java @@ -20,6 +20,7 @@ package org.jackhuang.hmcl.mod; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.Optional; import java.util.Properties; import org.apache.commons.compress.archivers.zip.ZipFile; @@ -83,7 +84,7 @@ public final class MultiMCInstanceConfiguration { showConsoleOnError = Boolean.parseBoolean(p.getProperty("ShowConsoleOnError")); wrapperCommand = p.getProperty("WrapperCommand"); name = defaultName; - notes = Lang.nonNull(p.getProperty("notes"), ""); + notes = Optional.ofNullable(p.getProperty("notes")).orElse(""); } /** diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/SimpleTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/SimpleTask.java index 618b1b777..aeb6a6221 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/SimpleTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/SimpleTask.java @@ -19,6 +19,7 @@ package org.jackhuang.hmcl.task; import java.util.function.Consumer; import org.jackhuang.hmcl.util.AutoTypingMap; +import org.jackhuang.hmcl.util.ExceptionalConsumer; /** * @@ -26,16 +27,18 @@ import org.jackhuang.hmcl.util.AutoTypingMap; */ class SimpleTask extends Task { - private final Consumer> consumer; + private final ExceptionalConsumer, ?> consumer; private final Scheduler scheduler; - public SimpleTask(Consumer> consumer) { + public SimpleTask(ExceptionalConsumer, ?> consumer) { this(consumer, Schedulers.defaultScheduler()); } - public SimpleTask(Consumer> consumer, Scheduler scheduler) { + public SimpleTask(ExceptionalConsumer, ?> consumer, Scheduler scheduler) { this.consumer = consumer; this.scheduler = scheduler; + + setName(consumer.toString()); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java index 18b502d31..257912949 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -29,6 +29,8 @@ import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringWrapper; import org.jackhuang.hmcl.util.AutoTypingMap; import org.jackhuang.hmcl.event.EventManager; +import org.jackhuang.hmcl.util.ExceptionalConsumer; +import org.jackhuang.hmcl.util.ExceptionalRunnable; import org.jackhuang.hmcl.util.Properties; /** @@ -204,11 +206,11 @@ public abstract class Task { return executor; } - public final TaskExecutor subscribe(Scheduler scheduler, Consumer> closure) { + public final TaskExecutor subscribe(Scheduler scheduler, ExceptionalConsumer, ?> closure) { return subscribe(of(closure, scheduler)); } - public final TaskExecutor subscribe(Consumer> closure) { + public final TaskExecutor subscribe(ExceptionalConsumer, ?> closure) { return subscribe(of(closure)); } @@ -228,15 +230,15 @@ public abstract class Task { return new CoupleTask<>(this, b, false); } - public static Task of(Runnable runnable) { + public static Task of(ExceptionalRunnable runnable) { return of(s -> runnable.run()); } - public static Task of(Consumer> closure) { + public static Task of(ExceptionalConsumer, ?> closure) { return of(closure, Schedulers.defaultScheduler()); } - public static Task of(Consumer> closure, Scheduler scheduler) { + public static Task of(ExceptionalConsumer, ?> closure, Scheduler scheduler) { return new SimpleTask(closure, scheduler); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskEvent.java index f4ed102dc..7ee613a80 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskEvent.java @@ -17,13 +17,13 @@ */ package org.jackhuang.hmcl.task; -import java.util.EventObject; +import org.jackhuang.hmcl.event.Event; /** * * @author huang */ -public class TaskEvent extends EventObject { +public class TaskEvent extends Event { private final Task task; private final boolean failed; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ExceptionalConsumer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ExceptionalConsumer.java new file mode 100644 index 000000000..158fae297 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ExceptionalConsumer.java @@ -0,0 +1,25 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.util; + +/** + * @author huangyuhui + */ +public interface ExceptionalConsumer { + void accept(T t) throws E; +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 81c1c35ef..917ff6ca5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -59,7 +59,7 @@ public final class Lang { * * @param type of argument. * @param type of result. - * @param supplier your method. + * @param function your method. * @return the result of the method to invoke. */ public static R invoke(ExceptionalFunction function, T t) { @@ -190,19 +190,19 @@ public final class Lang { return convert(map.get(key), clazz, defaultValue); } - public static List merge(Collection... collections) { + public static List merge(Collection a, Collection b) { LinkedList result = new LinkedList<>(); - for (Collection collection : collections) - if (collection != null) - result.addAll(collection); + result.addAll(a); + result.addAll(b); return result; } - public static T nonNull(T... t) { - for (T a : t) - if (a != null) - return a; - return null; + public static List merge(Collection a, Collection b, Collection c) { + LinkedList result = new LinkedList<>(); + result.addAll(a); + result.addAll(b); + result.addAll(c); + return result; } public static Thread thread(Runnable runnable) { @@ -223,10 +223,6 @@ public final class Lang { return thread; } - public static T get(Optional optional) { - return optional.isPresent() ? optional.get() : null; - } - public static Iterator asIterator(Enumeration enumeration) { return new Iterator() { @Override @@ -244,4 +240,12 @@ public final class Lang { public static Iterable asIterable(Enumeration enumeration) { return () -> asIterator(enumeration); } + + public static int parseInt(String string, int defaultValue) { + try { + return Integer.parseInt(string); + } catch (NumberFormatException e) { + return defaultValue; + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/OperatingSystem.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/OperatingSystem.java index 24ee1c16a..4012e6213 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/OperatingSystem.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/OperatingSystem.java @@ -65,12 +65,12 @@ public enum OperatingSystem { /** * The total memory/MB this computer have. */ - public static final long TOTAL_MEMORY; + public static final int TOTAL_MEMORY; /** * The suggested memory size/MB for Minecraft to allocate. */ - public static final long SUGGESTED_MEMORY; + public static final int SUGGESTED_MEMORY; public static final String PATH_SEPARATOR = File.pathSeparator; public static final String FILE_SEPARATOR = File.separator; @@ -104,11 +104,11 @@ public enum OperatingSystem { Object bytes = ReflectionHelper.call(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize"); if (bytes instanceof Long) - TOTAL_MEMORY = ((Long) bytes) / 1024 / 1024; + TOTAL_MEMORY = (int) (((Long) bytes) / 1024 / 1024); else TOTAL_MEMORY = 1024; - SUGGESTED_MEMORY = Math.round(1.0 * TOTAL_MEMORY / 4.0 / 128.0) * 128; + SUGGESTED_MEMORY = (int) (Math.round(1.0 * TOTAL_MEMORY / 4.0 / 128.0) * 128); String arch = System.getProperty("sun.arch.data.model"); if (arch == null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/VersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/VersionNumber.java index ce1d43abf..7f277d0e2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/VersionNumber.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/VersionNumber.java @@ -29,7 +29,7 @@ public abstract class VersionNumber implements Comparable { /** * @throws IllegalArgumentException if there are some characters excluding digits and dots. - * @param version + * @param version version string in form x.x.x * @return the int version number */ public static IntVersionNumber asIntVersionNumber(String version) {