This commit is contained in:
huangyuhui
2017-08-22 14:31:35 +08:00
parent b40b0ec47c
commit 0cc7d621bc
108 changed files with 752 additions and 371 deletions

View File

@@ -18,7 +18,7 @@
package org.jackhuang.hmcl
import org.jackhuang.hmcl.setting.Profile
import java.util.EventObject
import java.util.*
/**
* This event gets fired when the selected profile changed.

View File

@@ -20,7 +20,6 @@ package org.jackhuang.hmcl
import javafx.application.Application
import javafx.application.Platform
import javafx.stage.Stage
import javafx.stage.StageStyle
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.ui.Controllers
@@ -29,7 +28,6 @@ import org.jackhuang.hmcl.util.DEFAULT_USER_AGENT
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.OS
import java.io.File
import java.util.concurrent.Callable
import java.util.logging.Level
fun i18n(key: String): String {

View File

@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.setting.EnumGameDirectory
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.setting.VersionSetting
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.fromJson
import java.io.File

View File

@@ -29,15 +29,15 @@ import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.setting.VersionSetting
import org.jackhuang.hmcl.task.*
import org.jackhuang.hmcl.ui.*
import org.jackhuang.hmcl.util.JavaProcess
import org.jackhuang.hmcl.util.Log4jLevel
import org.jackhuang.hmcl.util.ManagedProcess
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
object LauncherHelper {
private val launchingStepsPane = LaunchingStepsPane()
val PROCESS = ConcurrentLinkedQueue<JavaProcess>()
val PROCESS = ConcurrentLinkedQueue<ManagedProcess>()
fun launch() {
val profile = Settings.selectedProfile
@@ -119,11 +119,11 @@ object LauncherHelper {
authInfo.username to "<player>"
)
private val launcherVisibility = setting.launcherVisibility
private lateinit var process: JavaProcess
private lateinit var process: ManagedProcess
private var lwjgl = false
private var logWindow: LogWindow? = null
private val logs = LinkedList<Pair<String, Log4jLevel>>()
override fun setProcess(process: JavaProcess) {
override fun setProcess(process: ManagedProcess) {
this.process = process
if (setting.showLogs) {

View File

@@ -20,8 +20,7 @@ package org.jackhuang.hmcl.setting
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.Main
import org.jackhuang.hmcl.util.JavaVersion
import java.io.File
import java.util.TreeMap
import java.util.*
class Config {
@SerializedName("last")

View File

@@ -22,9 +22,10 @@ import javafx.beans.InvalidationListener
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.game.HMCLGameRepository
import org.jackhuang.hmcl.mod.ModManager
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.property.ImmediateObjectProperty
import org.jackhuang.hmcl.util.property.ImmediateStringProperty
import org.jackhuang.hmcl.util.ImmediateObjectProperty
import org.jackhuang.hmcl.util.ImmediateStringProperty
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.setValue
import java.io.File
import java.lang.reflect.Type

View File

@@ -20,26 +20,23 @@ package org.jackhuang.hmcl.setting
import com.google.gson.GsonBuilder
import javafx.beans.InvalidationListener
import javafx.scene.text.Font
import java.io.IOException
import org.jackhuang.hmcl.Main
import org.jackhuang.hmcl.ProfileChangedEvent
import org.jackhuang.hmcl.ProfileLoadingEvent
import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.MojangDownloadProvider
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.auth.Account
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.event.EVENT_BUS
import org.jackhuang.hmcl.util.property.ImmediateObjectProperty
import org.jackhuang.hmcl.util.property.ImmediateStringProperty
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.net.Authenticator
import java.net.InetSocketAddress
import java.net.PasswordAuthentication
import java.net.Proxy
import java.util.*
import java.util.logging.Level
object Settings {
val GSON = GsonBuilder()

View File

@@ -22,7 +22,6 @@ import javafx.beans.InvalidationListener
import org.jackhuang.hmcl.Main
import org.jackhuang.hmcl.game.LaunchOptions
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.property.*
import java.io.File
import java.io.IOException
import java.lang.reflect.Type

View File

@@ -19,15 +19,14 @@ package org.jackhuang.hmcl.ui
import com.jfoenix.controls.*
import javafx.application.Platform
import javafx.fxml.FXML
import javafx.scene.Node
import javafx.scene.control.ScrollPane
import javafx.scene.layout.StackPane
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.beans.value.ChangeListener
import javafx.fxml.FXML
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.control.ScrollPane
import javafx.scene.control.ToggleGroup
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException
@@ -37,6 +36,7 @@ import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.taskResult
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
import org.jackhuang.hmcl.util.onChange
class AccountsPage() : StackPane(), DecoratorPage {
override val titleProperty: StringProperty = SimpleStringProperty(this, "title", "Accounts")
@@ -50,14 +50,6 @@ class AccountsPage() : StackPane(), DecoratorPage {
@FXML lateinit var cboType: JFXComboBox<String>
@FXML lateinit var progressBar: JFXProgressBar
val listener = ChangeListener<Account?> { _, _, newValue ->
masonryPane.children.forEach {
if (it is AccountItem) {
it.chkSelected.isSelected = newValue?.username == it.lblUser.text
}
}
}
init {
loadFXML("/assets/fxml/account.fxml")
children.remove(dialog)
@@ -75,8 +67,8 @@ class AccountsPage() : StackPane(), DecoratorPage {
}
txtPassword.validate()
cboType.selectionModel.selectedIndexProperty().addListener { _, _, newValue ->
val visible = newValue != 0
cboType.selectionModel.selectedIndexProperty().onChange {
val visible = it != 0
txtPassword.isVisible = visible
}
cboType.selectionModel.select(0)
@@ -84,7 +76,13 @@ class AccountsPage() : StackPane(), DecoratorPage {
txtPassword.setOnAction { onCreationAccept() }
txtUsername.setOnAction { onCreationAccept() }
Settings.selectedAccountProperty.addListener(listener)
Settings.selectedAccountProperty.setChangedListener { account ->
masonryPane.children.forEach { node ->
if (node is AccountItem) {
node.chkSelected.isSelected = account?.username == node.lblUser.text
}
}
}
loadAccounts()
@@ -92,10 +90,6 @@ class AccountsPage() : StackPane(), DecoratorPage {
addNewAccount()
}
override fun onClose() {
Settings.selectedAccountProperty.removeListener(listener)
}
fun loadAccounts() {
val children = mutableListOf<Node>()
var i = 0
@@ -103,9 +97,9 @@ class AccountsPage() : StackPane(), DecoratorPage {
for ((_, account) in Settings.getAccounts()) {
children += buildNode(++i, account, group)
}
group.selectedToggleProperty().addListener { _, _, newValue ->
if (newValue != null)
Settings.selectedAccount = newValue.properties["account"] as Account
group.selectedToggleProperty().onChange {
if (it != null)
Settings.selectedAccount = it.properties["account"] as Account
}
masonryPane.resetChildren(children)
Platform.runLater {

View File

@@ -23,7 +23,7 @@ import com.jfoenix.controls.JFXDrawer
import com.jfoenix.controls.JFXHamburger
import com.jfoenix.effects.JFXDepthManager
import com.jfoenix.svg.SVGGlyph
import javafx.animation.*
import javafx.animation.Timeline
import javafx.application.Platform
import javafx.beans.property.BooleanProperty
import javafx.beans.property.ObjectProperty
@@ -36,7 +36,6 @@ import javafx.geometry.Insets
import javafx.scene.Cursor
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.control.ScrollPane
import javafx.scene.control.Tooltip
import javafx.scene.input.MouseEvent
import javafx.scene.layout.*
@@ -49,7 +48,8 @@ import org.jackhuang.hmcl.ui.animation.AnimationProducer
import org.jackhuang.hmcl.ui.animation.ContainerAnimations
import org.jackhuang.hmcl.ui.animation.TransitionHandler
import org.jackhuang.hmcl.ui.wizard.*
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.setValue
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue

View File

@@ -18,13 +18,11 @@
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXComboBox
import javafx.scene.layout.*
import org.jackhuang.hmcl.auth.Account
import javafx.scene.layout.VBox
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.construct.IconedItem
import org.jackhuang.hmcl.ui.construct.RipplerContainer
import javax.swing.event.ChangeListener
class LeftPaneController(private val leftPane: AdvancedListBox) {
val versionsPane = VBox()

View File

@@ -35,6 +35,7 @@ import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.util.Log4jLevel
import org.jackhuang.hmcl.util.inc
import org.jackhuang.hmcl.util.onChange
import org.jackhuang.hmcl.util.readFullyAsString
import org.w3c.dom.Document
import org.w3c.dom.Node
@@ -90,8 +91,8 @@ class LogWindow : Stage() {
engine = webView.engine
engine.loadContent(javaClass.getResourceAsStream("/assets/log-window-content.html").readFullyAsString().replace("\${FONT}", "${Settings.font.size}px \"${Settings.font.family}\""))
engine.loadWorker.stateProperty().addListener { _, _, newValue ->
if (newValue == Worker.State.SUCCEEDED) {
engine.loadWorker.stateProperty().onChange {
if (it == Worker.State.SUCCEEDED) {
document = engine.document
body = document.getElementsByTagName("body").item(0)
engine.executeScript("limitedLogs=${Settings.logLines};")
@@ -105,8 +106,8 @@ class LogWindow : Stage() {
flag = true
}
}
cboLines.selectionModel.selectedItemProperty().addListener { _, _, newValue ->
Settings.logLines = newValue.toInt()
cboLines.selectionModel.selectedItemProperty().onChange {
Settings.logLines = it?.toInt() ?: 100
engine.executeScript("limitedLogs=${Settings.logLines};")
}
if (!flag) {

View File

@@ -38,6 +38,7 @@ import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.construct.RipplerContainer
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
import org.jackhuang.hmcl.util.onChange
/**
* @see /assets/fxml/main.fxml
@@ -104,9 +105,9 @@ class MainPage : StackPane(), DecoratorPage {
profile.repository.getVersions().forEach { version ->
children += buildNode(++i, profile, version.id, minecraftVersion(profile.repository.getVersionJar(version.id)) ?: "Unknown", group)
}
group.selectedToggleProperty().addListener { _, _, newValue ->
if (newValue != null)
profile.selectedVersion = newValue.properties["version"] as String
group.selectedToggleProperty().onChange {
if (it != null)
profile.selectedVersion = it.properties["version"] as String
}
masonryPane.resetChildren(children)
}

View File

@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.mod.ModManager
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.task
import org.jackhuang.hmcl.util.onChange
class ModController {
@FXML lateinit var scrollPane: ScrollPane
@@ -71,8 +72,8 @@ class ModController {
JFXDepthManager.setDepth(this, 1)
style += "-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;"
modInfo.activeProperty.addListener { _, _, newValue ->
if (newValue)
modInfo.activeProperty.onChange {
if (it)
styleClass -= "disabled"
else
styleClass += "disabled"

View File

@@ -22,6 +22,7 @@ import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.BorderPane
import org.jackhuang.hmcl.mod.ModInfo
import org.jackhuang.hmcl.util.onChange
class ModItem(info: ModInfo, private val deleteCallback: (ModItem) -> Unit) : BorderPane() {
@FXML lateinit var lblModFileName: Label
@@ -34,8 +35,8 @@ class ModItem(info: ModInfo, private val deleteCallback: (ModItem) -> Unit) : Bo
lblModFileName.text = info.fileName
lblModAuthor.text = "${info.name}, Version: ${info.version}, Game Version: ${info.mcversion}, Authors: ${info.authors}"
chkEnabled.isSelected = info.isActive
chkEnabled.selectedProperty().addListener { _, _, newValue ->
info.activeProperty.set(newValue)
chkEnabled.selectedProperty().onChange {
info.activeProperty.set(it)
}
}

View File

@@ -21,7 +21,6 @@ import javafx.fxml.FXMLLoader
import javafx.scene.Group
import javafx.scene.Node
import javafx.scene.shape.SVGPath
import kotlin.coroutines.experimental.EmptyCoroutineContext.plus
object SVG {
val svgNames = setOf("gear")

View File

@@ -35,6 +35,7 @@ import org.jackhuang.hmcl.ui.construct.FileItem
import org.jackhuang.hmcl.ui.construct.FontComboBox
import org.jackhuang.hmcl.ui.construct.Validator
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
import org.jackhuang.hmcl.util.onChange
class SettingsPage : StackPane(), DecoratorPage {
override val titleProperty: StringProperty = SimpleStringProperty(this, "title", i18n("launcher.title.launcher"))
@@ -59,41 +60,33 @@ class SettingsPage : StackPane(), DecoratorPage {
cboDownloadSource.limitWidth(400.0)
txtProxyHost.text = Settings.proxyHost
txtProxyHost.textProperty().addListener { _, _, newValue ->
Settings.proxyHost = newValue
}
txtProxyHost.textProperty().onChange { Settings.proxyHost = it }
txtProxyPort.text = Settings.proxyPort
txtProxyPort.textProperty().addListener { _, _, newValue ->
Settings.proxyPort = newValue
}
txtProxyPort.textProperty().onChange { Settings.proxyPort = it }
txtProxyUsername.text = Settings.proxyUser
txtProxyUsername.textProperty().addListener { _, _, newValue ->
Settings.proxyUser = newValue
}
txtProxyUsername.textProperty().onChange { Settings.proxyUser = it }
txtProxyPassword.text = Settings.proxyPass
txtProxyPassword.textProperty().addListener { _, _, newValue ->
Settings.proxyPass = newValue
}
txtProxyPassword.textProperty().onChange { Settings.proxyPass = it }
cboDownloadSource.selectionModel.select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.downloadProvider))
cboDownloadSource.selectionModel.selectedIndexProperty().addListener { _, _, newValue ->
Settings.downloadProvider = DownloadProviders.getDownloadProvider(newValue.toInt())
cboDownloadSource.selectionModel.selectedIndexProperty().onChange {
Settings.downloadProvider = DownloadProviders.getDownloadProvider(it)
}
cboFont.selectionModel.select(Settings.font.family)
cboFont.valueProperty().addListener { _, _, newValue ->
val font = Font.font(newValue, Settings.font.size)
cboFont.valueProperty().onChange {
val font = Font.font(it, Settings.font.size)
Settings.font = font
lblDisplay.style = "-fx-font: ${Settings.font.size} \"${font.family}\";"
}
txtFontSize.text = Settings.font.size.toString()
txtFontSize.validators += Validator { it.toDoubleOrNull() != null }
txtFontSize.textProperty().addListener { _, _, newValue ->
txtFontSize.textProperty().onChange {
if (txtFontSize.validate()) {
val font = Font.font(Settings.font.family, newValue.toDouble())
val font = Font.font(Settings.font.family, it!!.toDouble())
Settings.font = font
lblDisplay.style = "-fx-font: ${font.size} \"${Settings.font.family}\";"
}
@@ -106,13 +99,13 @@ class SettingsPage : StackPane(), DecoratorPage {
}
cboLanguage.items = list
cboLanguage.selectionModel.select(Locales.LOCALES.indexOf(Settings.locale))
cboLanguage.selectionModel.selectedIndexProperty().addListener { _, _, newValue ->
Settings.locale = Locales.getLocale(newValue.toInt())
cboLanguage.selectionModel.selectedIndexProperty().onChange {
Settings.locale = Locales.getLocale(it)
}
cboProxyType.selectionModel.select(Proxies.PROXIES.indexOf(Settings.proxyType))
cboProxyType.selectionModel.selectedIndexProperty().addListener { _, _, newValue ->
Settings.proxyType = Proxies.getProxyType(newValue.toInt())
cboProxyType.selectionModel.selectedIndexProperty().onChange {
Settings.proxyType = Proxies.getProxyType(it)
}
fileCommonLocation.setProperty(Settings.commonPathProperty)

View File

@@ -18,9 +18,7 @@
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXCheckBox
import com.jfoenix.controls.JFXRadioButton
import com.jfoenix.effects.JFXDepthManager
import javafx.beans.binding.Bindings
import javafx.fxml.FXML
import javafx.scene.control.Label

View File

@@ -20,10 +20,7 @@ package org.jackhuang.hmcl.ui
import javafx.fxml.FXML
import javafx.scene.control.Button
import javafx.scene.control.Label
import javafx.scene.control.Tooltip
import javafx.scene.layout.BorderPane
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.setting.VersionSetting
class VersionListItem(val versionName: String, val gameVersion: String) : StackPane() {

View File

@@ -17,13 +17,15 @@
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.*
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXListView
import com.jfoenix.controls.JFXPopup
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.fxml.FXML
import javafx.scene.control.Alert
import javafx.scene.layout.*
import org.jackhuang.hmcl.download.GameAssetIndexDownloadTask
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.ui.wizard.DecoratorPage

View File

@@ -19,15 +19,15 @@ package org.jackhuang.hmcl.ui
import com.jfoenix.controls.*
import javafx.beans.InvalidationListener
import javafx.beans.binding.Bindings
import javafx.beans.value.ChangeListener
import javafx.fxml.FXML
import javafx.geometry.Pos
import javafx.scene.control.Label
import javafx.scene.control.ScrollPane
import javafx.scene.control.Toggle
import javafx.scene.control.ToggleGroup
import javafx.scene.layout.*
import javafx.scene.layout.BorderPane
import javafx.scene.layout.Pane
import javafx.scene.layout.VBox
import javafx.stage.DirectoryChooser
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.VersionSetting

View File

@@ -26,9 +26,7 @@ import javafx.scene.SnapshotParameters
import javafx.scene.image.ImageView
import javafx.scene.image.WritableImage
import javafx.scene.layout.StackPane
import javafx.scene.shape.Rectangle
import javafx.util.Duration
import org.jackhuang.hmcl.ui.setOverflowHidden
import org.jackhuang.hmcl.ui.takeSnapshot
/**

View File

@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.construct
import javafx.beans.DefaultProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import javafx.collections.ListChangeListener
@@ -27,7 +26,10 @@ import javafx.collections.ObservableList
import javafx.scene.Node
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.setValue
import kotlin.collections.plusAssign
import kotlin.collections.set
@DefaultProperty("content")
class ComponentList: StackPane() {

View File

@@ -18,14 +18,14 @@
package org.jackhuang.hmcl.ui.construct
import com.jfoenix.controls.JFXButton
import javafx.animation.*
import javafx.animation.Animation
import javafx.animation.KeyFrame
import javafx.animation.KeyValue
import javafx.animation.Timeline
import javafx.beans.property.SimpleBooleanProperty
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.layout.HBox
import javafx.scene.layout.Priority
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import javafx.scene.shape.Rectangle
@@ -33,7 +33,9 @@ import javafx.util.Duration
import org.jackhuang.hmcl.ui.SINE
import org.jackhuang.hmcl.ui.SVG
import org.jackhuang.hmcl.ui.limitHeight
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.onChange
import org.jackhuang.hmcl.util.setValue
class ComponentListCell(private val content: Node) : StackPane() {
@@ -146,8 +148,8 @@ class ComponentListCell(private val content: Node) : StackPane() {
expandAnimation?.play()
}
expandedProperty.addListener { _, _, newValue ->
if (newValue) {
expandedProperty.onChange {
if (it) {
expandIcon.rotate = 180.0
} else {
expandIcon.rotate = 0.0

View File

@@ -27,7 +27,8 @@ import javafx.scene.layout.VBox
import javafx.stage.DirectoryChooser
import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.ui.SVG
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.setValue
class FileItem : BorderPane() {
val nameProperty = SimpleStringProperty(this, "name")

View File

@@ -23,12 +23,13 @@ import javafx.collections.FXCollections
import javafx.scene.control.ListCell
import javafx.scene.text.Font
import javafx.util.Callback
import org.jackhuang.hmcl.util.onChange
class FontComboBox(@NamedArg("fontSize") fontSize: Double = 12.0, @NamedArg("enableStyle") enableStyle: Boolean = false) : JFXComboBox<String>(FXCollections.observableArrayList(Font.getFamilies())) {
init {
valueProperty().addListener { _, _, newValue ->
valueProperty().onChange {
if (enableStyle)
style = "-fx-font-family: \"$newValue\";"
style = "-fx-font-family: \"$it\";"
}
cellFactory = Callback {
object : ListCell<String>() {

View File

@@ -26,11 +26,15 @@ import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.geometry.Insets
import javafx.scene.Node
import javafx.scene.layout.*
import javafx.scene.layout.Background
import javafx.scene.layout.BackgroundFill
import javafx.scene.layout.CornerRadii
import javafx.scene.layout.StackPane
import javafx.scene.paint.Color
import javafx.scene.paint.Paint
import javafx.scene.shape.Rectangle
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.onChange
import org.jackhuang.hmcl.util.setValue
import java.util.concurrent.Callable
@@ -92,8 +96,8 @@ open class RipplerContainer(@NamedArg("container") container: Node): StackPane()
}
}
focusedProperty().addListener { _, _, newVal ->
if (newVal) {
focusedProperty().onChange {
if (it) {
if (!isPressed) {
this.buttonRippler.showOverlay()
}
@@ -125,7 +129,7 @@ open class RipplerContainer(@NamedArg("container") container: Node): StackPane()
return@Callable background
}
}, backgroundProperty()))
ripplerFillProperty.addListener { _, _, newVal -> this.buttonRippler.ripplerFill = newVal }
ripplerFillProperty.onChange { this.buttonRippler.ripplerFill = it }
if (background == null || this.isJavaDefaultBackground(background)) {
background = Background(BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null))
}

View File

@@ -17,12 +17,10 @@
*/
package org.jackhuang.hmcl.ui.construct
import java.io.InputStreamReader
import java.util.PropertyResourceBundle
import java.util.ResourceBundle
import java.io.IOException
import java.io.InputStream
import java.util.Locale
import java.io.InputStreamReader
import java.util.*
object UTF8Control : ResourceBundle.Control() {
@Throws(IllegalAccessException::class, InstantiationException::class, IOException::class)

View File

@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.ui.loadFXML
import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardPage
import org.jackhuang.hmcl.util.onChange
class InstallersPage(private val controller: WizardController, private val downloadProvider: DownloadProvider): StackPane(), WizardPage {
@@ -42,9 +43,9 @@ class InstallersPage(private val controller: WizardController, private val downl
val gameVersion = controller.settings["game"] as String
txtName.text = gameVersion
list.selectionModel.selectedIndexProperty().addListener { _, _, newValue ->
controller.settings[INSTALLER_TYPE] = newValue
controller.onNext(when (newValue){
list.selectionModel.selectedIndexProperty().onChange {
controller.settings[INSTALLER_TYPE] = it
controller.onNext(when (it){
0 -> VersionsPage(controller, gameVersion, downloadProvider, "forge") { controller.onPrev(false) }
1 -> VersionsPage(controller, gameVersion, downloadProvider, "liteloader") { controller.onPrev(false) }
2 -> VersionsPage(controller, gameVersion, downloadProvider, "optifine") { controller.onPrev(false) }

View File

@@ -28,7 +28,6 @@ import javafx.stage.FileChooser
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.mod.readCurseForgeModpackManifest
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.ui.construct.Validator
import org.jackhuang.hmcl.ui.loadFXML

View File

@@ -21,8 +21,8 @@ import com.jfoenix.controls.JFXListView
import com.jfoenix.controls.JFXSpinner
import javafx.fxml.FXML
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.TaskExecutor
import org.jackhuang.hmcl.ui.animation.ContainerAnimations
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.ui.loadFXML
import org.jackhuang.hmcl.ui.wizard.Refreshable
import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardPage
import org.jackhuang.hmcl.util.onChange
class VersionsPage(private val controller: WizardController, private val gameVersion: String, private val downloadProvider: DownloadProvider, private val libraryId: String, private val callback: () -> Unit): StackPane(), WizardPage, Refreshable {
@@ -44,8 +45,8 @@ class VersionsPage(private val controller: WizardController, private val gameVer
init {
loadFXML("/assets/fxml/download/versions.fxml")
children.setAll(spinner)
list.selectionModel.selectedItemProperty().addListener { _, _, newValue ->
controller.settings[libraryId] = newValue.remoteVersion.selfVersion
list.selectionModel.selectedItemProperty().onChange {
controller.settings[libraryId] = it!!.remoteVersion.selfVersion
callback()
}
refresh()

View File

@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.download
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.BorderPane
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.ui.loadFXML

View File

@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.wizard
import com.jfoenix.concurrency.JFXUtilities
import com.jfoenix.controls.JFXProgressBar
import javafx.application.Platform
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.*?>
<?import com.jfoenix.transitions.hamburger.HamburgerBackArrowBasicTransition?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?>
@@ -60,7 +59,11 @@
<BorderPane minWidth="200" maxWidth="200" fx:id="titleWrapper">
<center>
<Label fx:id="lblTitle" BorderPane.alignment="CENTER" mouseTransparent="true"
style="-fx-background-color: transparent; -fx-text-fill: white; -fx-font-size: 15px;"/>
style="-fx-background-color: transparent; -fx-text-fill: white; -fx-font-size: 15px;">
<BorderPane.margin>
<Insets left="3" />
</BorderPane.margin>
</Label>
</center>
<right>
<Rectangle height="${navBar.height}" width="1" fill="gray"/>

View File

@@ -19,9 +19,7 @@ package org.jackhuang.hmcl.auth.yggdrasil
import com.google.gson.*
import java.lang.reflect.Type
import com.google.gson.JsonObject
import java.util.UUID
import com.google.gson.JsonParseException
import java.util.*
data class GameProfile(
val id: UUID? = null,

View File

@@ -20,13 +20,6 @@ package org.jackhuang.hmcl.auth.yggdrasil
import com.google.gson.*
import java.lang.reflect.Type
import java.util.*
import com.google.gson.JsonObject
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import kotlin.collections.HashMap
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
class PropertyMap: HashMap<String, Property>() {

View File

@@ -21,9 +21,6 @@ import com.google.gson.GsonBuilder
import com.google.gson.JsonParseException
import org.jackhuang.hmcl.auth.*
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.doGet
import org.jackhuang.hmcl.util.doPost
import org.jackhuang.hmcl.util.toURL
import java.io.IOException
import java.net.Proxy
import java.net.URL

View File

@@ -17,11 +17,11 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.game.*
import org.jackhuang.hmcl.game.GameRepository
import java.net.Proxy
abstract class AbstractDependencyManager(repository: GameRepository, proxy: Proxy)
: DependencyManager(repository, proxy) {
abstract class AbstractDependencyManager
: DependencyManager {
abstract val downloadProvider: DownloadProvider
fun getVersions(id: String, selfVersion: String) =

View File

@@ -17,6 +17,14 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.download.forge.ForgeVersionList
import org.jackhuang.hmcl.download.game.GameVersionList
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList
/**
* @see {@link http://bmclapi2.bangbang93.com}
*/
object BMCLAPIDownloadProvider : DownloadProvider() {
override val libraryBaseURL: String = "http://bmclapi2.bangbang93.com/libraries/"
override val versionListURL: String = "http://bmclapi2.bangbang93.com/mc/game/version_manifest.json"

View File

@@ -17,6 +17,13 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.download.forge.ForgeInstallTask
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask
import org.jackhuang.hmcl.download.game.GameLibrariesTask
import org.jackhuang.hmcl.download.game.GameLoggingDownloadTask
import org.jackhuang.hmcl.download.game.VersionJSONSaveTask
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask
import org.jackhuang.hmcl.game.DefaultGameRepository
import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.task.ParallelTask
@@ -27,8 +34,8 @@ import java.net.Proxy
/**
* This class has no state.
*/
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, proxy: Proxy = Proxy.NO_PROXY)
: AbstractDependencyManager(repository, proxy) {
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, override val proxy: Proxy = Proxy.NO_PROXY)
: AbstractDependencyManager() {
override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this)

View File

@@ -17,9 +17,16 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.game.*
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask
import org.jackhuang.hmcl.download.game.GameLibrariesTask
import org.jackhuang.hmcl.download.game.GameLoggingDownloadTask
import org.jackhuang.hmcl.download.game.VersionJSONSaveTask
import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.task.*
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.AutoTypingMap
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.fromJson
import org.jackhuang.hmcl.util.toURL
import java.util.*
class DefaultGameBuilder(val dependencyManager: DefaultDependencyManager): GameBuilder() {

View File

@@ -22,27 +22,51 @@ import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.task.Task
import java.net.Proxy
abstract class DependencyManager(open val repository: GameRepository, open val proxy: Proxy) {
/**
* Do everything that will connect to Internet.
* Downloading Minecraft files.
*/
interface DependencyManager {
/**
* The relied game repository.
*/
val repository: GameRepository
/**
* The proxy that all network operations should go through.
*/
val proxy: Proxy
/**
* Check if the game is complete.
* Check libraries, assets, logging files and so on.
*
* @return
* @return the task to check game completion.
*/
abstract fun checkGameCompletionAsync(version: Version): Task
fun checkGameCompletionAsync(version: Version): Task
/**
* The builder to build a brand new game then libraries such as Forge, LiteLoader and OptiFine.
*/
abstract fun gameBuilder(): GameBuilder
fun gameBuilder(): GameBuilder
abstract fun installLibraryAsync(gameVersion: String, version: Version, libraryId: String, libraryVersion: String): Task
/**
* Install a library to a version.
* **Note**: Installing a library may change the version.json.
*
* @param gameVersion the Minecraft version that the library relies on.
* @param version the version.json.
* @param libraryId the type of being installed library. i.e. "forge", "liteloader", "optifine"
* @param libraryVersion the version of being installed library.
* @return the task to install the specific library.
*/
fun installLibraryAsync(gameVersion: String, version: Version, libraryId: String, libraryVersion: String): Task
/**
* Get registered version list.
*
* @param id the id of version list. i.e. game, forge, liteloader, optifine
* @throws IllegalArgumentException if the version list of specific id is not found.
*/
abstract fun getVersionList(id: String): VersionList<*>
fun getVersionList(id: String): VersionList<*>
}

View File

@@ -17,14 +17,31 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.download.VersionList
/**
* The service provider that provides Minecraft online file downloads.
*/
abstract class DownloadProvider {
abstract val libraryBaseURL: String
abstract val versionListURL: String
abstract val versionBaseURL: String
abstract val assetIndexBaseURL: String
abstract val assetBaseURL: String
/**
* Inject into original URL provided by Mojang and Forge.
*
* Since there are many provided URLs that are written in JSONs and are unmodifiable,
* this method provides a way to change them.
*
* @param baseURL original URL provided by Mojang and Forge.
* @return the URL that is equivalent to [baseURL], but belongs to your own service provider.
*/
abstract fun injectURL(baseURL: String): String
/**
* the specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine"
* @param id the id of specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine"
* @return the version list
*/
abstract fun getVersionListById(id: String): VersionList<*>
}

View File

@@ -19,12 +19,20 @@ package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.task.Task
/**
* The builder which provide a task to build Minecraft environment.
*
* @author huangyuhui
*/
abstract class GameBuilder {
var name: String = ""
protected var gameVersion: String = ""
protected var toolVersions = HashMap<String, String>()
/**
* The new game version name, for .minecraft/<version name>.
* @param name the name of new game version.
*/
fun name(name: String): GameBuilder {
this.name = name
return this
@@ -35,10 +43,17 @@ abstract class GameBuilder {
return this
}
/**
* @param id the core library id. i.e. "forge", "liteloader", "optifine"
* @param version the version of the core library. For documents, you can first try [VersionList.versions]
*/
fun version(id: String, version: String): GameBuilder {
toolVersions[id] = version
return this
}
/**
* @return the task that can build thw whole Minecraft environment
*/
abstract fun buildAsync(): Task
}

View File

@@ -17,8 +17,15 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.download.forge.ForgeVersionList
import org.jackhuang.hmcl.download.game.GameVersionList
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList
import org.jackhuang.hmcl.download.optifine.OptiFineVersionList
import java.util.*
/**
* @see {@link http://wiki.vg}
*/
object MojangDownloadProvider : DownloadProvider() {
override val libraryBaseURL: String = "https://libraries.minecraft.net/"
override val versionBaseURL: String = "http://s3.amazonaws.com/Minecraft.Download/versions/"
@@ -37,15 +44,6 @@ object MojangDownloadProvider : DownloadProvider() {
}
override fun injectURL(baseURL: String): String {
/**if (baseURL.contains("scala-swing") || baseURL.contains("scala-xml") || baseURL.contains("scala-parser-combinators"))
return baseURL.replace("http://files.minecraftforge.net/maven", "http://ftb.cursecdn.com/FTB2/maven/");
else if (baseURL.contains("typesafe") || baseURL.contains("scala"))
if (Locale.getDefault() == Locale.CHINA)
return baseURL.replace("http://files.minecraftforge.net/maven", "http://maven.aliyun.com/nexus/content/groups/public");
else
return baseURL.replace("http://files.minecraftforge.net/maven", "http://repo1.maven.org/maven2");
else
return baseURL; */
if (baseURL.endsWith("net/minecraftforge/forge/json"))
return baseURL
else if (Locale.getDefault() == Locale.CHINA)

View File

@@ -19,14 +19,18 @@ package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.util.VersionNumber
import java.util.*
import kotlin.Comparator
/**
* The remote version.
*
* @param gameVersion the Minecraft version that this remote version suits.
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
* @param tag some necessary information for Installer Task.
*/
data class RemoteVersion<T> (
val gameVersion: String,
val selfVersion: String,
/**
* The file of remote version, may be an installer or an universal jar.
*/
val url: String,
val tag: T
): Comparable<RemoteVersion<T>> {
@@ -39,6 +43,7 @@ data class RemoteVersion<T> (
}
override fun compareTo(other: RemoteVersion<T>): Int {
// newer versions are smaller than older versions
return -selfVersion.compareTo(other.selfVersion)
}

View File

@@ -22,20 +22,37 @@ import org.jackhuang.hmcl.util.SimpleMultimap
import java.util.*
import kotlin.collections.HashMap
/**
* The remote version list.
* @param T The type of RemoteVersion<T>, the type of tags.
*/
abstract class VersionList<T> {
@JvmField
/**
* the remote version list.
* key: game version.
* values: corresponding remote versions.
*/
protected val versions = SimpleMultimap<String, RemoteVersion<T>>(::HashMap, ::TreeSet)
/**
* True if the version list has been loaded.
*/
val loaded = versions.isNotEmpty
/**
* @param downloadProvider DownloadProvider
* @return the task to reload the remote version list.
*/
abstract fun refreshAsync(downloadProvider: DownloadProvider): Task
protected open fun getVersionsImpl(gameVersion: String): Collection<RemoteVersion<T>> {
private fun getVersionsImpl(gameVersion: String): Collection<RemoteVersion<T>> {
val ans = versions[gameVersion]
return if (ans.isEmpty()) versions.values else ans
}
/**
* Get the remote versions that specifics Minecraft version.
*
* @param gameVersion the Minecraft version that remote versions belong to
* @return the collection of specific remote versions
*/
@@ -43,6 +60,13 @@ abstract class VersionList<T> {
return Collections.unmodifiableCollection(getVersionsImpl(gameVersion))
}
/**
* Get the specific remote version.
*
* @param gameVersion the Minecraft version that remote versions belong to
* @param remoteVersion the version of the remote version.
* @return the specific remote version, null if it is not found.
*/
fun getVersion(gameVersion: String, remoteVersion: String): RemoteVersion<T>? {
var result : RemoteVersion<T>? = null
versions[gameVersion].forEach {

View File

@@ -15,8 +15,11 @@
* 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.download
package org.jackhuang.hmcl.download.forge
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.game.GameLibrariesTask
import org.jackhuang.hmcl.game.Library
import org.jackhuang.hmcl.game.SimpleVersionProvider
import org.jackhuang.hmcl.game.Version
@@ -30,9 +33,9 @@ import java.io.IOException
import java.util.zip.ZipFile
class ForgeInstallTask(private val dependencyManager: DefaultDependencyManager,
private val gameVersion: String,
private val version: Version,
private val remoteVersion: String) : TaskResult<Version>() {
private val gameVersion: String,
private val version: Version,
private val remoteVersion: String) : TaskResult<Version>() {
private val forgeVersionList = dependencyManager.getVersionList("forge")
private val installer: File = File("forge-installer.jar").absoluteFile
lateinit var remote: RemoteVersion<*>

View File

@@ -15,7 +15,7 @@
* 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.download
package org.jackhuang.hmcl.download.forge
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.game.Version

View File

@@ -15,8 +15,11 @@
* 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.download
package org.jackhuang.hmcl.download.forge
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.VersionList
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.*

View File

@@ -15,8 +15,11 @@
* 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.download
package org.jackhuang.hmcl.download.game
import org.jackhuang.hmcl.download.AbstractDependencyManager
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.DependencyManager
import org.jackhuang.hmcl.game.AssetIndex
import org.jackhuang.hmcl.game.AssetObject
import org.jackhuang.hmcl.game.DownloadType
@@ -30,13 +33,12 @@ import java.io.IOException
import java.util.*
import java.util.logging.Level
/**
* This task is to download game libraries.
* This task should be executed last(especially after game downloading, Forge, LiteLoader and OptiFine install task)
* @param resolvedVersion the <b>resolved</b> version
*/
class GameLibrariesTask(private val dependencyManager: DefaultDependencyManager, private val resolvedVersion: Version): Task() {
class GameLibrariesTask(private val dependencyManager: AbstractDependencyManager, private val resolvedVersion: Version): Task() {
override val dependencies = LinkedList<Task>()
override fun execute() {
for (library in resolvedVersion.libraries)
@@ -49,7 +51,12 @@ class GameLibrariesTask(private val dependencyManager: DefaultDependencyManager,
}
class GameLoggingDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
/**
* This task is to download log4j configuration file provided in minecraft.json.
* @param dependencyManager the dependency manager that can provides proxy settings and [GameRepository]
* @param version the **resolved** version
*/
class GameLoggingDownloadTask(private val dependencyManager: DependencyManager, private val version: Version) : Task() {
override val dependencies = LinkedList<Task>()
override fun execute() {
val logging = version.logging?.get(DownloadType.CLIENT) ?: return
@@ -59,6 +66,11 @@ class GameLoggingDownloadTask(private val dependencyManager: DefaultDependencyMa
}
}
/**
* This task is to download asset index file provided in minecraft.json.
* @param dependencyManager the dependency manager that can provides proxy settings and [GameRepository]
* @param version the **resolved** version
*/
class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
override val dependencies = LinkedList<Task>()
override fun execute() {
@@ -72,6 +84,11 @@ class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependenc
}
}
/**
* This task is to extract all asset objects described in asset index json.
* @param dependencyManager the dependency manager that can provides proxy settings and [GameRepository]
* @param version the **resolved** version
*/
class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : TaskResult<Collection<Pair<File, AssetObject>>>() {
private val assetIndexTask = GameAssetIndexDownloadTask(dependencyManager, version)
private val assetIndexInfo = version.actualAssetIndex
@@ -100,6 +117,11 @@ class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManag
}
}
/**
* This task is to download all asset objects provided in asset index json.
* @param dependencyManager the dependency manager that can provides proxy settings and [GameRepository]
* @param version the **resolved** version
*/
class GameAssetDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
private val refreshTask = GameAssetRefreshTask(dependencyManager, version)
override val dependents = listOf(refreshTask)
@@ -140,6 +162,11 @@ class GameAssetDownloadTask(private val dependencyManager: DefaultDependencyMana
}
}
/**
* This task is to save the version json.
* @param dependencyManager the dependency manager that can provides proxy settings and [GameRepository]
* @param version the **resolved** version
*/
class VersionJSONSaveTask(private val dependencyManager: DefaultDependencyManager, private val version: Version): Task() {
override fun execute() {
val json = dependencyManager.repository.getVersionJson(version.id).absoluteFile

View File

@@ -15,7 +15,7 @@
* 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.download
package org.jackhuang.hmcl.download.game
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.game.ReleaseType

View File

@@ -15,13 +15,16 @@
* 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.download
package org.jackhuang.hmcl.download.game
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.VersionList
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.asVersion
import org.jackhuang.hmcl.util.fromJson
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.toURL

View File

@@ -15,8 +15,11 @@
* 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.download
package org.jackhuang.hmcl.download.liteloader
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.game.GameLibrariesTask
import org.jackhuang.hmcl.game.LibrariesDownloadInfo
import org.jackhuang.hmcl.game.Library
import org.jackhuang.hmcl.game.LibraryDownloadInfo
@@ -27,12 +30,12 @@ import org.jackhuang.hmcl.task.then
import org.jackhuang.hmcl.util.merge
/**
* LiteLoader must be installed after Forge.
* Note: LiteLoader must be installed after Forge.
*/
class LiteLoaderInstallTask(private val dependencyManager: DefaultDependencyManager,
private val gameVersion: String,
private val version: Version,
private val remoteVersion: String): TaskResult<Version>() {
private val gameVersion: String,
private val version: Version,
private val remoteVersion: String): TaskResult<Version>() {
private val liteLoaderVersionList = dependencyManager.getVersionList("liteloader") as LiteLoaderVersionList
lateinit var remote: RemoteVersion<LiteLoaderRemoteVersionTag>
override val dependents = mutableListOf<Task>()

View File

@@ -15,7 +15,7 @@
* 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.download
package org.jackhuang.hmcl.download.liteloader
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.game.Library

View File

@@ -15,13 +15,16 @@
* 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.download
package org.jackhuang.hmcl.download.liteloader
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.VersionList
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.asVersion
import org.jackhuang.hmcl.util.fromJson
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.toURL
object LiteLoaderVersionList : VersionList<LiteLoaderRemoteVersionTag>() {

View File

@@ -15,15 +15,17 @@
* 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.download
package org.jackhuang.hmcl.download.optifine
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.VersionList
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.asVersion
import org.jackhuang.hmcl.util.toURL
import org.jackhuang.hmcl.util.typeOf
import java.util.TreeSet
object OptiFineBMCLVersionList : VersionList<Unit>() {
override fun refreshAsync(downloadProvider: DownloadProvider): Task {

View File

@@ -15,7 +15,7 @@
* 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.download
package org.jackhuang.hmcl.download.optifine
object Opt

View File

@@ -15,8 +15,11 @@
* 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.download
package org.jackhuang.hmcl.download.optifine
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.game.GameLibrariesTask
import org.jackhuang.hmcl.game.LibrariesDownloadInfo
import org.jackhuang.hmcl.game.Library
import org.jackhuang.hmcl.game.LibraryDownloadInfo
@@ -26,10 +29,13 @@ import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.task.then
import org.jackhuang.hmcl.util.merge
/**
* **Note**: OptiFine should be installed in the end.
*/
class OptiFineInstallTask(private val dependencyManager: DefaultDependencyManager,
private val gameVersion: String,
private val version: Version,
private val remoteVersion: String): TaskResult<Version>() {
private val gameVersion: String,
private val version: Version,
private val remoteVersion: String): TaskResult<Version>() {
private val optiFineVersionList = dependencyManager.getVersionList("optifine")
lateinit var remote: RemoteVersion<*>
override val dependents = mutableListOf<Task>()

View File

@@ -15,7 +15,7 @@
* 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.download
package org.jackhuang.hmcl.download.optifine
import com.google.gson.annotations.SerializedName

View File

@@ -15,15 +15,18 @@
* 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.download
package org.jackhuang.hmcl.download.optifine
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.VersionList
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.toURL
import org.w3c.dom.Element
import java.io.ByteArrayInputStream
import javax.xml.parsers.DocumentBuilderFactory
import java.util.regex.Pattern
import javax.xml.parsers.DocumentBuilderFactory
object OptiFineVersionList : VersionList<Unit>() {
private val pattern = Pattern.compile("OptiFine (.*?) ")

View File

@@ -17,8 +17,8 @@
*/
package org.jackhuang.hmcl.event
import org.jackhuang.hmcl.util.JavaProcess
import java.util.EventObject
import org.jackhuang.hmcl.util.ManagedProcess
import java.util.*
/**
* This event gets fired when loading versions in a .minecraft folder.
@@ -59,7 +59,7 @@ class LoadedOneVersionEvent(source: Any, val version: String) : EventObject(sour
* @param JavaProcess The process that exited abnormally.
* @author huangyuhui
*/
class JavaProcessExitedAbnormallyEvent(source: Any, val value: JavaProcess) : EventObject(source)
class JavaProcessExitedAbnormallyEvent(source: Any, val value: ManagedProcess) : EventObject(source)
/**
* This event gets fired when minecraft process exited successfully and the exit code is 0.
@@ -69,7 +69,7 @@ class JavaProcessExitedAbnormallyEvent(source: Any, val value: JavaProcess) : Ev
* @param JavaProcess minecraft process
* @author huangyuhui
*/
class JavaProcessStoppedEvent(source: Any, val value: JavaProcess) : EventObject(source)
class JavaProcessStoppedEvent(source: Any, val value: ManagedProcess) : EventObject(source)
/**
* This event gets fired when we launch the JVM and it got crashed.
@@ -79,4 +79,4 @@ class JavaProcessStoppedEvent(source: Any, val value: JavaProcess) : EventObject
* @param JavaProcess the crashed process.
* @author huangyuhui
*/
class JVMLaunchFailedEvent(source: Any, val value: JavaProcess) : EventObject(source)
class JVMLaunchFailedEvent(source: Any, val value: ManagedProcess) : EventObject(source)

View File

@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game
import jdk.nashorn.internal.ir.annotations.Immutable
import java.net.URL
@Immutable
class AssetIndexInfo(

View File

@@ -20,6 +20,9 @@ package org.jackhuang.hmcl.game
import org.jackhuang.hmcl.util.Immutable
import java.io.File
/**
* The Minecraft version for 1.5.x and earlier.
*/
@Immutable
class ClassicVersion : Version(
mainClass = "net.minecraft.client.Minecraft",
@@ -41,6 +44,9 @@ class ClassicVersion : Version(
)
companion object {
/**
* Check if this directory is an old style Minecraft repository.
*/
fun hasClassicVersion(baseDirectory: File): Boolean {
val file = File(baseDirectory, "bin")
if (!file.exists()) return false

View File

@@ -18,7 +18,10 @@
package org.jackhuang.hmcl.game
import com.google.gson.JsonSyntaxException
import org.jackhuang.hmcl.event.*
import org.jackhuang.hmcl.event.EVENT_BUS
import org.jackhuang.hmcl.event.LoadedOneVersionEvent
import org.jackhuang.hmcl.event.RefreshedVersionsEvent
import org.jackhuang.hmcl.event.RefreshingVersionsEvent
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.fromJson
@@ -28,6 +31,9 @@ import java.io.IOException
import java.util.*
import java.util.logging.Level
/**
* An implementation of classic Minecraft game repository.
*/
open class DefaultGameRepository(var baseDirectory: File): GameRepository {
protected val versions: MutableMap<String, Version> = TreeMap<String, Version>()
@@ -44,6 +50,11 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
val id = v.jar ?: v.id
return getVersionRoot(id).resolve("$id.jar")
}
/**
* {@inheritsDoc}
* @return something like ".minecraft/versions/<version name>/<version name>-natives"
*/
override fun getNativeDirectory(id: String) = File(getVersionRoot(id), "$id-natives")
override fun getVersionRoot(id: String) = File(baseDirectory, "versions/$id")
open fun getVersionJson(id: String) = File(getVersionRoot(id), "$id.json")
@@ -83,7 +94,6 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
}
protected open fun refreshVersionsImpl() {
versions.clear()
if (ClassicVersion.hasClassicVersion(baseDirectory)) {
@@ -167,7 +177,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
return assetDir.resolve("objects/${obj.location}")
}
open fun getIndexFile(version: String, assetId: String): File =
override fun getIndexFile(version: String, assetId: String): File =
getAssetDirectory(version, assetId).resolve("indexes/$assetId.json")
override fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File =
@@ -176,9 +186,8 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
@Throws(IOException::class, JsonSyntaxException::class)
protected open fun reconstructAssets(version: String, assetId: String): File {
val assetsDir = getAssetDirectory(version, assetId)
val assetVersion = assetId
val indexFile: File = getIndexFile(version, assetVersion)
val virtualRoot = assetsDir.resolve("virtual").resolve(assetVersion)
val indexFile: File = getIndexFile(version, assetId)
val virtualRoot = assetsDir.resolve("virtual").resolve(assetId)
if (!indexFile.isFile) {
return assetsDir

View File

@@ -18,13 +18,15 @@
package org.jackhuang.hmcl.game
import java.io.File
import java.io.IOException
/**
* Supports operations on versioning.
*
* Note that game repository will not do any tasks that connect to Internet.
* Note that game repository will not do any tasks that connect to Internet, if do, see [org.jackhuang.hmcl.download.DependencyManager]
*/
interface GameRepository : VersionProvider {
/**
* Does the version of id exist?
* @param id the id of version
@@ -126,7 +128,8 @@ interface GameRepository : VersionProvider {
* Will reconstruct assets or do some blocking tasks if necessary.
* You'd better create a new thread to invoke this method.
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
* @throws java.io.IOException if I/O operation fails.
* @return the actual asset directory
*/
@@ -134,24 +137,31 @@ interface GameRepository : VersionProvider {
/**
* Get the asset directory according to the asset id.
*
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
* @return the asset directory
*/
fun getAssetDirectory(version: String, assetId: String): File
/**
* Get the file that given asset object refers to
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @param name the asset object name, [AssetIndex.objects.key]
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
* @param name the asset object name, you can find it in [AssetIndex.objects.keys]
* @throws java.io.IOException if I/O operation fails.
* @return the file that given asset object refers to
*/
@Throws(IOException::class)
fun getAssetObject(version: String, assetId: String, name: String): File
/**
* Get the file that given asset object refers to
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @param obj the asset object, [AssetIndex.objects]
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
* @param obj the asset object, you can find it in [AssetIndex.objects]
* @return the file that given asset object refers to
*/
fun getAssetObject(version: String, assetId: String, obj: AssetObject): File
@@ -159,17 +169,28 @@ interface GameRepository : VersionProvider {
/**
* Get asset index that assetId represents
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
* @return the asset index
*/
fun getAssetIndex(version: String, assetId: String): AssetIndex
/**
* Get the asset_index.json which includes asset objects information.
*
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
*/
fun getIndexFile(version: String, assetId: String): File
/**
* Get logging object
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.assets]
* @param loggingInfo the logging info
* @return the file that loggingInfo refers to
*/
fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File
}

View File

@@ -19,10 +19,10 @@ package org.jackhuang.hmcl.game
import org.jackhuang.hmcl.util.closeQuietly
import org.jackhuang.hmcl.util.readFullyAsByteArray
import java.io.File
import java.io.IOException
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.io.File
private fun lessThan32(b: ByteArray, startIndex: Int): Int {
for (i in startIndex until b.size)

View File

@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.game
import com.google.gson.JsonSyntaxException
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.util.Immutable
import org.jackhuang.hmcl.util.Validation

View File

@@ -23,6 +23,7 @@ package org.jackhuang.hmcl.game
* @see Version.resolve
*/
interface VersionProvider {
/**
* Does the version of id exist?
* @param id the id of version
@@ -36,4 +37,5 @@ interface VersionProvider {
* @return the version you want
*/
fun getVersion(id: String): Version
}

View File

@@ -18,14 +18,16 @@
package org.jackhuang.hmcl.launch
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.game.*
import org.jackhuang.hmcl.game.DownloadType
import org.jackhuang.hmcl.game.GameException
import org.jackhuang.hmcl.game.GameRepository
import org.jackhuang.hmcl.game.LaunchOptions
import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.util.*
import kotlin.concurrent.thread
import kotlin.coroutines.experimental.EmptyCoroutineContext.plus
/**
* @param versionId The version to be launched.
@@ -220,7 +222,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
}
}
override fun launch(): JavaProcess {
override fun launch(): ManagedProcess {
// To guarantee that when failed to generate code, we will not call precalled command
val builder = ProcessBuilder(rawCommandLine)
@@ -237,7 +239,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
builder.directory(repository.getRunDirectory(version.id))
.environment().put("APPDATA", options.gameDir.absoluteFile.parent)
val p = JavaProcess(builder.start(), rawCommandLine)
val p = ManagedProcess(builder.start(), rawCommandLine)
if (listener == null)
startMonitors(p)
else
@@ -245,8 +247,8 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
return p
}
fun launchAsync(): TaskResult<JavaProcess> {
return object : TaskResult<JavaProcess>() {
fun launchAsync(): TaskResult<ManagedProcess> {
return object : TaskResult<ManagedProcess>() {
override val id = LAUNCH_ASYNC_ID
override fun execute() {
result = launch()
@@ -279,20 +281,20 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
return scriptFile
}
private fun startMonitors(javaProcess: JavaProcess) {
javaProcess.relatedThreads += thread(name = "stdout-pump", isDaemon = true, block = StreamPump(javaProcess.process.inputStream)::run)
javaProcess.relatedThreads += thread(name = "stderr-pump", isDaemon = true, block = StreamPump(javaProcess.process.errorStream)::run)
private fun startMonitors(managedProcess: ManagedProcess) {
managedProcess.relatedThreads += thread(name = "stdout-pump", isDaemon = true, block = StreamPump(managedProcess.process.inputStream)::run)
managedProcess.relatedThreads += thread(name = "stderr-pump", isDaemon = true, block = StreamPump(managedProcess.process.errorStream)::run)
}
private fun startMonitors(javaProcess: JavaProcess, processListener: ProcessListener, isDaemon: Boolean = true) {
processListener.setProcess(javaProcess)
val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); javaProcess.lines += line }.apply { start() }
javaProcess.relatedThreads += logHandler
val stdout = thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.inputStream, { logHandler.newLine(it) } )::run)
javaProcess.relatedThreads += stdout
val stderr = thread(name = "stderr-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.errorStream, { processListener.onLog(it + OS.LINE_SEPARATOR, Log4jLevel.ERROR); javaProcess.lines += it })::run)
javaProcess.relatedThreads += stderr
javaProcess.relatedThreads += thread(name = "exit-waiter", isDaemon = isDaemon, block = ExitWaiter(javaProcess, listOf(stdout, stderr), { exitCode, exitType -> logHandler.onStopped(); processListener.onExit(exitCode, exitType) })::run)
private fun startMonitors(managedProcess: ManagedProcess, processListener: ProcessListener, isDaemon: Boolean = true) {
processListener.setProcess(managedProcess)
val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); managedProcess.lines += line }.apply { start() }
managedProcess.relatedThreads += logHandler
val stdout = thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(managedProcess.process.inputStream, { logHandler.newLine(it) } )::run)
managedProcess.relatedThreads += stdout
val stderr = thread(name = "stderr-pump", isDaemon = isDaemon, block = StreamPump(managedProcess.process.errorStream, { processListener.onLog(it + OS.LINE_SEPARATOR, Log4jLevel.ERROR); managedProcess.lines += it })::run)
managedProcess.relatedThreads += stderr
managedProcess.relatedThreads += thread(name = "exit-waiter", isDaemon = isDaemon, block = ExitWaiter(managedProcess, listOf(stdout, stderr), { exitCode, exitType -> logHandler.onStopped(); processListener.onExit(exitCode, exitType) })::run)
}
companion object {

View File

@@ -21,16 +21,15 @@ import org.jackhuang.hmcl.event.EVENT_BUS
import org.jackhuang.hmcl.event.JVMLaunchFailedEvent
import org.jackhuang.hmcl.event.JavaProcessExitedAbnormallyEvent
import org.jackhuang.hmcl.event.JavaProcessStoppedEvent
import org.jackhuang.hmcl.util.JavaProcess
import org.jackhuang.hmcl.util.ManagedProcess
import org.jackhuang.hmcl.util.containsOne
import org.jackhuang.hmcl.util.guessLogLineError
import java.util.*
/**
* @param process the process to wait for
* @param watcher the callback that will be called after process stops.
*/
internal class ExitWaiter(val process: JavaProcess, val joins: Collection<Thread>, val watcher: (Int, ProcessListener.ExitType) -> Unit) : Runnable {
internal class ExitWaiter(val process: ManagedProcess, val joins: Collection<Thread>, val watcher: (Int, ProcessListener.ExitType) -> Unit) : Runnable {
override fun run() {
try {
process.process.waitFor()

View File

@@ -21,7 +21,7 @@ import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.game.GameRepository
import org.jackhuang.hmcl.game.LaunchOptions
import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.util.JavaProcess
import org.jackhuang.hmcl.util.ManagedProcess
import java.io.File
abstract class Launcher(
@@ -34,7 +34,7 @@ abstract class Launcher(
val version: Version = repository.getVersion(versionId).resolve(repository)
abstract val rawCommandLine: List<String>
abstract fun launch(): JavaProcess
abstract fun launch(): ManagedProcess
/**
* @param file The file path without extension

View File

@@ -33,6 +33,7 @@ import java.util.concurrent.atomic.AtomicBoolean
/**
* This class is to parse log4j classic XML layout logging, since only vanilla Minecraft will enable this layout.
* Also supports plain logs.
*/
internal class Log4jHandler(val callback: (String, Log4jLevel) -> Unit) : Thread() {
val reader = XMLReaderFactory.createXMLReader().apply {
@@ -53,6 +54,9 @@ internal class Log4jHandler(val callback: (String, Log4jLevel) -> Unit) : Thread
}
}
/**
* Should be called to stop [Log4jHandler] manually.
*/
fun onStopped() {
if (interrupted.get())
return
@@ -66,7 +70,7 @@ internal class Log4jHandler(val callback: (String, Log4jLevel) -> Unit) : Thread
}
/**
* Should be called in [ProcessListener.onErrorLog] and [ProcessListener.onLog] manually.
* New XML line.
*/
fun newLine(content: String) =
Scheduler.COMPUTATION.schedule {
@@ -102,6 +106,7 @@ internal class Log4jHandler(val callback: (String, Log4jLevel) -> Unit) : Thread
l = Log4jLevel.ERROR
}
"log4j_Message" -> readingMessage = true
"log4j_Throwable" -> {}
}
}

View File

@@ -17,15 +17,15 @@
*/
package org.jackhuang.hmcl.launch
import org.jackhuang.hmcl.util.JavaProcess
import org.jackhuang.hmcl.util.Log4jLevel
import org.jackhuang.hmcl.util.ManagedProcess
interface ProcessListener {
/**
* When a game launched, this method will be called to get the new process.
* You should not override this method when your ProcessListener is shared with all processes.
*/
fun setProcess(process: JavaProcess) {}
fun setProcess(process: ManagedProcess) {}
/**
* Called when receiving a log from stdout/stderr.

View File

@@ -23,6 +23,12 @@ import java.io.IOException
import java.io.InputStream
import java.util.logging.Level
/**
* Pump the given input stream.
* @param inputStream the input stream to pump
* @param callback receives each line
*
*/
internal class StreamPump @JvmOverloads constructor(
private val inputStream: InputStream,
private val callback: (String) -> Unit = {}

View File

@@ -99,14 +99,27 @@ class CurseForgeModpackManifestFile (
val url: URL get() = "https://minecraft.curseforge.com/projects/$projectID/files/$fileID/download".toURL()
}
/**
* @param f the CurseForge modpack file.
* @return the manifest.
*/
fun readCurseForgeModpackManifest(f: File): CurseForgeModpackManifest {
ZipFile(f).use { zipFile ->
val entry = zipFile.getEntry("manifest.json") ?: throw IOException("Manifest.json not found. Not a valid CurseForge modpack.")
val entry = zipFile.getEntry("manifest.json") ?: throw IOException("`manifest.json` not found. Not a valid CurseForge modpack.")
val json = zipFile.getInputStream(entry).readFullyAsString()
return GSON.fromJson<CurseForgeModpackManifest>(json) ?: throw JsonParseException("Manifest.json not found. Not a valid CurseForge modpack.")
return GSON.fromJson<CurseForgeModpackManifest>(json) ?: throw JsonParseException("`manifest.json` not found. Not a valid CurseForge modpack.")
}
}
/**
* Install a downloaded CurseForge modpack.
*
* @param dependencyManager the dependency manager.
* @param zipFile the CurseForge modpack file.
* @param manifest The manifest content of given CurseForge modpack.
* @param name the new version name
* @see readCurseForgeModpackManifest
*/
class CurseForgeModpackInstallTask(private val dependencyManager: DefaultDependencyManager, private val zipFile: File, private val manifest: CurseForgeModpackManifest, private val name: String): Task() {
val repository = dependencyManager.repository
init {
@@ -136,7 +149,8 @@ class CurseForgeModpackInstallTask(private val dependencyManager: DefaultDepende
f.fileName = f.url.detectFileName(dependencyManager.proxy)
dependencies += FileDownloadTask(f.url, run.resolve("mods").resolve(f.fileName), proxy = dependencyManager.proxy)
} catch (e: IOException) {
// ignore it and retry next time.
// Because in China, the CurseForge is too difficult to visit.
// So if failed, ignore it and retry next time.
}
++finished
updateProgress(1.0 * finished / manifest.files.size)
@@ -146,6 +160,12 @@ class CurseForgeModpackInstallTask(private val dependencyManager: DefaultDepende
}
}
/**
* Complete the CurseForge version.
*
* @param dependencyManager the dependency manager.
* @param version the existent and physical version.
*/
class CurseForgeModpackCompletionTask(dependencyManager: DependencyManager, version: String): Task() {
val repository = dependencyManager.repository
val run = repository.getRunDirectory(version)
@@ -161,6 +181,8 @@ class CurseForgeModpackCompletionTask(dependencyManager: DependencyManager, vers
else {
manifest = GSON.fromJson<CurseForgeModpackManifest>(repository.getVersionRoot(version).resolve("manifest.json").readText())!!
// Because in China, the CurseForge is too difficult to visit.
// So caching the file name is necessary.
for (f in manifest!!.files) {
if (f.fileName.isBlank())
dependents += task { f.fileName = f.url.detectFileName(proxy) }

View File

@@ -26,7 +26,7 @@ import org.jackhuang.hmcl.util.typeOf
import java.io.File
import java.util.zip.ZipFile
class ForgeModMetadata(
class ForgeModMetadata @JvmOverloads internal constructor(
@SerializedName("modid")
val modId: String = "",
val name: String = "",
@@ -42,6 +42,9 @@ class ForgeModMetadata(
) {
companion object {
/**
* Read Forge mod ModInfo.
*/
fun fromFile(modFile: File): ModInfo {
ZipFile(modFile).use {
val entry = it.getEntry("mcmod.info") ?: throw JsonParseException("File $modFile is not a Forge mod.")

View File

@@ -18,11 +18,13 @@
package org.jackhuang.hmcl.mod
import com.google.gson.JsonParseException
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.fromJson
import org.jackhuang.hmcl.util.readFullyAsString
import java.io.File
import java.util.zip.ZipFile
class LiteModMetadata (
class LiteModMetadata @JvmOverloads internal constructor(
val name: String = "",
val version: String = "",
val mcversion: String = "",
@@ -37,6 +39,9 @@ class LiteModMetadata (
) {
companion object {
/**
* Read LiteLoader mod ModInfo.
*/
fun fromFile(modFile: File): ModInfo {
ZipFile(modFile).use {
val entry = it.getEntry("litemod.json")

View File

@@ -17,9 +17,9 @@
*/
package org.jackhuang.hmcl.mod
import com.google.gson.JsonParseException
import org.jackhuang.hmcl.util.property.ImmediateBooleanProperty
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.ImmediateBooleanProperty
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.setValue
import java.io.File
class ModInfo (

View File

@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.mod
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.task.Task
import java.io.File
class Modpack(val file: File) {

View File

@@ -19,6 +19,13 @@ package org.jackhuang.hmcl.task
import org.jackhuang.hmcl.util.AutoTypingMap
/**
* A task that combines two tasks and make sure [pred] runs before [succ].
*
* @param pred the task that runs before [succ]
* @param succ a callback that returns the task runs after [pred], [succ] will be executed asynchronously. You can do something that relies on the result of [pred].
* @param reliant true if this task chain will be broken when task [pred] fails.
*/
internal class CoupleTask<P: Task>(pred: P, private val succ: (AutoTypingMap<String>) -> Task?, override val reliant: Boolean) : Task() {
override val hidden: Boolean = true

View File

@@ -19,28 +19,39 @@ package org.jackhuang.hmcl.task
import org.jackhuang.hmcl.event.EventManager
import org.jackhuang.hmcl.event.FailedEvent
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.closeQuietly
import org.jackhuang.hmcl.util.DigestUtils
import org.jackhuang.hmcl.util.makeDirectory
import org.jackhuang.hmcl.util.createConnection
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.RandomAccessFile
import java.math.BigInteger
import java.net.Proxy
import java.net.URL
import java.math.BigInteger
import java.util.logging.Level
/**
* A task that can download a file online.
*
* @param url the URL of remote file.
* @param file the location that download to.
* @param hash the SHA-1 hash code of remote file, null if the hash is unknown or it is no need to check the hash code.
* @param retry the times for retrying if downloading fails.
* @param proxy the proxy.
*
* @author huangyuhui
*/
class FileDownloadTask @JvmOverloads constructor(val url: URL, val file: File, val hash: String? = null, val retry: Int = 5, val proxy: Proxy = Proxy.NO_PROXY): Task() {
override val scheduler: Scheduler = Scheduler.IO
/**
* Once downloading fails, this event will be fired to gain the substitute URL.
*/
var onFailed = EventManager<FailedEvent<URL>>()
private var rFile: RandomAccessFile? = null
private var stream: InputStream? = null
fun closeFiles() {
private fun closeFiles() {
rFile?.closeQuietly()
rFile = null
stream?.closeQuietly()

View File

@@ -25,6 +25,17 @@ import java.net.Proxy
import java.net.URL
import java.nio.charset.Charset
/**
* A task that can read the content of a remote text file.
*
* @param url the URL of remote text file.
* @param encoding the encoding/charset of the remote text file.
* @param retry the times for retrying if downloading fails.
* @param proxy the proxy.
* @param id the result variable id, see [Task.variables]
*
* @author huangyuhui
*/
class GetTask @JvmOverloads constructor(val url: URL, val encoding: Charset = Charsets.UTF_8, private val retry: Int = 5, private val proxy: Proxy = Proxy.NO_PROXY, override val id: String = ID): TaskResult<String>() {
override val scheduler: Scheduler = Scheduler.IO
@@ -65,6 +76,9 @@ class GetTask @JvmOverloads constructor(val url: URL, val encoding: Charset = Ch
}
companion object {
/**
* The default task result ID.
*/
const val ID = "http_get"
}
}

View File

@@ -17,6 +17,11 @@
*/
package org.jackhuang.hmcl.task
/**
* The tasks that provides a way to execute tasks parallelly.
*
* @param tasks the tasks that can be executed parallelly.
*/
class ParallelTask(vararg tasks: Task): Task() {
override val hidden: Boolean = true
override val dependents: Collection<Task> = listOf(*tasks)

View File

@@ -17,13 +17,25 @@
*/
package org.jackhuang.hmcl.task
import javafx.application.Platform
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicReference
import javax.swing.SwingUtilities
/**
* Determines how a task is executed.
*
* @see [Task.scheduler]
*/
interface Scheduler {
/**
* Schedules the given task.
* @return the future, null if future is not supported.
*/
fun schedule(block: () -> Unit): Future<*>? = schedule(Callable { block() })
/**
* Schedules the given task.
* @return the future, null if future is not supported.
*/
fun schedule(block: Callable<Unit>): Future<*>?
private class SchedulerImpl(val executor: (Runnable) -> Unit) : Scheduler {
@@ -58,7 +70,7 @@ interface Scheduler {
}
private class SchedulerExecutorService(val executorService: ExecutorService) : Scheduler {
override fun schedule(block: Callable<Unit>) = executorService.submit(block)
override fun schedule(block: Callable<Unit>): Future<*> = executorService.submit(block)
}
companion object Schedulers {
@@ -90,13 +102,44 @@ interface Scheduler {
return null
}
}
val JAVAFX: Scheduler = SchedulerImpl(Platform::runLater)
val SWING: Scheduler = SchedulerImpl(SwingUtilities::invokeLater)
/**
* A scheduler for JavaFX UI operations.
*/
val JAVAFX: Scheduler = SchedulerImpl(javafx.application.Platform::runLater)
/**
* A scheduler for Swing UI operations.
*/
val SWING: Scheduler = SchedulerImpl(javax.swing.SwingUtilities::invokeLater)
/**
* A scheduler that always create new threads to execute tasks.
* For tasks that do not do heavy operations.
*/
val NEW_THREAD: Scheduler = SchedulerExecutorService(CACHED_EXECUTOR)
/**
* A scheduler that exclusively executes tasks that do I/O operations.
* The number tasks that do I/O operations in the meantime cannot be larger then 6.
*/
val IO: Scheduler = SchedulerExecutorService(IO_EXECUTOR)
/**
* A scheduler that exclusively executes tasks that do computations.
* The best way to do computations is an event queue.
*/
val COMPUTATION: Scheduler = SchedulerExecutorService(SINGLE_EXECUTOR)
/**
* The default scheduler for tasks to be executed.
* @see [Task.scheduler]
*/
val DEFAULT = NEW_THREAD
/**
* Shut down all executor services to guarantee that the application can stop implicitly.
*/
fun shutdown() {
CACHED_EXECUTOR.shutdown()
IO_EXECUTOR.shutdown()

View File

@@ -17,8 +17,7 @@
*/
package org.jackhuang.hmcl.task
class SilentException : Exception {
constructor() : super() {}
constructor(message: String) : super(message) {}
constructor(message: String, cause: Throwable) : super(message, cause) {}
}
/**
* If a task throws [SilentException], the task will be marked as failure but do not log the stacktrace.
*/
class SilentException : Exception()

View File

@@ -22,12 +22,15 @@ import javafx.beans.property.ReadOnlyDoubleWrapper
import javafx.beans.property.ReadOnlyStringProperty
import javafx.beans.property.ReadOnlyStringWrapper
import org.jackhuang.hmcl.event.EventManager
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.AutoTypingMap
import org.jackhuang.hmcl.util.updateAsync
import java.util.concurrent.Callable
import java.util.concurrent.atomic.AtomicReference
/**
* Disposable task.
*
* @see [TaskExecutor]
*/
abstract class Task {
/**

View File

@@ -19,11 +19,13 @@ package org.jackhuang.hmcl.task
import org.jackhuang.hmcl.util.AutoTypingMap
import org.jackhuang.hmcl.util.LOG
import java.util.concurrent.*
import java.util.concurrent.Callable
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Future
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import kotlin.concurrent.thread
class TaskExecutor() {
var taskListener: TaskListener? = null

View File

@@ -17,6 +17,9 @@
*/
package org.jackhuang.hmcl.task
/**
* A task that has a result.
*/
abstract class TaskResult<V> : Task() {
open var result: V? = null

View File

@@ -17,6 +17,9 @@
*/
package org.jackhuang.hmcl.util
/**
* Mark if the model is immutable.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Immutable

View File

@@ -17,6 +17,9 @@
*/
package org.jackhuang.hmcl.util
/**
* A map that support auto casting.
*/
class AutoTypingMap<K>(private val impl: MutableMap<K, Any>) {
fun clear() = impl.clear()

View File

@@ -18,10 +18,6 @@
package org.jackhuang.hmcl.util
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
fun File.makeDirectory(): Boolean = isDirectory || mkdirs()

View File

@@ -20,15 +20,12 @@ package org.jackhuang.hmcl.util
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import java.lang.reflect.Type
import com.google.gson.stream.JsonToken
import java.io.IOException
import com.google.gson.TypeAdapter
import com.google.gson.Gson
import com.google.gson.TypeAdapterFactory
import com.google.gson.stream.JsonWriter
import org.jackhuang.hmcl.game.Library
import java.io.File
import java.io.IOException
import java.lang.reflect.Type
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat

View File

@@ -64,6 +64,10 @@ fun InputStream.copyToAndClose(dest: OutputStream) {
}
}
/**
* @param cmd the command line
* @return the final command line
*/
fun makeCommand(cmd: List<String>): String {
val cmdbuf = StringBuilder(120)
for (i in cmd.indices) {

View File

@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util.property
package org.jackhuang.hmcl.util
import javafx.beans.property.*
import javafx.beans.value.ChangeListener

View File

@@ -24,12 +24,26 @@ import java.io.Serializable
import java.util.*
import java.util.regex.Pattern
/**
* Represents a Java installation.
*/
data class JavaVersion internal constructor(
@SerializedName("location")
val binary: File,
val longVersion: String,
val platform: Platform) : Serializable
{
/**
* The major version of Java installation.
*
* @see JAVA_X
* @see JAVA_9
* @see JAVA_8
* @see JAVA_7
* @see JAVA_6
* @see JAVA_5
* @see UNKNOWN
*/
val version = parseVersion(longVersion)
companion object {
@@ -110,10 +124,12 @@ data class JavaVersion internal constructor(
return path.resolve("java")
}
private val currentJava: JavaVersion = JavaVersion(
binary = getJavaFile(File(System.getProperty("java.home"))),
longVersion = System.getProperty("java.version"),
platform = Platform.PLATFORM)
private val currentJava: JavaVersion by lazy {
JavaVersion(
binary = getJavaFile(File(System.getProperty("java.home"))),
longVersion = System.getProperty("java.version"),
platform = Platform.PLATFORM)
}
fun fromCurrentEnvironment() = currentJava
init {

View File

@@ -18,12 +18,9 @@
package org.jackhuang.hmcl.util
import javafx.beans.property.Property
import javafx.beans.value.ObservableValue
import java.util.*
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.HashMap
import sun.text.normalizer.UTF16.append
import java.lang.reflect.Array.getLength
inline fun ignoreException(func: () -> Unit) {
try {

View File

@@ -18,13 +18,13 @@
@file:JvmName("HMCLog")
package org.jackhuang.hmcl.util
import javafx.scene.paint.Color
import java.io.ByteArrayOutputStream
import java.io.PrintWriter
import java.text.SimpleDateFormat
import java.util.*
import java.util.logging.*
import java.util.logging.Formatter
import javafx.scene.paint.Color
import java.util.regex.Pattern
val LOG = Logger.getLogger("HMCL").apply {

View File

@@ -20,25 +20,58 @@ package org.jackhuang.hmcl.util
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
class JavaProcess(
/**
* The managed process.
*
* @param process the raw system process that this instance manages.
* @param commands the command line of [process].
* @see [org.jackhuang.hmcl.launch.ExitWaiter]
* @see [org.jackhuang.hmcl.launch.StreamPump]
*/
class ManagedProcess(
val process: Process,
val commands: List<String>
) {
/**
* To save some information you need.
*/
val properties = mutableMapOf<String, Any>()
/**
* The standard output/error lines.
*/
val lines: Queue<String> = ConcurrentLinkedQueue<String>()
/**
* The related threads.
*
* If a thread is monitoring this raw process,
* you are required to add the instance to [relatedThreads].
*/
val relatedThreads = mutableListOf<Thread>()
/**
* True if the managed process is running.
*/
val isRunning: Boolean = try {
process.exitValue()
true
} catch (ex: IllegalThreadStateException) {
false
}
/**
* The exit code of raw process.
*/
val exitCode: Int get() = process.exitValue()
override fun toString() = "JavaProcess[commands=$commands, isRunning=$isRunning]"
/**
* Destroys the raw process and other related threads that are monitoring this raw process.
*/
fun stop() {
process.destroy()
relatedThreads.forEach(Thread::interrupt)
}
override fun toString() = "ManagedProcess[commands=$commands, isRunning=$isRunning]"
}

View File

@@ -19,20 +19,19 @@ package org.jackhuang.hmcl.util
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.HttpURLConnection
import java.net.Proxy
import java.net.URL
import java.security.GeneralSecurityException
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.*
import java.util.logging.Level
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
import java.io.OutputStream
import java.util.*
import java.util.logging.Level
import kotlin.text.Charsets
private val XTM = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}

Some files were not shown because too many files have changed in this diff Show More