aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--REUSE.toml6
-rw-r--r--build.gradle.kts560
-rw-r--r--src/main/kotlin/Firmament.kt173
-rw-r--r--src/main/kotlin/events/HudRenderEvent.kt4
-rw-r--r--src/main/kotlin/features/debug/PowerUserTools.kt7
-rw-r--r--src/main/kotlin/features/mining/PickaxeAbility.kt262
-rw-r--r--src/main/kotlin/features/texturepack/NumberMatcher.kt231
-rw-r--r--src/main/kotlin/util/ErrorUtil.kt16
-rw-r--r--src/main/kotlin/util/MC.kt131
-rw-r--r--src/main/kotlin/util/TimeMark.kt82
-rw-r--r--src/main/kotlin/util/mc/SNbtFormatter.kt138
-rw-r--r--src/main/kotlin/util/regex.kt14
-rw-r--r--src/main/kotlin/util/skyblock/AbilityUtils.kt138
-rw-r--r--src/main/kotlin/util/textutil.kt2
-rw-r--r--src/main/resources/assets/firmament/lang/en_us.json4
-rw-r--r--src/main/resources/firmament.accesswidener5
-rw-r--r--src/test/kotlin/root.kt29
-rw-r--r--src/test/kotlin/testutil/ItemResources.kt30
-rw-r--r--src/test/kotlin/util/ColorCodeTest.kt (renamed from src/test/kotlin/moe/nea/firmament/test/ColorCode.kt)7
-rw-r--r--src/test/kotlin/util/skyblock/AbilityUtilsTest.kt79
-rw-r--r--src/test/resources/testdata/items/aspect-of-the-void.snbt59
-rw-r--r--src/test/resources/testdata/items/diamond-pickaxe.snbt48
-rw-r--r--src/test/resources/testdata/items/titanium-drill.snbt97
-rw-r--r--symbols/src/main/kotlin/process/GameTestContainingClassProcessor.kt80
24 files changed, 1477 insertions, 725 deletions
diff --git a/REUSE.toml b/REUSE.toml
index b5bd945..0ea37e6 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -41,3 +41,9 @@ SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"
path = ["**/META-INF/services/*"]
SPDX-License-Identifier = "CC0-1.0"
SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"]
+
+[[annotations]]
+path = ["src/test/resources/testdata/**/*.snbt"]
+SPDX-License-Identifier = "CC-BY-4.0"
+SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"]
+
diff --git a/build.gradle.kts b/build.gradle.kts
index cdb97ff..bb15a1f 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,6 +7,7 @@
*/
import com.google.devtools.ksp.gradle.KspTaskJvm
+import com.google.gson.JsonArray
import moe.nea.licenseextractificator.LicenseDiscoveryTask
import net.fabricmc.loom.LoomGradleExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
@@ -14,353 +15,356 @@ import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
- java
- `maven-publish`
- alias(libs.plugins.kotlin.jvm)
- alias(libs.plugins.kotlin.plugin.serialization)
- alias(libs.plugins.kotlin.plugin.powerassert)
- alias(libs.plugins.kotlin.plugin.ksp)
- alias(libs.plugins.loom)
- id("com.github.johnrengelman.shadow") version "8.1.1"
- id("moe.nea.licenseextractificator")
+ java
+ `maven-publish`
+ alias(libs.plugins.kotlin.jvm)
+ alias(libs.plugins.kotlin.plugin.serialization)
+ alias(libs.plugins.kotlin.plugin.powerassert)
+ alias(libs.plugins.kotlin.plugin.ksp)
+ alias(libs.plugins.loom)
+ id("com.github.johnrengelman.shadow") version "8.1.1"
+ id("moe.nea.licenseextractificator")
}
version = getGitTagInfo()
group = rootProject.property("maven_group").toString()
java {
- withSourcesJar()
- toolchain {
- languageVersion.set(JavaLanguageVersion.of(21))
- }
+ withSourcesJar()
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(21))
+ }
}
tasks.withType(KotlinCompile::class) {
- compilerOptions {
- jvmTarget.set(JvmTarget.JVM_21)
- }
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_21)
+ }
}
allprojects {
- repositories {
- mavenCentral()
- maven("https://maven.terraformersmc.com/releases/")
- maven("https://maven.shedaniel.me")
- maven("https://maven.fabricmc.net")
- maven("https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1")
- maven("https://api.modrinth.com/maven") {
- content {
- includeGroup("maven.modrinth")
- }
- }
- maven("https://repo.sleeping.town") {
- content {
- includeGroup("com.unascribed")
- }
- }
- ivy("https://github.com/HotswapProjects/HotswapAgent/releases/download") {
- patternLayout {
- artifact("[revision]/[artifact]-[revision].[ext]")
- }
- content {
- includeGroup("virtual.github.hotswapagent")
- }
- metadataSources {
- artifact()
- }
- }
- maven("https://server.bbkr.space/artifactory/libs-release")
- maven("https://repo.nea.moe/releases")
- maven("https://maven.notenoughupdates.org/releases")
- maven("https://repo.nea.moe/mirror")
- maven("https://jitpack.io/") {
- content {
- includeGroupByRegex("(com|io)\\.github\\..+")
- excludeModule("io.github.cottonmc", "LibGui")
- }
- }
- maven("https://repo.hypixel.net/repository/Hypixel/")
- maven("https://maven.azureaaron.net/snapshots")
- maven("https://maven.azureaaron.net/releases")
- maven("https://www.cursemaven.com")
- mavenLocal()
- }
+ repositories {
+ mavenCentral()
+ maven("https://maven.terraformersmc.com/releases/")
+ maven("https://maven.shedaniel.me")
+ maven("https://maven.fabricmc.net")
+ maven("https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1")
+ maven("https://api.modrinth.com/maven") {
+ content {
+ includeGroup("maven.modrinth")
+ }
+ }
+ maven("https://repo.sleeping.town") {
+ content {
+ includeGroup("com.unascribed")
+ }
+ }
+ ivy("https://github.com/HotswapProjects/HotswapAgent/releases/download") {
+ patternLayout {
+ artifact("[revision]/[artifact]-[revision].[ext]")
+ }
+ content {
+ includeGroup("virtual.github.hotswapagent")
+ }
+ metadataSources {
+ artifact()
+ }
+ }
+ maven("https://server.bbkr.space/artifactory/libs-release")
+ maven("https://repo.nea.moe/releases")
+ maven("https://maven.notenoughupdates.org/releases")
+ maven("https://repo.nea.moe/mirror")
+ maven("https://jitpack.io/") {
+ content {
+ includeGroupByRegex("(com|io)\\.github\\..+")
+ excludeModule("io.github.cottonmc", "LibGui")
+ }
+ }
+ maven("https://repo.hypixel.net/repository/Hypixel/")
+ maven("https://maven.azureaaron.net/snapshots")
+ maven("https://maven.azureaaron.net/releases")
+ maven("https://www.cursemaven.com")
+ mavenLocal()
+ }
}
kotlin {
- sourceSets.all {
- languageSettings {
- enableLanguageFeature("BreakContinueInInlineLambdas")
- }
- }
+ sourceSets.all {
+ languageSettings {
+ enableLanguageFeature("BreakContinueInInlineLambdas")
+ }
+ }
}
fun String.capitalizeN() = replaceFirstChar { it.uppercaseChar() }
fun innerJarsOf(name: String, dependency: Dependency): FileCollection {
- val task = tasks.create("unpackInnerJarsFor${name.capitalizeN()}", InnerJarsUnpacker::class) {
- this.inputJars.setFrom(files(configurations.detachedConfiguration(dependency)))
- this.outputDir.set(layout.buildDirectory.dir("unpackedJars/$name").also {
- it.get().asFile.mkdirs()
- })
- }
- println("Constructed innerJars task: ${project.files(task).toList()}")
- return project.files(task)
+ val task = tasks.create("unpackInnerJarsFor${name.capitalizeN()}", InnerJarsUnpacker::class) {
+ this.inputJars.setFrom(files(configurations.detachedConfiguration(dependency)))
+ this.outputDir.set(layout.buildDirectory.dir("unpackedJars/$name").also {
+ it.get().asFile.mkdirs()
+ })
+ }
+ println("Constructed innerJars task: ${project.files(task).toList()}")
+ return project.files(task)
}
val compatSourceSets: MutableSet<SourceSet> = mutableSetOf()
fun createIsolatedSourceSet(name: String, path: String = "compat/$name"): SourceSet {
- val ss = sourceSets.create(name) {
- this.java.setSrcDirs(listOf(layout.projectDirectory.dir("src/$path/java")))
- this.kotlin.setSrcDirs(listOf(layout.projectDirectory.dir("src/$path/java")))
- }
- compatSourceSets.add(ss)
- loom.createRemapConfigurations(ss)
- val mainSS = sourceSets.main.get()
- val upperName = ss.name.capitalizeN()
- configurations {
- (ss.implementationConfigurationName) {
- extendsFrom(getByName(mainSS.compileClasspathConfigurationName))
- }
- (ss.annotationProcessorConfigurationName) {
- extendsFrom(getByName(mainSS.annotationProcessorConfigurationName))
- }
- (mainSS.runtimeOnlyConfigurationName) {
- extendsFrom(getByName(ss.runtimeClasspathConfigurationName))
- }
- ("ksp$upperName") {
- extendsFrom(ksp.get())
- }
- }
- afterEvaluate {
- tasks.named("ksp${upperName}Kotlin", KspTaskJvm::class) {
- this.options.add(SubpluginOption("apoption", "firmament.sourceset=${ss.name}"))
- }
- }
- dependencies {
- runtimeOnly(ss.output)
- (ss.implementationConfigurationName)(sourceSets.main.get().output)
- }
- tasks.shadowJar {
- from(ss.output)
- }
- return ss
+ val ss = sourceSets.create(name) {
+ this.java.setSrcDirs(listOf(layout.projectDirectory.dir("src/$path/java")))
+ this.kotlin.setSrcDirs(listOf(layout.projectDirectory.dir("src/$path/java")))
+ }
+ compatSourceSets.add(ss)
+ loom.createRemapConfigurations(ss)
+ val mainSS = sourceSets.main.get()
+ val upperName = ss.name.capitalizeN()
+ configurations {
+ (ss.implementationConfigurationName) {
+ extendsFrom(getByName(mainSS.compileClasspathConfigurationName))
+ }
+ (ss.annotationProcessorConfigurationName) {
+ extendsFrom(getByName(mainSS.annotationProcessorConfigurationName))
+ }
+ (mainSS.runtimeOnlyConfigurationName) {
+ extendsFrom(getByName(ss.runtimeClasspathConfigurationName))
+ }
+ ("ksp$upperName") {
+ extendsFrom(ksp.get())
+ }
+ }
+ afterEvaluate {
+ tasks.named("ksp${upperName}Kotlin", KspTaskJvm::class) {
+ this.options.add(SubpluginOption("apoption", "firmament.sourceset=${ss.name}"))
+ }
+ }
+ dependencies {
+ runtimeOnly(ss.output)
+ (ss.implementationConfigurationName)(sourceSets.main.get().output)
+ }
+ tasks.shadowJar {
+ from(ss.output)
+ }
+ return ss
}
val SourceSet.modImplementationConfigurationName
- get() =
- loom.remapConfigurations.find {
- it.targetConfigurationName.get() == this.implementationConfigurationName
- }!!.sourceConfiguration
+ get() =
+ loom.remapConfigurations.find {
+ it.targetConfigurationName.get() == this.implementationConfigurationName
+ }!!.sourceConfiguration
val configuredSourceSet = createIsolatedSourceSet("configured")
val sodiumSourceSet = createIsolatedSourceSet("sodium")
val citResewnSourceSet = createIsolatedSourceSet("citresewn")
val shadowMe by configurations.creating {
- exclude(group = "org.jetbrains.kotlin")
- exclude(group = "org.jetbrains.kotlinx")
- exclude(group = "org.jetbrains")
- exclude(module = "gson")
- exclude(group = "org.slf4j")
+ exclude(group = "org.jetbrains.kotlin")
+ exclude(group = "org.jetbrains.kotlinx")
+ exclude(group = "org.jetbrains")
+ exclude(module = "gson")
+ exclude(group = "org.slf4j")
}
val transInclude by configurations.creating {
- exclude(group = "com.mojang")
- exclude(group = "org.jetbrains.kotlin")
- exclude(group = "org.jetbrains.kotlinx")
- isTransitive = true
+ exclude(group = "com.mojang")
+ exclude(group = "org.jetbrains.kotlin")
+ exclude(group = "org.jetbrains.kotlinx")
+ isTransitive = true
}
val hotswap by configurations.creating {
- isVisible = false
+ isVisible = false
}
val nonModImplentation by configurations.creating {
- configurations.implementation.get().extendsFrom(this)
+ configurations.implementation.get().extendsFrom(this)
}
dependencies {
- // Minecraft dependencies
- "minecraft"(libs.minecraft)
- "mappings"("net.fabricmc:yarn:${libs.versions.yarn.get()}:v2")
-
- // Hotswap Dependency
- hotswap(libs.hotswap)
-
- // Fabric dependencies
- modImplementation(libs.fabric.loader)
- modImplementation(libs.fabric.kotlin)
- modImplementation(libs.modmenu)
- modImplementation(libs.moulconfig)
- modImplementation(libs.manninghamMills)
- modCompileOnly(libs.explosiveenhancement)
- modImplementation(libs.hypixelmodapi)
- include(libs.hypixelmodapi.fabric)
- compileOnly(project(":javaplugin"))
- annotationProcessor(project(":javaplugin"))
- implementation("com.google.auto.service:auto-service-annotations:1.1.1")
- ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
- include(libs.manninghamMills)
- include(libs.moulconfig)
-
-
- annotationProcessor(libs.mixinextras)
- implementation(libs.mixinextras)
- include(libs.mixinextras)
-
- nonModImplentation(libs.nealisp)
- shadowMe(libs.nealisp)
-
- modCompileOnly(libs.fabric.api)
- modRuntimeOnly(libs.fabric.api.deprecated)
- modApi(libs.architectury)
- modCompileOnly(libs.jarvis.api)
- include(libs.jarvis.fabric)
-
- modCompileOnly(libs.femalegender)
- (configuredSourceSet.modImplementationConfigurationName)(libs.configured)
- (sodiumSourceSet.modImplementationConfigurationName)(libs.sodium)
-
- (citResewnSourceSet.modImplementationConfigurationName)(
- innerJarsOf("citresewn", dependencies.create(libs.citresewn.get())).asFileTree)
- (citResewnSourceSet.modImplementationConfigurationName)(libs.citresewn)
-
- // Actual dependencies
- modCompileOnly(libs.rei.api) {
- exclude(module = "architectury")
- exclude(module = "architectury-fabric")
- }
- nonModImplentation(libs.repoparser)
- shadowMe(libs.repoparser)
- fun ktor(mod: String) = "io.ktor:ktor-$mod-jvm:${libs.versions.ktor.get()}"
- modCompileOnly(libs.citresewn)
- transInclude(nonModImplentation(ktor("client-core"))!!)
- transInclude(nonModImplentation(ktor("client-java"))!!)
- transInclude(nonModImplentation(ktor("serialization-kotlinx-json"))!!)
- transInclude(nonModImplentation(ktor("client-content-negotiation"))!!)
- transInclude(nonModImplentation(ktor("client-encoding"))!!)
- transInclude(nonModImplentation(ktor("client-logging"))!!)
-
- // Dev environment preinstalled mods
- modLocalRuntime(libs.bundles.runtime.required)
- modLocalRuntime(libs.bundles.runtime.optional)
- modLocalRuntime(libs.jarvis.fabric)
-
- transInclude.resolvedConfiguration.resolvedArtifacts.forEach {
- include(it.moduleVersion.id.toString())
- }
-
-
- testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
-
- implementation(project(":symbols"))
- ksp(project(":symbols"))
+ // Minecraft dependencies
+ "minecraft"(libs.minecraft)
+ "mappings"("net.fabricmc:yarn:${libs.versions.yarn.get()}:v2")
+
+ // Hotswap Dependency
+ hotswap(libs.hotswap)
+
+ // Fabric dependencies
+ modImplementation(libs.fabric.loader)
+ modImplementation(libs.fabric.kotlin)
+ modImplementation(libs.modmenu)
+ modImplementation(libs.moulconfig)
+ modImplementation(libs.manninghamMills)
+ modCompileOnly(libs.explosiveenhancement)
+ modImplementation(libs.hypixelmodapi)
+ include(libs.hypixelmodapi.fabric)
+ compileOnly(project(":javaplugin"))
+ annotationProcessor(project(":javaplugin"))
+ implementation("com.google.auto.service:auto-service-annotations:1.1.1")
+ ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
+ include(libs.manninghamMills)
+ include(libs.moulconfig)
+
+
+ annotationProcessor(libs.mixinextras)
+ implementation(libs.mixinextras)
+ include(libs.mixinextras)
+
+ nonModImplentation(libs.nealisp)
+ shadowMe(libs.nealisp)
+
+ modCompileOnly(libs.fabric.api)
+ modRuntimeOnly(libs.fabric.api.deprecated)
+ modApi(libs.architectury)
+ modCompileOnly(libs.jarvis.api)
+ include(libs.jarvis.fabric)
+
+ modCompileOnly(libs.femalegender)
+ (configuredSourceSet.modImplementationConfigurationName)(libs.configured)
+ (sodiumSourceSet.modImplementationConfigurationName)(libs.sodium)
+
+ (citResewnSourceSet.modImplementationConfigurationName)(
+ innerJarsOf("citresewn", dependencies.create(libs.citresewn.get())).asFileTree)
+ (citResewnSourceSet.modImplementationConfigurationName)(libs.citresewn)
+
+ // Actual dependencies
+ modCompileOnly(libs.rei.api) {
+ exclude(module = "architectury")
+ exclude(module = "architectury-fabric")
+ }
+ nonModImplentation(libs.repoparser)
+ shadowMe(libs.repoparser)
+ fun ktor(mod: String) = "io.ktor:ktor-$mod-jvm:${libs.versions.ktor.get()}"
+ modCompileOnly(libs.citresewn)
+ transInclude(nonModImplentation(ktor("client-core"))!!)
+ transInclude(nonModImplentation(ktor("client-java"))!!)
+ transInclude(nonModImplentation(ktor("serialization-kotlinx-json"))!!)
+ transInclude(nonModImplentation(ktor("client-content-negotiation"))!!)
+ transInclude(nonModImplentation(ktor("client-encoding"))!!)
+ transInclude(nonModImplentation(ktor("client-logging"))!!)
+
+ // Dev environment preinstalled mods
+ modLocalRuntime(libs.bundles.runtime.required)
+ modLocalRuntime(libs.bundles.runtime.optional)
+ modLocalRuntime(libs.jarvis.fabric)
+
+ transInclude.resolvedConfiguration.resolvedArtifacts.forEach {
+ include(it.moduleVersion.id.toString())
+ }
+
+
+ testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
+
+ implementation(project(":symbols"))
+ ksp(project(":symbols"))
}
-tasks.test {
- useJUnitPlatform()
+loom {
+ clientOnlyMinecraftJar()
+ accessWidenerPath.set(project.file("src/main/resources/firmament.accesswidener"))
+ runs {
+ removeIf { it.name != "client" }
+ configureEach {
+ property("fabric.log.level", "info")
+ property("firmament.debug", "true")
+ property("firmament.classroots",
+ compatSourceSets.joinToString(File.pathSeparator) {
+ File(it.output.classesDirs.asPath).absolutePath
+ })
+ property("mixin.debug", "true")
+
+ parseEnvFile(file(".env")).forEach { (t, u) ->
+ environmentVariable(t, u)
+ }
+ parseEnvFile(file(".properties")).forEach { (t, u) ->
+ property(t, u)
+ }
+ }
+ named("client") {
+ property("devauth.enabled", "true")
+ vmArg("-ea")
+ vmArg("-XX:+AllowEnhancedClassRedefinition")
+ vmArg("-XX:HotswapAgent=external")
+ vmArg("-javaagent:${hotswap.resolve().single().absolutePath}")
+ }
+ }
}
-loom {
- clientOnlyMinecraftJar()
- accessWidenerPath.set(project.file("src/main/resources/firmament.accesswidener"))
- runs {
- removeIf { it.name != "client" }
- named("client") {
- property("devauth.enabled", "true")
- property("fabric.log.level", "info")
- property("firmament.debug", "true")
- property("firmament.classroots",
- compatSourceSets.joinToString(File.pathSeparator) {
- File(it.output.classesDirs.asPath).absolutePath
- })
- property("mixin.debug", "true")
-
- parseEnvFile(file(".env")).forEach { (t, u) ->
- environmentVariable(t, u)
- }
- parseEnvFile(file(".properties")).forEach { (t, u) ->
- property(t, u)
- }
- vmArg("-ea")
- vmArg("-XX:+AllowEnhancedClassRedefinition")
- vmArg("-XX:HotswapAgent=external")
- vmArg("-javaagent:${hotswap.resolve().single().absolutePath}")
- }
- }
+tasks.test {
+ useJUnitPlatform()
}
+
tasks.withType<JavaCompile> {
- this.sourceCompatibility = "21"
- this.targetCompatibility = "21"
- options.encoding = "UTF-8"
- val module = "ALL-UNNAMED"
- options.forkOptions.jvmArgs!!.addAll(listOf(
- "--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.tree=$module",
- "--add-exports=jdk.compiler/com.sun.tools.javac.api=$module",
- "--add-exports=jdk.compiler/com.sun.tools.javac.code=$module",
- ))
- options.isFork = true
- afterEvaluate {
- options.compilerArgs.add("-Xplugin:IntermediaryNameReplacement mappingFile=${LoomGradleExtension.get(project).mappingsFile.absolutePath} sourceNs=named")
- }
+ this.sourceCompatibility = "21"
+ this.targetCompatibility = "21"
+ options.encoding = "UTF-8"
+ val module = "ALL-UNNAMED"
+ options.forkOptions.jvmArgs!!.addAll(listOf(
+ "--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.tree=$module",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.api=$module",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.code=$module",
+ ))
+ options.isFork = true
+ afterEvaluate {
+ options.compilerArgs.add("-Xplugin:IntermediaryNameReplacement mappingFile=${LoomGradleExtension.get(project).mappingsFile.absolutePath} sourceNs=named")
+ }
}
tasks.jar {
- destinationDirectory.set(layout.buildDirectory.dir("badjars"))
- archiveClassifier.set("slim")
+ destinationDirectory.set(layout.buildDirectory.dir("badjars"))
+ archiveClassifier.set("slim")
}
tasks.shadowJar {
- configurations = listOf(shadowMe)
- archiveClassifier.set("dev")
- relocate("io.github.moulberry.repo", "moe.nea.firmament.deps.repo")
- destinationDirectory.set(layout.buildDirectory.dir("badjars"))
- mergeServiceFiles()
+ configurations = listOf(shadowMe)
+ archiveClassifier.set("dev")
+ relocate("io.github.moulberry.repo", "moe.nea.firmament.deps.repo")
+ destinationDirectory.set(layout.buildDirectory.dir("badjars"))
+ mergeServiceFiles()
}
tasks.remapJar {
- injectAccessWidener.set(true)
- inputFile.set(tasks.shadowJar.flatMap { it.archiveFile })
- dependsOn(tasks.shadowJar)
- archiveClassifier.set("")
+ injectAccessWidener.set(true)
+ inputFile.set(tasks.shadowJar.flatMap { it.archiveFile })
+ dependsOn(tasks.shadowJar)
+ archiveClassifier.set("")
}
tasks.processResources {
- val replacements = listOf(
- "version" to project.version.toString(),
- "minecraft_version" to libs.versions.minecraft.get(),
- "fabric_kotlin_version" to libs.versions.fabric.kotlin.get(),
- "rei_version" to libs.versions.rei.get()
- )
- replacements.forEach { (key, value) -> inputs.property(key, value) }
- filesMatching("**/fabric.mod.json") {
- expand(*replacements.toTypedArray())
- }
- exclude("**/*.license")
- from(tasks.scanLicenses)
+ val replacements = listOf(
+ "version" to project.version.toString(),
+ "minecraft_version" to libs.versions.minecraft.get(),
+ "fabric_kotlin_version" to libs.versions.fabric.kotlin.get(),
+ "rei_version" to libs.versions.rei.get()
+ )
+ replacements.forEach { (key, value) -> inputs.property(key, value) }
+ filesMatching("**/fabric.mod.json") {
+ expand(*replacements.toTypedArray())
+ }
+ exclude("**/*.license")
+ from(tasks.scanLicenses)
}
tasks.scanLicenses {
- scanConfiguration(nonModImplentation)
- scanConfiguration(configurations.modCompileClasspath.get())
- outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.json"))
- licenseFormatter.set(moe.nea.licenseextractificator.JsonLicenseFormatter())
+ scanConfiguration(nonModImplentation)
+ scanConfiguration(configurations.modCompileClasspath.get())
+ outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.json"))
+ licenseFormatter.set(moe.nea.licenseextractificator.JsonLicenseFormatter())
}
tasks.create("printAllLicenses", LicenseDiscoveryTask::class.java, licensing).apply {
- outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.txt"))
- licenseFormatter.set(moe.nea.licenseextractificator.TextLicenseFormatter())
- scanConfiguration(nonModImplentation)
- scanConfiguration(configurations.modCompileClasspath.get())
- doLast {
- println(outputFile.get().asFile.readText())
- }
- outputs.upToDateWhen { false }
+ outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.txt"))
+ licenseFormatter.set(moe.nea.licenseextractificator.TextLicenseFormatter())
+ scanConfiguration(nonModImplentation)
+ scanConfiguration(configurations.modCompileClasspath.get())
+ doLast {
+ println(outputFile.get().asFile.readText())
+ }
+ outputs.upToDateWhen { false }
}
tasks.withType<AbstractArchiveTask>().configureEach {
- isPreserveFileTimestamps = false
- isReproducibleFileOrder = true
+ isPreserveFileTimestamps = false
+ isReproducibleFileOrder = true
}
licensing.addExtraLicenseMatchers()
diff --git a/src/main/kotlin/Firmament.kt b/src/main/kotlin/Firmament.kt
index c1801f4..343ec40 100644
--- a/src/main/kotlin/Firmament.kt
+++ b/src/main/kotlin/Firmament.kt
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament
import com.mojang.brigadier.CommandDispatcher
@@ -33,7 +31,6 @@ import kotlinx.coroutines.plus
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlin.coroutines.EmptyCoroutineContext
-import net.minecraft.client.render.chunk.SectionBuilder
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.util.Identifier
import moe.nea.firmament.commands.registerFirmamentCommand
@@ -51,98 +48,98 @@ import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.data.IDataHolder
object Firmament {
- const val MOD_ID = "firmament"
+ const val MOD_ID = "firmament"
- val DEBUG = System.getProperty("firmament.debug") == "true"
- val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) }
- val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) }
- val logger: Logger = LogManager.getLogger("Firmament")
- private val metadata: ModMetadata by lazy {
- FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata
- }
- val version: Version by lazy { metadata.version }
+ val DEBUG = System.getProperty("firmament.debug") == "true"
+ val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) }
+ val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) }
+ val logger: Logger = LogManager.getLogger("Firmament")
+ private val metadata: ModMetadata by lazy {
+ FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata
+ }
+ val version: Version by lazy { metadata.version }
- val json = Json {
- prettyPrint = DEBUG
- isLenient = true
- ignoreUnknownKeys = true
- encodeDefaults = true
- }
+ val json = Json {
+ prettyPrint = DEBUG
+ isLenient = true
+ ignoreUnknownKeys = true
+ encodeDefaults = true
+ }
- val httpClient by lazy {
- HttpClient {
- install(ContentNegotiation) {
- json(json)
- }
- install(ContentEncoding) {
- gzip()
- deflate()
- }
- install(UserAgent) {
- agent = "Firmament/$version"
- }
- if (DEBUG)
- install(Logging) {
- level = LogLevel.INFO
- }
- install(HttpCache)
- }
- }
+ val httpClient by lazy {
+ HttpClient {
+ install(ContentNegotiation) {
+ json(json)
+ }
+ install(ContentEncoding) {
+ gzip()
+ deflate()
+ }
+ install(UserAgent) {
+ agent = "Firmament/$version"
+ }
+ if (DEBUG)
+ install(Logging) {
+ level = LogLevel.INFO
+ }
+ install(HttpCache)
+ }
+ }
- val globalJob = Job()
- val coroutineScope =
- CoroutineScope(EmptyCoroutineContext + CoroutineName("Firmament")) + SupervisorJob(globalJob)
+ val globalJob = Job()
+ val coroutineScope =
+ CoroutineScope(EmptyCoroutineContext + CoroutineName("Firmament")) + SupervisorJob(globalJob)
- private fun registerCommands(
- dispatcher: CommandDispatcher<FabricClientCommandSource>,
- @Suppress("UNUSED_PARAMETER")
- ctx: CommandRegistryAccess
- ) {
- registerFirmamentCommand(dispatcher)
- CommandEvent.publish(CommandEvent(dispatcher, ctx, MC.networkHandler?.commandDispatcher))
- }
+ private fun registerCommands(
+ dispatcher: CommandDispatcher<FabricClientCommandSource>,
+ @Suppress("UNUSED_PARAMETER")
+ ctx: CommandRegistryAccess
+ ) {
+ registerFirmamentCommand(dispatcher)
+ CommandEvent.publish(CommandEvent(dispatcher, ctx, MC.networkHandler?.commandDispatcher))
+ }
- @JvmStatic
- fun onInitialize() {
- }
+ @JvmStatic
+ fun onInitialize() {
+ }
- @JvmStatic
- fun onClientInitialize() {
- FeatureManager.subscribeEvents()
- var tick = 0
- ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance ->
- TickEvent.publish(TickEvent(tick++))
- })
- IDataHolder.registerEvents()
- RepoManager.initialize()
- SBData.init()
- FeatureManager.autoload()
- HypixelStaticData.spawnDataCollectionLoop()
- ClientCommandRegistrationCallback.EVENT.register(this::registerCommands)
- ClientLifecycleEvents.CLIENT_STARTED.register(ClientLifecycleEvents.ClientStarted {
- ClientStartedEvent.publish(ClientStartedEvent())
- })
- ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping {
- logger.info("Shutting down Firmament coroutines")
- globalJob.cancel()
- })
- registerFirmamentEvents()
- ItemTooltipCallback.EVENT.register { stack, context, type, lines ->
- ItemTooltipEvent.publish(ItemTooltipEvent(stack, context, type, lines))
- }
- ScreenEvents.AFTER_INIT.register(ScreenEvents.AfterInit { client, screen, scaledWidth, scaledHeight ->
- ScreenEvents.afterRender(screen)
- .register(ScreenEvents.AfterRender { screen, drawContext, mouseX, mouseY, tickDelta ->
- ScreenRenderPostEvent.publish(ScreenRenderPostEvent(screen, mouseX, mouseY, tickDelta, drawContext))
- })
- })
- }
+ @JvmStatic
+ fun onClientInitialize() {
+ FeatureManager.subscribeEvents()
+ var tick = 0
+ ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance ->
+ TickEvent.publish(TickEvent(tick++))
+ })
+ IDataHolder.registerEvents()
+ RepoManager.initialize()
+ SBData.init()
+ FeatureManager.autoload()
+ HypixelStaticData.spawnDataCollectionLoop()
+ ClientCommandRegistrationCallback.EVENT.register(this::registerCommands)
+ ClientLifecycleEvents.CLIENT_STARTED.register(ClientLifecycleEvents.ClientStarted {
+ ClientStartedEvent.publish(ClientStartedEvent())
+ })
+ ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping {
+ logger.info("Shutting down Firmament coroutines")
+ globalJob.cancel()
+ })
+ registerFirmamentEvents()
+ ItemTooltipCallback.EVENT.register { stack, context, type, lines ->
+ ItemTooltipEvent.publish(ItemTooltipEvent(stack, context, type, lines))
+ }
+ ScreenEvents.AFTER_INIT.register(ScreenEvents.AfterInit { client, screen, scaledWidth, scaledHeight ->
+ ScreenEvents.afterRender(screen)
+ .register(ScreenEvents.AfterRender { screen, drawContext, mouseX, mouseY, tickDelta ->
+ ScreenRenderPostEvent.publish(ScreenRenderPostEvent(screen, mouseX, mouseY, tickDelta, drawContext))
+ })
+ })
+ }
- fun identifier(path: String) = Identifier.of(MOD_ID, path)
- inline fun <reified T : Any> tryDecodeJsonFromStream(inputStream: InputStream): Result<T> {
- return runCatching {
- json.decodeFromStream<T>(inputStream)
- }
- }
+ fun identifier(path: String) = Identifier.of(MOD_ID, path)
+ inline fun <reified T : Any> tryDecodeJsonFromStream(inputStream: InputStream): Result<T> {
+ return runCatching {
+ json.decodeFromStream<T>(inputStream)
+ }
+ }
}
diff --git a/src/main/kotlin/events/HudRenderEvent.kt b/src/main/kotlin/events/HudRenderEvent.kt
index 555b3c8..a773a93 100644
--- a/src/main/kotlin/events/HudRenderEvent.kt
+++ b/src/main/kotlin/events/HudRenderEvent.kt
@@ -4,10 +4,14 @@ package moe.nea.firmament.events
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.render.RenderTickCounter
+import net.minecraft.world.GameMode
+import moe.nea.firmament.util.MC
/**
* Called when hud elements should be rendered, before the screen, but after the world.
*/
data class HudRenderEvent(val context: DrawContext, val tickDelta: RenderTickCounter) : FirmamentEvent() {
+ val isRenderingHud = !MC.options.hudHidden
+ val isRenderingCursor = MC.interactionManager?.currentGameMode != GameMode.SPECTATOR && isRenderingHud
companion object : FirmamentEventBus<HudRenderEvent>()
}
diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt
index 7ce14c0..529f011 100644
--- a/src/main/kotlin/features/debug/PowerUserTools.kt
+++ b/src/main/kotlin/features/debug/PowerUserTools.kt
@@ -28,6 +28,7 @@ import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.focusedItemStack
+import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.skyBlockId
@@ -44,6 +45,7 @@ object PowerUserTools : FirmamentFeature {
val copyLoreData by keyBindingWithDefaultUnbound("copy-lore")
val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture")
val copyEntityData by keyBindingWithDefaultUnbound("entity-data")
+ val copyItemStack by keyBindingWithDefaultUnbound("copy-item-stack")
}
override val config
@@ -125,7 +127,7 @@ object PowerUserTools : FirmamentFeature {
Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.modelid", model.toString()))
} else if (it.matches(TConfig.copyNbtData)) {
// TODO: copy full nbt
- val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toString() ?: "<empty>"
+ val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toPrettyString() ?: "<empty>"
ClipboardUtils.setTextContent(nbt)
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.nbt"))
} else if (it.matches(TConfig.copyLoreData)) {
@@ -157,6 +159,9 @@ object PowerUserTools : FirmamentFeature {
Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString())
)
println("Copied skull id: $skullTexture")
+ } else if (it.matches(TConfig.copyItemStack)) {
+ ClipboardUtils.setTextContent(item.encode(MC.currentOrDefaultRegistries).toPrettyString())
+ lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack"))
}
}
diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt
index 192419f..1853d65 100644
--- a/src/main/kotlin/features/mining/PickaxeAbility.kt
+++ b/src/main/kotlin/features/mining/PickaxeAbility.kt
@@ -1,4 +1,3 @@
-
package moe.nea.firmament.features.mining
import java.util.regex.Pattern
@@ -30,155 +29,146 @@ import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.parseTimePattern
import moe.nea.firmament.util.render.RenderCircleProgress
import moe.nea.firmament.util.render.lerp
+import moe.nea.firmament.util.skyblock.AbilityUtils
import moe.nea.firmament.util.toShedaniel
import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch
object PickaxeAbility : FirmamentFeature {
- override val identifier: String
- get() = "pickaxe-info"
-
-
- object TConfig : ManagedConfig(identifier) {
- val cooldownEnabled by toggle("ability-cooldown") { true }
- val cooldownScale by integer("ability-scale", 16, 64) { 16 }
- val drillFuelBar by toggle("fuel-bar") { true }
- }
-
- var lobbyJoinTime = TimeMark.farPast()
- var lastUsage = mutableMapOf<String, TimeMark>()
- var abilityOverride: String? = null
- var defaultAbilityDurations = mutableMapOf<String, Duration>(
- "Mining Speed Boost" to 120.seconds,
- "Pickobulus" to 110.seconds,
- "Gemstone Infusion" to 140.seconds,
- "Hazardous Miner" to 140.seconds,
- "Maniac Miner" to 59.seconds,
- "Vein Seeker" to 60.seconds
- )
-
- override val config: ManagedConfig
- get() = TConfig
-
- fun getCooldownPercentage(name: String, cooldown: Duration): Double {
- val sinceLastUsage = lastUsage[name]?.passedTime() ?: Duration.INFINITE
- val sinceLobbyJoin = lobbyJoinTime.passedTime()
+ override val identifier: String
+ get() = "pickaxe-info"
+
+
+ object TConfig : ManagedConfig(identifier) {
+ val cooldownEnabled by toggle("ability-cooldown") { true }
+ val cooldownScale by integer("ability-scale", 16, 64) { 16 }
+ val drillFuelBar by toggle("fuel-bar") { true }
+ }
+
+ var lobbyJoinTime = TimeMark.farPast()
+ var lastUsage = mutableMapOf<String, TimeMark>()
+ var abilityOverride: String? = null
+ var defaultAbilityDurations = mutableMapOf<String, Duration>(
+ "Mining Speed Boost" to 120.seconds,
+ "Pickobulus" to 110.seconds,
+ "Gemstone Infusion" to 140.seconds,
+ "Hazardous Miner" to 140.seconds,
+ "Maniac Miner" to 59.seconds,
+ "Vein Seeker" to 60.seconds
+ )
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+ fun getCooldownPercentage(name: String, cooldown: Duration): Double {
+ val sinceLastUsage = lastUsage[name]?.passedTime() ?: Duration.INFINITE
+ val sinceLobbyJoin = lobbyJoinTime.passedTime()
if (SBData.skyblockLocation == SkyBlockIsland.MINESHAFT) {
if (sinceLobbyJoin < sinceLastUsage) {
return 1.0
}
}
- if (sinceLastUsage < cooldown)
- return sinceLastUsage / cooldown
- return 1.0
- }
-
- @Subscribe
- fun onSlotClick(it: SlotClickEvent) {
- if (MC.screen?.title?.unformattedString == "Heart of the Mountain") {
- val name = it.stack.displayNameAccordingToNbt?.unformattedString ?: return
- val cooldown = it.stack.loreAccordingToNbt.firstNotNullOfOrNull {
- cooldownPattern.useMatch(it.unformattedString) {
- parseTimePattern(group("cooldown"))
- }
- } ?: return
- defaultAbilityDurations[name] = cooldown
- }
- }
-
- @Subscribe
- fun onDurabilityBar(it: DurabilityBarEvent) {
- if (!TConfig.drillFuelBar) return
- val lore = it.item.loreAccordingToNbt
- if (lore.lastOrNull()?.unformattedString?.contains("DRILL") != true) return
- val maxFuel = lore.firstNotNullOfOrNull {
- fuelPattern.useMatch(it.unformattedString) {
- parseShortNumber(group("maxFuel"))
- }
- } ?: return
- val extra = it.item.extraAttributes
- if (!extra.contains("drill_fuel")) return
- val fuel = extra.getInt("drill_fuel")
- val percentage = fuel / maxFuel.toFloat()
- it.barOverride = DurabilityBarEvent.DurabilityBar(
- lerp(
- DyeColor.RED.toShedaniel(),
- DyeColor.GREEN.toShedaniel(),
- percentage
- ), percentage
- )
- }
-
- @Subscribe
- fun onChatMessage(it: ProcessChatEvent) {
- abilityUsePattern.useMatch(it.unformattedString) {
- lastUsage[group("name")] = TimeMark.now()
- }
- abilitySwitchPattern.useMatch(it.unformattedString) {
- abilityOverride = group("ability")
- }
- }
-
- @Subscribe
- fun onWorldReady(event: WorldReadyEvent) {
- lobbyJoinTime = TimeMark.now()
- abilityOverride = null
- }
+ if (sinceLastUsage < cooldown)
+ return sinceLastUsage / cooldown
+ return 1.0
+ }
+
+ @Subscribe
+ fun onSlotClick(it: SlotClickEvent) {
+ if (MC.screen?.title?.unformattedString == "Heart of the Mountain") {
+ val name = it.stack.displayNameAccordingToNbt.unformattedString
+ val cooldown = it.stack.loreAccordingToNbt.firstNotNullOfOrNull {
+ cooldownPattern.useMatch(it.unformattedString) {
+ parseTimePattern(group("cooldown"))
+ }
+ } ?: return
+ defaultAbilityDurations[name] = cooldown
+ }
+ }
+
+ @Subscribe
+ fun onDurabilityBar(it: DurabilityBarEvent) {
+ if (!TConfig.drillFuelBar) return
+ val lore = it.item.loreAccordingToNbt
+ if (lore.lastOrNull()?.unformattedString?.contains("DRILL") != true) return
+ val maxFuel = lore.firstNotNullOfOrNull {
+ fuelPattern.useMatch(it.unformattedString) {
+ parseShortNumber(group("maxFuel"))
+ }
+ } ?: return
+ val extra = it.item.extraAttributes
+ if (!extra.contains("drill_fuel")) return
+ val fuel = extra.getInt("drill_fuel")
+ val percentage = fuel / maxFuel.toFloat()
+ it.barOverride = DurabilityBarEvent.DurabilityBar(
+ lerp(
+ DyeColor.RED.toShedaniel(),
+ DyeColor.GREEN.toShedaniel(),
+ percentage
+ ), percentage
+ )
+ }
+
+ @Subscribe
+ fun onChatMessage(it: ProcessChatEvent) {
+ abilityUsePattern.useMatch(it.unformattedString) {
+ lastUsage[group("name")] = TimeMark.now()
+ }
+ abilitySwitchPattern.useMatch(it.unformattedString) {
+ abilityOverride = group("ability")
+ }
+ }
+
+ @Subscribe
+ fun onWorldReady(event: WorldReadyEvent) {
+ lobbyJoinTime = TimeMark.now()
+ abilityOverride = null
+ }
@Subscribe
fun onProfileSwitch(event: ProfileSwitchEvent) {
lastUsage.clear()
}
- val abilityUsePattern = Pattern.compile("You used your (?<name>.*) Pickaxe Ability!")
- val fuelPattern = Pattern.compile("Fuel: .*/(?<maxFuel>$SHORT_NUMBER_FORMAT)")
-
- data class PickaxeAbilityData(
- val name: String,
- val cooldown: Duration,
- )
-
- fun getCooldownFromLore(itemStack: ItemStack): PickaxeAbilityData? {
- val lore = itemStack.loreAccordingToNbt
- if (!lore.any { it.unformattedString.contains("Breaking Power") })
- return null
- val cooldown = lore.firstNotNullOfOrNull {
- cooldownPattern.useMatch(it.unformattedString) {
- parseTimePattern(group("cooldown"))
- }
- } ?: return null
- val name = lore.firstNotNullOfOrNull {
- abilityPattern.useMatch(it.unformattedString) {
- group("name")
- }
- } ?: return null
- return PickaxeAbilityData(name, cooldown)
- }
-
-
- val cooldownPattern = Pattern.compile("Cooldown: (?<cooldown>$TIME_PATTERN)")
- val abilityPattern = Pattern.compile("(⦾ )?Ability: (?<name>.*) {2}RIGHT CLICK")
- val abilitySwitchPattern =
- Pattern.compile("You selected (?<ability>.*) as your Pickaxe Ability\\. This ability will apply to all of your pickaxes!")
-
-
- @Subscribe
- fun renderHud(event: HudRenderEvent) {
- if (!TConfig.cooldownEnabled) return
- var ability = getCooldownFromLore(MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return) ?: return
- defaultAbilityDurations[ability.name] = ability.cooldown
- val ao = abilityOverride
- if (ao != ability.name && ao != null) {
- ability = PickaxeAbilityData(ao, defaultAbilityDurations[ao] ?: 120.seconds)
- }
- event.context.matrices.push()
- event.context.matrices.translate(MC.window.scaledWidth / 2F, MC.window.scaledHeight / 2F, 0F)
- event.context.matrices.scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat(), 1F)
- RenderCircleProgress.renderCircle(
- event.context, Identifier.of("firmament", "textures/gui/circle.png"),
- getCooldownPercentage(ability.name, ability.cooldown).toFloat(),
- 0f, 1f, 0f, 1f
- )
- event.context.matrices.pop()
- }
+ val abilityUsePattern = Pattern.compile("You used your (?<name>.*) Pickaxe Ability!")
+ val fuelPattern = Pattern.compile("Fuel: .*/(?<maxFuel>$SHORT_NUMBER_FORMAT)")
+ val pickaxeAbilityCooldownPattern = Pattern.compile("Your pickaxe ability is on cooldown for (?<remainingCooldown>$TIME_PATTERN)\\.")
+
+ data class PickaxeAbilityData(
+ val name: String,
+ val cooldown: Duration,
+ )
+
+ fun getCooldownFromLore(itemStack: ItemStack): PickaxeAbilityData? {
+ val lore = itemStack.loreAccordingToNbt
+ if (!lore.any { it.unformattedString.contains("Breaking Power") })
+ return null
+ val ability = AbilityUtils.getAbilities(itemStack).firstOrNull() ?: return null
+ return PickaxeAbilityData(ability.name, ability.cooldown ?: return null)
+ }
+
+ val cooldownPattern = Pattern.compile("Cooldown: (?<cooldown>$TIME_PATTERN)")
+ val abilitySwitchPattern =
+ Pattern.compile("You selected (?<ability>.*) as your Pickaxe Ability\\. This ability will apply to all of your pickaxes!")
+
+ @Subscribe
+ fun renderHud(event: HudRenderEvent) {
+ if (!TConfig.cooldownEnabled) return
+ if (!event.isRenderingCursor) return
+ var ability = getCooldownFromLore(MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return) ?: return
+ defaultAbilityDurations[ability.name] = ability.cooldown
+ val ao = abilityOverride
+ if (ao != ability.name && ao != null) {
+ ability = PickaxeAbilityData(ao, defaultAbilityDurations[ao] ?: 120.seconds)
+ }
+ event.context.matrices.push()
+ event.context.matrices.translate(MC.window.scaledWidth / 2F, MC.window.scaledHeight / 2F, 0F)
+ event.context.matrices.scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat(), 1F)
+ RenderCircleProgress.renderCircle(
+ event.context, Identifier.of("firmament", "textures/gui/circle.png"),
+ getCooldownPercentage(ability.name, ability.cooldown).toFloat(),
+ 0f, 1f, 0f, 1f
+ )
+ event.context.matrices.pop()
+ }
}
diff --git a/src/main/kotlin/features/texturepack/NumberMatcher.kt b/src/main/kotlin/features/texturepack/NumberMatcher.kt
index 7e6665f..e6f2d01 100644
--- a/src/main/kotlin/features/texturepack/NumberMatcher.kt
+++ b/src/main/kotlin/features/texturepack/NumberMatcher.kt
@@ -1,4 +1,3 @@
-
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
@@ -6,120 +5,120 @@ import com.google.gson.JsonPrimitive
import moe.nea.firmament.util.useMatch
abstract class NumberMatcher {
- abstract fun test(number: Number): Boolean
-
-
- companion object {
- fun parse(jsonElement: JsonElement): NumberMatcher? {
- if (jsonElement is JsonPrimitive) {
- if (jsonElement.isString) {
- val string = jsonElement.asString
- return parseRange(string) ?: parseOperator(string)
- }
- if (jsonElement.isNumber) {
- val number = jsonElement.asNumber
- val hasDecimals = (number.toString().contains("."))
- return MatchNumberExact(if (hasDecimals) number.toLong() else number.toDouble())
- }
- }
- return null
- }
-
- private val intervalSpec =
- "(?<beginningOpen>[\\[\\(])(?<beginning>[0-9.]+)?,(?<ending>[0-9.]+)?(?<endingOpen>[\\]\\)])"
- .toPattern()
-
- fun parseRange(string: String): RangeMatcher? {
- intervalSpec.useMatch<Nothing>(string) {
- // Open in the set-theory sense, meaning does not include its end.
- val beginningOpen = group("beginningOpen") == "("
- val endingOpen = group("endingOpen") == ")"
- val beginning = group("beginning")?.toDouble()
- val ending = group("ending")?.toDouble()
- return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen)
- }
- return null
- }
-
- enum class Operator(val operator: String) {
- LESS("<") {
- override fun matches(comparisonResult: Int): Boolean {
- return comparisonResult < 0
- }
- },
- LESS_EQUALS("<=") {
- override fun matches(comparisonResult: Int): Boolean {
- return comparisonResult <= 0
- }
- },
- GREATER(">") {
- override fun matches(comparisonResult: Int): Boolean {
- return comparisonResult > 0
- }
- },
- GREATER_EQUALS(">=") {
- override fun matches(comparisonResult: Int): Boolean {
- return comparisonResult >= 0
- }
- },
- ;
-
- abstract fun matches(comparisonResult: Int): Boolean
- }
-
- private val operatorPattern = "(?<operator>${Operator.entries.joinToString("|") {it.operator}})(?<value>[0-9.]+)".toPattern()
-
- fun parseOperator(string: String): OperatorMatcher? {
- operatorPattern.useMatch<Nothing>(string) {
- val operatorName = group("operator")
- val operator = Operator.entries.find { it.operator == operatorName }!!
- val value = group("value").toDouble()
- return OperatorMatcher(operator, value)
- }
- return null
- }
-
- data class OperatorMatcher(val operator: Operator, val value: Double) : NumberMatcher() {
- override fun test(number: Number): Boolean {
- return operator.matches(number.toDouble().compareTo(value))
- }
- }
-
-
- data class MatchNumberExact(val number: Number) : NumberMatcher() {
- override fun test(number: Number): Boolean {
- return when (this.number) {
- is Double -> number.toDouble() == this.number.toDouble()
- else -> number.toLong() == this.number.toLong()
- }
- }
- }
-
- data class RangeMatcher(
- val beginning: Double?,
- val beginningInclusive: Boolean,
- val ending: Double?,
- val endingInclusive: Boolean,
- ) : NumberMatcher() {
- override fun test(number: Number): Boolean {
- val value = number.toDouble()
- if (beginning != null) {
- if (beginningInclusive) {
- if (value < beginning) return false
- } else {
- if (value <= beginning) return false
- }
- }
- if (ending != null) {
- if (endingInclusive) {
- if (value > ending) return false
- } else {
- if (value >= ending) return false
- }
- }
- return true
- }
- }
- }
+ abstract fun test(number: Number): Boolean
+
+
+ companion object {
+ fun parse(jsonElement: JsonElement): NumberMatcher? {
+ if (jsonElement is JsonPrimitive) {
+ if (jsonElement.isString) {
+ val string = jsonElement.asString
+ return parseRange(string) ?: parseOperator(string)
+ }
+ if (jsonElement.isNumber) {
+ val number = jsonElement.asNumber
+ val hasDecimals = (number.toString().contains("."))
+ return MatchNumberExact(if (hasDecimals) number.toLong() else number.toDouble())
+ }
+ }
+ return null
+ }
+
+ private val intervalSpec =
+ "(?<beginningOpen>[\\[\\(])(?<beginning>[0-9.]+)?,(?<ending>[0-9.]+)?(?<endingOpen>[\\]\\)])"
+ .toPattern()
+
+ fun parseRange(string: String): RangeMatcher? {
+ intervalSpec.useMatch<Nothing>(string) {
+ // Open in the set-theory sense, meaning does not include its end.
+ val beginningOpen = group("beginningOpen") == "("
+ val endingOpen = group("endingOpen") == ")"
+ val beginning = group("beginning")?.toDouble()
+ val ending = group("ending")?.toDouble()
+ return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen)
+ }
+ return null
+ }
+
+ enum class Operator(val operator: String) {
+ LESS("<") {
+ override fun matches(comparisonResult: Int): Boolean {
+ return comparisonResult < 0
+ }
+ },
+ LESS_EQUALS("<=") {
+ override fun matches(comparisonResult: Int): Boolean {
+ return comparisonResult <= 0
+ }
+ },
+ GREATER(">") {
+ override fun matches(comparisonResult: Int): Boolean {
+ return comparisonResult > 0
+ }
+ },
+ GREATER_EQUALS(">=") {
+ override fun matches(comparisonResult: Int): Boolean {
+ return comparisonResult >= 0
+ }
+ },
+ ;
+
+ abstract fun matches(comparisonResult: Int): Boolean
+ }
+
+ private val operatorPattern =
+ "(?<operator>${Operator.entries.joinToString("|") { it.operator }})(?<value>[0-9.]+)".toPattern()
+
+ fun parseOperator(string: String): OperatorMatcher? {
+ return operatorPattern.useMatch(string) {
+ val operatorName = group("operator")
+ val operator = Operator.entries.find { it.operator == operatorName }!!
+ val value = group("value").toDouble()
+ OperatorMatcher(operator, value)
+ }
+ }
+
+ data class OperatorMatcher(val operator: Operator, val value: Double) : NumberMatcher() {
+ override fun test(number: Number): Boolean {
+ return operator.matches(number.toDouble().compareTo(value))
+ }
+ }
+
+
+ data class MatchNumberExact(val number: Number) : NumberMatcher() {
+ override fun test(number: Number): Boolean {
+ return when (this.number) {
+ is Double -> number.toDouble() == this.number.toDouble()
+ else -> number.toLong() == this.number.toLong()
+ }
+ }
+ }
+
+ data class RangeMatcher(
+ val beginning: Double?,
+ val beginningInclusive: Boolean,
+ val ending: Double?,
+ val endingInclusive: Boolean,
+ ) : NumberMatcher() {
+ override fun test(number: Number): Boolean {
+ val value = number.toDouble()
+ if (beginning != null) {
+ if (beginningInclusive) {
+ if (value < beginning) return false
+ } else {
+ if (value <= beginning) return false
+ }
+ }
+ if (ending != null) {
+ if (endingInclusive) {
+ if (value > ending) return false
+ } else {
+ if (value >= ending) return false
+ }
+ }
+ return true
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/util/ErrorUtil.kt b/src/main/kotlin/util/ErrorUtil.kt
new file mode 100644
index 0000000..4f229af
--- /dev/null
+++ b/src/main/kotlin/util/ErrorUtil.kt
@@ -0,0 +1,16 @@
+package moe.nea.firmament.util
+
+import moe.nea.firmament.Firmament
+
+object ErrorUtil {
+ var aggressiveErrors = run {
+ Thread.currentThread().stackTrace.any { it.className.startsWith("org.junit.") } || Firmament.DEBUG
+ }
+
+ @Suppress("NOTHING_TO_INLINE") // Suppressed since i want the logger to not pick up the ErrorUtil stack-frame
+ inline fun softError(message: String) {
+ if (aggressiveErrors) error(message)
+ else Firmament.logger.error(message)
+ }
+
+}
diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt
index db8eccb..09aa7aa 100644
--- a/src/main/kotlin/util/MC.kt
+++ b/src/main/kotlin/util/MC.kt
@@ -4,7 +4,9 @@ import io.github.moulberry.repo.data.Coordinate
import java.util.concurrent.ConcurrentLinkedQueue
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.screen.ingame.HandledScreen
+import net.minecraft.client.option.GameOptions
import net.minecraft.client.render.WorldRenderer
+import net.minecraft.item.Item
import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket
import net.minecraft.registry.BuiltinRegistries
import net.minecraft.registry.RegistryKeys
@@ -16,79 +18,82 @@ import moe.nea.firmament.events.TickEvent
object MC {
- private val messageQueue = ConcurrentLinkedQueue<Text>()
+ private val messageQueue = ConcurrentLinkedQueue<Text>()
- init {
- TickEvent.subscribe("MC:push") {
- while (true) {
- inGameHud.chatHud.addMessage(messageQueue.poll() ?: break)
- }
- while (true) {
- (nextTickTodos.poll() ?: break).invoke()
- }
- }
- }
+ init {
+ TickEvent.subscribe("MC:push") {
+ while (true) {
+ inGameHud.chatHud.addMessage(messageQueue.poll() ?: break)
+ }
+ while (true) {
+ (nextTickTodos.poll() ?: break).invoke()
+ }
+ }
+ }
- fun sendChat(text: Text) {
- if (instance.isOnThread)
- inGameHud.chatHud.addMessage(text)
- else
- messageQueue.add(text)
- }
+ fun sendChat(text: Text) {
+ if (instance.isOnThread)
+ inGameHud.chatHud.addMessage(text)
+ else
+ messageQueue.add(text)
+ }
- fun sendServerCommand(command: String) {
- val nh = player?.networkHandler ?: return
- nh.sendPacket(
- CommandExecutionC2SPacket(
- command,
- )
- )
- }
+ fun sendServerCommand(command: String) {
+ val nh = player?.networkHandler ?: return
+ nh.sendPacket(
+ CommandExecutionC2SPacket(
+ command,
+ )
+ )
+ }
- fun sendServerChat(text: String) {
- player?.networkHandler?.sendChatMessage(text)
- }
+ fun sendServerChat(text: String) {
+ player?.networkHandler?.sendChatMessage(text)
+ }
- fun sendCommand(command: String) {
- player?.networkHandler?.sendCommand(command)
- }
+ fun sendCommand(command: String) {
+ player?.networkHandler?.sendCommand(command)
+ }
- fun onMainThread(block: () -> Unit) {
- if (instance.isOnThread)
- block()
- else
- instance.send(block)
- }
+ fun onMainThread(block: () -> Unit) {
+ if (instance.isOnThread)
+ block()
+ else
+ instance.send(block)
+ }
- private val nextTickTodos = ConcurrentLinkedQueue<() -> Unit>()
- fun nextTick(function: () -> Unit) {
- nextTickTodos.add(function)
- }
+ private val nextTickTodos = ConcurrentLinkedQueue<() -> Unit>()
+ fun nextTick(function: () -> Unit) {
+ nextTickTodos.add(function)
+ }
- inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl)
- inline val worldRenderer: WorldRenderer get() = instance.worldRenderer
- inline val networkHandler get() = player?.networkHandler
- inline val instance get() = MinecraftClient.getInstance()
- inline val keyboard get() = instance.keyboard
- inline val textureManager get() = instance.textureManager
- inline val inGameHud get() = instance.inGameHud
- inline val font get() = instance.textRenderer
- inline val soundManager get() = instance.soundManager
- inline val player get() = instance.player
- inline val camera get() = instance.cameraEntity
- inline val guiAtlasManager get() = instance.guiAtlasManager
- inline val world get() = instance.world
- inline var screen
- get() = instance.currentScreen
- set(value) = instance.setScreen(value)
- inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*>
- inline val window get() = instance.window
- inline val currentRegistries: RegistryWrapper.WrapperLookup? get() = world?.registryManager
- val defaultRegistries: RegistryWrapper.WrapperLookup = BuiltinRegistries.createWrapperLookup()
- val defaultItems = defaultRegistries.getWrapperOrThrow(RegistryKeys.ITEM)
+ inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl)
+ inline val worldRenderer: WorldRenderer get() = instance.worldRenderer
+ inline val networkHandler get() = player?.networkHandler
+ inline val instance get() = MinecraftClient.getInstance()
+ inline val keyboard get() = instance.keyboard
+ inline val interactionManager get() = instance.interactionManager
+ inline val textureManager get() = instance.textureManager
+ inline val options get() = instance.options
+ inline val inGameHud get() = instance.inGameHud
+ inline val font get() = instance.textRenderer
+ inline val soundManager get() = instance.soundManager
+ inline val player get() = instance.player
+ inline val camera get() = instance.cameraEntity
+ inline val guiAtlasManager get() = instance.guiAtlasManager
+ inline val world get() = instance.world
+ inline var screen
+ get() = instance.currentScreen
+ set(value) = instance.setScreen(value)
+ inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*>
+ inline val window get() = instance.window
+ inline val currentRegistries: RegistryWrapper.WrapperLookup? get() = world?.registryManager
+ val defaultRegistries: RegistryWrapper.WrapperLookup = BuiltinRegistries.createWrapperLookup()
+ inline val currentOrDefaultRegistries get() = currentRegistries ?: defaultRegistries
+ val defaultItems: RegistryWrapper.Impl<Item> = defaultRegistries.getWrapperOrThrow(RegistryKeys.ITEM)
}
val Coordinate.blockPos: BlockPos
- get() = BlockPos(x, y, z)
+ get() = BlockPos(x, y, z)
diff --git a/src/main/kotlin/util/TimeMark.kt b/src/main/kotlin/util/TimeMark.kt
index 1264212..4a076ac 100644
--- a/src/main/kotlin/util/TimeMark.kt
+++ b/src/main/kotlin/util/TimeMark.kt
@@ -1,44 +1,52 @@
-
-
package moe.nea.firmament.util
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
class TimeMark private constructor(private val timeMark: Long) : Comparable<TimeMark> {
- fun passedTime() = if (timeMark == 0L) Duration.INFINITE else (System.currentTimeMillis() - timeMark).milliseconds
-
- operator fun minus(other: TimeMark): Duration {
- if (other.timeMark == timeMark)
- return 0.milliseconds
- if (other.timeMark == 0L)
- return Duration.INFINITE
- if (timeMark == 0L)
- return -Duration.INFINITE
- return (timeMark - other.timeMark).milliseconds
- }
-
- companion object {
- fun now() = TimeMark(System.currentTimeMillis())
- fun farPast() = TimeMark(0L)
- fun ago(timeDelta: Duration): TimeMark {
- if (timeDelta.isFinite()) {
- return TimeMark(System.currentTimeMillis() - timeDelta.inWholeMilliseconds)
- }
- require(timeDelta.isPositive())
- return farPast()
- }
- }
-
- override fun hashCode(): Int {
- return timeMark.hashCode()
- }
-
- override fun equals(other: Any?): Boolean {
- return other is TimeMark && other.timeMark == timeMark
- }
-
- override fun compareTo(other: TimeMark): Int {
- return this.timeMark.compareTo(other.timeMark)
- }
+ fun passedTime() =
+ if (timeMark == 0L) Duration.INFINITE
+ else (System.currentTimeMillis() - timeMark).milliseconds
+
+ fun passedAt(fakeNow: TimeMark) =
+ if (timeMark == 0L) Duration.INFINITE
+ else (fakeNow.timeMark - timeMark).milliseconds
+
+ operator fun minus(other: TimeMark): Duration {
+ if (other.timeMark == timeMark)
+ return 0.milliseconds
+ if (other.timeMark == 0L)
+ return Duration.INFINITE
+ if (timeMark == 0L)
+ return -Duration.INFINITE
+ return (timeMark - other.timeMark).milliseconds
+ }
+
+ companion object {
+ fun now() = TimeMark(System.currentTimeMillis())
+ fun farPast() = TimeMark(0L)
+ fun ago(timeDelta: Duration): TimeMark {
+ if (timeDelta.isFinite()) {
+ return TimeMark(System.currentTimeMillis() - timeDelta.inWholeMilliseconds)
+ }
+ require(timeDelta.isPositive())
+ return farPast()
+ }
+ }
+
+ override fun hashCode(): Int {
+ return timeMark.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is TimeMark && other.timeMark == timeMark
+ }
+
+ override fun toString(): String {
+ return "https://time.is/$timeMark"
+ }
+
+ override fun compareTo(other: TimeMark): Int {
+ return this.timeMark.compareTo(other.timeMark)
+ }
}
diff --git a/src/main/kotlin/util/mc/SNbtFormatter.kt b/src/main/kotlin/util/mc/SNbtFormatter.kt
new file mode 100644
index 0000000..e773927
--- /dev/null
+++ b/src/main/kotlin/util/mc/SNbtFormatter.kt
@@ -0,0 +1,138 @@
+package moe.nea.firmament.util.mc
+
+import net.minecraft.nbt.NbtByte
+import net.minecraft.nbt.NbtByteArray
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtDouble
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtEnd
+import net.minecraft.nbt.NbtFloat
+import net.minecraft.nbt.NbtInt
+import net.minecraft.nbt.NbtIntArray
+import net.minecraft.nbt.NbtList
+import net.minecraft.nbt.NbtLong
+import net.minecraft.nbt.NbtLongArray
+import net.minecraft.nbt.NbtShort
+import net.minecraft.nbt.NbtString
+import net.minecraft.nbt.visitor.NbtElementVisitor
+
+class SNbtFormatter private constructor() : NbtElementVisitor {
+ private val result = StringBuilder()
+ private var indent = 0
+ private fun writeIndent() {
+ result.append("\t".repeat(indent))
+ }
+
+ private fun pushIndent() {
+ indent++
+ }
+
+ private fun popIndent() {
+ indent--
+ }
+
+ fun apply(element: NbtElement): StringBuilder {
+ element.accept(this)
+ return result
+ }
+
+
+ override fun visitString(element: NbtString) {
+ result.append(NbtString.escape(element.asString()))
+ }
+
+ override fun visitByte(element: NbtByte) {
+ result.append(element.numberValue()).append("b")
+ }
+
+ override fun visitShort(element: NbtShort) {
+ result.append(element.shortValue()).append("s")
+ }
+
+ override fun visitInt(element: NbtInt) {
+ result.append(element.intValue())
+ }
+
+ override fun visitLong(element: NbtLong) {
+ result.append(element.longValue()).append("L")
+ }
+
+ override fun visitFloat(element: NbtFloat) {
+ result.append(element.floatValue()).append("f")
+ }
+
+ override fun visitDouble(element: NbtDouble) {
+ result.append(element.doubleValue()).append("d")
+ }
+
+ private fun visitArrayContents(array: List<NbtElement>) {
+ array.forEachIndexed { index, element ->
+ writeIndent()
+ element.accept(this)
+ if (array.size != index + 1) {
+ result.append(",")
+ }
+ result.append("\n")
+ }
+ }
+
+ private fun writeArray(arrayTypeTag: String, array: List<NbtElement>) {
+ result.append("[").append(arrayTypeTag).append("\n")
+ pushIndent()
+ visitArrayContents(array)
+ popIndent()
+ writeIndent()
+ result.append("]")
+
+ }
+
+ override fun visitByteArray(element: NbtByteArray) {
+ writeArray("B;", element)
+ }
+
+ override fun visitIntArray(element: NbtIntArray) {
+ writeArray("I;", element)
+ }
+
+ override fun visitLongArray(element: NbtLongArray) {
+ writeArray("L;", element)
+ }
+
+ override fun visitList(element: NbtList) {
+ writeArray("", element)
+ }
+
+ override fun visitCompound(compound: NbtCompound) {
+ result.append("{\n")
+ pushIndent()
+ val keys = compound.keys.sorted()
+ keys.forEachIndexed { index, key ->
+ writeIndent()
+ val element = compound[key] ?: error("Key '$key' found but not present in compound: $compound")
+ val escapedName = if (key.matches(SIMPLE_NAME)) key else NbtString.escape(key)
+ result.append(escapedName).append(": ")
+ element.accept(this)
+ if (keys.size != index + 1) {
+ result.append(",")
+ }
+ result.append("\n")
+ }
+ popIndent()
+ writeIndent()
+ result.append("}")
+ }
+
+ override fun visitEnd(element: NbtEnd) {
+ result.append("END")
+ }
+
+ companion object {
+ fun prettify(nbt: NbtElement): String {
+ return SNbtFormatter().apply(nbt).toString()
+ }
+
+ fun NbtElement.toPrettyString() = prettify(this)
+
+ private val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex()
+ }
+}
diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt
index 78c90e8..a44435c 100644
--- a/src/main/kotlin/util/regex.kt
+++ b/src/main/kotlin/util/regex.kt
@@ -1,8 +1,14 @@
+@file:OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class)
+
package moe.nea.firmament.util
import java.util.regex.Matcher
import java.util.regex.Pattern
import org.intellij.lang.annotations.Language
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+import kotlin.experimental.ExperimentalTypeInference
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
@@ -10,10 +16,14 @@ import kotlin.time.Duration.Companion.seconds
inline fun <T> String.ifMatches(regex: Regex, block: (MatchResult) -> T): T? =
regex.matchEntire(this)?.let(block)
-inline fun <T> Pattern.useMatch(string: String, block: Matcher.() -> T): T? =
- matcher(string)
+inline fun <T> Pattern.useMatch(string: String, block: Matcher.() -> T): T? {
+ contract {
+ callsInPlace(block, InvocationKind.AT_MOST_ONCE)
+ }
+ return matcher(string)
.takeIf(Matcher::matches)
?.let(block)
+}
@Language("RegExp")
val TIME_PATTERN = "[0-9]+[ms]"
diff --git a/src/main/kotlin/util/skyblock/AbilityUtils.kt b/src/main/kotlin/util/skyblock/AbilityUtils.kt
new file mode 100644
index 0000000..0f0adbe
--- /dev/null
+++ b/src/main/kotlin/util/skyblock/AbilityUtils.kt
@@ -0,0 +1,138 @@
+package moe.nea.firmament.util.skyblock
+
+import kotlin.time.Duration
+import net.minecraft.item.ItemStack
+import net.minecraft.text.Text
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.directLiteralStringContent
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.parseShortNumber
+import moe.nea.firmament.util.parseTimePattern
+import moe.nea.firmament.util.unformattedString
+import moe.nea.firmament.util.useMatch
+
+object AbilityUtils {
+ data class ItemAbility(
+ val name: String,
+ val hasPowerScroll: Boolean,
+ val activation: AbilityActivation,
+ val manaCost: Int?,
+ val descriptionLines: List<Text>,
+ val cooldown: Duration?,
+ )
+
+ @JvmInline
+ value class AbilityActivation(
+ val label: String
+ ) {
+ companion object {
+ val RIGHT_CLICK = AbilityActivation("RIGHT CLICK")
+ val SNEAK_RIGHT_CLICK = AbilityActivation("SNEAK RIGHT CLICK")
+ val SNEAK = AbilityActivation("SNEAK")
+ val EMPTY = AbilityActivation("")
+ fun of(text: String?): AbilityActivation {
+ val trimmed = text?.trim()
+ if (trimmed.isNullOrBlank())
+ return EMPTY
+ return AbilityActivation(trimmed)
+ }
+ }
+ }
+
+ private val abilityNameRegex = "Ability: (?<name>.*?) *".toPattern()
+ private fun findAbility(iterator: ListIterator<Text>): ItemAbility? {
+ if (!iterator.hasNext()) {
+ return null
+ }
+ val line = iterator.next()
+ // The actual information about abilities is stored in the siblings
+ if (line.directLiteralStringContent != "") return null
+ var powerScroll: Boolean = false // This should instead determine the power scroll based on text colour
+ var abilityName: String? = null
+ var activation: String? = null
+ var hasProcessedActivation = false
+ for (sibling in line.siblings) {
+ val directContent = sibling.directLiteralStringContent ?: continue
+ if (directContent == "⦾ ") {
+ powerScroll = true
+ continue
+ }
+ if (!hasProcessedActivation && abilityName != null) {
+ hasProcessedActivation = true
+ activation = directContent
+ continue
+ }
+ abilityNameRegex.useMatch<Nothing>(directContent) {
+ abilityName = group("name")
+ continue
+ }
+ if (abilityName != null) {
+ ErrorUtil.softError("Found abilityName $abilityName without finding but encountered unprocessable element in: $line")
+ }
+ return null
+ }
+ if (abilityName == null) return null
+ val descriptionLines = mutableListOf<Text>()
+ var manaCost: Int? = null
+ var cooldown: Duration? = null
+ while (iterator.hasNext()) {
+ val descriptionLine = iterator.next()
+ if (descriptionLine.unformattedString == "") break
+ var nextIsManaCost = false
+ var isSpecialLine = false
+ var nextIsDuration = false
+ for (sibling in descriptionLine.siblings) {
+ val directContent = sibling.directLiteralStringContent ?: continue
+ if ("Mana Cost: " == directContent) { // TODO: 'Soulflow Cost: ' support (or maybe a generic 'XXX Cost: ')
+ nextIsManaCost = true
+ isSpecialLine = true
+ continue
+ }
+ if ("Cooldown: " == directContent) {
+ nextIsDuration = true
+ isSpecialLine = true
+ continue
+ }
+ if (nextIsDuration) {
+ nextIsDuration = false
+ cooldown = parseTimePattern(directContent)
+ continue
+ }
+ if (nextIsManaCost) {
+ nextIsManaCost = false
+ manaCost = parseShortNumber(directContent).toInt()
+ continue
+ }
+ if (isSpecialLine) {
+ ErrorUtil.softError("Unknown special line segment: '$sibling' in '$descriptionLine'")
+ }
+ }
+ if (!isSpecialLine) {
+ descriptionLines.add(descriptionLine)
+ }
+ }
+ return ItemAbility(
+ abilityName,
+ powerScroll,
+ AbilityActivation.of(activation),
+ manaCost,
+ descriptionLines,
+ cooldown
+ )
+ }
+
+ fun getAbilities(lore: List<Text>): List<ItemAbility> {
+ val iterator = lore.listIterator()
+ val abilities = mutableListOf<ItemAbility>()
+ while (iterator.hasNext()) {
+ findAbility(iterator)?.let(abilities::add)
+ }
+
+ return abilities
+ }
+
+ fun getAbilities(itemStack: ItemStack): List<ItemAbility> {
+ return getAbilities(itemStack.loreAccordingToNbt)
+ }
+
+}
diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt
index 36924a6..1cef5d4 100644
--- a/src/main/kotlin/util/textutil.kt
+++ b/src/main/kotlin/util/textutil.kt
@@ -90,6 +90,8 @@ fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String {
val Text.unformattedString: String
get() = string.removeColorCodes()
+val Text.directLiteralStringContent: String? get() = (this.content as? PlainTextContent)?.string()
+
fun Text.allSiblings(): List<Text> = listOf(this) + siblings.flatMap { it.allSiblings() }
fun MutableText.withColor(formatting: Formatting) = this.styled { it.withColor(formatting).withItalic(false) }
diff --git a/src/main/resources/assets/firmament/lang/en_us.json b/src/main/resources/assets/firmament/lang/en_us.json
index d3ae936..131eae2 100644
--- a/src/main/resources/assets/firmament/lang/en_us.json
+++ b/src/main/resources/assets/firmament/lang/en_us.json
@@ -182,8 +182,9 @@
"firmament.config.power-user.copy-texture-pack-id": "Copy Texture Pack Id",
"firmament.config.power-user.copy-skull-texture": "Copy Placed Skull Id",
"firmament.config.power-user.entity-data": "Show Entity Data",
- "firmament.config.power-user.copy-nbt-data": "Copy NBT data",
+ "firmament.config.power-user.copy-nbt-data": "Copy ExtraAttributes data",
"firmament.config.power-user.copy-lore": "Copy Name + Lore",
+ "firmament.config.power-user.copy-item-stack": "Copy ItemStack",
"firmament.config.power-user": "Power Users",
"firmament.tooltip.skyblockid": "SkyBlock Id: %s",
"firmament.tooltip.copied.skyblockid.fail": "Failed to copy SkyBlock Id",
@@ -194,6 +195,7 @@
"firmament.tooltip.copied.skull.fail": "Failed to copy skull id.",
"firmament.tooltip.copied.nbt": "Copied NBT data",
"firmament.tooltip.copied.lore": "Copied Name and Lore",
+ "firmament.tooltip.copied.stack": "Copied ItemStack",
"firmament.config.compatibility": "Intermod Features",
"firmament.config.compatibility.explosion-enabled": "Redirect Enhanced Explosions",
"firmament.config.compatibility.explosion-power": "Enhanced Explosion Power",
diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener
index 49d4383..60b31e3 100644
--- a/src/main/resources/firmament.accesswidener
+++ b/src/main/resources/firmament.accesswidener
@@ -22,3 +22,8 @@ mutable field net/minecraft/screen/slot/Slot y I
accessible field net/minecraft/entity/player/PlayerEntity PLAYER_MODEL_PARTS Lnet/minecraft/entity/data/TrackedData;
accessible field net/minecraft/client/render/WorldRenderer chunks Lnet/minecraft/client/render/BuiltChunkStorage;
+# Fix package-private access methods
+accessible method net/minecraft/registry/entry/RegistryEntry$Reference setRegistryKey (Lnet/minecraft/registry/RegistryKey;)V
+accessible method net/minecraft/entity/LivingEntity getHitbox ()Lnet/minecraft/util/math/Box;
+accessible method net/minecraft/registry/entry/RegistryEntryList$Named <init> (Lnet/minecraft/registry/entry/RegistryEntryOwner;Lnet/minecraft/registry/tag/TagKey;)V
+accessible method net/minecraft/registry/entry/RegistryEntry$Reference setValue (Ljava/lang/Object;)V
diff --git a/src/test/kotlin/root.kt b/src/test/kotlin/root.kt
new file mode 100644
index 0000000..679ecb4
--- /dev/null
+++ b/src/test/kotlin/root.kt
@@ -0,0 +1,29 @@
+package moe.nea.firmament.test
+
+import net.minecraft.Bootstrap
+import net.minecraft.SharedConstants
+import moe.nea.firmament.util.TimeMark
+
+ object FirmTestBootstrap {
+ val loadStart = TimeMark.now()
+
+ init {
+ println("Bootstrap started at $loadStart")
+ }
+
+ init {
+ SharedConstants.createGameVersion()
+ Bootstrap.initialize()
+ }
+
+ val loadEnd = TimeMark.now()
+
+ val loadDuration = loadStart.passedAt(loadEnd)
+
+ init {
+ println("Bootstrap completed at $loadEnd after $loadDuration")
+ }
+
+ fun bootstrapMinecraft() {
+ }
+}
diff --git a/src/test/kotlin/testutil/ItemResources.kt b/src/test/kotlin/testutil/ItemResources.kt
new file mode 100644
index 0000000..bd3c438
--- /dev/null
+++ b/src/test/kotlin/testutil/ItemResources.kt
@@ -0,0 +1,30 @@
+package moe.nea.firmament.test.testutil
+
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtOps
+import net.minecraft.nbt.StringNbtReader
+import moe.nea.firmament.test.FirmTestBootstrap
+
+object ItemResources {
+ init {
+ FirmTestBootstrap.bootstrapMinecraft()
+ }
+
+ fun loadString(path: String): String {
+ require(!path.startsWith("/"))
+ return ItemResources::class.java.classLoader
+ .getResourceAsStream(path)!!
+ .readAllBytes().decodeToString()
+ }
+
+ fun loadSNbt(path: String): NbtCompound {
+ return StringNbtReader.parse(loadString(path))
+ }
+
+ fun loadItem(name: String): ItemStack {
+ // TODO: make the load work with enchantments
+ return ItemStack.CODEC.parse(NbtOps.INSTANCE, loadSNbt("testdata/items/$name.snbt"))
+ .getOrThrow { IllegalStateException("Could not load test item '$name': $it") }
+ }
+}
diff --git a/src/test/kotlin/moe/nea/firmament/test/ColorCode.kt b/src/test/kotlin/util/ColorCodeTest.kt
index 5889bc7..d9de36a 100644
--- a/src/test/kotlin/moe/nea/firmament/test/ColorCode.kt
+++ b/src/test/kotlin/util/ColorCodeTest.kt
@@ -1,12 +1,13 @@
-
-package moe.nea.firmament.test
+package moe.nea.firmament.test.util
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
+import net.minecraft.Bootstrap
+import net.minecraft.SharedConstants
import moe.nea.firmament.util.removeColorCodes
-class ColorCode {
+class ColorCodeTest {
@Test
fun testWhatever() {
Assertions.assertEquals("", "".removeColorCodes().toString())
diff --git a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt
new file mode 100644
index 0000000..abe739d
--- /dev/null
+++ b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt
@@ -0,0 +1,79 @@
+package moe.nea.firmament.test.util.skyblock
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.text.Text
+import moe.nea.firmament.test.testutil.ItemResources
+import moe.nea.firmament.util.skyblock.AbilityUtils
+import moe.nea.firmament.util.unformattedString
+
+class AbilityUtilsTest {
+
+ fun List<AbilityUtils.ItemAbility>.stripDescriptions() = map {
+ it.copy(descriptionLines = it.descriptionLines.map { Text.literal(it.unformattedString) })
+ }
+
+ @Test
+ fun testUnpoweredDrill() {
+ Assertions.assertEquals(
+ listOf(
+ AbilityUtils.ItemAbility(
+ "Pickobulus",
+ false,
+ AbilityUtils.AbilityActivation.RIGHT_CLICK,
+ null,
+ listOf("Throw your pickaxe to create an",
+ "explosion mining all ores in a 3 block",
+ "radius.").map(Text::literal),
+ 48.seconds
+ )
+ ),
+ AbilityUtils.getAbilities(ItemResources.loadItem("titanium-drill")).stripDescriptions()
+ )
+ }
+
+ @Test
+ fun testPoweredPickaxe() {
+ Assertions.assertEquals(
+ listOf(
+ AbilityUtils.ItemAbility(
+ "Mining Speed Boost",
+ true,
+ AbilityUtils.AbilityActivation.RIGHT_CLICK,
+ null,
+ listOf("Grants +200% ⸕ Mining Speed for",
+ "10s.").map(Text::literal),
+ 2.minutes
+ )
+ ),
+ AbilityUtils.getAbilities(ItemResources.loadItem("diamond-pickaxe")).stripDescriptions()
+ )
+ }
+
+ @Test
+ fun testAOTV() {
+ Assertions.assertEquals(
+ listOf(
+ AbilityUtils.ItemAbility(
+ "Instant Transmission", true, AbilityUtils.AbilityActivation.RIGHT_CLICK, 23,
+ listOf("Teleport 12 blocks ahead of you and",
+ "gain +50 ✦ Speed for 3 seconds.").map(Text::literal),
+ null
+ ),
+ AbilityUtils.ItemAbility(
+ "Ether Transmission",
+ false,
+ AbilityUtils.AbilityActivation.SNEAK_RIGHT_CLICK,
+ 90,
+ listOf("Teleport to your targeted block up",
+ "to 61 blocks away.",
+ "Soulflow Cost: 1").map(Text::literal),
+ null
+ )
+ ),
+ AbilityUtils.getAbilities(ItemResources.loadItem("aspect-of-the-void")).stripDescriptions()
+ )
+ }
+}
diff --git a/src/test/resources/testdata/items/aspect-of-the-void.snbt b/src/test/resources/testdata/items/aspect-of-the-void.snbt
new file mode 100644
index 0000000..180c069
--- /dev/null
+++ b/src/test/resources/testdata/items/aspect-of-the-void.snbt
@@ -0,0 +1,59 @@
+{
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ donated_museum: 1b,
+ enchantments: {
+ ultimate_wise: 5
+ },
+ ethermerge: 1b,
+ gems: {
+ },
+ id: "ASPECT_OF_THE_VOID",
+ modifier: "heroic",
+ originTag: "ASPECT_OF_THE_VOID",
+ power_ability_scroll: "SAPPHIRE_POWER_SCROLL",
+ timestamp: 1641640380000L,
+ tuned_transmission: 4,
+ uuid: "b0572534-eb14-46cd-90c6-0df878fd56a2"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Heroic Aspect of the Void"}],"italic":false,"text":""}',
+ "minecraft:enchantment_glint_override": 1b,
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+120"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+132 "},{"color":"blue","text":"(+32)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Bonus Attack Speed: "},{"color":"red","text":"+3% "},{"color":"blue","text":"(+3%)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+80 "},{"color":"blue","text":"(+80)"}],"italic":false,"text":""}',
+ '{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"gray","text":"✎"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":""},{"bold":true,"color":"light_purple","text":"Ultimate Wise V"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Reduces the ability mana cost of this"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"item by "},{"color":"green","text":"50%"},{"color":"gray","text":"."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"aqua","text":"⦾ "},{"color":"gold","text":"Ability: Instant Transmission "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Teleport "},{"color":"green","text":"12 blocks"},{"color":"gray","text":" ahead of you and"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"gain "},{"color":"green","text":"+50 "},{"color":"white","text":"✦ Speed"},{"color":"gray","text":" for "},{"color":"green","text":"3 seconds"},{"color":"gray","text":"."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"23"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gold","text":"Ability: Ether Transmission "},{"bold":true,"color":"yellow","text":"SNEAK RIGHT CLICK"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Teleport to your targeted block up"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"to "},{"color":"green","text":"61 blocks "},{"color":"gray","text":"away."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"Soulflow Cost: "},{"color":"dark_aqua","text":"1"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"90"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC SWORD"}],"italic":false,"text":""}'
+ ],
+ "minecraft:unbreakable": {
+ show_in_tooltip: 0b
+ }
+ },
+ count: 1,
+ id: "minecraft:diamond_shovel"
+}
diff --git a/src/test/resources/testdata/items/diamond-pickaxe.snbt b/src/test/resources/testdata/items/diamond-pickaxe.snbt
new file mode 100644
index 0000000..cce12f9
--- /dev/null
+++ b/src/test/resources/testdata/items/diamond-pickaxe.snbt
@@ -0,0 +1,48 @@
+{
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ enchantments: {
+ efficiency: 10
+ },
+ id: "DIAMOND_PICKAXE",
+ power_ability_scroll: "SAPPHIRE_POWER_SCROLL",
+ timestamp: 1659795180000L,
+ uuid: "d213f48e-d927-4748-a58c-eb80735025b7"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"green","text":"Diamond Pickaxe"}],"italic":false,"text":""}',
+ "minecraft:enchantments": {
+ levels: {
+ }
+ },
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"dark_gray","text":"Breaking Power 4"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+30"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Mining Speed: "},{"color":"green","text":"+220"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Efficiency X"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Increases how quickly your tool"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"breaks blocks."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"aqua","text":"⦾ "},{"color":"gold","text":"Ability: Mining Speed Boost "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+200% "},{"color":"gold","text":"⸕ Mining Speed "},{"color":"gray","text":"for"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"green","text":"10s"},{"color":"gray","text":"."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"dark_gray","text":"Cooldown: "},{"color":"green","text":"120s"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"green","text":"UNCOMMON PICKAXE"}],"italic":false,"text":""}'
+ ],
+ "minecraft:unbreakable": {
+ show_in_tooltip: 0b
+ }
+ },
+ count: 1,
+ id: "minecraft:diamond_pickaxe"
+}
diff --git a/src/test/resources/testdata/items/titanium-drill.snbt b/src/test/resources/testdata/items/titanium-drill.snbt
new file mode 100644
index 0000000..e3b6819
--- /dev/null
+++ b/src/test/resources/testdata/items/titanium-drill.snbt
@@ -0,0 +1,97 @@
+{
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ compact_blocks: 1023815,
+ donated_museum: 1b,
+ drill_fuel: 16621,
+ drill_part_fuel_tank: "titanium_fuel_tank",
+ drill_part_upgrade_module: "goblin_omelette_blue_cheese",
+ enchantments: {
+ compact: 10,
+ efficiency: 5,
+ experience: 3,
+ fortune: 3,
+ paleontologist: 2,
+ pristine: 5
+ },
+ gems: {
+ AMBER_0: {
+ quality: "PERFECT",
+ uuid: "d28be6ae-75eb-49e4-90d8-31759db18d79"
+ },
+ JADE_0: {
+ quality: "PERFECT",
+ uuid: "657fea0b-88e2-483d-9d2c-0b821797a55a"
+ },
+ MINING_0: {
+ quality: "PERFECT",
+ uuid: "257bdcd2-585b-48b9-9517-a2e841dc0574"
+ },
+ MINING_0_gem: "TOPAZ",
+ unlocked_slots: [
+ "JADE_0",
+ "MINING_0"
+ ]
+ },
+ id: "TITANIUM_DRILL_4",
+ modifier: "auspicious",
+ rarity_upgrades: 1,
+ timestamp: 1700577120000L,
+ uuid: "367b85ab-5bb4-43b6-a055-084cbaaafc1c"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Auspicious Titanium Drill DR-X655"}],"italic":false,"text":""}',
+ "minecraft:enchantment_glint_override": 1b,
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"dark_gray","text":"Breaking Power 9"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+75"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Mining Speed: "},{"color":"green","text":"+1,885 "},{"color":"blue","text":"(+75) "},{"color":"light_purple","text":"(+100)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Pristine: "},{"color":"green","text":"+4.5 "},{"color":"light_purple","text":"(+2)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Mining Fortune: "},{"color":"green","text":"+220 "},{"color":"blue","text":"(+20) "},{"color":"light_purple","text":"(+50)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Mining Wisdom: "},{"color":"green","text":"+10"}],"italic":false,"text":""}',
+ '{"extra":[" ",{"color":"gold","text":"["},{"color":"gold","text":"⸕"},{"color":"gold","text":"] "},{"color":"gold","text":"["},{"color":"green","text":"☘"},{"color":"gold","text":"] "},{"color":"gold","text":"["},{"color":"yellow","text":"✦"},{"color":"gold","text":"]"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Compact X"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Efficiency V"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Experience III"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Fortune III"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Paleontologist II"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Prismatic V"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"green","text":"Titanium-Infused Fuel Tank."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"gray","text":""},{"color":"dark_green","text":"25,000 Max Fuel Capacity."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"gray","text":""},{"color":"green","text":"-4% Pickaxe Ability Cooldown."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Drill Engine: "},{"color":"red","text":"Not Installed"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases "},{"color":"gold","text":"⸕ Mining Speed "},{"color":"gray","text":"with part"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"installed."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"green","text":"Blue Cheese Goblin Omelette Part."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Adds "},{"color":"green","text":"+1 Level "},{"color":"gray","text":"to all of your unlocked "},{"color":"dark_purple","text":"Heart of"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"dark_purple","text":"the Mountain "},{"color":"gray","text":"perks."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Fuel: "},{"color":"dark_green","text":"16,621"},{"color":"dark_gray","text":"/25k"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gold","text":"Ability: Pickobulus "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Throw your pickaxe to create an"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"explosion mining all ores in a "},{"color":"green","text":"3 "},{"color":"gray","text":"block"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"radius."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"dark_gray","text":"Cooldown: "},{"color":"green","text":"48s"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Auspicious Bonus"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+0.9% "},{"color":"gold","text":"☘ Mining Fortune"},{"color":"gray","text":"."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"bold":true,"color":"light_purple","text":"MYTHIC DRILL "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}'
+ ]
+ },
+ count: 1,
+ id: "minecraft:prismarine_shard"
+}
diff --git a/symbols/src/main/kotlin/process/GameTestContainingClassProcessor.kt b/symbols/src/main/kotlin/process/GameTestContainingClassProcessor.kt
new file mode 100644
index 0000000..4fcf91f
--- /dev/null
+++ b/symbols/src/main/kotlin/process/GameTestContainingClassProcessor.kt
@@ -0,0 +1,80 @@
+package moe.nea.firmament.annotations.process
+
+import com.google.auto.service.AutoService
+import com.google.devtools.ksp.containingFile
+import com.google.devtools.ksp.processing.CodeGenerator
+import com.google.devtools.ksp.processing.Dependencies
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import com.google.devtools.ksp.symbol.KSAnnotated
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSFile
+import com.google.gson.Gson
+import com.google.gson.JsonArray
+import com.google.gson.JsonObject
+import java.io.OutputStreamWriter
+import java.nio.charset.StandardCharsets
+import java.util.TreeSet
+
+class GameTestContainingClassProcessor(
+ val logger: KSPLogger,
+ val codeGenerator: CodeGenerator,
+ val sourceSetName: String,
+) : SymbolProcessor {
+
+
+ @AutoService(SymbolProcessorProvider::class)
+ class Provider : SymbolProcessorProvider {
+ override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
+ return GameTestContainingClassProcessor(
+ environment.logger,
+ environment.codeGenerator,
+ environment.options["firmament.sourceset"] ?: "main")
+ }
+ }
+
+ val allClasses: MutableSet<String> = TreeSet()
+ val allSources = mutableSetOf<KSFile>()
+
+ override fun process(resolver: Resolver): List<KSAnnotated> {
+ val annotated = resolver.getSymbolsWithAnnotation("net.minecraft.test.GameTest").toList()
+ annotated.forEach {
+ val containingClass = it.parent as KSClassDeclaration
+ allClasses.add(containingClass.qualifiedName!!.asString())
+ allSources.add(it.containingFile!!)
+ }
+ return emptyList()
+ }
+
+ fun createJson(): JsonObject {
+ return JsonObject().apply {
+ addProperty("schemaVersion", 1)
+ addProperty("id", "firmament-gametest")
+ addProperty("name", "Firmament Gametest")
+ addProperty("version", "1.0.0")
+ addProperty("environment", "*")
+ add("entrypoints", JsonObject().apply {
+ add("fabric-gametest", JsonArray().apply {
+ allClasses.forEach {
+ add(it)
+ }
+ })
+ })
+ }
+ }
+
+ override fun finish() {
+ if (allClasses.isEmpty()) return
+ val stream = codeGenerator.createNewFile(Dependencies(aggregating = true, *allSources.toTypedArray()),
+ "",
+ "fabric.mod",
+ "json")
+ val output = OutputStreamWriter(stream, StandardCharsets.UTF_8)
+ Gson().toJson(createJson(), output)
+ output.close()
+ }
+
+}