From 41498b5d93de783ff0c7dc1b0881c144e3b10b4d Mon Sep 17 00:00:00 2001 From: huangyuhui Date: Wed, 2 Aug 2017 00:33:24 +0800 Subject: [PATCH] UI works --- .../main/java/org/jackhuang/hmcl/Events.kt | 43 +++++ .../jackhuang/hmcl/game/HMCLGameRepository.kt | 65 +++++++- .../java/org/jackhuang/hmcl/setting/Config.kt | 123 +++++++++++++++ .../org/jackhuang/hmcl/setting/Profile.kt | 117 ++++++++++++++ .../org/jackhuang/hmcl/setting/Settings.kt | 148 +++++++++++++++++- .../jackhuang/hmcl/setting/VersionSetting.kt | 30 ++-- .../org/jackhuang/hmcl/ui/MainController.kt | 88 ++++++----- .../jackhuang/hmcl/ui/VersionController.kt | 4 +- .../org/jackhuang/hmcl/ui/VersionListItem.kt | 4 +- .../hmcl/download/DefaultDependencyManager.kt | 2 +- .../java/org/jackhuang/hmcl/event/EventBus.kt | 4 +- .../org/jackhuang/hmcl/event/EventManager.kt | 31 ++-- .../java/org/jackhuang/hmcl/event/Events.kt | 51 ++++++ .../hmcl/game/DefaultGameRepository.kt | 15 +- .../org/jackhuang/hmcl/game/GameRepository.kt | 8 + .../org/jackhuang/hmcl/game/GameVersion.kt | 139 ++++++++++++++++ .../main/java/org/jackhuang/hmcl/util/Gson.kt | 24 +++ .../org/jackhuang/hmcl/util/JavaVersion.kt | 6 +- .../java/org/jackhuang/hmcl/util/Platform.kt | 23 ++- .../org/jackhuang/hmcl/util/SimpleMultimap.kt | 8 +- 20 files changed, 854 insertions(+), 79 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/Events.kt create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.kt create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.kt create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/event/Events.kt create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.kt diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Events.kt b/HMCL/src/main/java/org/jackhuang/hmcl/Events.kt new file mode 100644 index 000000000..38ab439b0 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Events.kt @@ -0,0 +1,43 @@ +/* + * 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 + +import org.jackhuang.hmcl.setting.Profile +import java.util.EventObject + +/** + * This event gets fired when the selected profile changed. + *

+ * This event is fired on the [org.jackhuang.hmcl.event.EVENT_BUS] + * @param source [org.jackhuang.hmcl.setting.Settings] + * * + * @param Profile the new profile. + * * + * @author huangyuhui + */ +class ProfileChangedEvent(source: Any, val value: Profile) : EventObject(source) + +/** + * This event gets fired when loading profiles. + *

+ * This event is fired on the [org.jackhuang.hmcl.event.EVENT_BUS] + * @param source [org.jackhuang.hmcl.setting.Settings] + * * + * @author huangyuhui + */ +class ProfileLoadingEvent(source: Any) : EventObject(source) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.kt b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.kt index 895140fc3..811c22dfa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.kt @@ -17,7 +17,12 @@ */ package org.jackhuang.hmcl.game +import com.google.gson.GsonBuilder +import javafx.beans.InvalidationListener +import org.jackhuang.hmcl.setting.VersionSetting +import org.jackhuang.hmcl.util.GSON import org.jackhuang.hmcl.util.LOG +import org.jackhuang.hmcl.util.fromJson import java.io.File import java.io.IOException import java.util.logging.Level @@ -25,19 +30,69 @@ import java.util.logging.Level class HMCLGameRepository(baseDirectory: File) : DefaultGameRepository(baseDirectory) { - val PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}" + private val versionSettings = HashMap() - @Synchronized - override fun refreshVersions() { - super.refreshVersions() + override fun refreshVersionsImpl() { + versionSettings.clear() + + super.refreshVersionsImpl() + + versions.keys.forEach(this::loadVersionSetting) + + checkModpack() try { val file = baseDirectory.resolve("launcher_profiles.json") - if (!file.exists()) + 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) } } + + 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] + } + + 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/java/org/jackhuang/hmcl/setting/Config.kt b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.kt new file mode 100644 index 000000000..8a7a9110a --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.kt @@ -0,0 +1,123 @@ +/* + * 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.annotations.SerializedName +import org.jackhuang.hmcl.MainApplication +import org.jackhuang.hmcl.util.JavaVersion +import java.io.File +import java.util.TreeMap + +class Config { + @SerializedName("last") + var last: String = "" + set(value) { + field = value + Settings.save() + } + @SerializedName("bgpath") + var bgpath: String? = null + set(value) { + field = value + Settings.save() + } + @SerializedName("commonpath") + var commonpath: File = MainApplication.getMinecraftDirectory() + set(value) { + field = value + Settings.save() + } + @SerializedName("proxyHost") + var proxyHost: String? = null + set(value) { + field = value + Settings.save() + } + @SerializedName("proxyPort") + var proxyPort: String? = null + set(value) { + field = value + Settings.save() + } + @SerializedName("proxyUserName") + var proxyUserName: String? = null + set(value) { + field = value + Settings.save() + } + @SerializedName("proxyPassword") + var proxyPassword: String? = null + set(value) { + field = value + Settings.save() + } + @SerializedName("theme") + var theme: String? = null + set(value) { + field = value + Settings.save() + } + @SerializedName("java") + var java: List? = null + set(value) { + field = value + Settings.save() + } + @SerializedName("localization") + var localization: String? = null + set(value) { + field = value + Settings.save() + } + @SerializedName("downloadtype") + var downloadtype: Int = 0 + set(value) { + field = value + Settings.save() + } + @SerializedName("configurations") + var configurations: MutableMap = TreeMap() + set(value) { + field = value + Settings.save() + } + @SerializedName("accounts") + var accounts: MutableMap> = TreeMap() + set(value) { + field = value + Settings.save() + } + @SerializedName("fontFamily") + var fontFamily: String? = null + set(value) { + field = value + Settings.save() + } + @SerializedName("fontSize") + var fontSize: Int = 12 + set(value) { + field = value + Settings.save() + } + @SerializedName("logLines") + var logLines: Int = 100 + set(value) { + field = value + Settings.save() + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.kt b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.kt new file mode 100644 index 000000000..d1e7ef07e --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.kt @@ -0,0 +1,117 @@ +/* + * 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 javafx.beans.property.* +import org.jackhuang.hmcl.game.HMCLGameRepository +import org.jackhuang.hmcl.util.* +import java.io.File +import java.lang.reflect.Type + +class Profile(var name: String = "Default", gameDir: File = File(".minecraft")) { + val globalProperty = SimpleObjectProperty(this, "global", VersionSetting()) + var global: VersionSetting by globalProperty + + val selectedVersionProperty = SimpleStringProperty(this, "selectedVersion", "") + var selectedVersion: String by selectedVersionProperty + + val gameDirProperty = SimpleObjectProperty(this, "gameDir", gameDir) + var gameDir: File by gameDirProperty + + val noCommonProperty = SimpleBooleanProperty(this, "noCommon", false) + var noCommon: Boolean by noCommonProperty + + var repository = HMCLGameRepository(gameDir) + + init { + gameDirProperty.addListener { _, _, newValue -> + repository.baseDirectory = newValue + repository.refreshVersions() + } + + selectedVersionProperty.addListener { _, _, newValue -> + if (newValue.isNotBlank() && !repository.hasVersion(newValue)) { + val newVersion = repository.getVersions().firstOrNull() + // will cause anthor change event, we must insure that there will not be dead recursion. + selectedVersion = newVersion?.id ?: "" + } + } + } + + fun specializeVersionSetting(id: String) { + var vs = repository.getVersionSetting(id) + if (vs == null) + vs = repository.createVersionSetting(id) ?: return + vs.usesGlobal = false + } + + 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 = + getVersionSetting(selectedVersion) + + fun addPropertyChangedListener(listener: InvalidationListener) { + globalProperty.addListener(listener) + selectedVersionProperty.addListener(listener) + gameDirProperty.addListener(listener) + noCommonProperty.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) + addProperty("noCommon", src.noCommon) + } + + 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(gameDir = File(json["gameDir"]?.asString ?: "")).apply { + global = context.deserialize(json["global"], VersionSetting::class.java) + selectedVersion = json["selectedVersion"]?.asString ?: "" + noCommon = json["noCommon"]?.asBoolean ?: false + } + } + + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.kt b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.kt index 7ac8bbab4..524e7354c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.kt @@ -17,8 +17,154 @@ */ package org.jackhuang.hmcl.setting +import com.google.gson.GsonBuilder +import javafx.beans.InvalidationListener +import java.io.IOException +import org.jackhuang.hmcl.MainApplication +import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider +import org.jackhuang.hmcl.download.DownloadProvider +import org.jackhuang.hmcl.download.MojangDownloadProvider +import org.jackhuang.hmcl.util.GSON +import org.jackhuang.hmcl.util.LOG +import java.io.File +import java.util.logging.Level +import org.jackhuang.hmcl.ProfileLoadingEvent +import org.jackhuang.hmcl.ProfileChangedEvent +import org.jackhuang.hmcl.event.EVENT_BUS +import org.jackhuang.hmcl.util.FileTypeAdapter + + object Settings { + val GSON = GsonBuilder() + .registerTypeAdapter(VersionSetting::class.java, VersionSetting) + .registerTypeAdapter(Profile::class.java, Profile) + .registerTypeAdapter(File::class.java, FileTypeAdapter) + .setPrettyPrinting().create() + + val DEFAULT_PROFILE = "Default" + val HOME_PROFILE = "Home" + + val SETTINGS_FILE = File("hmcl.json").absoluteFile + + val SETTINGS: Config + init { - + SETTINGS = initSettings(); + save() + + if (!getProfiles().containsKey(DEFAULT_PROFILE)) + getProfiles().put(DEFAULT_PROFILE, Profile()); + + for ((name, profile) in getProfiles().entries) { + profile.name = name + profile.addPropertyChangedListener(InvalidationListener { save() }) + } + } + + fun getDownloadProvider(): DownloadProvider = when (SETTINGS.downloadtype) { + 0 -> MojangDownloadProvider + 1 -> BMCLAPIDownloadProvider + else -> MojangDownloadProvider + } + + private fun initSettings(): Config { + var c = Config() + if (SETTINGS_FILE.exists()) + try { + val str = SETTINGS_FILE.readText() + if (str.trim() == "") + LOG.finer("Settings file is empty, use the default settings.") + else { + val d = GSON.fromJson(str, Config::class.java) + if (d != null) + c = d + } + LOG.finest("Initialized settings.") + } catch (e: Exception) { + LOG.log(Level.WARNING, "Something happened wrongly when load settings.", e) + } + else { + LOG.config("No settings file here, may be first loading.") + if (!c.configurations.containsKey(HOME_PROFILE)) + c.configurations[HOME_PROFILE] = Profile(HOME_PROFILE, MainApplication.getMinecraftDirectory()) + } + return c + } + + fun save() { + try { + SETTINGS_FILE.writeText(GSON.toJson(SETTINGS)) + } catch (ex: IOException) { + LOG.log(Level.SEVERE, "Failed to save config", ex) + } + } + + fun getLastProfile(): Profile { + if (!hasProfile(SETTINGS.last)) + SETTINGS.last = DEFAULT_PROFILE + return getProfile(SETTINGS.last) + } + + fun getProfile(name: String?): Profile { + var p: Profile? = getProfiles()[name ?: DEFAULT_PROFILE] + if (p == null) + if (getProfiles().containsKey(DEFAULT_PROFILE)) + p = getProfiles()[DEFAULT_PROFILE]!! + else { + p = Profile() + getProfiles().put(DEFAULT_PROFILE, p) + } + return p + } + + fun hasProfile(name: String?): Boolean { + return getProfiles().containsKey(name ?: DEFAULT_PROFILE) + } + + fun getProfiles(): MutableMap { + return SETTINGS.configurations + } + + fun getProfilesFiltered(): Collection { + return getProfiles().values.filter { t -> t.name.isNotBlank() } + } + + fun putProfile(ver: Profile?): Boolean { + if (ver == null || ver.name.isBlank() || getProfiles().containsKey(ver.name)) + return false + getProfiles().put(ver.name, ver) + return true + } + + fun delProfile(ver: Profile): Boolean { + return delProfile(ver.name) + } + + fun delProfile(ver: String): Boolean { + if (DEFAULT_PROFILE == ver) { + return false + } + var notify = false + if (getLastProfile().name == ver) + notify = true + val flag = getProfiles().remove(ver) != null + if (notify && flag) + onProfileChanged() + return flag + } + + internal fun onProfileChanged() { + val p = getLastProfile() + EVENT_BUS.fireEvent(ProfileChangedEvent(SETTINGS, p)) + p.repository.refreshVersions() + } + + /** + * Start profiles loading process. + * Invoked by loading GUI phase. + */ + fun onProfileLoading() { + EVENT_BUS.fireEvent(ProfileLoadingEvent(SETTINGS)) + onProfileChanged() } } \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt index 32ed64045..2676d9a9e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt @@ -18,17 +18,14 @@ package org.jackhuang.hmcl.setting import com.google.gson.* +import javafx.beans.InvalidationListener import javafx.beans.property.* import org.jackhuang.hmcl.util.* import java.lang.reflect.Type class VersionSetting() { - /** - * The displayed name. - */ - val nameProperty = SimpleStringProperty(this, "name", "") - var name: String by nameProperty + var isGlobal: Boolean = false /** * HMCL Version Settings have been divided into 2 parts. @@ -162,15 +159,31 @@ class VersionSetting() { val launcherVisibilityProperty = SimpleObjectProperty(this, "launcherVisibility", LauncherVisibility.HIDE) var launcherVisibility: LauncherVisibility by launcherVisibilityProperty - val gameVersion: String - get() = "1.7.10" + fun addPropertyChangedListener(listener: InvalidationListener) { + usesGlobalProperty.addListener(listener) + javaProperty.addListener(listener) + javaDirProperty.addListener(listener) + wrapperProperty.addListener(listener) + permSizeProperty.addListener(listener) + maxMemoryProperty.addListener(listener) + precalledCommandProperty.addListener(listener) + javaArgsProperty.addListener(listener) + minecraftArgsProperty.addListener(listener) + noJVMArgsProperty.addListener(listener) + notCheckGameProperty.addListener(listener) + serverIpProperty.addListener(listener) + fullscreenProperty.addListener(listener) + widthProperty.addListener(listener) + heightProperty.addListener(listener) + gameDirTypeProperty.addListener(listener) + launcherVisibilityProperty.addListener(listener) + } 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("name", src.name) addProperty("usesGlobal", src.usesGlobal) addProperty("javaArgs", src.javaArgs) addProperty("minecraftArgs", src.minecraftArgs) @@ -197,7 +210,6 @@ class VersionSetting() { if (json == null || json == JsonNull.INSTANCE || json !is JsonObject) return null return VersionSetting().apply { - name = json["name"]?.asString ?: "" usesGlobal = json["usesGlobal"]?.asBoolean ?: false javaArgs = json["javaArgs"]?.asString ?: "" minecraftArgs = json["minecraftArgs"]?.asString ?: "" diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainController.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainController.kt index 942164c1f..d290c1d5d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainController.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainController.kt @@ -21,10 +21,17 @@ import com.jfoenix.controls.JFXButton import com.jfoenix.controls.JFXComboBox import com.jfoenix.controls.JFXListCell import com.jfoenix.controls.JFXListView +import javafx.collections.FXCollections import javafx.fxml.FXML import javafx.scene.Node import javafx.scene.layout.Pane import javafx.scene.layout.StackPane +import org.jackhuang.hmcl.ProfileChangedEvent +import org.jackhuang.hmcl.ProfileLoadingEvent +import org.jackhuang.hmcl.event.EVENT_BUS +import org.jackhuang.hmcl.event.RefreshedVersionsEvent +import org.jackhuang.hmcl.game.minecraftVersion +import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.VersionSetting import org.jackhuang.hmcl.ui.animation.ContainerAnimations import org.jackhuang.hmcl.ui.download.DownloadWizardProvider @@ -51,7 +58,7 @@ class MainController { */ @FXML lateinit var page: StackPane - @FXML lateinit var listVersions: JFXListView // TODO: JFXListView including icon, title, game version(if equals to title, hidden) + @FXML lateinit var listVersions: JFXListView // TODO: JFXListView including icon, title, game version(if equals to title, hidden) lateinit var animationHandler: TransitionHandler @@ -61,56 +68,23 @@ class MainController { animationHandler = TransitionHandler(page) - listVersions.items.add(VersionSetting("1")) - listVersions.items.add(VersionSetting("2")) - listVersions.items.add(VersionSetting("3")) - listVersions.items.add(VersionSetting("4")) - listVersions.items.add(VersionSetting("5")) - listVersions.items.add(VersionSetting("6")) - listVersions.items.add(VersionSetting("7")) - listVersions.items.add(VersionSetting("8")) - listVersions.items.add(VersionSetting("9")) - listVersions.items.add(VersionSetting("10")) - listVersions.items.add(VersionSetting("11")) - listVersions.items.add(VersionSetting("12")) - - listVersions.setCellFactory { - object : JFXListCell() { - override fun updateItem(item: VersionSetting?, empty: Boolean) { - super.updateItem(item, empty) - - if (item == null || empty) return - val g = VersionListItem(item, item.gameVersion) - g.onSettingsButtonClicked { - setContentPage(Controllers.versionPane) - Controllers.versionController.loadVersionSetting(g.setting) - } - graphic = g - } - } - } + EVENT_BUS.channel() += this::loadVersions + EVENT_BUS.channel() += this::onProfilesLoading + EVENT_BUS.channel() += this::onProfileChanged listVersions.setOnMouseClicked { if (it.clickCount == 2) { setContentPage(Controllers.versionPane) - Controllers.versionController.loadVersionSetting(listVersions.selectionModel.selectedItem) + val id = listVersions.selectionModel.selectedItem.id + + Controllers.versionController.loadVersionSetting(id, Settings.getLastProfile().getVersionSetting(id)) } else it.consume() } - comboProfiles.items.add("SA") - comboProfiles.items.add("SB") - comboProfiles.items.add("SC") - comboProfiles.items.add("SD") - comboProfiles.items.add("SE") - comboProfiles.items.add("SF") - comboProfiles.items.add("SG") - comboProfiles.items.add("SH") - comboProfiles.items.add("SI") - comboProfiles.items.add("SJ") - comboProfiles.items.add("SK") - listVersions.smoothScrolling() + + Settings.onProfileLoading() } private val empty = Pane() @@ -122,4 +96,34 @@ class MainController { fun installNewVersion() { setContentPage(Wizard.createWizard("Install New Game", DownloadWizardProvider())) } + + fun onProfilesLoading() { + // TODO: Profiles + } + + fun onProfileChanged(event: ProfileChangedEvent) { + val profile = event.value + profile.selectedVersionProperty.addListener { _, _, newValue -> + versionChanged(newValue) + } + } + + val versionListItems = mutableMapOf() + + fun loadVersions() { + val profile = Settings.getLastProfile() + val list = mutableListOf() + versionListItems.clear() + profile.repository.getVersions().forEach { + val item = VersionListItem(it.id, minecraftVersion(Settings.getLastProfile().repository.getVersionJar(it.id)) ?: "Unknown") + list += item + versionListItems += it.id to item + } + + listVersions.items = FXCollections.observableList(list) + } + + fun versionChanged(selectedVersion: String) { + listVersions.selectionModel.select(versionListItems[selectedVersion]) + } } \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionController.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionController.kt index 8be42e113..48dc5224f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionController.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionController.kt @@ -59,8 +59,8 @@ class VersionController { JFXScrollPane.smoothScrolling(scroll) } - fun loadVersionSetting(version: VersionSetting) { - titleLabel.text = version.name + fun loadVersionSetting(id: String, version: VersionSetting) { + titleLabel.text = id } fun onExploreJavaDir() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionListItem.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionListItem.kt index 8b01f18e0..ce77712d1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionListItem.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionListItem.kt @@ -22,7 +22,7 @@ import javafx.scene.control.Label import javafx.scene.layout.BorderPane import org.jackhuang.hmcl.setting.VersionSetting -class VersionListItem(val setting: VersionSetting, val gameVersion: String) : BorderPane() { +class VersionListItem(val versionName: String, val gameVersion: String) : BorderPane() { @FXML lateinit var lblVersionName: Label @FXML lateinit var lblGameVersion: Label @@ -31,7 +31,7 @@ class VersionListItem(val setting: VersionSetting, val gameVersion: String) : Bo init { loadFXML("/assets/fxml/version-list-item.fxml") - lblVersionName.text = setting.name + lblVersionName.text = versionName lblGameVersion.text = gameVersion } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.kt index 9ea979b2f..abdada4c5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.kt +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.kt @@ -24,7 +24,7 @@ import org.jackhuang.hmcl.task.Task import org.jackhuang.hmcl.task.then import java.net.Proxy -class DefaultDependencyManager(override val repository: DefaultGameRepository, override val downloadProvider: DownloadProvider, val proxy: Proxy = Proxy.NO_PROXY) +class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, val proxy: Proxy = Proxy.NO_PROXY) : AbstractDependencyManager(repository) { override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.kt index e8c21a085..4b0d4514a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.kt +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventBus.kt @@ -35,4 +35,6 @@ class EventBus { channel(obj.javaClass).fireEvent(obj) } -} \ No newline at end of file +} + +val EVENT_BUS = EventBus() \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.kt index f0612b4ff..0584bb470 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.kt +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.kt @@ -17,30 +17,43 @@ */ package org.jackhuang.hmcl.event +import org.jackhuang.hmcl.util.SimpleMultimap import java.util.* class EventManager { - private val handlers = EnumMap Unit>>(EventPriority::class.java).apply { - for (value in EventPriority.values()) - put(value, LinkedList<(T) -> Unit>()) - } + private val handlers = SimpleMultimap Unit>({ EnumMap(EventPriority::class.java) }, ::HashSet) + private val handlers2 = SimpleMultimap Unit>({ EnumMap(EventPriority::class.java) }, ::HashSet) fun register(func: (T) -> Unit, priority: EventPriority = EventPriority.NORMAL) { - if (!handlers[priority]!!.contains(func)) - handlers[priority]!!.add(func) + if (!handlers[priority].contains(func)) + handlers.put(priority, func) + } + + fun register(func: () -> Unit, priority: EventPriority = EventPriority.NORMAL) { + if (!handlers2[priority].contains(func)) + handlers2.put(priority, func) } fun unregister(func: (T) -> Unit) { - EventPriority.values().forEach { handlers[it]!!.remove(func) } + handlers.remove(func) + } + + fun unregister(func: () -> Unit) { + handlers2.remove(func) } fun fireEvent(event: T) { - for (priority in EventPriority.values()) - for (handler in handlers[priority]!!) + for (priority in EventPriority.values()) { + for (handler in handlers[priority]) handler(event) + for (handler in handlers2[priority]) + handler() + } } operator fun plusAssign(func: (T) -> Unit) = register(func) + operator fun plusAssign(func: () -> Unit) = register(func) operator fun minusAssign(func: (T) -> Unit) = unregister(func) + operator fun minusAssign(func: () -> Unit) = unregister(func) operator fun invoke(event: T) = fireEvent(event) } \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/Events.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/Events.kt new file mode 100644 index 000000000..7332c003f --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/Events.kt @@ -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.event + +import java.util.EventObject + +/** + * This event gets fired when loading versions in a .minecraft folder. + *

