diff options
226 files changed, 6387 insertions, 2531 deletions
diff --git a/.editorconfig b/.editorconfig index 109d855..1c59d9d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,3 +17,4 @@ max_line_length = 120 ij_kotlin_name_count_to_use_star_import = 99999 ij_kotlin_name_count_to_use_star_import_for_members = 99999 ij_kotlin_imports_layout = *, |, kotlinx.**, kotlin.**, net.minecraft.**, moe.nea.firmament.**, |, $* +ij_kotlin_packages_to_use_import_on_demand = false diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..dfc9c09 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe> +# +# SPDX-License-Identifier: CC0-1.0 + +name: Mod Bug Report +description: Report a bug or crash in Firmament +labels: + - bug +body: + - type: input + id: version + attributes: + label: Firmament Version + description: Which version of Firmament are you using. Please provide the full version name (found in the file name, or the mod list). Please **don't** say latest. + validations: + required: true + - type: textarea + id: description + attributes: + label: Bug Description + description: Please describe the bug you encountered and what you did before you encountered it. + validations: + required: true + - type: textarea + attributes: + label: Minecraft Log + description: Please provide the complete Minecraft Log. Please do not truncate your log, or extract only parts. Earlier parts of the log can contain important information. + render: plain text + placeholder: If you are worried about your log containing private information, you can use an external uploader like https://mclo.gs/ to strip all personal information, and paste the link in here instead. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a3a11ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe> +# +# SPDX-License-Identifier: CC0-1.0 + +blank_issues_enabled: true +contact_links: + - name: Discord Server + url: https://discord.gg/64pFP94AWA + about: For non bug support, check out the Discord. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..29fa766 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe> +# +# SPDX-License-Identifier: CC0-1.0 + +name: Feature Request +description: Request a new feature or for a feature to be expanded. +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Thank you for expressing your interest in a new Firmament feature. + Please keep in mind that not every feature is a good fit for Firmament, + *especially* if there is already another mod for Firmaments versions + which covers that feature very well. It doesn't hurt to ask, but please + understand that even features I want to implement, might take some time + to be fully realized. + + Before creating a feature request, please check if the feature complies + with [HyPixel rules](https://hypixel.net/skyblock-rules). + - type: textarea + attributes: + label: Describe the feature + description: Please describe the feature you are looking for. + validations: + required: true + - type: input + attributes: + label: Other mods + description: Do you know of other mods that have this feature already? If so, please list them here. @@ -4,24 +4,31 @@ SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe> SPDX-License-Identifier: CC0-1.0 --> + + +<div align="center"> + # Firmament -<small><i>Powered by NEU</i></small> + + +<hr> [](https://hypixel.net/threads/firmament-a-skyblock-mod-for-1-20-1.5446366/) [](https://discord.gg/64pFP94AWA) [](https://modrinth.com/mod/firmament) -[](https://github.com/romangraef/firmament/releases) +[](https://github.com/nea89o/firmament/releases) + +</div> + ## Currently working features - Item List of all SkyBlock Items -- Grouping Items that belong together like minions - Recipe Viewer for Crafting Recipes - Recipe Viewer for Forge Recipes - ... as well as many more custom recipe types. - NPC waypoints -- Image Preview in chat - A storage overview as well as a full storage overlay - A crafting overlay when clicking the "Move Item" plus in a crafting recipe - Cursor position saver @@ -38,11 +45,15 @@ SPDX-License-Identifier: CC0-1.0 Firmament needs the following libraries to work: +- [Fabric API](https://modrinth.com/mod/fabric-api) +- [Fabric Language Kotlin](https://modrinth.com/mod/fabric-language-kotlin) + +As well as (for the item list): +- - [RoughlyEnoughItems](https://modrinth.com/mod/rei) - [Architectury](https://modrinth.com/mod/architectury-api) - [Cloth Config](https://modrinth.com/mod/cloth-config) -- [Fabric API](https://modrinth.com/mod/fabric-api) -- [Fabric Language Kotlin](https://modrinth.com/mod/fabric-language-kotlin) + You can download Firmament itself on [Modrinth](https://modrinth.com/mod/firmament) or on [GitHub](https://github.com/romangraef/firmament/releases). diff --git a/build.gradle.kts b/build.gradle.kts index 40eb5d8..3a72ed0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,20 +7,17 @@ */ import com.google.common.hash.Hashing -import com.google.devtools.ksp.gradle.KspTaskJvm +import com.google.devtools.ksp.gradle.KspAATask import com.google.gson.Gson import com.google.gson.JsonObject import moe.nea.licenseextractificator.LicenseDiscoveryTask import moe.nea.mcautotranslations.gradle.CollectTranslations import net.fabricmc.loom.LoomGradleExtension import org.apache.tools.ant.taskdefs.condition.Os -import org.gradle.platform.OperatingSystem import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.plugin.SubpluginOption -import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin.Companion.kotlinNodeJsEnvSpec import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.nio.charset.StandardCharsets -import java.util.Base64 +import java.util.* plugins { java @@ -31,10 +28,10 @@ plugins { alias(libs.plugins.kotlin.plugin.ksp) // alias(libs.plugins.loom) // TODO: use arch loom once they update to 1.8 - id("fabric-loom") version "1.9.2" + id("fabric-loom") version "1.10.1" alias(libs.plugins.shadow) id("moe.nea.licenseextractificator") - id("moe.nea.mc-auto-translations") version "0.1.0" + alias(libs.plugins.mcAutoTranslations) } version = getGitTagInfo(libs.versions.minecraft.get()) @@ -129,6 +126,7 @@ fun innerJarsOf(name: String, dependency: Dependency): Provider<FileTree> { val collectTranslations by tasks.registering(CollectTranslations::class) { this.baseTranslations.from(file("translations/en_us.json")) + this.baseTranslations.from(file("translations/extra.json")) this.classes.from(sourceSets.main.get().kotlin.classesDirectory) } @@ -141,8 +139,10 @@ fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabl val mainSS = sourceSets.main.get() val upperName = ss.name.capitalizeN() afterEvaluate { - tasks.named("ksp${upperName}Kotlin", KspTaskJvm::class) { - this.options.add(SubpluginOption("apoption", "firmament.sourceset=${ss.name}")) + tasks.named("ksp${upperName}Kotlin", KspAATask::class) { + this.commandLineArgumentProviders.add { // TODO: update https://github.com/google/ksp/issues/2075 + listOf("firmament.sourceset=${ss.name}") + } } tasks.named("compile${upperName}Kotlin", KotlinCompile::class) { this.enabled = isEnabled @@ -165,15 +165,18 @@ fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabl extendsFrom(getByName(mainSS.annotationProcessorConfigurationName)) } (mainSS.runtimeOnlyConfigurationName) { - extendsFrom(getByName(ss.runtimeClasspathConfigurationName)) + if (isEnabled) + extendsFrom(getByName(ss.runtimeClasspathConfigurationName)) } ("ksp$upperName") { extendsFrom(ksp.get()) } } dependencies { - runtimeOnly(ss.output) - (ss.implementationConfigurationName)(sourceSets.main.get().output) + if (isEnabled) + runtimeOnly(ss.output) + (ss.implementationConfigurationName)(project.files(tasks.compileKotlin.map { it.destinationDirectory })) + (ss.implementationConfigurationName)(project.files(tasks.compileJava.map { it.destinationDirectory })) } tasks.shadowJar { from(ss.output) @@ -183,8 +186,7 @@ fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabl classpath.from(configurations.getByName(ss.compileClasspathConfigurationName)) } collectTranslations { - // TODO: this does not work, somehow - this.classes.from(sourceSets.main.get().kotlin.classesDirectory) + this.classes.from(ss.kotlin.classesDirectory) } return ss } @@ -194,6 +196,11 @@ val SourceSet.modImplementationConfigurationName loom.remapConfigurations.find { it.targetConfigurationName.get() == this.implementationConfigurationName }!!.sourceConfiguration +val SourceSet.modRuntimeOnlyConfigurationName + get() = + loom.remapConfigurations.find { + it.targetConfigurationName.get() == this.runtimeOnlyConfigurationName + }!!.sourceConfiguration val shadowMe by configurations.creating { exclude(group = "org.jetbrains.kotlin") @@ -221,14 +228,17 @@ val testAgent by configurations.creating { } -val configuredSourceSet = createIsolatedSourceSet("configured", - isEnabled = false) // Wait for update (also low prio, because configured sucks) +val configuredSourceSet = createIsolatedSourceSet( + "configured", + isEnabled = false +) // Wait for update (also low prio, because configured sucks) val sodiumSourceSet = createIsolatedSourceSet("sodium", isEnabled = false) val citResewnSourceSet = createIsolatedSourceSet("citresewn", isEnabled = false) // TODO: Wait for update val yaclSourceSet = createIsolatedSourceSet("yacl") val explosiveEnhancementSourceSet = createIsolatedSourceSet("explosiveEnhancement", isEnabled = false) // TODO: wait for their port val wildfireGenderSourceSet = createIsolatedSourceSet("wildfireGender") +val jadeSourceSet = createIsolatedSourceSet("jade") val modmenuSourceSet = createIsolatedSourceSet("modmenu") val reiSourceSet = createIsolatedSourceSet("rei") val moulconfigSourceSet = createIsolatedSourceSet("moulconfig") @@ -253,16 +263,16 @@ dependencies { (explosiveEnhancementSourceSet.modImplementationConfigurationName)(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") + compileOnly(projects.javaplugin) + annotationProcessor(projects.javaplugin) + nonModImplentation("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) + nonModImplentation(libs.mixinextras) include(libs.mixinextras) nonModImplentation(libs.nealisp) @@ -270,7 +280,6 @@ dependencies { modCompileOnly(libs.fabric.api) modRuntimeOnly(libs.fabric.api.deprecated) - modApi(libs.architectury) modCompileOnly(libs.jarvis.api) include(libs.jarvis.fabric) @@ -278,6 +287,7 @@ dependencies { (wildfireGenderSourceSet.implementationConfigurationName)(customTexturesSourceSet.output) (configuredSourceSet.modImplementationConfigurationName)(libs.configured) (sodiumSourceSet.modImplementationConfigurationName)(libs.sodium) + (jadeSourceSet.modImplementationConfigurationName)(libs.jade) (citResewnSourceSet.modImplementationConfigurationName)( innerJarsOf("citresewn", dependencies.create(libs.citresewn.get())) @@ -286,10 +296,8 @@ dependencies { (yaclSourceSet.modImplementationConfigurationName)(libs.yacl) // Actual dependencies - (reiSourceSet.modImplementationConfigurationName)(libs.rei.api) { - exclude(module = "architectury") - exclude(module = "architectury-fabric") - } + (reiSourceSet.modImplementationConfigurationName)(libs.rei.api) + (reiSourceSet.modRuntimeOnlyConfigurationName)(libs.rei.fabric) nonModImplentation(libs.repoparser) shadowMe(libs.repoparser) fun ktor(mod: String) = "io.ktor:ktor-$mod-jvm:${libs.versions.ktor.get()}" @@ -312,11 +320,11 @@ dependencies { } - testImplementation("io.kotest:kotest-runner-junit5:6.0.0.M1") - testAgent(project(":testagent", configuration = "shadow")) + testImplementation("net.fabricmc:fabric-loader-junit:${libs.versions.fabric.loader.get()}") + testAgent(files(tasks.getByPath(":testagent:jar"))) - implementation(project(":symbols")) - ksp(project(":symbols")) + implementation(projects.symbols) + ksp(projects.symbols) } loom { @@ -327,11 +335,13 @@ loom { configureEach { property("fabric.log.level", "info") property("firmament.debug", "true") - property("firmament.classroots", - compatSourceSets.joinToString(File.pathSeparator) { - File(it.output.classesDirs.asPath).absolutePath - }) + property( + "firmament.classroots", + compatSourceSets.joinToString(File.pathSeparator) { + File(it.output.classesDirs.asPath).absolutePath + }) property("mixin.debug.export", "true") + property("mixin.debug", "true") parseEnvFile(file(".env")).forEach { (t, u) -> environmentVariable(t, u) @@ -364,12 +374,16 @@ val updateTestRepo by tasks.registering { doLast { val propertiesFile = rootProject.file("gradle.properties") val json = - Gson().fromJson(uri("https://api.github.com/repos/NotEnoughUpdates/NotEnoughUpdates-REPO/branches/master") - .toURL().readText(), JsonObject::class.java) + Gson().fromJson( + uri("https://api.github.com/repos/NotEnoughUpdates/NotEnoughUpdates-REPO/branches/master") + .toURL().readText(), JsonObject::class.java + ) val latestSha = json["commit"].asJsonObject["sha"].asString var text = propertiesFile.readText() - text = text.replace("firmament\\.compiletimerepohash=[^\n]*".toRegex(), - "firmament.compiletimerepohash=$latestSha") + text = text.replace( + "firmament\\.compiletimerepohash=[^\n]*".toRegex(), + "firmament.compiletimerepohash=$latestSha" + ) propertiesFile.writeText(text) } } @@ -383,8 +397,10 @@ tasks.test { doFirst { wd.mkdirs() wd.resolve("config").deleteRecursively() - systemProperty("firmament.testrepo", - downloadTestRepo.flatMap { it.outputDirectory.asFile }.map { it.absolutePath }.get()) + systemProperty( + "firmament.testrepo", + downloadTestRepo.flatMap { it.outputDirectory.asFile }.map { it.absolutePath }.get() + ) jvmArgs("-javaagent:${testAgent.singleFile.absolutePath}") } systemProperty("jdk.attach.allowAttachSelf", "true") @@ -402,13 +418,15 @@ tasks.withType<JavaCompile> { 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.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") @@ -440,6 +458,7 @@ tasks.processResources { "version" to project.version.toString(), "minecraft_version" to libs.versions.minecraft.get(), "fabric_kotlin_version" to libs.versions.fabric.kotlin.get(), + "fabric_api_version" to libs.versions.fabric.api.get(), "rei_version" to libs.versions.rei.get() ) replacements.forEach { (key, value) -> inputs.property(key, value) } @@ -456,12 +475,18 @@ tasks.processResources { tasks.scanLicenses { scanConfiguration(nonModImplentation) scanConfiguration(configurations.modCompileClasspath.get()) + compatSourceSets.forEach { + scanConfiguration(it.modImplementationConfigurationName.get()) + } outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.json")) 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")) licenseFormatter.set(moe.nea.licenseextractificator.TextLicenseFormatter()) + compatSourceSets.forEach { + scanConfiguration(it.modImplementationConfigurationName.get()) + } scanConfiguration(nonModImplentation) scanConfiguration(configurations.modCompileClasspath.get()) doLast { @@ -498,16 +523,20 @@ fun patchRenderDoc( if (!fileF.exists()) { fileF.parentFile.mkdirs() if (isWindows) { - fileF.writeText(""" + fileF.writeText( + """ setlocal enableextensions start "" renderdoccmd.exe capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" %* endlocal - """.trimIndent()) + """.trimIndent() + ) } else { - fileF.writeText(""" + fileF.writeText( + """ #!/usr/bin/env bash exec renderdoccmd capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" "$@" - """.trimIndent()) + """.trimIndent() + ) fileF.setExecutable(true) } } @@ -518,21 +547,6 @@ fun patchRenderDoc( tasks.runClient { javaLauncher.set(javaToolchains.launcherFor(java.toolchain).map { patchRenderDoc(it) }) } -//tasks.register<Exec>("runRenderDoc") { -// val runClient = tasks.runClient.get() -// commandLine( -// "renderdoc", -// "capture", -// "--opt-hook-children", -// "--wait-for-exit", -// "--working-dir", -// runClient.workingDir, -// runClient.javaLauncher.get().executablePath.asFile.absoluteFile, -// ) -// args(runClient.allJvmArgs) -// args() -// -//} tasks.withType<AbstractArchiveTask>().configureEach { isPreserveFileTimestamps = false diff --git a/docs/firmament_logo.webp b/docs/firmament_logo.webp Binary files differnew file mode 100644 index 0000000..d70327a --- /dev/null +++ b/docs/firmament_logo.webp diff --git a/docs/firmament_logo.webp.license b/docs/firmament_logo.webp.license new file mode 100644 index 0000000..8b77b1b --- /dev/null +++ b/docs/firmament_logo.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 ic22487 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/firmament_logo_256.webp b/docs/firmament_logo_256.webp Binary files differnew file mode 100644 index 0000000..2aba841 --- /dev/null +++ b/docs/firmament_logo_256.webp diff --git a/docs/firmament_logo_256.webp.license b/docs/firmament_logo_256.webp.license new file mode 100644 index 0000000..8b77b1b --- /dev/null +++ b/docs/firmament_logo_256.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 ic22487 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/firmament_logo_256_nobg.webp b/docs/firmament_logo_256_nobg.webp Binary files differnew file mode 100644 index 0000000..c557fca --- /dev/null +++ b/docs/firmament_logo_256_nobg.webp diff --git a/docs/firmament_logo_256_nobg.webp.license b/docs/firmament_logo_256_nobg.webp.license new file mode 100644 index 0000000..8b77b1b --- /dev/null +++ b/docs/firmament_logo_256_nobg.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 ic22487 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/firmament_logo_nobg.webp b/docs/firmament_logo_nobg.webp Binary files differnew file mode 100644 index 0000000..9b76f3c --- /dev/null +++ b/docs/firmament_logo_nobg.webp diff --git a/docs/firmament_logo_nobg.webp.license b/docs/firmament_logo_nobg.webp.license new file mode 100644 index 0000000..8b77b1b --- /dev/null +++ b/docs/firmament_logo_nobg.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 ic22487 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/release_script.sh b/docs/release_script.sh index 43663b4..8d87a09 100755 --- a/docs/release_script.sh +++ b/docs/release_script.sh @@ -4,6 +4,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # ARG_OPTIONAL_BOOLEAN([no-check],[n],[Skip checking preconditions, such as a clean git working directory]) +# ARG_OPTIONAL_BOOLEAN([no-test],[t],[Skip running gradle tests.]) +# ARG_OPTIONAL_BOOLEAN([dry],[d],[Dry run]) # ARG_HELP([Script to help creating releases]) # ARGBASH_GO() # needed because of Argbash --> m4_ignore([ @@ -23,20 +25,24 @@ die() begins_with_short_option() { - local first_option all_short_options='nh' + local first_option all_short_options='ntdh' first_option="${1:0:1}" test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0 } # THE DEFAULTS INITIALIZATION - OPTIONALS _arg_no_check="off" +_arg_no_test="off" +_arg_dry="off" print_help() { printf '%s\n' "Script to help creating releases" - printf 'Usage: %s [-n|--(no-)no-check] [-h|--help]\n' "$0" + printf 'Usage: %s [-n|--(no-)no-check] [-t|--(no-)no-test] [-d|--(no-)dry] [-h|--help]\n' "$0" printf '\t%s\n' "-n, --no-check, --no-no-check: Skip checking preconditions, such as a clean git working directory (off by default)" + printf '\t%s\n' "-t, --no-test, --no-no-test: Skip running gradle tests. (off by default)" + printf '\t%s\n' "-d, --dry, --no-dry: Dry run (off by default)" printf '\t%s\n' "-h, --help: Prints help" } @@ -59,6 +65,30 @@ parse_commandline() { begins_with_short_option "$_next" && shift && set -- "-n" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option." fi ;; + -t|--no-no-test|--no-test) + _arg_no_test="on" + test "${1:0:5}" = "--no-" && _arg_no_test="off" + ;; + -t*) + _arg_no_test="on" + _next="${_key##-t}" + if test -n "$_next" -a "$_next" != "$_key" + then + { begins_with_short_option "$_next" && shift && set -- "-t" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option." + fi + ;; + -d|--no-dry|--dry) + _arg_dry="on" + test "${1:0:5}" = "--no-" && _arg_dry="off" + ;; + -d*) + _arg_dry="on" + _next="${_key##-d}" + if test -n "$_next" -a "$_next" != "$_key" + then + { begins_with_short_option "$_next" && shift && set -- "-d" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option." + fi + ;; -h|--help) print_help exit 0 @@ -129,19 +159,28 @@ fi echo "Confirming new version as $newversion" -echo Committing release commit -git commit --allow-empty -m 'Prepare release '"$newversion"' +if [ "$_arg_dry" == off ]; then + echo Committing release commit + git commit --allow-empty -m 'Prepare release '"$newversion"' [no changelog]' -echo Tagging release commit -git tag "$newversion" + echo Tagging release commit + git tag "$newversion" +fi mkdir -p "$basedir/.gradle" releasenotes="$basedir/.gradle/releasenotes.md" +comparetag="$( +if [ "$_arg_dry" == off ]; then + echo "$newversion" +else + echo "HEAD" +fi)" + echo Building release notes -echo "**Full Changelog**: <https://github.com/nea89o/Firmament/compare/$oldversion...$newversion>" > "$releasenotes" +echo "**Full Changelog**: <https://github.com/nea89o/Firmament/compare/$oldversion...$comparetag>" > "$releasenotes" echo >> "$releasenotes" -git log --pretty='- %s' --grep '[no changelog]' --invert-grep --fixed-strings "$oldversion..$newversion" | tac >> "$releasenotes" +git log --pretty='- %s' --grep '[no changelog]' --invert-grep --fixed-strings "$oldversion..$comparetag" | tac >> "$releasenotes" echo >> "$releasenotes" echo Check Release notes: @@ -153,22 +192,29 @@ read echo Building JAR "$basedir"/gradlew --stop -"$basedir"/gradlew clean build +if [ "$_arg_no_test" == off ]; then + echo Building and testing + "$basedir"/gradlew clean build +else + echo Building without testing + "$basedir"/gradlew clean assemble +fi echo Release notes: echo ---------------------------------------------- cat "$releasenotes" echo ---------------------------------------------- -echo Pushing to github -git push "$REMOTE" "HEAD" "$newversion" - -if command -v gh; then - echo Creating github release - (set -x; gh release create -t "Firmament $newversion" "$newversion" -F "$releasenotes" "$basedir/build/libs/Firmament-$newversion.jar") -else - echo Could not find github command utility. Opening github releases - xdg-open "https://github.com/nea89o/firmament/releases/new" +if [ "$_arg_dry" == off ]; then + echo Pushing to github + git push "$REMOTE" "HEAD" "$newversion" + if command -v gh; then + echo Creating github release + (set -x; gh release create -t "Firmament $newversion" "$newversion" -F "$releasenotes" "$basedir/build/libs/Firmament-$newversion.jar") + else + echo Could not find github command utility. Opening github releases + xdg-open "https://github.com/nea89o/firmament/releases/new" + fi fi echo Opening modrinth releases diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c9c1c1..0750b11 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,23 +3,24 @@ # SPDX-License-Identifier: CC0-1.0 [versions] -minecraft = "1.21.4" +minecraft = "1.21.5" # Update from https://kotlinlang.org/ -kotlin = "2.1.0" +kotlin = "2.1.20" # Update from https://github.com/google/ksp/releases -kotlin_ksp = "2.1.0-1.0.29" +kotlin_ksp = "2.1.20-2.0.0" # Update from https://linkie.shedaniel.me/dependencies?loader=fabric -fabric_loader = "0.16.9" -fabric_api = "0.114.0+1.21.4" -yarn = "1.21.4+build.4" -modmenu = "13.0.0-beta.1" -architectury = "15.0.1" -rei = "18.0.796" +fabric_loader = "0.16.13" +fabric_api = "0.119.9+1.21.5" +yarn = "1.21.5+build.1" +modmenu = "14.0.0-rc.2" +architectury = "16.0.3" +# Update from https://maven.architectury.dev/me/shedaniel/RoughlyEnoughItems-fabric/ (but is typically late) +rei = "19.0.805" # Update from https://maven.fabricmc.net/net/fabricmc/fabric-language-kotlin/ -fabric_kotlin = "1.13.0+kotlin.2.1.0" +fabric_kotlin = "1.13.2+kotlin.2.1.20" # Update from https://maven.architectury.dev/dev/architectury/loom/dev.architectury.loom.gradle.plugin/ loom = "1.7.414" # TODO: port back to architectury (and) 1.9.424 @@ -28,33 +29,36 @@ loom = "1.7.414" # TODO: port back to architectury (and) 1.9.424 qolify = "1.6.0-1.21.1" # Update from https://modrinth.com/mod/sodium/versions?l=fabric -sodium = "mc1.21.4-0.6.6-fabric" +sodium = "mc1.21.5-0.6.13-fabric" # Update from https://modrinth.com/mod/freecam/versions?l=fabric -freecammod = "1.3.2+mc1.21.4" +freecammod = "1.3.3+mc1.21.5" # Update from https://modrinth.com/mod/no-chat-reports/versions?l=fabric -ncr = "Fabric-1.21.4-v2.11.0" +ncr = "Fabric-1.21.5-v2.12.0" # Update from https://modrinth.com/mod/female-gender/versions?l=fabric -femalegender = "4.3.3+1.21.4" +femalegender = "4.3.4+1.21.5" # Update from https://modrinth.com/mod/explosive-enhancement/versions?l=fabric explosiveenhancement = "1.2.3-1.21.0" # Update from https://modrinth.com/mod/not-enough-animations/versions?l=fabric -notenoughanimations = "eZykTicT" +notenoughanimations = "prj4BdjU" # Update from https://modrinth.com/mod/cit-resewn/versions?l=fabric citresewn = "1.2.0+1.21" +# Update from https://modrinth.com/mod/jade/versions?l=fabric +jade = "18.1.0+fabric" + devauth = "1.2.1" -# Update from https://ktor.io/ -ktor = "3.0.3" +# Update from https://ktor.io/docs/ +ktor = "3.1.2" # Update from https://repo.nea.moe/#/releases/moe/nea/neurepoparser -neurepoparser = "1.6.0" +neurepoparser = "1.7.0" # Update from https://github.com/HotswapProjects/HotswapAgent/releases # TODO: bump to 2.0.1 @@ -67,12 +71,15 @@ jarvis = "1.1.4" nealisp = "1.1.0" # Update from https://github.com/NotEnoughUpdates/MoulConfig/tags -moulconfig = "3.3.0" +moulconfig = "3.8.0" + +# Update from https://repo.nea.moe/#/releases/moe/nea/mc-auto-translations/moe.nea.mc-auto-translations.gradle.plugin +mcAutoTranslations = "0.3.0" # Update from https://www.curseforge.com/minecraft/mc-mods/configured/files/all?page=1&pageSize=20 configured = "6023970" -# Update from https://modrinth.com/mod/hypixel-mod-api/versions +# Update from https://modrinth.com/mod/hypixel-mod-api/versions?l=fabric hypixelmodapi = "1.0.1" hypixelmodapi_fabric = "1.0.1+build.1+mc1.21" @@ -81,16 +88,16 @@ manninghamMills = "2.4.1" # Update from https://docs.isxander.dev/yet-another-config-lib/installing-yacl # Nvm, they just don't update docs: https://modrinth.com/mod/yacl/versions?l=fabric -yacl = "3.6.2+1.21.4-fabric" +yacl = "3.6.6+1.21.5-fabric" -# Update from https://maven.shedaniel.me/me/shedaniel/cloth/basic-math/0.6.1/ +# Update from https://maven.shedaniel.me/me/shedaniel/cloth/basic-math/ basicMath = "0.6.1" # Update from https://mvnrepository.com/artifact/net.lenni0451.classtransform/core -classtransform = "1.14.0" +classtransform = "1.14.1" # Update from https://mvnrepository.com/artifact/org.ow2.asm/asm/ -asm = "9.7.1" +asm = "9.8" [libraries] minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } @@ -100,7 +107,7 @@ fabric_api_deprecated = { module = "net.fabricmc.fabric-api:fabric-api-deprecate fabric_kotlin = { module = "net.fabricmc:fabric-language-kotlin", version.ref = "fabric_kotlin" } architectury = { module = "dev.architectury:architectury", version.ref = "architectury" } rei_api = { module = "me.shedaniel:RoughlyEnoughItems-api", version.ref = "rei" } -moulconfig = { module = "org.notenoughupdates.moulconfig:modern", version.ref = "moulconfig" } +moulconfig = { module = "org.notenoughupdates.moulconfig:modern-1.21.5", version.ref = "moulconfig" } repoparser = { module = "moe.nea:neurepoparser", version.ref = "neurepoparser" } mixinextras = { module = "io.github.llamalad7:mixinextras-fabric", version.ref = "mixinextras" } jarvis_api = { module = "moe.nea.jarvis:jarvis-api", version.ref = "jarvis" } @@ -125,6 +132,7 @@ sodium = { module = "maven.modrinth:sodium", version.ref = "sodium" } freecammod = { module = "maven.modrinth:freecam", version.ref = "freecammod" } citresewn = { module = "maven.modrinth:cit-resewn", version.ref = "citresewn" } femalegender = { module = "maven.modrinth:female-gender", version.ref = "femalegender" } +jade = { module = "maven.modrinth:jade", version.ref = "jade" } yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } basicMath = { module = "me.shedaniel.cloth:basic-math", version.ref = "basicMath" } @@ -135,7 +143,7 @@ asm = { module = "org.ow2.asm:asm", version.ref = "asm" } [bundles] runtime_required = [ - "rei_fabric", + # "rei_fabric", # "notenoughanimations", "hypixelmodapi_fabric", ] @@ -143,8 +151,8 @@ runtime_optional = [ "devauth", # "freecammod", # "sodium", - # "qolify", - "ncr", + # "qolify", + # "ncr", # "citresewn", ] @@ -155,3 +163,4 @@ kotlin_plugin_powerassert = { id = "org.jetbrains.kotlin.plugin.power-assert", v kotlin_plugin_ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin_ksp" } loom = { id = "dev.architectury.loom", version.ref = "loom" } shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } +mcAutoTranslations = { id = "moe.nea.mc-auto-translations", version.ref = "mcAutoTranslations" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6e2ced2..4cdd0fb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,6 +4,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index aef468a..8a5dc47 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ pluginManagement { rootProject.name = "Firmament" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include("symbols") include("javaplugin") include("testagent") diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/Compat.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/Compat.kt new file mode 100644 index 0000000..d1cfef4 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/Compat.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.compat.jade + +import net.fabricmc.loader.api.FabricLoader +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return FabricLoader.getInstance().isModLoaded("jade") + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt new file mode 100644 index 0000000..53e3255 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt @@ -0,0 +1,41 @@ +package moe.nea.firmament.compat.jade + +import snownee.jade.api.Accessor +import snownee.jade.api.BlockAccessor +import snownee.jade.api.IWailaClientRegistration +import snownee.jade.api.callback.JadeRayTraceCallback +import net.minecraft.util.hit.HitResult +import moe.nea.firmament.repo.MiningRepoData +import moe.nea.firmament.util.mc.FirmamentDataComponentTypes + +class CustomFakeBlockProvider(val registration: IWailaClientRegistration) : JadeRayTraceCallback { + + override fun onRayTrace( + hitResult: HitResult, + accessor: Accessor<*>?, + originalAccessor: Accessor<*>? + ): Accessor<*>? { + if (!JadeIntegration.TConfig.blockDetection) return accessor + if (accessor !is BlockAccessor) return accessor + val customBlock = JadeIntegration.customBlocks[accessor.block] + if (customBlock == null) return accessor + return registration.blockAccessor() + .from(accessor) + .fakeBlock(customBlock.getDisplayItem(accessor.block)) + .build() + } + + companion object { + @JvmStatic + fun hasCustomBlock(accessor: BlockAccessor): Boolean { + return getCustomBlock(accessor) != null + } + + @JvmStatic + fun getCustomBlock(accessor: BlockAccessor): MiningRepoData.CustomMiningBlock? { + if (!accessor.isFakeBlock) return null + val item = accessor.fakeBlock + return item.get(FirmamentDataComponentTypes.CUSTOM_MINING_BLOCK_DATA) + } + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomMiningHardnessProvider.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomMiningHardnessProvider.kt new file mode 100644 index 0000000..29fecd2 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomMiningHardnessProvider.kt @@ -0,0 +1,97 @@ +package moe.nea.firmament.compat.jade + +import snownee.jade.api.BlockAccessor +import snownee.jade.api.IBlockComponentProvider +import snownee.jade.api.ITooltip +import snownee.jade.api.config.IPluginConfig +import kotlin.time.DurationUnit +import net.minecraft.block.BlockState +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.tr + +object CustomMiningHardnessProvider : IBlockComponentProvider { + + override fun appendTooltip( + tooltip: ITooltip, + block: BlockAccessor, + config: IPluginConfig? + ) { + val customBlock = CustomFakeBlockProvider.getCustomBlock(block) ?: return + if (customBlock.breakingPower > 0) + tooltip.add(tr("firmament.jade.breaking_power", "Required Breaking Power: ${customBlock.breakingPower}")) + } + + override fun getUid(): Identifier = + Firmament.identifier("custom_mining_hardness") + + data class BreakingInfo( + val blockPos: BlockPos, val stage: Int, + val state: BlockState?, + val ts: TimeMark = TimeMark.now() + ) + + var previousBreakingInfo: BreakingInfo? = null + var currentBreakingInfo: BreakingInfo? = null + + @Subscribe + fun clearInfoOnStopBreaking(event: TickEvent) { + val isBreakingBlock = MC.interactionManager?.isBreakingBlock ?: false + if (!isBreakingBlock) { + previousBreakingInfo = null + currentBreakingInfo = null + } + } + + @JvmStatic + fun setBreakingInfo(blockPos: BlockPos, stage: Int) { + previousBreakingInfo = currentBreakingInfo + val state = MC.world?.getBlockState(blockPos) + if (previousBreakingInfo?.let { it.state != state || it.blockPos != blockPos } ?: false) + previousBreakingInfo == null + currentBreakingInfo = BreakingInfo(blockPos.toImmutable(), stage, state) + // For some reason hypixel initially sends a stage 10 packet, and then fixes it up with a stage 0 packet. + // Ignore the stage 10 packet if we dont have any previous packets for this block. + // This could in theory still have issues if someone perfectly stops breaking a block the tick it finishes and then does not break another block until it respawns, but i deem that to be too much of an edge case. + if (stage == 10 && previousBreakingInfo == null) { + previousBreakingInfo = null + currentBreakingInfo = null + } + } + + @JvmStatic + fun replaceBreakProgress(original: Float): Float { + if (!JadeIntegration.TConfig.miningProgress) return original + if (!isOnMiningIsland()) return original + val pos = MC.interactionManager?.currentBreakingPos ?: return original + val info = currentBreakingInfo + if (info?.blockPos != pos || info.state != MC.world?.getBlockState(pos)) { + currentBreakingInfo = null + previousBreakingInfo = null + return original + } + // TODO: improve this interpolation to work across all stages, to alleviate some of the jittery bar. + // Maybe introduce a proper mining API that tracks the actual progress with some sort of FSM + val interpolatedStage = previousBreakingInfo?.let { prev -> + val timeBetweenTicks = (info.ts - prev.ts).toDouble(DurationUnit.SECONDS) + val stagesPerUpdate = (info.stage - prev.stage).toDouble() + if (stagesPerUpdate < 1) return@let null + val stagesPerSecond = stagesPerUpdate / timeBetweenTicks + info.stage + (info.ts.passedTime().toDouble(DurationUnit.SECONDS) * stagesPerSecond) + .coerceAtMost(stagesPerUpdate) + }?.toFloat() + val stage = interpolatedStage ?: info.stage.toFloat() + return stage / 10F + } + + @JvmStatic + fun replaceBlockBreakSpeed(original: Float): Float { + if (isOnMiningIsland()) return 0F + return original + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt new file mode 100644 index 0000000..ab45e7c --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt @@ -0,0 +1,78 @@ +package moe.nea.firmament.compat.jade + +import java.util.Optional +import java.util.function.UnaryOperator +import snownee.jade.api.BlockAccessor +import snownee.jade.api.IBlockComponentProvider +import snownee.jade.api.ITooltip +import snownee.jade.api.JadeIds +import snownee.jade.api.config.IPluginConfig +import snownee.jade.api.theme.IThemeHelper +import snownee.jade.api.ui.IElement +import snownee.jade.api.ui.IElementHelper +import snownee.jade.impl.ui.ItemStackElement +import snownee.jade.impl.ui.TextElement +import kotlin.jvm.optionals.getOrDefault +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.math.Vec2f +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.MC + +class DrillToolProvider : IBlockComponentProvider { + override fun appendTooltip( + tooltip: ITooltip, + accessor: BlockAccessor, + p2: IPluginConfig? + ) { + val customBlock = CustomFakeBlockProvider.getCustomBlock(accessor) ?: return + val tool = RepoManager.miningData.getToolsThatCanBreak(customBlock.breakingPower).firstOrNull() + ?.asImmutableItemStack() ?: return + tooltip.replace(JadeIds.MC_HARVEST_TOOL, UnaryOperator { elements -> + elements.map { inner -> + val lastItemIndex = inner.indexOfLast { it is ItemStackElement } + if (lastItemIndex < 0) return@map inner + val innerMut = inner.toMutableList() + val harvestIndicator = innerMut.indexOfLast { + it is TextElement && it.cachedSize == Vec2f.ZERO && it.text.visit { + if (it.isEmpty()) Optional.empty() else Optional.of(true) + }.getOrDefault(false) + } + val canHarvest = SBItemStack(MC.stackInHand).breakingPower >= customBlock.breakingPower + val lastItem = innerMut[lastItemIndex] as ItemStackElement + if (harvestIndicator < 0) { + innerMut.add(lastItemIndex + 1, canHarvestIndicator(canHarvest, lastItem.alignment)) + } else { + innerMut.set(harvestIndicator, canHarvestIndicator(canHarvest, lastItem.alignment)) + } + innerMut.set(lastItemIndex, IElementHelper.get() + .item(tool, 0.75f) + .translate(lastItem.translation) + .size(lastItem.size) + .message(null) + .align(lastItem.alignment)) + innerMut.subList(0, lastItemIndex - 1).removeIf { it is ItemStackElement } + innerMut + } + }) + } + + fun canHarvestIndicator(canHarvest: Boolean, align: IElement.Align): IElement { + val t = IThemeHelper.get() + val text = if (canHarvest) t.success(CHECK) else t.danger(X) + return IElementHelper.get().text(text) + .scale(0.75F).zOffset(800).size(Vec2f.ZERO).translate(Vec2f(-3F, 3.25F)).align(align) + } + + private val CHECK: Text = Text.literal("✔") + private val X: Text = Text.literal("✕") + + override fun getUid(): Identifier { + return Firmament.identifier("toolprovider") + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/FirmamentJadePlugin.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/FirmamentJadePlugin.kt new file mode 100644 index 0000000..51e2453 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/FirmamentJadePlugin.kt @@ -0,0 +1,21 @@ +package moe.nea.firmament.compat.jade + +import snownee.jade.api.IWailaClientRegistration +import snownee.jade.api.IWailaCommonRegistration +import snownee.jade.api.IWailaPlugin +import snownee.jade.api.WailaPlugin +import net.minecraft.block.Block +import moe.nea.firmament.Firmament + +@WailaPlugin +class FirmamentJadePlugin : IWailaPlugin { + override fun register(registration: IWailaCommonRegistration) { + Firmament.logger.debug("Registering Jade integration...") + } + + override fun registerClient(registration: IWailaClientRegistration) { + registration.registerBlockComponent(CustomMiningHardnessProvider, Block::class.java) + registration.registerBlockComponent(DrillToolProvider(), Block::class.java) + registration.addRayTraceCallback(CustomFakeBlockProvider(registration)) + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt new file mode 100644 index 0000000..d411c26 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt @@ -0,0 +1,50 @@ +package moe.nea.firmament.compat.jade + +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SkyblockServerUpdateEvent +import moe.nea.firmament.repo.MiningRepoData +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.ErrorUtil +import net.minecraft.block.Block +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.gui.config.ManagedConfig + +object JadeIntegration { + object TConfig : ManagedConfig("jade-integration", Category.INTEGRATIONS) { + val miningProgress by toggle("progress") { true } + val blockDetection by toggle("blocks") { true } + } + + var customBlocks: Map<Block, MiningRepoData.CustomMiningBlock> = mapOf() + + fun refreshBlockInfo() { + if (!isOnMiningIsland()) { + customBlocks = mapOf() + return + } + val blocks = RepoManager.miningData.customMiningBlocks + .flatMap { customBlock -> + // TODO: add a lifted helper method for this + customBlock.blocks189.filter { it.isCurrentlyActive } + .mapNotNull { it.block } + .map { customBlock to it } + } + .groupBy { it.second } + customBlocks = blocks.mapNotNull { (block, customBlocks) -> + val singleMatch = + ErrorUtil.notNullOr(customBlocks.singleOrNull()?.first, + "Two custom blocks both want to supply custom mining behaviour for $block.") { return@mapNotNull null } + block to singleMatch + }.toMap() + } + + @Subscribe + fun onRepoReload(event: ReloadRegistrationEvent) { + event.repo.registerReloadListener { refreshBlockInfo() } + } + + @Subscribe + fun onWorldSwap(event: SkyblockServerUpdateEvent) { + refreshBlockInfo() + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/utils.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/utils.kt new file mode 100644 index 0000000..364dc02 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/utils.kt @@ -0,0 +1,6 @@ +package moe.nea.firmament.compat.jade + +import moe.nea.firmament.util.SBData + +fun isOnMiningIsland(): Boolean = + SBData.skyblockLocation?.hasCustomMining ?: false diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/ElementAccessor.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/ElementAccessor.java new file mode 100644 index 0000000..1b58e3c --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/ElementAccessor.java @@ -0,0 +1,12 @@ +package moe.nea.firmament.mixins.compat.jade; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import snownee.jade.api.ui.Element; +import snownee.jade.api.ui.IElement; + +@Mixin(Element.class) +public interface ElementAccessor { + @Accessor("align") + IElement.Align getAlign_firmament(); +} diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java new file mode 100644 index 0000000..3677d01 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java @@ -0,0 +1,33 @@ +package moe.nea.firmament.mixins.compat.jade; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.compat.jade.CustomFakeBlockProvider; +import net.minecraft.block.Blocks; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import snownee.jade.addon.harvest.HarvestToolProvider; +import snownee.jade.api.BlockAccessor; + +import java.util.List; + +@Mixin(HarvestToolProvider.class) +public class EnforceToolDisplayForCustomBlocksInHarvestToolProvider { + @ModifyExpressionValue(method = "getText", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/BlockState;isToolRequired()Z")) + private boolean overwriteRequiresTool(boolean original, @Local(argsOnly = true) BlockAccessor accessor) { + if (CustomFakeBlockProvider.hasCustomBlock(accessor)) + return true; + return original; + } + + private static final List<ItemStack> REPLACEABLE_TOOL = List.of(new ItemStack(Blocks.ENCHANTING_TABLE)); + + @ModifyExpressionValue(method = "getText", at = @At(value = "INVOKE", target = "Lcom/google/common/cache/Cache;get(Ljava/lang/Object;Ljava/util/concurrent/Callable;)Ljava/lang/Object;")) + private Object overwriteAvailableTools(Object original, @Local(argsOnly = true) BlockAccessor accessor) { + var orig = (List<ItemStack>) original; + if (orig.isEmpty() && CustomFakeBlockProvider.hasCustomBlock(accessor)) + return REPLACEABLE_TOOL; + return orig; + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/OnUpdateBreakProgress.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/OnUpdateBreakProgress.java new file mode 100644 index 0000000..7d71ae8 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/OnUpdateBreakProgress.java @@ -0,0 +1,22 @@ +package moe.nea.firmament.mixins.compat.jade; + +import moe.nea.firmament.compat.jade.CustomMiningHardnessProvider; +import moe.nea.firmament.util.MC; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Objects; + +@Mixin(WorldRenderer.class) +public class OnUpdateBreakProgress { + @Inject(method = "setBlockBreakingInfo", at = @At("HEAD")) + private void replaceBreakProgress(int entityId, BlockPos pos, int stage, CallbackInfo ci) { + if (entityId == 0 && null != MC.INSTANCE.getInteractionManager() && Objects.equals(MC.INSTANCE.getInteractionManager().currentBreakingPos, pos)) { + CustomMiningHardnessProvider.setBreakingInfo(pos, stage); + } + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/PatchBreakingBarSpeedJade.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/PatchBreakingBarSpeedJade.java new file mode 100644 index 0000000..203f7e4 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/PatchBreakingBarSpeedJade.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.compat.jade; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.compat.jade.CustomMiningHardnessProvider; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import snownee.jade.JadeClient; + +@Mixin(JadeClient.class) +public class PatchBreakingBarSpeedJade { + @ModifyExpressionValue( + method = "drawBreakingProgress", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;currentBreakingProgress:F", opcode = Opcodes.GETFIELD) + ) + private static float replaceBlockBreakingProgress(float original) { + return CustomMiningHardnessProvider.replaceBreakProgress(original); + } + + @ModifyExpressionValue(method = "drawBreakingProgress", + at = @At(value = "INVOKE", target = "Lnet/minecraft/block/BlockState;calcBlockBreakingDelta(Lnet/minecraft/entity/player/PlayerEntity;Lnet/minecraft/world/BlockView;Lnet/minecraft/util/math/BlockPos;)F")) + private static float replacePlayerSpecificBreakingProgress(float original) { + return CustomMiningHardnessProvider.replaceBlockBreakSpeed(original); + } +} diff --git a/src/compat/moulconfig/java/ProcessedOptionFirm.kt b/src/compat/moulconfig/java/ProcessedOptionFirm.kt index 4d0096c..6936048 100644 --- a/src/compat/moulconfig/java/ProcessedOptionFirm.kt +++ b/src/compat/moulconfig/java/ProcessedOptionFirm.kt @@ -10,6 +10,9 @@ abstract class ProcessedOptionFirm( private val accordionId: Int, private val config: Config ) : ProcessedOption { + override fun getPath(): String? { + return "nonsense" + } lateinit var category: ProcessedCategoryFirm override fun getAccordionId(): Int { return accordionId diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/Compat.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/Compat.kt new file mode 100644 index 0000000..9ab4d22 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/Compat.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.compat.rei + +import net.fabricmc.loader.api.FabricLoader +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return FabricLoader.getInstance().isModLoaded("roughlyenoughitems") + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt index aade59e..b5c9a6d 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt @@ -1,5 +1,6 @@ package moe.nea.firmament.compat.rei +import io.github.moulberry.repo.data.NEUCraftingRecipe import me.shedaniel.rei.api.client.plugins.REIClientPlugin import me.shedaniel.rei.api.client.registry.category.CategoryRegistry import me.shedaniel.rei.api.client.registry.display.DisplayRegistry @@ -19,11 +20,10 @@ import net.minecraft.item.ItemStack import net.minecraft.text.Text import net.minecraft.util.ActionResult import net.minecraft.util.Identifier -import moe.nea.firmament.compat.rei.recipes.SBCraftingRecipe -import moe.nea.firmament.compat.rei.recipes.SBEssenceUpgradeRecipe -import moe.nea.firmament.compat.rei.recipes.SBForgeRecipe +import moe.nea.firmament.compat.rei.recipes.GenericREIRecipeCategory import moe.nea.firmament.compat.rei.recipes.SBKatRecipe import moe.nea.firmament.compat.rei.recipes.SBMobDropRecipe +import moe.nea.firmament.compat.rei.recipes.SBRecipe import moe.nea.firmament.compat.rei.recipes.SBReforgeRecipe import moe.nea.firmament.compat.rei.recipes.SBShopRecipe import moe.nea.firmament.events.HandledScreenPushREIEvent @@ -31,6 +31,9 @@ import moe.nea.firmament.features.inventory.CraftingOverlay import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.SBCraftingRecipeRenderer +import moe.nea.firmament.repo.recipes.SBEssenceUpgradeRecipeRenderer +import moe.nea.firmament.repo.recipes.SBForgeRecipeRenderer import moe.nea.firmament.util.MC import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.guessRecipeId @@ -52,19 +55,23 @@ class FirmamentReiPlugin : REIClientPlugin { registry.register(TransferHandler { context -> val screen = context.containerScreen val display = context.display - if (display !is SBCraftingRecipe) return@TransferHandler TransferHandler.Result.createNotApplicable() - val neuItem = RepoManager.getNEUItem(SkyblockId(display.neuRecipe.output.itemId)) - ?: error("Could not find neu item ${display.neuRecipe.output.itemId} which is used in a recipe output") + if (display !is SBRecipe) return@TransferHandler TransferHandler.Result.createNotApplicable() + val recipe = display.neuRecipe + if (recipe !is NEUCraftingRecipe) return@TransferHandler TransferHandler.Result.createNotApplicable() + val neuItem = RepoManager.getNEUItem(SkyblockId(recipe.output.itemId)) + ?: error("Could not find neu item ${recipe.output.itemId} which is used in a recipe output") val useSuperCraft = context.isStackedCrafting || RepoManager.Config.alwaysSuperCraft if (neuItem.isVanilla && useSuperCraft) return@TransferHandler TransferHandler.Result.createFailed(Text.translatable( "firmament.recipe.novanilla")) var shouldReturn = true if (context.isActuallyCrafting && !useSuperCraft) { - if (screen !is GenericContainerScreen || screen.title?.unformattedString != CraftingOverlay.CRAFTING_SCREEN_NAME) { + val craftingScreen = (screen as? GenericContainerScreen) + ?.takeIf { it.title?.unformattedString == CraftingOverlay.CRAFTING_SCREEN_NAME } + if (craftingScreen == null) { MC.sendCommand("craft") shouldReturn = false } - CraftingOverlay.setOverlay(screen as? GenericContainerScreen, display.neuRecipe) + CraftingOverlay.setOverlay(craftingScreen, recipe) } if (context.isActuallyCrafting && useSuperCraft) { shouldReturn = false @@ -75,13 +82,17 @@ class FirmamentReiPlugin : REIClientPlugin { } + val generics = listOf<GenericREIRecipeCategory<*>>( // Order matters: The order in here is the order in which they show up in REI + GenericREIRecipeCategory(SBCraftingRecipeRenderer), + GenericREIRecipeCategory(SBForgeRecipeRenderer), + GenericREIRecipeCategory(SBEssenceUpgradeRecipeRenderer), + ) + override fun registerCategories(registry: CategoryRegistry) { - registry.add(SBCraftingRecipe.Category) - registry.add(SBForgeRecipe.Category) + registry.add(generics) registry.add(SBMobDropRecipe.Category) registry.add(SBKatRecipe.Category) registry.add(SBReforgeRecipe.Category) - registry.add(SBEssenceUpgradeRecipe.Category) registry.add(SBShopRecipe.Category) } @@ -91,17 +102,14 @@ class FirmamentReiPlugin : REIClientPlugin { } override fun registerDisplays(registry: DisplayRegistry) { - registry.registerDisplayGenerator( - SBCraftingRecipe.Category.catIdentifier, - SkyblockCraftingRecipeDynamicGenerator) + generics.forEach { + it.registerDynamicGenerator(registry) + } registry.registerDisplayGenerator( SBReforgeRecipe.catIdentifier, SBReforgeRecipe.DynamicGenerator ) registry.registerDisplayGenerator( - SBForgeRecipe.Category.categoryIdentifier, - SkyblockForgeRecipeDynamicGenerator) - registry.registerDisplayGenerator( SBMobDropRecipe.Category.categoryIdentifier, SkyblockMobDropRecipeDynamicGenerator) registry.registerDisplayGenerator( @@ -110,10 +118,6 @@ class FirmamentReiPlugin : REIClientPlugin { registry.registerDisplayGenerator( SBKatRecipe.Category.categoryIdentifier, SkyblockKatRecipeDynamicGenerator) - registry.registerDisplayGenerator( - SBEssenceUpgradeRecipe.Category.categoryIdentifier, - SkyblockEssenceRecipeDynamicGenerator - ) } override fun registerCollapsibleEntries(registry: CollapsibleEntryRegistry) { diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt new file mode 100644 index 0000000..8e39f28 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt @@ -0,0 +1,62 @@ +package moe.nea.firmament.compat.rei + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.gui.widgets.Widget +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import net.minecraft.text.Text +import moe.nea.firmament.compat.rei.recipes.wrapWidget +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.RecipeLayouter + +class REIRecipeLayouter : RecipeLayouter { + val container: MutableList<Widget> = mutableListOf() + fun <T: Widget> add(t: T): T = t.also(container::add) + + override fun createItemSlot( + x: Int, + y: Int, + content: SBItemStack?, + slotKind: RecipeLayouter.SlotKind + ) { + val slot = Widgets.createSlot(Point(x, y)) + if (content != null) + slot.entry(SBItemEntryDefinition.getEntry(content)) + when (slotKind) { + RecipeLayouter.SlotKind.SMALL_INPUT -> slot.markInput() + RecipeLayouter.SlotKind.SMALL_OUTPUT -> slot.markOutput() + RecipeLayouter.SlotKind.BIG_OUTPUT -> { + slot.markOutput().disableBackground() + add(Widgets.createResultSlotBackground(Point(x, y))) + } + } + add(slot) + } + + override fun createTooltip(rectangle: Rectangle, label: Text) { + add(Widgets.createTooltip(rectangle, label)) + } + + override fun createLabel(x: Int, y: Int, text: Text) { + add(Widgets.createLabel(Point(x, y), text)) + } + + override fun createArrow(x: Int, y: Int) = + add(Widgets.createArrow(Point(x, y))).bounds + + override fun createMoulConfig( + x: Int, + y: Int, + w: Int, + h: Int, + component: GuiComponent + ) { + add(wrapWidget(Rectangle(Point(x, y), Dimension(w, h)), component)) + } + + override fun createFire(ingredientsCenter: Point, animationTicks: Int) { + add(Widgets.createBurningFire(ingredientsCenter).animationDurationTicks(animationTicks.toDouble())) + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt index e80840f..900ebab 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt @@ -1,6 +1,5 @@ package moe.nea.firmament.compat.rei -import io.github.moulberry.repo.data.NEUCraftingRecipe import io.github.moulberry.repo.data.NEUForgeRecipe import io.github.moulberry.repo.data.NEUKatUpgradeRecipe import io.github.moulberry.repo.data.NEUMobDropRecipe @@ -11,9 +10,6 @@ import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator import me.shedaniel.rei.api.client.view.ViewSearchBuilder import me.shedaniel.rei.api.common.display.Display import me.shedaniel.rei.api.common.entry.EntryStack -import moe.nea.firmament.compat.rei.recipes.SBCraftingRecipe -import moe.nea.firmament.compat.rei.recipes.SBEssenceUpgradeRecipe -import moe.nea.firmament.compat.rei.recipes.SBForgeRecipe import moe.nea.firmament.compat.rei.recipes.SBKatRecipe import moe.nea.firmament.compat.rei.recipes.SBMobDropRecipe import moe.nea.firmament.compat.rei.recipes.SBShopRecipe @@ -22,33 +18,27 @@ import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.SBItemStack -val SkyblockCraftingRecipeDynamicGenerator = - neuDisplayGenerator<SBCraftingRecipe, NEUCraftingRecipe> { SBCraftingRecipe(it) } - -val SkyblockForgeRecipeDynamicGenerator = - neuDisplayGenerator<SBForgeRecipe, NEUForgeRecipe> { SBForgeRecipe(it) } - val SkyblockMobDropRecipeDynamicGenerator = neuDisplayGenerator<SBMobDropRecipe, NEUMobDropRecipe> { SBMobDropRecipe(it) } val SkyblockShopRecipeDynamicGenerator = neuDisplayGenerator<SBShopRecipe, NEUNpcShopRecipe> { SBShopRecipe(it) } val SkyblockKatRecipeDynamicGenerator = neuDisplayGenerator<SBKatRecipe, NEUKatUpgradeRecipe> { SBKatRecipe(it) } -val SkyblockEssenceRecipeDynamicGenerator = - neuDisplayGeneratorWithItem<SBEssenceUpgradeRecipe, EssenceRecipeProvider.EssenceUpgradeRecipe> { item, recipe -> - SBEssenceUpgradeRecipe(recipe, item) - } inline fun <D : Display, reified T : NEURecipe> neuDisplayGenerator(crossinline mapper: (T) -> D) = neuDisplayGeneratorWithItem<D, T> { _, it -> mapper(it) } inline fun <D : Display, reified T : NEURecipe> neuDisplayGeneratorWithItem(crossinline mapper: (SBItemStack, T) -> D) = + neuDisplayGeneratorWithItem(T::class.java, mapper) +inline fun <D : Display, T : NEURecipe> neuDisplayGeneratorWithItem( + filter: Class<T>, + crossinline mapper: (SBItemStack, T) -> D) = object : DynamicDisplayGenerator<D> { override fun getRecipeFor(entry: EntryStack<*>): Optional<List<D>> { if (entry.type != SBItemEntryDefinition.type) return Optional.empty() val item = entry.castValue<SBItemStack>() val recipes = RepoManager.getRecipesFor(item.skyblockId) - val craftingRecipes = recipes.filterIsInstance<T>() + val craftingRecipes = recipes.filterIsInstance<T>(filter) return Optional.of(craftingRecipes.map { mapper(item, it) }) } @@ -60,7 +50,7 @@ inline fun <D : Display, reified T : NEURecipe> neuDisplayGeneratorWithItem(cros if (entry.type != SBItemEntryDefinition.type) return Optional.empty() val item = entry.castValue<SBItemStack>() val recipes = RepoManager.getUsagesFor(item.skyblockId) - val craftingRecipes = recipes.filterIsInstance<T>() + val craftingRecipes = recipes.filterIsInstance<T>(filter) return Optional.of(craftingRecipes.map { mapper(item, it) }) } } diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt index 518f7b4..9ccfab4 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt @@ -9,7 +9,6 @@ import me.shedaniel.rei.api.common.entry.EntryStack import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.ingame.HandledScreen import moe.nea.firmament.mixins.accessor.AccessorHandledScreen -import moe.nea.firmament.util.skyBlockId object SkyblockItemIdFocusedStackProvider : FocusedStackProvider { override fun provide(screen: Screen?, mouse: Point?): CompoundEventResult<EntryStack<*>> { diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt new file mode 100644 index 0000000..15cb818 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt @@ -0,0 +1,67 @@ +package moe.nea.firmament.compat.rei.recipes + +import io.github.moulberry.repo.data.NEURecipe +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.gui.Renderer +import me.shedaniel.rei.api.client.gui.widgets.Widget +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import me.shedaniel.rei.api.client.registry.display.DisplayCategory +import me.shedaniel.rei.api.client.registry.display.DisplayRegistry +import me.shedaniel.rei.api.common.category.CategoryIdentifier +import me.shedaniel.rei.api.common.util.EntryStacks +import net.minecraft.text.Text +import moe.nea.firmament.compat.rei.REIRecipeLayouter +import moe.nea.firmament.compat.rei.neuDisplayGeneratorWithItem +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.GenericRecipeRenderer + +class GenericREIRecipeCategory<T : NEURecipe>( + val renderer: GenericRecipeRenderer<T>, +) : DisplayCategory<GenericRecipe<T>> { + private val dynamicGenerator = + neuDisplayGeneratorWithItem<GenericRecipe<T>, T>(renderer.typ) { item, recipe -> + GenericRecipe( + recipe, + item, + categoryIdentifier + ) + } + + private val categoryIdentifier = CategoryIdentifier.of<GenericRecipe<T>>(renderer.identifier) + override fun getCategoryIdentifier(): CategoryIdentifier<GenericRecipe<T>> { + return categoryIdentifier + } + + override fun getDisplayHeight(): Int { + return renderer.displayHeight + } + + override fun getTitle(): Text? { + return renderer.title + } + + override fun getIcon(): Renderer? { + return EntryStacks.of(renderer.icon) + } + + override fun setupDisplay(display: GenericRecipe<T>, bounds: Rectangle): List<Widget> { + val layouter = REIRecipeLayouter() + layouter.container.add(Widgets.createRecipeBase(bounds)) + renderer.render(display.neuRecipe, bounds, layouter, display.sourceItem) + return layouter.container + } + + fun registerDynamicGenerator(registry: DisplayRegistry) { + registry.registerDisplayGenerator(categoryIdentifier, dynamicGenerator) + } +} + +class GenericRecipe<T : NEURecipe>( + override val neuRecipe: T, + val sourceItem: SBItemStack?, + val id: CategoryIdentifier<GenericRecipe<T>> +) : SBRecipe() { + override fun getCategoryIdentifier(): CategoryIdentifier<*>? { + return id + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt deleted file mode 100644 index c02e078..0000000 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt +++ /dev/null @@ -1,56 +0,0 @@ -package moe.nea.firmament.compat.rei.recipes - -import io.github.moulberry.repo.data.NEUCraftingRecipe -import io.github.moulberry.repo.data.NEUIngredient -import me.shedaniel.math.Point -import me.shedaniel.math.Rectangle -import me.shedaniel.rei.api.client.gui.Renderer -import me.shedaniel.rei.api.client.gui.widgets.Widget -import me.shedaniel.rei.api.client.gui.widgets.Widgets -import me.shedaniel.rei.api.client.registry.display.DisplayCategory -import me.shedaniel.rei.api.common.category.CategoryIdentifier -import me.shedaniel.rei.api.common.display.Display -import me.shedaniel.rei.api.common.display.DisplaySerializer -import me.shedaniel.rei.api.common.util.EntryStacks -import net.minecraft.block.Blocks -import net.minecraft.text.Text -import moe.nea.firmament.Firmament -import moe.nea.firmament.compat.rei.SBItemEntryDefinition -import moe.nea.firmament.repo.SBItemStack - -class SBCraftingRecipe(override val neuRecipe: NEUCraftingRecipe) : SBRecipe() { - override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.catIdentifier - - object Category : DisplayCategory<SBCraftingRecipe> { - val catIdentifier = CategoryIdentifier.of<SBCraftingRecipe>(Firmament.MOD_ID, "crafting_recipe") - override fun getCategoryIdentifier(): CategoryIdentifier<out SBCraftingRecipe> = catIdentifier - - override fun getTitle(): Text = Text.literal("SkyBlock Crafting") - - override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Blocks.CRAFTING_TABLE) - override fun setupDisplay(display: SBCraftingRecipe, bounds: Rectangle): List<Widget> { - val point = Point(bounds.centerX - 58, bounds.centerY - 27) - return buildList { - add(Widgets.createRecipeBase(bounds)) - add(Widgets.createArrow(Point(point.x + 60, point.y + 18))) - add(Widgets.createResultSlotBackground(Point(point.x + 95, point.y + 19))) - for (i in 0 until 3) { - for (j in 0 until 3) { - val slot = Widgets.createSlot(Point(point.x + 1 + i * 18, point.y + 1 + j * 18)).markInput() - add(slot) - val item = display.neuRecipe.inputs[i + j * 3] - if (item == NEUIngredient.SENTINEL_EMPTY) continue - slot.entry(SBItemEntryDefinition.getEntry(item)) - } - } - add( - Widgets.createSlot(Point(point.x + 95, point.y + 19)) - .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.output)) - .disableBackground().markOutput() - ) - } - } - - } - -} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt deleted file mode 100644 index e0a3784..0000000 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt +++ /dev/null @@ -1,62 +0,0 @@ -package moe.nea.firmament.compat.rei.recipes - -import me.shedaniel.math.Point -import me.shedaniel.math.Rectangle -import me.shedaniel.rei.api.client.gui.Renderer -import me.shedaniel.rei.api.client.gui.widgets.Widget -import me.shedaniel.rei.api.client.gui.widgets.Widgets -import me.shedaniel.rei.api.client.registry.display.DisplayCategory -import me.shedaniel.rei.api.common.category.CategoryIdentifier -import net.minecraft.text.Text -import moe.nea.firmament.Firmament -import moe.nea.firmament.compat.rei.SBItemEntryDefinition -import moe.nea.firmament.repo.EssenceRecipeProvider -import moe.nea.firmament.repo.SBItemStack -import moe.nea.firmament.util.SkyblockId - -class SBEssenceUpgradeRecipe(override val neuRecipe: EssenceRecipeProvider.EssenceUpgradeRecipe, - val sourceItem: SBItemStack) : SBRecipe() { - object Category : DisplayCategory<SBEssenceUpgradeRecipe> { - override fun getCategoryIdentifier(): CategoryIdentifier<SBEssenceUpgradeRecipe> = - CategoryIdentifier.of(Firmament.MOD_ID, "essence_upgrade") - - override fun getTitle(): Text { - return Text.literal("Essence Upgrades") - } - - override fun getIcon(): Renderer { - return SBItemEntryDefinition.getEntry(SkyblockId("ESSENCE_WITHER")) - } - - override fun setupDisplay(display: SBEssenceUpgradeRecipe, bounds: Rectangle): List<Widget> { - val recipe = display.neuRecipe - val list = mutableListOf<Widget>() - list.add(Widgets.createRecipeBase(bounds)) - list.add(Widgets.createSlot(Point(bounds.minX + 12, bounds.centerY - 8 - 18 / 2)) - .markInput() - .entry(SBItemEntryDefinition.getEntry(display.sourceItem.copy(stars = recipe.starCountAfter - 1)))) - list.add(Widgets.createSlot(Point(bounds.minX + 12, bounds.centerY - 8 + 18 / 2)) - .markInput() - .entry(SBItemEntryDefinition.getEntry(recipe.essenceIngredient))) - list.add(Widgets.createSlot(Point(bounds.maxX - 12 - 16, bounds.centerY - 8)) - .markOutput() - .entry(SBItemEntryDefinition.getEntry(display.sourceItem.copy(stars = recipe.starCountAfter)))) - val extraItems = recipe.extraItems - list.add(Widgets.createArrow(Point(bounds.centerX - 24 / 2, - if (extraItems.isEmpty()) bounds.centerY - 17 / 2 - else bounds.centerY + 18 / 2))) - for ((index, item) in extraItems.withIndex()) { - list.add(Widgets.createSlot( - Point(bounds.centerX - extraItems.size * 16 / 2 - 2 / 2 + index * 18, - bounds.centerY - 18 / 2)) - .markInput() - .entry(SBItemEntryDefinition.getEntry(item))) - } - return list - } - } - - override fun getCategoryIdentifier(): CategoryIdentifier<*> { - return Category.categoryIdentifier - } -} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt deleted file mode 100644 index 7a0ec78..0000000 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt +++ /dev/null @@ -1,71 +0,0 @@ -package moe.nea.firmament.compat.rei.recipes - -import io.github.moulberry.repo.data.NEUForgeRecipe -import me.shedaniel.math.Point -import me.shedaniel.math.Rectangle -import me.shedaniel.rei.api.client.gui.Renderer -import me.shedaniel.rei.api.client.gui.widgets.Widget -import me.shedaniel.rei.api.client.gui.widgets.Widgets -import me.shedaniel.rei.api.client.registry.display.DisplayCategory -import me.shedaniel.rei.api.common.category.CategoryIdentifier -import kotlin.math.cos -import kotlin.math.sin -import kotlin.time.Duration.Companion.seconds -import net.minecraft.block.Blocks -import net.minecraft.text.Text -import moe.nea.firmament.Firmament -import moe.nea.firmament.compat.rei.SBItemEntryDefinition -import moe.nea.firmament.compat.rei.plus - -class SBForgeRecipe(override val neuRecipe: NEUForgeRecipe) : SBRecipe() { - override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.categoryIdentifier - - object Category : DisplayCategory<SBForgeRecipe> { - override fun getCategoryIdentifier(): CategoryIdentifier<SBForgeRecipe> = - CategoryIdentifier.of(Firmament.MOD_ID, "forge_recipe") - - override fun getTitle(): Text = Text.literal("Forge Recipes") - override fun getDisplayHeight(): Int { - return 104 - } - - override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Blocks.ANVIL) - override fun setupDisplay(display: SBForgeRecipe, bounds: Rectangle): List<Widget> { - return buildList { - add(Widgets.createRecipeBase(bounds)) - add(Widgets.createResultSlotBackground(Point(bounds.minX + 124, bounds.minY + 46))) - val arrow = Widgets.createArrow(Point(bounds.minX + 90, bounds.minY + 54 - 18 / 2)) - add(arrow) - add(Widgets.createTooltip(arrow.bounds, - Text.stringifiedTranslatable("firmament.recipe.forge.time", - display.neuRecipe.duration.seconds))) - val ingredientsCenter = Point(bounds.minX + 49 - 8, bounds.minY + 54 - 8) - add(Widgets.createBurningFire(ingredientsCenter).animationDurationTicks(25.0)) - val count = display.neuRecipe.inputs.size - if (count == 1) { - add( - Widgets.createSlot(Point(ingredientsCenter.x, ingredientsCenter.y)).markInput() - .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.inputs.single())) - ) - } else { - display.neuRecipe.inputs.forEachIndexed { idx, ingredient -> - val rad = Math.PI * 2 * idx / count - add( - Widgets.createSlot( - Point( - cos(rad) * 30, - sin(rad) * 30, - ) + ingredientsCenter - ).markInput().entry(SBItemEntryDefinition.getEntry(ingredient)) - ) - } - } - add( - Widgets.createSlot(Point(bounds.minX + 124, bounds.minY + 46)).markOutput().disableBackground() - .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.outputStack)) - ) - } - } - } - -} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt index b8313a6..c5b4fb6 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt @@ -19,6 +19,7 @@ import me.shedaniel.rei.api.common.entry.EntryIngredient import me.shedaniel.rei.api.common.entry.EntryStack import net.minecraft.entity.EntityType import net.minecraft.entity.SpawnReason +import net.minecraft.registry.entry.RegistryEntry import net.minecraft.text.Text import net.minecraft.util.Identifier import net.minecraft.village.VillagerProfession @@ -33,6 +34,7 @@ import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.SBItemStack import moe.nea.firmament.util.AprilFoolsUtil import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.gold import moe.nea.firmament.util.grey @@ -92,7 +94,7 @@ class SBReforgeRecipe( list.add(Widgets.withTooltip( EntityWidget( EntityType.VILLAGER.create(EntityRenderer.fakeWorld, SpawnReason.COMMAND) - ?.also { it.villagerData = it.villagerData.withProfession(VillagerProfession.WEAPONSMITH) }, + ?.also { it.villagerData = it.villagerData.withProfession(MC.currentOrDefaultRegistries.getEntryOrThrow(VillagerProfession.WEAPONSMITH)) }, Point(bounds.minX + 10 + 24 + 8 - dimension.width / 2, bounds.centerY - dimension.height / 2), dimension ), diff --git a/src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt b/src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt new file mode 100644 index 0000000..347dd5d --- /dev/null +++ b/src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt @@ -0,0 +1,13 @@ +package moe.nea.firmament.compat.gender + +import net.fabricmc.loader.api.FabricLoader +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return FabricLoader.getInstance().isModLoaded("wildfire_gender") + } + +} diff --git a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java index 0713068..a9db7f9 100644 --- a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java +++ b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java @@ -1,6 +1,9 @@ package moe.nea.firmament.init; +import moe.nea.firmament.util.ErrorUtil; +import moe.nea.firmament.util.compatloader.ICompatMeta; + import java.io.File; import java.io.IOException; import java.net.MalformedURLException; @@ -94,7 +97,7 @@ public class AutoDiscoveryPlugin { String norm = (className.substring(0, className.length() - ".class".length())) .replace("\\", "/") .replace("/", "."); - if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) { + if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".") && ICompatMeta.Companion.shouldLoad(norm)) { mixins.add(norm.substring(getMixinPackage().length() + 1)); } } @@ -125,24 +128,25 @@ public class AutoDiscoveryPlugin { */ public List<String> getMixins() { if (mixins != null) return mixins; - System.out.println("Trying to discover mixins"); - mixins = new ArrayList<>(); - URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); - System.out.println("Found classes at " + classUrl); - tryDiscoverFromContentFile(classUrl); - var classRoots = System.getProperty("firmament.classroots"); - if (classRoots != null && !classRoots.isBlank()) { - System.out.println("Found firmament class roots: " + classRoots); - for (String s : classRoots.split(File.pathSeparator)) { - if (s.isBlank()) { - continue; - } - try { + try { + System.out.println("Trying to discover mixins"); + mixins = new ArrayList<>(); + URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); + System.out.println("Found classes at " + classUrl); + tryDiscoverFromContentFile(classUrl); + var classRoots = System.getProperty("firmament.classroots"); + if (classRoots != null && !classRoots.isBlank()) { + System.out.println("Found firmament class roots: " + classRoots); + for (String s : classRoots.split(File.pathSeparator)) { + if (s.isBlank()) { + continue; + } tryDiscoverFromContentFile(new File(s).toURI().toURL()); - } catch (MalformedURLException e) { - throw new RuntimeException(e); } } + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); } return mixins; } diff --git a/src/main/java/moe/nea/firmament/init/MixinPlugin.java b/src/main/java/moe/nea/firmament/init/MixinPlugin.java index 61e8f14..d48139b 100644 --- a/src/main/java/moe/nea/firmament/init/MixinPlugin.java +++ b/src/main/java/moe/nea/firmament/init/MixinPlugin.java @@ -8,54 +8,69 @@ import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class MixinPlugin implements IMixinConfigPlugin { - AutoDiscoveryPlugin autoDiscoveryPlugin = new AutoDiscoveryPlugin(); - public static String mixinPackage; - @Override - public void onLoad(String mixinPackage) { - MixinExtrasBootstrap.init(); - MixinPlugin.mixinPackage = mixinPackage; - autoDiscoveryPlugin.setMixinPackage(mixinPackage); - } - - @Override - public String getRefMapperConfig() { - return null; - } - - @Override - public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { - if (!Boolean.getBoolean("firmament.debug") && mixinClassName.contains("devenv.")) { - return false; - } - return true; - } - - @Override - public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) { - - } - - @Override - public List<String> getMixins() { - return autoDiscoveryPlugin.getMixins().stream().filter(it -> this.shouldApplyMixin(null, it)) - .toList(); - } - - @Override - public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { - - } - - public static List<String> appliedMixins = new ArrayList<>(); - - @Override - public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { - appliedMixins.add(mixinClassName); - } + AutoDiscoveryPlugin autoDiscoveryPlugin = new AutoDiscoveryPlugin(); + public static List<MixinPlugin> instances = new ArrayList<>(); + public String mixinPackage; + + @Override + public void onLoad(String mixinPackage) { + MixinExtrasBootstrap.init(); + instances.add(this); + this.mixinPackage = mixinPackage; + autoDiscoveryPlugin.setMixinPackage(mixinPackage); + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + if (!Boolean.getBoolean("firmament.debug") && mixinClassName.contains("devenv.")) { + return false; + } + return true; + } + + @Override + public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) { + + } + + @Override + public List<String> getMixins() { + return autoDiscoveryPlugin.getMixins().stream().filter(it -> this.shouldApplyMixin(null, it)) + .toList(); + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + public Set<String> getAppliedFullPathMixins() { + return new HashSet<>(appliedMixins); + } + + public Set<String> getExpectedFullPathMixins() { + return getMixins() + .stream() + .map(it -> mixinPackage + "." + it) + .collect(Collectors.toSet()); + } + + public List<String> appliedMixins = new ArrayList<>(); + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + appliedMixins.add(mixinClassName); + } } diff --git a/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java b/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java index f2c6c53..8b65946 100644 --- a/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java +++ b/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java @@ -3,10 +3,9 @@ package moe.nea.firmament.init; import me.shedaniel.mm.api.ClassTinkerers; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.block.BlockState; -import net.minecraft.client.render.block.BlockModels; import net.minecraft.client.render.block.BlockRenderManager; import net.minecraft.client.render.chunk.SectionBuilder; -import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.BlockStateModel; import net.minecraft.util.math.BlockPos; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -20,98 +19,100 @@ import org.objectweb.asm.tree.VarInsnNode; public class SectionBuilderRiser extends RiserUtils { - @IntermediaryName(SectionBuilder.class) - String SectionBuilder; - @IntermediaryName(BlockPos.class) - String BlockPos; - @IntermediaryName(BlockRenderManager.class) - String BlockRenderManager; - @IntermediaryName(BlockState.class) - String BlockState; - @IntermediaryName(BakedModel.class) - String BakedModel; - String CustomBlockTextures = "moe.nea.firmament.features.texturepack.CustomBlockTextures"; + @IntermediaryName(SectionBuilder.class) + String SectionBuilder; + @IntermediaryName(BlockPos.class) + String BlockPos; + @IntermediaryName(BlockRenderManager.class) + String BlockRenderManager; + @IntermediaryName(BlockState.class) + String BlockState; + @IntermediaryName(BlockStateModel.class) + String BlockStateModel; + String CustomBlockTextures = "moe.nea.firmament.features.texturepack.CustomBlockTextures"; - Type getModelDesc = Type.getMethodType( - getTypeForClassName(BlockRenderManager), - getTypeForClassName(BlockState) - ); - String getModel = remapper.mapMethodName( - "intermediary", - Intermediary.<BlockRenderManager>className(), - Intermediary.methodName(net.minecraft.client.render.block.BlockRenderManager::getModel), - Type.getMethodDescriptor( - getTypeForClassName(Intermediary.<BakedModel>className()), - getTypeForClassName(Intermediary.<BlockState>className()) - ) - ); + Type getModelDesc = Type.getMethodType( + getTypeForClassName(BlockRenderManager), + getTypeForClassName(BlockState) + ); + String getModel = remapper.mapMethodName( + "intermediary", + Intermediary.<BlockRenderManager>className(), + Intermediary.methodName(net.minecraft.client.render.block.BlockRenderManager::getModel), + Type.getMethodDescriptor( + getTypeForClassName(Intermediary.<BlockStateModel>className()), + getTypeForClassName(Intermediary.<BlockState>className()) + ) + ); - @Override - public void addTinkerers() { - if (FabricLoader.getInstance().isModLoaded("fabric-renderer-indigo")) - ClassTinkerers.addTransformation(SectionBuilder, this::handle, true); - } + @Override + public void addTinkerers() { + if (FabricLoader.getInstance().isModLoaded("fabric-renderer-indigo")) + ClassTinkerers.addTransformation(SectionBuilder, this::handle, true); + } - private void handle(ClassNode classNode) { - for (MethodNode method : classNode.methods) { - if ((method.name.endsWith("$fabric-renderer-indigo$hookBuildRenderBlock") - || method.name.endsWith("$fabric-renderer-indigo$hookChunkBuildTessellate")) && - method.name.startsWith("redirect$")) { - handleIndigo(method); - return; - } - } - System.err.println("Could not inject indigo rendering hook. Is a custom renderer installed (e.g. sodium)?"); - } + private void handle(ClassNode classNode) { + System.out.println("AVAST! "+ getModel); + for (MethodNode method : classNode.methods) { + if ((method.name.endsWith("$fabric-renderer-indigo$hookBuildRenderBlock") + || method.name.endsWith("$fabric-renderer-indigo$hookChunkBuildTessellate")) && + method.name.startsWith("redirect$")) { + handleIndigo(method); + return; + } + } + System.err.println("Could not inject indigo rendering hook. Is a custom renderer installed (e.g. sodium)?"); + } - private void handleIndigo(MethodNode method) { - LocalVariableNode blockPosVar = null, blockStateVar = null; - for (LocalVariableNode localVariable : method.localVariables) { - if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockPos))) { - blockPosVar = localVariable; - } - if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockState))) { - blockStateVar = localVariable; - } - } - if (blockPosVar == null || blockStateVar == null) { - System.err.println("Firmament could inject into indigo: missing either block pos or blockstate"); - return; - } - for (AbstractInsnNode instruction : method.instructions) { - if (instruction.getOpcode() != Opcodes.INVOKEVIRTUAL) continue; - var methodInsn = (MethodInsnNode) instruction; - if (!(methodInsn.name.equals(getModel) && Type.getObjectType(methodInsn.owner).equals(getTypeForClassName(BlockRenderManager)))) - continue; - method.instructions.insertBefore( - methodInsn, - new MethodInsnNode( - Opcodes.INVOKESTATIC, - getTypeForClassName(CustomBlockTextures).getInternalName(), - "enterFallbackCall", - Type.getMethodDescriptor(Type.VOID_TYPE) - )); + private void handleIndigo(MethodNode method) { + LocalVariableNode blockPosVar = null, blockStateVar = null; + for (LocalVariableNode localVariable : method.localVariables) { + if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockPos))) { + blockPosVar = localVariable; + } + if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockState))) { + blockStateVar = localVariable; + } + } + if (blockPosVar == null || blockStateVar == null) { + System.err.println("Firmament could inject into indigo: missing either block pos or blockstate"); + return; + } + for (AbstractInsnNode instruction : method.instructions) { + if (instruction.getOpcode() != Opcodes.INVOKEVIRTUAL) continue; + var methodInsn = (MethodInsnNode) instruction; + if (!(methodInsn.name.equals(getModel) && Type.getObjectType(methodInsn.owner).equals(getTypeForClassName(BlockRenderManager)))) + continue; + method.instructions.insertBefore( + methodInsn, + new MethodInsnNode( + Opcodes.INVOKESTATIC, + getTypeForClassName(CustomBlockTextures).getInternalName(), + "enterFallbackCall", + Type.getMethodDescriptor(Type.VOID_TYPE) + )); - var insnList = new InsnList(); - insnList.add(new MethodInsnNode( - Opcodes.INVOKESTATIC, - getTypeForClassName(CustomBlockTextures).getInternalName(), - "exitFallbackCall", - Type.getMethodDescriptor(Type.VOID_TYPE) - )); - insnList.add(new VarInsnNode(Opcodes.ALOAD, blockPosVar.index)); - insnList.add(new VarInsnNode(Opcodes.ALOAD, blockStateVar.index)); - insnList.add(new MethodInsnNode( - Opcodes.INVOKESTATIC, - getTypeForClassName(CustomBlockTextures).getInternalName(), - "patchIndigo", - Type.getMethodDescriptor(getTypeForClassName(BakedModel), - getTypeForClassName(BakedModel), - getTypeForClassName(BlockPos), - getTypeForClassName(BlockState)), - false - )); - method.instructions.insert(methodInsn, insnList); - } - } + var insnList = new InsnList(); + insnList.add(new MethodInsnNode( + Opcodes.INVOKESTATIC, + getTypeForClassName(CustomBlockTextures).getInternalName(), + "exitFallbackCall", + Type.getMethodDescriptor(Type.VOID_TYPE) + )); + insnList.add(new VarInsnNode(Opcodes.ALOAD, blockPosVar.index)); + insnList.add(new VarInsnNode(Opcodes.ALOAD, blockStateVar.index)); + insnList.add(new MethodInsnNode( + Opcodes.INVOKESTATIC, + getTypeForClassName(CustomBlockTextures).getInternalName(), + "patchIndigo", + Type.getMethodDescriptor( + getTypeForClassName(BlockStateModel), + getTypeForClassName(BlockStateModel), + getTypeForClassName(BlockPos), + getTypeForClassName(BlockState)), + false + )); + method.instructions.insert(methodInsn, insnList); + } + } } diff --git a/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java index 22ce991..d8e35d7 100644 --- a/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java +++ b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java @@ -1,28 +1,34 @@ package moe.nea.firmament.mixins; +import com.llamalad7.mixinextras.sugar.Local; import moe.nea.firmament.repo.RepoModResourcePack; import net.fabricmc.fabric.api.resource.ModResourcePack; +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackSorter; import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.resource.ResourceType; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.List; @Mixin(ModResourcePackUtil.class) public class AppendRepoAsResourcePack { - @Inject(method = "appendModResourcePacks", at = @At("TAIL")) - private static void onAppendModResourcePack( - List<ModResourcePack> packs, - ResourceType type, - @Nullable String subPath, - CallbackInfo ci - ) { - RepoModResourcePack.Companion.append(packs); - } + @Inject( + method = "getModResourcePacks", + at = @At(value = "INVOKE", target = "Lnet/fabricmc/fabric/impl/resource/loader/ModResourcePackSorter;getPacks()Ljava/util/List;"), + require = 0 + ) + private static void onAppendModResourcePack( + FabricLoader fabricLoader, ResourceType type, @Nullable String subPath, CallbackInfoReturnable<List<ModResourcePack>> cir, + @Local ModResourcePackSorter sorter + ) { + RepoModResourcePack.Companion.append(sorter); + } } diff --git a/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java b/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java new file mode 100644 index 0000000..ed7a2d4 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java @@ -0,0 +1,18 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.client.render.GameRenderer; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GameRenderer.class) +public class DisableHurtCam { + @ModifyExpressionValue(method = "tiltViewWhenHurt", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/LivingEntity;hurtTime:I", opcode = Opcodes.GETFIELD)) + private int replaceHurtTime(int original) { + if (Fixes.TConfig.INSTANCE.getNoHurtCam()) + return 0; + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java index c2d6e46..d956da9 100644 --- a/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java +++ b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java @@ -12,6 +12,7 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.LivingEntity; import net.minecraft.network.ClientConnection; import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket; +import net.minecraft.network.packet.s2c.play.EntityEquipmentUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -22,21 +23,26 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ClientPlayNetworkHandler.class) public abstract class EntityUpdateEventListener extends ClientCommonNetworkHandler { - @Shadow - private ClientWorld world; + @Shadow + private ClientWorld world; - protected EntityUpdateEventListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) { - super(client, connection, connectionState); - } + protected EntityUpdateEventListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) { + super(client, connection, connectionState); + } - @Inject(method = "onEntityAttributes", at = @At("TAIL")) - private void onAttributeUpdate(EntityAttributesS2CPacket packet, CallbackInfo ci) { - EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.AttributeUpdate( - (LivingEntity) world.getEntityById(packet.getEntityId()), packet.getEntries())); - } + @Inject(method = "onEntityEquipmentUpdate", at = @At(value = "INVOKE", target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V", shift = At.Shift.AFTER)) + private void onEquipmentUpdate(EntityEquipmentUpdateS2CPacket packet, CallbackInfo ci, @Local LivingEntity entity) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.EquipmentUpdate(entity, packet.getEquipmentList())); + } - @Inject(method = "onEntityTrackerUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/data/DataTracker;writeUpdatedEntries(Ljava/util/List;)V", shift = At.Shift.AFTER)) - private void onEntityTracker(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) { - EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.TrackedDataUpdate(entity, packet.trackedValues())); - } + @Inject(method = "onEntityAttributes", at = @At("TAIL")) + private void onAttributeUpdate(EntityAttributesS2CPacket packet, CallbackInfo ci) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.AttributeUpdate( + (LivingEntity) world.getEntityById(packet.getEntityId()), packet.getEntries())); + } + + @Inject(method = "onEntityTrackerUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/data/DataTracker;writeUpdatedEntries(Ljava/util/List;)V", shift = At.Shift.AFTER)) + private void onEntityTracker(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.TrackedDataUpdate(entity, packet.trackedValues())); + } } diff --git a/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java b/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java index 48f3c23..d2b3f91 100644 --- a/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java @@ -2,18 +2,19 @@ package moe.nea.firmament.mixins; +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; import moe.nea.firmament.events.WorldKeyboardEvent; import net.minecraft.client.Keyboard; +import net.minecraft.client.util.InputUtil; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(Keyboard.class) public class KeyPressInWorldEventPatch { - @Inject(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V")) - public void onKeyBoardInWorld(long window, int key, int scancode, int action, int modifiers, CallbackInfo ci) { - WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(key, scancode, modifiers)); - } + @WrapWithCondition(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V")) + public boolean onKeyBoardInWorld(InputUtil.Key key, long window, int _key, int scancode, int action, int modifiers) { + var event = WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(_key, scancode, modifiers)); + return !event.getCancelled(); + } } diff --git a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java index b20c223..f07604e 100644 --- a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java @@ -20,7 +20,7 @@ public abstract class PlayerDropEventPatch extends PlayerEntity { @Inject(method = "dropSelectedItem", at = @At("HEAD"), cancellable = true) public void onDropSelectedItem(boolean entireStack, CallbackInfoReturnable<Boolean> cir) { - Slot fakeSlot = new Slot(getInventory(), getInventory().selectedSlot, 0, 0); + Slot fakeSlot = new Slot(getInventory(), getInventory().getSelectedSlot(), 0, 0); if (IsSlotProtectedEvent.shouldBlockInteraction(fakeSlot, SlotActionType.THROW, IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR)) { cir.setReturnValue(false); } diff --git a/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java index 06ecbd4..a4ae931 100644 --- a/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java +++ b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java @@ -43,11 +43,11 @@ public abstract class SlotUpdateListener extends ClientCommonNetworkHandler { private void onMultiSlotUpdate(InventoryS2CPacket packet, CallbackInfo ci) { var player = this.client.player; assert player != null; - if (packet.getSyncId() == 0) { - PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.getContents())); - } else if (packet.getSyncId() == player.currentScreenHandler.syncId) { + if (packet.syncId() == 0) { + PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.contents())); + } else if (packet.syncId() == player.currentScreenHandler.syncId) { ChestInventoryUpdateEvent.Companion.publish( - new ChestInventoryUpdateEvent.Multi(packet.getContents()) + new ChestInventoryUpdateEvent.Multi(packet.contents()) ); } } diff --git a/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java b/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java index 5c52d70..b8cba80 100644 --- a/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java @@ -1,30 +1,32 @@ package moe.nea.firmament.mixins; +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; import moe.nea.firmament.events.SoundReceiveEvent; import net.minecraft.client.network.ClientPlayNetworkHandler; -import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ClientPlayNetworkHandler.class) public class SoundReceiveEventPatch { - @Inject(method = "onPlaySound", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;playSound(Lnet/minecraft/entity/player/PlayerEntity;DDDLnet/minecraft/registry/entry/RegistryEntry;Lnet/minecraft/sound/SoundCategory;FFJ)V"), cancellable = true) - private void postEventWhenSoundIsPlayed(PlaySoundS2CPacket packet, CallbackInfo ci) { + @WrapWithCondition(method = "onPlaySound", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;playSound(Lnet/minecraft/entity/Entity;DDDLnet/minecraft/registry/entry/RegistryEntry;Lnet/minecraft/sound/SoundCategory;FFJ)V")) + private boolean postEventWhenSoundIsPlayed(ClientWorld instance, @Nullable Entity source, double x, double y, double z, RegistryEntry<SoundEvent> sound, SoundCategory category, float volume, float pitch, long seed) { var event = new SoundReceiveEvent( - packet.getSound(), - packet.getCategory(), - new Vec3d(packet.getX(), packet.getY(), packet.getZ()), - packet.getPitch(), - packet.getVolume(), - packet.getSeed() + sound, + category, + new Vec3d(x,y,z), + pitch, + volume, + seed ); SoundReceiveEvent.Companion.publish(event); - if (event.getCancelled()) { - ci.cancel(); - } + return !event.getCancelled(); } } diff --git a/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java b/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java index 847fb4d..3ed8c1b 100644 --- a/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java @@ -2,11 +2,9 @@ package moe.nea.firmament.mixins; -import com.llamalad7.mixinextras.sugar.Local; import moe.nea.firmament.events.WorldRenderLastEvent; import net.minecraft.client.render.*; import net.minecraft.client.util.Handle; -import net.minecraft.client.util.ObjectAllocator; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.util.profiler.Profiler; import org.joml.Matrix4f; @@ -31,12 +29,12 @@ public abstract class WorldRenderLastEventPatch { protected abstract void checkEmpty(MatrixStack matrices); @Inject(method = "method_62214", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiler/Profiler;pop()V", shift = At.Shift.AFTER)) - public void onWorldRenderLast(Fog fog, RenderTickCounter tickCounter, Camera camera, Profiler profiler, Matrix4f matrix4f, Matrix4f matrix4f2, Handle handle, Handle handle2, Handle handle3, Handle handle4, boolean bl, Frustum frustum, Handle handle5, CallbackInfo ci) { + public void onWorldRenderLast(Fog fog, RenderTickCounter renderTickCounter, Camera camera, Profiler profiler, Matrix4f matrix4f, Matrix4f matrix4f2, Handle handle, Handle handle2, boolean bl, Frustum frustum, Handle handle3, Handle handle4, CallbackInfo ci) { var imm = this.bufferBuilders.getEntityVertexConsumers(); var stack = new MatrixStack(); // TODO: pre-cancel this event if F1 is active var event = new WorldRenderLastEvent( - stack, tickCounter, + stack, renderTickCounter, camera, imm ); diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java new file mode 100644 index 0000000..8b25562 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java @@ -0,0 +1,17 @@ +package moe.nea.firmament.mixins.accessor; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.entity.player.BlockBreakingInfo; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.SortedSet; + +@Mixin(WorldRenderer.class) +public interface AccessorWorldRenderer { + @Accessor("blockBreakingProgressions") + @NotNull + Long2ObjectMap<SortedSet<BlockBreakingInfo>> getBlockBreakingProgressions_firmament(); +} diff --git a/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java b/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java new file mode 100644 index 0000000..0abed22 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.feature; + +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Slot.class) +public abstract class DisableSlotHighlights { + @Shadow + public abstract ItemStack getStack(); + + @Inject(method = "canBeHighlighted", at = @At("HEAD"), cancellable = true) + private void dontHighlight(CallbackInfoReturnable<Boolean> cir) { + if (!Fixes.TConfig.INSTANCE.getHideSlotHighlights()) return; + var display = getStack().get(DataComponentTypes.TOOLTIP_DISPLAY); + if (display != null && display.hideTooltip()) + cir.setReturnValue(false); + } +} diff --git a/src/main/kotlin/Compat.kt b/src/main/kotlin/Compat.kt new file mode 100644 index 0000000..ba3c88d --- /dev/null +++ b/src/main/kotlin/Compat.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament + +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return true + } +} diff --git a/src/main/kotlin/Firmament.kt b/src/main/kotlin/Firmament.kt index 01905c7..7bc7d44 100644 --- a/src/main/kotlin/Firmament.kt +++ b/src/main/kotlin/Firmament.kt @@ -19,11 +19,14 @@ import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents +import net.fabricmc.fabric.api.resource.ResourceManagerHelper +import net.fabricmc.fabric.api.resource.ResourcePackActivationType import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.Version import net.fabricmc.loader.api.metadata.ModMetadata import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger +import org.spongepowered.asm.launch.MixinBootstrap import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -49,6 +52,7 @@ import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.util.MC import moe.nea.firmament.util.SBData import moe.nea.firmament.util.data.IDataHolder +import moe.nea.firmament.util.tr object Firmament { val modContainer by lazy { FabricLoader.getInstance().getModContainer(MOD_ID).get() } @@ -117,9 +121,8 @@ object Firmament { @JvmStatic fun onClientInitialize() { FeatureManager.subscribeEvents() - var tick = 0 ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance -> - TickEvent.publish(TickEvent(tick++)) + TickEvent.publish(TickEvent(MC.currentTick++)) }) IDataHolder.registerEvents() RepoManager.initialize() @@ -145,6 +148,12 @@ object Firmament { }) }) ClientInitEvent.publish(ClientInitEvent()) + ResourceManagerHelper.registerBuiltinResourcePack( + identifier("transparent_overlay"), + modContainer, + tr("firmament.resourcepack.transparentoverlay", "Transparent Firmament Overlay"), + ResourcePackActivationType.NORMAL + ) } diff --git a/src/main/kotlin/apis/Profiles.kt b/src/main/kotlin/apis/Profiles.kt index 789364a..156de89 100644 --- a/src/main/kotlin/apis/Profiles.kt +++ b/src/main/kotlin/apis/Profiles.kt @@ -188,7 +188,7 @@ data class PlayerData( } @Serializable -data class AshconNameLookup( - val username: String, - val uuid: UUID, +data class MowojangNameLookup( + val name: String, + val id: UUID, ) diff --git a/src/main/kotlin/apis/Routes.kt b/src/main/kotlin/apis/Routes.kt index bf55a2d..5e29402 100644 --- a/src/main/kotlin/apis/Routes.kt +++ b/src/main/kotlin/apis/Routes.kt @@ -28,13 +28,13 @@ object Routes { return withContext(MinecraftDispatcher) { UUIDToName.computeIfAbsent(uuid) { async(Firmament.coroutineScope.coroutineContext) { - val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$uuid") + val response = Firmament.httpClient.get("https://mowojang.matdoes.dev/$uuid") if (!response.status.isSuccess()) return@async null - val data = response.body<AshconNameLookup>() + val data = response.body<MowojangNameLookup>() launch(MinecraftDispatcher) { - nameToUUID[data.username] = async { data.uuid } + nameToUUID[data.name] = async { data.id } } - data.username + data.name } } }.await() @@ -44,13 +44,13 @@ object Routes { return withContext(MinecraftDispatcher) { nameToUUID.computeIfAbsent(name) { async(Firmament.coroutineScope.coroutineContext) { - val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$name") + val response = Firmament.httpClient.get("https://mowojang.matdoes.dev/$name") if (!response.status.isSuccess()) return@async null - val data = response.body<AshconNameLookup>() + val data = response.body<MowojangNameLookup>() launch(MinecraftDispatcher) { - UUIDToName[data.uuid] = async { data.username } + UUIDToName[data.id] = async { data.name } } - data.uuid + data.id } } }.await() diff --git a/src/main/kotlin/commands/rome.kt b/src/main/kotlin/commands/rome.kt index 5412792..9fc5386 100644 --- a/src/main/kotlin/commands/rome.kt +++ b/src/main/kotlin/commands/rome.kt @@ -1,6 +1,7 @@ package moe.nea.firmament.commands import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.arguments.IntegerArgumentType import com.mojang.brigadier.arguments.StringArgumentType.string import io.ktor.client.statement.bodyAsText import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource @@ -15,6 +16,7 @@ import moe.nea.firmament.features.debug.PowerUserTools import moe.nea.firmament.features.inventory.buttons.InventoryButtons import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen import moe.nea.firmament.features.inventory.storageoverlay.StorageOverviewScreen +import moe.nea.firmament.features.mining.MiningBlockInfoUi import moe.nea.firmament.gui.config.AllConfigsGui import moe.nea.firmament.gui.config.BooleanHandler import moe.nea.firmament.gui.config.ManagedConfig @@ -129,6 +131,15 @@ fun firmamentCommand() = literal("firmament") { } } thenLiteral("repo") { + thenLiteral("checkpr") { + thenArgument("prnum", IntegerArgumentType.integer(1)) { prnum -> + thenExecute { + val prnum = this[prnum] + source.sendFeedback(tr("firmament.repo.reload.pr", "Temporarily reloading repo from PR #${prnum}.")) + RepoManager.downloadOverridenBranch("refs/pull/$prnum/head") + } + } + } thenLiteral("reload") { thenLiteral("fetch") { thenExecute { @@ -217,6 +228,20 @@ fun firmamentCommand() = literal("firmament") { } } } + thenLiteral("screens") { + thenExecute { + MC.sendChat(Text.literal(""" + |Screen: ${MC.screen} (${MC.screen?.title}) + |Screen Handler: ${MC.handledScreen?.screenHandler} ${MC.handledScreen?.screenHandler?.syncId} + |Player Screen Handler: ${MC.player?.currentScreenHandler} ${MC.player?.currentScreenHandler?.syncId} + """.trimMargin())) + } + } + thenLiteral("blocks") { + thenExecute { + ScreenUtil.setScreenLater(MiningBlockInfoUi.makeScreen()) + } + } thenLiteral("dumpchat") { thenExecute { MC.inGameHud.chatHud.messages.forEach { @@ -246,6 +271,8 @@ fun firmamentCommand() = literal("firmament") { source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.gametype", locrawInfo.gametype)) source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.mode", locrawInfo.mode)) source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.map", locrawInfo.map)) + source.sendFeedback(tr("firmament.sbinfo.custommining", + "Custom Mining: ${formatBool(locrawInfo.skyblockLocation?.hasCustomMining ?: false)}")) } } } @@ -296,13 +323,15 @@ fun firmamentCommand() = literal("firmament") { } thenLiteral("mixins") { thenExecute { - source.sendFeedback(Text.translatable("firmament.mixins.start")) - MixinPlugin.appliedMixins - .map { it.removePrefix(MixinPlugin.mixinPackage) } - .forEach { - source.sendFeedback(Text.literal(" - ").withColor(0xD020F0) - .append(Text.literal(it).withColor(0xF6BA20))) - } + MixinPlugin.instances.forEach { plugin -> + source.sendFeedback(tr("firmament.mixins.start.package", "Mixins (base ${plugin.mixinPackage}):")) + plugin.appliedMixins + .map { it.removePrefix(plugin.mixinPackage) } + .forEach { + source.sendFeedback(Text.literal(" - ").withColor(0xD020F0) + .append(Text.literal(it).withColor(0xF6BA20))) + } + } } } thenLiteral("repo") { diff --git a/src/main/kotlin/events/CustomItemModelEvent.kt b/src/main/kotlin/events/CustomItemModelEvent.kt index 21ee326..7b86980 100644 --- a/src/main/kotlin/events/CustomItemModelEvent.kt +++ b/src/main/kotlin/events/CustomItemModelEvent.kt @@ -1,10 +1,13 @@ package moe.nea.firmament.events +import java.util.Objects import java.util.Optional import kotlin.jvm.optionals.getOrNull +import net.minecraft.component.DataComponentTypes import net.minecraft.item.ItemStack import net.minecraft.util.Identifier import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.collections.WeakCache.CacheFunction import moe.nea.firmament.util.mc.IntrospectableItemModelManager // TODO: assert an order on these events @@ -14,7 +17,36 @@ data class CustomItemModelEvent( var overrideModel: Identifier? = null, ) : FirmamentEvent() { companion object : FirmamentEventBus<CustomItemModelEvent>() { - val cache = WeakCache.memoize("ItemModelIdentifier", ::getModelIdentifier0) + val weakCache = + object : WeakCache<ItemStack, IntrospectableItemModelManager, Optional<Identifier>>("ItemModelIdentifier") { + override fun mkRef( + key: ItemStack, + extraData: IntrospectableItemModelManager + ): WeakCache<ItemStack, IntrospectableItemModelManager, Optional<Identifier>>.Ref { + return IRef(key, extraData) + } + + inner class IRef(weakInstance: ItemStack, data: IntrospectableItemModelManager) : + Ref(weakInstance, data) { + override fun shouldBeEvicted(): Boolean = false + val isSimpleStack = weakInstance.componentChanges.isEmpty || (weakInstance.componentChanges.size() == 1 && weakInstance.get( + DataComponentTypes.CUSTOM_DATA)?.isEmpty == true) + val item = weakInstance.item + override fun hashCode(): Int { + if (isSimpleStack) + return Objects.hash(item, extraData) + return super.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other is IRef && isSimpleStack) { + return other.isSimpleStack && item == other.item + } + return super.equals(other) + } + } + } + val cache = CacheFunction.WithExtraData(weakCache, ::getModelIdentifier0) @JvmStatic fun getModelIdentifier(itemStack: ItemStack?, itemModelManager: IntrospectableItemModelManager): Identifier? { diff --git a/src/main/kotlin/events/EntityUpdateEvent.kt b/src/main/kotlin/events/EntityUpdateEvent.kt index d091984..fec2fa5 100644 --- a/src/main/kotlin/events/EntityUpdateEvent.kt +++ b/src/main/kotlin/events/EntityUpdateEvent.kt @@ -1,10 +1,14 @@ - package moe.nea.firmament.events +import com.mojang.datafixers.util.Pair import net.minecraft.entity.Entity +import net.minecraft.entity.EquipmentSlot import net.minecraft.entity.LivingEntity import net.minecraft.entity.data.DataTracker +import net.minecraft.item.ItemStack import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.util.MC /** * This event is fired when some entity properties are updated. @@ -13,19 +17,44 @@ import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket * *after* the values have been applied to the entity. */ sealed class EntityUpdateEvent : FirmamentEvent() { - companion object : FirmamentEventBus<EntityUpdateEvent>() + companion object : FirmamentEventBus<EntityUpdateEvent>() { + @Subscribe + fun onPlayerInventoryUpdate(event: PlayerInventoryUpdate) { + val p = MC.player ?: return + val updatedSlots = listOf( + EquipmentSlot.HEAD to 39, + EquipmentSlot.CHEST to 38, + EquipmentSlot.LEGS to 37, + EquipmentSlot.FEET to 36, + EquipmentSlot.OFFHAND to 40, + EquipmentSlot.MAINHAND to p.inventory.selectedSlot, // TODO: also equipment update when you swap your selected slot perhaps + ).mapNotNull { (slot, stackIndex) -> + val slotIndex = p.playerScreenHandler.getSlotIndex(p.inventory, stackIndex).asInt + event.getOrNull(slotIndex)?.let { + Pair.of(slot, it) + } + } + if (updatedSlots.isNotEmpty()) + publish(EquipmentUpdate(p, updatedSlots)) + } + } + + abstract val entity: Entity - abstract val entity: Entity + data class AttributeUpdate( + override val entity: LivingEntity, + val attributes: List<EntityAttributesS2CPacket.Entry>, + ) : EntityUpdateEvent() - data class AttributeUpdate( - override val entity: LivingEntity, - val attributes: List<EntityAttributesS2CPacket.Entry>, - ) : EntityUpdateEvent() + data class TrackedDataUpdate( + override val entity: Entity, + val trackedValues: List<DataTracker.SerializedEntry<*>>, + ) : EntityUpdateEvent() - data class TrackedDataUpdate( - override val entity: Entity, - val trackedValues: List<DataTracker.SerializedEntry<*>>, - ) : EntityUpdateEvent() + data class EquipmentUpdate( + override val entity: Entity, + val newEquipment: List<Pair<EquipmentSlot, ItemStack>>, + ) : EntityUpdateEvent() -// TODO: onEntityPassengersSet, onEntityAttach?, onEntityEquipmentUpdate, onEntityStatusEffect +// TODO: onEntityPassengersSet, onEntityAttach?, onEntityStatusEffect } diff --git a/src/main/kotlin/events/IsSlotProtectedEvent.kt b/src/main/kotlin/events/IsSlotProtectedEvent.kt index eac2d9b..8fe0a96 100644 --- a/src/main/kotlin/events/IsSlotProtectedEvent.kt +++ b/src/main/kotlin/events/IsSlotProtectedEvent.kt @@ -6,6 +6,10 @@ import net.minecraft.screen.slot.SlotActionType import net.minecraft.text.Text import moe.nea.firmament.util.CommonSoundEffects import moe.nea.firmament.util.MC +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.hover +import moe.nea.firmament.util.red +import moe.nea.firmament.util.tr data class IsSlotProtectedEvent( val slot: Slot?, @@ -35,6 +39,7 @@ data class IsSlotProtectedEvent( INVENTORY_MOVE ; } + companion object : FirmamentEventBus<IsSlotProtectedEvent>() { @JvmStatic @JvmOverloads @@ -47,7 +52,11 @@ data class IsSlotProtectedEvent( val event = IsSlotProtectedEvent(slot, action, false, itemStackOverride, origin) publish(event) if (event.isProtected && !event.silent) { - MC.sendChat(Text.translatable("firmament.protectitem").append(event.itemStack.name)) + MC.sendChat(tr("firmament.protectitem", "Firmament protected your item: ${event.itemStack.name}.\n") + .red() + .append(tr("firmament.protectitem.hoverhint", "Hover for more info.").grey()) + .hover(tr("firmament.protectitem.hint", + "To unlock this item use the Lock Slot or Lock Item keybind from Firmament while hovering over this item."))) CommonSoundEffects.playFailure() } return event.isProtected diff --git a/src/main/kotlin/events/PlayerInventoryUpdate.kt b/src/main/kotlin/events/PlayerInventoryUpdate.kt index 6e8203a..88439a9 100644 --- a/src/main/kotlin/events/PlayerInventoryUpdate.kt +++ b/src/main/kotlin/events/PlayerInventoryUpdate.kt @@ -1,11 +1,22 @@ - package moe.nea.firmament.events import net.minecraft.item.ItemStack sealed class PlayerInventoryUpdate : FirmamentEvent() { - companion object : FirmamentEventBus<PlayerInventoryUpdate>() - data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate() - data class Multi(val contents: List<ItemStack>) : PlayerInventoryUpdate() + companion object : FirmamentEventBus<PlayerInventoryUpdate>() + data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate() { + override fun getOrNull(slot: Int): ItemStack? { + if (slot == this.slot) return stack + return null + } + + } + + data class Multi(val contents: List<ItemStack>) : PlayerInventoryUpdate() { + override fun getOrNull(slot: Int): ItemStack? { + return contents.getOrNull(slot) + } + } + abstract fun getOrNull(slot: Int): ItemStack? } diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt index 9a3cbf8..f0c1857 100644 --- a/src/main/kotlin/features/FeatureManager.kt +++ b/src/main/kotlin/features/FeatureManager.kt @@ -31,89 +31,96 @@ import moe.nea.firmament.features.mining.PickaxeAbility import moe.nea.firmament.features.mining.PristineProfitTracker import moe.nea.firmament.features.world.FairySouls import moe.nea.firmament.features.world.Waypoints +import moe.nea.firmament.util.compatloader.ICompatMeta import moe.nea.firmament.util.data.DataHolder object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) { - @Serializable - data class Config( - val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf() - ) + @Serializable + data class Config( + val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf() + ) - private val features = mutableMapOf<String, FirmamentFeature>() + private val features = mutableMapOf<String, FirmamentFeature>() - val allFeatures: Collection<FirmamentFeature> get() = features.values + val allFeatures: Collection<FirmamentFeature> get() = features.values - private var hasAutoloaded = false + private var hasAutoloaded = false - fun autoload() { - synchronized(this) { - if (hasAutoloaded) return - loadFeature(MinorTrolling) - loadFeature(FairySouls) - loadFeature(AutoCompletions) - // TODO: loadFeature(FishingWarning) - loadFeature(SlotLocking) - loadFeature(StorageOverlay) - loadFeature(PristineProfitTracker) - loadFeature(CraftingOverlay) - loadFeature(PowerUserTools) - loadFeature(Waypoints) - loadFeature(ChatLinks) - loadFeature(InventoryButtons) - loadFeature(CompatibliltyFeatures) - loadFeature(AnniversaryFeatures) - loadFeature(QuickCommands) - loadFeature(PetFeatures) - loadFeature(SaveCursorPosition) - loadFeature(PriceData) - loadFeature(Fixes) - loadFeature(DianaWaypoints) - loadFeature(ItemRarityCosmetics) - loadFeature(PickaxeAbility) - loadFeature(CarnivalFeatures) - if (Firmament.DEBUG) { - loadFeature(DeveloperFeatures) - loadFeature(DebugView) - } - allFeatures.forEach { it.config } - FeaturesInitializedEvent.publish(FeaturesInitializedEvent(allFeatures.toList())) - hasAutoloaded = true - } - } + fun autoload() { + synchronized(this) { + if (hasAutoloaded) return + loadFeature(MinorTrolling) + loadFeature(FairySouls) + loadFeature(AutoCompletions) + // TODO: loadFeature(FishingWarning) + loadFeature(SlotLocking) + loadFeature(StorageOverlay) + loadFeature(PristineProfitTracker) + loadFeature(CraftingOverlay) + loadFeature(PowerUserTools) + loadFeature(Waypoints) + loadFeature(ChatLinks) + loadFeature(InventoryButtons) + loadFeature(CompatibliltyFeatures) + loadFeature(AnniversaryFeatures) + loadFeature(QuickCommands) + loadFeature(PetFeatures) + loadFeature(SaveCursorPosition) + loadFeature(PriceData) + loadFeature(Fixes) + loadFeature(DianaWaypoints) + loadFeature(ItemRarityCosmetics) + loadFeature(PickaxeAbility) + loadFeature(CarnivalFeatures) + if (Firmament.DEBUG) { + loadFeature(DeveloperFeatures) + loadFeature(DebugView) + } + allFeatures.forEach { it.config } + FeaturesInitializedEvent.publish(FeaturesInitializedEvent(allFeatures.toList())) + hasAutoloaded = true + } + } - fun subscribeEvents() { - SubscriptionList.allLists.forEach { - it.provideSubscriptions { - it.owner.javaClass.classes.forEach { - runCatching { it.getDeclaredField("INSTANCE").get(null) } + fun subscribeEvents() { + SubscriptionList.allLists.forEach { list -> + if (ICompatMeta.shouldLoad(list.javaClass.name)) + runCatching { + list.provideSubscriptions { + it.owner.javaClass.classes.forEach { + runCatching { it.getDeclaredField("INSTANCE").get(null) } + } + subscribeSingleEvent(it) + } + }.getOrElse { + // TODO: allow annotating source sets to specifically opt out of loading for mods, maybe automatically + Firmament.logger.info("Ignoring events from $list, likely due to a missing compat mod.", it) } - subscribeSingleEvent(it) - } - } - } + } + } - private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) { - it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke) - } + private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) { + it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke) + } - fun loadFeature(feature: FirmamentFeature) { - synchronized(features) { - if (feature.identifier in features) { - Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature") - return - } - features[feature.identifier] = feature - feature.onLoad() - } - } + fun loadFeature(feature: FirmamentFeature) { + synchronized(features) { + if (feature.identifier in features) { + Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature") + return + } + features[feature.identifier] = feature + feature.onLoad() + } + } - fun isEnabled(identifier: String): Boolean? = - data.enabledFeatures[identifier] + fun isEnabled(identifier: String): Boolean? = + data.enabledFeatures[identifier] - fun setEnabled(identifier: String, value: Boolean) { - data.enabledFeatures[identifier] = value - markDirty() - } + fun setEnabled(identifier: String, value: Boolean) { + data.enabledFeatures[identifier] = value + markDirty() + } } diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt index f85825b..a084234 100644 --- a/src/main/kotlin/features/chat/ChatLinks.kt +++ b/src/main/kotlin/features/chat/ChatLinks.kt @@ -3,6 +3,7 @@ package moe.nea.firmament.features.chat import io.ktor.client.request.get import io.ktor.client.statement.bodyAsChannel import io.ktor.utils.io.jvm.javaio.toInputStream +import java.net.URI import java.net.URL import java.util.Collections import java.util.concurrent.atomic.AtomicInteger @@ -78,7 +79,7 @@ object ChatLinks : FirmamentFeature { val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}") MC.textureManager.registerTexture( texId, - NativeImageBackedTexture(image) + NativeImageBackedTexture({ texId.path }, image) ) Image(texId, image.width, image.height) } else @@ -102,8 +103,8 @@ object ChatLinks : FirmamentFeature { if (it.screen !is ChatScreen) return val hoveredComponent = MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return - val hoverEvent = hoveredComponent.hoverEvent ?: return - val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return + val hoverEvent = hoveredComponent.hoverEvent as? HoverEvent.ShowText ?: return + val value = hoverEvent.value val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return if (!isImageUrl(url)) return val imageFuture = imageCache[url] ?: return @@ -149,8 +150,8 @@ object ChatLinks : FirmamentFeature { Text.literal(url).setStyle( Style.EMPTY.withUnderline(true).withColor( Formatting.AQUA - ).withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(url))) - .withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url)) + ).withHoverEvent(HoverEvent.ShowText(Text.literal(url))) + .withClickEvent(ClickEvent.OpenUrl(URI(url))) ) ) if (isImageUrl(url)) diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt new file mode 100644 index 0000000..9f9f135 --- /dev/null +++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt @@ -0,0 +1,193 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.command.argument.RegistryKeyArgumentType +import net.minecraft.component.ComponentType +import net.minecraft.entity.Entity +import net.minecraft.entity.decoration.ArmorStandEntity +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtOps +import net.minecraft.registry.RegistryKeys +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.EntityUpdateEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.math.GChainReconciliation +import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle +import moe.nea.firmament.util.mc.NbtPrism +import moe.nea.firmament.util.tr + +object AnimatedClothingScanner { + + data class LensOfFashionTheft<T>( + val prism: NbtPrism, + val component: ComponentType<T>, + ) { + fun observe(itemStack: ItemStack): Collection<NbtElement> { + val x = itemStack.get(component) ?: return listOf() + val nbt = component.codecOrThrow.encodeStart(NbtOps.INSTANCE, x).orThrow + return prism.access(nbt) + } + } + + var lens: LensOfFashionTheft<*>? = null + var subject: Entity? = null + var history: MutableList<String> = mutableListOf() + val metaHistory: MutableList<List<String>> = mutableListOf() + + @OptIn(ExperimentalStdlibApi::class) + @Subscribe + fun onUpdate(event: EntityUpdateEvent) { + val s = subject ?: return + if (event.entity != s) return + val l = lens ?: return + if (event is EntityUpdateEvent.EquipmentUpdate) { + event.newEquipment.forEach { + val formatted = (l.observe(it.second)).joinToString() + history.add(formatted) + // TODO: add a slot filter + } + } + } + + fun reduceHistory(reducer: (List<String>, List<String>) -> List<String>): List<String> { + return metaHistory.fold(history, reducer).shortenCycle() + } + + @Subscribe + fun onSubCommand(event: CommandEvent.SubCommand) { + event.subcommand("dev") { + thenLiteral("stealthisfit") { + thenLiteral("clear") { + thenExecute { + subject = null + metaHistory.clear() + history.clear() + MC.sendChat(tr("firmament.fitstealer.clear", "Cleared fit stealing history")) + } + } + thenLiteral("copy") { + thenExecute { + val history = reduceHistory { a, b -> a + b } + copyHistory(history) + MC.sendChat(tr("firmament.fitstealer.copied", "Copied the history")) + } + thenLiteral("deduplicated") { + thenExecute { + val history = reduceHistory { a, b -> + (a.toMutableSet() + b).toList() + } + copyHistory(history) + MC.sendChat( + tr( + "firmament.fitstealer.copied.deduplicated", + "Copied the deduplicated history" + ) + ) + } + } + thenLiteral("merged") { + thenExecute { + val history = reduceHistory(GChainReconciliation::reconcileCycles) + copyHistory(history) + MC.sendChat(tr("firmament.fitstealer.copied.merged", "Copied the merged history")) + } + } + } + thenLiteral("target") { + thenLiteral("self") { + thenExecute { + toggleObserve(MC.player!!) + } + } + thenLiteral("pet") { + thenExecute { + source.sendFeedback( + tr( + "firmament.fitstealer.stealingpet", + "Observing nearest marker armourstand" + ) + ) + val p = MC.player!! + val nearestPet = p.world.getEntitiesByClass( + ArmorStandEntity::class.java, + p.boundingBox.expand(10.0), + { it.isMarker }) + .minBy { it.squaredDistanceTo(p) } + toggleObserve(nearestPet) + } + } + thenExecute { + val ent = MC.instance.targetedEntity + if (ent == null) { + source.sendFeedback( + tr( + "firmament.fitstealer.notargetundercursor", + "No entity under cursor" + ) + ) + } else { + toggleObserve(ent) + } + } + } + thenLiteral("path") { + thenArgument( + "component", + RegistryKeyArgumentType.registryKey(RegistryKeys.DATA_COMPONENT_TYPE) + ) { component -> + thenArgument("path", NbtPrism.Argument) { path -> + thenExecute { + lens = LensOfFashionTheft( + get(path), + MC.unsafeGetRegistryEntry(get(component))!!, + ) + source.sendFeedback( + tr( + "firmament.fitstealer.lensset", + "Analyzing path ${get(path)} for component ${get(component).value}" + ) + ) + } + } + } + } + } + } + } + + private fun copyHistory(toCopy: List<String>) { + ClipboardUtils.setTextContent(toCopy.joinToString("\n")) + } + + @Subscribe + fun onWorldSwap(event: WorldReadyEvent) { + subject = null + if (history.isNotEmpty()) { + metaHistory.add(history) + history = mutableListOf() + } + } + + private fun toggleObserve(entity: Entity?) { + subject = if (subject == null) entity else null + if (subject == null) { + metaHistory.add(history) + history = mutableListOf() + } + MC.sendChat( + subject?.let { + tr( + "firmament.fitstealer.targeted", + "Observing the equipment of ${it.name}." + ) + } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."), + ) + } +} diff --git a/src/main/kotlin/features/debug/DebugLogger.kt b/src/main/kotlin/features/debug/DebugLogger.kt index 2c6b962..9115956 100644 --- a/src/main/kotlin/features/debug/DebugLogger.kt +++ b/src/main/kotlin/features/debug/DebugLogger.kt @@ -10,6 +10,7 @@ class DebugLogger(val tag: String) { companion object { val allInstances = InstanceList<DebugLogger>("DebugLogger") } + object EnabledLogs : DataHolder<MutableSet<String>>(serializer(), "DebugLogs", ::mutableSetOf) init { @@ -17,6 +18,7 @@ class DebugLogger(val tag: String) { } fun isEnabled() = DeveloperFeatures.isEnabled && EnabledLogs.data.contains(tag) + fun log(text: String) = log { text } fun log(text: () -> String) { if (!isEnabled()) return MC.sendChat(Text.literal(text())) diff --git a/src/main/kotlin/features/debug/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt index 8f0c25c..af1e92e 100644 --- a/src/main/kotlin/features/debug/DeveloperFeatures.kt +++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt @@ -3,6 +3,10 @@ package moe.nea.firmament.features.debug import java.io.File import java.nio.file.Path import java.util.concurrent.CompletableFuture +import org.objectweb.asm.ClassReader +import org.objectweb.asm.Type +import org.objectweb.asm.tree.ClassNode +import org.spongepowered.asm.mixin.Mixin import kotlinx.serialization.json.encodeToStream import kotlin.io.path.absolute import kotlin.io.path.exists @@ -10,11 +14,14 @@ import net.minecraft.client.MinecraftClient import net.minecraft.text.Text import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.DebugInstantiateEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.init.MixinPlugin import moe.nea.firmament.util.MC import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.asm.AsmAnnotationUtil import moe.nea.firmament.util.iterate object DeveloperFeatures : FirmamentFeature { @@ -42,6 +49,42 @@ object DeveloperFeatures : FirmamentFeature { } @Subscribe + fun loadAllMixinClasses(event: DebugInstantiateEvent) { + val allMixinClasses = mutableSetOf<String>() + MixinPlugin.instances.forEach { plugin -> + val prefix = plugin.mixinPackage + "." + val classes = plugin.mixins.map { prefix + it } + allMixinClasses.addAll(classes) + for (cls in classes) { + val targets = javaClass.classLoader.getResourceAsStream("${cls.replace(".", "/")}.class").use { + val node = ClassNode() + ClassReader(it).accept(node, 0) + val mixins = mutableListOf<Mixin>() + (node.visibleAnnotations.orEmpty() + node.invisibleAnnotations.orEmpty()).forEach { + val annotationType = Type.getType(it.desc) + val mixinType = Type.getType(Mixin::class.java) + if (mixinType == annotationType) { + mixins.add(AsmAnnotationUtil.createProxy(Mixin::class.java, it)) + } + } + mixins.flatMap { it.targets.toList() } + mixins.flatMap { it.value.map { it.java.name } } + } + for (target in targets) + try { + Firmament.logger.debug("Loading ${target} to force instantiate ${cls}") + Class.forName(target, true, javaClass.classLoader) + } catch (ex: Throwable) { + Firmament.logger.error("Could not load class ${target} that has been mixind by $cls", ex) + } + } + } + Firmament.logger.info("Forceloaded all Firmament mixins:") + val applied = MixinPlugin.instances.flatMap { it.appliedMixins }.toSet() + applied.forEach { Firmament.logger.info(" - ${it}") } + require(allMixinClasses == applied) + } + + @Subscribe fun dumpMissingTranslations(tickEvent: TickEvent) { val toDump = missingTranslations ?: return missingTranslations = null diff --git a/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt new file mode 100644 index 0000000..a817dd6 --- /dev/null +++ b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt @@ -0,0 +1,19 @@ +package moe.nea.firmament.features.debug + +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import java.util.Optional + +data class ExportedTestConstantMeta( + val dataVersion: Int, + val modVersion: Optional<String>, +) { + companion object { + val CODEC: Codec<ExportedTestConstantMeta> = RecordCodecBuilder.create { + it.group( + Codec.INT.fieldOf("dataVersion").forGetter(ExportedTestConstantMeta::dataVersion), + Codec.STRING.optionalFieldOf("modVersion").forGetter(ExportedTestConstantMeta::modVersion), + ).apply(it, ::ExportedTestConstantMeta) + } + } +} diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt index 8be5d5d..893b176 100644 --- a/src/main/kotlin/features/debug/PowerUserTools.kt +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -1,31 +1,25 @@ package moe.nea.firmament.features.debug -import com.mojang.serialization.Codec -import com.mojang.serialization.DynamicOps import com.mojang.serialization.JsonOps -import com.mojang.serialization.codecs.RecordCodecBuilder import kotlin.jvm.optionals.getOrNull import net.minecraft.block.SkullBlock import net.minecraft.block.entity.SkullBlockEntity import net.minecraft.component.DataComponentTypes import net.minecraft.component.type.ProfileComponent import net.minecraft.entity.Entity -import net.minecraft.entity.EntityType import net.minecraft.entity.LivingEntity import net.minecraft.item.ItemStack import net.minecraft.item.Items -import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtList import net.minecraft.nbt.NbtOps -import net.minecraft.nbt.NbtString import net.minecraft.predicate.NbtPredicate import net.minecraft.text.Text import net.minecraft.text.TextCodecs import net.minecraft.util.Identifier +import net.minecraft.util.Nameable import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.hit.EntityHitResult import net.minecraft.util.hit.HitResult -import net.minecraft.util.math.Position -import net.minecraft.util.math.Vec3d import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent @@ -43,8 +37,10 @@ import moe.nea.firmament.util.mc.IntrospectableItemModelManager import moe.nea.firmament.util.mc.SNbtFormatter import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.iterableArmorItems import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr object PowerUserTools : FirmamentFeature { override val identifier: String @@ -59,6 +55,7 @@ object PowerUserTools : FirmamentFeature { val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture") val copyEntityData by keyBindingWithDefaultUnbound("entity-data") val copyItemStack by keyBindingWithDefaultUnbound("copy-item-stack") + val copyTitle by keyBindingWithDefaultUnbound("copy-title") } override val config @@ -108,7 +105,7 @@ object PowerUserTools : FirmamentFeature { MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos)) if (target is LivingEntity) { MC.sendChat(Text.translatable("firmament.poweruser.entity.armor")) - for (armorItem in target.armorItems) { + for ((slot, armorItem) in target.iterableArmorItems) { MC.sendChat(Text.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem))) } } @@ -179,11 +176,23 @@ object PowerUserTools : FirmamentFeature { Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString())) println("Copied skull id: $skullTexture") } else if (it.matches(TConfig.copyItemStack)) { - ClipboardUtils.setTextContent( - ItemStack.CODEC - .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item) - .orThrow.toPrettyString()) + val nbt = ItemStack.CODEC + .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item) + .orThrow + ClipboardUtils.setTextContent(nbt.toPrettyString()) lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack")) + } else if (it.matches(TConfig.copyTitle)) { + val allTitles = NbtList() + val inventoryNames = + it.screen.screenHandler.slots + .mapNotNullTo(mutableSetOf()) { it.inventory } + .filterIsInstance<Nameable>() + .map { it.name } + for (it in listOf(it.screen.title) + inventoryNames) { + allTitles.add(TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it).result().getOrNull()!!) + } + ClipboardUtils.setTextContent(allTitles.toPrettyString()) + MC.sendChat(tr("firmament.power-user.title.copied", "Copied screen and inventory titles")) } } diff --git a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt index 1824225..cfc05cc 100644 --- a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt +++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt @@ -222,7 +222,7 @@ object MinesweeperHelper { fun onChat(event: ProcessChatEvent) { if (CarnivalFeatures.TConfig.displayTutorials && event.unformattedString == startGameQuestion) { MC.sendChat(Text.translatable("firmament.carnival.tutorial.minesweeper").styled { - it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/firm minesweepertutorial")) + it.withClickEvent(ClickEvent.RunCommand("/firm minesweepertutorial")) }) } if (!CarnivalFeatures.TConfig.enableBombSolver) { @@ -259,7 +259,7 @@ object MinesweeperHelper { val boardPosition = BoardPosition.fromBlockPos(event.blockPos) log.log { "Breaking block at ${event.blockPos} ($boardPosition)" } gs.lastClickedPosition = boardPosition - gs.lastDowsingMode = DowsingMode.fromItem(event.player.inventory.mainHandStack) + gs.lastDowsingMode = DowsingMode.fromItem(event.player.mainHandStack) } @Subscribe diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt index 7030319..5e6350d 100644 --- a/src/main/kotlin/features/fixes/Fixes.kt +++ b/src/main/kotlin/features/fixes/Fixes.kt @@ -23,6 +23,8 @@ object Fixes : FirmamentFeature { val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) } val peekChat by keyBindingWithDefaultUnbound("peek-chat") val hidePotionEffects by toggle("hide-mob-effects") { false } + val noHurtCam by toggle("disable-hurt-cam") { false } + val hideSlotHighlights by toggle("hide-slot-highlights") { false } } override val config: ManagedConfig @@ -49,7 +51,7 @@ object Fixes : FirmamentFeature { "firmament.fixes.auto-sprint.sprinting" else "firmament.fixes.auto-sprint.not-sprinting" - ), 0, 0, -1, false + ), 0, 0, -1, true ) it.context.matrices.pop() } diff --git a/src/main/kotlin/features/inventory/PetFeatures.kt b/src/main/kotlin/features/inventory/PetFeatures.kt index 5ca10f7..bb39fbc 100644 --- a/src/main/kotlin/features/inventory/PetFeatures.kt +++ b/src/main/kotlin/features/inventory/PetFeatures.kt @@ -1,14 +1,24 @@ package moe.nea.firmament.features.inventory -import net.minecraft.util.Identifier +import moe.nea.jarvis.api.Point +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent import moe.nea.firmament.events.SlotRenderEvents import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.FirmFormatters.formatPercent +import moe.nea.firmament.util.FirmFormatters.shortFormat import moe.nea.firmament.util.MC import moe.nea.firmament.util.petData import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.titleCase import moe.nea.firmament.util.useMatch +import moe.nea.firmament.util.withColor object PetFeatures : FirmamentFeature { override val identifier: String @@ -19,9 +29,12 @@ object PetFeatures : FirmamentFeature { object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val highlightEquippedPet by toggle("highlight-pet") { true } + var petOverlay by toggle("pet-overlay") { false } + val petOverlayHud by position("pet-overlay-hud", 80, 10) { Point(0.5, 1.0) } } val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern() + var petItemStack: ItemStack? = null @Subscribe fun onSlotRender(event: SlotRenderEvents.Before) { @@ -29,12 +42,44 @@ object PetFeatures : FirmamentFeature { val stack = event.slot.stack if (stack.petData?.active == true) petMenuTitle.useMatch(MC.screenName ?: return) { - event.context.drawGuiTexture( - event.slot.x, event.slot.y, 0, 16, 16, - Identifier.of("firmament:selected_pet_background") - ) - } + petItemStack = stack + event.context.drawGuiTexture( + Firmament.identifier("selected_pet_background"), + event.slot.x, event.slot.y, 16, 16, + ) + } } + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.petOverlay) return + val itemStack = petItemStack ?: return + val petData = petItemStack?.petData ?: return + val rarity = Rarity.fromNeuRepo(petData.tier) + val rarityCode = Rarity.colourMap[rarity] ?: Formatting.WHITE + val xp = petData.level + val petType = titleCase(petData.type) + val heldItem = petData.heldItem?.let { item -> "Held Item: ${titleCase(item)}" } + + it.context.matrices.push() + TConfig.petOverlayHud.applyTransformations(it.context.matrices) + + val lines = mutableListOf<Text>() + it.context.matrices.push() + it.context.matrices.translate(-0.5, -0.5, 0.0) + it.context.matrices.scale(2f, 2f, 1f) + it.context.drawItem(itemStack, 0, 0) + it.context.matrices.pop() + lines.add(Text.literal("[Lvl ${xp.currentLevel}] ").append(Text.literal(petType).withColor(rarityCode))) + if (heldItem != null) lines.add(Text.literal(heldItem)) + if (xp.currentLevel != xp.maxLevel) lines.add(Text.literal("Required L${xp.currentLevel + 1}: ${shortFormat(xp.expInCurrentLevel.toDouble())}/${shortFormat(xp.expRequiredForNextLevel.toDouble())} (${formatPercent(xp.percentageToNextLevel.toDouble())})")) + lines.add(Text.literal("Required L100: ${shortFormat(xp.expTotal.toDouble())}/${shortFormat(xp.expRequiredForMaxLevel.toDouble())} (${formatPercent(xp.percentageToMaxLevel.toDouble())})")) + + for ((index, line) in lines.withIndex()) { + it.context.drawText(MC.font, line.copy().withColor(Formatting.GRAY), 36, MC.font.fontHeight * index, -1, true) + } + + it.context.matrices.pop() + } } diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt index 4477203..a724d63 100644 --- a/src/main/kotlin/features/inventory/PriceData.kt +++ b/src/main/kotlin/features/inventory/PriceData.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.features.inventory import net.minecraft.text.Text @@ -8,44 +6,68 @@ import moe.nea.firmament.events.ItemTooltipEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.repo.HypixelStaticData -import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.FirmFormatters.formatCommas +import moe.nea.firmament.util.bold +import moe.nea.firmament.util.gold import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.yellow object PriceData : FirmamentFeature { - override val identifier: String - get() = "price-data" + override val identifier: String + get() = "price-data" + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val tooltipEnabled by toggle("enable-always") { true } + val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") + } - object TConfig : ManagedConfig(identifier, Category.INVENTORY) { - val tooltipEnabled by toggle("enable-always") { true } - val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") - } + override val config get() = TConfig - override val config get() = TConfig + fun formatPrice(label: Text, price: Double): Text { + return Text.literal("") + .yellow() + .bold() + .append(label) + .append(": ") + .append( + Text.literal(formatCommas(price, fractionalDigits = 1)) + .append(if(price != 1.0) " coins" else " coin") + .gold() + .bold() + ) + } - @Subscribe - fun onItemTooltip(it: ItemTooltipEvent) { - if (!TConfig.tooltipEnabled && !TConfig.enableKeybinding.isPressed()) { - return - } - val sbId = it.stack.skyBlockId - val bazaarData = HypixelStaticData.bazaarData[sbId] - val lowestBin = HypixelStaticData.lowestBin[sbId] - if (bazaarData != null) { - it.lines.add(Text.literal("")) - it.lines.add( - Text.stringifiedTranslatable("firmament.tooltip.bazaar.sell-order", - FirmFormatters.formatCommas(bazaarData.quickStatus.sellPrice, 1)) - ) - it.lines.add( - Text.stringifiedTranslatable("firmament.tooltip.bazaar.buy-order", - FirmFormatters.formatCommas(bazaarData.quickStatus.buyPrice, 1)) - ) - } else if (lowestBin != null) { - it.lines.add(Text.literal("")) - it.lines.add( - Text.stringifiedTranslatable("firmament.tooltip.ah.lowestbin", - FirmFormatters.formatCommas(lowestBin, 1)) - ) - } - } + @Subscribe + fun onItemTooltip(it: ItemTooltipEvent) { + if (!TConfig.tooltipEnabled && !TConfig.enableKeybinding.isPressed()) { + return + } + val sbId = it.stack.skyBlockId + val bazaarData = HypixelStaticData.bazaarData[sbId] + val lowestBin = HypixelStaticData.lowestBin[sbId] + if (bazaarData != null) { + it.lines.add(Text.literal("")) + it.lines.add( + formatPrice( + tr("firmament.tooltip.bazaar.sell-order", "Bazaar Sell Order"), + bazaarData.quickStatus.sellPrice + ) + ) + it.lines.add( + formatPrice( + tr("firmament.tooltip.bazaar.buy-order", "Bazaar Buy Order"), + bazaarData.quickStatus.buyPrice + ) + ) + } else if (lowestBin != null) { + it.lines.add(Text.literal("")) + it.lines.add( + formatPrice( + tr("firmament.tooltip.ah.lowestbin", "Lowest BIN"), + lowestBin + ) + ) + } + } } diff --git a/src/main/kotlin/features/inventory/REIDependencyWarner.kt b/src/main/kotlin/features/inventory/REIDependencyWarner.kt index 1e9b1b8..7d88dd1 100644 --- a/src/main/kotlin/features/inventory/REIDependencyWarner.kt +++ b/src/main/kotlin/features/inventory/REIDependencyWarner.kt @@ -1,5 +1,6 @@ package moe.nea.firmament.features.inventory +import java.net.URI import net.fabricmc.loader.api.FabricLoader import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -38,7 +39,7 @@ object REIDependencyWarner { .white() .append(Text.literal("[").aqua()) .append(Text.translatable("firmament.download", modName) - .styled { it.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, modrinthLink(slug))) } + .styled { it.withClickEvent(ClickEvent.OpenUrl(URI (modrinthLink(slug)))) } .yellow() .also { if (alreadyDownloaded) diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt index 7a3a152..d3348a2 100644 --- a/src/main/kotlin/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -4,10 +4,23 @@ package moe.nea.firmament.features.inventory import java.util.UUID import org.lwjgl.glfw.GLFW +import util.render.CustomRenderLayers +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.int import kotlinx.serialization.serializer import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.RenderLayers +import net.minecraft.client.render.TexturedRenderLayers import net.minecraft.entity.player.PlayerInventory import net.minecraft.screen.GenericContainerScreenHandler import net.minecraft.screen.slot.Slot @@ -36,7 +49,6 @@ import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex import moe.nea.firmament.util.mc.SlotUtils.swapWithHotBar import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt -import moe.nea.firmament.util.render.GuiRenderLayers import moe.nea.firmament.util.render.drawLine import moe.nea.firmament.util.skyblock.DungeonUtil import moe.nea.firmament.util.skyblockUUID @@ -51,9 +63,66 @@ object SlotLocking : FirmamentFeature { val lockedSlots: MutableSet<Int> = mutableSetOf(), val lockedSlotsRift: MutableSet<Int> = mutableSetOf(), val lockedUUIDs: MutableSet<UUID> = mutableSetOf(), - val boundSlots: MutableMap<Int, Int> = mutableMapOf() + val boundSlots: BoundSlots = BoundSlots() ) + @Serializable + data class BoundSlot( + val hotbar: Int, + val inventory: Int, + ) + + @Serializable(with = BoundSlots.Serializer::class) + data class BoundSlots( + val pairs: MutableSet<BoundSlot> = mutableSetOf() + ) { + fun findMatchingSlots(index: Int): List<BoundSlot> { + return pairs.filter { it.hotbar == index || it.inventory == index } + } + + fun removeDuplicateForInventory(index: Int) { + pairs.removeIf { it.inventory == index } + } + + fun removeAllInvolving(index: Int): Boolean { + return pairs.removeIf { it.inventory == index || it.hotbar == index } + } + + fun insert(hotbar: Int, inventory: Int) { + if (!TConfig.allowMultiBinding) { + removeAllInvolving(hotbar) + removeAllInvolving(inventory) + } + pairs.add(BoundSlot(hotbar, inventory)) + } + + object Serializer : KSerializer<BoundSlots> { + override val descriptor: SerialDescriptor + get() = serializer<JsonElement>().descriptor + + override fun serialize( + encoder: Encoder, + value: BoundSlots + ) { + serializer<MutableSet<BoundSlot>>() + .serialize(encoder, value.pairs) + } + + override fun deserialize(decoder: Decoder): BoundSlots { + decoder as JsonDecoder + val json = decoder.decodeJsonElement() + if (json is JsonObject) { + return BoundSlots(json.entries.map { + BoundSlot(it.key.toInt(), (it.value as JsonPrimitive).int) + }.toMutableSet()) + } + return BoundSlots(decoder.json.decodeFromJsonElement(serializer<MutableSet<BoundSlot>>(), json)) + + } + } + } + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L } val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") { @@ -62,6 +131,7 @@ object SlotLocking : FirmamentFeature { val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L } val slotBindRequireShift by toggle("require-quick-move") { true } val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES } + val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option val allowDroppingInDungeons by toggle("drop-in-dungeons") { true } } @@ -177,19 +247,19 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) { - val boundSlots = DConfig.data?.boundSlots ?: mapOf() + val boundSlots = DConfig.data?.boundSlots ?: BoundSlots() val isValidAction = it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift) if (!isValidAction) return val handler = MC.handledScreen?.screenHandler ?: return val slot = it.slot if (slot != null && it.slot.inventory is PlayerInventory) { - val boundSlot = boundSlots.entries.find { - it.value == slot.index || it.key == slot.index - } ?: return + val matchingSlots = boundSlots.findMatchingSlots(slot.index) + if (matchingSlots.isEmpty()) return it.protectSilent() - val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.value, true) - inventorySlot?.swapWithHotBar(handler, boundSlot.key) + val boundSlot = matchingSlots.singleOrNull() ?: return + val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.inventory, true) + inventorySlot?.swapWithHotBar(handler, boundSlot.hotbar) } } @@ -228,10 +298,8 @@ object SlotLocking : FirmamentFeature { val boundSlots = DConfig.data?.boundSlots ?: return lockedSlots?.remove(hotBarSlot.index) lockedSlots?.remove(invSlot.index) - boundSlots.entries.removeIf { - it.value == invSlot.index - } - boundSlots[hotBarSlot.index] = invSlot.index + boundSlots.removeDuplicateForInventory(invSlot.index) + boundSlots.insert(hotBarSlot.index, invSlot.index) DConfig.markDirty() CommonSoundEffects.playSuccess() return @@ -245,9 +313,7 @@ object SlotLocking : FirmamentFeature { storedLockingSlot = null val boundSlots = DConfig.data?.boundSlots ?: return if (slot != null) - boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - } + boundSlots.removeAllInvolving(slot.index) } } @@ -258,9 +324,10 @@ object SlotLocking : FirmamentFeature { val accScreen = event.screen as AccessorHandledScreen val sx = accScreen.x_Firmament val sy = accScreen.y_Firmament - for (it in boundSlots.entries) { - val hotbarSlot = findByIndex(it.key) ?: continue - val inventorySlot = findByIndex(it.value) ?: continue + val highlitSlots = mutableSetOf<Slot>() + for (it in boundSlots.pairs) { + val hotbarSlot = findByIndex(it.hotbar) ?: continue + val inventorySlot = findByIndex(it.inventory) ?: continue val (hotX, hotY) = hotbarSlot.lineCenter() val (invX, invY) = inventorySlot.lineCenter() @@ -268,22 +335,27 @@ object SlotLocking : FirmamentFeature { || accScreen.focusedSlot_Firmament === inventorySlot if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING) continue - val color = if (anyHovered) - me.shedaniel.math.Color.ofOpaque(0x00FF00) - else - me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt()) + if (anyHovered) { + highlitSlots.add(hotbarSlot) + highlitSlots.add(inventorySlot) + } + fun color(highlit: Boolean) = + if (highlit) + me.shedaniel.math.Color.ofOpaque(0x00FF00) + else + me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt()) if (TConfig.slotRenderLines == SlotRenderLinesMode.EVERYTHING || anyHovered) event.context.drawLine( invX + sx, invY + sy, hotX + sx, hotY + sy, - color + color(anyHovered) ) event.context.drawBorder(hotbarSlot.x + sx, hotbarSlot.y + sy, - 16, 16, color.color) + 16, 16, color(hotbarSlot in highlitSlots).color) event.context.drawBorder(inventorySlot.x + sx, inventorySlot.y + sy, - 16, 16, color.color) + 16, 16, color(inventorySlot in highlitSlots).color) } } @@ -339,11 +411,9 @@ object SlotLocking : FirmamentFeature { fun toggleSlotLock(slot: Slot) { val lockedSlots = lockedSlots ?: return - val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf() + val boundSlots = DConfig.data?.boundSlots ?: BoundSlots() if (slot.inventory is PlayerInventory) { - if (boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - }) { + if (boundSlots.removeAllInvolving(slot.index)) { // intentionally do nothing } else if (slot.index in lockedSlots) { lockedSlots.remove(slot.index) @@ -378,7 +448,7 @@ object SlotLocking : FirmamentFeature { val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf()) if (isSlotLocked || isUUIDLocked) { it.context.drawGuiTexture( - GuiRenderLayers.GUI_TEXTURED_NO_DEPTH, + RenderLayer::getGuiTexturedOverlay, when { isSlotLocked -> (Identifier.of("firmament:slot_locked")) diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt index f1b77c6..309ea61 100644 --- a/src/main/kotlin/features/inventory/TimerInLore.kt +++ b/src/main/kotlin/features/inventory/TimerInLore.kt @@ -80,7 +80,8 @@ object TimerInLore { COMMUNITYPROJECTS("Contribute again", "Come back at"), CHOCOLATEFACTORY("Next Charge", "Available at"), STONKSAUCTION("Auction ends in", "Ends at"), - LIZSTONKREDEMPTION("Resets in:", "Resets at"); + LIZSTONKREDEMPTION("Resets in:", "Resets at"), + EVENTENDING("Event ends in:", "Ends at"); } val regex = diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt index ee3ae8b..c4ea519 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt @@ -6,6 +6,7 @@ import io.github.notenoughupdates.moulconfig.xml.Bind import me.shedaniel.math.Point import me.shedaniel.math.Rectangle import org.lwjgl.glfw.GLFW +import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.util.InputUtil @@ -55,6 +56,11 @@ class InventoryButtonEditor( super.close() } + override fun resize(client: MinecraftClient, width: Int, height: Int) { + lastGuiRect.move(MC.window.scaledWidth / 2 - lastGuiRect.width / 2, MC.window.scaledHeight / 2 - lastGuiRect.height / 2) + super.resize(client, width, height) + } + override fun init() { super.init() addDrawableChild( @@ -114,6 +120,13 @@ class InventoryButtonEditor( return newButtons } + override fun renderBackground(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + context.matrices.push() + context.matrices.translate(0F, 0F, -15F) + super.renderBackground(context, mouseX, mouseY, delta) + context.matrices.pop() + } + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { super.render(context, mouseX, mouseY, delta) context.matrices.push() diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt index d5b5417..361e8d0 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt @@ -5,16 +5,22 @@ package moe.nea.firmament.features.inventory.buttons import me.shedaniel.math.Rectangle import kotlinx.serialization.Serializable import kotlinx.serialization.serializer +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.MinecraftClient +import net.minecraft.text.Text import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HandledScreenClickEvent import moe.nea.firmament.events.HandledScreenForegroundEvent import moe.nea.firmament.events.HandledScreenPushREIEvent import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.FirmHoverComponent import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.util.MC import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.data.DataHolder import moe.nea.firmament.util.accessors.getRectangle +import moe.nea.firmament.util.gold object InventoryButtons : FirmamentFeature { override val identifier: String @@ -24,6 +30,7 @@ object InventoryButtons : FirmamentFeature { val _openEditor by button("open-editor") { openEditor() } + val hoverText by toggle("hover-text") { true } } object DConfig : DataHolder<Data>(serializer(), identifier, ::Data) @@ -60,16 +67,36 @@ object InventoryButtons : FirmamentFeature { } } + var lastHoveredComponent: InventoryButton? = null + var lastMouseMove = TimeMark.farPast() + @Subscribe fun onRenderForeground(it: HandledScreenForegroundEvent) { val bounds = it.screen.getRectangle() + + var hoveredComponent: InventoryButton? = null for (button in getValidButtons()) { val buttonBounds = button.getBounds(bounds) it.context.matrices.push() it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat(), 0F) button.render(it.context) it.context.matrices.pop() + + if (buttonBounds.contains(it.mouseX, it.mouseY) && TConfig.hoverText && hoveredComponent == null) { + hoveredComponent = button + if (lastMouseMove.passedTime() > 0.6.seconds && lastHoveredComponent === button) { + it.context.drawTooltip( + MC.font, + listOf(Text.literal(button.command).gold()), + buttonBounds.minX - 15, + buttonBounds.maxY + 20, + ) + } + } } + if (hoveredComponent !== lastHoveredComponent) + lastMouseMove = TimeMark.now() + lastHoveredComponent = hoveredComponent lastRectangle = bounds } @@ -78,9 +105,9 @@ object InventoryButtons : FirmamentFeature { ScreenUtil.setScreenLater( InventoryButtonEditor( lastRectangle ?: Rectangle( - MC.window.scaledWidth / 2 - 100, - MC.window.scaledHeight / 2 - 100, - 200, 200, + MC.window.scaledWidth / 2 - 88, + MC.window.scaledHeight / 2 - 83, + 176, 166, ) ) ) diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt index 8fad4df..d7346c2 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt @@ -32,8 +32,8 @@ sealed interface StorageBackingHandle { StorageBackingHandle, HasBackingScreen companion object { - private val enderChestName = "^Ender Chest \\(([1-9])/[1-9]\\)$".toRegex() - private val backPackName = "^.+Backpack \\(Slot #([0-9]+)\\)$".toRegex() + private val enderChestName = "^Ender Chest (?:✦ )?\\(([1-9])/[1-9]\\)$".toRegex() + private val backPackName = "^.+Backpack (?:✦ )?\\(Slot #([0-9]+)\\)$".toRegex() /** * Parse a screen into a [StorageBackingHandle]. If this returns null it means that the screen is not diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt index 2e807de..2101915 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt @@ -33,6 +33,7 @@ object StorageOverlay : FirmamentFeature { val inverseScroll by toggle("inverse-scroll") { false } val padding by integer("padding", 1, 20) { 5 } val margin by integer("margin", 1, 60) { 20 } + val itemsBlockScrolling by toggle("block-item-scrolling") { true } } fun adjustScrollSpeed(amount: Double): Double { diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt index 6092e26..81f058e 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt @@ -9,6 +9,7 @@ import net.minecraft.entity.player.PlayerInventory import net.minecraft.screen.slot.Slot import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.customgui.CustomGui +import moe.nea.firmament.util.focusedItemStack class StorageOverlayCustom( val handler: StorageBackingHandle, @@ -113,6 +114,8 @@ class StorageOverlayCustom( horizontalAmount: Double, verticalAmount: Double ): Boolean { + if (screen.focusedItemStack != null && StorageOverlay.TConfig.itemsBlockScrolling) + return false return overview.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt index 633a8fe..22f4dab 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt @@ -218,7 +218,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { } fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { - val items = MC.player?.inventory?.main ?: return + val items = MC.player?.inventory?.mainStacks ?: return items.withIndex().forEach { (index, item) -> val (x, y) = getPlayerInventorySlotPosition(index) context.drawItem(item, x, y, 0) @@ -357,6 +357,10 @@ class StorageOverlayScreen : Screen(Text.literal("")) { return super.keyReleased(keyCode, scanCode, modifiers) } + override fun shouldCloseOnEsc(): Boolean { + return this === MC.screen // Fixes this UI closing the handled screen on Escape press. + } + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { if (typeMCComponentInPlace( controlComponent, diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt index 3b86184..d99acd7 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlin.jvm.optionals.getOrNull import net.minecraft.item.ItemStack import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtIo @@ -42,15 +43,15 @@ data class VirtualInventory( override fun deserialize(decoder: Decoder): VirtualInventory { val s = decoder.decodeString() val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000)) - val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt()) + val items = n.getList(INVENTORY).getOrNull() val ops = getOps() - return VirtualInventory(items.map { + return VirtualInventory(items?.map { it as NbtCompound if (it.isEmpty) ItemStack.EMPTY else ErrorUtil.catch("Could not deserialize item") { ItemStack.CODEC.parse(ops, it).orThrow }.or { ItemStack.EMPTY } - }) + } ?: listOf()) } fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries) diff --git a/src/main/kotlin/features/macros/ComboProcessor.kt b/src/main/kotlin/features/macros/ComboProcessor.kt new file mode 100644 index 0000000..5c5ac0e --- /dev/null +++ b/src/main/kotlin/features/macros/ComboProcessor.kt @@ -0,0 +1,114 @@ +package moe.nea.firmament.features.macros + +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.util.InputUtil +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.tr + +object ComboProcessor { + + var rootTrie: Branch = Branch(mapOf()) + private set + + var activeTrie: Branch = rootTrie + private set + + var isInputting = false + var lastInput = TimeMark.farPast() + val breadCrumbs = mutableListOf<SavedKeyBinding>() + + init { + val f = SavedKeyBinding(InputUtil.GLFW_KEY_F) + val one = SavedKeyBinding(InputUtil.GLFW_KEY_1) + val two = SavedKeyBinding(InputUtil.GLFW_KEY_2) + setActions( + MacroData.DConfig.data.comboActions + ) + } + + fun setActions(actions: List<ComboKeyAction>) { + rootTrie = KeyComboTrie.fromComboList(actions) + reset() + } + + fun reset() { + activeTrie = rootTrie + lastInput = TimeMark.now() + isInputting = false + breadCrumbs.clear() + } + + @Subscribe + fun onTick(event: TickEvent) { + if (isInputting && lastInput.passedTime() > 3.seconds) + reset() + } + + + @Subscribe + fun onRender(event: HudRenderEvent) { + if (!isInputting) return + if (!event.isRenderingHud) return + event.context.matrices.push() + val width = 120 + event.context.matrices.translate( + (MC.window.scaledWidth - width) / 2F, + (MC.window.scaledHeight) / 2F + 8, + 0F + ) + val breadCrumbText = breadCrumbs.joinToString(" > ") + event.context.drawText( + MC.font, + tr("firmament.combo.active", "Current Combo: ").append(breadCrumbText), + 0, + 0, + -1, + true + ) + event.context.matrices.translate(0F, MC.font.fontHeight + 2F, 0F) + for ((key, value) in activeTrie.nodes) { + event.context.drawText( + MC.font, + Text.literal("$breadCrumbText > $key: ").append(value.label), + 0, + 0, + -1, + true + ) + event.context.matrices.translate(0F, MC.font.fontHeight + 1F, 0F) + } + event.context.matrices.pop() + } + + @Subscribe + fun onKeyBinding(event: WorldKeyboardEvent) { + val nextEntry = activeTrie.nodes.entries + .find { event.matches(it.key) } + if (nextEntry == null) { + reset() + return + } + event.cancel() + breadCrumbs.add(nextEntry.key) + lastInput = TimeMark.now() + isInputting = true + val value = nextEntry.value + when (value) { + is Branch -> { + activeTrie = value + } + + is Leaf -> { + value.execute() + reset() + } + }.let { } + } +} diff --git a/src/main/kotlin/features/macros/HotkeyAction.kt b/src/main/kotlin/features/macros/HotkeyAction.kt new file mode 100644 index 0000000..011f797 --- /dev/null +++ b/src/main/kotlin/features/macros/HotkeyAction.kt @@ -0,0 +1,40 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.minecraft.text.Text +import moe.nea.firmament.util.MC + +@Serializable +sealed interface HotkeyAction { + // TODO: execute + val label: Text + fun execute() +} + +@Serializable +@SerialName("command") +data class CommandAction(val command: String) : HotkeyAction { + override val label: Text + get() = Text.literal("/$command") + + override fun execute() { + MC.sendCommand(command) + } +} + +// Mit onscreen anzeige: +// F -> 1 /equipment +// F -> 2 /wardrobe +// Bei Combos: Keys buffern! (für wardrobe hotkeys beispielsweiße) + +// Radial menu +// Hold F +// Weight (mach eins doppelt so groß) +// /equipment +// /wardrobe + +// Bei allen: Filter! +// - Nur in Dungeons / andere Insel +// - Nur wenn ich Item X im inventar habe (fishing rod) + diff --git a/src/main/kotlin/features/macros/KeyComboTrie.kt b/src/main/kotlin/features/macros/KeyComboTrie.kt new file mode 100644 index 0000000..57ff289 --- /dev/null +++ b/src/main/kotlin/features/macros/KeyComboTrie.kt @@ -0,0 +1,68 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.Serializable +import net.minecraft.text.Text +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.ErrorUtil + +sealed interface KeyComboTrie { + val label: Text + + companion object { + fun fromComboList( + combos: List<ComboKeyAction>, + ): Branch { + val root = Branch(mutableMapOf()) + for (combo in combos) { + var p = root + if (combo.keys.isEmpty()) { + ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty") + continue + } + for ((index, key) in combo.keys.withIndex()) { + val m = (p.nodes as MutableMap) + if (index == combo.keys.lastIndex) { + if (key in m) { + ErrorUtil.softUserError("Overlapping actions found for ${combo.keys.joinToString(" > ")} (another action ${m[key]} already exists).") + break + } + + m[key] = Leaf(combo.action) + } else { + val c = m.getOrPut(key) { Branch(mutableMapOf()) } + if (c !is Branch) { + ErrorUtil.softUserError("Overlapping actions found for ${combo.keys} (final node exists at index $index) through another action already") + break + } else { + p = c + } + } + } + } + return root + } + } +} + + +@Serializable +data class ComboKeyAction( + val action: HotkeyAction, + val keys: List<SavedKeyBinding>, +) + +data class Leaf(val action: HotkeyAction) : KeyComboTrie { + override val label: Text + get() = action.label + + fun execute() { + action.execute() + } +} + +data class Branch( + val nodes: Map<SavedKeyBinding, KeyComboTrie> +) : KeyComboTrie { + override val label: Text + get() = Text.literal("...") // TODO: better labels +} diff --git a/src/main/kotlin/features/macros/MacroData.kt b/src/main/kotlin/features/macros/MacroData.kt new file mode 100644 index 0000000..78a5948 --- /dev/null +++ b/src/main/kotlin/features/macros/MacroData.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.Serializable +import moe.nea.firmament.util.data.DataHolder + +@Serializable +data class MacroData( + var comboActions: List<ComboKeyAction> = listOf(), +){ + object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData) +} diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt new file mode 100644 index 0000000..17fdd0a --- /dev/null +++ b/src/main/kotlin/features/macros/MacroUI.kt @@ -0,0 +1,161 @@ +package moe.nea.firmament.features.macros + +import io.github.notenoughupdates.moulconfig.gui.CloseEventListener +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.xml.Bind +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.gui.config.AllConfigsGui.toObservableList +import moe.nea.firmament.gui.config.KeyBindingStateManager +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil + +class MacroUI { + + + companion object { + @Subscribe + fun onCommands(event: CommandEvent.SubCommand) { + // TODO: add button in config + event.subcommand("macros") { + thenExecute { + ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("config/macros/index", MacroUI(), null)) + } + } + } + + } + + @field:Bind("combos") + val combos = Combos() + + class Combos { + @field:Bind("actions") + val actions: ObservableList<ActionEditor> = ObservableList( + MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) { + ActionEditor(it, this) + } + ) + + var dontSave = false + + @Bind + fun beforeClose(): CloseEventListener.CloseAction { + if (!dontSave) + save() + return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE + } + + @Bind + fun addCommand() { + actions.add( + ActionEditor( + ComboKeyAction( + CommandAction("ac Hello from a Firmament Hotkey"), + listOf() + ), + this + ) + ) + } + + @Bind + fun discard() { + dontSave = true + MC.screen?.close() + } + + @Bind + fun saveAndClose() { + save() + MC.screen?.close() + } + + @Bind + fun save() { + MacroData.DConfig.data.comboActions = actions.map { it.asSaveable() } + MacroData.DConfig.markDirty() + ComboProcessor.setActions(MacroData.DConfig.data.comboActions) // TODO: automatically reload those from the config on startup + } + } + + class KeyBindingEditor(var binding: SavedKeyBinding, val parent: ActionEditor) { + val sm = KeyBindingStateManager( + { binding }, + { binding = it }, + ::blur, + ::requestFocus + ) + + @field:Bind + val button = sm.createButton() + + init { + sm.updateLabel() + } + + fun blur() { + button.blur() + } + + @Bind + fun delete() { + parent.combo.removeIf { it === this } + parent.combo.update() + } + + fun requestFocus() { + button.requestFocus() + } + } + + class ActionEditor(val action: ComboKeyAction, val parent: MacroUI.Combos) { + fun asSaveable(): ComboKeyAction { + return ComboKeyAction( + CommandAction(command), + combo.map { it.binding } + ) + } + + @field:Bind("command") + var command: String = (action.action as CommandAction).command + + @field:Bind("combo") + val combo = action.keys.map { KeyBindingEditor(it, this) }.toObservableList() + + @Bind + fun formattedCombo() = + combo.joinToString(" > ") { it.binding.toString() } + + @Bind + fun addStep() { + combo.add(KeyBindingEditor(SavedKeyBinding.unbound(), this)) + } + + @Bind + fun back() { + MC.screen?.close() + } + + @Bind + fun delete() { + parent.actions.removeIf { it === this } + parent.actions.update() + } + @Bind + fun edit() { + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor", this, MC.screen) + } + } +} + +private fun <T> ObservableList<T>.setAll(ts: Collection<T>) { + val observer = this.observer + this.clear() + this.addAll(ts) + this.observer = observer + this.update() +} diff --git a/src/main/kotlin/features/mining/MiningBlockInfoUi.kt b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt new file mode 100644 index 0000000..e8ea4f4 --- /dev/null +++ b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt @@ -0,0 +1,54 @@ +package moe.nea.firmament.features.mining + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.observer.Property +import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.xml.Bind +import net.minecraft.client.gui.screen.Screen +import net.minecraft.item.ItemStack +import moe.nea.firmament.repo.MiningRepoData +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.SkyBlockIsland + +object MiningBlockInfoUi { + class MiningInfo(miningData: MiningRepoData) { + @field:Bind("search") + @JvmField + var search = "" + + @get:Bind("ores") + val blocks = miningData.customMiningBlocks.mapTo(ObservableList(mutableListOf())) { OreInfo(it, this) } + } + + class OreInfo(block: MiningRepoData.CustomMiningBlock, info: MiningInfo) { + @get:Bind("oreName") + val oreName = block.name ?: "No Name" + + @get:Bind("blocks") + val res = ObservableList(block.blocks189.map { BlockInfo(it, info) }) + } + + class BlockInfo(val block: MiningRepoData.Block189, val info: MiningInfo) { + @get:Bind("item") + val item = ModernItemStack.of(block.block?.let { ItemStack(it) } ?: ItemStack.EMPTY) + + @get:Bind("isSelected") + val isSelected get() = info.search.let { block.isActiveIn(SkyBlockIsland.forMode(it)) } + + @get:Bind("itemName") + val itemName get() = item.getDisplayName() + + @get:Bind("restrictions") + val res = ObservableList( + if (block.onlyIn != null) + block.onlyIn.map { " §r- §a${it.userFriendlyName}" } + else + listOf("Everywhere") + ) + } + + fun makeScreen(): Screen { + return MoulConfigUtils.loadScreen("mining_block_info/index", MiningInfo(RepoManager.miningData), null) + } +} diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt index 1737969..d39694a 100644 --- a/src/main/kotlin/features/mining/PickaxeAbility.kt +++ b/src/main/kotlin/features/mining/PickaxeAbility.kt @@ -1,9 +1,13 @@ package moe.nea.firmament.features.mining import java.util.regex.Pattern +import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.MinecraftClient +import net.minecraft.client.toast.SystemToast import net.minecraft.item.ItemStack +import net.minecraft.text.Text import net.minecraft.util.DyeColor import net.minecraft.util.Hand import net.minecraft.util.Identifier @@ -47,6 +51,7 @@ object PickaxeAbility : FirmamentFeature { object TConfig : ManagedConfig(identifier, Category.MINING) { val cooldownEnabled by toggle("ability-cooldown") { false } val cooldownScale by integer("ability-scale", 16, 64) { 16 } + val cooldownReadyToast by toggle("ability-cooldown-toast") { false } val drillFuelBar by toggle("fuel-bar") { true } val blockOnPrivateIsland by choice( "block-on-dynamic", @@ -140,8 +145,7 @@ object PickaxeAbility : FirmamentFeature { } } ?: return val extra = it.item.extraAttributes - if (!extra.contains("drill_fuel")) return - val fuel = extra.getInt("drill_fuel") + val fuel = extra.getInt("drill_fuel").getOrNull() ?: return val percentage = fuel / maxFuel.toFloat() it.barOverride = DurabilityBarEvent.DurabilityBar( lerp( @@ -170,6 +174,11 @@ object PickaxeAbility : FirmamentFeature { nowAvailable.useMatch(it.unformattedString) { val ability = group("name") lastUsage[ability] = TimeMark.farPast() + if (!TConfig.cooldownReadyToast) return + val mc: MinecraftClient = MinecraftClient.getInstance() + mc.toastManager.add( + SystemToast.create(mc, SystemToast.Type.NARRATOR_TOGGLE, tr("firmament.pickaxe.ability-ready","Pickaxe Cooldown"), tr("firmament.pickaxe.ability-ready.desc", "Pickaxe ability is ready!")) + ) } } diff --git a/src/main/kotlin/features/misc/LicenseViewer.kt b/src/main/kotlin/features/misc/LicenseViewer.kt new file mode 100644 index 0000000..4219177 --- /dev/null +++ b/src/main/kotlin/features/misc/LicenseViewer.kt @@ -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() + } + } + } +} diff --git a/src/main/kotlin/features/world/ColeWeightCompat.kt b/src/main/kotlin/features/world/ColeWeightCompat.kt new file mode 100644 index 0000000..b92a91e --- /dev/null +++ b/src/main/kotlin/features/world/ColeWeightCompat.kt @@ -0,0 +1,125 @@ +package moe.nea.firmament.features.world + +import kotlinx.serialization.Serializable +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.tr + +object ColeWeightCompat { + @Serializable + data class ColeWeightWaypoint( + val x: Int, + val y: Int, + val z: Int, + val r: Int = 0, + val g: Int = 0, + val b: Int = 0, + ) + + fun fromFirm(waypoints: FirmWaypoints, relativeTo: BlockPos): List<ColeWeightWaypoint> { + return waypoints.waypoints.map { + ColeWeightWaypoint(it.x - relativeTo.x, it.y - relativeTo.y, it.z - relativeTo.z) + } + } + + fun intoFirm(waypoints: List<ColeWeightWaypoint>, relativeTo: BlockPos): FirmWaypoints { + val w = waypoints.map { + FirmWaypoints.Waypoint(it.x + relativeTo.x, it.y + relativeTo.y, it.z + relativeTo.z) + } + return FirmWaypoints( + "Imported Waypoints", + "imported", + null, + w.toMutableList(), + false + ) + } + + fun copyAndInform( + source: DefaultSource, + origin: BlockPos, + positiveFeedback: (Int) -> Text, + ) { + val waypoints = Waypoints.useNonEmptyWaypoints() + ?.let { fromFirm(it, origin) } + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return + } + val data = + Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints) + ClipboardUtils.setTextContent(data) + source.sendFeedback(positiveFeedback(waypoints.size)) + } + + fun importAndInform( + source: DefaultSource, + pos: BlockPos?, + positiveFeedback: (Int) -> Text + ) { + val text = ClipboardUtils.getTextContents() + val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ORIGIN) } + val waypoints = wr.getOrElse { + source.sendError( + tr("firmament.command.waypoint.import.cw.error", + "Could not import ColeWeight waypoints.")) + Firmament.logger.error(it) + return + } + waypoints.lastRelativeImport = pos + Waypoints.waypoints = waypoints + source.sendFeedback(positiveFeedback(waypoints.size)) + } + + @Subscribe + fun onEvent(event: CommandEvent.SubCommand) { + event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) { + thenLiteral("exportcw") { + thenExecute { + copyAndInform(source, BlockPos.ORIGIN) { + tr("firmament.command.waypoint.export.cw", + "Copied $it waypoints to clipboard in ColeWeight format.") + } + } + } + thenLiteral("exportrelativecw") { + thenExecute { + copyAndInform(source, MC.player?.blockPos ?: BlockPos.ORIGIN) { + tr("firmament.command.waypoint.export.cw.relative", + "Copied $it relative waypoints to clipboard in ColeWeight format. Make sure to stand in the same position when importing.") + } + } + } + thenLiteral("importcw") { + thenExecute { + importAndInform(source, null) { + Text.stringifiedTranslatable("firmament.command.waypoint.import.cw", + it) + } + } + } + thenLiteral("importrelativecw") { + thenExecute { + importAndInform(source, MC.player!!.blockPos) { + tr("firmament.command.waypoint.import.cw.relative", + "Imported $it relative waypoints from clipboard. Make sure you stand in the same position as when you exported these waypoints for them to line up correctly.") + } + } + } + } + } + + fun tryParse(string: String): Result<List<ColeWeightWaypoint>> { + return runCatching { + Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(string) + } + } +} diff --git a/src/main/kotlin/features/world/FirmWaypointManager.kt b/src/main/kotlin/features/world/FirmWaypointManager.kt new file mode 100644 index 0000000..d18483c --- /dev/null +++ b/src/main/kotlin/features/world/FirmWaypointManager.kt @@ -0,0 +1,168 @@ +package moe.nea.firmament.features.world + +import com.mojang.brigadier.arguments.StringArgumentType +import kotlinx.serialization.serializer +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.suggestsList +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TemplateUtil +import moe.nea.firmament.util.data.MultiFileDataHolder +import moe.nea.firmament.util.tr + +object FirmWaypointManager { + object DataHolder : MultiFileDataHolder<FirmWaypoints>(serializer(), "waypoints") + + val SHARE_PREFIX = "FIRM_WAYPOINTS/" + val ENCODED_SHARE_PREFIX = TemplateUtil.getPrefixComparisonSafeBase64Encoding(SHARE_PREFIX) + + fun createExportableCopy( + waypoints: FirmWaypoints, + ): FirmWaypoints { + val copy = waypoints.copy(waypoints = waypoints.waypoints.toMutableList()) + if (waypoints.isRelativeTo != null) { + val origin = waypoints.lastRelativeImport + if (origin != null) { + copy.waypoints.replaceAll { + it.copy( + x = it.x - origin.x, + y = it.y - origin.y, + z = it.z - origin.z, + ) + } + } else { + TODO("Add warning!") + } + } + return copy + } + + fun loadWaypoints(waypoints: FirmWaypoints, sendFeedback: (Text) -> Unit) { + val copy = waypoints.deepCopy() + if (copy.isRelativeTo != null) { + val origin = MC.player!!.blockPos + copy.waypoints.replaceAll { + it.copy( + x = it.x + origin.x, + y = it.y + origin.y, + z = it.z + origin.z, + ) + } + copy.lastRelativeImport = origin.toImmutable() + sendFeedback(tr("firmament.command.waypoint.import.ordered.success", + "Imported ${copy.size} relative waypoints. Make sure you stand in the correct spot while loading the waypoints: ${copy.isRelativeTo}.")) + } else { + sendFeedback(tr("firmament.command.waypoint.import.success", + "Imported ${copy.size} waypoints.")) + } + Waypoints.waypoints = copy + } + + fun setOrigin(source: DefaultSource, text: String?) { + val waypoints = Waypoints.useEditableWaypoints() + waypoints.isRelativeTo = text ?: waypoints.isRelativeTo ?: "" + val pos = MC.player!!.blockPos + waypoints.lastRelativeImport = pos + source.sendFeedback(tr("firmament.command.waypoint.originset", + "Set the origin of waypoints to ${FirmFormatters.formatPosition(pos)}. Run /firm waypoints export to save the waypoints relative to this position.")) + } + + @Subscribe + fun onCommands(event: CommandEvent.SubCommand) { + event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) { + thenLiteral("setorigin") { + thenExecute { + setOrigin(source, null) + } + thenArgument("hint", RestArgumentType) { text -> + thenExecute { + setOrigin(source, this[text]) + } + } + } + thenLiteral("clearorigin") { + thenExecute { + val waypoints = Waypoints.useEditableWaypoints() + waypoints.lastRelativeImport = null + waypoints.isRelativeTo = null + source.sendFeedback(tr("firmament.command.waypoint.originunset", + "Unset the origin of the waypoints. Run /firm waypoints export to save the waypoints with absolute coordinates.")) + } + } + thenLiteral("save") { + thenArgument("name", StringArgumentType.string()) { name -> + suggestsList { DataHolder.list().keys } + thenExecute { + val waypoints = Waypoints.useNonEmptyWaypoints() + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return@thenExecute + } + waypoints.id = get(name) + val exportableWaypoints = createExportableCopy(waypoints) + DataHolder.insert(get(name), exportableWaypoints) + DataHolder.save() + source.sendFeedback(tr("firmament.command.waypoint.saved", + "Saved waypoints locally as ${get(name)}. Use /firm waypoints load to load them again.")) + } + } + } + thenLiteral("load") { + thenArgument("name", StringArgumentType.string()) { name -> + suggestsList { DataHolder.list().keys } + thenExecute { + val name = get(name) + val waypoints = DataHolder.list()[name] + if (waypoints == null) { + source.sendError( + tr("firmament.command.waypoint.nosaved", + "No saved waypoint for ${name}. Use tab completion to see available names.")) + return@thenExecute + } + loadWaypoints(waypoints, source::sendFeedback) + } + } + } + thenLiteral("export") { + thenExecute { + val waypoints = Waypoints.useNonEmptyWaypoints() + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return@thenExecute + } + val exportableWaypoints = createExportableCopy(waypoints) + val data = TemplateUtil.encodeTemplate(SHARE_PREFIX, exportableWaypoints) + ClipboardUtils.setTextContent(data) + source.sendFeedback(tr("firmament.command.waypoint.export", + "Copied ${exportableWaypoints.size} waypoints to clipboard in Firmament format.")) + } + } + thenLiteral("import") { + thenExecute { + val text = ClipboardUtils.getTextContents() + if (text.startsWith("[")) { + source.sendError(tr("firmament.command.waypoint.import.lookslikecw", + "The waypoints in your clipboard look like they might be ColeWeight waypoints. If so, use /firm waypoints importcw or /firm waypoints importrelativecw.")) + return@thenExecute + } + val waypoints = TemplateUtil.maybeDecodeTemplate<FirmWaypoints>(SHARE_PREFIX, text) + if (waypoints == null) { + source.sendError(tr("firmament.command.waypoint.import.error", + "Could not import Firmament waypoints from your clipboard. Make sure they are Firmament compatible waypoints.")) + return@thenExecute + } + loadWaypoints(waypoints, source::sendFeedback) + } + } + } + } +} diff --git a/src/main/kotlin/features/world/FirmWaypoints.kt b/src/main/kotlin/features/world/FirmWaypoints.kt new file mode 100644 index 0000000..d0cd55a --- /dev/null +++ b/src/main/kotlin/features/world/FirmWaypoints.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.features.world + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import net.minecraft.util.math.BlockPos + +@Serializable +data class FirmWaypoints( + var label: String, + var id: String, + /** + * A hint to indicate where to stand while loading the waypoints. + */ + var isRelativeTo: String?, + var waypoints: MutableList<Waypoint>, + var isOrdered: Boolean, + // TODO: val resetOnSwap: Boolean, +) { + + fun deepCopy() = copy(waypoints = waypoints.toMutableList()) + @Transient + var lastRelativeImport: BlockPos? = null + + val size get() = waypoints.size + @Serializable + data class Waypoint( + val x: Int, + val y: Int, + val z: Int, + ) { + val blockPos get() = BlockPos(x, y, z) + + companion object { + fun from(blockPos: BlockPos) = Waypoint(blockPos.x, blockPos.y, blockPos.z) + } + } +} diff --git a/src/main/kotlin/features/world/TemporaryWaypoints.kt b/src/main/kotlin/features/world/TemporaryWaypoints.kt new file mode 100644 index 0000000..b36c49d --- /dev/null +++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt @@ -0,0 +1,73 @@ +package moe.nea.firmament.features.world + +import kotlin.compareTo +import kotlin.text.clear +import kotlin.time.Duration.Companion.seconds +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.world.Waypoints.TConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.render.RenderInWorldContext + +object TemporaryWaypoints { + data class TemporaryWaypoint( + val pos: BlockPos, + val postedAt: TimeMark, + ) + val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>() + val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern() + @Subscribe + fun onProcessChat(it: ProcessChatEvent) { + val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString) + if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) { + temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(BlockPos( + matcher.group(1).toInt(), + matcher.group(2).toInt(), + matcher.group(3).toInt(), + ), TimeMark.now()) + } + } + @Subscribe + fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) { + temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration } + if (temporaryPlayerWaypointList.isEmpty()) return + RenderInWorldContext.renderInWorld(event) { + temporaryPlayerWaypointList.forEach { (_, waypoint) -> + block(waypoint.pos, 0xFFFFFF00.toInt()) + } + temporaryPlayerWaypointList.forEach { (player, waypoint) -> + val skin = + MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }?.skinTextures?.texture + withFacingThePlayer(waypoint.pos.toCenterPos()) { + waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player)) + if (skin != null) { + matrixStack.translate(0F, -20F, 0F) + // Head front + texture( + skin, 16, 16, + 1 / 8f, 1 / 8f, + 2 / 8f, 2 / 8f, + ) + // Head overlay + texture( + skin, 16, 16, + 5 / 8f, 1 / 8f, + 6 / 8f, 2 / 8f, + ) + } + } + } + } + } + + @Subscribe + fun onWorldReady(event: WorldReadyEvent) { + temporaryPlayerWaypointList.clear() + } + +} diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt index 3ebfe70..b5c2b66 100644 --- a/src/main/kotlin/features/world/Waypoints.kt +++ b/src/main/kotlin/features/world/Waypoints.kt @@ -2,36 +2,24 @@ package moe.nea.firmament.features.world import com.mojang.brigadier.arguments.IntegerArgumentType import me.shedaniel.math.Color -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds import net.minecraft.command.argument.BlockPosArgumentType -import net.minecraft.server.command.CommandOutput -import net.minecraft.server.command.ServerCommandSource import net.minecraft.text.Text -import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Vec3d -import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.get import moe.nea.firmament.commands.thenArgument import moe.nea.firmament.commands.thenExecute import moe.nea.firmament.commands.thenLiteral import moe.nea.firmament.events.CommandEvent -import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.WorldReadyEvent import moe.nea.firmament.events.WorldRenderLastEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig -import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC -import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.mc.asFakeServer import moe.nea.firmament.util.render.RenderInWorldContext import moe.nea.firmament.util.tr @@ -43,99 +31,85 @@ object Waypoints : FirmamentFeature { val tempWaypointDuration by duration("temp-waypoint-duration", 0.seconds, 1.hours) { 30.seconds } val showIndex by toggle("show-index") { true } val skipToNearest by toggle("skip-to-nearest") { false } + val resetWaypointOrderOnWorldSwap by toggle("reset-order-on-swap") { true } // TODO: look ahead size } - data class TemporaryWaypoint( - val pos: BlockPos, - val postedAt: TimeMark, - ) - override val config get() = TConfig - - val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>() - val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern() - - val waypoints = mutableListOf<BlockPos>() - var ordered = false + var waypoints: FirmWaypoints? = null var orderedIndex = 0 - @Serializable - data class ColeWeightWaypoint( - val x: Int, - val y: Int, - val z: Int, - val r: Int = 0, - val g: Int = 0, - val b: Int = 0, - ) - @Subscribe fun onRenderOrderedWaypoints(event: WorldRenderLastEvent) { - if (waypoints.isEmpty()) return + val w = useNonEmptyWaypoints() ?: return RenderInWorldContext.renderInWorld(event) { - if (!ordered) { - waypoints.withIndex().forEach { - block(it.value, 0x800050A0.toInt()) - if (TConfig.showIndex) - withFacingThePlayer(it.value.toCenterPos()) { - text(Text.literal(it.index.toString())) - } + if (!w.isOrdered) { + w.waypoints.withIndex().forEach { + block(it.value.blockPos, 0x800050A0.toInt()) + if (TConfig.showIndex) withFacingThePlayer(it.value.blockPos.toCenterPos()) { + text(Text.literal(it.index.toString())) + } } } else { - orderedIndex %= waypoints.size + orderedIndex %= w.waypoints.size val firstColor = Color.ofRGBA(0, 200, 40, 180) color(firstColor) - tracer(waypoints[orderedIndex].toCenterPos(), lineWidth = 3f) - waypoints.withIndex().toList() - .wrappingWindow(orderedIndex, 3) - .zip( - listOf( - firstColor, - Color.ofRGBA(180, 200, 40, 150), - Color.ofRGBA(180, 80, 20, 140), - ) - ) - .reversed() - .forEach { (waypoint, col) -> - val (index, pos) = waypoint - block(pos, col.color) - if (TConfig.showIndex) - withFacingThePlayer(pos.toCenterPos()) { - text(Text.literal(index.toString())) - } + tracer(w.waypoints[orderedIndex].blockPos.toCenterPos(), lineWidth = 3f) + w.waypoints.withIndex().toList().wrappingWindow(orderedIndex, 3).zip(listOf( + firstColor, + Color.ofRGBA(180, 200, 40, 150), + Color.ofRGBA(180, 80, 20, 140), + )).reversed().forEach { (waypoint, col) -> + val (index, pos) = waypoint + block(pos.blockPos, col.color) + if (TConfig.showIndex) withFacingThePlayer(pos.blockPos.toCenterPos()) { + text(Text.literal(index.toString())) } + } } } } @Subscribe fun onTick(event: TickEvent) { - if (waypoints.isEmpty() || !ordered) return - orderedIndex %= waypoints.size + val w = useNonEmptyWaypoints() ?: return + if (!w.isOrdered) return + orderedIndex %= w.waypoints.size val p = MC.player?.pos ?: return if (TConfig.skipToNearest) { orderedIndex = - (waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size + (w.waypoints.withIndex().minBy { it.value.blockPos.getSquaredDistance(p) }.index + 1) % w.waypoints.size + } else { - if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) { - orderedIndex = (orderedIndex + 1) % waypoints.size + if (w.waypoints[orderedIndex].blockPos.isWithinDistance(p, 3.0)) { + orderedIndex = (orderedIndex + 1) % w.waypoints.size } } } + + fun useEditableWaypoints(): FirmWaypoints { + var w = waypoints + if (w == null) { + w = FirmWaypoints("Unlabeled", "unknown", null, mutableListOf(), false) + waypoints = w + } + return w + } + + fun useNonEmptyWaypoints(): FirmWaypoints? { + val w = waypoints + if (w == null) return null + if (w.waypoints.isEmpty()) return null + return w + } + + val WAYPOINTS_SUBCOMMAND = "waypoints" + @Subscribe - fun onProcessChat(it: ProcessChatEvent) { - val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString) - if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) { - temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint( - BlockPos( - matcher.group(1).toInt(), - matcher.group(2).toInt(), - matcher.group(3).toInt(), - ), - TimeMark.now() - ) + fun onWorldSwap(event: WorldReadyEvent) { + if (TConfig.resetWaypointOrderOnWorldSwap) { + orderedIndex = 0 } } @@ -144,41 +118,77 @@ object Waypoints : FirmamentFeature { event.subcommand("waypoint") { thenArgument("pos", BlockPosArgumentType.blockPos()) { pos -> thenExecute { + source val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer()) - waypoints.add(position) - source.sendFeedback( - Text.stringifiedTranslatable( - "firmament.command.waypoint.added", - position.x, - position.y, - position.z - ) - ) + val w = useEditableWaypoints() + w.waypoints.add(FirmWaypoints.Waypoint.from(position)) + source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.added", + position.x, + position.y, + position.z)) } } } - event.subcommand("waypoints") { + event.subcommand(WAYPOINTS_SUBCOMMAND) { + thenLiteral("reset") { + thenExecute { + orderedIndex = 0 + source.sendFeedback(tr( + "firmament.command.waypoint.reset", + "Reset your ordered waypoint index back to 0. If you want to delete all waypoints use /firm waypoints clear instead.")) + } + } + thenLiteral("changeindex") { + thenArgument("from", IntegerArgumentType.integer(0)) { fromIndex -> + thenArgument("to", IntegerArgumentType.integer(0)) { toIndex -> + thenExecute { + val w = useEditableWaypoints() + val toIndex = toIndex.get(this) + val fromIndex = fromIndex.get(this) + if (fromIndex !in w.waypoints.indices) { + source.sendError(textInvalidIndex(fromIndex)) + return@thenExecute + } + if (toIndex !in w.waypoints.indices) { + source.sendError(textInvalidIndex(toIndex)) + return@thenExecute + } + val waypoint = w.waypoints.removeAt(fromIndex) + w.waypoints.add( + if (toIndex > fromIndex) toIndex - 1 + else toIndex, + waypoint) + source.sendFeedback( + tr("firmament.command.waypoint.indexchange", + "Moved waypoint from index $fromIndex to $toIndex. Note that this only matters for ordered waypoints.") + ) + } + } + } + } thenLiteral("clear") { thenExecute { - waypoints.clear() + waypoints = null source.sendFeedback(Text.translatable("firmament.command.waypoint.clear")) } } thenLiteral("toggleordered") { thenExecute { - ordered = !ordered - if (ordered) { + val w = useEditableWaypoints() + w.isOrdered = !w.isOrdered + if (w.isOrdered) { val p = MC.player?.pos ?: Vec3d.ZERO - orderedIndex = - waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0 + orderedIndex = // TODO: this should be extracted to a utility method + w.waypoints.withIndex().minByOrNull { it.value.blockPos.getSquaredDistance(p) }?.index ?: 0 } - source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered")) + source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.${w.isOrdered}")) } } thenLiteral("skip") { thenExecute { - if (ordered && waypoints.isNotEmpty()) { - orderedIndex = (orderedIndex + 1) % waypoints.size + val w = useNonEmptyWaypoints() + if (w != null && w.isOrdered) { + orderedIndex = (orderedIndex + 1) % w.size source.sendFeedback(Text.translatable("firmament.command.waypoint.skip")) } else { source.sendError(Text.translatable("firmament.command.waypoint.skip.error")) @@ -189,90 +199,27 @@ object Waypoints : FirmamentFeature { thenArgument("index", IntegerArgumentType.integer(0)) { indexArg -> thenExecute { val index = get(indexArg) - if (index in waypoints.indices) { - waypoints.removeAt(index) - source.sendFeedback(Text.stringifiedTranslatable( - "firmament.command.waypoint.remove", - index)) + val w = useNonEmptyWaypoints() + if (w != null && index in w.waypoints.indices) { + w.waypoints.removeAt(index) + source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.remove", + index)) } else { source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error")) } } } } - thenLiteral("export") { - thenExecute { - val data = Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints.map { - ColeWeightWaypoint(it.x, - it.y, - it.z) - }) - ClipboardUtils.setTextContent(data) - source.sendFeedback(tr("firmament.command.waypoint.export", "Copied ${waypoints.size} waypoints to clipboard")) - } - } - thenLiteral("import") { - thenExecute { - val contents = ClipboardUtils.getTextContents() - val data = try { - Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(contents) - } catch (ex: Exception) { - Firmament.logger.error("Could not load waypoints from clipboard", ex) - source.sendError(Text.translatable("firmament.command.waypoint.import.error")) - return@thenExecute - } - waypoints.clear() - data.mapTo(waypoints) { BlockPos(it.x, it.y, it.z) } - source.sendFeedback( - Text.stringifiedTranslatable( - "firmament.command.waypoint.import", - data.size - ) - ) - } - } } } - @Subscribe - fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) { - temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration } - if (temporaryPlayerWaypointList.isEmpty()) return - RenderInWorldContext.renderInWorld(event) { - temporaryPlayerWaypointList.forEach { (player, waypoint) -> - block(waypoint.pos, 0xFFFFFF00.toInt()) - } - temporaryPlayerWaypointList.forEach { (player, waypoint) -> - val skin = - MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player } - ?.skinTextures - ?.texture - withFacingThePlayer(waypoint.pos.toCenterPos()) { - waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player)) - if (skin != null) { - matrixStack.translate(0F, -20F, 0F) - // Head front - texture( - skin, 16, 16, - 1 / 8f, 1 / 8f, - 2 / 8f, 2 / 8f, - ) - // Head overlay - texture( - skin, 16, 16, - 5 / 8f, 1 / 8f, - 6 / 8f, 2 / 8f, - ) - } - } - } - } - } + fun textInvalidIndex(index: Int) = + tr("firmament.command.waypoint.invalid-index", + "Invalid index $index provided.") - @Subscribe - fun onWorldReady(event: WorldReadyEvent) { - temporaryPlayerWaypointList.clear() - } + fun textNothingToExport(): Text = + tr("firmament.command.waypoint.export.nowaypoints", + "No waypoints to export found. Add some with /firm waypoint ~ ~ ~.") } fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> { @@ -285,35 +232,3 @@ fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> { } return result } - - -fun FabricClientCommandSource.asFakeServer(): ServerCommandSource { - val source = this - return ServerCommandSource( - object : CommandOutput { - override fun sendMessage(message: Text?) { - source.player.sendMessage(message, false) - } - - override fun shouldReceiveFeedback(): Boolean { - return true - } - - override fun shouldTrackOutput(): Boolean { - return true - } - - override fun shouldBroadcastConsoleToOps(): Boolean { - return true - } - }, - source.position, - source.rotation, - null, - 0, - "FakeServerCommandSource", - Text.literal("FakeServerCommandSource"), - null, - source.player - ) -} diff --git a/src/main/kotlin/gui/CheckboxComponent.kt b/src/main/kotlin/gui/CheckboxComponent.kt index 761c086..fc48661 100644 --- a/src/main/kotlin/gui/CheckboxComponent.kt +++ b/src/main/kotlin/gui/CheckboxComponent.kt @@ -28,8 +28,8 @@ class CheckboxComponent<T>( val ctx = (context.renderContext as ModernRenderContext).drawContext ctx.drawGuiTexture( RenderLayer::getGuiTextured, - if (isEnabled()) Firmament.identifier("firmament:widget/checkbox_checked") - else Firmament.identifier("firmament:widget/checkbox_unchecked"), + if (isEnabled()) Firmament.identifier("widget/checkbox_checked") + else Firmament.identifier("widget/checkbox_unchecked"), 0, 0, 16, 16 ) @@ -43,6 +43,7 @@ class CheckboxComponent<T>( isClicking = false if (context.isHovered) state.set(value) + blur() return true } if (mouseEvent.mouseState && mouseEvent.mouseButton == 0 && context.isHovered) { diff --git a/src/main/kotlin/gui/config/KeyBindingHandler.kt b/src/main/kotlin/gui/config/KeyBindingHandler.kt index d7d0b47..14a4b32 100644 --- a/src/main/kotlin/gui/config/KeyBindingHandler.kt +++ b/src/main/kotlin/gui/config/KeyBindingHandler.kt @@ -40,34 +40,7 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) : { button.blur() }, { button.requestFocus() } ) - button = object : FirmButtonComponent( - TextComponent( - IMinecraft.instance.defaultFontRenderer, - { sm.label.string }, - 130, - TextComponent.TextAlignment.LEFT, - false, - false - ), action = { - sm.onClick() - }) { - override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean { - if (event is KeyboardEvent.KeyPressed) { - return sm.keyboardEvent(event.keycode, event.pressed) - } - return super.keyboardEvent(event, context) - } - - override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> { - if (sm.editing) return activeBg - return super.getBackground(context) - } - - - override fun onLostFocus() { - sm.onLostFocus() - } - } + button = sm.createButton() sm.updateLabel() return button } diff --git a/src/main/kotlin/gui/config/KeyBindingStateManager.kt b/src/main/kotlin/gui/config/KeyBindingStateManager.kt index cc8178d..1528ac4 100644 --- a/src/main/kotlin/gui/config/KeyBindingStateManager.kt +++ b/src/main/kotlin/gui/config/KeyBindingStateManager.kt @@ -1,8 +1,15 @@ package moe.nea.firmament.gui.config +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.common.MyResourceLocation +import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent import org.lwjgl.glfw.GLFW import net.minecraft.text.Text import net.minecraft.util.Formatting +import moe.nea.firmament.gui.FirmButtonComponent import moe.nea.firmament.keybindings.SavedKeyBinding class KeyBindingStateManager( @@ -51,9 +58,11 @@ class KeyBindingStateManager( ) { lastPressed = ch } else { - setValue(SavedKeyBinding( - ch, modifiers - )) + setValue( + SavedKeyBinding( + ch, modifiers + ) + ) editing = false blur() lastPressed = 0 @@ -104,5 +113,34 @@ class KeyBindingStateManager( label = stroke } + fun createButton(): FirmButtonComponent { + return object : FirmButtonComponent( + TextComponent( + IMinecraft.instance.defaultFontRenderer, + { this@KeyBindingStateManager.label.string }, + 130, + TextComponent.TextAlignment.LEFT, + false, + false + ), action = { + this@KeyBindingStateManager.onClick() + }) { + override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean { + if (event is KeyboardEvent.KeyPressed) { + return this@KeyBindingStateManager.keyboardEvent(event.keycode, event.pressed) + } + return super.keyboardEvent(event, context) + } + + override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> { + if (this@KeyBindingStateManager.editing) return activeBg + return super.getBackground(context) + } + + override fun onLostFocus() { + this@KeyBindingStateManager.onLostFocus() + } + } + } } diff --git a/src/main/kotlin/gui/entity/EntityRenderer.kt b/src/main/kotlin/gui/entity/EntityRenderer.kt index fd7a0c4..b4c1c7f 100644 --- a/src/main/kotlin/gui/entity/EntityRenderer.kt +++ b/src/main/kotlin/gui/entity/EntityRenderer.kt @@ -169,13 +169,13 @@ object EntityRenderer { val oldBodyYaw = entity.bodyYaw val oldYaw = entity.yaw val oldPitch = entity.pitch - val oldPrevHeadYaw = entity.prevHeadYaw + val oldPrevHeadYaw = entity.lastHeadYaw val oldHeadYaw = entity.headYaw entity.bodyYaw = 180.0f + targetYaw * 20.0f entity.yaw = 180.0f + targetYaw * 40.0f entity.pitch = -targetPitch * 20.0f entity.headYaw = entity.yaw - entity.prevHeadYaw = entity.yaw + entity.lastHeadYaw = entity.yaw val vector3f = Vector3f(0.0f, (entity.height / 2.0f + bottomOffset).toFloat(), 0.0f) InventoryScreen.drawEntity( context, @@ -190,7 +190,7 @@ object EntityRenderer { entity.bodyYaw = oldBodyYaw entity.yaw = oldYaw entity.pitch = oldPitch - entity.prevHeadYaw = oldPrevHeadYaw + entity.lastHeadYaw = oldPrevHeadYaw entity.headYaw = oldHeadYaw context.disableScissor() } diff --git a/src/main/kotlin/gui/entity/FakeWorld.kt b/src/main/kotlin/gui/entity/FakeWorld.kt deleted file mode 100644 index ccf6b60..0000000 --- a/src/main/kotlin/gui/entity/FakeWorld.kt +++ /dev/null @@ -1,343 +0,0 @@ -package moe.nea.firmament.gui.entity - -import java.util.UUID -import java.util.function.BooleanSupplier -import java.util.function.Consumer -import net.minecraft.block.Block -import net.minecraft.block.BlockState -import net.minecraft.client.gui.screen.world.SelectWorldScreen -import net.minecraft.component.type.MapIdComponent -import net.minecraft.entity.Entity -import net.minecraft.entity.boss.dragon.EnderDragonPart -import net.minecraft.entity.damage.DamageSource -import net.minecraft.entity.player.PlayerEntity -import net.minecraft.fluid.Fluid -import net.minecraft.item.FuelRegistry -import net.minecraft.item.map.MapState -import net.minecraft.particle.ParticleEffect -import net.minecraft.recipe.BrewingRecipeRegistry -import net.minecraft.recipe.RecipeManager -import net.minecraft.recipe.RecipePropertySet -import net.minecraft.recipe.StonecuttingRecipe -import net.minecraft.recipe.display.CuttingRecipeDisplay -import net.minecraft.registry.DynamicRegistryManager -import net.minecraft.registry.Registries -import net.minecraft.registry.RegistryKey -import net.minecraft.registry.RegistryKeys -import net.minecraft.registry.ServerDynamicRegistryType -import net.minecraft.registry.entry.RegistryEntry -import net.minecraft.resource.DataConfiguration -import net.minecraft.resource.ResourcePackManager -import net.minecraft.resource.featuretoggle.FeatureFlags -import net.minecraft.resource.featuretoggle.FeatureSet -import net.minecraft.scoreboard.Scoreboard -import net.minecraft.server.SaveLoading -import net.minecraft.server.command.CommandManager -import net.minecraft.sound.SoundCategory -import net.minecraft.sound.SoundEvent -import net.minecraft.util.Identifier -import net.minecraft.util.TypeFilter -import net.minecraft.util.function.LazyIterationConsumer -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Box -import net.minecraft.util.math.ChunkPos -import net.minecraft.util.math.Direction -import net.minecraft.util.math.Vec3d -import net.minecraft.world.BlockView -import net.minecraft.world.Difficulty -import net.minecraft.world.MutableWorldProperties -import net.minecraft.world.World -import net.minecraft.world.biome.Biome -import net.minecraft.world.biome.BiomeKeys -import net.minecraft.world.chunk.Chunk -import net.minecraft.world.chunk.ChunkManager -import net.minecraft.world.chunk.ChunkStatus -import net.minecraft.world.chunk.EmptyChunk -import net.minecraft.world.chunk.light.LightingProvider -import net.minecraft.world.entity.EntityLookup -import net.minecraft.world.event.GameEvent -import net.minecraft.world.explosion.ExplosionBehavior -import net.minecraft.world.tick.OrderedTick -import net.minecraft.world.tick.QueryableTickScheduler -import net.minecraft.world.tick.TickManager -import moe.nea.firmament.util.MC - -fun createDynamicRegistry(): DynamicRegistryManager.Immutable { - // TODO: use SaveLoading.load() to properly load a full registry - return DynamicRegistryManager.of(Registries.REGISTRIES) -} - -class FakeWorld( - registries: DynamicRegistryManager.Immutable = createDynamicRegistry(), -) : World( - Properties, - RegistryKey.of(RegistryKeys.WORLD, Identifier.of("firmament", "fakeworld")), - registries, - MC.defaultRegistries.getOrThrow(RegistryKeys.DIMENSION_TYPE) - .getOrThrow(RegistryKey.of(RegistryKeys.DIMENSION_TYPE, Identifier.of("minecraft", "overworld"))), - true, - false, - 0L, - 0 -) { - object Properties : MutableWorldProperties { - override fun getSpawnPos(): BlockPos { - return BlockPos.ORIGIN - } - - override fun getSpawnAngle(): Float { - return 0F - } - - override fun getTime(): Long { - return 0 - } - - override fun getTimeOfDay(): Long { - return 0 - } - - override fun isThundering(): Boolean { - return false - } - - override fun isRaining(): Boolean { - return false - } - - override fun setRaining(raining: Boolean) { - } - - override fun isHardcore(): Boolean { - return false - } - - override fun getDifficulty(): Difficulty { - return Difficulty.HARD - } - - override fun isDifficultyLocked(): Boolean { - return false - } - - override fun setSpawnPos(pos: BlockPos?, angle: Float) {} - } - - override fun getPlayers(): List<PlayerEntity> { - return emptyList() - } - - override fun getBrightness(direction: Direction?, shaded: Boolean): Float { - return 1f - } - - override fun getGeneratorStoredBiome(biomeX: Int, biomeY: Int, biomeZ: Int): RegistryEntry<Biome> { - return registryManager.getOptionalEntry(BiomeKeys.PLAINS).get() - } - - override fun getSeaLevel(): Int { - return 0 - } - - override fun getEnabledFeatures(): FeatureSet { - return FeatureFlags.VANILLA_FEATURES - } - - class FakeTickScheduler<T> : QueryableTickScheduler<T> { - override fun scheduleTick(orderedTick: OrderedTick<T>?) { - } - - override fun isQueued(pos: BlockPos?, type: T): Boolean { - return true - } - - override fun getTickCount(): Int { - return 0 - } - - override fun isTicking(pos: BlockPos?, type: T): Boolean { - return true - } - - } - - override fun getBlockTickScheduler(): QueryableTickScheduler<Block> { - return FakeTickScheduler() - } - - override fun getFluidTickScheduler(): QueryableTickScheduler<Fluid> { - return FakeTickScheduler() - } - - - class FakeChunkManager(val world: FakeWorld) : ChunkManager() { - override fun getChunk(x: Int, z: Int, leastStatus: ChunkStatus?, create: Boolean): Chunk { - return EmptyChunk( - world, - ChunkPos(x, z), - world.registryManager.getOptionalEntry(BiomeKeys.PLAINS).get() - ) - } - - override fun getWorld(): BlockView { - return world - } - - override fun tick(shouldKeepTicking: BooleanSupplier?, tickChunks: Boolean) { - } - - override fun getDebugString(): String { - return "FakeChunkManager" - } - - override fun getLoadedChunkCount(): Int { - return 0 - } - - override fun getLightingProvider(): LightingProvider { - return FakeLightingProvider(this) - } - } - - class FakeLightingProvider(chunkManager: FakeChunkManager) : LightingProvider(chunkManager, false, false) - - override fun getChunkManager(): ChunkManager { - return FakeChunkManager(this) - } - - override fun playSound( - source: PlayerEntity?, - x: Double, - y: Double, - z: Double, - sound: RegistryEntry<SoundEvent>?, - category: SoundCategory?, - volume: Float, - pitch: Float, - seed: Long - ) { - } - - override fun syncWorldEvent(player: PlayerEntity?, eventId: Int, pos: BlockPos?, data: Int) { - } - - override fun emitGameEvent(event: RegistryEntry<GameEvent>?, emitterPos: Vec3d?, emitter: GameEvent.Emitter?) { - } - - override fun updateListeners(pos: BlockPos?, oldState: BlockState?, newState: BlockState?, flags: Int) { - } - - override fun playSoundFromEntity( - source: PlayerEntity?, - entity: Entity?, - sound: RegistryEntry<SoundEvent>?, - category: SoundCategory?, - volume: Float, - pitch: Float, - seed: Long - ) { - } - - override fun createExplosion( - entity: Entity?, - damageSource: DamageSource?, - behavior: ExplosionBehavior?, - x: Double, - y: Double, - z: Double, - power: Float, - createFire: Boolean, - explosionSourceType: ExplosionSourceType?, - smallParticle: ParticleEffect?, - largeParticle: ParticleEffect?, - soundEvent: RegistryEntry<SoundEvent>? - ) { - TODO("Not yet implemented") - } - - override fun asString(): String { - return "FakeWorld" - } - - override fun getEntityById(id: Int): Entity? { - return null - } - - override fun getEnderDragonParts(): MutableCollection<EnderDragonPart> { - return mutableListOf() - } - - override fun getTickManager(): TickManager { - return TickManager() - } - - override fun getMapState(id: MapIdComponent?): MapState? { - return null - } - - override fun putMapState(id: MapIdComponent?, state: MapState?) { - } - - override fun increaseAndGetMapId(): MapIdComponent { - return MapIdComponent(0) - } - - override fun setBlockBreakingInfo(entityId: Int, pos: BlockPos?, progress: Int) { - } - - override fun getScoreboard(): Scoreboard { - return Scoreboard() - } - - override fun getRecipeManager(): RecipeManager { - return object : RecipeManager { - override fun getPropertySet(key: RegistryKey<RecipePropertySet>?): RecipePropertySet { - return RecipePropertySet.EMPTY - } - - override fun getStonecutterRecipes(): CuttingRecipeDisplay.Grouping<StonecuttingRecipe> { - return CuttingRecipeDisplay.Grouping.empty() - } - } - } - - object FakeEntityLookup : EntityLookup<Entity> { - override fun get(id: Int): Entity? { - return null - } - - override fun get(uuid: UUID?): Entity? { - return null - } - - override fun iterate(): MutableIterable<Entity> { - return mutableListOf() - } - - override fun <U : Entity?> forEachIntersects( - filter: TypeFilter<Entity, U>?, - box: Box?, - consumer: LazyIterationConsumer<U>? - ) { - } - - override fun forEachIntersects(box: Box?, action: Consumer<Entity>?) { - } - - override fun <U : Entity?> forEach(filter: TypeFilter<Entity, U>?, consumer: LazyIterationConsumer<U>?) { - } - - } - - override fun getEntityLookup(): EntityLookup<Entity> { - return FakeEntityLookup - } - - override fun getBrewingRecipeRegistry(): BrewingRecipeRegistry { - return BrewingRecipeRegistry.EMPTY - } - - override fun getFuelRegistry(): FuelRegistry { - TODO("Not yet implemented") - } -} diff --git a/src/main/kotlin/gui/entity/ModifyEquipment.kt b/src/main/kotlin/gui/entity/ModifyEquipment.kt index a558936..712f5ca 100644 --- a/src/main/kotlin/gui/entity/ModifyEquipment.kt +++ b/src/main/kotlin/gui/entity/ModifyEquipment.kt @@ -47,7 +47,7 @@ object ModifyEquipment : EntityModifier { private fun coloredLeatherArmor(leatherArmor: Item, data: String): ItemStack { val stack = ItemStack(leatherArmor) - stack.set(DataComponentTypes.DYED_COLOR, DyedColorComponent(data.toInt(16), false)) + stack.set(DataComponentTypes.DYED_COLOR, DyedColorComponent(data.toInt(16))) return stack } } diff --git a/src/main/kotlin/gui/entity/ModifyHorse.kt b/src/main/kotlin/gui/entity/ModifyHorse.kt index f094ca4..7c8baa7 100644 --- a/src/main/kotlin/gui/entity/ModifyHorse.kt +++ b/src/main/kotlin/gui/entity/ModifyHorse.kt @@ -1,4 +1,3 @@ - package moe.nea.firmament.gui.entity import com.google.gson.JsonNull @@ -7,6 +6,7 @@ import kotlin.experimental.and import kotlin.experimental.inv import kotlin.experimental.or import net.minecraft.entity.EntityType +import net.minecraft.entity.EquipmentSlot import net.minecraft.entity.LivingEntity import net.minecraft.entity.SpawnReason import net.minecraft.entity.passive.AbstractHorseEntity @@ -15,48 +15,45 @@ import net.minecraft.item.Items import moe.nea.firmament.gui.entity.EntityRenderer.fakeWorld object ModifyHorse : EntityModifier { - override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { - require(entity is AbstractHorseEntity) - var entity: AbstractHorseEntity = entity - info["kind"]?.let { - entity = when (it.asString) { - "skeleton" -> EntityType.SKELETON_HORSE.create(fakeWorld, SpawnReason.LOAD)!! - "zombie" -> EntityType.ZOMBIE_HORSE.create(fakeWorld, SpawnReason.LOAD)!! - "mule" -> EntityType.MULE.create(fakeWorld, SpawnReason.LOAD)!! - "donkey" -> EntityType.DONKEY.create(fakeWorld, SpawnReason.LOAD)!! - "horse" -> EntityType.HORSE.create(fakeWorld, SpawnReason.LOAD)!! - else -> error("Unknown horse kind $it") - } - } - info["armor"]?.let { - if (it is JsonNull) { - entity.setHorseArmor(ItemStack.EMPTY) - } else { - when (it.asString) { - "iron" -> entity.setHorseArmor(ItemStack(Items.IRON_HORSE_ARMOR)) - "golden" -> entity.setHorseArmor(ItemStack(Items.GOLDEN_HORSE_ARMOR)) - "diamond" -> entity.setHorseArmor(ItemStack(Items.DIAMOND_HORSE_ARMOR)) - else -> error("Unknown horse armor $it") - } - } - } - info["saddled"]?.let { - entity.setIsSaddled(it.asBoolean) - } - return entity - } + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + require(entity is AbstractHorseEntity) + var entity: AbstractHorseEntity = entity + info["kind"]?.let { + entity = when (it.asString) { + "skeleton" -> EntityType.SKELETON_HORSE.create(fakeWorld, SpawnReason.LOAD)!! + "zombie" -> EntityType.ZOMBIE_HORSE.create(fakeWorld, SpawnReason.LOAD)!! + "mule" -> EntityType.MULE.create(fakeWorld, SpawnReason.LOAD)!! + "donkey" -> EntityType.DONKEY.create(fakeWorld, SpawnReason.LOAD)!! + "horse" -> EntityType.HORSE.create(fakeWorld, SpawnReason.LOAD)!! + else -> error("Unknown horse kind $it") + } + } + info["armor"]?.let { + if (it is JsonNull) { + entity.setHorseArmor(ItemStack.EMPTY) + } else { + when (it.asString) { + "iron" -> entity.setHorseArmor(ItemStack(Items.IRON_HORSE_ARMOR)) + "golden" -> entity.setHorseArmor(ItemStack(Items.GOLDEN_HORSE_ARMOR)) + "diamond" -> entity.setHorseArmor(ItemStack(Items.DIAMOND_HORSE_ARMOR)) + else -> error("Unknown horse armor $it") + } + } + } + info["saddled"]?.let { + entity.setIsSaddled(it.asBoolean) + } + return entity + } } fun AbstractHorseEntity.setIsSaddled(shouldBeSaddled: Boolean) { - val oldFlag = dataTracker.get(AbstractHorseEntity.HORSE_FLAGS) - dataTracker.set( - AbstractHorseEntity.HORSE_FLAGS, - if (shouldBeSaddled) oldFlag or AbstractHorseEntity.SADDLED_FLAG.toByte() - else oldFlag and AbstractHorseEntity.SADDLED_FLAG.toByte().inv() - ) + this.equipStack(EquipmentSlot.SADDLE, + if (shouldBeSaddled) ItemStack(Items.SADDLE) + else ItemStack.EMPTY) } fun AbstractHorseEntity.setHorseArmor(itemStack: ItemStack) { - items.setStack(1, itemStack) + this.equipBodyArmor(itemStack) } diff --git a/src/main/kotlin/keybindings/SavedKeyBinding.kt b/src/main/kotlin/keybindings/SavedKeyBinding.kt index 5bca87e..d85c303 100644 --- a/src/main/kotlin/keybindings/SavedKeyBinding.kt +++ b/src/main/kotlin/keybindings/SavedKeyBinding.kt @@ -57,7 +57,9 @@ data class SavedKeyBinding( fun isShiftDown() = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SHIFT) || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SHIFT) - } + fun unbound(): SavedKeyBinding = + SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN) + } fun isPressed(atLeast: Boolean = false): Boolean { if (!isBound) return false @@ -87,6 +89,10 @@ data class SavedKeyBinding( return keyCode == this.keyCode && getMods(modifiers) == Triple(shift, ctrl, alt) } + override fun toString(): String { + return format().string + } + fun format(): Text { val stroke = Text.literal("") if (ctrl) { diff --git a/src/main/kotlin/repo/ExpLadder.kt b/src/main/kotlin/repo/ExpLadder.kt index fbc9eb8..25a74de 100644 --- a/src/main/kotlin/repo/ExpLadder.kt +++ b/src/main/kotlin/repo/ExpLadder.kt @@ -19,7 +19,8 @@ object ExpLadders : IReloadable { val expInCurrentLevel: Float, var expTotal: Float, ) { - val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel + val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel + val percentageToMaxLevel: Float = expTotal / expRequiredForMaxLevel } data class ExpLadder( diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt index 9fa0083..09eedac 100644 --- a/src/main/kotlin/repo/ItemCache.kt +++ b/src/main/kotlin/repo/ItemCache.kt @@ -4,7 +4,6 @@ import com.mojang.serialization.Dynamic import io.github.moulberry.repo.IReloadable import io.github.moulberry.repo.NEURepository import io.github.moulberry.repo.data.NEUItem -import io.github.notenoughupdates.moulconfig.xml.Bind import java.text.NumberFormat import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -13,7 +12,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlin.jvm.optionals.getOrNull import net.minecraft.SharedConstants -import net.minecraft.client.resource.language.I18n import net.minecraft.component.DataComponentTypes import net.minecraft.component.type.NbtComponent import net.minecraft.datafixer.Schemas @@ -28,9 +26,6 @@ import net.minecraft.text.MutableText import net.minecraft.text.Style import net.minecraft.text.Text import moe.nea.firmament.Firmament -import moe.nea.firmament.gui.config.HudMeta -import moe.nea.firmament.gui.config.HudPosition -import moe.nea.firmament.gui.hud.MoulConfigHud import moe.nea.firmament.repo.RepoManager.initialize import moe.nea.firmament.util.LegacyFormattingCode import moe.nea.firmament.util.LegacyTagParser @@ -61,11 +56,12 @@ object ItemCache : IReloadable { putShort("Damage", damage.toShort()) } - private fun NbtCompound.transformFrom10809ToModern(): NbtCompound? = + private fun NbtCompound.transformFrom10809ToModern() = convert189ToModern(this@transformFrom10809ToModern) + fun convert189ToModern(nbtComponent: NbtCompound): NbtCompound? = try { df.update( TypeReferences.ITEM_STACK, - Dynamic(NbtOps.INSTANCE, this), + Dynamic(NbtOps.INSTANCE, nbtComponent), -1, SharedConstants.getGameVersion().saveVersion.id ).value as NbtCompound @@ -139,7 +135,8 @@ object ItemCache : IReloadable { ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this) itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) } itemInstance.displayNameAccordingToNbt = un189Lore(displayName) - val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes") + val extraAttributes = oldItemTag.getCompound("tag").flatMap { it.getCompound("ExtraAttributes") } + .getOrNull() if (extraAttributes != null) itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes)) return itemInstance @@ -184,31 +181,6 @@ object ItemCache : IReloadable { var job: Job? = null - object ReloadProgressHud : MoulConfigHud( - "repo_reload", HudMeta(HudPosition(0.0, 0.0, 1F), Text.literal("Repo Reload"), 180, 18)) { - - - var isEnabled = false - override fun shouldRender(): Boolean { - return isEnabled - } - - @get:Bind("current") - var current: Double = 0.0 - - @get:Bind("label") - var label: String = "" - - @get:Bind("max") - var max: Double = 0.0 - - fun reportProgress(label: String, current: Int, max: Int) { - this.label = label - this.current = current.toDouble() - this.max = max.toDouble() - } - } - override fun reload(repository: NEURepository) { val j = job if (j != null && j.isActive) { @@ -218,20 +190,10 @@ object ItemCache : IReloadable { isFlawless = true if (TestUtil.isInTest) return job = Firmament.coroutineScope.launch { - val items = repository.items?.items - if (items == null) { - ReloadProgressHud.isEnabled = false - return@launch - } - val recacheItems = I18n.translate("firmament.repo.cache") - ReloadProgressHud.reportProgress(recacheItems, 0, items.size) - ReloadProgressHud.isEnabled = true - var i = 0 + val items = repository.items?.items ?: return@launch items.values.forEach { it.asItemStack() // Rebuild cache - ReloadProgressHud.reportProgress(recacheItems, i++, items.size) } - ReloadProgressHud.isEnabled = false } } diff --git a/src/main/kotlin/repo/MiningRepoData.kt b/src/main/kotlin/repo/MiningRepoData.kt new file mode 100644 index 0000000..e40292d --- /dev/null +++ b/src/main/kotlin/repo/MiningRepoData.kt @@ -0,0 +1,131 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUItem +import java.util.Collections +import java.util.NavigableMap +import java.util.TreeMap +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.serializer +import kotlin.jvm.optionals.getOrNull +import kotlin.streams.asSequence +import net.minecraft.block.Block +import net.minecraft.item.BlockItem +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtCompound +import net.minecraft.text.Text +import moe.nea.firmament.repo.ReforgeStore.kJson +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.mc.FirmamentDataComponentTypes +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.skyblockId + +class MiningRepoData : IReloadable { + var customMiningAreas: Map<SkyBlockIsland, CustomMiningArea> = mapOf() + private set + var customMiningBlocks: List<CustomMiningBlock> = listOf() + private set + var toolsByBreakingPower: NavigableMap<BreakingPowerKey, SBItemStack> = Collections.emptyNavigableMap() + private set + + + data class BreakingPowerKey( + val breakingPower: Int, + val itemId: SkyblockId? = null + ) { + companion object { + val COMPARATOR: Comparator<BreakingPowerKey> = + Comparator + .comparingInt<BreakingPowerKey> { it.breakingPower } + .thenComparing(Comparator.comparing( + { it.itemId }, + nullsFirst(Comparator.comparing<SkyblockId, Boolean> { "PICK" in it.neuItem || "BING" in it.neuItem }.thenComparing(Comparator.naturalOrder<SkyblockId>())))) + } + } + + override fun reload(repo: NEURepository) { + customMiningAreas = repo.file("mining/custom_mining_areas.json") + ?.kJson(serializer()) ?: mapOf() + customMiningBlocks = repo.tree("mining/blocks") + .asSequence() + .filter { it.path.endsWith(".json") } + .map { it.kJson(serializer<CustomMiningBlock>()) } + .toList() + toolsByBreakingPower = Collections.unmodifiableNavigableMap( + repo.items.items + .values + .asSequence() + .map { SBItemStack(it.skyblockId) } + .filter { it.breakingPower > 0 } + .associateTo(TreeMap<BreakingPowerKey, SBItemStack>(BreakingPowerKey.COMPARATOR)) { + BreakingPowerKey(it.breakingPower, it.skyblockId) to it + }) + } + + fun getToolsThatCanBreak(breakingPower: Int): Collection<SBItemStack> { + return toolsByBreakingPower.tailMap(BreakingPowerKey(breakingPower, null), true).values + } + + @Serializable + data class CustomMiningBlock( + val breakingPower: Int = 0, + val blockStrength: Int = 0, + val name: String? = null, + val baseDrop: SkyblockId? = null, + val blocks189: List<Block189> = emptyList() + ) { + @Transient + val dropItem = baseDrop?.let(::SBItemStack) + private val labeledStack by lazy { + dropItem?.asCopiedItemStack()?.also(::markItemStack) + } + + private fun markItemStack(itemStack: ItemStack) { + itemStack.set(FirmamentDataComponentTypes.CUSTOM_MINING_BLOCK_DATA, this) + if (name != null) + itemStack.displayNameAccordingToNbt = Text.literal(name) + } + + fun getDisplayItem(block: Block): ItemStack { + return labeledStack ?: ItemStack(block).also(::markItemStack) + } + } + + @Serializable + data class Block189( + val itemId: String, + val damage: Short = 0, + val onlyIn: List<SkyBlockIsland>? = null, + ) { + @Transient + val block = convertToModernBlock() + + val isCurrentlyActive: Boolean + get() = isActiveIn(SBData.skyblockLocation ?: SkyBlockIsland.NIL) + + fun isActiveIn(location: SkyBlockIsland) = onlyIn == null || location in onlyIn + + private fun convertToModernBlock(): Block? { + // TODO: this should be in a shared util, really + val newCompound = ItemCache.convert189ToModern(NbtCompound().apply { + putString("id", itemId) + putShort("Damage", damage) + }) ?: return null + val itemStack = ItemStack.fromNbt(MC.defaultRegistries, newCompound).getOrNull() ?: return null + val blockItem = itemStack.item as? BlockItem ?: return null + return blockItem.block + } + } + + @Serializable + data class CustomMiningArea( + val isSpecialMining: Boolean = true + ) + + +} diff --git a/src/main/kotlin/repo/RepoDownloadManager.kt b/src/main/kotlin/repo/RepoDownloadManager.kt index 3efd83b..888248d 100644 --- a/src/main/kotlin/repo/RepoDownloadManager.kt +++ b/src/main/kotlin/repo/RepoDownloadManager.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.repo import io.ktor.client.call.body @@ -28,101 +26,102 @@ import moe.nea.firmament.util.iterate object RepoDownloadManager { - val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted") - val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt") - - private fun loadSavedVersionHash(): String? = - if (repoSavedLocation.exists()) { - if (repoMetadataLocation.exists()) { - try { - repoMetadataLocation.readText().trim() - } catch (e: IOException) { - null - } - } else { - null - } - } else null - - private fun saveVersionHash(versionHash: String) { - latestSavedVersionHash = versionHash - repoMetadataLocation.writeText(versionHash) - } - - var latestSavedVersionHash: String? = loadSavedVersionHash() - private set - - @Serializable - private class GithubCommitsResponse(val sha: String) - - private suspend fun requestLatestGithubSha(): String? { - if (RepoManager.Config.branch == "prerelease") { - RepoManager.Config.branch = "master" - } - val response = - Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${RepoManager.Config.branch}") - if (response.status.value != 200) { - return null - } - return response.body<GithubCommitsResponse>().sha - } - - private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) { - val response = Firmament.httpClient.get(url) - val targetFile = Files.createTempFile("firmament-repo", ".zip") - val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE) - response.bodyAsChannel().copyTo(outputChannel) - targetFile - } - - /** - * Downloads the latest repository from github, setting [latestSavedVersionHash]. - * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update) - */ - suspend fun downloadUpdate(force: Boolean): Boolean = withContext(CoroutineName("Repo Update Check")) { - val latestSha = requestLatestGithubSha() - if (latestSha == null) { - logger.warn("Could not request github API to retrieve latest REPO sha.") - return@withContext false - } - val currentSha = loadSavedVersionHash() - if (latestSha != currentSha || force) { - val requestUrl = - "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/archive/$latestSha.zip" - logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl") - val zipFile = downloadGithubArchive(requestUrl) - logger.info("Download repository zip file to $zipFile. Deleting old repository") - withContext(IO) { repoSavedLocation.toFile().deleteRecursively() } - logger.info("Extracting new repository") - withContext(IO) { extractNewRepository(zipFile) } - logger.info("Repository loaded on disk.") - saveVersionHash(latestSha) - return@withContext true - } else { - logger.debug("Repository on latest sha $currentSha. Not performing update") - return@withContext false - } - } - - private fun extractNewRepository(zipFile: Path) { - repoSavedLocation.createDirectories() - ZipInputStream(zipFile.inputStream()).use { cis -> - while (true) { - val entry = cis.nextEntry ?: break - if (entry.isDirectory) continue - val extractedLocation = - repoSavedLocation.resolve( - entry.name.substringAfter('/', missingDelimiterValue = "") - ) - if (repoSavedLocation !in extractedLocation.iterate { it.parent }) { - logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") - throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") - } - extractedLocation.parent.createDirectories() - extractedLocation.outputStream().use { cis.copyTo(it) } - } - } - } + val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted") + val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt") + + private fun loadSavedVersionHash(): String? = + if (repoSavedLocation.exists()) { + if (repoMetadataLocation.exists()) { + try { + repoMetadataLocation.readText().trim() + } catch (e: IOException) { + null + } + } else { + null + } + } else null + + private fun saveVersionHash(versionHash: String) { + latestSavedVersionHash = versionHash + repoMetadataLocation.writeText(versionHash) + } + + var latestSavedVersionHash: String? = loadSavedVersionHash() + private set + + @Serializable + private class GithubCommitsResponse(val sha: String) + + private suspend fun requestLatestGithubSha(branchOverride: String?): String? { + if (RepoManager.Config.branch == "prerelease") { + RepoManager.Config.branch = "master" + } + val response = + Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${branchOverride ?: RepoManager.Config.branch}") + if (response.status.value != 200) { + return null + } + return response.body<GithubCommitsResponse>().sha + } + + private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) { + val response = Firmament.httpClient.get(url) + val targetFile = Files.createTempFile("firmament-repo", ".zip") + val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE) + response.bodyAsChannel().copyTo(outputChannel) + targetFile + } + + /** + * Downloads the latest repository from github, setting [latestSavedVersionHash]. + * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update) + */ + suspend fun downloadUpdate(force: Boolean, branch: String? = null): Boolean = + withContext(CoroutineName("Repo Update Check")) { + val latestSha = requestLatestGithubSha(branch) + if (latestSha == null) { + logger.warn("Could not request github API to retrieve latest REPO sha.") + return@withContext false + } + val currentSha = loadSavedVersionHash() + if (latestSha != currentSha || force) { + val requestUrl = + "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/archive/$latestSha.zip" + logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl") + val zipFile = downloadGithubArchive(requestUrl) + logger.info("Download repository zip file to $zipFile. Deleting old repository") + withContext(IO) { repoSavedLocation.toFile().deleteRecursively() } + logger.info("Extracting new repository") + withContext(IO) { extractNewRepository(zipFile) } + logger.info("Repository loaded on disk.") + saveVersionHash(latestSha) + return@withContext true + } else { + logger.debug("Repository on latest sha $currentSha. Not performing update") + return@withContext false + } + } + + private fun extractNewRepository(zipFile: Path) { + repoSavedLocation.createDirectories() + ZipInputStream(zipFile.inputStream()).use { cis -> + while (true) { + val entry = cis.nextEntry ?: break + if (entry.isDirectory) continue + val extractedLocation = + repoSavedLocation.resolve( + entry.name.substringAfter('/', missingDelimiterValue = "") + ) + if (repoSavedLocation !in extractedLocation.iterate { it.parent }) { + logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") + throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") + } + extractedLocation.parent.createDirectories() + extractedLocation.outputStream().use { cis.copyTo(it) } + } + } + } } diff --git a/src/main/kotlin/repo/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt index 6d9ba14..cc36fba 100644 --- a/src/main/kotlin/repo/RepoManager.kt +++ b/src/main/kotlin/repo/RepoManager.kt @@ -54,6 +54,7 @@ object RepoManager { val essenceRecipeProvider = EssenceRecipeProvider() val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider, ReforgeStore) + val miningData = MiningRepoData() fun makeNEURepository(path: Path): NEURepository { return NEURepository.of(path).apply { @@ -63,6 +64,8 @@ object RepoManager { registerReloadListener(ItemNameLookup) registerReloadListener(ReforgeStore) registerReloadListener(essenceRecipeProvider) + registerReloadListener(recipeCache) + registerReloadListener(miningData) ReloadRegistrationEvent.publish(ReloadRegistrationEvent(this)) registerReloadListener { if (TestUtil.isInTest) return@registerReloadListener @@ -73,7 +76,6 @@ object RepoManager { } } } - registerReloadListener(recipeCache) } } @@ -100,16 +102,16 @@ object RepoManager { fun getNEUItem(skyblockId: SkyblockId): NEUItem? = neuRepo.items.getItemBySkyblockId(skyblockId.neuItem) + fun downloadOverridenBranch(branch: String) { + Firmament.coroutineScope.launch { + RepoDownloadManager.downloadUpdate(true, branch) + reload() + } + } + fun launchAsyncUpdate(force: Boolean = false) { Firmament.coroutineScope.launch { - ItemCache.ReloadProgressHud.reportProgress("Downloading", 0, -1) // TODO: replace with a proper bouncy bar - ItemCache.ReloadProgressHud.isEnabled = true - try { - RepoDownloadManager.downloadUpdate(force) - ItemCache.ReloadProgressHud.reportProgress("Download complete", 1, 1) - } finally { - ItemCache.ReloadProgressHud.isEnabled = false - } + RepoDownloadManager.downloadUpdate(force) reload() } } @@ -127,10 +129,6 @@ object RepoManager { return } try { - ItemCache.ReloadProgressHud.reportProgress("Reloading from Disk", - 0, - -1) // TODO: replace with a proper bouncy bar - ItemCache.ReloadProgressHud.isEnabled = true logger.info("Repo reload started.") neuRepo.reload() logger.info("Repo reload completed.") @@ -140,7 +138,6 @@ object RepoManager { tr("firmament.repo.reloadfail", "Failed to reload repository. This will result in some mod features not working.") ) - ItemCache.ReloadProgressHud.isEnabled = false } } diff --git a/src/main/kotlin/repo/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt index 617efec..4fec14a 100644 --- a/src/main/kotlin/repo/RepoModResourcePack.kt +++ b/src/main/kotlin/repo/RepoModResourcePack.kt @@ -5,6 +5,7 @@ import java.nio.file.Files import java.nio.file.Path import java.util.* import net.fabricmc.fabric.api.resource.ModResourcePack +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackSorter import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.metadata.ModMetadata import kotlin.io.path.exists @@ -23,14 +24,14 @@ import net.minecraft.resource.metadata.ResourceMetadata import net.minecraft.resource.metadata.ResourceMetadataSerializer import net.minecraft.text.Text import net.minecraft.util.Identifier -import net.minecraft.util.PathUtil +import net.minecraft.util.path.PathUtil import moe.nea.firmament.Firmament class RepoModResourcePack(val basePath: Path) : ModResourcePack { companion object { - fun append(packs: MutableList<in ModResourcePack>) { + fun append(packs: ModResourcePackSorter) { Firmament.logger.info("Registering mod resource pack") - packs.add(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) + packs.addPack(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) } fun createResourceDirectly(identifier: Identifier): Optional<Resource> { diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt index da34707..3690866 100644 --- a/src/main/kotlin/repo/SBItemStack.kt +++ b/src/main/kotlin/repo/SBItemStack.kt @@ -34,6 +34,7 @@ import moe.nea.firmament.util.modifyExtraAttributes import moe.nea.firmament.util.petData import moe.nea.firmament.util.prepend import moe.nea.firmament.util.reconstitute +import moe.nea.firmament.util.removeColorCodes import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyblock.ItemType import moe.nea.firmament.util.skyblock.Rarity @@ -81,6 +82,7 @@ data class SBItemStack constructor( } val EMPTY = SBItemStack(SkyblockId.NULL, 0) + private val BREAKING_POWER_REGEX = "Breaking Power (?<power>[0-9]+)".toPattern() operator fun invoke(itemStack: ItemStack): SBItemStack { val skyblockId = itemStack.skyBlockId ?: SkyblockId.NULL return SBItemStack( @@ -349,6 +351,12 @@ data class SBItemStack constructor( private var itemStack_: ItemStack? = null + val breakingPower: Int + get() = + BREAKING_POWER_REGEX.useMatch(neuItem?.lore?.firstOrNull()?.removeColorCodes()) { + group("power").toInt() + } ?: 0 + private val itemStack: ItemStack get() { val itemStack = itemStack_ ?: run { diff --git a/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt index 9a1aea5..3774f26 100644 --- a/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt +++ b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt @@ -9,11 +9,13 @@ import net.minecraft.util.Identifier import moe.nea.firmament.repo.SBItemStack interface GenericRecipeRenderer<T : NEURecipe> { - fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter) + fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter, mainItem: SBItemStack?) fun getInputs(recipe: T): Collection<SBItemStack> fun getOutputs(recipe: T): Collection<SBItemStack> val icon: ItemStack val title: Text val identifier: Identifier fun findAllRecipes(neuRepository: NEURepository): Iterable<T> + val displayHeight: Int get() = 66 + val typ: Class<T> } diff --git a/src/main/kotlin/repo/recipes/RecipeLayouter.kt b/src/main/kotlin/repo/recipes/RecipeLayouter.kt index 109bff5..ed0dca2 100644 --- a/src/main/kotlin/repo/recipes/RecipeLayouter.kt +++ b/src/main/kotlin/repo/recipes/RecipeLayouter.kt @@ -1,6 +1,8 @@ package moe.nea.firmament.repo.recipes import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle import net.minecraft.text.Text import moe.nea.firmament.repo.SBItemStack @@ -21,13 +23,16 @@ interface RecipeLayouter { slotKind: SlotKind, ) + fun createTooltip(rectangle: Rectangle, label: Text) + fun createLabel( x: Int, y: Int, text: Text ) - fun createArrow(x: Int, y: Int) + fun createArrow(x: Int, y: Int): Rectangle fun createMoulConfig(x: Int, y: Int, w: Int, h: Int, component: GuiComponent) + fun createFire(ingredientsCenter: Point, animationTicks: Int) } diff --git a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt index 679aec8..e38380c 100644 --- a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt +++ b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt @@ -12,17 +12,24 @@ import moe.nea.firmament.Firmament import moe.nea.firmament.repo.SBItemStack import moe.nea.firmament.util.tr -class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { - override fun render(recipe: NEUCraftingRecipe, bounds: Rectangle, layouter: RecipeLayouter) { +object SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { + override fun render( + recipe: NEUCraftingRecipe, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack?, + ) { val point = Point(bounds.centerX - 58, bounds.centerY - 27) layouter.createArrow(point.x + 60, point.y + 18) for (i in 0 until 3) { for (j in 0 until 3) { val item = recipe.inputs[i + j * 3] - layouter.createItemSlot(point.x + 1 + i * 18, - point.y + 1 + j * 18, - SBItemStack(item), - RecipeLayouter.SlotKind.SMALL_INPUT) + layouter.createItemSlot( + point.x + 1 + i * 18, + point.y + 1 + j * 18, + SBItemStack(item), + RecipeLayouter.SlotKind.SMALL_INPUT + ) } } layouter.createItemSlot( @@ -32,6 +39,9 @@ class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { ) } + override val typ: Class<NEUCraftingRecipe> + get() = NEUCraftingRecipe::class.java + override fun getInputs(recipe: NEUCraftingRecipe): Collection<SBItemStack> { return recipe.allInputs.mapNotNull { SBItemStack(it) } } @@ -45,6 +55,6 @@ class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { } override val icon: ItemStack = ItemStack(Blocks.CRAFTING_TABLE) - override val title: Text = tr("firmament.category.crafting", "SkyBlock Crafting") // TODO: fix tr not being included in jars + override val title: Text = tr("firmament.category.crafting", "SkyBlock Crafting") override val identifier: Identifier = Firmament.identifier("crafting_recipe") } diff --git a/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt new file mode 100644 index 0000000..0f5271f --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt @@ -0,0 +1,74 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUForgeRecipe +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.EssenceRecipeProvider +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.tr + +object SBEssenceUpgradeRecipeRenderer : GenericRecipeRenderer<EssenceRecipeProvider.EssenceUpgradeRecipe> { + override fun render( + recipe: EssenceRecipeProvider.EssenceUpgradeRecipe, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack? + ) { + val sourceItem = mainItem ?: SBItemStack(recipe.itemId) + layouter.createItemSlot( + bounds.minX + 12, + bounds.centerY - 8 - 18 / 2, + sourceItem.copy(stars = recipe.starCountAfter - 1), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + layouter.createItemSlot( + bounds.minX + 12, bounds.centerY - 8 + 18 / 2, + SBItemStack(recipe.essenceIngredient), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + layouter.createItemSlot( + bounds.maxX - 12 - 16, bounds.centerY - 8, + sourceItem.copy(stars = recipe.starCountAfter), + RecipeLayouter.SlotKind.SMALL_OUTPUT + ) + val extraItems = recipe.extraItems + layouter.createArrow( + bounds.centerX - 24 / 2, + if (extraItems.isEmpty()) bounds.centerY - 17 / 2 + else bounds.centerY + 18 / 2 + ) + for ((index, item) in extraItems.withIndex()) { + layouter.createItemSlot( + bounds.centerX - extraItems.size * 16 / 2 - 2 / 2 + index * 18, + bounds.centerY - 18 / 2, + SBItemStack(item), + RecipeLayouter.SlotKind.SMALL_INPUT, + ) + } + } + + override fun getInputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection<SBItemStack> { + return recipe.allInputs.mapNotNull { SBItemStack(it) } + } + + override fun getOutputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection<SBItemStack> { + return listOfNotNull(SBItemStack(recipe.itemId)) + } + + override val icon: ItemStack get() = SBItemStack(SkyblockId("ESSENCE_WITHER")).asImmutableItemStack() + override val title: Text = tr("firmament.category.essence", "Essence Upgrades") + override val identifier: Identifier = Firmament.identifier("essence_upgrade") + override fun findAllRecipes(neuRepository: NEURepository): Iterable<EssenceRecipeProvider.EssenceUpgradeRecipe> { + return RepoManager.essenceRecipeProvider.recipes + } + + override val typ: Class<EssenceRecipeProvider.EssenceUpgradeRecipe> + get() = EssenceRecipeProvider.EssenceUpgradeRecipe::class.java +} diff --git a/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt new file mode 100644 index 0000000..9fdb756 --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt @@ -0,0 +1,83 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUCraftingRecipe +import io.github.moulberry.repo.data.NEUForgeRecipe +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import kotlin.math.cos +import kotlin.math.sin +import kotlin.time.Duration.Companion.seconds +import net.minecraft.block.Blocks +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.tr + +object SBForgeRecipeRenderer : GenericRecipeRenderer<NEUForgeRecipe> { + override fun render( + recipe: NEUForgeRecipe, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack?, + ) { + val arrow = layouter.createArrow(bounds.minX + 90, bounds.minY + 54 - 18 / 2) + layouter.createTooltip( + arrow, + Text.stringifiedTranslatable( + "firmament.recipe.forge.time", + recipe.duration.seconds + ) + ) + + val ingredientsCenter = Point(bounds.minX + 49 - 8, bounds.minY + 54 - 8) + layouter.createFire(ingredientsCenter, 25) + val count = recipe.inputs.size + if (count == 1) { + layouter.createItemSlot( + ingredientsCenter.x, ingredientsCenter.y - 18, + SBItemStack(recipe.inputs.single()), + RecipeLayouter.SlotKind.SMALL_INPUT, + ) + } else { + recipe.inputs.forEachIndexed { idx, ingredient -> + val rad = Math.PI * 2 * idx / count + layouter.createItemSlot( + (ingredientsCenter.x + cos(rad) * 30).toInt(), (ingredientsCenter.y + sin(rad) * 30).toInt(), + SBItemStack(ingredient), + RecipeLayouter.SlotKind.SMALL_INPUT, + ) + } + } + layouter.createItemSlot( + bounds.minX + 124, bounds.minY + 46, + SBItemStack(recipe.outputStack), + RecipeLayouter.SlotKind.BIG_OUTPUT + ) + } + + override val displayHeight: Int + get() = 104 + + override fun getInputs(recipe: NEUForgeRecipe): Collection<SBItemStack> { + return recipe.inputs.mapNotNull { SBItemStack(it) } + } + + override fun getOutputs(recipe: NEUForgeRecipe): Collection<SBItemStack> { + return listOfNotNull(SBItemStack(recipe.outputStack)) + } + + override val icon: ItemStack = ItemStack(Blocks.ANVIL) + override val title: Text = tr("firmament.category.forge", "Forge Recipes") + override val identifier: Identifier = Firmament.identifier("forge_recipe") + + override fun findAllRecipes(neuRepository: NEURepository): Iterable<NEUForgeRecipe> { + // TODO: theres gotta be an index for these tbh. + return neuRepository.items.items.values.flatMap { it.recipes }.filterIsInstance<NEUForgeRecipe>() + } + + override val typ: Class<NEUForgeRecipe> + get() = NEUForgeRecipe::class.java +} diff --git a/src/main/kotlin/util/Base64Util.kt b/src/main/kotlin/util/Base64Util.kt index 44bcdfd..c39c601 100644 --- a/src/main/kotlin/util/Base64Util.kt +++ b/src/main/kotlin/util/Base64Util.kt @@ -1,7 +1,14 @@ package moe.nea.firmament.util +import java.util.Base64 + object Base64Util { + fun decodeString(str: String): String { + return Base64.getDecoder().decode(str.padToValidBase64()) + .decodeToString() + } + fun String.padToValidBase64(): String { val align = this.length % 4 if (align == 0) return this diff --git a/src/main/kotlin/util/ErrorUtil.kt b/src/main/kotlin/util/ErrorUtil.kt index 190381d..e82d369 100644 --- a/src/main/kotlin/util/ErrorUtil.kt +++ b/src/main/kotlin/util/ErrorUtil.kt @@ -38,6 +38,8 @@ object ErrorUtil { } class Catch<T> private constructor(val value: T?, val exc: Throwable?) { + fun orNull(): T? = value + inline fun or(block: (exc: Throwable) -> T): T { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) @@ -73,4 +75,7 @@ object ErrorUtil { return nullable } + fun softUserError(string: String) { + MC.sendChat(tr("frimanet.usererror", "Firmament encountered a user caused error: $string")) + } } diff --git a/src/main/kotlin/util/FirmFormatters.kt b/src/main/kotlin/util/FirmFormatters.kt index acb7102..03dafc5 100644 --- a/src/main/kotlin/util/FirmFormatters.kt +++ b/src/main/kotlin/util/FirmFormatters.kt @@ -13,6 +13,7 @@ import kotlin.math.roundToInt import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos object FirmFormatters { @@ -131,4 +132,11 @@ object FirmFormatters { return if (boolean == trueIsGood) text.lime() else text.red() } + fun formatPosition(position: BlockPos): Text { + return Text.literal("x: ${position.x}, y: ${position.y}, z: ${position.z}") + } + + fun formatPercent(value: Double, decimals: Int = 1): String { + return "%.${decimals}f%%".format(value * 100) + } } diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt index ca3742d..e85b119 100644 --- a/src/main/kotlin/util/MC.kt +++ b/src/main/kotlin/util/MC.kt @@ -1,7 +1,9 @@ package moe.nea.firmament.util import io.github.moulberry.repo.data.Coordinate +import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.jvm.optionals.getOrNull import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.hud.InGameHud import net.minecraft.client.gui.screen.Screen @@ -16,10 +18,14 @@ import net.minecraft.item.Item import net.minecraft.item.ItemStack import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket import net.minecraft.registry.BuiltinRegistries +import net.minecraft.registry.Registry +import net.minecraft.registry.RegistryKey import net.minecraft.registry.RegistryKeys import net.minecraft.registry.RegistryWrapper import net.minecraft.resource.ReloadableResourceManagerImpl import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.Util import net.minecraft.util.math.BlockPos import net.minecraft.world.World import moe.nea.firmament.events.TickEvent @@ -99,7 +105,7 @@ object MC { inline val soundManager get() = instance.soundManager inline val player: ClientPlayerEntity? get() = TestUtil.unlessTesting { instance.player } inline val camera: Entity? get() = instance.cameraEntity - inline val stackInHand: ItemStack? get() = player?.inventory?.mainHandStack + inline val stackInHand: ItemStack get() = player?.mainHandStack ?: ItemStack.EMPTY inline val guiAtlasManager get() = instance.guiAtlasManager inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world } inline val playerName: String? get() = player?.name?.unformattedString @@ -113,12 +119,32 @@ object MC { val defaultRegistries: RegistryWrapper.WrapperLookup by lazy { BuiltinRegistries.createWrapperLookup() } inline val currentOrDefaultRegistries get() = currentRegistries ?: defaultRegistries val defaultItems: RegistryWrapper.Impl<Item> by lazy { defaultRegistries.getOrThrow(RegistryKeys.ITEM) } + var currentTick = 0 var lastWorld: World? = null get() { field = world ?: field return field } private set + + val currentMoulConfigContext + get() = (screen as? GuiComponentWrapper)?.context + + fun openUrl(uri: String) { + Util.getOperatingSystem().open(uri) + } + + fun <T> unsafeGetRegistryEntry(registry: RegistryKey<out Registry<T>>, identifier: Identifier) = + unsafeGetRegistryEntry(RegistryKey.of(registry, identifier)) + + + fun <T> unsafeGetRegistryEntry(registryKey: RegistryKey<T>): T? { + return currentOrDefaultRegistries + .getOrThrow(registryKey.registryRef) + .getOptional(registryKey) + .getOrNull() + ?.value() + } } diff --git a/src/main/kotlin/util/MoulConfigUtils.kt b/src/main/kotlin/util/MoulConfigUtils.kt index 362a4d9..a9e3241 100644 --- a/src/main/kotlin/util/MoulConfigUtils.kt +++ b/src/main/kotlin/util/MoulConfigUtils.kt @@ -9,6 +9,7 @@ import io.github.notenoughupdates.moulconfig.gui.GuiContext import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent import io.github.notenoughupdates.moulconfig.observer.GetSetter import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext import io.github.notenoughupdates.moulconfig.xml.ChildCount @@ -20,6 +21,7 @@ import java.io.File import java.util.function.Supplier import javax.xml.namespace.QName import me.shedaniel.math.Color +import org.jetbrains.annotations.Unmodifiable import org.w3c.dom.Element import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -35,6 +37,19 @@ import moe.nea.firmament.gui.TickComponent import moe.nea.firmament.util.render.isUntranslatedGuiDrawContext 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 universe = XMLUniverse.getDefaultUniverse().also { uni -> uni.registerMapper(java.awt.Color::class.java) { @@ -179,10 +194,8 @@ object MoulConfigUtils { uni.registerLoader(object : XMLGuiLoader.Basic<FixedComponent> { override fun createInstance(context: XMLContext<*>, element: Element): FixedComponent { return FixedComponent( - context.getPropertyFromAttribute(element, QName("width"), Int::class.java) - ?: error("Requires width specified"), - context.getPropertyFromAttribute(element, QName("height"), Int::class.java) - ?: error("Requires height specified"), + context.getPropertyFromAttribute(element, QName("width"), Int::class.java), + context.getPropertyFromAttribute(element, QName("height"), Int::class.java), context.getChildFragment(element) ) } @@ -196,7 +209,7 @@ object MoulConfigUtils { } override fun getAttributeNames(): Map<String, Boolean> { - return mapOf("width" to true, "height" to true) + return mapOf("width" to false, "height" to false) } }) } @@ -210,19 +223,6 @@ object MoulConfigUtils { 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 { return object : GuiComponentWrapper(loadGui(name, bindTo)) { override fun close() { diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt index b2f9449..1a4734c 100644 --- a/src/main/kotlin/util/SBData.kt +++ b/src/main/kotlin/util/SBData.kt @@ -31,6 +31,10 @@ object SBData { val hypixelTimeZone = ZoneId.of("US/Eastern") private var hasReceivedProfile = false var locraw: Locraw? = null + + /** + * The current server location the player is in. This will be null outside of SkyBlock. + */ val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation val hasValidLocraw get() = locraw?.server !in listOf("limbo", null) val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK" diff --git a/src/main/kotlin/util/SkyBlockIsland.kt b/src/main/kotlin/util/SkyBlockIsland.kt index a86543c..e7f955a 100644 --- a/src/main/kotlin/util/SkyBlockIsland.kt +++ b/src/main/kotlin/util/SkyBlockIsland.kt @@ -1,4 +1,3 @@ - package moe.nea.firmament.util import kotlinx.serialization.KSerializer @@ -13,33 +12,41 @@ import moe.nea.firmament.repo.RepoManager @Serializable(with = SkyBlockIsland.Serializer::class) class SkyBlockIsland private constructor( - val locrawMode: String, + val locrawMode: String, ) { - object Serializer : KSerializer<SkyBlockIsland> { - override val descriptor: SerialDescriptor - get() = PrimitiveSerialDescriptor("SkyBlockIsland", PrimitiveKind.STRING) - - override fun deserialize(decoder: Decoder): SkyBlockIsland { - return forMode(decoder.decodeString()) - } - - override fun serialize(encoder: Encoder, value: SkyBlockIsland) { - encoder.encodeString(value.locrawMode) - } - } - companion object { - private val allIslands = mutableMapOf<String, SkyBlockIsland>() - fun forMode(mode: String): SkyBlockIsland = allIslands.computeIfAbsent(mode, ::SkyBlockIsland) - val HUB = forMode("hub") - val PRIVATE_ISLAND = forMode("dynamic") - val RIFT = forMode("rift") - val MINESHAFT = forMode("mineshaft") - val GARDEN = forMode("garden") - val DUNGEON = forMode("dungeon") - } - - val userFriendlyName - get() = RepoManager.neuRepo.constants.islands.areaNames - .getOrDefault(locrawMode, locrawMode) + object Serializer : KSerializer<SkyBlockIsland> { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("SkyBlockIsland", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): SkyBlockIsland { + return forMode(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: SkyBlockIsland) { + encoder.encodeString(value.locrawMode) + } + } + + companion object { + private val allIslands = mutableMapOf<String, SkyBlockIsland>() + fun forMode(mode: String): SkyBlockIsland = allIslands.computeIfAbsent(mode, ::SkyBlockIsland) + val HUB = forMode("hub") + val DWARVEN_MINES = forMode("dwarven_mines") + val CRYSTAL_HOLLOWS = forMode("crystal_hollows") + val CRIMSON_ISLE = forMode("crimson_isle") + val PRIVATE_ISLAND = forMode("dynamic") + val RIFT = forMode("rift") + val MINESHAFT = forMode("mineshaft") + val GARDEN = forMode("garden") + val DUNGEON = forMode("dungeon") + val NIL = forMode("_") + } + + val hasCustomMining + get() = RepoManager.miningData.customMiningAreas[this]?.isSpecialMining ?: false + + val userFriendlyName + get() = RepoManager.neuRepo.constants.islands.areaNames + .getOrDefault(locrawMode, locrawMode) } diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt index a99afda..7d8c96c 100644 --- a/src/main/kotlin/util/SkyblockId.kt +++ b/src/main/kotlin/util/SkyblockId.kt @@ -21,6 +21,7 @@ import net.minecraft.network.RegistryByteBuf import net.minecraft.network.codec.PacketCodec import net.minecraft.network.codec.PacketCodecs import net.minecraft.util.Identifier +import moe.nea.firmament.repo.ExpLadders import moe.nea.firmament.repo.ItemCache.asItemStack import moe.nea.firmament.repo.set import moe.nea.firmament.util.collections.WeakCache @@ -28,13 +29,13 @@ import moe.nea.firmament.util.json.DashlessUUIDSerializer /** * A SkyBlock item id, as used by the NEU repo. - * This is not exactly the format used by HyPixel, but is mostly the same. - * Usually this id splits an id used by HyPixel into more sub items. For example `PET` becomes `$PET_ID;$PET_RARITY`, + * This is not exactly the format used by Hypixel, but is mostly the same. + * Usually this id splits an id used by Hypixel into more sub items. For example `PET` becomes `$PET_ID;$PET_RARITY`, * with those values extracted from other metadata. */ @JvmInline @Serializable -value class SkyblockId(val neuItem: String) { +value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> { val identifier get() = Identifier.of("skyblockitem", neuItem.lowercase().replace(";", "__") @@ -48,8 +49,12 @@ value class SkyblockId(val neuItem: String) { return neuItem } + override fun compareTo(other: SkyblockId): Int { + return neuItem.compareTo(other.neuItem) + } + /** - * A bazaar stock item id, as returned by the HyPixel bazaar api endpoint. + * A bazaar stock item id, as returned by the Hypixel bazaar api endpoint. * These are not equivalent to the in-game ids, or the NEU repo ids, and in fact, do not refer to items, but instead * to bazaar stocks. The main difference from [SkyblockId]s is concerning enchanted books. There are probably more, * but for now this holds. @@ -100,8 +105,10 @@ data class HypixelPetInfo( val candyUsed: Int = 0, val uuid: UUID? = null, val active: Boolean = false, + val heldItem: String? = null, ) { val skyblockId get() = SkyblockId("${type.uppercase()};${tier.ordinal}") // TODO: is this ordinal set up correctly? + val level get() = ExpLadders.getExpLadder(type, tier).getPetLevel(exp) } private val jsonparser = Json { ignoreUnknownKeys = true } @@ -126,13 +133,14 @@ fun ItemStack.modifyExtraAttributes(block: (NbtCompound) -> Unit) { } val ItemStack.skyblockUUIDString: String? - get() = extraAttributes.getString("uuid")?.takeIf { it.isNotBlank() } + get() = extraAttributes.getString("uuid").getOrNull()?.takeIf { it.isNotBlank() } val ItemStack.skyblockUUID: UUID? get() = skyblockUUIDString?.let { UUID.fromString(it) } private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo>>("PetInfo") { val jsonString = it.extraAttributes.getString("petInfo") + .getOrNull() if (jsonString.isNullOrBlank()) return@memoize Optional.empty() ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") { jsonparser.decodeFromString<HypixelPetInfo>(jsonString) @@ -141,8 +149,8 @@ private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo> } fun ItemStack.getUpgradeStars(): Int { - return extraAttributes.getInt("upgrade_level").takeIf { it > 0 } - ?: extraAttributes.getInt("dungeon_item_level").takeIf { it > 0 } + return extraAttributes.getInt("upgrade_level").getOrNull()?.takeIf { it > 0 } + ?: extraAttributes.getInt("dungeon_item_level").getOrNull()?.takeIf { it > 0 } ?: 0 } @@ -151,7 +159,7 @@ fun ItemStack.getUpgradeStars(): Int { value class ReforgeId(val id: String) fun ItemStack.getReforgeId(): ReforgeId? { - return extraAttributes.getString("modifier").takeIf { it.isNotBlank() }?.let(::ReforgeId) + return extraAttributes.getString("modifier").getOrNull()?.takeIf { it.isNotBlank() }?.let(::ReforgeId) } val ItemStack.petData: HypixelPetInfo? @@ -165,8 +173,8 @@ fun ItemStack.setSkyBlockId(skyblockId: SkyblockId): ItemStack { val ItemStack.skyBlockId: SkyblockId? get() { - return when (val id = extraAttributes.getString("id")) { - "" -> { + return when (val id = extraAttributes.getString("id").getOrNull()) { + "", null -> { null } @@ -176,23 +184,63 @@ val ItemStack.skyBlockId: SkyblockId? "RUNE", "UNIQUE_RUNE" -> { val runeData = extraAttributes.getCompound("runes") - val runeKind = runeData.keys.singleOrNull() + .getOrNull() + val runeKind = runeData?.keys?.singleOrNull() if (runeKind == null) SkyblockId("RUNE") - else SkyblockId("${runeKind.uppercase()}_RUNE;${runeData.getInt(runeKind)}") + else SkyblockId("${runeKind.uppercase()}_RUNE;${runeData.getInt(runeKind).getOrNull()}") } "ABICASE" -> { - SkyblockId("ABICASE_${extraAttributes.getString("model").uppercase()}") + SkyblockId("ABICASE_${extraAttributes.getString("model").getOrNull()?.uppercase()}") } "ENCHANTED_BOOK" -> { val enchantmentData = extraAttributes.getCompound("enchantments") - val enchantName = enchantmentData.keys.singleOrNull() + .getOrNull() + val enchantName = enchantmentData?.keys?.singleOrNull() if (enchantName == null) SkyblockId("ENCHANTED_BOOK") - else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName)}") + else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName).getOrNull()}") + } + + "ATTRIBUTE_SHARD" -> { + val attributeData = extraAttributes.getCompound("attributes").getOrNull() + val attributeName = attributeData?.keys?.singleOrNull() + if (attributeName == null) SkyblockId("ATTRIBUTE_SHARD") + else SkyblockId( + "ATTRIBUTE_SHARD_${attributeName.uppercase()};${ + attributeData.getInt(attributeName).getOrNull() + }" + ) + } + + "POTION" -> { + val potionData = extraAttributes.getString("potion").getOrNull() + val potionName = extraAttributes.getString("potion_name").getOrNull() + val potionLevel = extraAttributes.getInt("potion_level").getOrNull() + val potionType = extraAttributes.getString("potion_type").getOrNull() + when { + potionName != null -> SkyblockId("POTION_${potionName.uppercase()};$potionLevel") + potionData != null -> SkyblockId("POTION_${potionData.uppercase()};$potionLevel") + potionType != null -> SkyblockId("POTION_${potionType.uppercase()}") + else -> SkyblockId("WATER_BOTTLE") + } + } + + "PARTY_HAT_SLOTH", "PARTY_HAT_CRAB", "PARTY_HAT_CRAB_ANIMATED" -> { + val partyHatEmoji = extraAttributes.getString("party_hat_emoji").getOrNull() + val partyHatYear = extraAttributes.getInt("party_hat_year").getOrNull() + val partyHatColor = extraAttributes.getString("party_hat_color").getOrNull() + when { + partyHatEmoji != null -> SkyblockId("PARTY_HAT_SLOTH_${partyHatEmoji.uppercase()}") + partyHatYear == 2022 -> SkyblockId("PARTY_HAT_CRAB_${partyHatColor?.uppercase()}_ANIMATED") + else -> SkyblockId("PARTY_HAT_CRAB_${partyHatColor?.uppercase()}") + } + } + + "BALLOON_HAT_2024" -> { + SkyblockId("BALLOON_HAT_2024_${extraAttributes.getString("party_hat_color").getOrNull()?.uppercase()}") } - // TODO: PARTY_HAT_CRAB{,_ANIMATED,_SLOTH},POTION else -> { SkyblockId(id) } diff --git a/src/main/kotlin/util/TestUtil.kt b/src/main/kotlin/util/TestUtil.kt index 45e3dde..da8ba38 100644 --- a/src/main/kotlin/util/TestUtil.kt +++ b/src/main/kotlin/util/TestUtil.kt @@ -2,6 +2,7 @@ package moe.nea.firmament.util object TestUtil { inline fun <T> unlessTesting(block: () -> T): T? = if (isInTest) null else block() + @JvmField val isInTest = Thread.currentThread().stackTrace.any { it.className.startsWith("org.junit.") || it.className.startsWith("io.kotest.") diff --git a/src/main/kotlin/util/asm/AsmAnnotationUtil.kt b/src/main/kotlin/util/asm/AsmAnnotationUtil.kt new file mode 100644 index 0000000..fb0e92c --- /dev/null +++ b/src/main/kotlin/util/asm/AsmAnnotationUtil.kt @@ -0,0 +1,89 @@ +package moe.nea.firmament.util.asm + +import com.google.common.base.Defaults +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.lang.reflect.Proxy +import org.objectweb.asm.Type +import org.objectweb.asm.tree.AnnotationNode + +object AsmAnnotationUtil { + class AnnotationProxy( + val originalType: Class<out Annotation>, + val annotationNode: AnnotationNode, + ) : InvocationHandler { + val offsets = annotationNode.values.withIndex() + .chunked(2) + .map { it.first() } + .associate { (idx, value) -> value as String to idx + 1 } + + fun nestArrayType(depth: Int, comp: Class<*>): Class<*> = + if (depth == 0) comp + else java.lang.reflect.Array.newInstance(nestArrayType(depth - 1, comp), 0).javaClass + + fun unmap( + value: Any?, + comp: Class<*>, + depth: Int, + ): Any? { + value ?: return null + if (depth > 0) + return ((value as List<Any>) + .map { unmap(it, comp, depth - 1) } as java.util.List<Any>) + .toArray(java.lang.reflect.Array.newInstance(nestArrayType(depth - 1, comp), 0) as Array<*>) + if (comp.isEnum) { + comp as Class<out Enum<*>> + when (value) { + is String -> return java.lang.Enum.valueOf(comp, value) + is List<*> -> return java.lang.Enum.valueOf(comp, value[1] as String) + else -> error("Unknown enum variant $value for $comp") + } + } + when (value) { + is Type -> return Class.forName(value.className) + is AnnotationNode -> return createProxy(comp as Class<out Annotation>, value) + is String, is Boolean, is Byte, is Double, is Int, is Float, is Long, is Short, is Char -> return value + } + error("Unknown enum variant $value for $comp") + } + + fun defaultFor(fullType: Class<*>): Any? { + if (fullType.isArray) return java.lang.reflect.Array.newInstance(fullType.componentType, 0) + if (fullType.isPrimitive) { + return Defaults.defaultValue(fullType) + } + if (fullType == String::class.java) + return "" + return null + } + + override fun invoke( + proxy: Any, + method: Method, + args: Array<out Any?>? + ): Any? { + val name = method.name + val ret = method.returnType + val retU = generateSequence(ret) { if (it.isArray) it.componentType else null } + .toList() + val arrayDepth = retU.size - 1 + val componentType = retU.last() + + val off = offsets[name] + if (off == null) { + return defaultFor(ret) + } + return unmap(annotationNode.values[off], componentType, arrayDepth) + } + } + + fun <T : Annotation> createProxy( + annotationClass: Class<T>, + annotationNode: AnnotationNode + ): T { + require(Type.getType(annotationClass) == Type.getType(annotationNode.desc)) + return Proxy.newProxyInstance(javaClass.classLoader, + arrayOf(annotationClass), + AnnotationProxy(annotationClass, annotationNode)) as T + } +} diff --git a/src/main/kotlin/util/collections/WeakCache.kt b/src/main/kotlin/util/collections/WeakCache.kt index 38f9886..4a48c63 100644 --- a/src/main/kotlin/util/collections/WeakCache.kt +++ b/src/main/kotlin/util/collections/WeakCache.kt @@ -9,102 +9,108 @@ import moe.nea.firmament.features.debug.DebugLogger * the key. Each key can have additional extra data that is used to look up values. That extra data is not required to * be a life reference. The main Key is compared using strict reference equality. This map is not synchronized. */ -class WeakCache<Key : Any, ExtraKey : Any, Value : Any>(val name: String) { - private val queue = object : ReferenceQueue<Key>() {} - private val map = mutableMapOf<Ref, Value>() - - val size: Int - get() { - clearOldReferences() - return map.size - } - - fun clearOldReferences() { - var successCount = 0 - var totalCount = 0 - while (true) { - val reference = queue.poll() ?: break - totalCount++ - if (map.remove(reference) != null) - successCount++ - } - if (totalCount > 0) - logger.log { "Cleared $successCount/$totalCount references from queue" } - } - - fun get(key: Key, extraData: ExtraKey): Value? { - clearOldReferences() - return map[Ref(key, extraData)] - } - - fun put(key: Key, extraData: ExtraKey, value: Value) { - clearOldReferences() - map[Ref(key, extraData)] = value - } - - fun getOrPut(key: Key, extraData: ExtraKey, value: (Key, ExtraKey) -> Value): Value { - clearOldReferences() - return map.getOrPut(Ref(key, extraData)) { value(key, extraData) } - } - - fun clear() { - map.clear() - } - - init { - allInstances.add(this) - } - - companion object { - val allInstances = InstanceList<WeakCache<*, *, *>>("WeakCaches") - private val logger = DebugLogger("WeakCache") - fun <Key : Any, Value : Any> memoize(name: String, function: (Key) -> Value): - CacheFunction.NoExtraData<Key, Value> { - return CacheFunction.NoExtraData(WeakCache(name), function) - } - - fun <Key : Any, ExtraKey : Any, Value : Any> memoize(name: String, function: (Key, ExtraKey) -> Value): - CacheFunction.WithExtraData<Key, ExtraKey, Value> { - return CacheFunction.WithExtraData(WeakCache(name), function) - } - } - - inner class Ref( - weakInstance: Key, - val extraData: ExtraKey, - ) : WeakReference<Key>(weakInstance, queue) { - val hashCode = System.identityHashCode(weakInstance) * 31 + extraData.hashCode() - override fun equals(other: Any?): Boolean { - if (other !is WeakCache<*, *, *>.Ref) return false - return other.hashCode == this.hashCode - && other.get() === this.get() - && other.extraData == this.extraData - } - - override fun hashCode(): Int { - return hashCode - } - } - - interface CacheFunction { - val cache: WeakCache<*, *, *> - - data class NoExtraData<Key : Any, Value : Any>( - override val cache: WeakCache<Key, Unit, Value>, - val wrapped: (Key) -> Value, - ) : CacheFunction, (Key) -> Value { - override fun invoke(p1: Key): Value { - return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) }) - } - } - - data class WithExtraData<Key : Any, ExtraKey : Any, Value : Any>( - override val cache: WeakCache<Key, ExtraKey, Value>, - val wrapped: (Key, ExtraKey) -> Value, - ) : CacheFunction, (Key, ExtraKey) -> Value { - override fun invoke(p1: Key, p2: ExtraKey): Value { - return cache.getOrPut(p1, p2, wrapped) - } - } - } +open class WeakCache<Key : Any, ExtraKey : Any, Value : Any>(val name: String) { + private val queue = object : ReferenceQueue<Key>() {} + private val map = mutableMapOf<Ref, Value>() + + val size: Int + get() { + clearOldReferences() + return map.size + } + + fun clearOldReferences() { + var successCount = 0 + var totalCount = 0 + while (true) { + val reference = queue.poll() as WeakCache<*, *, *>.Ref? ?: break + totalCount++ + if (reference.shouldBeEvicted() && map.remove(reference) != null) + successCount++ + } + if (totalCount > 0) + logger.log("Cleared $successCount/$totalCount references from queue") + } + + open fun mkRef(key: Key, extraData: ExtraKey): Ref { + return Ref(key, extraData) + } + + fun get(key: Key, extraData: ExtraKey): Value? { + clearOldReferences() + return map[mkRef(key, extraData)] + } + + fun put(key: Key, extraData: ExtraKey, value: Value) { + clearOldReferences() + map[mkRef(key, extraData)] = value + } + + fun getOrPut(key: Key, extraData: ExtraKey, value: (Key, ExtraKey) -> Value): Value { + clearOldReferences() + return map.getOrPut(mkRef(key, extraData)) { value(key, extraData) } + } + + fun clear() { + map.clear() + } + + init { + allInstances.add(this) + } + + companion object { + val allInstances = InstanceList<WeakCache<*, *, *>>("WeakCaches") + private val logger = DebugLogger("WeakCache") + fun <Key : Any, Value : Any> memoize(name: String, function: (Key) -> Value): + CacheFunction.NoExtraData<Key, Value> { + return CacheFunction.NoExtraData(WeakCache(name), function) + } + + fun <Key : Any, ExtraKey : Any, Value : Any> dontMemoize(name: String, function: (Key, ExtraKey) -> Value) = function + fun <Key : Any, ExtraKey : Any, Value : Any> memoize(name: String, function: (Key, ExtraKey) -> Value): + CacheFunction.WithExtraData<Key, ExtraKey, Value> { + return CacheFunction.WithExtraData(WeakCache(name), function) + } + } + + open inner class Ref( + weakInstance: Key, + val extraData: ExtraKey, + ) : WeakReference<Key>(weakInstance, queue) { + open fun shouldBeEvicted() = true + val hashCode = System.identityHashCode(weakInstance) * 31 + extraData.hashCode() + override fun equals(other: Any?): Boolean { + if (other !is WeakCache<*, *, *>.Ref) return false + return other.hashCode == this.hashCode + && other.get() === this.get() + && other.extraData == this.extraData + } + + override fun hashCode(): Int { + return hashCode + } + } + + interface CacheFunction { + val cache: WeakCache<*, *, *> + + data class NoExtraData<Key : Any, Value : Any>( + override val cache: WeakCache<Key, Unit, Value>, + val wrapped: (Key) -> Value, + ) : CacheFunction, (Key) -> Value { + override fun invoke(p1: Key): Value { + return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) }) + } + } + + data class WithExtraData<Key : Any, ExtraKey : Any, Value : Any>( + override val cache: WeakCache<Key, ExtraKey, Value>, + val wrapped: (Key, ExtraKey) -> Value, + ) : CacheFunction, (Key, ExtraKey) -> Value { + override fun invoke(p1: Key, p2: ExtraKey): Value { + return cache.getOrPut(p1, p2, wrapped) + } + } + } } diff --git a/src/main/kotlin/util/compatloader/CompatLoader.kt b/src/main/kotlin/util/compatloader/CompatLoader.kt index 6b60e87..d1073af 100644 --- a/src/main/kotlin/util/compatloader/CompatLoader.kt +++ b/src/main/kotlin/util/compatloader/CompatLoader.kt @@ -6,7 +6,7 @@ import kotlin.reflect.KClass import kotlin.streams.asSequence import moe.nea.firmament.Firmament -abstract class CompatLoader<T : Any>(val kClass: Class<T>) { +open class CompatLoader<T : Any>(val kClass: Class<T>) { constructor(kClass: KClass<T>) : this(kClass.java) val loader: ServiceLoader<T> = ServiceLoader.load(kClass) diff --git a/src/main/kotlin/util/compatloader/CompatMeta.kt b/src/main/kotlin/util/compatloader/CompatMeta.kt new file mode 100644 index 0000000..cf63645 --- /dev/null +++ b/src/main/kotlin/util/compatloader/CompatMeta.kt @@ -0,0 +1,48 @@ +package moe.nea.firmament.util.compatloader + +import java.util.ServiceLoader +import moe.nea.firmament.events.subscription.SubscriptionList +import moe.nea.firmament.init.AutoDiscoveryPlugin +import moe.nea.firmament.util.ErrorUtil + +/** + * Declares the compat meta interface for the current source set. + * This is used by [CompatLoader], [SubscriptionList], and [AutoDiscoveryPlugin]. Annotate a [ICompatMeta] object with + * this. + */ +annotation class CompatMeta + +interface ICompatMetaGen { + fun owns(className: String): Boolean + val meta: ICompatMeta +} + +interface ICompatMeta { + fun shouldLoad(): Boolean + + companion object { + val allMetas = ServiceLoader + .load(ICompatMetaGen::class.java) + .toList() + + fun shouldLoad(className: String): Boolean { + // TODO: replace this with a more performant package lookup + val meta = if (ErrorUtil.aggressiveErrors) { + val fittingMetas = allMetas.filter { it.owns(className) } + require(fittingMetas.size == 1) { "Orphaned or duplicate owned class $className (${fittingMetas.map { it.meta }}). Consider adding a @CompatMeta object." } + fittingMetas.single() + } else { + allMetas.firstOrNull { it.owns(className) } + } + return meta?.meta?.shouldLoad() ?: true + } + } +} + +object CompatHelper { + fun isOwnedByPackage(className: String, vararg packages: String): Boolean { + // TODO: create package lookup structure once + val packageName = className.substringBeforeLast('.') + return packageName in packages + } +} diff --git a/src/main/kotlin/util/data/MultiFileDataHolder.kt b/src/main/kotlin/util/data/MultiFileDataHolder.kt new file mode 100644 index 0000000..94c6f05 --- /dev/null +++ b/src/main/kotlin/util/data/MultiFileDataHolder.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.util.data + +import kotlinx.serialization.KSerializer +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteExisting +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.readText +import kotlin.io.path.writeText +import moe.nea.firmament.Firmament + +abstract class MultiFileDataHolder<T>( + val dataSerializer: KSerializer<T>, + val configName: String +) { // TODO: abstract this + ProfileSpecificDataHolder + val configDirectory = Firmament.CONFIG_DIR.resolve(configName) + private var allData = readValues() + protected fun readValues(): MutableMap<String, T> { + if (!configDirectory.exists()) { + configDirectory.createDirectories() + } + val profileFiles = configDirectory.listDirectoryEntries() + return profileFiles + .filter { it.extension == "json" } + .mapNotNull { + try { + it.nameWithoutExtension to Firmament.json.decodeFromString(dataSerializer, it.readText()) + } catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/ + IDataHolder.badLoads.add(configName) + Firmament.logger.error( + "Exception during loading of multi file data holder $it ($configName). This will reset that profiles config.", + e + ) + null + } + }.toMap().toMutableMap() + } + + fun save() { + if (!configDirectory.exists()) { + configDirectory.createDirectories() + } + val c = allData + configDirectory.listDirectoryEntries().forEach { + if (it.nameWithoutExtension !in c.mapKeys { it.toString() }) { + it.deleteExisting() + } + } + c.forEach { (name, value) -> + val f = configDirectory.resolve("$name.json") + f.writeText(Firmament.json.encodeToString(dataSerializer, value)) + } + } + + fun list(): Map<String, T> = allData + val validPathRegex = "[a-zA-Z0-9_][a-zA-Z0-9\\-_.]*".toPattern() + fun insert(name: String, value: T) { + require(validPathRegex.matcher(name).matches()) { "Not a valid name: $name" } + allData[name] = value + } +} diff --git a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt index acb1dc8..6bafebe 100644 --- a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt +++ b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import moe.nea.firmament.util.parseDashlessUUID +import moe.nea.firmament.util.parsePotentiallyDashlessUUID object DashlessUUIDSerializer : KSerializer<UUID> { override val descriptor: SerialDescriptor = @@ -17,10 +18,7 @@ object DashlessUUIDSerializer : KSerializer<UUID> { override fun deserialize(decoder: Decoder): UUID { val str = decoder.decodeString() - if ("-" in str) { - return UUID.fromString(str) - } - return parseDashlessUUID(str) + return parsePotentiallyDashlessUUID(str) } override fun serialize(encoder: Encoder, value: UUID) { diff --git a/src/main/kotlin/util/math/GChainReconciliation.kt b/src/main/kotlin/util/math/GChainReconciliation.kt new file mode 100644 index 0000000..37998d5 --- /dev/null +++ b/src/main/kotlin/util/math/GChainReconciliation.kt @@ -0,0 +1,102 @@ +package moe.nea.firmament.util.math + +import kotlin.math.min + +/** + * Algorithm for (sort of) cheap reconciliation of two cycles with missing frames. + */ +object GChainReconciliation { + // Step one: Find the most common element and shift the arrays until it is at the start in both (this could be just rotating until minimal levenshtein distance or smth. that would be way better for cycles with duplicates, but i do not want to implement levenshtein as well) + // Step two: Find the first different element. + // Step three: Find the next index of both of the elements. + // Step four: Insert the element that is further away. + + fun <T> Iterable<T>.frequencies(): Map<T, Int> { + val acc = mutableMapOf<T, Int>() + for (t in this) { + acc.compute(t, { _, old -> (old ?: 0) + 1 }) + } + return acc + } + + fun <T> findMostCommonlySharedElement( + leftChain: List<T>, + rightChain: List<T>, + ): T { + val lf = leftChain.frequencies() + val rf = rightChain.frequencies() + val mostCommonlySharedElement = lf.maxByOrNull { min(it.value, rf[it.key] ?: 0) }?.key + if (mostCommonlySharedElement == null || mostCommonlySharedElement !in rf) + error("Could not find a shared element") + return mostCommonlySharedElement + } + + fun <T> List<T>.getMod(index: Int): T { + return this[index.mod(size)] + } + + fun <T> List<T>.rotated(offset: Int): List<T> { + val newList = mutableListOf<T>() + for (index in indices) { + newList.add(getMod(index - offset)) + } + return newList + } + + fun <T> shiftToFront(list: List<T>, element: T): List<T> { + val shiftDistance = list.indexOf(element) + require(shiftDistance >= 0) + return list.rotated(-shiftDistance) + } + + fun <T> List<T>.indexOfOrMaxInt(element: T): Int = indexOf(element).takeUnless { it < 0 } ?: Int.MAX_VALUE + + fun <T> reconcileCycles( + leftChain: List<T>, + rightChain: List<T>, + ): List<T> { + val mostCommonElement = findMostCommonlySharedElement(leftChain, rightChain) + val left = shiftToFront(leftChain, mostCommonElement).toMutableList() + val right = shiftToFront(rightChain, mostCommonElement).toMutableList() + + var index = 0 + while (index < left.size && index < right.size) { + val leftEl = left[index] + val rightEl = right[index] + if (leftEl == rightEl) { + index++ + continue + } + val nextLeftInRight = right.subList(index, right.size) + .indexOfOrMaxInt(leftEl) + + val nextRightInLeft = left.subList(index, left.size) + .indexOfOrMaxInt(rightEl) + if (nextLeftInRight < nextRightInLeft) { + left.add(index, rightEl) + } else if (nextRightInLeft < nextLeftInRight) { + right.add(index, leftEl) + } else { + index++ + } + } + return if (left.size < right.size) right else left + } + + fun <T> isValidCycle(longList: List<T>, cycle: List<T>): Boolean { + for ((i, value) in longList.withIndex()) { + if (cycle.getMod(i) != value) + return false + } + return true + } + + fun <T> List<T>.shortenCycle(): List<T> { + for (i in (1..<size)) { + if (isValidCycle(this, subList(0, i))) + return subList(0, i) + } + return this + } + +} diff --git a/src/main/kotlin/util/mc/ArmorUtil.kt b/src/main/kotlin/util/mc/ArmorUtil.kt new file mode 100644 index 0000000..fd1867c --- /dev/null +++ b/src/main/kotlin/util/mc/ArmorUtil.kt @@ -0,0 +1,8 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.LivingEntity + +val LivingEntity.iterableArmorItems + get() = EquipmentSlot.entries.asSequence() + .map { it to getEquippedStack(it) } diff --git a/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt b/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt index 012f52e..0866665 100644 --- a/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt +++ b/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt @@ -1,12 +1,15 @@ package moe.nea.firmament.util.mc import com.mojang.serialization.Codec +import io.netty.buffer.ByteBuf import net.minecraft.component.ComponentType +import net.minecraft.network.codec.PacketCodec import net.minecraft.registry.Registries import net.minecraft.registry.Registry import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ClientInitEvent +import moe.nea.firmament.repo.MiningRepoData object FirmamentDataComponentTypes { @@ -26,11 +29,32 @@ object FirmamentDataComponentTypes { ) } + fun <T> errorCodec(message: String): PacketCodec<in ByteBuf, T> = + object : PacketCodec<ByteBuf, T> { + override fun decode(buf: ByteBuf?): T? { + error(message) + } + + override fun encode(buf: ByteBuf?, value: T?) { + error(message) + } + } + + fun <T, B : ComponentType.Builder<T>> B.neverEncode(message: String = "This element should never be encoded or decoded"): B { + packetCodec(errorCodec(message)) + codec(null) + return this + } + val IS_BROKEN = register<Boolean>( "is_broken" ) { it.codec(Codec.BOOL.fieldOf("is_broken").codec()) } + val CUSTOM_MINING_BLOCK_DATA = register<MiningRepoData.CustomMiningBlock>("custom_mining_block") { + it.neverEncode() + } + } diff --git a/src/main/kotlin/util/mc/NbtPrism.kt b/src/main/kotlin/util/mc/NbtPrism.kt new file mode 100644 index 0000000..f034210 --- /dev/null +++ b/src/main/kotlin/util/mc/NbtPrism.kt @@ -0,0 +1,91 @@ +package moe.nea.firmament.util.mc + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import com.mojang.serialization.JsonOps +import java.util.concurrent.CompletableFuture +import kotlin.collections.indices +import kotlin.collections.map +import kotlin.jvm.optionals.getOrNull +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtList +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.NbtString +import moe.nea.firmament.util.Base64Util + +class NbtPrism(val path: List<String>) { + companion object { + fun fromElement(path: JsonElement): NbtPrism? { + if (path is JsonArray) { + return NbtPrism(path.map { (it as JsonPrimitive).asString }) + } else if (path is JsonPrimitive && path.isString) { + return NbtPrism(path.asString.split(".")) + } + return null + } + } + + object Argument : ArgumentType<NbtPrism> { + override fun parse(reader: StringReader): NbtPrism? { + return fromElement(JsonPrimitive(StringArgumentType.string().parse(reader))) + } + + override fun getExamples(): Collection<String?>? { + return listOf("some.nbt.path", "some.other.*", "some.path.*json.in.a.json.string") + } + } + + override fun toString(): String { + return "Prism($path)" + } + + fun access(root: NbtElement): Collection<NbtElement> { + var rootSet = mutableListOf(root) + var switch = mutableListOf<NbtElement>() + for (pathSegment in path) { + if (pathSegment == ".") continue + if (pathSegment != "*" && pathSegment.startsWith("*")) { + if (pathSegment == "*json") { + for (element in rootSet) { + val eString = element.asString().getOrNull() ?: continue + val element = Gson().fromJson(eString, JsonElement::class.java) + switch.add(JsonOps.INSTANCE.convertTo(NbtOps.INSTANCE, element)) + } + } else if (pathSegment == "*base64") { + for (element in rootSet) { + val string = element.asString().getOrNull() ?: continue + switch.add(NbtString.of(Base64Util.decodeString(string))) + } + } + } + for (element in rootSet) { + if (element is NbtList) { + if (pathSegment == "*") + switch.addAll(element) + val index = pathSegment.toIntOrNull() ?: continue + if (index !in element.indices) continue + switch.add(element[index]) + } + if (element is NbtCompound) { + if (pathSegment == "*") + element.keys.mapTo(switch) { element.get(it)!! } + switch.add(element.get(pathSegment) ?: continue) + } + } + val temp = switch + switch = rootSet + rootSet = temp + switch.clear() + } + return rootSet + } +} diff --git a/src/main/kotlin/util/mc/PlayerUtil.kt b/src/main/kotlin/util/mc/PlayerUtil.kt new file mode 100644 index 0000000..53ef1f4 --- /dev/null +++ b/src/main/kotlin/util/mc/PlayerUtil.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.player.PlayerEntity + + +val PlayerEntity.mainHandStack get() = this.getEquippedStack(EquipmentSlot.MAINHAND) diff --git a/src/main/kotlin/util/mc/SNbtFormatter.kt b/src/main/kotlin/util/mc/SNbtFormatter.kt index e773927..e2c24f6 100644 --- a/src/main/kotlin/util/mc/SNbtFormatter.kt +++ b/src/main/kotlin/util/mc/SNbtFormatter.kt @@ -1,5 +1,6 @@ package moe.nea.firmament.util.mc +import net.minecraft.nbt.AbstractNbtList import net.minecraft.nbt.NbtByte import net.minecraft.nbt.NbtByteArray import net.minecraft.nbt.NbtCompound @@ -38,7 +39,7 @@ class SNbtFormatter private constructor() : NbtElementVisitor { override fun visitString(element: NbtString) { - result.append(NbtString.escape(element.asString())) + result.append(NbtString.escape(element.value)) } override fun visitByte(element: NbtByte) { @@ -65,18 +66,18 @@ class SNbtFormatter private constructor() : NbtElementVisitor { result.append(element.doubleValue()).append("d") } - private fun visitArrayContents(array: List<NbtElement>) { + private fun visitArrayContents(array: AbstractNbtList) { array.forEachIndexed { index, element -> writeIndent() element.accept(this) - if (array.size != index + 1) { + if (array.size() != index + 1) { result.append(",") } result.append("\n") } } - private fun writeArray(arrayTypeTag: String, array: List<NbtElement>) { + private fun writeArray(arrayTypeTag: String, array: AbstractNbtList) { result.append("[").append(arrayTypeTag).append("\n") pushIndent() visitArrayContents(array) diff --git a/src/main/kotlin/util/mc/asFakeServer.kt b/src/main/kotlin/util/mc/asFakeServer.kt new file mode 100644 index 0000000..d3811bd --- /dev/null +++ b/src/main/kotlin/util/mc/asFakeServer.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.util.mc + +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import net.minecraft.server.command.CommandOutput +import net.minecraft.server.command.ServerCommandSource +import net.minecraft.text.Text + +fun FabricClientCommandSource.asFakeServer(): ServerCommandSource { + val source = this + return ServerCommandSource( + object : CommandOutput { + override fun sendMessage(message: Text?) { + source.player.sendMessage(message, false) + } + + override fun shouldReceiveFeedback(): Boolean { + return true + } + + override fun shouldTrackOutput(): Boolean { + return true + } + + override fun shouldBroadcastConsoleToOps(): Boolean { + return true + } + }, + source.position, + source.rotation, + null, + 0, + "FakeServerCommandSource", + Text.literal("FakeServerCommandSource"), + null, + source.player + ) +} diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt index a44435c..f239810 100644 --- a/src/main/kotlin/util/regex.kt +++ b/src/main/kotlin/util/regex.kt @@ -16,12 +16,13 @@ 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? { +inline fun <T> Pattern.useMatch(string: String?, block: Matcher.() -> T): T? { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } - return matcher(string) - .takeIf(Matcher::matches) + return string + ?.let(this::matcher) + ?.takeIf(Matcher::matches) ?.let(block) } diff --git a/src/main/kotlin/util/render/CustomRenderLayers.kt b/src/main/kotlin/util/render/CustomRenderLayers.kt new file mode 100644 index 0000000..7f3cdec --- /dev/null +++ b/src/main/kotlin/util/render/CustomRenderLayers.kt @@ -0,0 +1,74 @@ +package util.render + +import com.mojang.blaze3d.pipeline.RenderPipeline +import com.mojang.blaze3d.platform.DepthTestFunction +import com.mojang.blaze3d.vertex.VertexFormat.DrawMode +import java.util.function.Function +import net.minecraft.client.gl.RenderPipelines +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.RenderPhase +import net.minecraft.client.render.VertexFormats +import net.minecraft.util.Identifier +import net.minecraft.util.TriState +import net.minecraft.util.Util +import moe.nea.firmament.Firmament + +object CustomRenderPipelines { + val GUI_TEXTURED_NO_DEPTH_TRIS = + RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET) + .withVertexFormat(VertexFormats.POSITION_TEXTURE_COLOR, DrawMode.TRIANGLES) + .withLocation(Firmament.identifier("gui_textured_overlay_tris")) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withDepthWrite(false) + .build() + val OMNIPRESENT_LINES = RenderPipeline + .builder(RenderPipelines.RENDERTYPE_LINES_SNIPPET) + .withLocation(Firmament.identifier("lines")) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .build() + val COLORED_OMNIPRESENT_QUADS = + RenderPipeline.builder(RenderPipelines.MATRICES_COLOR_SNIPPET)// TODO: split this up to support better transparent ordering. + .withLocation(Firmament.identifier("colored_omnipresent_quads")) + .withVertexShader("core/position_color") + .withFragmentShader("core/position_color") + .withVertexFormat(VertexFormats.POSITION_COLOR, DrawMode.QUADS) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withDepthWrite(false) + .build() +} + +object CustomRenderLayers { + + + inline fun memoizeTextured(crossinline func: (Identifier) -> RenderLayer) = memoize(func) + inline fun <T, R> memoize(crossinline func: (T) -> R): Function<T, R> { + return Util.memoize { it: T -> func(it) } + } + + val GUI_TEXTURED_NO_DEPTH_TRIS = memoizeTextured { texture -> + RenderLayer.of("firmament_gui_textured_overlay_tris", + RenderLayer.DEFAULT_BUFFER_SIZE, + CustomRenderPipelines.GUI_TEXTURED_NO_DEPTH_TRIS, + RenderLayer.MultiPhaseParameters.builder().texture( + RenderPhase.Texture(texture, TriState.DEFAULT, false)) + .build(false)) + } + val LINES = RenderLayer.of( + "firmament_lines", + RenderLayer.DEFAULT_BUFFER_SIZE, + CustomRenderPipelines.OMNIPRESENT_LINES, + RenderLayer.MultiPhaseParameters.builder() // TODO: accept linewidth here + .build(false) + ) + val COLORED_QUADS = RenderLayer.of( + "firmament_quads", + RenderLayer.DEFAULT_BUFFER_SIZE, + CustomRenderPipelines.COLORED_OMNIPRESENT_QUADS, + RenderLayer.MultiPhaseParameters.builder() + .lightmap(RenderPhase.DISABLE_LIGHTMAP) + .build(false) + ) +} diff --git a/src/main/kotlin/util/render/DrawContextExt.kt b/src/main/kotlin/util/render/DrawContextExt.kt index a143d4d..fa92cd7 100644 --- a/src/main/kotlin/util/render/DrawContextExt.kt +++ b/src/main/kotlin/util/render/DrawContextExt.kt @@ -1,52 +1,24 @@ package moe.nea.firmament.util.render +import com.mojang.blaze3d.pipeline.RenderPipeline +import com.mojang.blaze3d.platform.DepthTestFunction import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.vertex.VertexFormat.DrawMode import me.shedaniel.math.Color import org.joml.Matrix4f +import util.render.CustomRenderLayers +import net.minecraft.client.gl.RenderPipelines import net.minecraft.client.gui.DrawContext import net.minecraft.client.render.RenderLayer -import net.minecraft.client.render.RenderLayer.MultiPhaseParameters -import net.minecraft.client.render.RenderPhase -import net.minecraft.client.render.VertexFormat -import net.minecraft.client.render.VertexFormat.DrawMode import net.minecraft.client.render.VertexFormats import net.minecraft.util.Identifier -import net.minecraft.util.TriState -import net.minecraft.util.Util +import moe.nea.firmament.Firmament import moe.nea.firmament.util.MC fun DrawContext.isUntranslatedGuiDrawContext(): Boolean { return (matrices.peek().positionMatrix.properties() and Matrix4f.PROPERTY_IDENTITY.toInt()) != 0 } -object GuiRenderLayers { - val GUI_TEXTURED_NO_DEPTH = Util.memoize<Identifier, RenderLayer> { texture: Identifier -> - RenderLayer.of("firmament_gui_textured_no_depth", - VertexFormats.POSITION_TEXTURE_COLOR, - DrawMode.QUADS, - DEFAULT_BUFFER_SIZE, - MultiPhaseParameters.builder() - .texture(RenderPhase.Texture(texture, TriState.FALSE, false)) - .program(RenderPhase.POSITION_TEXTURE_COLOR_PROGRAM) - .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) - .depthTest(RenderPhase.ALWAYS_DEPTH_TEST) - .build(false)) - } - val GUI_TEXTURED_TRIS = Util.memoize { texture: Identifier -> - RenderLayer.of("firmament_gui_textured_overlay_tris", - VertexFormats.POSITION_TEXTURE_COLOR, - DrawMode.TRIANGLES, - DEFAULT_BUFFER_SIZE, - MultiPhaseParameters.builder() - .texture(RenderPhase.Texture(texture, TriState.DEFAULT, false)) - .program(RenderPhase.POSITION_TEXTURE_COLOR_PROGRAM) - .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) - .depthTest(RenderPhase.ALWAYS_DEPTH_TEST) - .writeMaskState(RenderPhase.COLOR_MASK) - .build(false)) - } -} - @Deprecated("Use the other drawGuiTexture") fun DrawContext.drawGuiTexture( x: Int, y: Int, z: Int, width: Int, height: Int, sprite: Identifier @@ -91,7 +63,7 @@ fun DrawContext.drawLine(fromX: Int, fromY: Int, toX: Int, toY: Int, color: Colo } RenderSystem.lineWidth(MC.window.scaleFactor.toFloat()) draw { vertexConsumers -> - val buf = vertexConsumers.getBuffer(RenderInWorldContext.RenderLayers.LINES) + val buf = vertexConsumers.getBuffer(CustomRenderLayers.LINES) buf.vertex(fromX.toFloat(), fromY.toFloat(), 0F).color(color.color) .normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F) buf.vertex(toX.toFloat(), toY.toFloat(), 0F).color(color.color) diff --git a/src/main/kotlin/util/render/FacingThePlayerContext.kt b/src/main/kotlin/util/render/FacingThePlayerContext.kt index daa8da9..670beb6 100644 --- a/src/main/kotlin/util/render/FacingThePlayerContext.kt +++ b/src/main/kotlin/util/render/FacingThePlayerContext.kt @@ -1,18 +1,12 @@ package moe.nea.firmament.util.render -import com.mojang.blaze3d.systems.RenderSystem import io.github.notenoughupdates.moulconfig.platform.next import org.joml.Matrix4f import net.minecraft.client.font.TextRenderer -import net.minecraft.client.render.BufferRenderer -import net.minecraft.client.render.GameRenderer import net.minecraft.client.render.LightmapTextureManager import net.minecraft.client.render.RenderLayer -import net.minecraft.client.render.Tessellator import net.minecraft.client.render.VertexConsumer -import net.minecraft.client.render.VertexFormat -import net.minecraft.client.render.VertexFormats import net.minecraft.text.Text import net.minecraft.util.Identifier import net.minecraft.util.math.BlockPos diff --git a/src/main/kotlin/util/render/FirmamentShaders.kt b/src/main/kotlin/util/render/FirmamentShaders.kt index ba67dbb..cc6cd49 100644 --- a/src/main/kotlin/util/render/FirmamentShaders.kt +++ b/src/main/kotlin/util/render/FirmamentShaders.kt @@ -1,9 +1,10 @@ package moe.nea.firmament.util.render +import com.mojang.blaze3d.vertex.VertexFormat +import net.minecraft.client.gl.CompiledShader import net.minecraft.client.gl.Defines -import net.minecraft.client.gl.ShaderProgramKey +import net.minecraft.client.gl.ShaderProgram import net.minecraft.client.render.RenderPhase -import net.minecraft.client.render.VertexFormat import net.minecraft.client.render.VertexFormats import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe @@ -11,20 +12,9 @@ import moe.nea.firmament.events.DebugInstantiateEvent import moe.nea.firmament.util.MC object FirmamentShaders { - val shaders = mutableListOf<ShaderProgramKey>() - - private fun shader(name: String, format: VertexFormat, defines: Defines): ShaderProgramKey { - val key = ShaderProgramKey(Firmament.identifier(name), format, defines) - shaders.add(key) - return key - } - - val LINES = RenderPhase.ShaderProgram(shader("core/rendertype_lines", VertexFormats.LINES, Defines.EMPTY)) @Subscribe fun debugLoad(event: DebugInstantiateEvent) { - shaders.forEach { - MC.instance.shaderLoader.getOrCreateProgram(it) - } + // TODO: do i still need to work with shaders like this? } } diff --git a/src/main/kotlin/util/render/RenderCircleProgress.kt b/src/main/kotlin/util/render/RenderCircleProgress.kt index 805633c..d759033 100644 --- a/src/main/kotlin/util/render/RenderCircleProgress.kt +++ b/src/main/kotlin/util/render/RenderCircleProgress.kt @@ -1,18 +1,12 @@ package moe.nea.firmament.util.render -import com.mojang.blaze3d.systems.RenderSystem import io.github.notenoughupdates.moulconfig.platform.next import org.joml.Matrix4f import org.joml.Vector2f +import util.render.CustomRenderLayers import kotlin.math.atan2 import kotlin.math.tan import net.minecraft.client.gui.DrawContext -import net.minecraft.client.render.BufferRenderer -import net.minecraft.client.render.RenderLayer -import net.minecraft.client.render.RenderPhase -import net.minecraft.client.render.Tessellator -import net.minecraft.client.render.VertexFormat.DrawMode -import net.minecraft.client.render.VertexFormats import net.minecraft.util.Identifier object RenderCircleProgress { @@ -26,9 +20,8 @@ object RenderCircleProgress { v1: Float, v2: Float, ) { - RenderSystem.enableBlend() drawContext.draw { - val bufferBuilder = it.getBuffer(GuiRenderLayers.GUI_TEXTURED_TRIS.apply(texture)) + val bufferBuilder = it.getBuffer(CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture)) val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix val corners = listOf( @@ -86,7 +79,6 @@ object RenderCircleProgress { .next() } } - RenderSystem.disableBlend() } diff --git a/src/main/kotlin/util/render/RenderInWorldContext.kt b/src/main/kotlin/util/render/RenderInWorldContext.kt index bb58200..98b10ca 100644 --- a/src/main/kotlin/util/render/RenderInWorldContext.kt +++ b/src/main/kotlin/util/render/RenderInWorldContext.kt @@ -5,15 +5,12 @@ import io.github.notenoughupdates.moulconfig.platform.next import java.lang.Math.pow import org.joml.Matrix4f import org.joml.Vector3f -import net.minecraft.client.gl.VertexBuffer +import util.render.CustomRenderLayers import net.minecraft.client.render.Camera import net.minecraft.client.render.RenderLayer -import net.minecraft.client.render.RenderPhase import net.minecraft.client.render.RenderTickCounter -import net.minecraft.client.render.Tessellator import net.minecraft.client.render.VertexConsumer import net.minecraft.client.render.VertexConsumerProvider -import net.minecraft.client.render.VertexFormat import net.minecraft.client.render.VertexFormats import net.minecraft.client.texture.Sprite import net.minecraft.client.util.math.MatrixStack @@ -27,47 +24,12 @@ import moe.nea.firmament.util.MC @RenderContextDSL class RenderInWorldContext private constructor( - private val tesselator: Tessellator, val matrixStack: MatrixStack, private val camera: Camera, private val tickCounter: RenderTickCounter, val vertexConsumers: VertexConsumerProvider.Immediate, ) { - object RenderLayers { - val TRANSLUCENT_TRIS = RenderLayer.of("firmament_translucent_tris", - VertexFormats.POSITION_COLOR, - VertexFormat.DrawMode.TRIANGLES, - RenderLayer.CUTOUT_BUFFER_SIZE, - false, true, - RenderLayer.MultiPhaseParameters.builder() - .depthTest(RenderPhase.ALWAYS_DEPTH_TEST) - .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) - .program(RenderPhase.POSITION_COLOR_PROGRAM) - .build(false)) - val LINES = RenderLayer.of("firmament_rendertype_lines", - VertexFormats.LINES, - VertexFormat.DrawMode.LINES, - RenderLayer.CUTOUT_BUFFER_SIZE, - false, false, // do we need translucent? i dont think so - RenderLayer.MultiPhaseParameters.builder() - .depthTest(RenderPhase.ALWAYS_DEPTH_TEST) - .program(FirmamentShaders.LINES) - .build(false) - ) - val COLORED_QUADS = RenderLayer.of( - "firmament_quads", - VertexFormats.POSITION_COLOR, - VertexFormat.DrawMode.QUADS, - RenderLayer.CUTOUT_BUFFER_SIZE, - false, true, - RenderLayer.MultiPhaseParameters.builder() - .depthTest(RenderPhase.ALWAYS_DEPTH_TEST) - .program(RenderPhase.POSITION_COLOR_PROGRAM) - .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) - .build(false) - ) - } @Deprecated("stateful color management is no longer a thing") fun color(color: me.shedaniel.math.Color) { @@ -82,7 +44,7 @@ class RenderInWorldContext private constructor( fun block(blockPos: BlockPos, color: Int) { matrixStack.push() matrixStack.translate(blockPos.x.toFloat(), blockPos.y.toFloat(), blockPos.z.toFloat()) - buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(RenderLayers.COLORED_QUADS), color) + buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color) matrixStack.pop() } @@ -155,7 +117,7 @@ class RenderInWorldContext private constructor( matrixStack.translate(vec3d.x, vec3d.y, vec3d.z) matrixStack.scale(size, size, size) matrixStack.translate(-.5, -.5, -.5) - buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(RenderLayers.COLORED_QUADS), color) + buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color) matrixStack.pop() vertexConsumers.draw() } @@ -182,8 +144,7 @@ class RenderInWorldContext private constructor( fun line(points: List<Vec3d>, lineWidth: Float = 10F) { RenderSystem.lineWidth(lineWidth) - // TODO: replace with renderlayers - val buffer = tesselator.begin(VertexFormat.DrawMode.LINES, VertexFormats.LINES) + val buffer = vertexConsumers.getBuffer(CustomRenderLayers.LINES) val matrix = matrixStack.peek() var lastNormal: Vector3f? = null @@ -203,7 +164,6 @@ class RenderInWorldContext private constructor( .next() } - RenderLayers.LINES.draw(buffer.end()) } // TODO: put the favourite icons in front of items again @@ -281,16 +241,15 @@ class RenderInWorldContext private constructor( fun renderInWorld(event: WorldRenderLastEvent, block: RenderInWorldContext. () -> Unit) { // TODO: there should be *no more global state*. the only thing we should be doing is render layers. that includes settings like culling, blending, shader color, and depth testing // For now i will let these functions remain, but this needs to go before i do a full (non-beta) release - RenderSystem.disableDepthTest() - RenderSystem.enableBlend() - RenderSystem.defaultBlendFunc() - RenderSystem.disableCull() +// RenderSystem.disableDepthTest() +// RenderSystem.enableBlend() +// RenderSystem.defaultBlendFunc() +// RenderSystem.disableCull() event.matrices.push() event.matrices.translate(-event.camera.pos.x, -event.camera.pos.y, -event.camera.pos.z) val ctx = RenderInWorldContext( - RenderSystem.renderThreadTesselator(), event.matrices, event.camera, event.tickCounter, @@ -302,10 +261,6 @@ class RenderInWorldContext private constructor( event.matrices.pop() event.vertexConsumers.draw() RenderSystem.setShaderColor(1F, 1F, 1F, 1F) - VertexBuffer.unbind() - RenderSystem.enableDepthTest() - RenderSystem.enableCull() - RenderSystem.disableBlend() } } } diff --git a/src/main/kotlin/util/render/TintedOverlayTexture.kt b/src/main/kotlin/util/render/TintedOverlayTexture.kt index a02eccc..0677846 100644 --- a/src/main/kotlin/util/render/TintedOverlayTexture.kt +++ b/src/main/kotlin/util/render/TintedOverlayTexture.kt @@ -1,7 +1,5 @@ package moe.nea.firmament.util.render -import com.mojang.blaze3d.platform.GlConst -import com.mojang.blaze3d.systems.RenderSystem import me.shedaniel.math.Color import net.minecraft.client.render.OverlayTexture import net.minecraft.util.math.ColorHelper @@ -29,16 +27,9 @@ class TintedOverlayTexture : OverlayTexture() { } } - RenderSystem.activeTexture(GlConst.GL_TEXTURE1) - texture.bindTexture() texture.setFilter(false, false) texture.setClamp(true) - image.upload(0, - 0, 0, - 0, 0, - image.width, image.height, - false) - RenderSystem.activeTexture(GlConst.GL_TEXTURE0) + texture.upload() return this } } diff --git a/src/main/kotlin/util/skyblock/SackUtil.kt b/src/main/kotlin/util/skyblock/SackUtil.kt index fd67c44..c46542e 100644 --- a/src/main/kotlin/util/skyblock/SackUtil.kt +++ b/src/main/kotlin/util/skyblock/SackUtil.kt @@ -93,7 +93,7 @@ object SackUtil { fun updateFromHoverText(text: Text) { text.siblings.forEach(::updateFromHoverText) - val hoverText = text.style.hoverEvent?.getValue(HoverEvent.Action.SHOW_TEXT) ?: return + val hoverText = (text.style.hoverEvent as? HoverEvent.ShowText)?.value ?: return val cleanedText = hoverText.unformattedString if (cleanedText.startsWith("Added items:\n")) { if (!foundAdded) { diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt index c295ae0..2458891 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -2,6 +2,7 @@ package moe.nea.firmament.util import java.util.Optional import net.minecraft.text.ClickEvent +import net.minecraft.text.HoverEvent import net.minecraft.text.MutableText import net.minecraft.text.OrderedText import net.minecraft.text.PlainTextContent @@ -126,12 +127,13 @@ fun MutableText.darkGrey() = withColor(Formatting.DARK_GRAY) fun MutableText.red() = withColor(Formatting.RED) fun MutableText.white() = withColor(Formatting.WHITE) fun MutableText.bold(): MutableText = styled { it.withBold(true) } +fun MutableText.hover(text: Text): MutableText = styled {it.withHoverEvent(HoverEvent.ShowText(text))} fun MutableText.clickCommand(command: String): MutableText { require(command.startsWith("/")) return this.styled { - it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, command)) + it.withClickEvent(ClickEvent.RunCommand(command)) } } @@ -162,4 +164,14 @@ fun Text.transformEachRecursively(function: (Text) -> Text): Text { fun tr(key: String, default: String): MutableText = error("Compiler plugin did not run.") fun trResolved(key: String, vararg args: Any): MutableText = Text.stringifiedTranslatable(key, *args) +fun titleCase(str: String): String { + return str + .lowercase() + .replace("_", " ") + .split(" ") + .joinToString(" ") { word -> + word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + } +} + diff --git a/src/main/kotlin/util/uuid.kt b/src/main/kotlin/util/uuid.kt index cccfdd2..14aa83d 100644 --- a/src/main/kotlin/util/uuid.kt +++ b/src/main/kotlin/util/uuid.kt @@ -3,6 +3,12 @@ package moe.nea.firmament.util import java.math.BigInteger import java.util.UUID +fun parsePotentiallyDashlessUUID(unknownFormattedUUID: String): UUID { + if ("-" in unknownFormattedUUID) + return UUID.fromString(unknownFormattedUUID) + return parseDashlessUUID(unknownFormattedUUID) +} + fun parseDashlessUUID(dashlessUuid: String): UUID { val most = BigInteger(dashlessUuid.substring(0, 16), 16) val least = BigInteger(dashlessUuid.substring(16, 32), 16) diff --git a/src/main/resources/assets/firmament/gui/config/macros/combos.xml b/src/main/resources/assets/firmament/gui/config/macros/combos.xml new file mode 100644 index 0000000..b2865ab --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/combos.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig" +> + <Panel background="TRANSPARENT" insets="10"> + <Column> + <Meta beforeClose="@beforeClose"/> + <ScrollPanel width="380" height="300"> + <Align horizontal="CENTER"> + <Array data="@actions"> + <!-- evenBackground="#8B8B8B" oddBackground="#C6C6C6" --> + <Panel background="TRANSPARENT" insets="3"> + <Panel background="VANILLA" insets="6"> + <Column> + <Row> + <Text text="@command" width="280"/> + </Row> + <Row> + <Text text="@formattedCombo" width="250"/> + <Align horizontal="RIGHT"> + <Row> + <firm:Button onClick="@edit"> + <Text text="Edit"/> + </firm:Button> + <Spacer width="12"/> + <firm:Button onClick="@delete"> + <Text text="Delete"/> + </firm:Button> + </Row> + </Align> + </Row> + </Column> + </Panel> + + </Panel> + </Array> + </Align> + </ScrollPanel> + <Align horizontal="RIGHT"> + <Row> + <firm:Button onClick="@discard"> + <Text text="Discard Changes"/> + </firm:Button> + <firm:Button onClick="@saveAndClose"> + <Text text="Save & Close"/> + </firm:Button> + <firm:Button onClick="@save"> + <Text text="Save"/> + </firm:Button> + <firm:Button onClick="@addCommand"> + <Text text="Add Combo Command"/> + </firm:Button> + </Row> + </Align> + </Column> + </Panel> +</Root> diff --git a/src/main/resources/assets/firmament/gui/config/macros/editor.xml b/src/main/resources/assets/firmament/gui/config/macros/editor.xml new file mode 100644 index 0000000..50a1d99 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/editor.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig" +> + <Center> + <Panel background="VANILLA" insets="10"> + <Column> + <Row> + <firm:Button onClick="@back"> + <Text text="←"/> + </firm:Button> + <Text text="Editing command macro"/> + </Row> + <Row> + <Text text="Command: /"/> + <Align horizontal="RIGHT"> + <TextField value="@command" width="200"/> + </Align> + </Row> + <Row> + <Text text="Key Combo:"/> + <Align horizontal="RIGHT"> + <firm:Button onClick="@addStep"> + <Text text="+"/> + </firm:Button> + </Align> + </Row> + <Array data="@combo"> + <Row> + <firm:Fixed width="160"> + <Indirect value="@button"/> + </firm:Fixed> + <Align horizontal="RIGHT"> + <firm:Button onClick="@delete"> + <Text text="Delete"/> + </firm:Button> + </Align> + </Row> + </Array> + </Column> + </Panel> + </Center> +</Root> diff --git a/src/main/resources/assets/firmament/gui/config/macros/index.xml b/src/main/resources/assets/firmament/gui/config/macros/index.xml new file mode 100644 index 0000000..3fa9f99 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/index.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" + xmlns:firm="http://firmament.nea.moe/moulconfig"> + <Center> + <Tabs> + <Tab> + <Tab.Header> + <Text text="Combo Macros"/> + </Tab.Header> + <Tab.Body> + <Fragment value="firmament:gui/config/macros/combos.xml" bind="@combos"/> + </Tab.Body> + </Tab> + </Tabs> + </Center> +</Root> diff --git a/src/main/resources/assets/firmament/gui/license_viewer/index.xml b/src/main/resources/assets/firmament/gui/license_viewer/index.xml new file mode 100644 index 0000000..c23153d --- /dev/null +++ b/src/main/resources/assets/firmament/gui/license_viewer/index.xml @@ -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> diff --git a/src/main/resources/assets/firmament/gui/mining_block_info/index.xml b/src/main/resources/assets/firmament/gui/mining_block_info/index.xml new file mode 100644 index 0000000..6404995 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/mining_block_info/index.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" + xmlns:firm="http://firmament.nea.moe/moulconfig" +> + <Gui> + <Column> + <Row> + <Text text="Search: "/> + <TextField value="@search"/> + </Row> + <ScrollPanel width="200" height="150"> + <Array data="@ores"> + <Column> + <Text text="@oreName"/> + <Array data="@blocks"> + <Row> + <When condition="@isSelected"> + <Center> + <Text text="§a+" textAlign="CENTER" width="10"/> + </Center> + <Spacer width="10" height="0"/> + </When> + <firm:Hover lines="@restrictions"> + <Row> + <ItemStack value="@item"/> + <Align horizontal="LEFT" vertical="CENTER"> + <Text text="@itemName"/> + </Align> + </Row> + </firm:Hover> + </Row> + </Array> + </Column> + </Array> + </ScrollPanel> + </Column> + </Gui> +</Root> diff --git a/src/main/resources/assets/firmament/logo.png b/src/main/resources/assets/firmament/logo.png Binary files differindex e00a2fa..e3f063a 100644 --- a/src/main/resources/assets/firmament/logo.png +++ b/src/main/resources/assets/firmament/logo.png diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 3b988b1..02c11ee 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -40,6 +40,9 @@ "modmenu": [ "moe.nea.firmament.compat.modmenu.FirmamentModMenuPlugin" ], + "jade": [ + "moe.nea.firmament.compat.jade.FirmamentJadePlugin" + ], "jarvis": [ "moe.nea.firmament.jarvis.JarvisIntegration" ] @@ -48,7 +51,7 @@ "firmament.mixins.json" ], "depends": { - "fabric": "*", + "fabric": ">=${fabric_api_version}", "fabric-language-kotlin": ">=${fabric_kotlin_version}", "minecraft": ">=${minecraft_version}" }, diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener index 1805721..eb78b8b 100644 --- a/src/main/resources/firmament.accesswidener +++ b/src/main/resources/firmament.accesswidener @@ -6,12 +6,11 @@ accessible field net/minecraft/client/gui/hud/InGameHud SCOREBOARD_ENTRY_COMPARA accessible field net/minecraft/client/network/ClientPlayNetworkHandler combinedDynamicRegistries Lnet/minecraft/registry/DynamicRegistryManager$Immutable; accessible method net/minecraft/registry/RegistryOps <init> (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/registry/RegistryOps$RegistryInfoGetter;)V accessible class net/minecraft/registry/RegistryOps$CachedRegistryInfoGetter +accessible class net/minecraft/client/render/model/ModelBaker$BakerImpl +accessible method net/minecraft/client/render/model/ModelBaker$BakerImpl <init> (Lnet/minecraft/client/render/model/ModelBaker;Lnet/minecraft/client/render/model/ErrorCollectingSpriteGetter;)V accessible field net/minecraft/entity/mob/CreeperEntity CHARGED Lnet/minecraft/entity/data/TrackedData; accessible method net/minecraft/entity/decoration/ArmorStandEntity setSmall (Z)V -accessible field net/minecraft/entity/passive/AbstractHorseEntity items Lnet/minecraft/inventory/SimpleInventory; -accessible field net/minecraft/entity/passive/AbstractHorseEntity SADDLED_FLAG I -accessible field net/minecraft/entity/passive/AbstractHorseEntity HORSE_FLAGS Lnet/minecraft/entity/data/TrackedData; accessible method net/minecraft/resource/NamespaceResourceManager loadMetadata (Lnet/minecraft/resource/InputSupplier;)Lnet/minecraft/resource/metadata/ResourceMetadata; accessible method net/minecraft/client/gui/DrawContext drawTexturedQuad (Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIIIFFFFI)V @@ -25,3 +24,6 @@ accessible field net/minecraft/client/render/OverlayTexture texture Lnet/minecra accessible method net/minecraft/client/render/RenderPhase$Texture getId ()Ljava/util/Optional; accessible field net/minecraft/client/render/RenderLayer$MultiPhase phases Lnet/minecraft/client/render/RenderLayer$MultiPhaseParameters; accessible field net/minecraft/client/render/RenderLayer$MultiPhaseParameters texture Lnet/minecraft/client/render/RenderPhase$TextureBase; +accessible field net/minecraft/client/network/ClientPlayerInteractionManager currentBreakingPos Lnet/minecraft/util/math/BlockPos; + +mutable field net/minecraft/client/render/entity/state/LivingEntityRenderState headItemRenderState Lnet/minecraft/client/render/item/ItemRenderState; diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png Binary files differnew file mode 100644 index 0000000..46c86f4 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png Binary files differnew file mode 100644 index 0000000..1831ef3 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png Binary files differnew file mode 100644 index 0000000..5b774b2 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta new file mode 100644 index 0000000..94b9a1d --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 17, + "height": 18, + "border": 2 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png Binary files differnew file mode 100644 index 0000000..d4852d8 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta new file mode 100644 index 0000000..5964a6f --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 91, + "height": 184, + "border": 7 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png Binary files differnew file mode 100644 index 0000000..61e9ee5 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta new file mode 100644 index 0000000..cd2857e --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta @@ -0,0 +1,9 @@ +{ + "gui": { + "scaling": { + "type": "tile", + "width": 162, + "height": 18 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png Binary files differnew file mode 100644 index 0000000..653a99e --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta new file mode 100644 index 0000000..a29299d --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 176, + "height": 222, + "border": 10 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta new file mode 100644 index 0000000..035feaa --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta @@ -0,0 +1,10 @@ +{ + "pack": { + "pack_format": 15, + "supported_formats": { + "min_inclusive": 15, + "max_inclusive": 2147483647 + }, + "description": "Adds a more transparent overlay for Firmament" + } +} diff --git a/src/test/kotlin/MixinTest.kt b/src/test/kotlin/MixinTest.kt new file mode 100644 index 0000000..55aa7c2 --- /dev/null +++ b/src/test/kotlin/MixinTest.kt @@ -0,0 +1,34 @@ +package moe.nea.firmament.test + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.spongepowered.asm.mixin.MixinEnvironment +import org.spongepowered.asm.mixin.transformer.IMixinTransformer +import moe.nea.firmament.init.MixinPlugin + +class MixinTest { + @Test + fun mixinAudit() { + FirmTestBootstrap.bootstrapMinecraft() + MixinEnvironment.getCurrentEnvironment().audit() + val mp = MixinPlugin.instances.single() + Assertions.assertEquals( + mp.expectedFullPathMixins, + mp.appliedFullPathMixins, + ) + Assertions.assertNotEquals( + 0, + mp.mixins.size + ) + + } + + @Test + fun hasInstalledMixinTransformer() { + Assertions.assertInstanceOf( + IMixinTransformer::class.java, + MixinEnvironment.getCurrentEnvironment().activeTransformer + ) + } +} + diff --git a/src/test/kotlin/features/macros/KeyComboTrieCreation.kt b/src/test/kotlin/features/macros/KeyComboTrieCreation.kt new file mode 100644 index 0000000..f0e7a1b --- /dev/null +++ b/src/test/kotlin/features/macros/KeyComboTrieCreation.kt @@ -0,0 +1,103 @@ +package moe.nea.firmament.test.features.macros + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import net.minecraft.client.util.InputUtil +import moe.nea.firmament.features.macros.Branch +import moe.nea.firmament.features.macros.ComboKeyAction +import moe.nea.firmament.features.macros.CommandAction +import moe.nea.firmament.features.macros.KeyComboTrie +import moe.nea.firmament.features.macros.Leaf +import moe.nea.firmament.keybindings.SavedKeyBinding + +class KeyComboTrieCreation { + val basicAction = CommandAction("ac Hello") + val aPress = SavedKeyBinding(InputUtil.GLFW_KEY_A) + val bPress = SavedKeyBinding(InputUtil.GLFW_KEY_B) + val cPress = SavedKeyBinding(InputUtil.GLFW_KEY_C) + + @Test + fun testValidShortTrie() { + val actions = listOf( + ComboKeyAction(basicAction, listOf(aPress)), + ComboKeyAction(basicAction, listOf(bPress)), + ComboKeyAction(basicAction, listOf(cPress)), + ) + Assertions.assertEquals( + Branch( + mapOf( + aPress to Leaf(basicAction), + bPress to Leaf(basicAction), + cPress to Leaf(basicAction), + ), + ), KeyComboTrie.fromComboList(actions) + ) + } + + @Test + fun testOverlappingLeafs() { + Assertions.assertThrows(IllegalStateException::class.java) { + KeyComboTrie.fromComboList( + listOf( + ComboKeyAction(basicAction, listOf(aPress, aPress)), + ComboKeyAction(basicAction, listOf(aPress, aPress)), + ) + ) + } + Assertions.assertThrows(IllegalStateException::class.java) { + KeyComboTrie.fromComboList( + listOf( + ComboKeyAction(basicAction, listOf(aPress)), + ComboKeyAction(basicAction, listOf(aPress)), + ) + ) + } + } + + @Test + fun testBranchOverlappingLeaf() { + Assertions.assertThrows(IllegalStateException::class.java) { + KeyComboTrie.fromComboList( + listOf( + ComboKeyAction(basicAction, listOf(aPress)), + ComboKeyAction(basicAction, listOf(aPress, aPress)), + ) + ) + } + } + @Test + fun testLeafOverlappingBranch() { + Assertions.assertThrows(IllegalStateException::class.java) { + KeyComboTrie.fromComboList( + listOf( + ComboKeyAction(basicAction, listOf(aPress, aPress)), + ComboKeyAction(basicAction, listOf(aPress)), + ) + ) + } + } + + + @Test + fun testValidNestedTrie() { + val actions = listOf( + ComboKeyAction(basicAction, listOf(aPress, aPress)), + ComboKeyAction(basicAction, listOf(aPress, bPress)), + ComboKeyAction(basicAction, listOf(cPress)), + ) + Assertions.assertEquals( + Branch( + mapOf( + aPress to Branch( + mapOf( + aPress to Leaf(basicAction), + bPress to Leaf(basicAction), + ) + ), + cPress to Leaf(basicAction), + ), + ), KeyComboTrie.fromComboList(actions) + ) + } + +} diff --git a/src/test/kotlin/root.kt b/src/test/kotlin/root.kt index 045fdd5..000ddda 100644 --- a/src/test/kotlin/root.kt +++ b/src/test/kotlin/root.kt @@ -24,6 +24,7 @@ object FirmTestBootstrap { println("Bootstrap completed at $loadEnd after $loadDuration") } + @JvmStatic fun bootstrapMinecraft() { } } diff --git a/src/test/kotlin/testutil/AutoBootstrapExtension.kt b/src/test/kotlin/testutil/AutoBootstrapExtension.kt new file mode 100644 index 0000000..6f225a0 --- /dev/null +++ b/src/test/kotlin/testutil/AutoBootstrapExtension.kt @@ -0,0 +1,14 @@ +package moe.nea.firmament.test.testutil + +import com.google.auto.service.AutoService +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.Extension +import org.junit.jupiter.api.extension.ExtensionContext +import moe.nea.firmament.test.FirmTestBootstrap + +@AutoService(Extension::class) +class AutoBootstrapExtension : Extension, BeforeAllCallback { + override fun beforeAll(p0: ExtensionContext) { + FirmTestBootstrap.bootstrapMinecraft() + } +} diff --git a/src/test/kotlin/testutil/ItemResources.kt b/src/test/kotlin/testutil/ItemResources.kt index 107b565..17198f1 100644 --- a/src/test/kotlin/testutil/ItemResources.kt +++ b/src/test/kotlin/testutil/ItemResources.kt @@ -1,13 +1,23 @@ package moe.nea.firmament.test.testutil +import com.mojang.datafixers.DSL +import com.mojang.datafixers.DataFixUtils +import com.mojang.datafixers.types.templates.Named +import com.mojang.serialization.Dynamic +import com.mojang.serialization.JsonOps +import net.minecraft.SharedConstants +import net.minecraft.datafixer.Schemas +import net.minecraft.datafixer.TypeReferences import net.minecraft.item.ItemStack import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtElement import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.NbtString import net.minecraft.nbt.StringNbtReader import net.minecraft.registry.RegistryOps import net.minecraft.text.Text import net.minecraft.text.TextCodecs +import moe.nea.firmament.features.debug.ExportedTestConstantMeta import moe.nea.firmament.test.FirmTestBootstrap import moe.nea.firmament.util.MC @@ -24,18 +34,49 @@ object ItemResources { } fun loadSNbt(path: String): NbtCompound { - return StringNbtReader.parse(loadString(path)) + return StringNbtReader.readCompound(loadString(path)) } fun getNbtOps(): RegistryOps<NbtElement> = MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE) + fun tryMigrateNbt( + nbtCompound: NbtCompound, + typ: DSL.TypeReference, + ): NbtElement { + val source = nbtCompound.get("source", ExportedTestConstantMeta.CODEC) + nbtCompound.remove("source") + if (source.isPresent) { + val wrappedNbtSource = if (typ == TypeReferences.TEXT_COMPONENT && source.get().dataVersion < 4325) { + // Per 1.21.5 text components are wrapped in a string, which firmament unwrapped in the snbt files + NbtString.of( + NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, nbtCompound) + .toString()) + } else { + nbtCompound + } + return Schemas.getFixer() + .update( + typ, + Dynamic(NbtOps.INSTANCE, wrappedNbtSource), + source.get().dataVersion, + SharedConstants.getGameVersion().saveVersion.id + ).value + } + return nbtCompound + } + fun loadText(name: String): Text { - return TextCodecs.CODEC.parse(getNbtOps(), loadSNbt("testdata/chat/$name.snbt")) - .getOrThrow { IllegalStateException("Could not load test chat '$name': $it") } + return TextCodecs.CODEC.parse( + getNbtOps(), + tryMigrateNbt(loadSNbt("testdata/chat/$name.snbt"), TypeReferences.TEXT_COMPONENT) + ).getOrThrow { IllegalStateException("Could not load test chat '$name': $it") } } fun loadItem(name: String): ItemStack { - // TODO: make the load work with enchantments - return ItemStack.CODEC.parse(getNbtOps(), loadSNbt("testdata/items/$name.snbt")) - .getOrThrow { IllegalStateException("Could not load test item '$name': $it") } + try { + val itemNbt = loadSNbt("testdata/items/$name.snbt") + return ItemStack.CODEC.parse(getNbtOps(), tryMigrateNbt(itemNbt, TypeReferences.ITEM_STACK)).orThrow + } catch (ex: Exception) { + throw RuntimeException("Could not load item resource '$name'", ex) + } } } diff --git a/src/test/kotlin/testutil/KotestPlugin.kt b/src/test/kotlin/testutil/KotestPlugin.kt deleted file mode 100644 index 6db50fb..0000000 --- a/src/test/kotlin/testutil/KotestPlugin.kt +++ /dev/null @@ -1,16 +0,0 @@ -package moe.nea.firmament.test.testutil - -import io.kotest.core.config.AbstractProjectConfig -import io.kotest.core.extensions.Extension -import moe.nea.firmament.test.FirmTestBootstrap - -class KotestPlugin : AbstractProjectConfig() { - override fun extensions(): List<Extension> { - return listOf() - } - - override suspend fun beforeProject() { - FirmTestBootstrap.bootstrapMinecraft() - super.beforeProject() - } -} diff --git a/src/test/kotlin/util/ColorCodeTest.kt b/src/test/kotlin/util/ColorCodeTest.kt index 949749e..7c581c5 100644 --- a/src/test/kotlin/util/ColorCodeTest.kt +++ b/src/test/kotlin/util/ColorCodeTest.kt @@ -1,57 +1,57 @@ package moe.nea.firmament.test.util -import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test import moe.nea.firmament.util.removeColorCodes -class ColorCodeTest : AnnotationSpec() { - @Test - fun testWhatever() { - Assertions.assertEquals("", "".removeColorCodes()) - Assertions.assertEquals("", "§".removeColorCodes()) - Assertions.assertEquals("", "§a".removeColorCodes()) - Assertions.assertEquals("ab", "a§ab".removeColorCodes()) - Assertions.assertEquals("ab", "a§ab§§".removeColorCodes()) - Assertions.assertEquals("abc", "a§ab§§c".removeColorCodes()) - Assertions.assertEquals("bc", "§ab§§c".removeColorCodes()) - Assertions.assertEquals("b§lc", "§ab§l§§c".removeColorCodes(true)) - Assertions.assertEquals("b§lc§l", "§ab§l§§c§l".removeColorCodes(true)) - Assertions.assertEquals("§lb§lc", "§l§ab§l§§c".removeColorCodes(true)) - } - - @Test - fun testEdging() { - Assertions.assertEquals("", "§".removeColorCodes()) - Assertions.assertEquals("a", "a§".removeColorCodes()) - Assertions.assertEquals("b", "§ab§".removeColorCodes()) - } - - @Test - fun `testDouble§`() { - Assertions.assertEquals("1", "§§1".removeColorCodes()) - } - - @Test - fun testKeepNonColor() { - Assertions.assertEquals("§k§l§m§n§o§r", "§k§l§m§f§n§o§r".removeColorCodes(true)) - } - - @Test - fun testPlainString() { - Assertions.assertEquals("bcdefgp", "bcdefgp".removeColorCodes()) - Assertions.assertEquals("", "".removeColorCodes()) - } - - @Test - fun testSomeNormalTestCases() { - Assertions.assertEquals( - "You are not currently in a party.", - "§r§cYou are not currently in a party.§r".removeColorCodes() - ) - Assertions.assertEquals( - "Ancient Necron's Chestplate ✪✪✪✪", - "§dAncient Necron's Chestplate §6✪§6✪§6✪§6✪".removeColorCodes() - ) - } +class ColorCodeTest { + @Test + fun testWhatever() { + Assertions.assertEquals("", "".removeColorCodes()) + Assertions.assertEquals("", "§".removeColorCodes()) + Assertions.assertEquals("", "§a".removeColorCodes()) + Assertions.assertEquals("ab", "a§ab".removeColorCodes()) + Assertions.assertEquals("ab", "a§ab§§".removeColorCodes()) + Assertions.assertEquals("abc", "a§ab§§c".removeColorCodes()) + Assertions.assertEquals("bc", "§ab§§c".removeColorCodes()) + Assertions.assertEquals("b§lc", "§ab§l§§c".removeColorCodes(true)) + Assertions.assertEquals("b§lc§l", "§ab§l§§c§l".removeColorCodes(true)) + Assertions.assertEquals("§lb§lc", "§l§ab§l§§c".removeColorCodes(true)) + } + + @Test + fun testEdging() { + Assertions.assertEquals("", "§".removeColorCodes()) + Assertions.assertEquals("a", "a§".removeColorCodes()) + Assertions.assertEquals("b", "§ab§".removeColorCodes()) + } + + @Test + fun `testDouble§`() { + Assertions.assertEquals("1", "§§1".removeColorCodes()) + } + + @Test + fun testKeepNonColor() { + Assertions.assertEquals("§k§l§m§n§o§r", "§k§l§m§f§n§o§r".removeColorCodes(true)) + } + + @Test + fun testPlainString() { + Assertions.assertEquals("bcdefgp", "bcdefgp".removeColorCodes()) + Assertions.assertEquals("", "".removeColorCodes()) + } + + @Test + fun testSomeNormalTestCases() { + Assertions.assertEquals( + "You are not currently in a party.", + "§r§cYou are not currently in a party.§r".removeColorCodes() + ) + Assertions.assertEquals( + "Ancient Necron's Chestplate ✪✪✪✪", + "§dAncient Necron's Chestplate §6✪§6✪§6✪§6✪".removeColorCodes() + ) + } } diff --git a/src/test/kotlin/util/TextUtilText.kt b/src/test/kotlin/util/TextUtilText.kt index 46ed3b4..94ab222 100644 --- a/src/test/kotlin/util/TextUtilText.kt +++ b/src/test/kotlin/util/TextUtilText.kt @@ -1,16 +1,18 @@ package moe.nea.firmament.test.util -import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.getLegacyFormatString -class TextUtilText : AnnotationSpec() { +class TextUtilText { @Test fun testThing() { // TODO: add more tests that are directly validated with 1.8.9 code val text = ItemResources.loadText("all-chat") - Assertions.assertEquals("§r§r§8[§r§9302§r§8] §r§6♫ §r§b[MVP§r§d+§r§b] lrg89§r§f: test§r", - text.getLegacyFormatString()) + Assertions.assertEquals( + "§r§r§8[§r§9302§r§8] §r§6♫ §r§b[MVP§r§d+§r§b] lrg89§r§f: test§r", + text.getLegacyFormatString() + ) } } diff --git a/src/test/kotlin/util/math/GChainReconciliationTest.kt b/src/test/kotlin/util/math/GChainReconciliationTest.kt new file mode 100644 index 0000000..380ea5c --- /dev/null +++ b/src/test/kotlin/util/math/GChainReconciliationTest.kt @@ -0,0 +1,75 @@ +package moe.nea.firmament.test.util.math + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import moe.nea.firmament.util.math.GChainReconciliation +import moe.nea.firmament.util.math.GChainReconciliation.rotated + +class GChainReconciliationTest { + + fun <T> assertEqualCycles( + expected: List<T>, + actual: List<T> + ) { + for (offset in expected.indices) { + val rotated = expected.rotated(offset) + val matchesAtRotation = run { + for ((i, v) in actual.withIndex()) { + if (rotated[i % rotated.size] != v) + return@run false + } + true + } + if (matchesAtRotation) + return + } + assertEquals(expected, actual, "Expected arrays to be cycle equivalent") + } + + @Test + fun testUnfixableCycleNotBeingModified() { + assertEquals( + listOf(1, 2, 3, 4, 6, 1, 2, 3, 4, 6), + GChainReconciliation.reconcileCycles( + listOf(1, 2, 3, 4, 6, 1, 2, 3, 4, 6), + listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1) + ) + ) + } + + @Test + fun testMultipleIndependentHoles() { + assertEqualCycles( + listOf(1, 2, 3, 4, 5, 6), + GChainReconciliation.reconcileCycles( + listOf(1, 3, 4, 5, 6, 1, 3, 4, 5, 6), + listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1) + ) + ) + + } + + @Test + fun testBigHole() { + assertEqualCycles( + listOf(1, 2, 3, 4, 5, 6), + GChainReconciliation.reconcileCycles( + listOf(1, 4, 5, 6, 1, 4, 5, 6), + listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1) + ) + ) + + } + + @Test + fun testOneMissingBeingDetected() { + assertEqualCycles( + listOf(1, 2, 3, 4, 5, 6), + GChainReconciliation.reconcileCycles( + listOf(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6), + listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1) + ) + ) + } +} diff --git a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt index 206a357..9d25aad 100644 --- a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt +++ b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt @@ -1,7 +1,7 @@ package moe.nea.firmament.test.util.skyblock -import io.kotest.core.spec.style.AnnotationSpec 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 @@ -9,7 +9,7 @@ import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.skyblock.AbilityUtils import moe.nea.firmament.util.unformattedString -class AbilityUtilsTest : AnnotationSpec() { +class AbilityUtilsTest { fun List<AbilityUtils.ItemAbility>.stripDescriptions() = map { it.copy(descriptionLines = it.descriptionLines.map { Text.literal(it.unformattedString) }) @@ -24,9 +24,11 @@ class AbilityUtilsTest : AnnotationSpec() { 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), + listOf( + "Throw your pickaxe to create an", + "explosion mining all ores in a 3 block", + "radius." + ).map(Text::literal), 48.seconds ) ), @@ -43,8 +45,10 @@ class AbilityUtilsTest : AnnotationSpec() { true, AbilityUtils.AbilityActivation.RIGHT_CLICK, null, - listOf("Grants +200% ⸕ Mining Speed for", - "10s.").map(Text::literal), + listOf( + "Grants +200% ⸕ Mining Speed for", + "10s." + ).map(Text::literal), 2.minutes ) ), @@ -58,8 +62,10 @@ class AbilityUtilsTest : AnnotationSpec() { 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), + listOf( + "Teleport 12 blocks ahead of you and", + "gain +50 ✦ Speed for 3 seconds." + ).map(Text::literal), null ), AbilityUtils.ItemAbility( @@ -67,9 +73,11 @@ class AbilityUtilsTest : AnnotationSpec() { false, AbilityUtils.AbilityActivation.SNEAK_RIGHT_CLICK, 90, - listOf("Teleport to your targeted block up", - "to 61 blocks away.", - "Soulflow Cost: 1").map(Text::literal), + listOf( + "Teleport to your targeted block up", + "to 61 blocks away.", + "Soulflow Cost: 1" + ).map(Text::literal), null ) ), diff --git a/src/test/kotlin/util/skyblock/ItemTypeTest.kt b/src/test/kotlin/util/skyblock/ItemTypeTest.kt index cca3d13..c0ef2a3 100644 --- a/src/test/kotlin/util/skyblock/ItemTypeTest.kt +++ b/src/test/kotlin/util/skyblock/ItemTypeTest.kt @@ -1,26 +1,28 @@ package moe.nea.firmament.test.util.skyblock -import io.kotest.core.spec.style.ShouldSpec -import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.skyblock.ItemType -class ItemTypeTest - : ShouldSpec( - { - context("ItemType.fromItemstack") { - listOf( - "pets/lion-item" to ItemType.PET, - "pets/rabbit-selected" to ItemType.PET, - "pets/mithril-golem-not-selected" to ItemType.PET, - "aspect-of-the-void" to ItemType.SWORD, - "titanium-drill" to ItemType.DRILL, - "diamond-pickaxe" to ItemType.PICKAXE, - "gemstone-gauntlet" to ItemType.GAUNTLET, - ).forEach { (name, typ) -> - should("return $typ for $name") { - ItemType.fromItemStack(ItemResources.loadItem(name)) shouldBe typ - } +class ItemTypeTest { + @TestFactory + fun fromItemstack() = + listOf( + "pets/lion-item" to ItemType.PET, + "pets/rabbit-selected" to ItemType.PET, + "pets/mithril-golem-not-selected" to ItemType.PET, + "aspect-of-the-void" to ItemType.SWORD, + "titanium-drill" to ItemType.DRILL, + "diamond-pickaxe" to ItemType.PICKAXE, + "gemstone-gauntlet" to ItemType.GAUNTLET, + ).map { (name, typ) -> + DynamicTest.dynamicTest("return $typ for $name") { + Assertions.assertEquals( + typ, + ItemType.fromItemStack(ItemResources.loadItem(name)) + ) } } - }) +} diff --git a/src/test/kotlin/util/skyblock/SackUtilTest.kt b/src/test/kotlin/util/skyblock/SackUtilTest.kt index f93cd2b..e0e3e63 100644 --- a/src/test/kotlin/util/skyblock/SackUtilTest.kt +++ b/src/test/kotlin/util/skyblock/SackUtilTest.kt @@ -1,12 +1,12 @@ package moe.nea.firmament.test.util.skyblock -import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.skyblock.SackUtil import moe.nea.firmament.util.skyblock.SkyBlockItems -class SackUtilTest : AnnotationSpec() { +class SackUtilTest { @Test fun testOneRottenFlesh() { Assertions.assertEquals( diff --git a/src/test/resources/testdata/chat/all-chat.snbt b/src/test/resources/testdata/chat/all-chat.snbt index 15cc2de..386194b 100644 --- a/src/test/resources/testdata/chat/all-chat.snbt +++ b/src/test/resources/testdata/chat/all-chat.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, extra: [ { bold: 0b, diff --git a/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt b/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt index 924a558..d7b8b90 100644 --- a/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt +++ b/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, color: "#FFAA00", extra: [ { diff --git a/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt b/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt index 924a558..d7b8b90 100644 --- a/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt +++ b/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, color: "#FFAA00", extra: [ { diff --git a/src/test/resources/testdata/items/aspect-of-the-void.snbt b/src/test/resources/testdata/items/aspect-of-the-void.snbt index 180c069..9ffd385 100644 --- a/src/test/resources/testdata/items/aspect-of-the-void.snbt +++ b/src/test/resources/testdata/items/aspect-of-the-void.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/test/resources/testdata/items/books/feather_falling.snbt b/src/test/resources/testdata/items/books/feather_falling.snbt index 1de4632..4a0b7c6 100644 --- a/src/test/resources/testdata/items/books/feather_falling.snbt +++ b/src/test/resources/testdata/items/books/feather_falling.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/test/resources/testdata/items/diamond-pickaxe.snbt b/src/test/resources/testdata/items/diamond-pickaxe.snbt index cce12f9..aa5e590 100644 --- a/src/test/resources/testdata/items/diamond-pickaxe.snbt +++ b/src/test/resources/testdata/items/diamond-pickaxe.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/test/resources/testdata/items/gemstone-gauntlet.snbt b/src/test/resources/testdata/items/gemstone-gauntlet.snbt index 92ce739..92bb806 100644 --- a/src/test/resources/testdata/items/gemstone-gauntlet.snbt +++ b/src/test/resources/testdata/items/gemstone-gauntlet.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/test/resources/testdata/items/hyperion.snbt b/src/test/resources/testdata/items/hyperion.snbt index c57d457..f0025b9 100644 --- a/src/test/resources/testdata/items/hyperion.snbt +++ b/src/test/resources/testdata/items/hyperion.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/test/resources/testdata/items/implosion-belt.snbt b/src/test/resources/testdata/items/implosion-belt.snbt index b73542d..875047d 100644 --- a/src/test/resources/testdata/items/implosion-belt.snbt +++ b/src/test/resources/testdata/items/implosion-belt.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/test/resources/testdata/items/necron-boots.snbt b/src/test/resources/testdata/items/necron-boots.snbt new file mode 100644 index 0000000..fd740ce --- /dev/null +++ b/src/test/resources/testdata/items/necron-boots.snbt @@ -0,0 +1,71 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + enchantments: { + depth_strider: 3, + feather_falling: 10, + growth: 5, + protection: 5 + }, + id: "POWER_WITHER_BOOTS", + modifier: "ancient", + rarity_upgrades: 1, + timestamp: 1704550620000L, + upgrade_level: 5, + uuid: "8b6c7485-cb59-44d3-ac8f-9e52a611cc64" + }, + "minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Ancient Necron\'s Boots "},{"color":"gold","text":"✪✪✪✪✪"}],"italic":false,"text":""}', + "minecraft:dyed_color": { + rgb: 15167036, + show_in_tooltip: 0b + }, + "minecraft:enchantments": { + levels: { + "minecraft:depth_strider": 3 + } + }, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"gray","text":"Gear Score: "},{"color":"light_purple","text":"713 "},{"color":"dark_gray","text":"(2753)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+79 "},{"color":"blue","text":"(+35) "},{"color":"dark_gray","text":"(+333.75)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Crit Chance: "},{"color":"red","text":"+15% "},{"color":"blue","text":"(+15%) "},{"color":"dark_gray","text":"(+23.1%)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Crit Damage: "},{"color":"red","text":"+71% "},{"color":"blue","text":"(+38%) "},{"color":"dark_gray","text":"(+302.6%)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Health: "},{"color":"green","text":"+241.5 "},{"color":"blue","text":"(+7) "},{"color":"dark_gray","text":"(+1,010.15)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+120.5 "},{"color":"blue","text":"(+7) "},{"color":"dark_gray","text":"(+498.4)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+36 "},{"color":"blue","text":"(+25) "},{"color":"dark_gray","text":"(+155.75)"}],"italic":false,"text":""}', + '{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"❁"},{"color":"dark_gray","text":"] "},{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"⚔"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Depth Strider III"},{"color":"blue","text":", "},{"color":"blue","text":"Feather Falling X"},{"color":"blue","text":", "},{"color":"blue","text":"Growth V"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Protection V"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Reduces the damage you take from"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"withers by "},{"color":"red","text":"10%"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Full Set Bonus: Witherborn "},{"color":"gray","text":"(3/4)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Spawns a wither minion every "},{"color":"yellow","text":"30"}],"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":""},{"color":"gray","text":"seconds up to a maximum "},{"color":"green","text":"1 "},{"color":"gray","text":"wither."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Your withers will travel to and"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"explode on nearby enemies."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Ancient Bonus"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"green","text":"+1 "},{"color":"blue","text":"☠ Crit Damage "},{"color":"gray","text":"per"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"red","text":"Catacombs "},{"color":"gray","text":"level."}],"italic":false,"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 DUNGEON BOOTS "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}' + ], + "minecraft:unbreakable": { + show_in_tooltip: 0b + } + }, + count: 1, + id: "minecraft:leather_boots" +} diff --git a/src/test/resources/testdata/items/pets/lion-item.snbt b/src/test/resources/testdata/items/pets/lion-item.snbt index 6e92685..c364032 100644 --- a/src/test/resources/testdata/items/pets/lion-item.snbt +++ b/src/test/resources/testdata/items/pets/lion-item.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt b/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt index c0ef585..79f32c9 100644 --- a/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt +++ b/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:custom_data": { id: "PET", diff --git a/src/test/resources/testdata/items/pets/rabbit-selected.snbt b/src/test/resources/testdata/items/pets/rabbit-selected.snbt index 48a6f6f..d4c7235 100644 --- a/src/test/resources/testdata/items/pets/rabbit-selected.snbt +++ b/src/test/resources/testdata/items/pets/rabbit-selected.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:custom_data": { id: "PET", diff --git a/src/test/resources/testdata/items/rune-in-sack.snbt b/src/test/resources/testdata/items/rune-in-sack.snbt index b15488a..4624c0f 100644 --- a/src/test/resources/testdata/items/rune-in-sack.snbt +++ b/src/test/resources/testdata/items/rune-in-sack.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:custom_data": { }, diff --git a/src/test/resources/testdata/items/titanium-drill.snbt b/src/test/resources/testdata/items/titanium-drill.snbt index e3b6819..e49c6b0 100644 --- a/src/test/resources/testdata/items/titanium-drill.snbt +++ b/src/test/resources/testdata/items/titanium-drill.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt new file mode 100644 index 0000000..d95712b --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.features.texturepack + +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return true + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt index dc3b109..462b1e1 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt @@ -3,6 +3,8 @@ package moe.nea.firmament.features.texturepack import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.function.Function import net.fabricmc.loader.api.FabricLoader import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer @@ -19,8 +21,11 @@ import kotlinx.serialization.serializer import kotlin.jvm.optionals.getOrNull import net.minecraft.block.Block import net.minecraft.block.BlockState -import net.minecraft.client.render.model.BakedModel -import net.minecraft.client.util.ModelIdentifier +import net.minecraft.client.render.model.Baker +import net.minecraft.client.render.model.BlockStateModel +import net.minecraft.client.render.model.ReferencedModelsCollector +import net.minecraft.client.render.model.SimpleBlockStateModel +import net.minecraft.client.render.model.json.ModelVariant import net.minecraft.registry.RegistryKey import net.minecraft.registry.RegistryKeys import net.minecraft.resource.ResourceManager @@ -28,11 +33,13 @@ import net.minecraft.resource.SinglePreparationResourceReloader import net.minecraft.util.Identifier import net.minecraft.util.math.BlockPos import net.minecraft.util.profiler.Profiler +import net.minecraft.util.thread.AsyncHelper import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.EarlyResourceReloadEvent import moe.nea.firmament.events.FinalizeResourceManagerEvent import moe.nea.firmament.events.SkyblockServerUpdateEvent +import moe.nea.firmament.features.texturepack.CustomBlockTextures.createBakedModels import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger import moe.nea.firmament.util.IdentifierSerializer import moe.nea.firmament.util.MC @@ -43,249 +50,301 @@ import moe.nea.firmament.util.json.SingletonSerializableList object CustomBlockTextures { - @Serializable - data class CustomBlockOverride( - val modes: @Serializable(SingletonSerializableList::class) List<String>, - val area: List<Area>? = null, - val replacements: Map<Identifier, Replacement>, - ) - - @Serializable(with = Replacement.Serializer::class) - data class Replacement( - val block: Identifier, - val sound: Identifier?, - ) { - - @Transient - val blockModelIdentifier get() = ModelIdentifier(block.withPrefixedPath("block/"), "firmament") - - @Transient - val bakedModel: BakedModel by lazy(LazyThreadSafetyMode.NONE) { - MC.instance.bakedModelManager.getModel(blockModelIdentifier) - } - - @OptIn(ExperimentalSerializationApi::class) - @kotlinx.serialization.Serializer(Replacement::class) - object DefaultSerializer : KSerializer<Replacement> - - object Serializer : KSerializer<Replacement> { - val delegate = serializer<JsonElement>() - override val descriptor: SerialDescriptor - get() = delegate.descriptor - - override fun deserialize(decoder: Decoder): Replacement { - val jsonElement = decoder.decodeSerializableValue(delegate) - if (jsonElement is JsonPrimitive) { - require(jsonElement.isString) - return Replacement(Identifier.tryParse(jsonElement.content)!!, null) - } - return (decoder as JsonDecoder).json.decodeFromJsonElement(DefaultSerializer, jsonElement) - } - - override fun serialize(encoder: Encoder, value: Replacement) { - encoder.encodeSerializableValue(DefaultSerializer, value) - } - } - } - - @Serializable - data class Area( - val min: BlockPos, - val max: BlockPos, - ) { - @Transient - val realMin = BlockPos( - minOf(min.x, max.x), - minOf(min.y, max.y), - minOf(min.z, max.z), - ) - - @Transient - val realMax = BlockPos( - maxOf(min.x, max.x), - maxOf(min.y, max.y), - maxOf(min.z, max.z), - ) - - fun roughJoin(other: Area): Area { - return Area( - BlockPos( - minOf(realMin.x, other.realMin.x), - minOf(realMin.y, other.realMin.y), - minOf(realMin.z, other.realMin.z), - ), - BlockPos( - maxOf(realMax.x, other.realMax.x), - maxOf(realMax.y, other.realMax.y), - maxOf(realMax.z, other.realMax.z), - ) - ) - } - - fun contains(blockPos: BlockPos): Boolean { - return (blockPos.x in realMin.x..realMax.x) && - (blockPos.y in realMin.y..realMax.y) && - (blockPos.z in realMin.z..realMax.z) - } - } - - data class LocationReplacements( - val lookup: Map<Block, List<BlockReplacement>> - ) - - data class BlockReplacement( - val checks: List<Area>?, - val replacement: Replacement, - ) { - val roughCheck by lazy(LazyThreadSafetyMode.NONE) { - if (checks == null || checks.size < 3) return@lazy null - checks.reduce { acc, next -> acc.roughJoin(next) } - } - } - - data class BakedReplacements(val data: Map<SkyBlockIsland, LocationReplacements>) - - var allLocationReplacements: BakedReplacements = BakedReplacements(mapOf()) - var currentIslandReplacements: LocationReplacements? = null - - fun refreshReplacements() { - val location = SBData.skyblockLocation - val replacements = - if (CustomSkyBlockTextures.TConfig.enableBlockOverrides) location?.let(allLocationReplacements.data::get) - else null - val lastReplacements = currentIslandReplacements - currentIslandReplacements = replacements - if (lastReplacements != replacements) { - MC.nextTick { - MC.worldRenderer.chunks?.chunks?.forEach { - // false schedules rebuilds outside a 27 block radius to happen async - it.scheduleRebuild(false) - } - sodiumReloadTask?.run() - } - } - } - - private val sodiumReloadTask = runCatching { - val r = Class.forName("moe.nea.firmament.compat.sodium.SodiumChunkReloader") - .getConstructor() - .newInstance() as Runnable - r.run() - r - }.getOrElse { - if (FabricLoader.getInstance().isModLoaded("sodium")) - logger.error("Could not create sodium chunk reloader") - null - } - - - fun matchesPosition(replacement: BlockReplacement, blockPos: BlockPos?): Boolean { - if (blockPos == null) return true - val rc = replacement.roughCheck - if (rc != null && !rc.contains(blockPos)) return false - val areas = replacement.checks - if (areas != null && !areas.any { it.contains(blockPos) }) return false - return true - } - - @JvmStatic - fun getReplacementModel(block: BlockState, blockPos: BlockPos?): BakedModel? { - return getReplacement(block, blockPos)?.bakedModel - } - - @JvmStatic - fun getReplacement(block: BlockState, blockPos: BlockPos?): Replacement? { - if (isInFallback() && blockPos == null) { - return null - } - val replacements = currentIslandReplacements?.lookup?.get(block.block) ?: return null - for (replacement in replacements) { - if (replacement.checks == null || matchesPosition(replacement, blockPos)) - return replacement.replacement - } - return null - } - - - @Subscribe - fun onLocation(event: SkyblockServerUpdateEvent) { - refreshReplacements() - } - - @Volatile - var preparationFuture: CompletableFuture<BakedReplacements> = CompletableFuture.completedFuture(BakedReplacements( - mapOf())) - - val insideFallbackCall = ThreadLocal.withInitial { 0 } - - @JvmStatic - fun enterFallbackCall() { - insideFallbackCall.set(insideFallbackCall.get() + 1) - } - - fun isInFallback() = insideFallbackCall.get() > 0 - - @JvmStatic - fun exitFallbackCall() { - insideFallbackCall.set(insideFallbackCall.get() - 1) - } - - @Subscribe - fun onEarlyReload(event: EarlyResourceReloadEvent) { - preparationFuture = CompletableFuture - .supplyAsync( - { prepare(event.resourceManager) }, event.preparationExecutor) - } - - private fun prepare(manager: ResourceManager): BakedReplacements { - val resources = manager.findResources("overrides/blocks") { - it.namespace == "firmskyblock" && it.path.endsWith(".json") - } - val map = mutableMapOf<SkyBlockIsland, MutableMap<Block, MutableList<BlockReplacement>>>() - for ((file, resource) in resources) { - val json = - Firmament.tryDecodeJsonFromStream<CustomBlockOverride>(resource.inputStream) - .getOrElse { ex -> - logger.error("Failed to load block texture override at $file", ex) - continue - } - for (mode in json.modes) { - val island = SkyBlockIsland.forMode(mode) - val islandMpa = map.getOrPut(island, ::mutableMapOf) - for ((blockId, replacement) in json.replacements) { - val block = MC.defaultRegistries.getOrThrow(RegistryKeys.BLOCK) - .getOptional(RegistryKey.of(RegistryKeys.BLOCK, blockId)) - .getOrNull() - if (block == null) { - logger.error("Failed to load block texture override at ${file}: unknown block '$blockId'") - continue - } - val replacements = islandMpa.getOrPut(block.value(), ::mutableListOf) - replacements.add(BlockReplacement(json.area, replacement)) - } - } - } - - return BakedReplacements(map.mapValues { LocationReplacements(it.value) }) - } - - @JvmStatic - fun patchIndigo(orig: BakedModel, pos: BlockPos, state: BlockState): BakedModel { - return getReplacementModel(state, pos) ?: orig - } - - @Subscribe - fun onStart(event: FinalizeResourceManagerEvent) { - event.resourceManager.registerReloader(object : - SinglePreparationResourceReloader<BakedReplacements>() { - override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements { - return preparationFuture.join() - } - - override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: Profiler?) { - allLocationReplacements = prepared - refreshReplacements() - } - }) - } + @Serializable + data class CustomBlockOverride( + val modes: @Serializable(SingletonSerializableList::class) List<String>, + val area: List<Area>? = null, + val replacements: Map<Identifier, Replacement>, + ) + + @Serializable(with = Replacement.Serializer::class) + data class Replacement( + val block: Identifier, + val sound: Identifier?, + ) { + + @Transient + val blockModelIdentifier get() = block.withPrefixedPath("block/") + + /** + * Guaranteed to be set after [BakedReplacements.modelBakingFuture] is complete. + */ + @Transient + lateinit var blockModel: BlockStateModel + + @OptIn(ExperimentalSerializationApi::class) + @kotlinx.serialization.Serializer(Replacement::class) + object DefaultSerializer : KSerializer<Replacement> + + object Serializer : KSerializer<Replacement> { + val delegate = serializer<JsonElement>() + override val descriptor: SerialDescriptor + get() = delegate.descriptor + + override fun deserialize(decoder: Decoder): Replacement { + val jsonElement = decoder.decodeSerializableValue(delegate) + if (jsonElement is JsonPrimitive) { + require(jsonElement.isString) + return Replacement(Identifier.tryParse(jsonElement.content)!!, null) + } + return (decoder as JsonDecoder).json.decodeFromJsonElement(DefaultSerializer, jsonElement) + } + + override fun serialize(encoder: Encoder, value: Replacement) { + encoder.encodeSerializableValue(DefaultSerializer, value) + } + } + } + + @Serializable + data class Area( + val min: BlockPos, + val max: BlockPos, + ) { + @Transient + val realMin = BlockPos( + minOf(min.x, max.x), + minOf(min.y, max.y), + minOf(min.z, max.z), + ) + + @Transient + val realMax = BlockPos( + maxOf(min.x, max.x), + maxOf(min.y, max.y), + maxOf(min.z, max.z), + ) + + fun roughJoin(other: Area): Area { + return Area( + BlockPos( + minOf(realMin.x, other.realMin.x), + minOf(realMin.y, other.realMin.y), + minOf(realMin.z, other.realMin.z), + ), + BlockPos( + maxOf(realMax.x, other.realMax.x), + maxOf(realMax.y, other.realMax.y), + maxOf(realMax.z, other.realMax.z), + ) + ) + } + + fun contains(blockPos: BlockPos): Boolean { + return (blockPos.x in realMin.x..realMax.x) && + (blockPos.y in realMin.y..realMax.y) && + (blockPos.z in realMin.z..realMax.z) + } + } + + data class LocationReplacements( + val lookup: Map<Block, List<BlockReplacement>> + ) + + data class BlockReplacement( + val checks: List<Area>?, + val replacement: Replacement, + ) { + val roughCheck by lazy(LazyThreadSafetyMode.NONE) { + if (checks == null || checks.size < 3) return@lazy null + checks.reduce { acc, next -> acc.roughJoin(next) } + } + } + + data class BakedReplacements(val data: Map<SkyBlockIsland, LocationReplacements>) { + /** + * Fulfilled by [createBakedModels] which is called during model baking. Once completed, all [Replacement.blockModel] will be set. + */ + val modelBakingFuture = CompletableFuture<Unit>() + + /** + * @returns a list of all [Replacement]s. + */ + fun collectAllReplacements(): Sequence<Replacement> { + return data.values.asSequence() + .flatMap { it.lookup.values } + .flatten() + .map { it.replacement } + } + } + + var allLocationReplacements: BakedReplacements = BakedReplacements(mapOf()) + var currentIslandReplacements: LocationReplacements? = null + + fun refreshReplacements() { + val location = SBData.skyblockLocation + val replacements = + if (CustomSkyBlockTextures.TConfig.enableBlockOverrides) location?.let(allLocationReplacements.data::get) + else null + val lastReplacements = currentIslandReplacements + currentIslandReplacements = replacements + if (lastReplacements != replacements) { + MC.nextTick { + MC.worldRenderer.chunks?.chunks?.forEach { + // false schedules rebuilds outside a 27 block radius to happen async + it.scheduleRebuild(false) + } + sodiumReloadTask?.run() + } + } + } + + private val sodiumReloadTask = runCatching { + val r = Class.forName("moe.nea.firmament.compat.sodium.SodiumChunkReloader") + .getConstructor() + .newInstance() as Runnable + r.run() + r + }.getOrElse { + if (FabricLoader.getInstance().isModLoaded("sodium")) + logger.error("Could not create sodium chunk reloader") + null + } + + + fun matchesPosition(replacement: BlockReplacement, blockPos: BlockPos?): Boolean { + if (blockPos == null) return true + val rc = replacement.roughCheck + if (rc != null && !rc.contains(blockPos)) return false + val areas = replacement.checks + if (areas != null && !areas.any { it.contains(blockPos) }) return false + return true + } + + @JvmStatic + fun getReplacementModel(block: BlockState, blockPos: BlockPos?): BlockStateModel? { + return getReplacement(block, blockPos)?.blockModel + } + + @JvmStatic + fun getReplacement(block: BlockState, blockPos: BlockPos?): Replacement? { + if (isInFallback() && blockPos == null) { + return null + } + val replacements = currentIslandReplacements?.lookup?.get(block.block) ?: return null + for (replacement in replacements) { + if (replacement.checks == null || matchesPosition(replacement, blockPos)) + return replacement.replacement + } + return null + } + + + @Subscribe + fun onLocation(event: SkyblockServerUpdateEvent) { + refreshReplacements() + } + + @Volatile + var preparationFuture: CompletableFuture<BakedReplacements> = CompletableFuture.completedFuture(BakedReplacements( + mapOf())) + + val insideFallbackCall = ThreadLocal.withInitial { 0 } + + @JvmStatic + fun enterFallbackCall() { + insideFallbackCall.set(insideFallbackCall.get() + 1) + } + + fun isInFallback() = insideFallbackCall.get() > 0 + + @JvmStatic + fun exitFallbackCall() { + insideFallbackCall.set(insideFallbackCall.get() - 1) + } + + @Subscribe + fun onEarlyReload(event: EarlyResourceReloadEvent) { + preparationFuture = CompletableFuture + .supplyAsync( + { prepare(event.resourceManager) }, event.preparationExecutor) + } + + private fun prepare(manager: ResourceManager): BakedReplacements { + val resources = manager.findResources("overrides/blocks") { + it.namespace == "firmskyblock" && it.path.endsWith(".json") + } + val map = mutableMapOf<SkyBlockIsland, MutableMap<Block, MutableList<BlockReplacement>>>() + for ((file, resource) in resources) { + val json = + Firmament.tryDecodeJsonFromStream<CustomBlockOverride>(resource.inputStream) + .getOrElse { ex -> + logger.error("Failed to load block texture override at $file", ex) + continue + } + for (mode in json.modes) { + val island = SkyBlockIsland.forMode(mode) + val islandMpa = map.getOrPut(island, ::mutableMapOf) + for ((blockId, replacement) in json.replacements) { + val block = MC.defaultRegistries.getOrThrow(RegistryKeys.BLOCK) + .getOptional(RegistryKey.of(RegistryKeys.BLOCK, blockId)) + .getOrNull() + if (block == null) { + logger.error("Failed to load block texture override at ${file}: unknown block '$blockId'") + continue + } + val replacements = islandMpa.getOrPut(block.value(), ::mutableListOf) + replacements.add(BlockReplacement(json.area, replacement)) + } + } + } + + return BakedReplacements(map.mapValues { LocationReplacements(it.value) }) + } + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloader(object : + SinglePreparationResourceReloader<BakedReplacements>() { + override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements { + return preparationFuture.join().also { + it.modelBakingFuture.join() + } + } + + override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: Profiler?) { + allLocationReplacements = prepared + refreshReplacements() + } + }) + } + + fun simpleBlockModel(blockId: Identifier): SimpleBlockStateModel.Unbaked { + // TODO: does this need to be shared between resolving and baking? I think not, but it would probably be wise to do so in the future. + return SimpleBlockStateModel.Unbaked( + ModelVariant(blockId) + ) + } + + /** + * Used by [moe.nea.firmament.init.SectionBuilderRiser] + */ + + @JvmStatic + fun patchIndigo(original: BlockStateModel, pos: BlockPos?, state: BlockState): BlockStateModel { + return getReplacementModel(state, pos) ?: original + } + + @JvmStatic + fun collectExtraModels(modelsCollector: ReferencedModelsCollector) { + preparationFuture.join().collectAllReplacements() + .forEach { modelsCollector.resolve(simpleBlockModel(it.blockModelIdentifier)) } + } + + @JvmStatic + fun createBakedModels(baker: Baker, executor: Executor): CompletableFuture<Void?> { + return preparationFuture.thenComposeAsync(Function { replacements -> + val byModel = replacements.collectAllReplacements().groupBy { it.blockModelIdentifier } + val modelBakingTask = AsyncHelper.mapValues(byModel, { blockId, replacements -> + val unbakedModel = SimpleBlockStateModel.Unbaked( + ModelVariant(blockId) + ) + val baked = unbakedModel.bake(baker) + replacements.forEach { + it.blockModel = baked + } + }, executor) + modelBakingTask.thenAcceptAsync { replacements.modelBakingFuture.complete(Unit) } + }, executor) + } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt index aafc85a..8a2bde5 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt @@ -81,20 +81,27 @@ object CustomGlobalArmorOverrides { null, Optional.of(RegistryKey.of(EquipmentAssetKeys.REGISTRY_KEY, model)), Optional.empty(), - Optional.empty(), false, false, false + Optional.empty(), + false, + false, + false, + false ) } + // TODO: BipedEntityRenderer.getEquippedStack create copies of itemstacks for rendering. This means this cache is essentially useless + // If i figure out how to circumvent this (maybe track the origin of those copied itemstacks in some sort of variable in the itemstack to track back the original instance) i should reenable this cache. + // Then also re add this to the cache clearing function val overrideCache = - WeakCache.memoize<ItemStack, EquipmentSlot, Optional<EquippableComponent>>("ArmorOverrides") { stack, slot -> - val id = stack.skyBlockId ?: return@memoize Optional.empty() - val override = overrides[id.neuItem] ?: return@memoize Optional.empty() + WeakCache.dontMemoize<ItemStack, EquipmentSlot, Optional<EquippableComponent>>("ArmorOverrides") { stack, slot -> + val id = stack.skyBlockId ?: return@dontMemoize Optional.empty() + val override = overrides[id.neuItem] ?: return@dontMemoize Optional.empty() for (suboverride in override.overrides) { if (suboverride.predicate.test(stack)) { - return@memoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional() + return@dontMemoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional() } } - return@memoize resolveComponent(slot, override.modelIdentifier).intoOptional() + return@dontMemoize resolveComponent(slot, override.modelIdentifier).intoOptional() } var overrides: Map<String, ArmorOverride> = mapOf() @@ -111,7 +118,7 @@ object CustomGlobalArmorOverrides { val equipmentLayers = layers.map { EquipmentModel.Layer( it.identifier, if (it.tint) { - Optional.of(EquipmentModel.Dyeable(Optional.empty())) + Optional.of(EquipmentModel.Dyeable(Optional.of(0xFFA06540.toInt()))) } else { Optional.empty() }, diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt index 20f1fb6..403e3bd 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt @@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import kotlin.jvm.optionals.getOrNull -import net.minecraft.client.util.ModelIdentifier import net.minecraft.resource.ResourceManager import net.minecraft.resource.SinglePreparationResourceReloader import net.minecraft.text.Text diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt index 4529d1d..1da840d 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt @@ -17,12 +17,14 @@ import moe.nea.firmament.features.texturepack.predicates.AndPredicate import moe.nea.firmament.features.texturepack.predicates.CastPredicate import moe.nea.firmament.features.texturepack.predicates.DisplayNamePredicate import moe.nea.firmament.features.texturepack.predicates.ExtraAttributesPredicate +import moe.nea.firmament.features.texturepack.predicates.GenericComponentPredicate import moe.nea.firmament.features.texturepack.predicates.ItemPredicate import moe.nea.firmament.features.texturepack.predicates.LorePredicate import moe.nea.firmament.features.texturepack.predicates.NotPredicate import moe.nea.firmament.features.texturepack.predicates.OrPredicate import moe.nea.firmament.features.texturepack.predicates.PetPredicate import moe.nea.firmament.features.texturepack.predicates.PullingPredicate +import moe.nea.firmament.features.texturepack.predicates.SkullPredicate import moe.nea.firmament.util.json.KJsonOps object CustomModelOverrideParser { @@ -63,6 +65,8 @@ object CustomModelOverrideParser { registerPredicateParser("item", ItemPredicate.Parser) registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser) registerPredicateParser("pet", PetPredicate.Parser) + registerPredicateParser("component", GenericComponentPredicate.Parser) + registerPredicateParser("skull", SkullPredicate.Parser) } private val neverPredicate = listOf( @@ -110,6 +114,10 @@ object CustomModelOverrideParser { Firmament.identifier("predicates/legacy"), PredicateModel.Unbaked.CODEC ) + ItemModelTypes.ID_MAPPER.put( + Firmament.identifier("head_model"), + HeadModelChooser.Unbaked.CODEC + ) } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt index bef52d2..cf2a232 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt @@ -45,7 +45,7 @@ object CustomSkyBlockTextures : FirmamentFeature { listOf( skullTextureCache.cache, CustomItemModelEvent.cache.cache, - CustomGlobalArmorOverrides.overrideCache.cache + // TODO: re-add this once i figure out how to make the cache useful again CustomGlobalArmorOverrides.overrideCache.cache ) } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt index 6cef4ca..e020d66 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt @@ -1,8 +1,10 @@ package moe.nea.firmament.features.texturepack +import kotlinx.serialization.Serializable import net.minecraft.entity.LivingEntity import net.minecraft.item.ItemStack +@Serializable(with = FirmamentRootPredicateSerializer::class) interface FirmamentModelPredicate { fun test(stack: ItemStack, holder: LivingEntity?): Boolean = test(stack) fun test(stack: ItemStack): Boolean = test(stack, null) diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt new file mode 100644 index 0000000..3e8cc4e --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt @@ -0,0 +1,90 @@ +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonObject +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.minecraft.client.item.ItemModelManager +import net.minecraft.client.render.item.ItemRenderState +import net.minecraft.client.render.item.model.BasicItemModel +import net.minecraft.client.render.item.model.ItemModel +import net.minecraft.client.render.item.model.ItemModelTypes +import net.minecraft.client.render.model.ResolvableModel +import net.minecraft.client.world.ClientWorld +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemDisplayContext +import net.minecraft.item.ItemStack +import net.minecraft.util.Identifier + +object HeadModelChooser { + val IS_CHOOSING_HEAD_MODEL = ThreadLocal.withInitial { false } + + interface HasExplicitHeadModelMarker { + fun markExplicitHead_Firmament() + fun isExplicitHeadModel_Firmament(): Boolean + companion object{ + @JvmStatic + fun cast(state: ItemRenderState) = state as HasExplicitHeadModelMarker + } + } + + data class Baked(val head: ItemModel, val regular: ItemModel) : ItemModel { + override fun update( + state: ItemRenderState, + stack: ItemStack?, + resolver: ItemModelManager?, + displayContext: ItemDisplayContext, + world: ClientWorld?, + user: LivingEntity?, + seed: Int + ) { + val instance = + if (IS_CHOOSING_HEAD_MODEL.get()) { + HasExplicitHeadModelMarker.cast(state).markExplicitHead_Firmament() + head + } else { + regular + } + instance.update(state, stack, resolver, displayContext, world, user, seed) + } + } + + data class Unbaked( + val head: ItemModel.Unbaked, + val regular: ItemModel.Unbaked, + ) : ItemModel.Unbaked { + override fun getCodec(): MapCodec<out ItemModel.Unbaked> { + return CODEC + } + + override fun bake(context: ItemModel.BakeContext): ItemModel { + return Baked( + head.bake(context), + regular.bake(context) + ) + } + + override fun resolve(resolver: ResolvableModel.Resolver) { + head.resolve(resolver) + regular.resolve(resolver) + } + + companion object { + @JvmStatic + fun fromLegacyJson(jsonObject: JsonObject, unbakedModel: ItemModel.Unbaked): ItemModel.Unbaked { + val model = jsonObject["firmament:head_model"] ?: return unbakedModel + val modelUrl = model.asJsonPrimitive.asString + val headModel = BasicItemModel.Unbaked(Identifier.of(modelUrl), listOf()) + return Unbaked(headModel, unbakedModel) + } + + val CODEC = RecordCodecBuilder.mapCodec { + it.group( + ItemModelTypes.CODEC.fieldOf("head") + .forGetter(Unbaked::head), + ItemModelTypes.CODEC.fieldOf("regular") + .forGetter(Unbaked::regular), + ).apply(it, ::Unbaked) + } + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt index 0edad4c..e6b5bcf 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt @@ -9,12 +9,11 @@ import net.minecraft.client.render.item.ItemRenderState import net.minecraft.client.render.item.model.BasicItemModel import net.minecraft.client.render.item.model.ItemModel import net.minecraft.client.render.item.model.ItemModelTypes -import net.minecraft.client.render.item.tint.TintSource import net.minecraft.client.render.model.ResolvableModel import net.minecraft.client.world.ClientWorld import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemDisplayContext import net.minecraft.item.ItemStack -import net.minecraft.item.ModelTransformationMode import net.minecraft.util.Identifier import moe.nea.firmament.features.texturepack.predicates.AndPredicate @@ -29,10 +28,10 @@ class PredicateModel { ) override fun update( - state: ItemRenderState, + state: ItemRenderState?, stack: ItemStack, - resolver: ItemModelManager, - transformationMode: ModelTransformationMode, + resolver: ItemModelManager?, + displayContext: ItemDisplayContext?, world: ClientWorld?, user: LivingEntity?, seed: Int @@ -42,7 +41,7 @@ class PredicateModel { .findLast { it.predicate.test(stack, user) } ?.model ?: fallback - model.update(state, stack, resolver, transformationMode, world, user, seed) + model.update(state, stack, resolver, displayContext, world, user, seed) } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt index 2b13284..dd28d9f 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt @@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlin.jvm.optionals.getOrNull import net.minecraft.nbt.NbtString import net.minecraft.text.Text import moe.nea.firmament.util.MC @@ -26,7 +27,7 @@ interface StringMatcher { } fun matches(nbt: NbtString): Boolean { - val string = nbt.asString() + val string = nbt.value val jsonStart = string.indexOf('{') val stringStart = string.indexOf('"') val isString = stringStart >= 0 && string.subSequence(0, stringStart).isBlank() diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt index 2b79c1a..321f87c 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt @@ -16,7 +16,7 @@ class CastPredicate : FirmamentModelPredicate { } override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { - return (holder as? PlayerEntity)?.fishHook != null && holder.activeItem === stack + return (holder as? PlayerEntity)?.fishHook != null && holder.mainHandStack === stack } override fun test(stack: ItemStack): Boolean { diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt index 3c8023d..8115739 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt @@ -1,215 +1,220 @@ package moe.nea.firmament.features.texturepack.predicates -import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonPrimitive +import kotlin.jvm.optionals.getOrDefault +import kotlin.jvm.optionals.getOrNull import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser import moe.nea.firmament.features.texturepack.StringMatcher import net.minecraft.item.ItemStack import net.minecraft.nbt.NbtByte -import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtDouble import net.minecraft.nbt.NbtElement import net.minecraft.nbt.NbtFloat import net.minecraft.nbt.NbtInt -import net.minecraft.nbt.NbtList import net.minecraft.nbt.NbtLong import net.minecraft.nbt.NbtShort -import net.minecraft.nbt.NbtString import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.mc.NbtPrism fun interface NbtMatcher { - fun matches(nbt: NbtElement): Boolean - - object Parser { - fun parse(jsonElement: JsonElement): NbtMatcher? { - if (jsonElement is JsonPrimitive) { - if (jsonElement.isString) { - val string = jsonElement.asString - return MatchStringExact(string) - } - if (jsonElement.isNumber) { - return MatchNumberExact(jsonElement.asLong) //TODO: parse generic number - } - } - if (jsonElement is JsonObject) { - var encounteredParser: NbtMatcher? = null - for (entry in ExclusiveParserType.entries) { - val data = jsonElement[entry.key] ?: continue - if (encounteredParser != null) { - // TODO: warn - return null - } - encounteredParser = entry.parse(data) ?: return null - } - return encounteredParser - } - return null - } - - enum class ExclusiveParserType(val key: String) { - STRING("string") { - override fun parse(element: JsonElement): NbtMatcher? { - return MatchString(StringMatcher.parse(element)) - } - }, - INT("int") { - override fun parse(element: JsonElement): NbtMatcher? { - return parseGenericNumber(element, - { it.asInt }, - { (it as? NbtInt)?.intValue() }, - { a, b -> - if (a == b) Comparison.EQUAL - else if (a < b) Comparison.LESS_THAN - else Comparison.GREATER - }) - } - }, - FLOAT("float") { - override fun parse(element: JsonElement): NbtMatcher? { - return parseGenericNumber(element, - { it.asFloat }, - { (it as? NbtFloat)?.floatValue() }, - { a, b -> - if (a == b) Comparison.EQUAL - else if (a < b) Comparison.LESS_THAN - else Comparison.GREATER - }) - } - }, - DOUBLE("double") { - override fun parse(element: JsonElement): NbtMatcher? { - return parseGenericNumber(element, - { it.asDouble }, - { (it as? NbtDouble)?.doubleValue() }, - { a, b -> - if (a == b) Comparison.EQUAL - else if (a < b) Comparison.LESS_THAN - else Comparison.GREATER - }) - } - }, - LONG("long") { - override fun parse(element: JsonElement): NbtMatcher? { - return parseGenericNumber(element, - { it.asLong }, - { (it as? NbtLong)?.longValue() }, - { a, b -> - if (a == b) Comparison.EQUAL - else if (a < b) Comparison.LESS_THAN - else Comparison.GREATER - }) - } - }, - SHORT("short") { - override fun parse(element: JsonElement): NbtMatcher? { - return parseGenericNumber(element, - { it.asShort }, - { (it as? NbtShort)?.shortValue() }, - { a, b -> - if (a == b) Comparison.EQUAL - else if (a < b) Comparison.LESS_THAN - else Comparison.GREATER - }) - } - }, - BYTE("byte") { - override fun parse(element: JsonElement): NbtMatcher? { - return parseGenericNumber(element, - { it.asByte }, - { (it as? NbtByte)?.byteValue() }, - { a, b -> - if (a == b) Comparison.EQUAL - else if (a < b) Comparison.LESS_THAN - else Comparison.GREATER - }) - } - }, - ; - - abstract fun parse(element: JsonElement): NbtMatcher? - } - - enum class Comparison { - LESS_THAN, EQUAL, GREATER - } - - inline fun <T : Any> parseGenericNumber( - jsonElement: JsonElement, - primitiveExtractor: (JsonPrimitive) -> T?, - crossinline nbtExtractor: (NbtElement) -> T?, - crossinline compare: (T, T) -> Comparison - ): NbtMatcher? { - if (jsonElement is JsonPrimitive) { - val expected = primitiveExtractor(jsonElement) ?: return null - return NbtMatcher { - val actual = nbtExtractor(it) ?: return@NbtMatcher false - compare(actual, expected) == Comparison.EQUAL - } - } - if (jsonElement is JsonObject) { - val minElement = jsonElement.getAsJsonPrimitive("min") - val min = if (minElement != null) primitiveExtractor(minElement) ?: return null else null - val minExclusive = jsonElement.get("minExclusive")?.asBoolean ?: false - val maxElement = jsonElement.getAsJsonPrimitive("max") - val max = if (maxElement != null) primitiveExtractor(maxElement) ?: return null else null - val maxExclusive = jsonElement.get("maxExclusive")?.asBoolean ?: true - if (min == null && max == null) return null - return NbtMatcher { - val actual = nbtExtractor(it) ?: return@NbtMatcher false - if (max != null) { - val comp = compare(actual, max) - if (comp == Comparison.GREATER) return@NbtMatcher false - if (comp == Comparison.EQUAL && maxExclusive) return@NbtMatcher false - } - if (min != null) { - val comp = compare(actual, min) - if (comp == Comparison.LESS_THAN) return@NbtMatcher false - if (comp == Comparison.EQUAL && minExclusive) return@NbtMatcher false - } - return@NbtMatcher true - } - } - return null - - } - } - - class MatchNumberExact(val number: Long) : NbtMatcher { - override fun matches(nbt: NbtElement): Boolean { - return when (nbt) { - is NbtByte -> nbt.byteValue().toLong() == number - is NbtInt -> nbt.intValue().toLong() == number - is NbtShort -> nbt.shortValue().toLong() == number - is NbtLong -> nbt.longValue().toLong() == number - else -> false - } - } - - } - - class MatchStringExact(val string: String) : NbtMatcher { - override fun matches(nbt: NbtElement): Boolean { - return nbt is NbtString && nbt.asString() == string - } - - override fun toString(): String { - return "MatchNbtStringExactly($string)" - } - } - - class MatchString(val string: StringMatcher) : NbtMatcher { - override fun matches(nbt: NbtElement): Boolean { - return nbt is NbtString && string.matches(nbt.asString()) - } - - override fun toString(): String { - return "MatchNbtString($string)" - } - } + fun matches(nbt: NbtElement): Boolean + + object Parser { + fun parse(jsonElement: JsonElement): NbtMatcher? { + if (jsonElement is JsonPrimitive) { + if (jsonElement.isString) { + val string = jsonElement.asString + return MatchStringExact(string) + } + if (jsonElement.isNumber) { + return MatchNumberExact(jsonElement.asLong) // TODO: parse generic number + } + } + if (jsonElement is JsonObject) { + var encounteredParser: NbtMatcher? = null + for (entry in ExclusiveParserType.entries) { + val data = jsonElement[entry.key] ?: continue + if (encounteredParser != null) { + // TODO: warn + return null + } + encounteredParser = entry.parse(data) ?: return null + } + return encounteredParser + } + return null + } + + enum class ExclusiveParserType(val key: String) { + STRING("string") { + override fun parse(element: JsonElement): NbtMatcher? { + return MatchString(StringMatcher.parse(element)) + } + }, + INT("int") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asInt }, + { (it as? NbtInt)?.intValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + FLOAT("float") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asFloat }, + { (it as? NbtFloat)?.floatValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + DOUBLE("double") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asDouble }, + { (it as? NbtDouble)?.doubleValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + LONG("long") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asLong }, + { (it as? NbtLong)?.longValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + SHORT("short") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asShort }, + { (it as? NbtShort)?.shortValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + BYTE("byte") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asByte }, + { (it as? NbtByte)?.byteValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + ; + + abstract fun parse(element: JsonElement): NbtMatcher? + } + + enum class Comparison { + LESS_THAN, EQUAL, GREATER + } + + inline fun <T : Any> parseGenericNumber( + jsonElement: JsonElement, + primitiveExtractor: (JsonPrimitive) -> T?, + crossinline nbtExtractor: (NbtElement) -> T?, + crossinline compare: (T, T) -> Comparison + ): NbtMatcher? { + if (jsonElement is JsonPrimitive) { + val expected = primitiveExtractor(jsonElement) ?: return null + return NbtMatcher { + val actual = nbtExtractor(it) ?: return@NbtMatcher false + compare(actual, expected) == Comparison.EQUAL + } + } + if (jsonElement is JsonObject) { + val minElement = jsonElement.getAsJsonPrimitive("min") + val min = if (minElement != null) primitiveExtractor(minElement) ?: return null else null + val minExclusive = jsonElement.get("minExclusive")?.asBoolean ?: false + val maxElement = jsonElement.getAsJsonPrimitive("max") + val max = if (maxElement != null) primitiveExtractor(maxElement) ?: return null else null + val maxExclusive = jsonElement.get("maxExclusive")?.asBoolean ?: true + if (min == null && max == null) return null + return NbtMatcher { + val actual = nbtExtractor(it) ?: return@NbtMatcher false + if (max != null) { + val comp = compare(actual, max) + if (comp == Comparison.GREATER) return@NbtMatcher false + if (comp == Comparison.EQUAL && maxExclusive) return@NbtMatcher false + } + if (min != null) { + val comp = compare(actual, min) + if (comp == Comparison.LESS_THAN) return@NbtMatcher false + if (comp == Comparison.EQUAL && minExclusive) return@NbtMatcher false + } + return@NbtMatcher true + } + } + return null + + } + } + + class MatchNumberExact(val number: Long) : NbtMatcher { + override fun matches(nbt: NbtElement): Boolean { + return when (nbt) { + is NbtByte -> nbt.byteValue().toLong() == number + is NbtInt -> nbt.intValue().toLong() == number + is NbtShort -> nbt.shortValue().toLong() == number + is NbtLong -> nbt.longValue().toLong() == number + else -> false + } + } + + } + + class MatchStringExact(val string: String) : NbtMatcher { + override fun matches(nbt: NbtElement): Boolean { + return nbt.asString().getOrNull() == string + } + + override fun toString(): String { + return "MatchNbtStringExactly($string)" + } + } + + class MatchString(val string: StringMatcher) : NbtMatcher { + override fun matches(nbt: NbtElement): Boolean { + return nbt.asString().map(string::matches).getOrDefault(false) + } + + override fun toString(): String { + return "MatchNbtString($string)" + } + } } data class ExtraAttributesPredicate( @@ -217,55 +222,20 @@ data class ExtraAttributesPredicate( val matcher: NbtMatcher, ) : FirmamentModelPredicate { - object Parser : FirmamentModelPredicateParser { - override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { - if (jsonElement !is JsonObject) return null - val path = jsonElement.get("path") ?: return null - val pathSegments = if (path is JsonArray) { - path.map { (it as JsonPrimitive).asString } - } else if (path is JsonPrimitive && path.isString) { - path.asString.split(".") - } else return null - val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement) - ?: return null - return ExtraAttributesPredicate(NbtPrism(pathSegments), matcher) - } - } - - override fun test(stack: ItemStack): Boolean { - return path.access(stack.extraAttributes) - .any { matcher.matches(it) } - } + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + if (jsonElement !is JsonObject) return null + val path = jsonElement.get("path") ?: return null + val prism = NbtPrism.fromElement(path) ?: return null + val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement) + ?: return null + return ExtraAttributesPredicate(prism, matcher) + } + } + + override fun test(stack: ItemStack): Boolean { + return path.access(stack.extraAttributes) + .any { matcher.matches(it) } + } } -class NbtPrism(val path: List<String>) { - override fun toString(): String { - return "Prism($path)" - } - fun access(root: NbtElement): Collection<NbtElement> { - var rootSet = mutableListOf(root) - var switch = mutableListOf<NbtElement>() - for (pathSegment in path) { - if (pathSegment == ".") continue - for (element in rootSet) { - if (element is NbtList) { - if (pathSegment == "*") - switch.addAll(element) - val index = pathSegment.toIntOrNull() ?: continue - if (index !in element.indices) continue - switch.add(element[index]) - } - if (element is NbtCompound) { - if (pathSegment == "*") - element.keys.mapTo(switch) { element.get(it)!! } - switch.add(element.get(pathSegment) ?: continue) - } - } - val temp = switch - switch = rootSet - rootSet = temp - switch.clear() - } - return rootSet - } -} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt new file mode 100644 index 0000000..71392ef --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt @@ -0,0 +1,58 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.mojang.serialization.Codec +import kotlin.jvm.optionals.getOrNull +import net.minecraft.component.ComponentType +import net.minecraft.component.type.NbtComponent +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtOps +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.util.Identifier +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.NbtPrism + +data class GenericComponentPredicate<T>( + val componentType: ComponentType<T>, + val codec: Codec<T>, + val path: NbtPrism, + val matcher: NbtMatcher, +) : FirmamentModelPredicate { + constructor(componentType: ComponentType<T>, path: NbtPrism, matcher: NbtMatcher) + : this(componentType, componentType.codecOrThrow, path, matcher) + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + val component = stack.get(componentType) ?: return false + // TODO: cache this + val nbt = + if (component is NbtComponent) component.nbt + else codec.encodeStart(NbtOps.INSTANCE, component) + .resultOrPartial().getOrNull() ?: return false + return path.access(nbt).any { matcher.matches(it) } + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): GenericComponentPredicate<*>? { + if (jsonElement !is JsonObject) return null + val path = jsonElement.get("path") ?: return null + val prism = NbtPrism.fromElement(path) ?: return null + val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement) + ?: return null + val component = MC.currentOrDefaultRegistries + .getOrThrow(RegistryKeys.DATA_COMPONENT_TYPE) + .getOrThrow( + RegistryKey.of( + RegistryKeys.DATA_COMPONENT_TYPE, + Identifier.of(jsonElement.get("component").asString) + ) + ).value() + return GenericComponentPredicate(component, prism, matcher) + } + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt new file mode 100644 index 0000000..416e86c --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.mojang.authlib.minecraft.MinecraftProfileTexture +import java.util.UUID +import kotlin.jvm.optionals.getOrNull +import net.minecraft.component.DataComponentTypes +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import moe.nea.firmament.features.texturepack.StringMatcher +import moe.nea.firmament.util.mc.decodeProfileTextureProperty +import moe.nea.firmament.util.parsePotentiallyDashlessUUID + +class SkullPredicate( + val profileId: UUID?, + val textureProfileId: UUID?, + val skinUrl: StringMatcher?, + val textureValue: StringMatcher?, +) : FirmamentModelPredicate { + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + val obj = jsonElement.asJsonObject + val profileId = obj.getAsJsonPrimitive("profileId") + ?.asString?.let(::parsePotentiallyDashlessUUID) + val textureProfileId = obj.getAsJsonPrimitive("textureProfileId") + ?.asString?.let(::parsePotentiallyDashlessUUID) + val textureValue = obj.get("textureValue")?.let(StringMatcher::parse) + val skinUrl = obj.get("skinUrl")?.let(StringMatcher::parse) + return SkullPredicate(profileId, textureProfileId, skinUrl, textureValue) + } + } + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + if (!stack.isOf(Items.PLAYER_HEAD)) return false + val profile = stack.get(DataComponentTypes.PROFILE) ?: return false + val textureProperty = profile.properties["textures"].firstOrNull() + val textureMode = lazy(LazyThreadSafetyMode.NONE) { + decodeProfileTextureProperty(textureProperty ?: return@lazy null) + } + when { + profileId != null + && profileId != profile.id.getOrNull() -> + return false + + textureValue != null + && !textureValue.matches(textureProperty?.value ?: "") -> + return false + + skinUrl != null + && !skinUrl.matches(textureMode.value?.textures?.get(MinecraftProfileTexture.Type.SKIN)?.url ?: "") -> + return false + + textureProfileId != null + && textureProfileId != textureMode.value?.profileId -> + return false + + else -> return true + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/BuildExtraBlockStateModels.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/BuildExtraBlockStateModels.java new file mode 100644 index 0000000..6b3c929 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/BuildExtraBlockStateModels.java @@ -0,0 +1,24 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.client.render.model.Baker; +import net.minecraft.client.render.model.ModelBaker; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +@Mixin(ModelBaker.class) +public class BuildExtraBlockStateModels { + @ModifyReturnValue(method = "bake", at = @At("RETURN")) + private CompletableFuture<ModelBaker.BakedModels> injectMoreBlockModels(CompletableFuture<ModelBaker.BakedModels> original, @Local ModelBaker.BakerImpl baker, @Local(argsOnly = true) Executor executor) { + Baker b = baker; + return original.thenCombine( + CustomBlockTextures.createBakedModels(b, executor), + (a, _void) -> a + ); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/InsertExtraBlockModelDependencies.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/InsertExtraBlockModelDependencies.java new file mode 100644 index 0000000..91779e7 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/InsertExtraBlockModelDependencies.java @@ -0,0 +1,28 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.client.item.ItemAssetsLoader; +import net.minecraft.client.render.model.BakedModelManager; +import net.minecraft.client.render.model.BlockStatesLoader; +import net.minecraft.client.render.model.ReferencedModelsCollector; +import net.minecraft.client.render.model.UnbakedModel; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Map; + +@Mixin(BakedModelManager.class) +public class InsertExtraBlockModelDependencies { + @Inject(method = "collect", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/ReferencedModelsCollector;addSpecialModel(Lnet/minecraft/util/Identifier;Lnet/minecraft/client/render/model/UnbakedModel;)V", shift = At.Shift.AFTER)) + private static void insertExtraModels( + Map<Identifier, UnbakedModel> modelMap, + BlockStatesLoader.LoadedModels stateDefinition, + ItemAssetsLoader.Result result, + CallbackInfoReturnable cir, @Local ReferencedModelsCollector modelsCollector) { + CustomBlockTextures.collectExtraModels(modelsCollector); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ItemRenderStateExtraInfo.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ItemRenderStateExtraInfo.java new file mode 100644 index 0000000..2872dd1 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ItemRenderStateExtraInfo.java @@ -0,0 +1,28 @@ +package moe.nea.firmament.mixins.custommodels; + +import moe.nea.firmament.features.texturepack.HeadModelChooser; +import net.minecraft.client.render.item.ItemRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ItemRenderState.class) +public class ItemRenderStateExtraInfo implements HeadModelChooser.HasExplicitHeadModelMarker { + boolean hasExplicitHead_firmament = false; + + @Inject(method = "clear", at = @At("HEAD")) + private void clear(CallbackInfo ci) { + hasExplicitHead_firmament = false; + } + + @Override + public void markExplicitHead_Firmament() { + hasExplicitHead_firmament = true; + } + + @Override + public boolean isExplicitHeadModel_Firmament() { + return hasExplicitHead_firmament; + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java index 81ea6cd..951e3be 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java @@ -1,23 +1,22 @@ package moe.nea.firmament.mixins.custommodels; -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides; import net.minecraft.client.render.entity.equipment.EquipmentModel; import net.minecraft.client.render.entity.equipment.EquipmentModelLoader; -import net.minecraft.client.render.entity.equipment.EquipmentRenderer; import net.minecraft.item.equipment.EquipmentAsset; import net.minecraft.registry.RegistryKey; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; // TODO: auto import legacy models, maybe!!! in a later patch tho -@Mixin(EquipmentRenderer.class) +@Mixin(EquipmentModelLoader.class) public class PatchLegacyArmorLayerSupport { - @WrapOperation(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/equipment/EquipmentModelLoader;get(Lnet/minecraft/registry/RegistryKey;)Lnet/minecraft/client/render/entity/equipment/EquipmentModel;")) - private EquipmentModel patchModelLayers(EquipmentModelLoader instance, RegistryKey<EquipmentAsset> assetKey, Operation<EquipmentModel> original) { + @Inject(method = "get", at = @At(value = "HEAD"), cancellable = true) + private void patchModelLayers(RegistryKey<EquipmentAsset> assetKey, CallbackInfoReturnable<EquipmentModel> cir) { var modelOverride = CustomGlobalArmorOverrides.overrideArmorLayer(assetKey.getValue()); - if (modelOverride != null) return modelOverride; - return original.call(instance, assetKey); + if (modelOverride != null) + cir.setReturnValue(modelOverride); } } diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java index f829da0..0fb6bf8 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java @@ -26,7 +26,6 @@ public class PatchLegacyTexturePathsIntoArmorLayers { // legacy format: "assets/{identifier.namespace}/textures/models/armor/{identifier.path}_layer_{isLegs ? 2 : 1}{suffix}.png" // suffix is sadly not available to us here. this means leather armor will look a bit shite var legacyIdentifier = this.textureId.withPath((textureName) -> { - String var10000 = layerType.asString(); return "textures/models/armor/" + textureName + "_layer_" + (layerType == EquipmentModel.LayerType.HUMANOID_LEGGINGS ? 2 : 1) + ".png"; diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java index f9a1d0d..95e7dce 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java @@ -16,7 +16,8 @@ import org.spongepowered.asm.mixin.injection.At; @Mixin(ClientPlayerInteractionManager.class) public class ReplaceBlockHitSoundPatch { - @WrapOperation(method = "updateBlockBreakingProgress", at = @At(value = "NEW", target = "(Lnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FFLnet/minecraft/util/math/random/Random;Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/client/sound/PositionedSoundInstance;")) + @WrapOperation(method = "updateBlockBreakingProgress", + at = @At(value = "NEW", target = "(Lnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FFLnet/minecraft/util/math/random/Random;Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/client/sound/PositionedSoundInstance;")) private PositionedSoundInstance replaceSound( SoundEvent sound, SoundCategory category, float volume, float pitch, Random random, BlockPos pos, Operation<PositionedSoundInstance> original, diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java index 711b2af..8d2ba38 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java @@ -5,34 +5,33 @@ import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; import moe.nea.firmament.features.texturepack.CustomBlockTextures; import net.minecraft.block.BlockState; -import net.minecraft.client.render.block.BlockModels; import net.minecraft.client.render.block.BlockRenderManager; -import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.chunk.SectionBuilder; +import net.minecraft.client.render.model.BlockStateModel; import net.minecraft.util.math.BlockPos; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -@Mixin(BlockRenderManager.class) +@Mixin(SectionBuilder.class) public class ReplaceBlockRenderManagerBlockModel { - @WrapOperation(method = "renderBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockRenderManager;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;")) - private BakedModel replaceModelInRenderBlock( - BlockRenderManager instance, BlockState state, Operation<BakedModel> original, @Local(argsOnly = true) BlockPos pos) { - var replacement = CustomBlockTextures.getReplacementModel(state, pos); - if (replacement != null) return replacement; - CustomBlockTextures.enterFallbackCall(); - var fallback = original.call(instance, state); - CustomBlockTextures.exitFallbackCall(); - return fallback; - } - - @WrapOperation(method = "renderDamage", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockModels;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;")) - private BakedModel replaceModelInRenderDamage( - BlockModels instance, BlockState state, Operation<BakedModel> original, @Local(argsOnly = true) BlockPos pos) { - var replacement = CustomBlockTextures.getReplacementModel(state, pos); - if (replacement != null) return replacement; - CustomBlockTextures.enterFallbackCall(); - var fallback = original.call(instance, state); - CustomBlockTextures.exitFallbackCall(); - return fallback; - } + @WrapOperation(method = "build", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockRenderManager;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BlockStateModel;")) + private BlockStateModel replaceModelInRenderBlock(BlockRenderManager instance, BlockState state, Operation<BlockStateModel> original, @Local(ordinal = 2) BlockPos pos) { + var replacement = CustomBlockTextures.getReplacementModel(state, pos); + if (replacement != null) return replacement; + CustomBlockTextures.enterFallbackCall(); + var fallback = original.call(instance, state); + CustomBlockTextures.exitFallbackCall(); + return fallback; + } +//TODO: cover renderDamage model +// @WrapOperation(method = "renderDamage", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockModels;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;")) +// private BakedModel replaceModelInRenderDamage( +// BlockModels instance, BlockState state, Operation<BakedModel> original, @Local(argsOnly = true) BlockPos pos) { +// var replacement = CustomBlockTextures.getReplacementModel(state, pos); +// if (replacement != null) return replacement; +// CustomBlockTextures.enterFallbackCall(); +// var fallback = original.call(instance, state); +// CustomBlockTextures.exitFallbackCall(); +// return fallback; +// } } diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java index 53ab74a..455fbf1 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java @@ -3,7 +3,7 @@ package moe.nea.firmament.mixins.custommodels; import moe.nea.firmament.features.texturepack.CustomBlockTextures; import net.minecraft.block.BlockState; import net.minecraft.client.render.block.BlockModels; -import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.BlockStateModel; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -13,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; public class ReplaceFallbackBlockModel { // TODO: add check to BlockDustParticle @Inject(method = "getModel", at = @At("HEAD"), cancellable = true) - private void getModel(BlockState state, CallbackInfoReturnable<BakedModel> cir) { + private void getModel(BlockState state, CallbackInfoReturnable<BlockStateModel> cir) { var replacement = CustomBlockTextures.getReplacementModel(state, null); if (replacement != null) cir.setReturnValue(replacement); diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceHeadModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceHeadModel.java new file mode 100644 index 0000000..f445f02 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceHeadModel.java @@ -0,0 +1,51 @@ +package moe.nea.firmament.mixins.custommodels; + +import moe.nea.firmament.features.texturepack.HeadModelChooser; +import net.minecraft.client.item.ItemModelManager; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.model.EntityModel; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.render.item.ItemRenderState; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemDisplayContext; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntityRenderer.class) +public class ReplaceHeadModel<T extends LivingEntity, S extends LivingEntityRenderState, M extends EntityModel<? super S>> { + @Shadow + @Final + protected ItemModelManager itemModelResolver; + + @Unique + private ItemRenderState tempRenderState = new ItemRenderState(); + + @Inject( + method = "updateRenderState(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;F)V", + at = @At("TAIL") + ) + private void replaceHeadModel( + T livingEntity, S livingEntityRenderState, float f, CallbackInfo ci + ) { + var headItemStack = livingEntity.getEquippedStack(EquipmentSlot.HEAD); + + HeadModelChooser.INSTANCE.getIS_CHOOSING_HEAD_MODEL().set(true); + tempRenderState.clear(); + this.itemModelResolver.updateForLivingEntity(tempRenderState, headItemStack, ItemDisplayContext.HEAD, livingEntity); + HeadModelChooser.INSTANCE.getIS_CHOOSING_HEAD_MODEL().set(false); + + if (HeadModelChooser.HasExplicitHeadModelMarker.cast(tempRenderState) + .isExplicitHeadModel_Firmament()) { + livingEntityRenderState.wearingSkullType = null; + var temp = livingEntityRenderState.headItemRenderState; + livingEntityRenderState.headItemRenderState = tempRenderState; + tempRenderState = temp; + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java index 97abd1f..f2a7409 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java @@ -26,7 +26,7 @@ public class ReplaceItemModelPatch implements IntrospectableItemModelManager { private Function<Identifier, ItemModel> modelGetter; @WrapOperation( - method = "update(Lnet/minecraft/client/render/item/ItemRenderState;Lnet/minecraft/item/ItemStack;Lnet/minecraft/item/ModelTransformationMode;Lnet/minecraft/world/World;Lnet/minecraft/entity/LivingEntity;I)V", + method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;")) private Object replaceItemModelByIdentifier(ItemStack instance, ComponentType componentType, Operation<Object> original) { var override = CustomItemModelEvent.getModelIdentifier(instance, this); diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java index 850ea53..75cedf8 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java @@ -5,6 +5,7 @@ import com.llamalad7.mixinextras.injector.ModifyReturnValue; import com.llamalad7.mixinextras.sugar.Local; import moe.nea.firmament.Firmament; import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures; +import moe.nea.firmament.features.texturepack.HeadModelChooser; import moe.nea.firmament.features.texturepack.PredicateModel; import moe.nea.firmament.util.ErrorUtil; import net.minecraft.client.item.ItemAsset; @@ -61,6 +62,7 @@ public class SupplyFakeModelPatch { try (var is = resource.getInputStream()) { var jsonObject = Firmament.INSTANCE.getGson().fromJson(new InputStreamReader(is), JsonObject.class); unbakedModel = PredicateModel.Unbaked.fromLegacyJson(jsonObject, unbakedModel); + unbakedModel = HeadModelChooser.Unbaked.fromLegacyJson(jsonObject, unbakedModel); } catch (Exception e) { ErrorUtil.INSTANCE.softError("Could not create resource for fake model supplication: " + model.getKey(), e); } diff --git a/symbols/src/main/kotlin/process/CompatMetaProcessor.kt b/symbols/src/main/kotlin/process/CompatMetaProcessor.kt new file mode 100644 index 0000000..0753e4c --- /dev/null +++ b/symbols/src/main/kotlin/process/CompatMetaProcessor.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.annotations.process + +import com.google.auto.service.AutoService +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.KSName + +class CompatMetaProcessor(val logger: KSPLogger, val codeGenerator: CodeGenerator, val sourceSetName: String) : + SymbolProcessor { + override fun process(resolver: Resolver): List<KSAnnotated> { + val files = resolver.getAllFiles().toList() + val packages = files.mapTo(mutableSetOf()) { it.packageName.asString() } + packages.add("moe.nea.firmament.annotations.generated.$sourceSetName") + val compatMeta = resolver.getSymbolsWithAnnotation("moe.nea.firmament.util.compatloader.CompatMeta") + .singleOrNull() as KSClassDeclaration? ?: return listOf() + val dependencies = Dependencies(aggregating = true, *files.toTypedArray()) + val generatedFileName = "GeneratedCompat${sourceSetName.replaceFirstChar { it.uppercaseChar() }}" + val compatFile = + codeGenerator.createNewFile(dependencies, "moe.nea.firmament.annotations.generated.$sourceSetName", generatedFileName) + .bufferedWriter() + compatFile.appendLine("// This file is @generated by SubscribeAnnotationProcessor") + compatFile.appendLine("// Do not edit") + compatFile.appendLine("package moe.nea.firmament.annotations.generated.$sourceSetName") + compatFile.appendLine("class $generatedFileName : moe.nea.firmament.util.compatloader.ICompatMetaGen {") + compatFile.appendLine(""" + override fun owns(className: String): Boolean { + return moe.nea.firmament.util.compatloader.CompatHelper.isOwnedByPackage(className, ${ + packages.joinToString { "\"" + it + "\"" } + }) + } + + override val meta: moe.nea.firmament.util.compatloader.ICompatMeta + get() = ${compatMeta.qualifiedName!!.asString()} +""") + compatFile.appendLine("}") + compatFile.close() + val metaInf = codeGenerator.createNewFileByPath( + dependencies, + "META-INF/services/moe.nea.firmament.util.compatloader.ICompatMetaGen", extensionName = "") + .bufferedWriter() + metaInf.append("moe.nea.firmament.annotations.generated.$sourceSetName.") + metaInf.appendLine(generatedFileName) + metaInf.close() + return listOf() + } + + + @AutoService(SymbolProcessorProvider::class) + class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return CompatMetaProcessor(environment.logger, + environment.codeGenerator, + environment.options["firmament.sourceset"] ?: "main") + } + } +} diff --git a/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt b/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt index d7aaf28..3eaf3d6 100644 --- a/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt +++ b/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt @@ -25,22 +25,19 @@ class SubscribeAnnotationProcessor( override fun finish() { subscriptions.sort() if (subscriptions.isEmpty()) return - val subscriptionSet = subscriptions.mapTo(mutableSetOf()) { it.parent.containingFile!! } + val subscriptionSet = subscriptions.mapTo(mutableSetOf()) { it.cf } val dependencies = Dependencies( aggregating = true, *subscriptionSet.toTypedArray()) val generatedFileName = "AllSubscriptions${sourceSetName.replaceFirstChar { it.uppercaseChar() }}" val subscriptionsFile = codeGenerator - .createNewFile(dependencies, "moe.nea.firmament.annotations.generated", generatedFileName) + .createNewFile(dependencies, "moe.nea.firmament.annotations.generated.$sourceSetName", generatedFileName) .bufferedWriter() subscriptionsFile.apply { appendLine("// This file is @generated by SubscribeAnnotationProcessor") appendLine("// Do not edit") - for (file in subscriptionSet) { - appendLine("// Dependency: ${file.filePath}") - } - appendLine("package moe.nea.firmament.annotations.generated") + appendLine("package moe.nea.firmament.annotations.generated.$sourceSetName") appendLine() appendLine("import moe.nea.firmament.events.subscription.*") appendLine() @@ -48,7 +45,7 @@ class SubscribeAnnotationProcessor( appendLine("class $generatedFileName : SubscriptionList {") appendLine(" override fun provideSubscriptions(addSubscription: (Subscription<*>) -> Unit) {") for (subscription in subscriptions) { - val owner = subscription.parent.qualifiedName!!.asString() + val owner = subscription.pQName.asString() val method = subscription.child.simpleName.asString() val type = subscription.type.declaration.qualifiedName!!.asString() appendLine(" addSubscription(Subscription<$type>(") @@ -65,7 +62,7 @@ class SubscribeAnnotationProcessor( dependencies, "META-INF/services/moe.nea.firmament.events.subscription.SubscriptionList", extensionName = "") .bufferedWriter() - metaInf.append("moe.nea.firmament.annotations.generated.") + metaInf.append("moe.nea.firmament.annotations.generated.$sourceSetName.") metaInf.appendLine(generatedFileName) metaInf.close() } @@ -75,13 +72,15 @@ class SubscribeAnnotationProcessor( val child: KSFunctionDeclaration, val type: KSType, ) : Comparable<Subscription> { + val cf = parent.containingFile!! + val pQName = parent.qualifiedName!! + val tName = type.declaration.qualifiedName!! override fun compareTo(other: Subscription): Int { - var compare = parent.qualifiedName!!.asString().compareTo(other.parent.qualifiedName!!.asString()) + var compare = pQName.asString().compareTo(other.pQName.asString()) if (compare != 0) return compare compare = other.child.simpleName.asString().compareTo(child.simpleName.asString()) if (compare != 0) return compare - compare = other.type.declaration.qualifiedName!!.asString() - .compareTo(type.declaration.qualifiedName!!.asString()) + compare = other.tName.asString().compareTo(tName.asString()) if (compare != 0) return compare return 0 } diff --git a/translations/en_us.json b/translations/en_us.json index 1d0a8ed..2f66c84 100644 --- a/translations/en_us.json +++ b/translations/en_us.json @@ -7,7 +7,8 @@ "firmament.command.waypoint.added": "Added waypoint %s %s %s.", "firmament.command.waypoint.clear": "Cleared waypoints.", "firmament.command.waypoint.import": "Imported %s waypoints from clipboard.", - "firmament.command.waypoint.import.error": "Could not import waypoints from clipboard. Make sure they are on ColeWeight format:\n[{\"x\": 69, \"y\":420, \"z\": 36}]", + "firmament.command.waypoint.ordered.toggle.false": "Disabled ordered waypoints", + "firmament.command.waypoint.ordered.toggle.true": "Enabled ordered waypoints", "firmament.command.waypoint.remove": "Removed waypoint %s. Other waypoints may have different indexes now.", "firmament.command.waypoint.remove.error": "Could not find waypoint with that index to delete.", "firmament.command.waypoint.skip": "Skipped 1 waypoint", @@ -117,13 +118,19 @@ "firmament.config.fixes.auto-sprint-keybinding": "Auto Sprint KeyBinding", "firmament.config.fixes.auto-sprint-keybinding.description": "Toggle auto sprint via this keybinding.", "firmament.config.fixes.auto-sprint.description": "This is different from vanilla sprint in the way that it only marks the keybinding pressed for the first tick of walking.", + "firmament.config.fixes.disable-hurt-cam": "No Hurt Cam", + "firmament.config.fixes.disable-hurt-cam.description": "Disable the damage screen shake animation.", "firmament.config.fixes.hide-mob-effects": "Hide Potion Effects", "firmament.config.fixes.hide-mob-effects.description": "Hide Potion effects on the right side of your player inventory.", + "firmament.config.fixes.hide-slot-highlights": "Hide Slot Highlights", + "firmament.config.fixes.hide-slot-highlights.description": "Hide slot highlights for items with disabled tooltip. This makes /sbmenu look nicer with smooth texture packs.", "firmament.config.fixes.peek-chat": "Peek Chat", "firmament.config.fixes.peek-chat.description": "Hold this keybinding to view the chat as if you have it opened, but while still being able to control your character.", "firmament.config.fixes.player-skins": "Fix unsigned Player Skins", "firmament.config.fixes.player-skins.description": "Mark all player skins as signed, preventing console spam, and some rendering issues.", "firmament.config.inventory-buttons": "Inventory buttons", + "firmament.config.inventory-buttons.hover-text": "Hover Tooltip", + "firmament.config.inventory-buttons.hover-text.description": "Hovering over inventory buttons will show the command they run.", "firmament.config.inventory-buttons.open-editor": "Open Editor", "firmament.config.inventory-buttons.open-editor.description": "Click anywhere to create a new inventory button or to edit one. Hold SHIFT to grid align.", "firmament.config.item-hotkeys": "Item Hotkeys", @@ -134,6 +141,11 @@ "firmament.config.item-rarity-cosmetics.background-hotbar": "Hotbar Background Rarity", "firmament.config.item-rarity-cosmetics.background-hotbar.description": "Show item rarity background in the hotbar.", "firmament.config.item-rarity-cosmetics.background.description": "Show a background behind each item, depending on its rarity.", + "firmament.config.jade-integration": "Jade / WAILA", + "firmament.config.jade-integration.blocks": "Enable Custom Blocks", + "firmament.config.jade-integration.blocks.description": "Show custom block descriptions and hardness levels in Jade.", + "firmament.config.jade-integration.progress": "Enable Custom Mining Progress", + "firmament.config.jade-integration.progress.description": "Show the custom mining progress in Jade, when in a world with mining fatigue.", "firmament.config.lore-timers": "Lore Timers", "firmament.config.lore-timers.format": "Time Format", "firmament.config.lore-timers.format.choice.american": "§9Ame§cri§fcan", @@ -153,8 +165,14 @@ "firmament.config.pets": "Pets", "firmament.config.pets.highlight-pet": "Highlight active pet", "firmament.config.pets.highlight-pet.description": "Highlight your currently selected pet in the /pets menu.", + "firmament.config.pets.pet-overlay": "Pet Info", + "firmament.config.pets.pet-overlay-hud": "Pet Info Hud", + "firmament.config.pets.pet-overlay-hud.description": "A HUD showing current active pet and the pet exp.", + "firmament.config.pets.pet-overlay.description": "Shows current active pet and pet exp on screen.", "firmament.config.pickaxe-info": "Pickaxes & Drills", "firmament.config.pickaxe-info.ability-cooldown": "Pickaxe Ability Cooldown", + "firmament.config.pickaxe-info.ability-cooldown-toast": "Pickaxe Ability Ready Toast", + "firmament.config.pickaxe-info.ability-cooldown-toast.description": "Shows a toast when your pickaxe ability is ready.", "firmament.config.pickaxe-info.ability-cooldown.description": "Show a cooldown on your cross-hair for your pickaxe ability.", "firmament.config.pickaxe-info.ability-scale": "Ability Cooldown Scale", "firmament.config.pickaxe-info.ability-scale.description": "Resize the cooldown around your cross-hair for your pickaxe ability.", @@ -178,6 +196,8 @@ "firmament.config.power-user.copy-skull-texture.description": "Copy the texture location that can be used to re-texture the skull under your cross-hair.", "firmament.config.power-user.copy-texture-pack-id": "Copy Texture Pack Id", "firmament.config.power-user.copy-texture-pack-id.description": "Copy the texture pack id that is used for the item stack under your cursor.", + "firmament.config.power-user.copy-title": "Copy Inventory Title", + "firmament.config.power-user.copy-title.description": "Copies Inventory and Screen Titles", "firmament.config.power-user.entity-data": "Show Entity Data", "firmament.config.power-user.entity-data.description": "Print out information about the entity under your cross-hair.", "firmament.config.power-user.show-item-id": "Show SkyBlock Ids", @@ -242,11 +262,15 @@ "firmament.config.slot-locking.lock-uuid": "Lock UUID (Lock Item)", "firmament.config.slot-locking.lock-uuid.description": "Lock a SkyBlock item by it's UUID. This blocks a specific item from being dropped/sold, but still allows moving it around.", "firmament.config.slot-locking.lock.description": "Lock a slot, preventing any item from being moved from, dropped from, or placed into this slot.", + "firmament.config.slot-locking.multi-bind": "Allow Multi Binding", + "firmament.config.slot-locking.multi-bind.description": "Allow binding the same hotbar slot to multiple inventory slots.", "firmament.config.slot-locking.require-quick-move": "Require Shift-Click for Bound Slots", "firmament.config.slot-locking.require-quick-move.description": "If turned off, allows to switch between bound slots without holding shift.", "firmament.config.storage-overlay": "Storage Overlay", "firmament.config.storage-overlay.always-replace": "Always Open Overlay", "firmament.config.storage-overlay.always-replace.description": "Always replace the ender chest with Firmament's storage overlay.", + "firmament.config.storage-overlay.block-item-scrolling": "Block Scrolling on Items", + "firmament.config.storage-overlay.block-item-scrolling.description": "Disables scrolling the storage overlay screen while you are hovering over an item. Useful if you have a tooltip scrolling mod.", "firmament.config.storage-overlay.height": "Storage Height", "firmament.config.storage-overlay.height.description": "The height of the scrollable storage panel.", "firmament.config.storage-overlay.inverse-scroll": "Invert Scroll", @@ -260,6 +284,8 @@ "firmament.config.storage-overlay.scroll-speed": "Scroll Speed", "firmament.config.storage-overlay.scroll-speed.description": "Scroll speed inside of the storage overlay and overview.", "firmament.config.waypoints": "Waypoints", + "firmament.config.waypoints.reset-order-on-swap": "Reset Ordered Waypoints On Hop", + "firmament.config.waypoints.reset-order-on-swap.description": "Resets Ordered Waypoint progress after swapping to another world.", "firmament.config.waypoints.show-index": "Show ordered waypoint indexes", "firmament.config.waypoints.show-index.description": "Show the number of an ordered waypoint in the world.", "firmament.config.waypoints.skip-to-nearest": "Allow skipping waypoints", @@ -287,7 +313,6 @@ "firmament.inventory-buttons.save-preset": "Save Preset", "firmament.key.category": "Firmament", "firmament.keybinding.external": "%s", - "firmament.mixins.start": "Applied firmament mixins:", "firmament.modapi.event": "Received mod API event: %s", "firmament.poweruser.entity.armor": "Entity Armor:", "firmament.poweruser.entity.armor.item": " - %s", @@ -306,7 +331,6 @@ "firmament.price.lowestbin": "Lowest BIN: %s", "firmament.pristine-profit.collection": "Collection: %s/h", "firmament.pristine-profit.money": "Money: %s/h", - "firmament.protectitem": "Firmament protected your item: ", "firmament.pv.lookingup": "Looking up %s", "firmament.pv.noplayer": "%s is not a Minecraft player", "firmament.pv.noprofile": "%s has no SkyBlock profiles", @@ -338,8 +362,6 @@ "firmament.recipe.mobs.name": "§8[§7Lv %d§8] §c%s", "firmament.recipe.mobs.name.nolevel": "§c%s", "firmament.recipe.novanilla": "Hypixel cannot super craft vanilla recipes", - "firmament.recipecategory.reforge": "Reforge", - "firmament.recipecategory.reforge.basic": "This is a basic reforge, available at the Blacksmith.", "firmament.reiwarning": "Firmament needs RoughlyEnoughItems to display its item list!", "firmament.reiwarning.disable": "Click here to disable this warning", "firmament.reiwarning.disabled": "Disabled the RoughlyEnoughItems warning. Keep in mind that you will not have an item list without REI.", @@ -355,9 +377,6 @@ "firmament.sbinfo.server": "Locraw Server: %s", "firmament.toggle.false": "Off", "firmament.toggle.true": "On", - "firmament.tooltip.ah.lowestbin": "Lowest BIN: %d", - "firmament.tooltip.bazaar.buy-order": "Bazaar Buy Order: %s", - "firmament.tooltip.bazaar.sell-order": "Bazaar Sell Order: %s", "firmament.tooltip.copied.lore": "Copied Name and Lore", "firmament.tooltip.copied.modelid": "Copied Texture Id: %s", "firmament.tooltip.copied.modelid.fail": "Failed to copy Texture Id", diff --git a/translations/extra.json b/translations/extra.json new file mode 100644 index 0000000..cb21fc9 --- /dev/null +++ b/translations/extra.json @@ -0,0 +1,6 @@ +{ + // These are require by jade, but i don't think they are actually rendered in game. + // Jade throws exceptions if they are not present however. + "config.jade.plugin_firmament.toolprovider": "Firmament Tool Provider", + "config.jade.plugin_firmament.custom_mining_hardness": "Firmament Mining Hardness" +} diff --git a/web/src/pages/docs/_texture-pack-format.md b/web/src/pages/docs/_texture-pack-format.md index da66043..182c413 100644 --- a/web/src/pages/docs/_texture-pack-format.md +++ b/web/src/pages/docs/_texture-pack-format.md @@ -139,11 +139,31 @@ Filter by item type: "firmament:item": "minecraft:clock" ``` +#### Skulls + +You can match skulls using the skull textures and other properties using the skull predicate. If there are no properties specified this is equivalent to checking if the item is a `minecraft:player_head`. + +```json +"firmament:skull": { + "profileId": "cca2d452-c6d3-39cb-b695-5ec92b2d6729", + "textureProfileId": "1d5233d388624bafb00e3150a7aa3a89", + "skinUrl": "http://textures.minecraft.net/texture/7bf01c198f6e16965e230235cd22a5a9f4a40e40941234478948ff9a56e51775", + "textureValue": "ewogICJ0aW1lc3RhbXAiIDogMTYxODUyMTY2MzY1NCwKICAicHJvZmlsZUlkIiA6ICIxZDUyMzNkMzg4NjI0YmFmYjAwZTMxNTBhN2FhM2E4OSIsCiAgInByb2ZpbGVOYW1lIiA6ICIwMDAwMDAwMDAwMDAwMDBKIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdiZjAxYzE5OGY2ZTE2OTY1ZTIzMDIzNWNkMjJhNWE5ZjRhNDBlNDA5NDEyMzQ0Nzg5NDhmZjlhNTZlNTE3NzUiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ" +} +``` + +| Name | Type | Description | +|--------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `profileId` | UUID | Match the uuid of the profile component directly. | +| `textureProfileId` | UUID | Match the uuid of the skin owner in the encoded texture value. This is more expensive, but can deviate from the profile id of the profile owner. | +| `skinUrl` | [string](#string-matcher) | Match the texture url of the skin. This starts with `http://`, not with `https:/` in most cases. | +| `textureValue` | [string](#string-matcher) | Match the texture value. This is the encoded base64 string of the texture url along with metadata. It is faster to query than the `skinUrl`, but it can out of changed without causing any semantic changes, and is less readable than the skinUrl. | + #### Extra attributes Filter by extra attribute NBT data: -Specify a `path` to look at, separating sub elements with a `.`. You can use a `*` to check any child. +Specify a `path` (using an [nbt prism](#nbt-prism)) to look at, separating sub elements with a `.`. You can use a `*` to check any child. Then either specify a `match` sub-object or directly inline that object in the format of an [nbt matcher](#nbt-matcher). @@ -167,6 +187,32 @@ Sub object match: } ``` +#### Components + +You can match generic components similarly to [extra attributes](#extra-attributes). If you want to match an extra +attribute match directly using that, for better performance. + +You can specify a `path` (using an [nbt prism](#nbt-prism)) and match similar to extra attributes, but in addition you can also specify a `component`. This +variable is the identifier of a component type that will then be encoded to nbt and matched according to the `match` +using a [nbt matcher](#nbt-matcher). + +```json5 +"firmament:component": { + "path": "rgb", + "component": "minecraft:dyed_color", + "int": 255 +} +// Alternatively +"firmament:component": { + "path": "rgb", + "component": "minecraft:dyed_color", + "match": { + "int": 255 + } +} +``` + + #### Pet Data Filter by pet information. While you can already filter by the skyblock id for pet type and tier, this allows you to @@ -310,6 +356,58 @@ compare your number: This example would match if the level is less than fifty. The available operators are `<`, `>`, `<=` and `>=`. The operator needs to be specified on the left. The versions of the operator with `=` also allow the number to be equal. +### Nbt Prism + +An nbt prism (or path) is used to specify where in a complex nbt construct to look for a value. A basic prism just looks +like a dot-separated path (`parent.child.grandchild`), but more complex paths can be constructed. + +First the specified path is split into dot separated chunks: `"a.b.c"` -> `["a", "b", "c"]`. You can also directly +specify the list if you would like. Any entry in that list not starting with a `*` is treated as an attribute name or +an index: + +```json +{ + "propA": { + "propB": { + "propC": 100, + "propD": 1000 + } + }, + "someOtherProp": "hello", + "someThirdProp": "{\"innerProp\": true}", + "someFourthProp": "aGlkZGVuIHZhbHVl" +} +``` + +In this example json (which is supposed to represent a corresponding nbt object), you can use a path like +`propA.propB.propC` to directly extract the value `100`. + +If you want to extract all of the innermost values of `propB` +(for example if `propB` was an array instead), you could use `propA.propB.*`. You can use the `*` at any level: +`*.*.*` for example extracts all properties that are exactly at the third level. In that case you would try to match any +of the values of `[100, 1000]` to your match object. + +Sometimes values are encoded in a non-nbt format inside a string. For those you can use other star based directives like +`*base64` or `*json` to decode those entries. + +`*base64` turns a base64 encoded string into the base64 decoded counterpart. `*json` decodes a string into the json +object represented by that string. Note that json to nbt conversion isn't always straightforwards and the types can +end up being mangled (for example what could have been a byte ends up an int). + +| Path | Result | +|---------------------------------|---------------------------------| +| `propA.propB` | `{"propC": 100, "propD": 1000}` | +| `propA.propB.propC` | `100` | +| `propA.*.propC` | `100` | +| `propA.propB.*` | `100`, `1000` | +| `someOtherProp` | `"hello"` | +| `someThirdProp` | "{\"innerProp\": true}" | +| `someThirdProp.*json` | {"innerProp": true} | +| `someThirdProp.*json.innerProp` | true | +| `someFourthProp` | `"aGlkZGVuIHZhbHVl"` | +| `someFourthProp.*base64` | `"hidden value"` | + + ### Nbt Matcher This matches a single nbt element. |