This commit is contained in:
huangyuhui 2017-08-02 00:33:24 +08:00
parent 3c32ef39c7
commit 41498b5d93
20 changed files with 854 additions and 79 deletions

View File

@ -0,0 +1,43 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.
* <br></br>
* 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.
* <br></br>
* 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)

View File

@ -17,7 +17,12 @@
*/ */
package org.jackhuang.hmcl.game 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.LOG
import org.jackhuang.hmcl.util.fromJson
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.logging.Level import java.util.logging.Level
@ -25,19 +30,69 @@ import java.util.logging.Level
class HMCLGameRepository(baseDirectory: File) class HMCLGameRepository(baseDirectory: File)
: DefaultGameRepository(baseDirectory) { : DefaultGameRepository(baseDirectory) {
val PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}" private val versionSettings = HashMap<String, VersionSetting>()
@Synchronized override fun refreshVersionsImpl() {
override fun refreshVersions() { versionSettings.clear()
super.refreshVersions()
super.refreshVersionsImpl()
versions.keys.forEach(this::loadVersionSetting)
checkModpack()
try { try {
val file = baseDirectory.resolve("launcher_profiles.json") val file = baseDirectory.resolve("launcher_profiles.json")
if (!file.exists()) if (!file.exists() && versions.isNotEmpty())
file.writeText(PROFILE) file.writeText(PROFILE)
} catch (ex: IOException) { } catch (ex: IOException) {
LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex) 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<VersionSetting>(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()
}
} }

View File

@ -0,0 +1,123 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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<JavaVersion>? = 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<String, Profile> = TreeMap()
set(value) {
field = value
Settings.save()
}
@SerializedName("accounts")
var accounts: MutableMap<String, 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()
}
}

View File

@ -0,0 +1,117 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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<VersionSetting>(this, "global", VersionSetting())
var global: VersionSetting by globalProperty
val selectedVersionProperty = SimpleStringProperty(this, "selectedVersion", "")
var selectedVersion: String by selectedVersionProperty
val gameDirProperty = SimpleObjectProperty<File>(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<Profile>, JsonDeserializer<Profile> {
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
}
}
}
}

View File

@ -17,8 +17,154 @@
*/ */
package org.jackhuang.hmcl.setting package org.jackhuang.hmcl.setting
object Settings { import com.google.gson.GsonBuilder
init { 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<String, Profile> {
return SETTINGS.configurations
}
fun getProfilesFiltered(): Collection<Profile> {
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()
} }
} }

View File

