mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-11-07 05:04:31 +01:00
Add support for mode field, with CurseForge metadata lookup
Now always asks the user before proceeding past the point where optional mods could be selected and configured When updating files, the hash is checked so an update isn't redownloaded if it already exists Added DevMain file for running in a dev environment
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import link.infra.packwiz.installer.metadata.curseforge.UpdateData
|
||||
import link.infra.packwiz.installer.metadata.curseforge.UpdateDeserializer
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||
@@ -19,11 +22,16 @@ class ModFile {
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String? = null
|
||||
var hash: String? = null
|
||||
var mode: String? = null
|
||||
}
|
||||
|
||||
var update: Map<String, Any>? = null
|
||||
@JsonAdapter(UpdateDeserializer::class)
|
||||
var update: Map<String, UpdateData>? = null
|
||||
var option: Option? = null
|
||||
|
||||
@Transient
|
||||
val resolvedUpdateData = mutableMapOf<String, SpaceSafeURI>()
|
||||
|
||||
class Option {
|
||||
var optional = false
|
||||
var description: String? = null
|
||||
@@ -34,11 +42,20 @@ class ModFile {
|
||||
@Throws(Exception::class)
|
||||
fun getSource(baseLoc: SpaceSafeURI?): Source {
|
||||
download?.let {
|
||||
if (it.url == null) {
|
||||
throw Exception("Metadata file doesn't have a download URI")
|
||||
if (it.mode == null || it.mode == "" || it.mode == "url") {
|
||||
if (it.url == null) {
|
||||
throw Exception("Metadata file doesn't have a download URI")
|
||||
}
|
||||
val newLoc = getNewLoc(baseLoc, it.url) ?: throw Exception("Metadata file URI is invalid")
|
||||
return getFileSource(newLoc)
|
||||
} else if (it.mode == "metadata:curseforge") {
|
||||
if (!resolvedUpdateData.contains("curseforge")) {
|
||||
throw Exception("Metadata file specifies CurseForge mode, but is missing metadata")
|
||||
}
|
||||
return getFileSource(resolvedUpdateData["curseforge"]!!)
|
||||
} else {
|
||||
throw Exception("Unsupported download mode " + it.mode)
|
||||
}
|
||||
val newLoc = getNewLoc(baseLoc, it.url) ?: throw Exception("Metadata file URI is invalid")
|
||||
return getFileSource(newLoc)
|
||||
} ?: throw Exception("Metadata file doesn't have download")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonIOException
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import link.infra.packwiz.installer.metadata.IndexFile
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.target.ClientHolder
|
||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.ByteString.Companion.decodeBase64
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
private class GetFilesRequest(val fileIds: List<Int>)
|
||||
private class GetModsRequest(val modIds: List<Int>)
|
||||
|
||||
private class GetFilesResponse {
|
||||
class CfFile {
|
||||
var id = 0
|
||||
var modId = 0
|
||||
var downloadUrl: SpaceSafeURI? = null
|
||||
}
|
||||
val data = mutableListOf<CfFile>()
|
||||
}
|
||||
|
||||
private class GetModsResponse {
|
||||
class CfMod {
|
||||
var id = 0
|
||||
var name = ""
|
||||
var links: CfLinks? = null
|
||||
}
|
||||
class CfLinks {
|
||||
var websiteUrl = ""
|
||||
}
|
||||
val data = mutableListOf<CfMod>()
|
||||
}
|
||||
|
||||
private const val APIServer = "api.curseforge.com"
|
||||
// If you fork/derive from packwiz, I request that you obtain your own API key.
|
||||
private val APIKey = "JDJhJDEwJHNBWVhqblU1N0EzSmpzcmJYM3JVdk92UWk2NHBLS3BnQ2VpbGc1TUM1UGNKL0RYTmlGWWxh".decodeBase64()!!
|
||||
.string(StandardCharsets.UTF_8)
|
||||
|
||||
private val clientHolder = ClientHolder()
|
||||
|
||||
// TODO: switch to PackwizPath stuff and OkHttp in old code
|
||||
|
||||
@Throws(JsonSyntaxException::class, JsonIOException::class)
|
||||
fun resolveCfMetadata(mods: List<IndexFile.File>): List<ExceptionDetails> {
|
||||
val failures = mutableListOf<ExceptionDetails>()
|
||||
val fileIdMap = mutableMapOf<Int, IndexFile.File>()
|
||||
|
||||
for (mod in mods) {
|
||||
if (mod.linkedFile!!.update == null) {
|
||||
failures.add(ExceptionDetails(mod.linkedFile!!.name ?: mod.linkedFile!!.filename!!, Exception("Failed to resolve CurseForge metadata: no update section")))
|
||||
continue
|
||||
}
|
||||
if (!mod.linkedFile!!.update!!.contains("curseforge")) {
|
||||
failures.add(ExceptionDetails(mod.linkedFile!!.name ?: mod.linkedFile!!.filename!!, Exception("Failed to resolve CurseForge metadata: no CurseForge update section")))
|
||||
continue
|
||||
}
|
||||
fileIdMap[(mod.linkedFile!!.update!!["curseforge"] as CurseForgeUpdateData).fileId] = mod
|
||||
}
|
||||
|
||||
val reqData = GetFilesRequest(fileIdMap.keys.toList())
|
||||
val req = Request.Builder()
|
||||
.url("https://${APIServer}/v1/mods/files")
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", "packwiz-installer")
|
||||
.header("X-API-Key", APIKey)
|
||||
.post(Gson().toJson(reqData, GetFilesRequest::class.java).toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
val res = clientHolder.okHttpClient.newCall(req).execute()
|
||||
if (!res.isSuccessful || res.body == null) {
|
||||
res.closeQuietly()
|
||||
failures.add(ExceptionDetails("Other", Exception("Failed to resolve CurseForge metadata for file data: error code ${res.code}")))
|
||||
return failures
|
||||
}
|
||||
|
||||
val resData = Gson().fromJson(res.body!!.charStream(), GetFilesResponse::class.java)
|
||||
res.closeQuietly()
|
||||
|
||||
val manualDownloadMods = mutableMapOf<Int, Pair<IndexFile.File, Int>>()
|
||||
for (file in resData.data) {
|
||||
if (!fileIdMap.contains(file.id)) {
|
||||
failures.add(ExceptionDetails(file.id.toString(),
|
||||
Exception("Failed to find file from result: ID ${file.id}, Project ID ${file.modId}")))
|
||||
continue
|
||||
}
|
||||
if (file.downloadUrl == null) {
|
||||
manualDownloadMods[file.modId] = Pair(fileIdMap[file.id]!!, file.id)
|
||||
continue
|
||||
}
|
||||
fileIdMap[file.id]!!.linkedFile!!.resolvedUpdateData["curseforge"] = file.downloadUrl!!
|
||||
}
|
||||
|
||||
if (manualDownloadMods.isNotEmpty()) {
|
||||
val reqModsData = GetModsRequest(manualDownloadMods.keys.toList())
|
||||
val reqMods = Request.Builder()
|
||||
.url("https://${APIServer}/v1/mods")
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", "packwiz-installer")
|
||||
.header("X-API-Key", APIKey)
|
||||
.post(Gson().toJson(reqModsData, GetModsRequest::class.java).toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
val resMods = clientHolder.okHttpClient.newCall(reqMods).execute()
|
||||
if (!resMods.isSuccessful || resMods.body == null) {
|
||||
resMods.closeQuietly()
|
||||
failures.add(ExceptionDetails("Other", Exception("Failed to resolve CurseForge metadata for mod data: error code ${resMods.code}")))
|
||||
return failures
|
||||
}
|
||||
|
||||
val resModsData = Gson().fromJson(resMods.body!!.charStream(), GetModsResponse::class.java)
|
||||
resMods.closeQuietly()
|
||||
|
||||
for (mod in resModsData.data) {
|
||||
if (!manualDownloadMods.contains(mod.id)) {
|
||||
failures.add(ExceptionDetails(mod.name,
|
||||
Exception("Failed to find project from result: ID ${mod.id}")))
|
||||
continue
|
||||
}
|
||||
|
||||
val modFile = manualDownloadMods[mod.id]!!
|
||||
failures.add(ExceptionDetails(mod.name, Exception("This mod is excluded from the CurseForge API and must be downloaded manually.\n" +
|
||||
"Please go to ${mod.links?.websiteUrl}/files/${modFile.second} and save this file to ${modFile.first.destURI}")))
|
||||
}
|
||||
}
|
||||
|
||||
return failures
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class CurseForgeUpdateData: UpdateData {
|
||||
@SerializedName("file-id")
|
||||
var fileId = 0
|
||||
@SerializedName("project-id")
|
||||
var projectId = 0
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
interface UpdateData
|
||||
@@ -0,0 +1,22 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class UpdateDeserializer: JsonDeserializer<Map<String, UpdateData>> {
|
||||
override fun deserialize(
|
||||
json: JsonElement?,
|
||||
typeOfT: Type?,
|
||||
context: JsonDeserializationContext?
|
||||
): Map<String, UpdateData> {
|
||||
val out = mutableMapOf<String, UpdateData>()
|
||||
for ((k, v) in json!!.asJsonObject.entrySet()) {
|
||||
if (k == "curseforge") {
|
||||
out[k] = context!!.deserialize(v, CurseForgeUpdateData::class.java)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user