Make pickaxe ability display use AbilityUtils

[no changelog]
This commit is contained in:
Linnea Gräf
2024-10-13 17:32:10 +02:00
parent daa63bd914
commit e6142bb936
24 changed files with 1433 additions and 681 deletions

View File

@@ -41,3 +41,9 @@ SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"
path = ["**/META-INF/services/*"]
SPDX-License-Identifier = "CC0-1.0"
SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"]
[[annotations]]
path = ["src/test/resources/testdata/**/*.snbt"]
SPDX-License-Identifier = "CC-BY-4.0"
SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"]

View File

@@ -7,6 +7,7 @@
*/
import com.google.devtools.ksp.gradle.KspTaskJvm
import com.google.gson.JsonArray
import moe.nea.licenseextractificator.LicenseDiscoveryTask
import net.fabricmc.loom.LoomGradleExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
@@ -255,17 +256,12 @@ dependencies {
ksp(project(":symbols"))
}
tasks.test {
useJUnitPlatform()
}
loom {
clientOnlyMinecraftJar()
accessWidenerPath.set(project.file("src/main/resources/firmament.accesswidener"))
runs {
removeIf { it.name != "client" }
named("client") {
property("devauth.enabled", "true")
configureEach {
property("fabric.log.level", "info")
property("firmament.debug", "true")
property("firmament.classroots",
@@ -280,6 +276,9 @@ loom {
parseEnvFile(file(".properties")).forEach { (t, u) ->
property(t, u)
}
}
named("client") {
property("devauth.enabled", "true")
vmArg("-ea")
vmArg("-XX:+AllowEnhancedClassRedefinition")
vmArg("-XX:HotswapAgent=external")
@@ -288,6 +287,11 @@ loom {
}
}
tasks.test {
useJUnitPlatform()
}
tasks.withType<JavaCompile> {
this.sourceCompatibility = "21"
this.targetCompatibility = "21"

View File

@@ -1,5 +1,3 @@
package moe.nea.firmament
import com.mojang.brigadier.CommandDispatcher
@@ -33,7 +31,6 @@ import kotlinx.coroutines.plus
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlin.coroutines.EmptyCoroutineContext
import net.minecraft.client.render.chunk.SectionBuilder
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.util.Identifier
import moe.nea.firmament.commands.registerFirmamentCommand

View File

@@ -4,10 +4,14 @@ package moe.nea.firmament.events
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.render.RenderTickCounter
import net.minecraft.world.GameMode
import moe.nea.firmament.util.MC
/**
* Called when hud elements should be rendered, before the screen, but after the world.
*/
data class HudRenderEvent(val context: DrawContext, val tickDelta: RenderTickCounter) : FirmamentEvent() {
val isRenderingHud = !MC.options.hudHidden
val isRenderingCursor = MC.interactionManager?.currentGameMode != GameMode.SPECTATOR && isRenderingHud
companion object : FirmamentEventBus<HudRenderEvent>()
}

View File

@@ -28,6 +28,7 @@ import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.focusedItemStack
import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.skyBlockId
@@ -44,6 +45,7 @@ object PowerUserTools : FirmamentFeature {
val copyLoreData by keyBindingWithDefaultUnbound("copy-lore")
val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture")
val copyEntityData by keyBindingWithDefaultUnbound("entity-data")
val copyItemStack by keyBindingWithDefaultUnbound("copy-item-stack")
}
override val config
@@ -125,7 +127,7 @@ object PowerUserTools : FirmamentFeature {
Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.modelid", model.toString()))
} else if (it.matches(TConfig.copyNbtData)) {
// TODO: copy full nbt
val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toString() ?: "<empty>"
val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toPrettyString() ?: "<empty>"
ClipboardUtils.setTextContent(nbt)
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.nbt"))
} else if (it.matches(TConfig.copyLoreData)) {
@@ -157,6 +159,9 @@ object PowerUserTools : FirmamentFeature {
Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString())
)
println("Copied skull id: $skullTexture")
} else if (it.matches(TConfig.copyItemStack)) {
ClipboardUtils.setTextContent(item.encode(MC.currentOrDefaultRegistries).toPrettyString())
lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack"))
}
}

View File

@@ -1,4 +1,3 @@
package moe.nea.firmament.features.mining
import java.util.regex.Pattern
@@ -30,6 +29,7 @@ import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.parseTimePattern
import moe.nea.firmament.util.render.RenderCircleProgress
import moe.nea.firmament.util.render.lerp
import moe.nea.firmament.util.skyblock.AbilityUtils
import moe.nea.firmament.util.toShedaniel
import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch
@@ -76,7 +76,7 @@ object PickaxeAbility : FirmamentFeature {
@Subscribe
fun onSlotClick(it: SlotClickEvent) {
if (MC.screen?.title?.unformattedString == "Heart of the Mountain") {
val name = it.stack.displayNameAccordingToNbt?.unformattedString ?: return
val name = it.stack.displayNameAccordingToNbt.unformattedString
val cooldown = it.stack.loreAccordingToNbt.firstNotNullOfOrNull {
cooldownPattern.useMatch(it.unformattedString) {
parseTimePattern(group("cooldown"))
@@ -132,6 +132,7 @@ object PickaxeAbility : FirmamentFeature {
val abilityUsePattern = Pattern.compile("You used your (?<name>.*) Pickaxe Ability!")
val fuelPattern = Pattern.compile("Fuel: .*/(?<maxFuel>$SHORT_NUMBER_FORMAT)")
val pickaxeAbilityCooldownPattern = Pattern.compile("Your pickaxe ability is on cooldown for (?<remainingCooldown>$TIME_PATTERN)\\.")
data class PickaxeAbilityData(
val name: String,
@@ -142,29 +143,18 @@ object PickaxeAbility : FirmamentFeature {
val lore = itemStack.loreAccordingToNbt
if (!lore.any { it.unformattedString.contains("Breaking Power") })
return null
val cooldown = lore.firstNotNullOfOrNull {
cooldownPattern.useMatch(it.unformattedString) {
parseTimePattern(group("cooldown"))
val ability = AbilityUtils.getAbilities(itemStack).firstOrNull() ?: return null
return PickaxeAbilityData(ability.name, ability.cooldown ?: return null)
}
} ?: return null
val name = lore.firstNotNullOfOrNull {
abilityPattern.useMatch(it.unformattedString) {
group("name")
}
} ?: return null
return PickaxeAbilityData(name, cooldown)
}
val cooldownPattern = Pattern.compile("Cooldown: (?<cooldown>$TIME_PATTERN)")
val abilityPattern = Pattern.compile("(⦾ )?Ability: (?<name>.*) {2}RIGHT CLICK")
val abilitySwitchPattern =
Pattern.compile("You selected (?<ability>.*) as your Pickaxe Ability\\. This ability will apply to all of your pickaxes!")
@Subscribe
fun renderHud(event: HudRenderEvent) {
if (!TConfig.cooldownEnabled) return
if (!event.isRenderingCursor) return
var ability = getCooldownFromLore(MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return) ?: return
defaultAbilityDurations[ability.name] = ability.cooldown
val ao = abilityOverride

View File

@@ -1,4 +1,3 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
@@ -67,16 +66,16 @@ abstract class NumberMatcher {
abstract fun matches(comparisonResult: Int): Boolean
}
private val operatorPattern = "(?<operator>${Operator.entries.joinToString("|") {it.operator}})(?<value>[0-9.]+)".toPattern()
private val operatorPattern =
"(?<operator>${Operator.entries.joinToString("|") { it.operator }})(?<value>[0-9.]+)".toPattern()
fun parseOperator(string: String): OperatorMatcher? {
operatorPattern.useMatch<Nothing>(string) {
return operatorPattern.useMatch(string) {
val operatorName = group("operator")
val operator = Operator.entries.find { it.operator == operatorName }!!
val value = group("value").toDouble()
return OperatorMatcher(operator, value)
OperatorMatcher(operator, value)
}
return null
}
data class OperatorMatcher(val operator: Operator, val value: Double) : NumberMatcher() {

View File

@@ -0,0 +1,16 @@
package moe.nea.firmament.util
import moe.nea.firmament.Firmament
object ErrorUtil {
var aggressiveErrors = run {
Thread.currentThread().stackTrace.any { it.className.startsWith("org.junit.") } || Firmament.DEBUG
}
@Suppress("NOTHING_TO_INLINE") // Suppressed since i want the logger to not pick up the ErrorUtil stack-frame
inline fun softError(message: String) {
if (aggressiveErrors) error(message)
else Firmament.logger.error(message)
}
}

View File

@@ -4,7 +4,9 @@ import io.github.moulberry.repo.data.Coordinate
import java.util.concurrent.ConcurrentLinkedQueue
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.client.option.GameOptions
import net.minecraft.client.render.WorldRenderer
import net.minecraft.item.Item
import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket
import net.minecraft.registry.BuiltinRegistries
import net.minecraft.registry.RegistryKeys
@@ -71,7 +73,9 @@ object MC {
inline val networkHandler get() = player?.networkHandler
inline val instance get() = MinecraftClient.getInstance()
inline val keyboard get() = instance.keyboard
inline val interactionManager get() = instance.interactionManager
inline val textureManager get() = instance.textureManager
inline val options get() = instance.options
inline val inGameHud get() = instance.inGameHud
inline val font get() = instance.textRenderer
inline val soundManager get() = instance.soundManager
@@ -86,7 +90,8 @@ object MC {
inline val window get() = instance.window
inline val currentRegistries: RegistryWrapper.WrapperLookup? get() = world?.registryManager
val defaultRegistries: RegistryWrapper.WrapperLookup = BuiltinRegistries.createWrapperLookup()
val defaultItems = defaultRegistries.getWrapperOrThrow(RegistryKeys.ITEM)
inline val currentOrDefaultRegistries get() = currentRegistries ?: defaultRegistries
val defaultItems: RegistryWrapper.Impl<Item> = defaultRegistries.getWrapperOrThrow(RegistryKeys.ITEM)
}

View File

@@ -1,12 +1,16 @@
package moe.nea.firmament.util
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
class TimeMark private constructor(private val timeMark: Long) : Comparable<TimeMark> {
fun passedTime() = if (timeMark == 0L) Duration.INFINITE else (System.currentTimeMillis() - timeMark).milliseconds
fun passedTime() =
if (timeMark == 0L) Duration.INFINITE
else (System.currentTimeMillis() - timeMark).milliseconds
fun passedAt(fakeNow: TimeMark) =
if (timeMark == 0L) Duration.INFINITE
else (fakeNow.timeMark - timeMark).milliseconds
operator fun minus(other: TimeMark): Duration {
if (other.timeMark == timeMark)
@@ -38,6 +42,10 @@ class TimeMark private constructor(private val timeMark: Long) : Comparable<Time
return other is TimeMark && other.timeMark == timeMark
}
override fun toString(): String {
return "https://time.is/$timeMark"
}
override fun compareTo(other: TimeMark): Int {
return this.timeMark.compareTo(other.timeMark)
}

View File

@@ -0,0 +1,138 @@
package moe.nea.firmament.util.mc
import net.minecraft.nbt.NbtByte
import net.minecraft.nbt.NbtByteArray
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtDouble
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtEnd
import net.minecraft.nbt.NbtFloat
import net.minecraft.nbt.NbtInt
import net.minecraft.nbt.NbtIntArray
import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtLong
import net.minecraft.nbt.NbtLongArray
import net.minecraft.nbt.NbtShort
import net.minecraft.nbt.NbtString
import net.minecraft.nbt.visitor.NbtElementVisitor
class SNbtFormatter private constructor() : NbtElementVisitor {
private val result = StringBuilder()
private var indent = 0
private fun writeIndent() {
result.append("\t".repeat(indent))
}
private fun pushIndent() {
indent++
}
private fun popIndent() {
indent--
}
fun apply(element: NbtElement): StringBuilder {
element.accept(this)
return result
}
override fun visitString(element: NbtString) {
result.append(NbtString.escape(element.asString()))
}
override fun visitByte(element: NbtByte) {
result.append(element.numberValue()).append("b")
}
override fun visitShort(element: NbtShort) {
result.append(element.shortValue()).append("s")
}
override fun visitInt(element: NbtInt) {
result.append(element.intValue())
}
override fun visitLong(element: NbtLong) {
result.append(element.longValue()).append("L")
}
override fun visitFloat(element: NbtFloat) {
result.append(element.floatValue()).append("f")
}
override fun visitDouble(element: NbtDouble) {
result.append(element.doubleValue()).append("d")
}
private fun visitArrayContents(array: List<NbtElement>) {
array.forEachIndexed { index, element ->
writeIndent()
element.accept(this)
if (array.size != index + 1) {
result.append(",")
}
result.append("\n")
}
}
private fun writeArray(arrayTypeTag: String, array: List<NbtElement>) {
result.append("[").append(arrayTypeTag).append("\n")
pushIndent()
visitArrayContents(array)
popIndent()
writeIndent()
result.append("]")
}
override fun visitByteArray(element: NbtByteArray) {
writeArray("B;", element)
}
override fun visitIntArray(element: NbtIntArray) {
writeArray("I;", element)
}
override fun visitLongArray(element: NbtLongArray) {
writeArray("L;", element)
}
override fun visitList(element: NbtList) {
writeArray("", element)
}
override fun visitCompound(compound: NbtCompound) {
result.append("{\n")
pushIndent()
val keys = compound.keys.sorted()
keys.forEachIndexed { index, key ->
writeIndent()
val element = compound[key] ?: error("Key '$key' found but not present in compound: $compound")
val escapedName = if (key.matches(SIMPLE_NAME)) key else NbtString.escape(key)
result.append(escapedName).append(": ")
element.accept(this)
if (keys.size != index + 1) {
result.append(",")
}
result.append("\n")
}
popIndent()
writeIndent()
result.append("}")
}
override fun visitEnd(element: NbtEnd) {
result.append("END")
}
companion object {
fun prettify(nbt: NbtElement): String {
return SNbtFormatter().apply(nbt).toString()
}
fun NbtElement.toPrettyString() = prettify(this)
private val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex()
}
}

View File

@@ -1,8 +1,14 @@
@file:OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class)
package moe.nea.firmament.util
import java.util.regex.Matcher
import java.util.regex.Pattern
import org.intellij.lang.annotations.Language
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.experimental.ExperimentalTypeInference
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
@@ -10,10 +16,14 @@ import kotlin.time.Duration.Companion.seconds
inline fun <T> String.ifMatches(regex: Regex, block: (MatchResult) -> T): T? =
regex.matchEntire(this)?.let(block)
inline fun <T> Pattern.useMatch(string: String, block: Matcher.() -> T): T? =
matcher(string)
inline fun <T> Pattern.useMatch(string: String, block: Matcher.() -> T): T? {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return matcher(string)
.takeIf(Matcher::matches)
?.let(block)
}
@Language("RegExp")
val TIME_PATTERN = "[0-9]+[ms]"

View File

@@ -0,0 +1,138 @@
package moe.nea.firmament.util.skyblock
import kotlin.time.Duration
import net.minecraft.item.ItemStack
import net.minecraft.text.Text
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.directLiteralStringContent
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.parseTimePattern
import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch
object AbilityUtils {
data class ItemAbility(
val name: String,
val hasPowerScroll: Boolean,
val activation: AbilityActivation,
val manaCost: Int?,
val descriptionLines: List<Text>,
val cooldown: Duration?,
)
@JvmInline
value class AbilityActivation(
val label: String
) {
companion object {
val RIGHT_CLICK = AbilityActivation("RIGHT CLICK")
val SNEAK_RIGHT_CLICK = AbilityActivation("SNEAK RIGHT CLICK")
val SNEAK = AbilityActivation("SNEAK")
val EMPTY = AbilityActivation("")
fun of(text: String?): AbilityActivation {
val trimmed = text?.trim()
if (trimmed.isNullOrBlank())
return EMPTY
return AbilityActivation(trimmed)
}
}
}
private val abilityNameRegex = "Ability: (?<name>.*?) *".toPattern()
private fun findAbility(iterator: ListIterator<Text>): ItemAbility? {
if (!iterator.hasNext()) {
return null
}
val line = iterator.next()
// The actual information about abilities is stored in the siblings
if (line.directLiteralStringContent != "") return null
var powerScroll: Boolean = false // This should instead determine the power scroll based on text colour
var abilityName: String? = null
var activation: String? = null
var hasProcessedActivation = false
for (sibling in line.siblings) {
val directContent = sibling.directLiteralStringContent ?: continue
if (directContent == "") {
powerScroll = true
continue
}
if (!hasProcessedActivation && abilityName != null) {
hasProcessedActivation = true
activation = directContent
continue
}
abilityNameRegex.useMatch<Nothing>(directContent) {
abilityName = group("name")
continue
}
if (abilityName != null) {
ErrorUtil.softError("Found abilityName $abilityName without finding but encountered unprocessable element in: $line")
}
return null
}
if (abilityName == null) return null
val descriptionLines = mutableListOf<Text>()
var manaCost: Int? = null
var cooldown: Duration? = null
while (iterator.hasNext()) {
val descriptionLine = iterator.next()
if (descriptionLine.unformattedString == "") break
var nextIsManaCost = false
var isSpecialLine = false
var nextIsDuration = false
for (sibling in descriptionLine.siblings) {
val directContent = sibling.directLiteralStringContent ?: continue
if ("Mana Cost: " == directContent) { // TODO: 'Soulflow Cost: ' support (or maybe a generic 'XXX Cost: ')
nextIsManaCost = true
isSpecialLine = true
continue
}
if ("Cooldown: " == directContent) {
nextIsDuration = true
isSpecialLine = true
continue
}
if (nextIsDuration) {
nextIsDuration = false
cooldown = parseTimePattern(directContent)
continue
}
if (nextIsManaCost) {
nextIsManaCost = false
manaCost = parseShortNumber(directContent).toInt()
continue
}
if (isSpecialLine) {
ErrorUtil.softError("Unknown special line segment: '$sibling' in '$descriptionLine'")
}
}
if (!isSpecialLine) {
descriptionLines.add(descriptionLine)
}
}
return ItemAbility(
abilityName,
powerScroll,
AbilityActivation.of(activation),
manaCost,
descriptionLines,
cooldown
)
}
fun getAbilities(lore: List<Text>): List<ItemAbility> {
val iterator = lore.listIterator()
val abilities = mutableListOf<ItemAbility>()
while (iterator.hasNext()) {
findAbility(iterator)?.let(abilities::add)
}
return abilities
}
fun getAbilities(itemStack: ItemStack): List<ItemAbility> {
return getAbilities(itemStack.loreAccordingToNbt)
}
}

View File

@@ -90,6 +90,8 @@ fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String {
val Text.unformattedString: String
get() = string.removeColorCodes()
val Text.directLiteralStringContent: String? get() = (this.content as? PlainTextContent)?.string()
fun Text.allSiblings(): List<Text> = listOf(this) + siblings.flatMap { it.allSiblings() }
fun MutableText.withColor(formatting: Formatting) = this.styled { it.withColor(formatting).withItalic(false) }

View File

@@ -182,8 +182,9 @@
"firmament.config.power-user.copy-texture-pack-id": "Copy Texture Pack Id",
"firmament.config.power-user.copy-skull-texture": "Copy Placed Skull Id",
"firmament.config.power-user.entity-data": "Show Entity Data",
"firmament.config.power-user.copy-nbt-data": "Copy NBT data",
"firmament.config.power-user.copy-nbt-data": "Copy ExtraAttributes data",
"firmament.config.power-user.copy-lore": "Copy Name + Lore",
"firmament.config.power-user.copy-item-stack": "Copy ItemStack",
"firmament.config.power-user": "Power Users",
"firmament.tooltip.skyblockid": "SkyBlock Id: %s",
"firmament.tooltip.copied.skyblockid.fail": "Failed to copy SkyBlock Id",
@@ -194,6 +195,7 @@
"firmament.tooltip.copied.skull.fail": "Failed to copy skull id.",
"firmament.tooltip.copied.nbt": "Copied NBT data",
"firmament.tooltip.copied.lore": "Copied Name and Lore",
"firmament.tooltip.copied.stack": "Copied ItemStack",
"firmament.config.compatibility": "Intermod Features",
"firmament.config.compatibility.explosion-enabled": "Redirect Enhanced Explosions",
"firmament.config.compatibility.explosion-power": "Enhanced Explosion Power",

View File

@@ -22,3 +22,8 @@ mutable field net/minecraft/screen/slot/Slot y I
accessible field net/minecraft/entity/player/PlayerEntity PLAYER_MODEL_PARTS Lnet/minecraft/entity/data/TrackedData;
accessible field net/minecraft/client/render/WorldRenderer chunks Lnet/minecraft/client/render/BuiltChunkStorage;
# Fix package-private access methods
accessible method net/minecraft/registry/entry/RegistryEntry$Reference setRegistryKey (Lnet/minecraft/registry/RegistryKey;)V
accessible method net/minecraft/entity/LivingEntity getHitbox ()Lnet/minecraft/util/math/Box;
accessible method net/minecraft/registry/entry/RegistryEntryList$Named <init> (Lnet/minecraft/registry/entry/RegistryEntryOwner;Lnet/minecraft/registry/tag/TagKey;)V
accessible method net/minecraft/registry/entry/RegistryEntry$Reference setValue (Ljava/lang/Object;)V

29
src/test/kotlin/root.kt Normal file
View File

@@ -0,0 +1,29 @@
package moe.nea.firmament.test
import net.minecraft.Bootstrap
import net.minecraft.SharedConstants
import moe.nea.firmament.util.TimeMark
object FirmTestBootstrap {
val loadStart = TimeMark.now()
init {
println("Bootstrap started at $loadStart")
}
init {
SharedConstants.createGameVersion()
Bootstrap.initialize()
}
val loadEnd = TimeMark.now()
val loadDuration = loadStart.passedAt(loadEnd)
init {
println("Bootstrap completed at $loadEnd after $loadDuration")
}
fun bootstrapMinecraft() {
}
}

View File

@@ -0,0 +1,30 @@
package moe.nea.firmament.test.testutil
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.StringNbtReader
import moe.nea.firmament.test.FirmTestBootstrap
object ItemResources {
init {
FirmTestBootstrap.bootstrapMinecraft()
}
fun loadString(path: String): String {
require(!path.startsWith("/"))
return ItemResources::class.java.classLoader
.getResourceAsStream(path)!!
.readAllBytes().decodeToString()
}
fun loadSNbt(path: String): NbtCompound {
return StringNbtReader.parse(loadString(path))
}
fun loadItem(name: String): ItemStack {
// TODO: make the load work with enchantments
return ItemStack.CODEC.parse(NbtOps.INSTANCE, loadSNbt("testdata/items/$name.snbt"))
.getOrThrow { IllegalStateException("Could not load test item '$name': $it") }
}
}

View File

@@ -1,12 +1,13 @@
package moe.nea.firmament.test
package moe.nea.firmament.test.util
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import net.minecraft.Bootstrap
import net.minecraft.SharedConstants
import moe.nea.firmament.util.removeColorCodes
class ColorCode {
class ColorCodeTest {
@Test
fun testWhatever() {
Assertions.assertEquals("", "".removeColorCodes().toString())

View File

@@ -0,0 +1,79 @@
package moe.nea.firmament.test.util.skyblock
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import net.minecraft.text.Text
import moe.nea.firmament.test.testutil.ItemResources
import moe.nea.firmament.util.skyblock.AbilityUtils
import moe.nea.firmament.util.unformattedString
class AbilityUtilsTest {
fun List<AbilityUtils.ItemAbility>.stripDescriptions() = map {
it.copy(descriptionLines = it.descriptionLines.map { Text.literal(it.unformattedString) })
}
@Test
fun testUnpoweredDrill() {
Assertions.assertEquals(
listOf(
AbilityUtils.ItemAbility(
"Pickobulus",
false,
AbilityUtils.AbilityActivation.RIGHT_CLICK,
null,
listOf("Throw your pickaxe to create an",
"explosion mining all ores in a 3 block",
"radius.").map(Text::literal),
48.seconds
)
),
AbilityUtils.getAbilities(ItemResources.loadItem("titanium-drill")).stripDescriptions()
)
}
@Test
fun testPoweredPickaxe() {
Assertions.assertEquals(
listOf(
AbilityUtils.ItemAbility(
"Mining Speed Boost",
true,
AbilityUtils.AbilityActivation.RIGHT_CLICK,
null,
listOf("Grants +200% ⸕ Mining Speed for",
"10s.").map(Text::literal),
2.minutes
)
),
AbilityUtils.getAbilities(ItemResources.loadItem("diamond-pickaxe")).stripDescriptions()
)
}
@Test
fun testAOTV() {
Assertions.assertEquals(
listOf(
AbilityUtils.ItemAbility(
"Instant Transmission", true, AbilityUtils.AbilityActivation.RIGHT_CLICK, 23,
listOf("Teleport 12 blocks ahead of you and",
"gain +50 ✦ Speed for 3 seconds.").map(Text::literal),
null
),
AbilityUtils.ItemAbility(
"Ether Transmission",
false,
AbilityUtils.AbilityActivation.SNEAK_RIGHT_CLICK,
90,
listOf("Teleport to your targeted block up",
"to 61 blocks away.",
"Soulflow Cost: 1").map(Text::literal),
null
)
),
AbilityUtils.getAbilities(ItemResources.loadItem("aspect-of-the-void")).stripDescriptions()
)
}
}

View File

@@ -0,0 +1,59 @@
{
components: {
"minecraft:attribute_modifiers": {
modifiers: [
],
show_in_tooltip: 0b
},
"minecraft:custom_data": {
donated_museum: 1b,
enchantments: {
ultimate_wise: 5
},
ethermerge: 1b,
gems: {
},
id: "ASPECT_OF_THE_VOID",
modifier: "heroic",
originTag: "ASPECT_OF_THE_VOID",
power_ability_scroll: "SAPPHIRE_POWER_SCROLL",
timestamp: 1641640380000L,
tuned_transmission: 4,
uuid: "b0572534-eb14-46cd-90c6-0df878fd56a2"
},
"minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Heroic Aspect of the Void"}],"italic":false,"text":""}',
"minecraft:enchantment_glint_override": 1b,
"minecraft:hide_additional_tooltip": {
},
"minecraft:lore": [
'{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+120"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+132 "},{"color":"blue","text":"(+32)"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Bonus Attack Speed: "},{"color":"red","text":"+3% "},{"color":"blue","text":"(+3%)"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+80 "},{"color":"blue","text":"(+80)"}],"italic":false,"text":""}',
'{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"gray","text":"✎"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"blue","text":""},{"bold":true,"color":"light_purple","text":"Ultimate Wise V"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Reduces the ability mana cost of this"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"item by "},{"color":"green","text":"50%"},{"color":"gray","text":"."}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"bold":true,"color":"aqua","text":"⦾ "},{"color":"gold","text":"Ability: Instant Transmission "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Teleport "},{"color":"green","text":"12 blocks"},{"color":"gray","text":" ahead of you and"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"gain "},{"color":"green","text":"+50 "},{"color":"white","text":"✦ Speed"},{"color":"gray","text":" for "},{"color":"green","text":"3 seconds"},{"color":"gray","text":"."}],"italic":false,"text":""}',
'{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"23"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"gold","text":"Ability: Ether Transmission "},{"bold":true,"color":"yellow","text":"SNEAK RIGHT CLICK"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Teleport to your targeted block up"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"to "},{"color":"green","text":"61 blocks "},{"color":"gray","text":"away."}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"Soulflow Cost: "},{"color":"dark_aqua","text":"1"}],"italic":false,"text":""}',
'{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"90"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}',
'{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC SWORD"}],"italic":false,"text":""}'
],
"minecraft:unbreakable": {
show_in_tooltip: 0b
}
},
count: 1,
id: "minecraft:diamond_shovel"
}

View File

@@ -0,0 +1,48 @@
{
components: {
"minecraft:attribute_modifiers": {
modifiers: [
],
show_in_tooltip: 0b
},
"minecraft:custom_data": {
enchantments: {
efficiency: 10
},
id: "DIAMOND_PICKAXE",
power_ability_scroll: "SAPPHIRE_POWER_SCROLL",
timestamp: 1659795180000L,
uuid: "d213f48e-d927-4748-a58c-eb80735025b7"
},
"minecraft:custom_name": '{"extra":[{"color":"green","text":"Diamond Pickaxe"}],"italic":false,"text":""}',
"minecraft:enchantments": {
levels: {
}
},
"minecraft:hide_additional_tooltip": {
},
"minecraft:lore": [
'{"extra":[{"color":"dark_gray","text":"Breaking Power 4"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+30"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Mining Speed: "},{"color":"green","text":"+220"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"blue","text":"Efficiency X"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Increases how quickly your tool"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"breaks blocks."}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"bold":true,"color":"aqua","text":"⦾ "},{"color":"gold","text":"Ability: Mining Speed Boost "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+200% "},{"color":"gold","text":"⸕ Mining Speed "},{"color":"gray","text":"for"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":""},{"color":"green","text":"10s"},{"color":"gray","text":"."}],"italic":false,"text":""}',
'{"extra":[{"color":"dark_gray","text":"Cooldown: "},{"color":"green","text":"120s"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}',
'{"extra":[{"bold":true,"color":"green","text":"UNCOMMON PICKAXE"}],"italic":false,"text":""}'
],
"minecraft:unbreakable": {
show_in_tooltip: 0b
}
},
count: 1,
id: "minecraft:diamond_pickaxe"
}

View File

@@ -0,0 +1,97 @@
{
components: {
"minecraft:attribute_modifiers": {
modifiers: [
],
show_in_tooltip: 0b
},
"minecraft:custom_data": {
compact_blocks: 1023815,
donated_museum: 1b,
drill_fuel: 16621,
drill_part_fuel_tank: "titanium_fuel_tank",
drill_part_upgrade_module: "goblin_omelette_blue_cheese",
enchantments: {
compact: 10,
efficiency: 5,
experience: 3,
fortune: 3,
paleontologist: 2,
pristine: 5
},
gems: {
AMBER_0: {
quality: "PERFECT",
uuid: "d28be6ae-75eb-49e4-90d8-31759db18d79"
},
JADE_0: {
quality: "PERFECT",
uuid: "657fea0b-88e2-483d-9d2c-0b821797a55a"
},
MINING_0: {
quality: "PERFECT",
uuid: "257bdcd2-585b-48b9-9517-a2e841dc0574"
},
MINING_0_gem: "TOPAZ",
unlocked_slots: [
"JADE_0",
"MINING_0"
]
},
id: "TITANIUM_DRILL_4",
modifier: "auspicious",
rarity_upgrades: 1,
timestamp: 1700577120000L,
uuid: "367b85ab-5bb4-43b6-a055-084cbaaafc1c"
},
"minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Auspicious Titanium Drill DR-X655"}],"italic":false,"text":""}',
"minecraft:enchantment_glint_override": 1b,
"minecraft:hide_additional_tooltip": {
},
"minecraft:lore": [
'{"extra":[{"color":"dark_gray","text":"Breaking Power 9"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+75"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Mining Speed: "},{"color":"green","text":"+1,885 "},{"color":"blue","text":"(+75) "},{"color":"light_purple","text":"(+100)"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Pristine: "},{"color":"green","text":"+4.5 "},{"color":"light_purple","text":"(+2)"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Mining Fortune: "},{"color":"green","text":"+220 "},{"color":"blue","text":"(+20) "},{"color":"light_purple","text":"(+50)"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Mining Wisdom: "},{"color":"green","text":"+10"}],"italic":false,"text":""}',
'{"extra":[" ",{"color":"gold","text":"["},{"color":"gold","text":"⸕"},{"color":"gold","text":"] "},{"color":"gold","text":"["},{"color":"green","text":"☘"},{"color":"gold","text":"] "},{"color":"gold","text":"["},{"color":"yellow","text":"✦"},{"color":"gold","text":"]"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"blue","text":"Compact X"}],"italic":false,"text":""}',
'{"extra":[{"color":"blue","text":"Efficiency V"}],"italic":false,"text":""}',
'{"extra":[{"color":"blue","text":"Experience III"}],"italic":false,"text":""}',
'{"extra":[{"color":"blue","text":"Fortune III"}],"italic":false,"text":""}',
'{"extra":[{"color":"blue","text":"Paleontologist II"}],"italic":false,"text":""}',
'{"extra":[{"color":"blue","text":"Prismatic V"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":""},{"color":"green","text":"Titanium-Infused Fuel Tank."}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":""},{"color":"gray","text":""},{"color":"dark_green","text":"25,000 Max Fuel Capacity."}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":""},{"color":"gray","text":""},{"color":"green","text":"-4% Pickaxe Ability Cooldown."}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Drill Engine: "},{"color":"red","text":"Not Installed"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases "},{"color":"gold","text":"⸕ Mining Speed "},{"color":"gray","text":"with part"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"installed."}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":""},{"color":"green","text":"Blue Cheese Goblin Omelette Part."}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Adds "},{"color":"green","text":"+1 Level "},{"color":"gray","text":"to all of your unlocked "},{"color":"dark_purple","text":"Heart of"}],"italic":false,"text":""}',
'{"extra":[{"color":"dark_purple","text":"the Mountain "},{"color":"gray","text":"perks."}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Fuel: "},{"color":"dark_green","text":"16,621"},{"color":"dark_gray","text":"/25k"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"gold","text":"Ability: Pickobulus "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Throw your pickaxe to create an"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"explosion mining all ores in a "},{"color":"green","text":"3 "},{"color":"gray","text":"block"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"radius."}],"italic":false,"text":""}',
'{"extra":[{"color":"dark_gray","text":"Cooldown: "},{"color":"green","text":"48s"}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"color":"blue","text":"Auspicious Bonus"}],"italic":false,"text":""}',
'{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+0.9% "},{"color":"gold","text":"☘ Mining Fortune"},{"color":"gray","text":"."}],"italic":false,"text":""}',
'{"italic":false,"text":""}',
'{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}',
'{"extra":[{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"bold":true,"color":"light_purple","text":"MYTHIC DRILL "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}'
]
},
count: 1,
id: "minecraft:prismarine_shard"
}

View File

@@ -0,0 +1,80 @@
package moe.nea.firmament.annotations.process
import com.google.auto.service.AutoService
import com.google.devtools.ksp.containingFile
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFile
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import java.util.TreeSet
class GameTestContainingClassProcessor(
val logger: KSPLogger,
val codeGenerator: CodeGenerator,
val sourceSetName: String,
) : SymbolProcessor {
@AutoService(SymbolProcessorProvider::class)
class Provider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return GameTestContainingClassProcessor(
environment.logger,
environment.codeGenerator,
environment.options["firmament.sourceset"] ?: "main")
}
}
val allClasses: MutableSet<String> = TreeSet()
val allSources = mutableSetOf<KSFile>()
override fun process(resolver: Resolver): List<KSAnnotated> {
val annotated = resolver.getSymbolsWithAnnotation("net.minecraft.test.GameTest").toList()
annotated.forEach {
val containingClass = it.parent as KSClassDeclaration
allClasses.add(containingClass.qualifiedName!!.asString())
allSources.add(it.containingFile!!)
}
return emptyList()
}
fun createJson(): JsonObject {
return JsonObject().apply {
addProperty("schemaVersion", 1)
addProperty("id", "firmament-gametest")
addProperty("name", "Firmament Gametest")
addProperty("version", "1.0.0")
addProperty("environment", "*")
add("entrypoints", JsonObject().apply {
add("fabric-gametest", JsonArray().apply {
allClasses.forEach {
add(it)
}
})
})
}
}
override fun finish() {
if (allClasses.isEmpty()) return
val stream = codeGenerator.createNewFile(Dependencies(aggregating = true, *allSources.toTypedArray()),
"",
"fabric.mod",
"json")
val output = OutputStreamWriter(stream, StandardCharsets.UTF_8)
Gson().toJson(createJson(), output)
output.close()
}
}