@ -18,17 +18,14 @@
package org.jackhuang.hmcl.setting package org.jackhuang.hmcl.setting
import com.google.gson.* import com.google.gson.*
import javafx.beans.InvalidationListener
import javafx.beans.property.* import javafx.beans.property.*
import org.jackhuang.hmcl.util.* import org.jackhuang.hmcl.util.*
import java.lang.reflect.Type import java.lang.reflect.Type
class VersionSetting() { class VersionSetting() {
/** var isGlobal: Boolean = false
* The displayed name.
*/
val nameProperty = SimpleStringProperty(this, "name", "")
var name: String by nameProperty
/** /**
* HMCL Version Settings have been divided into 2 parts. * HMCL Version Settings have been divided into 2 parts.
@ -162,15 +159,31 @@ class VersionSetting() {
val launcherVisibilityProperty = SimpleObjectProperty<LauncherVisibility>(this, "launcherVisibility", LauncherVisibility.HIDE) val launcherVisibilityProperty = SimpleObjectProperty<LauncherVisibility>(this, "launcherVisibility", LauncherVisibility.HIDE)
var launcherVisibility: LauncherVisibility by launcherVisibilityProperty var launcherVisibility: LauncherVisibility by launcherVisibilityProperty
val gameVersion: String fun addPropertyChangedListener(listener: InvalidationListener) {
get() = "1.7.10" 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<VersionSetting>, JsonDeserializer<VersionSetting> { companion object Serializer: JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
override fun serialize(src: VersionSetting?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { override fun serialize(src: VersionSetting?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
if (src == null) return JsonNull.INSTANCE if (src == null) return JsonNull.INSTANCE
val jsonObject = JsonObject() val jsonObject = JsonObject()
with(jsonObject) { with(jsonObject) {
addProperty("name", src.name)
addProperty("usesGlobal", src.usesGlobal) addProperty("usesGlobal", src.usesGlobal)
addProperty("javaArgs", src.javaArgs) addProperty("javaArgs", src.javaArgs)
addProperty("minecraftArgs", src.minecraftArgs) addProperty("minecraftArgs", src.minecraftArgs)
@ -197,7 +210,6 @@ class VersionSetting() {
if (json == null || json == JsonNull.INSTANCE || json !is JsonObject) return null if (json == null || json == JsonNull.INSTANCE || json !is JsonObject) return null
return VersionSetting().apply { return VersionSetting().apply {
name = json["name"]?.asString ?: ""
usesGlobal = json["usesGlobal"]?.asBoolean ?: false usesGlobal = json["usesGlobal"]?.asBoolean ?: false
javaArgs = json["javaArgs"]?.asString ?: "" javaArgs = json["javaArgs"]?.asString ?: ""
minecraftArgs = json["minecraftArgs"]?.asString ?: "" minecraftArgs = json["minecraftArgs"]?.asString ?: ""

View File

@ -21,10 +21,17 @@ import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXComboBox import com.jfoenix.controls.JFXComboBox
import com.jfoenix.controls.JFXListCell import com.jfoenix.controls.JFXListCell
import com.jfoenix.controls.JFXListView import com.jfoenix.controls.JFXListView
import javafx.collections.FXCollections
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.layout.Pane import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane 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.setting.VersionSetting
import org.jackhuang.hmcl.ui.animation.ContainerAnimations import org.jackhuang.hmcl.ui.animation.ContainerAnimations
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
@ -51,7 +58,7 @@ class MainController {
*/ */
@FXML lateinit var page: StackPane @FXML lateinit var page: StackPane
@FXML lateinit var listVersions: JFXListView<VersionSetting> // TODO: JFXListView<Version> including icon, title, game version(if equals to title, hidden) @FXML lateinit var listVersions: JFXListView<VersionListItem> // TODO: JFXListView<Version> including icon, title, game version(if equals to title, hidden)
lateinit var animationHandler: TransitionHandler lateinit var animationHandler: TransitionHandler
@ -61,56 +68,23 @@ class MainController {
animationHandler = TransitionHandler(page) animationHandler = TransitionHandler(page)
listVersions.items.add(VersionSetting("1")) EVENT_BUS.channel<RefreshedVersionsEvent>() += this::loadVersions
listVersions.items.add(VersionSetting("2")) EVENT_BUS.channel<ProfileLoadingEvent>() += this::onProfilesLoading
listVersions.items.add(VersionSetting("3")) EVENT_BUS.channel<ProfileChangedEvent>() += this::onProfileChanged
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<VersionSetting>() {
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
}
}
}
listVersions.setOnMouseClicked { listVersions.setOnMouseClicked {
if (it.clickCount == 2) { if (it.clickCount == 2) {
setContentPage(Controllers.versionPane) setContentPage(Controllers.versionPane)
Controllers.versionController.loadVersionSetting(listVersions.selectionModel.selectedItem) val id = listVersions.selectionModel.selectedItem.id
Controllers.versionController.loadVersionSetting(id, Settings.getLastProfile().getVersionSetting(id))
} else } else
it.consume() 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() listVersions.smoothScrolling()
Settings.onProfileLoading()
} }
private val empty = Pane() private val empty = Pane()
@ -122,4 +96,34 @@ class MainController {
fun installNewVersion() { fun installNewVersion() {
setContentPage(Wizard.createWizard("Install New Game", DownloadWizardProvider())) 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<String, VersionListItem>()
fun loadVersions() {
val profile = Settings.getLastProfile()
val list = mutableListOf<VersionListItem>()
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])
}
} }

View File

