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
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<String, VersionSetting>()
@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<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
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<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
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<LauncherVisibility>(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<VersionSetting>, JsonDeserializer<VersionSetting> {
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 ?: ""

View File

@ -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<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
@ -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<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
}
}
}
EVENT_BUS.channel<RefreshedVersionsEvent>() += this::loadVersions
EVENT_BUS.channel<ProfileLoadingEvent>() += this::onProfilesLoading
EVENT_BUS.channel<ProfileChangedEvent>() += 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<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)
}
fun loadVersionSetting(version: VersionSetting) {
titleLabel.text = version.name
fun loadVersionSetting(id: String, version: VersionSetting) {
titleLabel.text = id
}
fun onExploreJavaDir() {

View File

@ -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
}

View File

@ -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)

View File

@ -35,4 +35,6 @@ class EventBus {
channel(obj.javaClass).fireEvent(obj)
}
}
}
val EVENT_BUS = EventBus()

View File

@ -17,30 +17,43 @@
*/
package org.jackhuang.hmcl.event
import org.jackhuang.hmcl.util.SimpleMultimap
import java.util.*
class EventManager<T : EventObject> {
private val handlers = EnumMap<EventPriority, MutableList<(T) -> Unit>>(EventPriority::class.java).apply {
for (value in EventPriority.values())
put(value, LinkedList<(T) -> Unit>())
}
private val handlers = SimpleMultimap<EventPriority, (T) -> Unit>({ EnumMap(EventPriority::class.java) }, ::HashSet)
private val handlers2 = SimpleMultimap<EventPriority, () -> 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)
}

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
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<String, Version> = TreeMap<String, Version>()
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 {

View File

@ -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.
*

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.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 <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.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.
*/
@ -189,4 +200,17 @@ object DateTypeAdapter : JsonSerializer<Date>, JsonDeserializer<Date> {
return result.substring(0, 22) + ":" + result.substring(22)
}
}
}
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
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

View File

@ -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<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 {
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
}
fun remove(key: K): Collection<V>? {
fun removeAll(key: K): Collection<V>? {
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()