+ * This event is fired on the [org.jackhuang.hmcl.api.HMCLApi.EVENT_BUS] + * @param source [org.jackhuang.hmcl.core.version.MinecraftVersionManager] + * * + * @param IMinecraftService .minecraft folder. + * * + * @author huangyuhui + */ +class RefreshingVersionsEvent(source: Any) : EventObject(source) + +/** + * 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 [org.jackhuang.hmcl.game.GameRepository] + * @author huangyuhui + */ +class RefreshedVersionsEvent(source: Any) : EventObject(source) + +/** + * This event gets fired when a minecraft version has been loaded. + *

+ * This event is fired on the [org.jackhuang.hmcl.api.HMCLApi.EVENT_BUS] + * @param source [org.jackhuang.hmcl.core.version.MinecraftVersionManager] + * @param version the version id. + * @author huangyuhui + */ +class LoadedOneVersionEvent(source: Any, val version: String) : EventObject(source) \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.kt index a540eade9..c45f4dbd0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.kt +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.kt @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.game import com.google.gson.JsonSyntaxException +import org.jackhuang.hmcl.event.* import org.jackhuang.hmcl.util.GSON import org.jackhuang.hmcl.util.LOG import org.jackhuang.hmcl.util.fromJson @@ -27,7 +28,7 @@ import java.io.IOException import java.util.* import java.util.logging.Level -open class DefaultGameRepository(val baseDirectory: File): GameRepository { +open class DefaultGameRepository(var baseDirectory: File): GameRepository { protected val versions: MutableMap = TreeMap() override fun hasVersion(id: String) = versions.containsKey(id) @@ -81,9 +82,8 @@ open class DefaultGameRepository(val baseDirectory: File): GameRepository { return file.deleteRecursively() } + protected open fun refreshVersionsImpl() { - @Synchronized - override fun refreshVersions() { versions.clear() if (ClassicVersion.hasClassicVersion(baseDirectory)) { @@ -123,7 +123,16 @@ open class DefaultGameRepository(val baseDirectory: File): GameRepository { } versions[id] = version + EVENT_BUS.fireEvent(LoadedOneVersionEvent(this, id)) } + + } + + @Synchronized + final override fun refreshVersions() { + EVENT_BUS.fireEvent(RefreshingVersionsEvent(this)) + refreshVersionsImpl() + EVENT_BUS.fireEvent(RefreshedVersionsEvent(this)) } override fun getAssetIndex(assetId: String): AssetIndex { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.kt index 0cca97320..637606657 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.kt +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.kt @@ -95,6 +95,14 @@ interface GameRepository : VersionProvider { */ fun getVersionJar(version: Version): File + /** + * Get minecraft jar + * + * @param version version id + * @return the minecraft jar + */ + fun getVersionJar(version: String): File = getVersionJar(getVersion(version).resolve(this)) + /** * Rename given version to new name. * diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.kt new file mode 100644 index 000000000..2a331d1a4 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.kt @@ -0,0 +1,139 @@ +/* + * 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.util.closeQuietly +import org.jackhuang.hmcl.util.readFullyAsByteArray +import java.io.IOException +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.io.File + +private fun lessThan32(b: ByteArray, x: Int): Int { + var x = x + while (x < b.size) { + if (b[x] < 32) + return x + x++ + } + return -1 +} + +fun matchArray(a: ByteArray, b: ByteArray): Int { + for (i in 0..a.size - b.size - 1) { + var j = 1 + for (k in b.indices) { + if (b[k] == a[i + k]) + continue + j = 0 + break + } + if (j != 0) + return i + } + return -1 +} + +@Throws(IOException::class) +private fun getVersionOfOldMinecraft(file: ZipFile, entry: ZipEntry): String? { + val tmp = file.getInputStream(entry).readFullyAsByteArray() + + val bytes = "Minecraft Minecraft ".toByteArray(Charsets.US_ASCII) + var j = matchArray(tmp, bytes) + if (j < 0) { + return null + } + val i = j + bytes.size + j = lessThan32(tmp, i) + + if (j < 0) { + return null + } + val ver = String(tmp, i, j - i, Charsets.US_ASCII) + return ver +} + +@Throws(IOException::class) +private fun getVersionOfNewMinecraft(file: ZipFile, entry: ZipEntry): String? { + val tmp = file.getInputStream(entry).readFullyAsByteArray() + + var str = "-server.txt".toByteArray(charset("ASCII")) + var j = matchArray(tmp, str) + if (j < 0) { + return null + } + var i = j + str.size + i += 11 + j = lessThan32(tmp, i) + if (j < 0) { + return null + } + val result = String(tmp, i, j - i, Charsets.US_ASCII) + + val ch = result[0] + // 1.8.1+ + if (ch < '0' || ch > '9') { + str = "Can't keep up! Did the system time change, or is the server overloaded?".toByteArray(charset("ASCII")) + j = matchArray(tmp, str) + if (j < 0) { + return null + } + i = -1 + while (j > 0) { + if (tmp[j] in 48..57) { + i = j + break + } + j-- + } + if (i == -1) { + return null + } + var k = i + if (tmp[i + 1] >= 'a'.toInt() && tmp[i + 1] <= 'z'.toInt()) + i++ + while (tmp[k] in 48..57 || tmp[k] == '-'.toByte() || tmp[k] == '.'.toByte() || tmp[k] >= 97 && tmp[k] <= 'z'.toByte()) + k-- + k++ + return String(tmp, k, i - k + 1, Charsets.US_ASCII) + } + return result +} + +fun minecraftVersion(file: File?): String? { + if (file == null || !file.isFile || !file.canRead()) { + return null + } + var f: ZipFile? = null + try { + f = ZipFile(file) + val minecraft = f + .getEntry("net/minecraft/client/Minecraft.class") + if (minecraft != null) + return getVersionOfOldMinecraft(f, minecraft) + val main = f.getEntry("net/minecraft/client/main/Main.class") + val minecraftserver = f.getEntry("net/minecraft/server/MinecraftServer.class") + if (main != null && minecraftserver != null) + return getVersionOfNewMinecraft(f, minecraftserver) + return null + } catch (e: IOException) { + return null + } finally { + f?.closeQuietly() + } +} \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Gson.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Gson.kt index 38091e514..b43a8de55 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Gson.kt +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Gson.kt @@ -28,6 +28,7 @@ import com.google.gson.TypeAdapter import com.google.gson.Gson import com.google.gson.TypeAdapterFactory import org.jackhuang.hmcl.game.Library +import java.io.File import java.text.DateFormat import java.text.ParseException import java.text.SimpleDateFormat @@ -40,6 +41,8 @@ val GSON: Gson = GsonBuilder() .registerTypeAdapter(Library::class.java, Library) .registerTypeAdapter(Date::class.java, DateTypeAdapter) .registerTypeAdapter(UUID::class.java, UUIDTypeAdapter) + .registerTypeAdapter(Platform::class.java, Platform) + .registerTypeAdapter(File::class.java, FileTypeAdapter) .registerTypeAdapterFactory(ValidationTypeAdapterFactory) .registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory) .create() @@ -48,6 +51,14 @@ inline fun typeOf(): Type = object : TypeToken() {}.type inline fun Gson.fromJson(json: String): T? = fromJson(json, T::class.java) +inline fun Gson.fromJsonQuietly(json: String): T? { + try { + return fromJson(json) + } catch (json: JsonParseException) { + return null + } +} + /** * Check if the json object's fields automatically filled by Gson are in right format. */ @@ -189,4 +200,17 @@ object DateTypeAdapter : JsonSerializer, JsonDeserializer { return result.substring(0, 22) + ":" + result.substring(22) } } +} + +object FileTypeAdapter : JsonSerializer, JsonDeserializer { + override fun serialize(src: File?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { + if (src == null) return JsonNull.INSTANCE + else return JsonPrimitive(src.path) + } + + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): File? { + if (json == null) return null + else return File(json.asString) + } + } \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/JavaVersion.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/JavaVersion.kt index aacb1c7b5..3335ebea4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/JavaVersion.kt +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/JavaVersion.kt @@ -17,12 +17,14 @@ */ package org.jackhuang.hmcl.util +import com.google.gson.annotations.SerializedName import java.io.File import java.io.IOException import java.io.Serializable import java.util.regex.Pattern data class JavaVersion internal constructor( + @SerializedName("location") val binary: File, val version: Int, val platform: Platform) : Serializable @@ -74,8 +76,8 @@ data class JavaVersion internal constructor( } fun getJavaFile(home: File): File { - var path = home.resolve("bin") - var javaw = path.resolve("javaw.exe") + val path = home.resolve("bin") + val javaw = path.resolve("javaw.exe") if (OS.CURRENT_OS === OS.WINDOWS && javaw.isFile) return javaw else diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Platform.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Platform.kt index b9fe28039..44f64391b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Platform.kt +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Platform.kt @@ -17,12 +17,33 @@ */ package org.jackhuang.hmcl.util +import com.google.gson.* +import java.lang.reflect.Type + enum class Platform(val bit: String) { BIT_32("32"), BIT_64("64"), UNKNOWN("unknown"); - companion object { + companion object Serializer: JsonSerializer, JsonDeserializer { + + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Platform? { + if (json == null) return null + return when (json.asInt) { + 0 -> BIT_32 + 1 -> BIT_64 + else -> UNKNOWN + } + } + + override fun serialize(src: Platform?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement? { + if (src == null) return null + return when (src) { + BIT_32 -> JsonPrimitive(0) + BIT_64 -> JsonPrimitive(1) + UNKNOWN -> JsonPrimitive(-1) + } + } val PLATFORM: Platform by lazy { if (IS_64_BIT) BIT_64 else BIT_32 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.kt b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.kt index ee8832602..51dc7bc26 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.kt +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.kt @@ -40,13 +40,19 @@ class SimpleMultimap(val maper: () -> MutableMap>, val va valuesImpl += value } - fun remove(key: K): Collection? { + fun removeAll(key: K): Collection? { val result = map.remove(key) if (result != null) valuesImpl.removeAll(result) return result } + fun remove(value: V) { + map.values.forEach { + it.remove(value) + } + } + fun clear() { map.clear() valuesImpl.clear()