feat: Add license viewer /firm licenses

This commit is contained in:
Linnea Gräf
2025-05-25 22:18:33 +02:00
parent 03064dd01f
commit 8f82b1e6bf
6 changed files with 264 additions and 46 deletions

View File

@@ -17,7 +17,7 @@ import org.apache.tools.ant.taskdefs.condition.Os
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.Base64 import java.util.*
plugins { plugins {
java java
@@ -227,8 +227,10 @@ val testAgent by configurations.creating {
} }
val configuredSourceSet = createIsolatedSourceSet("configured", val configuredSourceSet = createIsolatedSourceSet(
isEnabled = false) // Wait for update (also low prio, because configured sucks) "configured",
isEnabled = false
) // Wait for update (also low prio, because configured sucks)
val sodiumSourceSet = createIsolatedSourceSet("sodium", isEnabled = false) val sodiumSourceSet = createIsolatedSourceSet("sodium", isEnabled = false)
val citResewnSourceSet = createIsolatedSourceSet("citresewn", isEnabled = false) // TODO: Wait for update val citResewnSourceSet = createIsolatedSourceSet("citresewn", isEnabled = false) // TODO: Wait for update
val yaclSourceSet = createIsolatedSourceSet("yacl") val yaclSourceSet = createIsolatedSourceSet("yacl")
@@ -262,14 +264,14 @@ dependencies {
include(libs.hypixelmodapi.fabric) include(libs.hypixelmodapi.fabric)
compileOnly(projects.javaplugin) compileOnly(projects.javaplugin)
annotationProcessor(projects.javaplugin) annotationProcessor(projects.javaplugin)
implementation("com.google.auto.service:auto-service-annotations:1.1.1") nonModImplentation("com.google.auto.service:auto-service-annotations:1.1.1")
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
include(libs.manninghamMills) include(libs.manninghamMills)
include(libs.moulconfig) include(libs.moulconfig)
annotationProcessor(libs.mixinextras) annotationProcessor(libs.mixinextras)
implementation(libs.mixinextras) nonModImplentation(libs.mixinextras)
include(libs.mixinextras) include(libs.mixinextras)
nonModImplentation(libs.nealisp) nonModImplentation(libs.nealisp)
@@ -332,7 +334,8 @@ loom {
configureEach { configureEach {
property("fabric.log.level", "info") property("fabric.log.level", "info")
property("firmament.debug", "true") property("firmament.debug", "true")
property("firmament.classroots", property(
"firmament.classroots",
compatSourceSets.joinToString(File.pathSeparator) { compatSourceSets.joinToString(File.pathSeparator) {
File(it.output.classesDirs.asPath).absolutePath File(it.output.classesDirs.asPath).absolutePath
}) })
@@ -370,12 +373,16 @@ val updateTestRepo by tasks.registering {
doLast { doLast {
val propertiesFile = rootProject.file("gradle.properties") val propertiesFile = rootProject.file("gradle.properties")
val json = val json =
Gson().fromJson(uri("https://api.github.com/repos/NotEnoughUpdates/NotEnoughUpdates-REPO/branches/master") Gson().fromJson(
.toURL().readText(), JsonObject::class.java) uri("https://api.github.com/repos/NotEnoughUpdates/NotEnoughUpdates-REPO/branches/master")
.toURL().readText(), JsonObject::class.java
)
val latestSha = json["commit"].asJsonObject["sha"].asString val latestSha = json["commit"].asJsonObject["sha"].asString
var text = propertiesFile.readText() var text = propertiesFile.readText()
text = text.replace("firmament\\.compiletimerepohash=[^\n]*".toRegex(), text = text.replace(
"firmament.compiletimerepohash=$latestSha") "firmament\\.compiletimerepohash=[^\n]*".toRegex(),
"firmament.compiletimerepohash=$latestSha"
)
propertiesFile.writeText(text) propertiesFile.writeText(text)
} }
} }
@@ -389,8 +396,10 @@ tasks.test {
doFirst { doFirst {
wd.mkdirs() wd.mkdirs()
wd.resolve("config").deleteRecursively() wd.resolve("config").deleteRecursively()
systemProperty("firmament.testrepo", systemProperty(
downloadTestRepo.flatMap { it.outputDirectory.asFile }.map { it.absolutePath }.get()) "firmament.testrepo",
downloadTestRepo.flatMap { it.outputDirectory.asFile }.map { it.absolutePath }.get()
)
jvmArgs("-javaagent:${testAgent.singleFile.absolutePath}") jvmArgs("-javaagent:${testAgent.singleFile.absolutePath}")
} }
systemProperty("jdk.attach.allowAttachSelf", "true") systemProperty("jdk.attach.allowAttachSelf", "true")
@@ -408,13 +417,15 @@ tasks.withType<JavaCompile> {
this.targetCompatibility = "21" this.targetCompatibility = "21"
options.encoding = "UTF-8" options.encoding = "UTF-8"
val module = "ALL-UNNAMED" val module = "ALL-UNNAMED"
options.forkOptions.jvmArgs!!.addAll(listOf( options.forkOptions.jvmArgs!!.addAll(
listOf(
"--add-exports=jdk.compiler/com.sun.tools.javac.util=$module", "--add-exports=jdk.compiler/com.sun.tools.javac.util=$module",
"--add-exports=jdk.compiler/com.sun.tools.javac.comp=$module", "--add-exports=jdk.compiler/com.sun.tools.javac.comp=$module",
"--add-exports=jdk.compiler/com.sun.tools.javac.tree=$module", "--add-exports=jdk.compiler/com.sun.tools.javac.tree=$module",
"--add-exports=jdk.compiler/com.sun.tools.javac.api=$module", "--add-exports=jdk.compiler/com.sun.tools.javac.api=$module",
"--add-exports=jdk.compiler/com.sun.tools.javac.code=$module", "--add-exports=jdk.compiler/com.sun.tools.javac.code=$module",
)) )
)
options.isFork = true options.isFork = true
afterEvaluate { afterEvaluate {
options.compilerArgs.add("-Xplugin:IntermediaryNameReplacement mappingFile=${LoomGradleExtension.get(project).mappingsFile.absolutePath} sourceNs=named") options.compilerArgs.add("-Xplugin:IntermediaryNameReplacement mappingFile=${LoomGradleExtension.get(project).mappingsFile.absolutePath} sourceNs=named")
@@ -463,12 +474,18 @@ tasks.processResources {
tasks.scanLicenses { tasks.scanLicenses {
scanConfiguration(nonModImplentation) scanConfiguration(nonModImplentation)
scanConfiguration(configurations.modCompileClasspath.get()) scanConfiguration(configurations.modCompileClasspath.get())
compatSourceSets.forEach {
scanConfiguration(it.modImplementationConfigurationName.get())
}
outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.json")) outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.json"))
licenseFormatter.set(moe.nea.licenseextractificator.JsonLicenseFormatter()) licenseFormatter.set(moe.nea.licenseextractificator.JsonLicenseFormatter())
} }
tasks.create("printAllLicenses", LicenseDiscoveryTask::class.java, licensing).apply { tasks.register("printAllLicenses", LicenseDiscoveryTask::class.java, licensing).configure {
outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.txt")) outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.txt"))
licenseFormatter.set(moe.nea.licenseextractificator.TextLicenseFormatter()) licenseFormatter.set(moe.nea.licenseextractificator.TextLicenseFormatter())
compatSourceSets.forEach {
scanConfiguration(it.modImplementationConfigurationName.get())
}
scanConfiguration(nonModImplentation) scanConfiguration(nonModImplentation)
scanConfiguration(configurations.modCompileClasspath.get()) scanConfiguration(configurations.modCompileClasspath.get())
doLast { doLast {
@@ -505,16 +522,20 @@ fun patchRenderDoc(
if (!fileF.exists()) { if (!fileF.exists()) {
fileF.parentFile.mkdirs() fileF.parentFile.mkdirs()
if (isWindows) { if (isWindows) {
fileF.writeText(""" fileF.writeText(
"""
setlocal enableextensions setlocal enableextensions
start "" renderdoccmd.exe capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" %* start "" renderdoccmd.exe capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" %*
endlocal endlocal
""".trimIndent()) """.trimIndent()
)
} else { } else {
fileF.writeText(""" fileF.writeText(
"""
#!/usr/bin/env bash #!/usr/bin/env bash
exec renderdoccmd capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" "$@" exec renderdoccmd capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" "$@"
""".trimIndent()) """.trimIndent()
)
fileF.setExecutable(true) fileF.setExecutable(true)
} }
} }

View File

@@ -0,0 +1,128 @@
package moe.nea.firmament.features.misc
import io.github.notenoughupdates.moulconfig.observer.ObservableList
import io.github.notenoughupdates.moulconfig.xml.Bind
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.decodeFromStream
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil
import moe.nea.firmament.util.tr
object LicenseViewer {
@Serializable
data class Software(
val licenses: List<License> = listOf(),
val webPresence: String? = null,
val projectName: String,
val projectDescription: String? = null,
val developers: List<Developer> = listOf(),
) {
@Bind
fun hasWebPresence() = webPresence != null
@Bind
fun webPresence() = webPresence ?: "<no web presence>"
@Bind
fun open() {
MC.openUrl(webPresence ?: return)
}
@Bind
fun projectName() = projectName
@Bind
fun projectDescription() = projectDescription ?: "<no project description>"
@get:Bind("developers")
@Transient
val developersO = ObservableList(developers)
@get:Bind("licenses")
@Transient
val licenses0 = ObservableList(licenses)
}
@Serializable
data class Developer(
@get:Bind("name") val name: String,
val webPresence: String? = null
) {
@Bind
fun open() {
MC.openUrl(webPresence ?: return)
}
@Bind
fun hasWebPresence() = webPresence != null
@Bind
fun webPresence() = webPresence ?: "<no web presence>"
}
@Serializable
data class License(
@get:Bind("name") val licenseName: String,
val licenseUrl: String? = null
) {
@Bind
fun open() {
MC.openUrl(licenseUrl ?: return)
}
@Bind
fun hasUrl() = licenseUrl != null
@Bind
fun url() = licenseUrl ?: "<no link to license text>"
}
data class LicenseList(
val softwares: List<Software>
) {
@get:Bind("softwares")
val obs = ObservableList(softwares)
}
@OptIn(ExperimentalSerializationApi::class)
val licenses: LicenseList? = ErrorUtil.catch("Could not load licenses") {
Firmament.json.decodeFromStream<List<Software>?>(
javaClass.getResourceAsStream("/LICENSES-FIRMAMENT.json") ?: error("Could not find LICENSES-FIRMAMENT.json")
)?.let { LicenseList(it) }
}.orNull()
fun showLicenses() {
ErrorUtil.catch("Could not display licenses") {
ScreenUtil.setScreenLater(
MoulConfigUtils.loadScreen(
"license_viewer/index", licenses!!, null
)
)
}.or {
MC.sendChat(
tr(
"firmament.licenses.notfound",
"Could not load licenses. Please check the Firmament source code for information directly."
)
)
}
}
@Subscribe
fun onSubcommand(event: CommandEvent.SubCommand) {
event.subcommand("licenses") {
thenExecute {
showLicenses()
}
}
}
}

View File

@@ -38,6 +38,8 @@ object ErrorUtil {
} }
class Catch<T> private constructor(val value: T?, val exc: Throwable?) { class Catch<T> private constructor(val value: T?, val exc: Throwable?) {
fun orNull(): T? = value
inline fun or(block: (exc: Throwable) -> T): T { inline fun or(block: (exc: Throwable) -> T): T {
contract { contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE) callsInPlace(block, InvocationKind.AT_MOST_ONCE)

View File

@@ -21,10 +21,10 @@ import net.minecraft.registry.Registry
import net.minecraft.registry.RegistryKey import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys import net.minecraft.registry.RegistryKeys
import net.minecraft.registry.RegistryWrapper import net.minecraft.registry.RegistryWrapper
import net.minecraft.registry.entry.RegistryEntry
import net.minecraft.resource.ReloadableResourceManagerImpl import net.minecraft.resource.ReloadableResourceManagerImpl
import net.minecraft.text.Text import net.minecraft.text.Text
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.minecraft.util.Util
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.world.World import net.minecraft.world.World
import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.TickEvent
@@ -127,6 +127,10 @@ object MC {
private set private set
fun openUrl(uri: String) {
Util.getOperatingSystem().open(uri)
}
fun <T> unsafeGetRegistryEntry(registry: RegistryKey<out Registry<T>>, identifier: Identifier) = fun <T> unsafeGetRegistryEntry(registry: RegistryKey<out Registry<T>>, identifier: Identifier) =
unsafeGetRegistryEntry(RegistryKey.of(registry, identifier)) unsafeGetRegistryEntry(RegistryKey.of(registry, identifier))

View File

@@ -37,6 +37,19 @@ import moe.nea.firmament.gui.TickComponent
import moe.nea.firmament.util.render.isUntranslatedGuiDrawContext import moe.nea.firmament.util.render.isUntranslatedGuiDrawContext
object MoulConfigUtils { object MoulConfigUtils {
@JvmStatic
fun main(args: Array<out String>) {
generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS)
generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl)
File("wrapper.xsd").writeText("""
<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/>
<xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/>
</xs:schema>
""".trimIndent())
}
val firmUrl = "http://firmament.nea.moe/moulconfig" val firmUrl = "http://firmament.nea.moe/moulconfig"
val universe = XMLUniverse.getDefaultUniverse().also { uni -> val universe = XMLUniverse.getDefaultUniverse().also { uni ->
uni.registerMapper(java.awt.Color::class.java) { uni.registerMapper(java.awt.Color::class.java) {
@@ -181,10 +194,8 @@ object MoulConfigUtils {
uni.registerLoader(object : XMLGuiLoader.Basic<FixedComponent> { uni.registerLoader(object : XMLGuiLoader.Basic<FixedComponent> {
override fun createInstance(context: XMLContext<*>, element: Element): FixedComponent { override fun createInstance(context: XMLContext<*>, element: Element): FixedComponent {
return FixedComponent( return FixedComponent(
context.getPropertyFromAttribute(element, QName("width"), Int::class.java) context.getPropertyFromAttribute(element, QName("width"), Int::class.java),
?: error("Requires width specified"), context.getPropertyFromAttribute(element, QName("height"), Int::class.java),
context.getPropertyFromAttribute(element, QName("height"), Int::class.java)
?: error("Requires height specified"),
context.getChildFragment(element) context.getChildFragment(element)
) )
} }
@@ -198,7 +209,7 @@ object MoulConfigUtils {
} }
override fun getAttributeNames(): Map<String, Boolean> { override fun getAttributeNames(): Map<String, Boolean> {
return mapOf("width" to true, "height" to true) return mapOf("width" to false, "height" to false)
} }
}) })
} }
@@ -212,19 +223,6 @@ object MoulConfigUtils {
generator.dumpToFile(file) generator.dumpToFile(file)
} }
@JvmStatic
fun main(args: Array<out String>) {
generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS)
generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl)
File("wrapper.xsd").writeText("""
<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/>
<xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/>
</xs:schema>
""".trimIndent())
}
fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen { fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen {
return object : GuiComponentWrapper(loadGui(name, bindTo)) { return object : GuiComponentWrapper(loadGui(name, bindTo)) {
override fun close() { override fun close() {

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Root xmlns="http://notenoughupdates.org/moulconfig"
xmlns:firm="http://firmament.nea.moe/moulconfig"
>
<Center>
<Panel background="VANILLA">
<Column>
<Center>
<Scale scale="2">
<Text text="Firmament Licenses"/>
</Scale>
</Center>
<!-- <firm:Line/>-->
<ScrollPanel width="306" height="250">
<Panel insets="3" background="TRANSPARENT">
<Array data="@softwares">
<Center>
<firm:Fixed width="300">
<Panel background="VANILLA" insets="8">
<Column>
<Scale scale="1.2">
<Text text="@projectName"/>
</Scale>
<When condition="@hasWebPresence">
<Row>
<firm:Button onClick="@open">
<Text text="Navigate to WebSite"/>
</firm:Button>
</Row>
<Spacer/>
</When>
<Text text="@projectDescription" width="280"/>
<Array data="@developers">
<Row>
<Text text="by "/>
<Text text="@name"/>
</Row>
</Array>
<Array data="@licenses">
<When condition="@hasUrl">
<firm:Button onClick="@open">
<Center>
<Row>
<Text text="License: "/>
<Text text="@name"/>
</Row>
</Center>
</firm:Button>
<Row>
<Text text="License: "/>
<Text text="@name"/>
</Row>
</When>
</Array>
</Column>
</Panel>
</firm:Fixed>
</Center>
</Array>
</Panel>
</ScrollPanel>
</Column>
</Panel>
</Center>
</Root>