@ -59,8 +59,8 @@ class VersionController {
JFXScrollPane.smoothScrolling(scroll) JFXScrollPane.smoothScrolling(scroll)
} }
fun loadVersionSetting(version: VersionSetting) { fun loadVersionSetting(id: String, version: VersionSetting) {
titleLabel.text = version.name titleLabel.text = id
} }
fun onExploreJavaDir() { fun onExploreJavaDir() {

View File

@ -22,7 +22,7 @@ import javafx.scene.control.Label
import javafx.scene.layout.BorderPane import javafx.scene.layout.BorderPane
import org.jackhuang.hmcl.setting.VersionSetting 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 lblVersionName: Label
@FXML lateinit var lblGameVersion: Label @FXML lateinit var lblGameVersion: Label
@ -31,7 +31,7 @@ class VersionListItem(val setting: VersionSetting, val gameVersion: String) : Bo
init { init {
loadFXML("/assets/fxml/version-list-item.fxml") loadFXML("/assets/fxml/version-list-item.fxml")
lblVersionName.text = setting.name lblVersionName.text = versionName
lblGameVersion.text = gameVersion lblGameVersion.text = gameVersion
} }

View File

@ -24,7 +24,7 @@ import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.then import org.jackhuang.hmcl.task.then
import java.net.Proxy 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) { : AbstractDependencyManager(repository) {
override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this) override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this)

View File

@ -36,3 +36,5 @@ class EventBus {
} }
} }
val EVENT_BUS = EventBus()

View File

@ -17,30 +17,43 @@
*/ */
package org.jackhuang.hmcl.event package org.jackhuang.hmcl.event
import org.jackhuang.hmcl.util.SimpleMultimap
import java.util.* import java.util.*
class EventManager<T : EventObject> { class EventManager<T : EventObject> {
private val handlers = EnumMap<EventPriority, MutableList<(T) -> Unit>>(EventPriority::class.java).apply { private val handlers = SimpleMultimap<EventPriority, (T) -> Unit>({ EnumMap(EventPriority::class.java) }, ::HashSet)
for (value in EventPriority.values()) private val handlers2 = SimpleMultimap<EventPriority, () -> Unit>({ EnumMap(EventPriority::class.java) }, ::HashSet)
put(value, LinkedList<(T) -> Unit>())
}
fun register(func: (T) -> Unit, priority: EventPriority = EventPriority.NORMAL) { fun register(func: (T) -> Unit, priority: EventPriority = EventPriority.NORMAL) {
if (!handlers[priority]!!.contains(func)) if (!handlers[priority].contains(func))
handlers[priority]!!.add(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) { 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) { fun fireEvent(event: T) {
for (priority in EventPriority.values()) for (priority in EventPriority.values()) {
for (handler in handlers[priority]!!) for (handler in handlers[priority])
handler(event) handler(event)
for (handler in handlers2[priority])
handler()
}
} }
operator fun plusAssign(func: (T) -> Unit) = register(func) 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: (T) -> Unit) = unregister(func)
operator fun minusAssign(func: () -> Unit) = unregister(func)
operator fun invoke(event: T) = fireEvent(event) operator fun invoke(event: T) = fireEvent(event)
} }

View File

@ -0,0 +1,51 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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.
* <br></br>
* 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.
* <br>
* 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.
* <br></br>
* 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)

View File

