CurseForge modpack support. Random UserAgent for OptiFine. Fixed mis-repositioning of maronry pane

This commit is contained in:
huangyuhui 2017-08-17 13:14:34 +08:00
parent 1394034160
commit 99f60ea6e5
35 changed files with 2367 additions and 84 deletions

View File

@ -26,6 +26,7 @@ import org.jackhuang.hmcl.util.DEFAULT_USER_AGENT
import org.jackhuang.hmcl.util.LOG import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.OS import org.jackhuang.hmcl.util.OS
import java.io.File import java.io.File
import java.util.concurrent.Callable
import java.util.logging.Level import java.util.logging.Level
fun i18n(key: String): String { fun i18n(key: String): String {
@ -51,12 +52,13 @@ class Main : Application() {
companion object { companion object {
val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@" val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
val TITLE = "HMCL $VERSION" val NAME = "HMCL"
val TITLE = "$NAME $VERSION"
lateinit var PRIMARY_STAGE: Stage lateinit var PRIMARY_STAGE: Stage
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
DEFAULT_USER_AGENT = "Hello Minecraft! Launcher" DEFAULT_USER_AGENT = { "Hello Minecraft! Launcher" }
launch(Main::class.java, *args) launch(Main::class.java, *args)
} }

View File

@ -0,0 +1,34 @@
/*
* 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.Main
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.launch.DefaultLauncher
import org.jackhuang.hmcl.launch.ProcessListener
class HMCLGameLauncher(repository: GameRepository, versionId: String, account: AuthInfo, options: LaunchOptions, listener: ProcessListener? = null, isDaemon: Boolean = true)
: DefaultLauncher(repository, versionId, account, options, listener, isDaemon) {
override fun appendJvmArgs(res: MutableList<String>) {
super.appendJvmArgs(res)
res.add("-Dminecraft.launcher.version=" + Main.VERSION);
res.add("-Dminecraft.launcher.brand=" + Main.NAME);
}
}

View File

@ -19,6 +19,9 @@ package org.jackhuang.hmcl.game
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import javafx.beans.InvalidationListener import javafx.beans.InvalidationListener
import org.jackhuang.hmcl.setting.EnumGameDirectory
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.setting.VersionSetting import org.jackhuang.hmcl.setting.VersionSetting
import org.jackhuang.hmcl.util.GSON import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.LOG import org.jackhuang.hmcl.util.LOG
@ -27,10 +30,44 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.util.logging.Level import java.util.logging.Level
class HMCLGameRepository(baseDirectory: File) class HMCLGameRepository(val profile: Profile, baseDirectory: File)
: DefaultGameRepository(baseDirectory) { : DefaultGameRepository(baseDirectory) {
private val versionSettings = HashMap<String, VersionSetting>() private val versionSettings = mutableMapOf<String, VersionSetting>()
private val beingModpackVersions = mutableSetOf<String>()
private fun useSelf(version: String, assetId: String): Boolean {
val vs = profile.getVersionSetting(version)
return File(baseDirectory, "assets/indexes/$assetId.json").exists() || vs.noCommon
}
override fun getAssetDirectory(version: String, assetId: String): File {
if (useSelf(version, assetId))
return super.getAssetDirectory(version, assetId)
else
return File(Settings.commonPath).resolve("assets")
}
override fun getRunDirectory(id: String): File {
if (beingModpackVersions.contains(id))
return getVersionRoot(id)
else {
val vs = profile.getVersionSetting(id)
return when (vs.gameDirType) {
EnumGameDirectory.VERSION_FOLDER -> getVersionRoot(id)
EnumGameDirectory.ROOT_FOLDER -> super.getRunDirectory(id)
}
}
}
override fun getLibraryFile(version: Version, lib: Library): File {
val vs = profile.getVersionSetting(version.id)
val self = super.getLibraryFile(version, lib);
if (self.exists() || vs.noCommon)
return self;
else
return File(Settings.commonPath).resolve("libraries/${lib.path}")
}
override fun refreshVersionsImpl() { override fun refreshVersionsImpl() {
versionSettings.clear() versionSettings.clear()
@ -91,6 +128,14 @@ class HMCLGameRepository(baseDirectory: File)
return versionSettings[id] return versionSettings[id]
} }
fun markVersionAsModpack(id: String) {
beingModpackVersions += id
}
fun undoMark(id: String) {
beingModpackVersions -= id
}
companion object { companion object {
val PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}" val PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}"
val GSON = GsonBuilder().registerTypeAdapter(VersionSetting::class.java, VersionSetting).setPrettyPrinting().create() val GSON = GsonBuilder().registerTypeAdapter(VersionSetting::class.java, VersionSetting).setPrettyPrinting().create()

View File

@ -17,7 +17,7 @@
*/ */
package org.jackhuang.hmcl.game package org.jackhuang.hmcl.game
import org.jackhuang.hmcl.launch.DefaultLauncher import org.jackhuang.hmcl.mod.CurseForgeModpackCompletionTask
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler import org.jackhuang.hmcl.task.Scheduler
@ -25,16 +25,18 @@ object LauncherHelper {
fun launch() { fun launch() {
val profile = Settings.selectedProfile val profile = Settings.selectedProfile
val repository = profile.repository val repository = profile.repository
val dependency = profile.dependency
val account = Settings.selectedAccount ?: throw IllegalStateException("No account here") val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
val version = repository.getVersion(profile.selectedVersion) val version = repository.getVersion(profile.selectedVersion)
val launcher = DefaultLauncher( val launcher = HMCLGameLauncher(
repository = repository, repository = repository,
versionId = profile.selectedVersion, versionId = profile.selectedVersion,
options = profile.getVersionSetting(profile.selectedVersion).toLaunchOptions(profile.gameDir), options = profile.getVersionSetting(profile.selectedVersion).toLaunchOptions(profile.gameDir),
account = account.logIn(Settings.proxy) account = account.logIn(Settings.proxy)
) )
profile.dependency.checkGameCompletionAsync(version) dependency.checkGameCompletionAsync(version)
.then(CurseForgeModpackCompletionTask(dependency, profile.selectedVersion))
.then(launcher.launchAsync()) .then(launcher.launchAsync())
.subscribe(Scheduler.JAVAFX) { println("lalala") } .subscribe(Scheduler.JAVAFX) { println("lalala") }
} }

View File

@ -38,7 +38,7 @@ class Profile(var name: String = "Default", initialGameDir: File = File(".minecr
val gameDirProperty = ImmediateObjectProperty<File>(this, "gameDir", initialGameDir) val gameDirProperty = ImmediateObjectProperty<File>(this, "gameDir", initialGameDir)
var gameDir: File by gameDirProperty var gameDir: File by gameDirProperty
var repository = HMCLGameRepository(initialGameDir) var repository = HMCLGameRepository(this, initialGameDir)
val dependency: DefaultDependencyManager get() = DefaultDependencyManager(repository, Settings.downloadProvider, Settings.proxy) val dependency: DefaultDependencyManager get() = DefaultDependencyManager(repository, Settings.downloadProvider, Settings.proxy)
var modManager = ModManager(repository) var modManager = ModManager(repository)
@ -57,11 +57,15 @@ class Profile(var name: String = "Default", initialGameDir: File = File(".minecr
} }
} }
fun specializeVersionSetting(id: String) { /**
* @return null if the given version id does not exist.
*/
fun specializeVersionSetting(id: String): VersionSetting? {
var vs = repository.getVersionSetting(id) var vs = repository.getVersionSetting(id)
if (vs == null) if (vs == null)
vs = repository.createVersionSetting(id) ?: return vs = repository.createVersionSetting(id) ?: return null
vs.usesGlobal = false vs.usesGlobal = false
return vs
} }
fun globalizeVersionSetting(id: String) { fun globalizeVersionSetting(id: String) {

View File

@ -28,7 +28,7 @@ import java.net.Proxy
import java.util.* import java.util.*
object Proxies { object Proxies {
val PROXIES = listOf(null, Proxy.Type.DIRECT, Proxy.Type.HTTP, Proxy.Type.SOCKS) val PROXIES = listOf(Proxy.Type.DIRECT, Proxy.Type.HTTP, Proxy.Type.SOCKS)
fun getProxyType(index: Int): Proxy.Type? = PROXIES.getOrNull(index) fun getProxyType(index: Int): Proxy.Type? = PROXIES.getOrNull(index)
} }

View File

@ -35,6 +35,7 @@ import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Task import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.task
import org.jackhuang.hmcl.ui.wizard.DecoratorPage import org.jackhuang.hmcl.ui.wizard.DecoratorPage
import java.util.concurrent.Callable import java.util.concurrent.Callable
@ -104,7 +105,7 @@ class AccountsPage() : StackPane(), DecoratorPage {
if (newValue != null) if (newValue != null)
Settings.selectedAccount = newValue.properties["account"] as Account Settings.selectedAccount = newValue.properties["account"] as Account
} }
masonryPane.children.setAll(children) masonryPane.resetChildren(children)
Platform.runLater { Platform.runLater {
masonryPane.requestLayout() masonryPane.requestLayout()
scrollPane.requestLayout() scrollPane.requestLayout()
@ -135,7 +136,7 @@ class AccountsPage() : StackPane(), DecoratorPage {
val username = txtUsername.text val username = txtUsername.text
val password = txtPassword.text val password = txtPassword.text
progressBar.isVisible = true progressBar.isVisible = true
val task = Task.of(Callable { val task = task(Callable {
try { try {
val account = when (type) { val account = when (type) {
0 -> OfflineAccount.fromUsername(username) 0 -> OfflineAccount.fromUsername(username)

View File

@ -17,6 +17,7 @@
*/ */
package org.jackhuang.hmcl.ui package org.jackhuang.hmcl.ui
import com.jfoenix.adapters.ReflectionHelper
import com.jfoenix.concurrency.JFXUtilities import com.jfoenix.concurrency.JFXUtilities
import com.jfoenix.controls.* import com.jfoenix.controls.*
import javafx.animation.Animation import javafx.animation.Animation
@ -182,4 +183,10 @@ val SINE: Interpolator = object : Interpolator() {
override fun toString(): String { override fun toString(): String {
return "Interpolator.DISCRETE" return "Interpolator.DISCRETE"
} }
}
fun JFXMasonryPane.resetChildren(children: List<Node>) {
// Fixes mis-repositioning.
ReflectionHelper.setFieldContent(JFXMasonryPane::class.java, this, "oldBoxes", null)
this.children.setAll(children)
} }

View File

@ -36,6 +36,7 @@ import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Profile import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.construct.RipplerContainer 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.ui.wizard.DecoratorPage
/** /**
@ -45,6 +46,8 @@ class MainPage : StackPane(), DecoratorPage {
override val titleProperty: StringProperty = SimpleStringProperty(this, "title", i18n("launcher.title.main")) override val titleProperty: StringProperty = SimpleStringProperty(this, "title", i18n("launcher.title.main"))
@FXML lateinit var btnLaunch: JFXButton @FXML lateinit var btnLaunch: JFXButton
@FXML lateinit var btnRefresh: JFXButton
@FXML lateinit var btnAdd: JFXButton
@FXML lateinit var masonryPane: JFXMasonryPane @FXML lateinit var masonryPane: JFXMasonryPane
init { init {
@ -60,8 +63,8 @@ class MainPage : StackPane(), DecoratorPage {
Settings.onProfileLoading() Settings.onProfileLoading()
// Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game") btnAdd.setOnMouseClicked { Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game") }
// Settings.selectedProfile.repository.refreshVersions() btnRefresh.setOnMouseClicked { Settings.selectedProfile.repository.refreshVersions() }
btnLaunch.setOnMouseClicked { LauncherHelper.launch() } btnLaunch.setOnMouseClicked { LauncherHelper.launch() }
} }
@ -106,7 +109,7 @@ class MainPage : StackPane(), DecoratorPage {
if (newValue != null) if (newValue != null)
profile.selectedVersion = newValue.properties["version"] as String profile.selectedVersion = newValue.properties["version"] as String
} }
masonryPane.children.setAll(children) masonryPane.resetChildren(children)
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")

View File

@ -25,6 +25,7 @@ import javafx.scene.layout.VBox
import org.jackhuang.hmcl.mod.ModManager import org.jackhuang.hmcl.mod.ModManager
import org.jackhuang.hmcl.task.Scheduler import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Task import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.task
import java.util.concurrent.Callable import java.util.concurrent.Callable
class ModController { class ModController {
@ -40,9 +41,9 @@ class ModController {
fun loadMods(modManager: ModManager, versionId: String) { fun loadMods(modManager: ModManager, versionId: String) {
this.modManager = modManager this.modManager = modManager
this.versionId = versionId this.versionId = versionId
Task.of(Callable { task {
modManager.refreshMods(versionId) modManager.refreshMods(versionId)
}).subscribe(Scheduler.JAVAFX) { }.subscribe(Scheduler.JAVAFX) {
rootPane.children.clear() rootPane.children.clear()
for (modInfo in modManager.getMods(versionId)) { for (modInfo in modManager.getMods(versionId)) {
rootPane.children += ModItem(modInfo) { rootPane.children += ModItem(modInfo) {

View File

@ -0,0 +1,33 @@
/*
* 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.ui.construct
import com.jfoenix.validation.base.ValidatorBase
import javafx.scene.control.TextInputControl
/**
* @param validator return true if the input string is valid.
*/
class Validator(val validator: (String) -> Boolean) : ValidatorBase() {
override fun eval() {
if (this.srcControl.get() is TextInputControl) {
val text = (srcControl.get() as TextInputControl).text
hasErrors.set(!validator(text))
}
}
}

View File

@ -20,14 +20,29 @@ package org.jackhuang.hmcl.ui.download
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.layout.Pane import javafx.scene.layout.Pane
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
import org.jackhuang.hmcl.mod.CurseForgeModpackCompletionTask
import org.jackhuang.hmcl.mod.CurseForgeModpackInstallTask
import org.jackhuang.hmcl.mod.CurseForgeModpackManifest
import org.jackhuang.hmcl.setting.EnumGameDirectory
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.task
import org.jackhuang.hmcl.ui.wizard.WizardController import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardProvider import org.jackhuang.hmcl.ui.wizard.WizardProvider
import java.io.File
class DownloadWizardProvider(): WizardProvider() { class DownloadWizardProvider(): WizardProvider() {
lateinit var profile: Profile
override fun finish(settings: Map<String, Any>): Any? { override fun start(settings: MutableMap<String, Any>) {
val builder = Settings.selectedProfile.dependency.gameBuilder() profile = Settings.selectedProfile
settings[PROFILE] = profile
}
private fun finishVersionDownloading(settings: MutableMap<String, Any>): Task {
val builder = profile.dependency.gameBuilder()
builder.name(settings["name"] as String) builder.name(settings["name"] as String)
builder.gameVersion(settings["game"] as String) builder.gameVersion(settings["game"] as String)
@ -44,12 +59,42 @@ class DownloadWizardProvider(): WizardProvider() {
return builder.buildAsync() return builder.buildAsync()
} }
override fun createPage(controller: WizardController, step: Int, settings: Map<String, Any>): Node { private fun finishModpackInstalling(settings: MutableMap<String, Any>): Task? {
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
return null
val selectedFile = settings[ModpackPage.MODPACK_FILE] as? File? ?: return null
val manifest = settings[ModpackPage.MODPACK_CURSEFORGE_MANIFEST] as? CurseForgeModpackManifest? ?: return null
val name = settings[ModpackPage.MODPACK_NAME] as? String? ?: return null
profile.repository.markVersionAsModpack(name)
return CurseForgeModpackInstallTask(profile.dependency, selectedFile, manifest, name) with task {
profile.repository.refreshVersions()
val vs = profile.specializeVersionSetting(name)
profile.repository.undoMark(name)
if (vs != null) {
vs.gameDirType = EnumGameDirectory.VERSION_FOLDER
}
}
}
override fun finish(settings: MutableMap<String, Any>): Any? {
return when (settings[InstallTypePage.INSTALL_TYPE]) {
0 -> finishVersionDownloading(settings)
1 -> finishModpackInstalling(settings)
else -> null
}
}
override fun createPage(controller: WizardController, step: Int, settings: MutableMap<String, Any>): Node {
return when (step) { return when (step) {
0 -> InstallTypePage(controller) 0 -> InstallTypePage(controller)
1 -> when (settings[InstallTypePage.INSTALL_TYPE]) { 1 -> when (settings[InstallTypePage.INSTALL_TYPE]) {
0 -> VersionsPage(controller, "", BMCLAPIDownloadProvider, "game", { controller.onNext(InstallersPage(controller, BMCLAPIDownloadProvider)) }) 0 -> VersionsPage(controller, "", BMCLAPIDownloadProvider, "game", { controller.onNext(InstallersPage(controller, BMCLAPIDownloadProvider)) })
else -> Pane() 1 -> ModpackPage(controller)
else -> throw Error()
} }
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
@ -59,4 +104,8 @@ class DownloadWizardProvider(): WizardProvider() {
return true return true
} }
companion object {
const val PROFILE = "PROFILE"
}
} }

View File

@ -0,0 +1,97 @@
/*
* 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.ui.download
import com.google.gson.JsonParseException
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXTextField
import javafx.application.Platform
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
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
import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardPage
import java.io.IOException
class ModpackPage(private val controller: WizardController): StackPane(), WizardPage {
override val title: String = "Install a modpack"
@FXML lateinit var lblName: Label
@FXML lateinit var lblVersion: Label
@FXML lateinit var lblAuthor: Label
@FXML lateinit var lblModpackLocation: Label
@FXML lateinit var txtModpackName: JFXTextField
@FXML lateinit var btnInstall: JFXButton
init {
loadFXML("/assets/fxml/download/modpack.fxml")
val profile = controller.settings["PROFILE"] as Profile
val chooser = FileChooser()
chooser.title = i18n("modpack.choose")
val selectedFile = chooser.showOpenDialog(Controllers.stage)
if (selectedFile == null) Platform.runLater { controller.onFinish() }
else {
// TODO: original HMCL modpack support.
controller.settings[MODPACK_FILE] = selectedFile
lblModpackLocation.text = selectedFile.absolutePath
txtModpackName.text = selectedFile.nameWithoutExtension
txtModpackName.validators += Validator { !profile.repository.hasVersion(it) }
txtModpackName.textProperty().addListener { _ ->
btnInstall.isDisabled = !txtModpackName.validate()
}
try {
val manifest = readCurseForgeModpackManifest(selectedFile)
controller.settings[MODPACK_CURSEFORGE_MANIFEST] = manifest
lblName.text = manifest.name
lblVersion.text = manifest.version
lblAuthor.text = manifest.author
} catch (e: IOException) {
// TODO
} catch (e: JsonParseException) {
// TODO
}
}
}
override fun cleanup(settings: MutableMap<String, Any>) {
settings.remove(MODPACK_FILE)
}
fun onInstall() {
if (!txtModpackName.validate()) return
controller.settings[MODPACK_NAME] = txtModpackName.text
controller.onFinish()
}
companion object {
val MODPACK_FILE = "MODPACK_FILE"
val MODPACK_NAME = "MODPACK_NAME"
val MODPACK_CURSEFORGE_MANIFEST = "CURSEFORGE_MANIFEST"
}
}

View File

@ -126,7 +126,7 @@ interface AbstractWizardDisplayer : WizardDisplayer {
cancelQueue.add(executor) cancelQueue.add(executor)
executor.submit(Task.of(Scheduler.JAVAFX) { executor.submit(org.jackhuang.hmcl.task.task(Scheduler.JAVAFX) {
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH) navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
}) })
}.start() }.start()

View File

@ -31,6 +31,7 @@ class WizardController(protected val displayer: WizardDisplayer) : Navigation {
pages.clear() pages.clear()
val page = navigatingTo(0) val page = navigatingTo(0)
pages.push(page) pages.push(page)
provider.start(settings)
displayer.onStart() displayer.onStart()
displayer.navigateTo(page, Navigation.NavigationDirection.START) displayer.navigateTo(page, Navigation.NavigationDirection.START)
} }

View File

@ -20,8 +20,8 @@ package org.jackhuang.hmcl.ui.wizard
import javafx.scene.Node import javafx.scene.Node
abstract class WizardProvider { abstract class WizardProvider {
abstract fun start(settings: MutableMap<String, Any>)
abstract fun finish(settings: Map<String, Any>): Any? abstract fun finish(settings: MutableMap<String, Any>): Any?
abstract fun createPage(controller: WizardController, step: Int, settings: Map<String, Any>): Node abstract fun createPage(controller: WizardController, step: Int, settings: MutableMap<String, Any>): Node
abstract fun cancel(): Boolean abstract fun cancel(): Boolean
} }

View File

@ -414,6 +414,12 @@
-jfx-mask-type: CIRCLE; -jfx-mask-type: CIRCLE;
} }
.jfx-button-raised {
-fx-text-fill: white;
-fx-background-color: #5264AE;
-fx-font-size:14px;
}
/******************************************************************************* /*******************************************************************************
* * * *
* JFX Check Box * * JFX Check Box *

View File

@ -15,7 +15,7 @@
<top> <top>
<VBox alignment="CENTER" style="-fx-padding: 40px;" spacing="20"> <VBox alignment="CENTER" style="-fx-padding: 40px;" spacing="20">
<Label fx:id="lblGameVersion" alignment="CENTER" /> <Label fx:id="lblGameVersion" alignment="CENTER" />
<JFXTextField fx:id="txtName" labelFloat="true" promptText="Enter the name of this new version" maxWidth="300" /> <JFXTextField fx:id="txtName" labelFloat="true" promptText="%modpack.enter_name" maxWidth="300" />
</VBox> </VBox>
</top> </top>
<center> <center>
@ -48,8 +48,7 @@
</center> </center>
<bottom> <bottom>
<HBox alignment="CENTER"> <HBox alignment="CENTER">
<JFXButton onMouseClicked="#onInstall" prefWidth="100" prefHeight="40" buttonType="RAISED" text="Install" <JFXButton onMouseClicked="#onInstall" prefWidth="100" prefHeight="40" buttonType="RAISED" text="%ui.button.install" styleClass="jfx-button-raised" />
style="-fx-text-fill:WHITE;-fx-background-color:#5264AE;-fx-font-size:14px;"/>
</HBox> </HBox>
</bottom> </bottom>
</BorderPane> </BorderPane>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.JFXTextField?>
<?import com.jfoenix.controls.JFXButton?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<VBox maxWidth="500" maxHeight="500" spacing="50">
<Label text="Installing modpack" />
<Label fx:id="lblModpackLocation" />
<JFXTextField fx:id="txtModpackName" labelFloat="true" promptText="%modpack.enter_name" />
<GridPane>
<columnConstraints>
<ColumnConstraints />
<ColumnConstraints hgrow="ALWAYS" />
</columnConstraints>
<Label text="Name" GridPane.rowIndex="0" GridPane.columnIndex="0" />
<Label text="Version" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<Label text="Author" GridPane.rowIndex="2" GridPane.columnIndex="0" />
<Label fx:id="lblName" GridPane.rowIndex="0" GridPane.columnIndex="1" />
<Label fx:id="lblVersion" GridPane.rowIndex="1" GridPane.columnIndex="1" />
<Label fx:id="lblAuthor" GridPane.rowIndex="2" GridPane.columnIndex="1" />
</GridPane>
<JFXButton buttonType="RAISED" fx:id="btnInstall" onMouseClicked="#onInstall" text="%ui.button.install" styleClass="jfx-button-raised" />
</VBox>
</fx:root>

View File

@ -27,7 +27,7 @@
</items> </items>
</JFXComboBox></right></BorderPane> </JFXComboBox></right></BorderPane>
<BorderPane><left><Label text="%launcher.lang" /></left><right><JFXComboBox fx:id="cboLanguage" /></right></BorderPane> <BorderPane><left><Label text="%launcher.lang" /></left><right><JFXComboBox fx:id="cboLanguage" /></right></BorderPane>
<BorderPane><left><Label text="%launcher.proxy" /></left><right><HBox alignment="CENTER_LEFT"> <BorderPane><left><Label text="%launcher.proxy" /></left><right><HBox alignment="CENTER_LEFT" spacing="5">
<JFXComboBox fx:id="cboProxyType"> <JFXComboBox fx:id="cboProxyType">
<items> <items>
<FXCollections fx:factory="observableArrayList"> <FXCollections fx:factory="observableArrayList">

View File

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

View File

@ -27,8 +27,8 @@ import java.net.Proxy
/** /**
* This class has no state. * This class has no state.
*/ */
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, val proxy: Proxy = Proxy.NO_PROXY) class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, proxy: Proxy = Proxy.NO_PROXY)
: AbstractDependencyManager(repository) { : AbstractDependencyManager(repository, proxy) {
override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this) override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this)

View File

@ -30,7 +30,7 @@ class DefaultGameBuilder(val dependencyManager: DefaultDependencyManager): GameB
val gameVersion = gameVersion val gameVersion = gameVersion
return VersionJSONDownloadTask(gameVersion = gameVersion) then a@{ task -> return VersionJSONDownloadTask(gameVersion = gameVersion) then a@{ task ->
var version = GSON.fromJson<Version>(task.result!!) ?: return@a null var version = GSON.fromJson<Version>(task.result!!) ?: return@a null
version = version.copy(id = name) version = version.copy(id = name, jar = null)
var result = ParallelTask( var result = ParallelTask(
GameAssetDownloadTask(dependencyManager, version), GameAssetDownloadTask(dependencyManager, version),
GameLoggingDownloadTask(dependencyManager, version), GameLoggingDownloadTask(dependencyManager, version),

View File

@ -20,8 +20,9 @@ package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.game.GameRepository import org.jackhuang.hmcl.game.GameRepository
import org.jackhuang.hmcl.game.Version import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.task.Task import org.jackhuang.hmcl.task.Task
import java.net.Proxy
abstract class DependencyManager(open val repository: GameRepository) { abstract class DependencyManager(open val repository: GameRepository, open val proxy: Proxy) {
/** /**
* Check if the game is complete. * Check if the game is complete.

View File

@ -53,7 +53,7 @@ class GameLoggingDownloadTask(private val dependencyManager: DefaultDependencyMa
override val dependencies: MutableCollection<Task> = LinkedList() override val dependencies: MutableCollection<Task> = LinkedList()
override fun execute() { override fun execute() {
val logging = version.logging?.get(DownloadType.CLIENT) ?: return val logging = version.logging?.get(DownloadType.CLIENT) ?: return
val file = dependencyManager.repository.getLoggingObject(version.actualAssetIndex.id, logging) val file = dependencyManager.repository.getLoggingObject(version.id, version.actualAssetIndex.id, logging)
if (!file.exists()) if (!file.exists())
dependencies += FileDownloadTask(logging.file.url.toURL(), file, proxy = dependencyManager.proxy) dependencies += FileDownloadTask(logging.file.url.toURL(), file, proxy = dependencyManager.proxy)
} }
@ -63,11 +63,11 @@ class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependenc
override val dependencies: MutableCollection<Task> = LinkedList() override val dependencies: MutableCollection<Task> = LinkedList()
override fun execute() { override fun execute() {
val assetIndexInfo = version.actualAssetIndex val assetIndexInfo = version.actualAssetIndex
val assetDir = dependencyManager.repository.getAssetDirectory(assetIndexInfo.id) val assetDir = dependencyManager.repository.getAssetDirectory(version.id, assetIndexInfo.id)
if (!assetDir.makeDirectory()) if (!assetDir.makeDirectory())
throw IOException("Cannot create directory: $assetDir") throw IOException("Cannot create directory: $assetDir")
val assetIndexFile = dependencyManager.repository.getIndexFile(assetIndexInfo.id) val assetIndexFile = dependencyManager.repository.getIndexFile(version.id, assetIndexInfo.id)
dependencies += FileDownloadTask(dependencyManager.downloadProvider.injectURL(assetIndexInfo.url).toURL(), assetIndexFile, proxy = dependencyManager.proxy) dependencies += FileDownloadTask(dependencyManager.downloadProvider.injectURL(assetIndexInfo.url).toURL(), assetIndexFile, proxy = dependencyManager.proxy)
} }
} }
@ -75,7 +75,7 @@ class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependenc
class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : TaskResult<Collection<Pair<File, AssetObject>>>() { class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : TaskResult<Collection<Pair<File, AssetObject>>>() {
private val assetIndexTask = GameAssetIndexDownloadTask(dependencyManager, version) private val assetIndexTask = GameAssetIndexDownloadTask(dependencyManager, version)
private val assetIndexInfo = version.actualAssetIndex private val assetIndexInfo = version.actualAssetIndex
private val assetIndexFile = dependencyManager.repository.getIndexFile(assetIndexInfo.id) private val assetIndexFile = dependencyManager.repository.getIndexFile(version.id, assetIndexInfo.id)
override val dependents: MutableCollection<Task> = LinkedList() override val dependents: MutableCollection<Task> = LinkedList()
init { init {
@ -88,7 +88,7 @@ class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManag
val res = LinkedList<Pair<File, AssetObject>>() val res = LinkedList<Pair<File, AssetObject>>()
var progress = 0 var progress = 0
index?.objects?.entries?.forEach { (_, assetObject) -> index?.objects?.entries?.forEach { (_, assetObject) ->
res += Pair(dependencyManager.repository.getAssetObject(assetIndexInfo.id, assetObject), assetObject) res += Pair(dependencyManager.repository.getAssetObject(version.id, assetIndexInfo.id, assetObject), assetObject)
updateProgress(++progress, index.objects.size) updateProgress(++progress, index.objects.size)
} }
result = res result = res

View File

@ -37,7 +37,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
} }
override fun getVersionCount() = versions.size override fun getVersionCount() = versions.size
override fun getVersions() = versions.values override fun getVersions() = versions.values
override fun getLibraryFile(id: Version, lib: Library) = File(baseDirectory, "libraries/${lib.path}") override fun getLibraryFile(version: Version, lib: Library) = baseDirectory.resolve("libraries/${lib.path}")
override fun getRunDirectory(id: String) = baseDirectory override fun getRunDirectory(id: String) = baseDirectory
override fun getVersionJar(version: Version): File { override fun getVersionJar(version: Version): File {
val v = version.resolve(this) val v = version.resolve(this)
@ -45,7 +45,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
return getVersionRoot(id).resolve("$id.jar") return getVersionRoot(id).resolve("$id.jar")
} }
override fun getNativeDirectory(id: String) = File(getVersionRoot(id), "$id-natives") override fun getNativeDirectory(id: String) = File(getVersionRoot(id), "$id-natives")
open fun getVersionRoot(id: String) = File(baseDirectory, "versions/$id") override fun getVersionRoot(id: String) = File(baseDirectory, "versions/$id")
open fun getVersionJson(id: String) = File(getVersionRoot(id), "$id.json") open fun getVersionJson(id: String) = File(getVersionRoot(id), "$id.json")
open fun readVersionJson(id: String): Version? = readVersionJson(getVersionJson(id)) open fun readVersionJson(id: String): Version? = readVersionJson(getVersionJson(id))
@Throws(IOException::class, JsonSyntaxException::class, VersionNotFoundException::class) @Throws(IOException::class, JsonSyntaxException::class, VersionNotFoundException::class)
@ -135,49 +135,49 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
EVENT_BUS.fireEvent(RefreshedVersionsEvent(this)) EVENT_BUS.fireEvent(RefreshedVersionsEvent(this))
} }
override fun getAssetIndex(assetId: String): AssetIndex { override fun getAssetIndex(version: String, assetId: String): AssetIndex {
return GSON.fromJson(getIndexFile(assetId).readText())!! return GSON.fromJson(getIndexFile(version, assetId).readText())!!
} }
override fun getActualAssetDirectory(assetId: String): File { override fun getActualAssetDirectory(version: String, assetId: String): File {
try { try {
return reconstructAssets(assetId) return reconstructAssets(version, assetId)
} catch (e: IOException) { } catch (e: IOException) {
LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e) LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e)
return getAssetDirectory(assetId) return getAssetDirectory(version, assetId)
} }
} }
override fun getAssetDirectory(assetId: String): File = override fun getAssetDirectory(version: String, assetId: String): File =
baseDirectory.resolve("assets") baseDirectory.resolve("assets")
@Throws(IOException::class) @Throws(IOException::class)
override fun getAssetObject(assetId: String, name: String): File { override fun getAssetObject(version: String, assetId: String, name: String): File {
try { try {
return getAssetObject(assetId, getAssetIndex(assetId).objects["name"]!!) return getAssetObject(version, assetId, getAssetIndex(version, assetId).objects["name"]!!)
} catch (e: Exception) { } catch (e: Exception) {
throw IOException("Asset index file malformed", e) throw IOException("Asset index file malformed", e)
} }
} }
override fun getAssetObject(assetId: String, obj: AssetObject): File = override fun getAssetObject(version: String, assetId: String, obj: AssetObject): File =
getAssetObject(getAssetDirectory(assetId), obj) getAssetObject(version, getAssetDirectory(version, assetId), obj)
open fun getAssetObject(assetDir: File, obj: AssetObject): File { open fun getAssetObject(version: String, assetDir: File, obj: AssetObject): File {
return assetDir.resolve("objects/${obj.location}") return assetDir.resolve("objects/${obj.location}")
} }
open fun getIndexFile(assetId: String): File = open fun getIndexFile(version: String, assetId: String): File =
getAssetDirectory(assetId).resolve("indexes/$assetId.json") getAssetDirectory(version, assetId).resolve("indexes/$assetId.json")
override fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File = override fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File =
getAssetDirectory(assetId).resolve("log_configs/${loggingInfo.file.id}") getAssetDirectory(version, assetId).resolve("log_configs/${loggingInfo.file.id}")
@Throws(IOException::class, JsonSyntaxException::class) @Throws(IOException::class, JsonSyntaxException::class)
protected open fun reconstructAssets(assetId: String): File { protected open fun reconstructAssets(version: String, assetId: String): File {
val assetsDir = getAssetDirectory(assetId) val assetsDir = getAssetDirectory(version, assetId)
val assetVersion = assetId val assetVersion = assetId
val indexFile: File = getIndexFile(assetVersion) val indexFile: File = getIndexFile(version, assetVersion)
val virtualRoot = assetsDir.resolve("virtual").resolve(assetVersion) val virtualRoot = assetsDir.resolve("virtual").resolve(assetVersion)
if (!indexFile.isFile) { if (!indexFile.isFile) {
@ -193,7 +193,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
val tot = index.objects.entries.size val tot = index.objects.entries.size
for ((location, assetObject) in index.objects.entries) { for ((location, assetObject) in index.objects.entries) {
val target = File(virtualRoot, location) val target = File(virtualRoot, location)
val original = getAssetObject(assetsDir, assetObject) val original = getAssetObject(version, assetsDir, assetObject)
if (original.exists()) { if (original.exists()) {
cnt++ cnt++
if (!target.isFile) if (!target.isFile)

View File

@ -59,6 +59,13 @@ interface GameRepository : VersionProvider {
*/ */
fun refreshVersions() fun refreshVersions()
/**
* Gets the root folder of specific version.
* The root folders the versions must be unique.
* For example, .minecraft/versions/<version name>/.
*/
fun getVersionRoot(id: String): File
/** /**
* Gets the current running directory of the given version for game. * Gets the current running directory of the given version for game.
* @param id the version id * @param id the version id
@ -69,11 +76,11 @@ interface GameRepository : VersionProvider {
* Get the library file in disk. * Get the library file in disk.
* This method allows versions and libraries that are not loaded by this game repository. * This method allows versions and libraries that are not loaded by this game repository.
* *
* @param id version id * @param version versionversion
* @param lib the library, [Version.libraries] * @param lib the library, [Version.libraries]
* @return the library file * @return the library file
*/ */
fun getLibraryFile(id: Version, lib: Library): File fun getLibraryFile(version: Version, lib: Library): File
/** /**
* Get the directory that native libraries will be unzipped to. * Get the directory that native libraries will be unzipped to.
@ -123,12 +130,12 @@ interface GameRepository : VersionProvider {
* @throws java.io.IOException if I/O operation fails. * @throws java.io.IOException if I/O operation fails.
* @return the actual asset directory * @return the actual asset directory
*/ */
fun getActualAssetDirectory(assetId: String): File fun getActualAssetDirectory(version: String, assetId: String): File
/** /**
* Get the asset directory according to the asset id. * Get the asset directory according to the asset id.
*/ */
fun getAssetDirectory(assetId: String): File fun getAssetDirectory(version: String, assetId: String): File
/** /**
* Get the file that given asset object refers to * Get the file that given asset object refers to
@ -138,7 +145,7 @@ interface GameRepository : VersionProvider {
* @throws java.io.IOException if I/O operation fails. * @throws java.io.IOException if I/O operation fails.
* @return the file that given asset object refers to * @return the file that given asset object refers to
*/ */
fun getAssetObject(assetId: String, name: String): File fun getAssetObject(version: String, assetId: String, name: String): File
/** /**
* Get the file that given asset object refers to * Get the file that given asset object refers to
@ -147,7 +154,7 @@ interface GameRepository : VersionProvider {
* @param obj the asset object, [AssetIndex.objects] * @param obj the asset object, [AssetIndex.objects]
* @return the file that given asset object refers to * @return the file that given asset object refers to
*/ */
fun getAssetObject(assetId: String, obj: AssetObject): File fun getAssetObject(version: String, assetId: String, obj: AssetObject): File
/** /**
* Get asset index that assetId represents * Get asset index that assetId represents
@ -155,7 +162,7 @@ interface GameRepository : VersionProvider {
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets] * @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @return the asset index * @return the asset index
*/ */
fun getAssetIndex(assetId: String): AssetIndex fun getAssetIndex(version: String, assetId: String): AssetIndex
/** /**
* Get logging object * Get logging object
@ -164,5 +171,5 @@ interface GameRepository : VersionProvider {
* @param loggingInfo the logging info * @param loggingInfo the logging info
* @return the file that loggingInfo refers to * @return the file that loggingInfo refers to
*/ */
fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File
} }

View File

@ -68,14 +68,14 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
if (OS.CURRENT_OS == OS.OSX) { if (OS.CURRENT_OS == OS.OSX) {
res.add("-Xdock:name=Minecraft ${version.id}") res.add("-Xdock:name=Minecraft ${version.id}")
res.add("-Xdock:icon=" + repository.getAssetObject(version.actualAssetIndex.id, "icons/minecraft.icns").absolutePath); res.add("-Xdock:icon=" + repository.getAssetObject(version.id, version.actualAssetIndex.id, "icons/minecraft.icns").absolutePath);
} }
val logging = version.logging val logging = version.logging
if (logging != null) { if (logging != null) {
val loggingInfo = logging[DownloadType.CLIENT] val loggingInfo = logging[DownloadType.CLIENT]
if (loggingInfo != null) { if (loggingInfo != null) {
val loggingFile = repository.getLoggingObject(version.actualAssetIndex.id, loggingInfo) val loggingFile = repository.getLoggingObject(version.id, version.actualAssetIndex.id, loggingInfo)
if (loggingFile.exists()) if (loggingFile.exists())
res.add(loggingInfo.argument.replace("\${path}", loggingFile.absolutePath)) res.add(loggingInfo.argument.replace("\${path}", loggingFile.absolutePath))
} }
@ -139,7 +139,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
res.add(version.mainClass!!) res.add(version.mainClass!!)
// Provided Minecraft arguments // Provided Minecraft arguments
val gameAssets = repository.getActualAssetDirectory(version.actualAssetIndex.id) val gameAssets = repository.getActualAssetDirectory(version.id, version.actualAssetIndex.id)
version.minecraftArguments!!.tokenize().forEach { line -> version.minecraftArguments!!.tokenize().forEach { line ->
res.add(line res.add(line

View File

@ -0,0 +1,186 @@
/*
* 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.mod
import com.google.gson.JsonParseException
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.DependencyManager
import org.jackhuang.hmcl.game.GameException
import org.jackhuang.hmcl.task.FileDownloadTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.task
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.net.URL
import java.util.logging.Level
import java.util.zip.ZipFile
class CurseForgeModpackManifest @JvmOverloads constructor(
@SerializedName("manifestType")
val manifestType: String = MINECRAFT_MODPACK,
@SerializedName("manifestVersion")
val manifestVersion: Int = 1,
@SerializedName("name")
val name: String = "",
@SerializedName("version")
val version: String = "1.0",
@SerializedName("author")
val author: String = "",
@SerializedName("overrides")
val overrides: String = "overrides",
@SerializedName("minecraft")
val minecraft: CurseForgeModpackManifestMinecraft = CurseForgeModpackManifestMinecraft(),
@SerializedName("files")
val files: List<CurseForgeModpackManifestFile> = emptyList()
): Validation {
override fun validate() {
check(manifestType == MINECRAFT_MODPACK, { "Only support Minecraft modpack" })
}
companion object {
val MINECRAFT_MODPACK = "minecraftModpack"
}
}
class CurseForgeModpackManifestMinecraft (
@SerializedName("version")
val gameVersion: String = "",
@SerializedName("modLoaders")
val modLoaders: List<CurseForgeModpackManifestModLoader> = emptyList()
): Validation {
override fun validate() {
check(gameVersion.isNotBlank())
}
}
class CurseForgeModpackManifestModLoader (
@SerializedName("id")
val id: String = "",
@SerializedName("primary")
val primary: Boolean = false
): Validation {
override fun validate() {
check(id.isNotBlank(), { "Curse Forge modpack manifest Mod loader id cannot be blank." })
}
}
class CurseForgeModpackManifestFile (
@SerializedName("projectID")
val projectID: Int = 0,
@SerializedName("fileID")
val fileID: Int = 0,
@SerializedName("fileName")
var fileName: String = "",
@SerializedName("required")
val required: Boolean = true
): Validation {
override fun validate() {
check(projectID != 0)
check(fileID != 0)
}
val url: URL get() = "https://minecraft.curseforge.com/projects/$projectID/files/$fileID/download".toURL()
}
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 json = zipFile.getInputStream(entry).readFullyAsString()
return GSON.fromJson<CurseForgeModpackManifest>(json) ?: throw JsonParseException("Manifest.json not found. Not a valid CurseForge modpack.")
}
}
class CurseForgeModpackInstallTask(private val dependencyManager: DefaultDependencyManager, private val zipFile: File, private val manifest: CurseForgeModpackManifest, private val name: String): Task() {
val repository = dependencyManager.repository
init {
if (repository.hasVersion(name))
throw IllegalStateException("Version $name already exists.")
}
val root = repository.getVersionRoot(name)
val run = repository.getRunDirectory(name)
override val dependents = mutableListOf<Task>()
override val dependencies = mutableListOf<Task>()
init {
val builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.minecraft.gameVersion)
manifest.minecraft.modLoaders.forEach {
if (it.id.startsWith("forge-"))
builder.version("forge", it.id.substring("forge-".length))
}
dependents += builder.buildAsync()
}
override fun execute() {
unzipSubDirectory(zipFile, run, manifest.overrides)
var finished = 0
for (f in manifest.files) {
try {
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.
}
++finished
updateProgress(1.0 * finished / manifest.files.size)
}
root.resolve("manifest.json").writeText(GSON.toJson(manifest))
}
}
class CurseForgeModpackCompletionTask(dependencyManager: DependencyManager, version: String): Task() {
val repository = dependencyManager.repository
val run = repository.getRunDirectory(version)
private var manifest: CurseForgeModpackManifest?
private val proxy = dependencyManager.proxy
override val dependents = mutableListOf<Task>()
override val dependencies = mutableListOf<Task>()
init {
try {
val manifestFile = repository.getVersionRoot(version).resolve("manifest.json")
if (!manifestFile.exists()) manifest = null
else {
manifest = GSON.fromJson<CurseForgeModpackManifest>(repository.getVersionRoot(version).resolve("manifest.json").readText())!!
for (f in manifest!!.files) {
if (f.fileName.isBlank())
dependents += task { f.fileName = f.url.detectFileName(proxy) }
}
}
} catch (e: Exception) {
LOG.log(Level.WARNING, "Unable to read CurseForge modpack manifest.json", e)
manifest = null
}
}
override fun execute() {
if (manifest == null) return
for (f in manifest!!.files) {
if (f.fileName.isBlank())
throw GameException("Unable to download mod, cannot continue")
val file = run.resolve("mods").resolve(f.fileName)
if (!file.exists())
dependencies += FileDownloadTask(f.url, file, proxy = proxy)
}
}
}

View File

@ -61,12 +61,12 @@ abstract class Task {
infix fun with(b: Task): Task = CoupleTask(this, { b }, false) infix fun with(b: Task): Task = CoupleTask(this, { b }, false)
/** /**
* The collection of sub-tasks that should execute before this task running. * The collection of sub-tasks that should execute **before** this task running.
*/ */
open val dependents: Collection<Task> = emptySet() open val dependents: Collection<Task> = emptySet()
/** /**
* The collection of sub-tasks that should execute after this task running. * The collection of sub-tasks that should execute **after** this task running.
*/ */
open val dependencies: Collection<Task> = emptySet() open val dependencies: Collection<Task> = emptySet()
@ -119,14 +119,12 @@ abstract class Task {
submit(subscriber).start() submit(subscriber).start()
} }
fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(Task.of(scheduler, closure)) fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(task(scheduler, closure))
override fun toString(): String { override fun toString(): String {
return title return title
} }
}
companion object { fun task(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit): Task = SimpleTask(closure, scheduler)
fun of(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit): Task = SimpleTask(closure, scheduler) fun <V> task(callable: Callable<V>): TaskResult<V> = TaskCallable(callable)
fun <V> of(callable: Callable<V>): TaskResult<V> = TaskCallable(callable)
}
}

View File

@ -30,6 +30,8 @@ import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
import java.io.OutputStream import java.io.OutputStream
import java.util.*
import java.util.logging.Level
import kotlin.text.Charsets import kotlin.text.Charsets
private val XTM = object : X509TrustManager { private val XTM = object : X509TrustManager {
@ -56,7 +58,7 @@ fun initHttps() {
HttpsURLConnection.setDefaultHostnameVerifier(HNV); HttpsURLConnection.setDefaultHostnameVerifier(HNV);
} }
var DEFAULT_USER_AGENT = "JMCCC" var DEFAULT_USER_AGENT: () -> String = { RandomUserAgent.randomUserAgent }
fun String.toURL() = URL(this) fun String.toURL() = URL(this)
@ -67,7 +69,7 @@ fun URL.createConnection(proxy: Proxy): HttpURLConnection {
useCaches = false useCaches = false
connectTimeout = 15000 connectTimeout = 15000
readTimeout = 15000 readTimeout = 15000
addRequestProperty("User-Agent", DEFAULT_USER_AGENT) addRequestProperty("User-Agent", DEFAULT_USER_AGENT())
} as HttpURLConnection } as HttpURLConnection
} }
@ -118,4 +120,34 @@ fun HttpURLConnection.readData(): String {
} finally { } finally {
input?.closeQuietly() input?.closeQuietly()
} }
}
fun URL.detectFileNameQuietly(proxy: Proxy = Proxy.NO_PROXY): String {
try {
val conn = createConnection(proxy)
conn.connect()
if (conn.responseCode / 100 != 2)
throw IOException("Response code ${conn.responseCode}")
return conn.detectFileName()
} catch (e: IOException) {
LOG.log(Level.WARNING, "Cannot detect the file name of URL $this", e)
return UUIDTypeAdapter.fromUUID(UUID.randomUUID())
}
}
fun URL.detectFileName(proxy: Proxy = Proxy.NO_PROXY): String {
val conn = createConnection(proxy)
conn.connect()
if (conn.responseCode / 100 != 2)
throw IOException("Response code ${conn.responseCode}")
return conn.detectFileName()
}
fun HttpURLConnection.detectFileName(): String {
val disposition = getHeaderField("Content-Disposition")
if (disposition == null || disposition.indexOf("filename=") == -1) {
val u = url.toString()
return u.substringAfterLast('/')
} else
return disposition.substringAfter("filename=").removeSurrounding("\"")
} }

File diff suppressed because it is too large Load Diff

View File

@ -155,4 +155,62 @@ fun unzip(zip: File, dest: File, callback: ((String) -> Boolean)? = null, ignore
} }
} }
} }
}
/**
* 将文件压缩成zip文件
* @param zip zip文件路径
* *
* @param dest 待压缩文件根目录
* *
* @param callback will be called for every entry in the zip file,
* * returns false if you dont want this file unzipped.
* *
* *
* @throws java.io.IOException 解压失败或无法写入
*/
@JvmOverloads
@Throws(IOException::class)
fun unzipSubDirectory(zip: File, dest: File, subDirectory: String, ignoreExistsFile: Boolean = true) {
val buf = ByteArray(1024)
dest.mkdirs()
ZipInputStream(zip.inputStream()).use { zipFile ->
if (zip.exists()) {
var gbkPath: String
var strtemp: String
val strPath = dest.absolutePath
var zipEnt: ZipEntry?
while (true) {
zipEnt = zipFile.nextEntry
if (zipEnt == null)
break
gbkPath = zipEnt.name
if (!gbkPath.startsWith(subDirectory))
continue
gbkPath = gbkPath.substring(subDirectory.length)
if (gbkPath.startsWith("/") || gbkPath.startsWith("\\")) gbkPath = gbkPath.substring(1)
strtemp = strPath + File.separator + gbkPath
if (zipEnt.isDirectory) {
val dir = File(strtemp)
dir.mkdirs()
} else {
//建目录
val strsubdir = gbkPath
for (i in 0..strsubdir.length - 1)
if (strsubdir.substring(i, i + 1).equals("/", ignoreCase = true)) {
val temp = strPath + File.separator + strsubdir.substring(0, i)
val subdir = File(temp)
if (!subdir.exists())
subdir.mkdir()
}
if (ignoreExistsFile && File(strtemp).exists())
continue
File(strtemp).outputStream().use({ fos ->
zipFile.copyTo(fos, buf)
})
}
}
}
}
} }

View File

@ -30,6 +30,8 @@ import org.jackhuang.hmcl.launch.ProcessListener
import org.jackhuang.hmcl.util.makeCommand import org.jackhuang.hmcl.util.makeCommand
import org.jackhuang.hmcl.task.Task import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.TaskListener import org.jackhuang.hmcl.task.TaskListener
import org.jackhuang.hmcl.util.detectFileName
import org.jackhuang.hmcl.util.toURL
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.net.InetSocketAddress import java.net.InetSocketAddress

View File

@ -20,7 +20,7 @@ group 'org.jackhuang'
version '3.0' version '3.0'
buildscript { buildscript {
ext.kotlin_version = '1.1.3-2' ext.kotlin_version = '1.1.4'
repositories { repositories {
mavenCentral() mavenCentral()