Skip to content

Commit

Permalink
feat: download and extract default resourcePack for use in other plug…
Browse files Browse the repository at this point in the history
…ins (#77)

* feat: download and extract default resourcePack for use in other plugins

* refactor: use Bukkit#getMinecraftVersion for check, extract only when called on

* feat: more useful ResourcePacks api methods
  • Loading branch information
Boy0000 authored Aug 7, 2024
1 parent 29a67ec commit 941f969
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.mineinabyss.idofront

import com.mineinabyss.idofront.resourcepacks.MinecraftAssetExtractor
import org.bukkit.Bukkit
import org.bukkit.plugin.java.JavaPlugin

class IdofrontPlugin : JavaPlugin() {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.mineinabyss.idofront.resourcepacks

import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.mineinabyss.idofront.messaging.idofrontLogger
import org.bukkit.Bukkit
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileOutputStream
import java.net.URI
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream


object MinecraftAssetExtractor {

private const val VERSION_MANIFEST_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
val assetPath = Bukkit.getPluginsFolder().resolve("Idofront/assetCache/${Bukkit.getMinecraftVersion()}")

fun extractLatest() {
idofrontLogger.i("Extracting latest vanilla-assets...")
val versionInfo = runCatching {
downloadJson(findVersionInfoUrl() ?: return)?.asJsonObject
}.getOrNull() ?: return

val clientJar = downloadClientJar(versionInfo)
extractJarAssets(clientJar, assetPath)
idofrontLogger.s("Finished extracting vanilla assets for ${assetPath.name}")
}

private fun extractJarAssets(clientJar: ByteArray?, assetPath: File) {
var entry: ZipEntry

ByteArrayInputStream(clientJar).use { inputStream ->
ZipInputStream(inputStream).use { zipInputStream ->

kotlin.runCatching {
while (zipInputStream.nextEntry.also { entry = it } != null) {
if (!entry.name.startsWith("assets/")) continue
if (entry.name.startsWith("assets/minecraft/shaders")) continue
if (entry.name.startsWith("assets/minecraft/particles")) continue

val file = checkAndCreateFile(assetPath, entry)
if (entry.isDirectory && !file.isDirectory && !file.mkdirs())
error("Failed to create directory ${entry.name}")
else {
val parent = file.parentFile
if (!parent.isDirectory && !parent.mkdirs()) error("Failed to create directory ${parent?.path}")

runCatching {
zipInputStream.copyTo(FileOutputStream(file))
}.onFailure {
error("Failed to extract ${entry.name} from ${parent?.path}")
}
}
}
}
}
}
}

private fun checkAndCreateFile(assetPath: File, entry: ZipEntry): File {
val destFile = assetPath.resolve(entry.name)
val dirPath = assetPath.canonicalPath
val filePath = destFile.canonicalPath

if (!filePath.startsWith(dirPath + File.separator)) error("Entry outside target: ${entry.name}")
return destFile
}

private fun downloadClientJar(versionInfo: JsonObject) = runCatching {
val url = versionInfo.getAsJsonObject("downloads").getAsJsonObject("client").get("url").asString
URI(url).toURL().readBytes()
}.onFailure { it.printStackTrace() }.getOrNull() ?: error("Failed to download client JAR")

private fun findVersionInfoUrl(): String? {
val version = Bukkit.getMinecraftVersion()
if (!assetPath.mkdirs()) {
idofrontLogger.i("Latest has already been extracted for $version, skipping...")
return null
}

val manifest = runCatching {
downloadJson(VERSION_MANIFEST_URL)
}.getOrNull() ?: error("Failed to download version manifest")

return manifest.getAsJsonArray("versions").firstOrNull {
(it as? JsonObject)?.get("id")?.asString?.equals(version) ?: false
}?.asJsonObject?.get("url")?.asString ?: error("Failed to find version inof url for version $version")
}

private fun downloadJson(url: String) = runCatching { JsonParser.parseString(URI.create(url).toURL().readText()) }
.getOrNull()?.takeIf { it.isJsonObject }?.asJsonObject
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.mineinabyss.idofront.resourcepacks

import com.mineinabyss.idofront.messaging.idofrontLogger
import net.kyori.adventure.key.Key
import org.bukkit.Material
import team.unnamed.creative.ResourcePack
import team.unnamed.creative.model.Model
import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackReader
import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackWriter
import team.unnamed.creative.sound.SoundRegistry
Expand All @@ -11,6 +14,34 @@ object ResourcePacks {
val resourcePackWriter = MinecraftResourcePackWriter.builder().prettyPrinting(true).build()
val resourcePackReader = MinecraftResourcePackReader.builder().lenient(true).build()

/**
* Loads a copy of the vanilla resourcepack for the current version
* If it has not been yet, it will first be extracted locally
* The ResourcePack instance does not contain any of the vanilla OGG files due to filesize optimizations
*/
val defaultVanillaResourcePack by lazy {
MinecraftAssetExtractor.assetPath.apply { if (!exists()) MinecraftAssetExtractor.extractLatest() }.let(::readToResourcePack)
}

/**
* Returns the Model used in #defaultVanillaResourcePack by the given Material
*/
fun vanillaModelForMaterial(material: Material): Model? {
return defaultVanillaResourcePack?.model(vanillaKeyForMaterial(material))
}

/**
* Returns the vanilla Key used for the item-model
*/
fun vanillaKeyForMaterial(material: Material): Key {
return Key.key(when (material) {
Material.CROSSBOW -> "item/crossbow_standby"
Material.SPYGLASS -> "item/spyglass_in_hand"
Material.TRIDENT -> "item/trident_in_hand"
else -> "item/${material.key().value()}"
})
}

fun readToResourcePack(file: File): ResourcePack? {
return runCatching {
when {
Expand All @@ -32,14 +63,18 @@ object ResourcePacks {
}
}

/**
* Merges the content of two ResourcePacks, handling conflicts where possible
* Will sort ItemOverrides for models
*/
fun mergeResourcePacks(originalPack: ResourcePack, mergePack: ResourcePack) {
mergePack.textures().forEach(originalPack::texture)
mergePack.sounds().forEach(originalPack::sound)
mergePack.unknownFiles().forEach(originalPack::unknownFile)

mergePack.models().forEach { model ->
val baseModel = originalPack.model(model.key()) ?: return@forEach originalPack.model(model)
originalPack.model(model.apply { overrides().addAll(baseModel.overrides()) })
originalPack.model(ensureItemOverridesSorted(model.apply { overrides().addAll(baseModel.overrides()) }))
}
mergePack.fonts().forEach { font ->
val baseFont = originalPack.font(font.key()) ?: return@forEach originalPack.font(font)
Expand Down Expand Up @@ -69,19 +104,38 @@ object ResourcePacks {

if (originalPack.packMeta()?.description().isNullOrEmpty()) mergePack.packMeta()?.let { originalPack.packMeta(it) }
if (originalPack.icon() == null) mergePack.icon()?.let { originalPack.icon(it) }
sortItemOverrides(originalPack)
}

/**
* Ensures that the ResourcePack's models all have their ItemOverrides sorted based on their CustomModelData
* Ensures that a Model's overrides are sorted based on the CustomModelData predicate
* Returns the Model with any present overrides sorted
*/
fun sortItemOverrides(resourcePack: ResourcePack) {
resourcePack.models().toHashSet().forEach { model ->
val sortedOverrides = model.overrides().sortedBy { override ->
// value() is a LazilyParsedNumber so convert it to an Int
override.predicate().find { it.name() == "custom_model_data" }?.value()?.toString()?.toIntOrNull() ?: 0
}
resourcePack.model(model.toBuilder().overrides(sortedOverrides).build())
fun ensureItemOverridesSorted(model: Model): Model {
val sortedOverrides = (model.overrides().takeIf { it.isNotEmpty() } ?: return model).sortedBy { override ->
// value() is a LazilyParsedNumber so convert it to an Int
override.predicate().find { it.name() == "custom_model_data" }?.value()?.toString()?.toIntOrNull() ?: 0
}

return model.toBuilder().overrides(sortedOverrides).build()
}

/**
* Ensure that vanilla models have all their properties set
* Returns a new Model with all vanilla properties set, or the original Model if it was not a vanilla model
*/
fun ensureVanillaModelProperties(model: Model): Model {
val vanillaModel = defaultVanillaResourcePack?.model(model.key()) ?: return model
val builder = model.toBuilder()

if (model.textures().let { it.variables().isEmpty() && it.layers().isEmpty() && it.particle() == null })
builder.textures(vanillaModel.textures())
if (model.elements().isEmpty()) builder.elements(vanillaModel.elements())
if (model.overrides().isEmpty()) builder.overrides(vanillaModel.overrides())
if (model.display().isEmpty()) builder.display(vanillaModel.display())
if (model.guiLight() == null) builder.guiLight(vanillaModel.guiLight())
if (model.parent() == null) builder.parent(vanillaModel.parent())
if (!model.ambientOcclusion()) builder.ambientOcclusion(vanillaModel.ambientOcclusion())

return ensureItemOverridesSorted(builder.build())
}
}

0 comments on commit 941f969

Please sign in to comment.