@ -18,6 +18,7 @@
package org.jackhuang.hmcl.game package org.jackhuang.hmcl.game
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import org.jackhuang.hmcl.event.*
import org.jackhuang.hmcl.util.GSON import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.LOG import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.fromJson import org.jackhuang.hmcl.util.fromJson
@ -27,7 +28,7 @@ import java.io.IOException
import java.util.* import java.util.*
import java.util.logging.Level import java.util.logging.Level
open class DefaultGameRepository(val baseDirectory: File): GameRepository { open class DefaultGameRepository(var baseDirectory: File): GameRepository {
protected val versions: MutableMap<String, Version> = TreeMap<String, Version>() protected val versions: MutableMap<String, Version> = TreeMap<String, Version>()
override fun hasVersion(id: String) = versions.containsKey(id) override fun hasVersion(id: String) = versions.containsKey(id)
@ -81,9 +82,8 @@ open class DefaultGameRepository(val baseDirectory: File): GameRepository {
return file.deleteRecursively() return file.deleteRecursively()
} }
protected open fun refreshVersionsImpl() {
@Synchronized
override fun refreshVersions() {
versions.clear() versions.clear()
if (ClassicVersion.hasClassicVersion(baseDirectory)) { if (ClassicVersion.hasClassicVersion(baseDirectory)) {
@ -123,7 +123,16 @@ open class DefaultGameRepository(val baseDirectory: File): GameRepository {
} }
versions[id] = version 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 { override fun getAssetIndex(assetId: String): AssetIndex {

View File

@ -95,6 +95,14 @@ interface GameRepository : VersionProvider {
*/ */
fun getVersionJar(version: Version): File 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. * Rename given version to new name.
* *

View File

@ -0,0 +1,139 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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()
}
}

View File

@ -28,6 +28,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.TypeAdapterFactory import com.google.gson.TypeAdapterFactory
import org.jackhuang.hmcl.game.Library import org.jackhuang.hmcl.game.Library
import java.io.File
import java.text.DateFormat import java.text.DateFormat
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -40,6 +41,8 @@ val GSON: Gson = GsonBuilder()
.registerTypeAdapter(Library::class.java, Library) .registerTypeAdapter(Library::class.java, Library)
.registerTypeAdapter(Date::class.java, DateTypeAdapter) .registerTypeAdapter(Date::class.java, DateTypeAdapter)
.registerTypeAdapter(UUID::class.java, UUIDTypeAdapter) .registerTypeAdapter(UUID::class.java, UUIDTypeAdapter)
.registerTypeAdapter(Platform::class.java, Platform)
.registerTypeAdapter(File::class.java, FileTypeAdapter)
.registerTypeAdapterFactory(ValidationTypeAdapterFactory) .registerTypeAdapterFactory(ValidationTypeAdapterFactory)
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory) .registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory)
.create() .create()
@ -48,6 +51,14 @@ inline fun <reified T> typeOf(): Type = object : TypeToken<T>() {}.type
inline fun <reified T> Gson.fromJson(json: String): T? = fromJson<T>(json, T::class.java) inline fun <reified T> Gson.fromJson(json: String): T? = fromJson<T>(json, T::class.java)
inline fun <reified T> Gson.fromJsonQuietly(json: String): T? {
try {
return fromJson<T>(json)
} catch (json: JsonParseException) {
return null
}
}
/** /**
* Check if the json object's fields automatically filled by Gson are in right format. * Check if the json object's fields automatically filled by Gson are in right format.
*/ */
@ -190,3 +201,16 @@ object DateTypeAdapter : JsonSerializer<Date>, JsonDeserializer<Date> {
} }
} }
} }
object FileTypeAdapter : JsonSerializer<File>, JsonDeserializer<File> {
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)
}
}

View File

@ -17,12 +17,14 @@
*/ */
package org.jackhuang.hmcl.util package org.jackhuang.hmcl.util
import com.google.gson.annotations.SerializedName
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.Serializable import java.io.Serializable
import java.util.regex.Pattern import java.util.regex.Pattern
data class JavaVersion internal constructor( data class JavaVersion internal constructor(
@SerializedName("location")
val binary: File, val binary: File,
val version: Int, val version: Int,
val platform: Platform) : Serializable val platform: Platform) : Serializable
@ -74,8 +76,8 @@ data class JavaVersion internal constructor(
} }
fun getJavaFile(home: File): File { fun getJavaFile(home: File): File {
var path = home.resolve("bin") val path = home.resolve("bin")
var javaw = path.resolve("javaw.exe") val javaw = path.resolve("javaw.exe")
if (OS.CURRENT_OS === OS.WINDOWS && javaw.isFile) if (OS.CURRENT_OS === OS.WINDOWS && javaw.isFile)
return javaw return javaw
else else

View File

@ -17,12 +17,33 @@
*/ */
package org.jackhuang.hmcl.util package org.jackhuang.hmcl.util
import com.google.gson.*
import java.lang.reflect.Type
enum class Platform(val bit: String) { enum class Platform(val bit: String) {
BIT_32("32"), BIT_32("32"),
BIT_64("64"), BIT_64("64"),
UNKNOWN("unknown"); UNKNOWN("unknown");
companion object { companion object Serializer: JsonSerializer<Platform>, JsonDeserializer<Platform> {
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 { val PLATFORM: Platform by lazy {
if (IS_64_BIT) BIT_64 else BIT_32 if (IS_64_BIT) BIT_64 else BIT_32

View File

@ -40,13 +40,19 @@ class SimpleMultimap<K, V>(val maper: () -> MutableMap<K, Collection<V>>, val va
valuesImpl += value valuesImpl += value
} }
fun remove(key: K): Collection<V>? { fun removeAll(key: K): Collection<V>? {
val result = map.remove(key) val result = map.remove(key)
if (result != null) if (result != null)
valuesImpl.removeAll(result) valuesImpl.removeAll(result)
return result return result
} }
fun remove(value: V) {
map.values.forEach {
it.remove(value)
}
}
fun clear() { fun clear() {
map.clear() map.clear()
valuesImpl.clear() valuesImpl.clear()