diff options
Diffstat (limited to 'src/main/kotlin/util')
32 files changed, 979 insertions, 203 deletions
diff --git a/src/main/kotlin/util/BazaarPriceStrategy.kt b/src/main/kotlin/util/BazaarPriceStrategy.kt index 002eedb..13b6d95 100644 --- a/src/main/kotlin/util/BazaarPriceStrategy.kt +++ b/src/main/kotlin/util/BazaarPriceStrategy.kt @@ -9,7 +9,7 @@ enum class BazaarPriceStrategy { NPC_SELL; fun getSellPrice(skyblockId: SkyblockId): Double { - val bazaarEntry = HypixelStaticData.bazaarData[skyblockId] ?: return 0.0 + val bazaarEntry = HypixelStaticData.bazaarData[skyblockId.asBazaarStock] ?: return 0.0 return when (this) { BUY_ORDER -> bazaarEntry.quickStatus.sellPrice SELL_ORDER -> bazaarEntry.quickStatus.buyPrice diff --git a/src/main/kotlin/util/ChromaColourUtil.kt b/src/main/kotlin/util/ChromaColourUtil.kt new file mode 100644 index 0000000..0130326 --- /dev/null +++ b/src/main/kotlin/util/ChromaColourUtil.kt @@ -0,0 +1,10 @@ +package moe.nea.firmament.util + +import io.github.notenoughupdates.moulconfig.ChromaColour +import java.awt.Color + +fun ChromaColour.getRGBAWithoutAnimation() = + Color(ChromaColour.specialToSimpleRGB(toLegacyString()), true) + +fun Color.toChromaWithoutAnimation(timeForFullRotationInMillis: Int = 0) = + ChromaColour.fromRGB(red, green, blue, timeForFullRotationInMillis, alpha) diff --git a/src/main/kotlin/util/ErrorUtil.kt b/src/main/kotlin/util/ErrorUtil.kt index 190381d..3db4ecd 100644 --- a/src/main/kotlin/util/ErrorUtil.kt +++ b/src/main/kotlin/util/ErrorUtil.kt @@ -29,15 +29,31 @@ object ErrorUtil { inline fun softError(message: String, exception: Throwable) { if (aggressiveErrors) throw IllegalStateException(message, exception) - else Firmament.logger.error(message, exception) + else logError(message, exception) + } + + fun logError(message: String, exception: Throwable) { + Firmament.logger.error(message, exception) + } + fun logError(message: String) { + Firmament.logger.error(message) } inline fun softError(message: String) { if (aggressiveErrors) error(message) - else Firmament.logger.error(message) + else logError(message) + } + + fun <T> Result<T>.intoCatch(message: String): Catch<T> { + return this.map { Catch.succeed(it) }.getOrElse { + softError(message, it) + Catch.fail(it) + } } 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 +89,9 @@ object ErrorUtil { return nullable } + fun softUserError(string: String) { + if (TestUtil.isInTest) + error(string) + MC.sendChat(tr("firmament.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 a660f51..03dafc5 100644 --- a/src/main/kotlin/util/FirmFormatters.kt +++ b/src/main/kotlin/util/FirmFormatters.kt @@ -135,4 +135,8 @@ object FirmFormatters { 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/HoveredItemStack.kt b/src/main/kotlin/util/HoveredItemStack.kt index a2e4ad2..526820a 100644 --- a/src/main/kotlin/util/HoveredItemStack.kt +++ b/src/main/kotlin/util/HoveredItemStack.kt @@ -24,4 +24,4 @@ class VanillaScreenProvider : HoveredItemStackProvider { val HandledScreen<*>.focusedItemStack: ItemStack? get() = HoveredItemStackProvider.allValidInstances - .firstNotNullOfOrNull { it.provideHoveredItemStack(this) } + .firstNotNullOfOrNull { it.provideHoveredItemStack(this)?.takeIf { !it.isEmpty } } diff --git a/src/main/kotlin/util/IntUtil.kt b/src/main/kotlin/util/IntUtil.kt new file mode 100644 index 0000000..2695906 --- /dev/null +++ b/src/main/kotlin/util/IntUtil.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.util + +object IntUtil { + data class RGBA(val r: Int, val g: Int, val b: Int, val a: Int) + + fun Int.toRGBA(): RGBA { + return RGBA( + r = (this shr 16) and 0xFF, g = (this shr 8) and 0xFF, b = this and 0xFF, a = (this shr 24) and 0xFF + ) + } + +} diff --git a/src/main/kotlin/util/LegacyTagWriter.kt b/src/main/kotlin/util/LegacyTagWriter.kt new file mode 100644 index 0000000..9889b2c --- /dev/null +++ b/src/main/kotlin/util/LegacyTagWriter.kt @@ -0,0 +1,103 @@ +package moe.nea.firmament.util + +import kotlinx.serialization.json.JsonPrimitive +import net.minecraft.nbt.AbstractNbtList +import net.minecraft.nbt.NbtByte +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.NbtLong +import net.minecraft.nbt.NbtShort +import net.minecraft.nbt.NbtString +import moe.nea.firmament.util.mc.SNbtFormatter.Companion.SIMPLE_NAME + +class LegacyTagWriter(val compact: Boolean) { + companion object { + fun stringify(nbt: NbtElement, compact: Boolean): String { + return LegacyTagWriter(compact).also { it.writeElement(nbt) } + .stringWriter.toString() + } + + fun NbtElement.toLegacyString(pretty: Boolean = false): String { + return stringify(this, !pretty) + } + } + + val stringWriter = StringBuilder() + var indent = 0 + fun newLine() { + if (compact) return + stringWriter.append('\n') + repeat(indent) { + stringWriter.append(" ") + } + } + + fun writeElement(nbt: NbtElement) { + when (nbt) { + is NbtInt -> stringWriter.append(nbt.value.toString()) + is NbtString -> stringWriter.append(escapeString(nbt.value)) + is NbtFloat -> stringWriter.append(nbt.value).append('F') + is NbtDouble -> stringWriter.append(nbt.value).append('D') + is NbtByte -> stringWriter.append(nbt.value).append('B') + is NbtLong -> stringWriter.append(nbt.value).append('L') + is NbtShort -> stringWriter.append(nbt.value).append('S') + is NbtCompound -> writeCompound(nbt) + is NbtEnd -> {} + is AbstractNbtList -> writeArray(nbt) + } + } + + fun writeArray(nbt: AbstractNbtList) { + stringWriter.append('[') + indent++ + newLine() + nbt.forEachIndexed { index, element -> + writeName(index.toString()) + writeElement(element) + if (index != nbt.size() - 1) { + stringWriter.append(',') + newLine() + } + } + indent-- + if (nbt.size() != 0) + newLine() + stringWriter.append(']') + } + + fun writeCompound(nbt: NbtCompound) { + stringWriter.append('{') + indent++ + newLine() + val entries = nbt.entrySet().sortedBy { it.key } + entries.forEachIndexed { index, it -> + writeName(it.key) + writeElement(it.value) + if (index != entries.lastIndex) { + stringWriter.append(',') + newLine() + } + } + indent-- + if (nbt.size != 0) + newLine() + stringWriter.append('}') + } + + fun escapeString(string: String): String { + return JsonPrimitive(string).toString() + } + + fun escapeName(key: String): String = + if (key.matches(SIMPLE_NAME)) key else escapeString(key) + + fun writeName(key: String) { + stringWriter.append(escapeName(key)) + stringWriter.append(':') + if (!compact) stringWriter.append(' ') + } +} diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt index a31d181..e85b119 100644 --- a/src/main/kotlin/util/MC.kt +++ b/src/main/kotlin/util/MC.kt @@ -1,6 +1,7 @@ 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 @@ -21,10 +22,10 @@ import net.minecraft.registry.Registry import net.minecraft.registry.RegistryKey import net.minecraft.registry.RegistryKeys import net.minecraft.registry.RegistryWrapper -import net.minecraft.registry.entry.RegistryEntry 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 @@ -126,6 +127,12 @@ object MC { } 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)) diff --git a/src/main/kotlin/util/MoulConfigUtils.kt b/src/main/kotlin/util/MoulConfigUtils.kt index 362a4d9..51ff340 100644 --- a/src/main/kotlin/util/MoulConfigUtils.kt +++ b/src/main/kotlin/util/MoulConfigUtils.kt @@ -35,6 +35,21 @@ 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) { @@ -81,9 +96,11 @@ object MoulConfigUtils { override fun createInstance(context: XMLContext<*>, element: Element): FirmHoverComponent { return FirmHoverComponent( context.getChildFragment(element), - context.getPropertyFromAttribute(element, - QName("lines"), - List::class.java) as Supplier<List<String>>, + context.getPropertyFromAttribute( + element, + QName("lines"), + List::class.java + ) as Supplier<List<String>>, context.getPropertyFromAttribute(element, QName("delay"), Duration::class.java, 0.6.seconds), ) } @@ -179,10 +196,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 +211,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,29 +225,21 @@ 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)) { + fun wrapScreen(guiContext: GuiContext, parent: Screen?, onClose: () -> Unit = {}): Screen { + return object : GuiComponentWrapper(guiContext) { override fun close() { if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) { client!!.setScreen(parent) + onClose() } } } } + fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen { + return wrapScreen(loadGui(name, bindTo), parent) + } + // TODO: move this utility into moulconfig (also rework guicontext into an interface so i can make this mesh better into vanilla) fun GuiContext.adopt(element: GuiComponent) = element.foldRecursive(Unit, { comp, unit -> comp.context = this }) @@ -288,12 +295,14 @@ object MoulConfigUtils { assert(drawContext?.isUntranslatedGuiDrawContext() != false) val context = drawContext?.let(::ModernRenderContext) ?: IMinecraft.instance.provideTopLevelRenderContext() - val immContext = GuiImmediateContext(context, - 0, 0, 0, 0, - mouseX, mouseY, - mouseX, mouseY, - mouseX.toFloat(), - mouseY.toFloat()) + val immContext = GuiImmediateContext( + context, + 0, 0, 0, 0, + mouseX, mouseY, + mouseX, mouseY, + mouseX.toFloat(), + mouseY.toFloat() + ) return immContext } diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt index 897c6be..b4d583a 100644 --- a/src/main/kotlin/util/SkyblockId.kt +++ b/src/main/kotlin/util/SkyblockId.kt @@ -6,6 +6,11 @@ import com.mojang.serialization.Codec import io.github.moulberry.repo.data.NEUIngredient import io.github.moulberry.repo.data.NEUItem import io.github.moulberry.repo.data.Rarity +import java.time.Instant +import java.time.LocalDateTime +import java.time.format.DateTimeFormatterBuilder +import java.time.format.SignStyle +import java.time.temporal.ChronoField import java.util.Optional import java.util.UUID import kotlinx.serialization.Serializable @@ -21,28 +26,32 @@ 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.ExpensiveItemCacheApi import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.set import moe.nea.firmament.util.collections.WeakCache 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) : Comparable<SkyblockId> { val identifier - get() = Identifier.of("skyblockitem", - neuItem.lowercase().replace(";", "__") - .replace(":", "___") - .replace(illlegalPathRegex) { - it.value.toCharArray() - .joinToString("") { "__" + it.code.toString(16).padStart(4, '0') } - }) + get() = Identifier.of( + "skyblockitem", + neuItem.lowercase().replace(";", "__") + .replace(":", "___") + .replace(illlegalPathRegex) { + it.value.toCharArray() + .joinToString("") { "__" + it.code.toString(16).padStart(4, '0') } + }) override fun toString(): String { return neuItem @@ -53,7 +62,7 @@ value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> { } /** - * 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. @@ -61,11 +70,10 @@ value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> { @JvmInline @Serializable value class BazaarStock(val bazaarId: String) { - fun toRepoId(): SkyblockId { - bazaarEnchantmentRegex.matchEntire(bazaarId)?.let { - return SkyblockId("${it.groupValues[1]};${it.groupValues[2]}") + companion object { + fun fromSkyBlockId(skyblockId: SkyblockId): BazaarStock { + return BazaarStock(RepoManager.neuRepo.constants.bazaarStocks.getBazaarStockOrDefault(skyblockId.neuItem)) } - return SkyblockId(bazaarId.replace(":", "-")) } } @@ -84,7 +92,9 @@ value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> { val NEUItem.skyblockId get() = SkyblockId(skyblockItemId) val NEUIngredient.skyblockId get() = SkyblockId(itemId) +val SkyblockId.asBazaarStock get() = SkyblockId.BazaarStock.fromSkyBlockId(this) +@ExpensiveItemCacheApi fun NEUItem.guessRecipeId(): String? { if (!skyblockItemId.contains(";")) return skyblockItemId val item = this.asItemStack() @@ -103,9 +113,11 @@ data class HypixelPetInfo( val exp: Double = 0.0, val candyUsed: Int = 0, val uuid: UUID? = null, - val active: Boolean = false, + 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 } @@ -132,6 +144,30 @@ fun ItemStack.modifyExtraAttributes(block: (NbtCompound) -> Unit) { val ItemStack.skyblockUUIDString: String? get() = extraAttributes.getString("uuid").getOrNull()?.takeIf { it.isNotBlank() } +private val timestampFormat = //"10/11/21 3:39 PM" + DateTimeFormatterBuilder().apply { + appendValue(ChronoField.MONTH_OF_YEAR, 2) + appendLiteral("/") + appendValue(ChronoField.DAY_OF_MONTH, 2) + appendLiteral("/") + appendValueReduced(ChronoField.YEAR, 2, 2, 1950) + appendLiteral(" ") + appendValue(ChronoField.HOUR_OF_AMPM, 1, 2, SignStyle.NEVER) + appendLiteral(":") + appendValue(ChronoField.MINUTE_OF_HOUR, 2) + appendLiteral(" ") + appendText(ChronoField.AMPM_OF_DAY) + }.toFormatter() +val ItemStack.timestamp + get() = + extraAttributes.getLong("timestamp").getOrNull()?.let { Instant.ofEpochMilli(it) } + ?: extraAttributes.getString("timestamp").getOrNull()?.let { + ErrorUtil.catch("Could not parse timestamp $it") { + LocalDateTime.from(timestampFormat.parse(it)).atZone(SBData.hypixelTimeZone) + .toInstant() + }.orNull() + } + val ItemStack.skyblockUUID: UUID? get() = skyblockUUIDString?.let { UUID.fromString(it) } @@ -199,9 +235,49 @@ val ItemStack.skyBlockId: SkyblockId? else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName).getOrNull()}") } - // TODO: PARTY_HAT_CRAB{,_ANIMATED,_SLOTH},POTION + "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", "BALLOON_HAT_2025" -> { + val partyHatYear = extraAttributes.getInt("party_hat_year").getOrNull() + val partyHatColor = extraAttributes.getString("party_hat_color").getOrNull() + SkyblockId("BALLOON_HAT_${partyHatYear}_${partyHatColor?.uppercase()}") + } + else -> { - SkyblockId(id) + SkyblockId(id.replace(":", "-")) } } } diff --git a/src/main/kotlin/util/StringUtil.kt b/src/main/kotlin/util/StringUtil.kt index 68e161a..dc98dc0 100644 --- a/src/main/kotlin/util/StringUtil.kt +++ b/src/main/kotlin/util/StringUtil.kt @@ -9,6 +9,8 @@ object StringUtil { return string.replace(",", "").toInt() } + fun String.title() = replaceFirstChar { it.titlecase() } + fun Iterable<String>.unwords() = joinToString(" ") fun nextLexicographicStringOfSameLength(string: String): String { val next = StringBuilder(string) 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/async/input.kt b/src/main/kotlin/util/async/input.kt index f22c595..2c546ba 100644 --- a/src/main/kotlin/util/async/input.kt +++ b/src/main/kotlin/util/async/input.kt @@ -1,47 +1,89 @@ - - package moe.nea.firmament.util.async +import io.github.notenoughupdates.moulconfig.gui.GuiContext +import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent +import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent +import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume +import net.minecraft.client.gui.screen.Screen import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.gui.FirmButtonComponent import moe.nea.firmament.keybindings.IKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil private object InputHandler { - data class KeyInputContinuation(val keybind: IKeyBinding, val onContinue: () -> Unit) - - private val activeContinuations = mutableListOf<KeyInputContinuation>() - - fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit { - synchronized(InputHandler) { - activeContinuations.add(keyInputContinuation) - } - return { - synchronized(this) { - activeContinuations.remove(keyInputContinuation) - } - } - } - - init { - HandledScreenKeyPressedEvent.subscribe("Input:resumeAfterInput") { event -> - synchronized(InputHandler) { - val toRemove = activeContinuations.filter { - event.matches(it.keybind) - } - toRemove.forEach { it.onContinue() } - activeContinuations.removeAll(toRemove) - } - } - } + data class KeyInputContinuation(val keybind: IKeyBinding, val onContinue: () -> Unit) + + private val activeContinuations = mutableListOf<KeyInputContinuation>() + + fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit { + synchronized(InputHandler) { + activeContinuations.add(keyInputContinuation) + } + return { + synchronized(this) { + activeContinuations.remove(keyInputContinuation) + } + } + } + + init { + HandledScreenKeyPressedEvent.subscribe("Input:resumeAfterInput") { event -> + synchronized(InputHandler) { + val toRemove = activeContinuations.filter { + event.matches(it.keybind) + } + toRemove.forEach { it.onContinue() } + activeContinuations.removeAll(toRemove) + } + } + } } suspend fun waitForInput(keybind: IKeyBinding): Unit = suspendCancellableCoroutine { cont -> - val unregister = - InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) }) - cont.invokeOnCancellation { - unregister() - } + val unregister = + InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) }) + cont.invokeOnCancellation { + unregister() + } } +fun createPromptScreenGuiComponent(suggestion: String, prompt: String, action: Runnable) = (run { + val text = GetSetter.floating(suggestion) + GuiContext( + CenterComponent( + PanelComponent( + ColumnComponent( + TextFieldComponent(text, 120), + FirmButtonComponent(TextComponent(prompt), action = action) + ) + ) + ) + ) to text +}) + +suspend fun waitForTextInput(suggestion: String, prompt: String) = + suspendCancellableCoroutine<String> { cont -> + lateinit var screen: Screen + lateinit var text: GetSetter<String> + val action = { + if (MC.screen === screen) + MC.screen = null + // TODO: should this exit + cont.resume(text.get()) + } + val (gui, text_) = createPromptScreenGuiComponent(suggestion, prompt, action) + text = text_ + screen = MoulConfigUtils.wrapScreen(gui, null, onClose = action) + ScreenUtil.setScreenLater(screen) + cont.invokeOnCancellation { + action() + } + } diff --git a/src/main/kotlin/util/collections/RangeUtil.kt b/src/main/kotlin/util/collections/RangeUtil.kt new file mode 100644 index 0000000..a7029ac --- /dev/null +++ b/src/main/kotlin/util/collections/RangeUtil.kt @@ -0,0 +1,40 @@ +package moe.nea.firmament.util.collections + +import kotlin.math.floor + +val ClosedFloatingPointRange<Float>.centre get() = (endInclusive + start) / 2 + +fun ClosedFloatingPointRange<Float>.nonNegligibleSubSectionsAlignedWith( + interval: Float +): Iterable<Float> { + require(interval.isFinite()) + val range = this + return object : Iterable<Float> { + override fun iterator(): Iterator<Float> { + return object : FloatIterator() { + var polledValue: Float = range.start + var lastValue: Float = polledValue + + override fun nextFloat(): Float { + if (!hasNext()) throw NoSuchElementException() + lastValue = polledValue + polledValue = Float.NaN + return lastValue + } + + override fun hasNext(): Boolean { + if (!polledValue.isNaN()) { + return true + } + if (lastValue == range.endInclusive) + return false + polledValue = (floor(lastValue / interval) + 1) * interval + if (polledValue > range.endInclusive) { + polledValue = range.endInclusive + } + return true + } + } + } + } +} diff --git a/src/main/kotlin/util/json/KJsonUtils.kt b/src/main/kotlin/util/json/KJsonUtils.kt new file mode 100644 index 0000000..b15119b --- /dev/null +++ b/src/main/kotlin/util/json/KJsonUtils.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.util.json + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive + +fun <T : JsonElement> List<T>.asJsonArray(): JsonArray { + return JsonArray(this) +} + +fun Iterable<String>.toJsonArray(): JsonArray = map { JsonPrimitive(it) }.asJsonArray() 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/math/Projections.kt b/src/main/kotlin/util/math/Projections.kt new file mode 100644 index 0000000..359b21b --- /dev/null +++ b/src/main/kotlin/util/math/Projections.kt @@ -0,0 +1,46 @@ +package moe.nea.firmament.util.math + +import kotlin.math.absoluteValue +import kotlin.math.cos +import kotlin.math.sin +import net.minecraft.util.math.Vec2f +import moe.nea.firmament.util.render.wrapAngle + +object Projections { + object Two { + val ε = 1e-6 + val π = moe.nea.firmament.util.render.π + val τ = 2 * π + + fun isNullish(float: Float) = float.absoluteValue < ε + + fun xInterceptOfLine(origin: Vec2f, direction: Vec2f): Vec2f? { + if (isNullish(direction.x)) + return Vec2f(origin.x, 0F) + if (isNullish(direction.y)) + return null + + val slope = direction.y / direction.x + return Vec2f(origin.x - origin.y / slope, 0F) + } + + fun interceptAlongCardinal(distanceFromAxis: Float, slope: Float): Float? { + if (isNullish(slope)) + return null + return -distanceFromAxis / slope + } + + fun projectAngleOntoUnitBox(angleRadians: Double): Vec2f { + val angleRadians = wrapAngle(angleRadians) + val cx = cos(angleRadians) + val cy = sin(angleRadians) + + val ex = 1 / cx.absoluteValue + val ey = 1 / cy.absoluteValue + + val e = minOf(ex, ey) + + return Vec2f((cx * e).toFloat(), (cy * e).toFloat()) + } + } +} diff --git a/src/main/kotlin/util/mc/InitLevel.kt b/src/main/kotlin/util/mc/InitLevel.kt new file mode 100644 index 0000000..2c3eedb --- /dev/null +++ b/src/main/kotlin/util/mc/InitLevel.kt @@ -0,0 +1,25 @@ +package moe.nea.firmament.util.mc + +enum class InitLevel { + STARTING, + MC_INIT, + RENDER_INIT, + RENDER, + MAIN_MENU, + ; + + companion object { + var initLevel = InitLevel.STARTING + private set + + @JvmStatic + fun isAtLeast(wantedLevel: InitLevel): Boolean = initLevel >= wantedLevel + + @JvmStatic + fun bump(nextLevel: InitLevel) { + if (nextLevel.ordinal != initLevel.ordinal + 1) + error("Cannot bump initLevel $nextLevel from $initLevel") + initLevel = nextLevel + } + } +} diff --git a/src/main/kotlin/util/mc/MCTabListAPI.kt b/src/main/kotlin/util/mc/MCTabListAPI.kt new file mode 100644 index 0000000..66bdd55 --- /dev/null +++ b/src/main/kotlin/util/mc/MCTabListAPI.kt @@ -0,0 +1,96 @@ +package moe.nea.firmament.util.mc + +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import java.util.Optional +import org.jetbrains.annotations.TestOnly +import net.minecraft.client.gui.hud.PlayerListHud +import net.minecraft.nbt.NbtOps +import net.minecraft.scoreboard.Team +import net.minecraft.text.Text +import net.minecraft.text.TextCodecs +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.features.debug.ExportedTestConstantMeta +import moe.nea.firmament.mixins.accessor.AccessorPlayerListHud +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.intoOptional +import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString + +object MCTabListAPI { + + fun PlayerListHud.cast() = this as AccessorPlayerListHud + + @Subscribe + fun onTick(event: TickEvent) { + _currentTabList = null + } + + @Subscribe + fun devCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("copytablist") { + thenExecute { + currentTabList.body.forEach { + MC.sendChat(Text.literal(TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it).orThrow.toString())) + } + var compound = CurrentTabList.CODEC.encodeStart(NbtOps.INSTANCE, currentTabList).orThrow + compound = ExportedTestConstantMeta.SOURCE_CODEC.encode( + ExportedTestConstantMeta.current, + NbtOps.INSTANCE, + compound + ).orThrow + ClipboardUtils.setTextContent( + compound.toPrettyString() + ) + } + } + } + } + + @get:TestOnly + @set:TestOnly + var _currentTabList: CurrentTabList? = null + + val currentTabList get() = _currentTabList ?: getTabListNow().also { _currentTabList = it } + + data class CurrentTabList( + val header: Optional<Text>, + val footer: Optional<Text>, + val body: List<Text>, + ) { + companion object { + val CODEC: Codec<CurrentTabList> = RecordCodecBuilder.create { + it.group( + TextCodecs.CODEC.optionalFieldOf("header").forGetter(CurrentTabList::header), + TextCodecs.CODEC.optionalFieldOf("footer").forGetter(CurrentTabList::footer), + TextCodecs.CODEC.listOf().fieldOf("body").forGetter(CurrentTabList::body), + ).apply(it, ::CurrentTabList) + } + } + } + + private fun getTabListNow(): CurrentTabList { + // This is a precondition for PlayerListHud.collectEntries to be valid + MC.networkHandler ?: return CurrentTabList(Optional.empty(), Optional.empty(), emptyList()) + val hud = MC.inGameHud.playerListHud.cast() + val entries = hud.collectPlayerEntries_firmament() + .map { + it.displayName ?: run { + val team = it.scoreboardTeam + val name = it.profile.name + Team.decorateName(team, Text.literal(name)) + } + } + return CurrentTabList( + header = hud.header_firmament.intoOptional(), + footer = hud.footer_firmament.intoOptional(), + body = entries, + ) + } +} diff --git a/src/main/kotlin/util/mc/NbtUtil.kt b/src/main/kotlin/util/mc/NbtUtil.kt new file mode 100644 index 0000000..2cab1c7 --- /dev/null +++ b/src/main/kotlin/util/mc/NbtUtil.kt @@ -0,0 +1,10 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtList + +fun Iterable<NbtElement>.toNbtList() = NbtList().also { + for (element in this) { + it.add(element) + } +} diff --git a/src/main/kotlin/util/mc/SNbtFormatter.kt b/src/main/kotlin/util/mc/SNbtFormatter.kt index e2c24f6..7617d17 100644 --- a/src/main/kotlin/util/mc/SNbtFormatter.kt +++ b/src/main/kotlin/util/mc/SNbtFormatter.kt @@ -110,7 +110,7 @@ class SNbtFormatter private constructor() : NbtElementVisitor { 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) + val escapedName = escapeName(key) result.append(escapedName).append(": ") element.accept(this) if (keys.size != index + 1) { @@ -134,6 +134,9 @@ class SNbtFormatter private constructor() : NbtElementVisitor { fun NbtElement.toPrettyString() = prettify(this) - private val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex() + fun escapeName(key: String): String = + if (key.matches(SIMPLE_NAME)) key else NbtString.escape(key) + + val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex() } } diff --git a/src/main/kotlin/util/mc/SkullItemData.kt b/src/main/kotlin/util/mc/SkullItemData.kt index 0405b65..1b7dcba 100644 --- a/src/main/kotlin/util/mc/SkullItemData.kt +++ b/src/main/kotlin/util/mc/SkullItemData.kt @@ -10,7 +10,6 @@ import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers -import kotlinx.serialization.encodeToString import net.minecraft.component.DataComponentTypes import net.minecraft.component.type.ProfileComponent import net.minecraft.item.ItemStack @@ -51,7 +50,7 @@ fun ItemStack.setEncodedSkullOwner(uuid: UUID, encodedData: String) { this.set(DataComponentTypes.PROFILE, ProfileComponent(gameProfile)) } -val zeroUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1") +val arbitraryUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1") fun createSkullItem(uuid: UUID, url: String) = ItemStack(Items.PLAYER_HEAD) .also { it.setSkullOwner(uuid, url) } diff --git a/src/main/kotlin/util/mc/SlotUtils.kt b/src/main/kotlin/util/mc/SlotUtils.kt index 4709dcf..9eb4918 100644 --- a/src/main/kotlin/util/mc/SlotUtils.kt +++ b/src/main/kotlin/util/mc/SlotUtils.kt @@ -1,5 +1,6 @@ package moe.nea.firmament.util.mc +import org.lwjgl.glfw.GLFW import net.minecraft.screen.ScreenHandler import net.minecraft.screen.slot.Slot import net.minecraft.screen.slot.SlotActionType @@ -10,7 +11,7 @@ object SlotUtils { MC.interactionManager?.clickSlot( handler.syncId, this.id, - 2, + GLFW.GLFW_MOUSE_BUTTON_MIDDLE, SlotActionType.CLONE, MC.player ) @@ -20,14 +21,25 @@ object SlotUtils { MC.interactionManager?.clickSlot( handler.syncId, this.id, hotbarIndex, SlotActionType.SWAP, - MC.player) + MC.player + ) } fun Slot.clickRightMouseButton(handler: ScreenHandler) { MC.interactionManager?.clickSlot( handler.syncId, this.id, - 1, + GLFW.GLFW_MOUSE_BUTTON_RIGHT, + SlotActionType.PICKUP, + MC.player + ) + } + + fun Slot.clickLeftMouseButton(handler: ScreenHandler) { + MC.interactionManager?.clickSlot( + handler.syncId, + this.id, + GLFW.GLFW_MOUSE_BUTTON_LEFT, SlotActionType.PICKUP, MC.player ) diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt index f239810..be6bcfb 100644 --- a/src/main/kotlin/util/regex.kt +++ b/src/main/kotlin/util/regex.kt @@ -26,6 +26,13 @@ inline fun <T> Pattern.useMatch(string: String?, block: Matcher.() -> T): T? { ?.let(block) } +fun <T> String.ifDropLast(suffix: String, block: (String) -> T): T? { + if (endsWith(suffix)) { + return block(dropLast(suffix.length)) + } + return null +} + @Language("RegExp") val TIME_PATTERN = "[0-9]+[ms]" diff --git a/src/main/kotlin/util/render/CustomRenderLayers.kt b/src/main/kotlin/util/render/CustomRenderLayers.kt index 7f3cdec..f713a81 100644 --- a/src/main/kotlin/util/render/CustomRenderLayers.kt +++ b/src/main/kotlin/util/render/CustomRenderLayers.kt @@ -1,10 +1,12 @@ package util.render +import com.mojang.blaze3d.pipeline.BlendFunction 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.gl.UniformType import net.minecraft.client.render.RenderLayer import net.minecraft.client.render.RenderPhase import net.minecraft.client.render.VertexFormats @@ -37,24 +39,44 @@ object CustomRenderPipelines { .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withCull(false) .withDepthWrite(false) + .withBlend(BlendFunction.TRANSLUCENT) + .build() + + val CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS = + RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET) + .withVertexFormat(VertexFormats.POSITION_TEXTURE_COLOR, DrawMode.TRIANGLES) + .withLocation(Firmament.identifier("gui_textured_overlay_tris_circle")) + .withUniform("InnerCutoutRadius", UniformType.FLOAT) + .withFragmentShader(Firmament.identifier("circle_discard_color")) + .withBlend(BlendFunction.TRANSLUCENT) + .build() + val PARALLAX_CAPE_SHADER = + RenderPipeline.builder(RenderPipelines.ENTITY_SNIPPET) + .withLocation(Firmament.identifier("parallax_cape")) + .withFragmentShader(Firmament.identifier("cape/parallax")) + .withSampler("Sampler0") + .withSampler("Sampler1") + .withSampler("Sampler3") + .withUniform("Animation", UniformType.FLOAT) .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)) + 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", @@ -71,4 +93,13 @@ object CustomRenderLayers { .lightmap(RenderPhase.DISABLE_LIGHTMAP) .build(false) ) + + val TRANSLUCENT_CIRCLE_GUI = + RenderLayer.of( + "firmament_circle_gui", + RenderLayer.DEFAULT_BUFFER_SIZE, + CustomRenderPipelines.CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS, + RenderLayer.MultiPhaseParameters.builder() + .build(false) + ) } diff --git a/src/main/kotlin/util/render/DrawContextExt.kt b/src/main/kotlin/util/render/DrawContextExt.kt index fa92cd7..a833c86 100644 --- a/src/main/kotlin/util/render/DrawContextExt.kt +++ b/src/main/kotlin/util/render/DrawContextExt.kt @@ -1,18 +1,12 @@ 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.VertexFormats import net.minecraft.util.Identifier -import moe.nea.firmament.Firmament import moe.nea.firmament.util.MC fun DrawContext.isUntranslatedGuiDrawContext(): Boolean { @@ -64,9 +58,10 @@ 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(CustomRenderLayers.LINES) - buf.vertex(fromX.toFloat(), fromY.toFloat(), 0F).color(color.color) + val matrix = this.matrices.peek() + buf.vertex(matrix, 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) + buf.vertex(matrix, toX.toFloat(), toY.toFloat(), 0F).color(color.color) .normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F) } } diff --git a/src/main/kotlin/util/render/LerpUtils.kt b/src/main/kotlin/util/render/LerpUtils.kt index f2c2f25..63a13ec 100644 --- a/src/main/kotlin/util/render/LerpUtils.kt +++ b/src/main/kotlin/util/render/LerpUtils.kt @@ -1,33 +1,36 @@ - package moe.nea.firmament.util.render import me.shedaniel.math.Color -val pi = Math.PI -val tau = Math.PI * 2 -fun lerpAngle(a: Float, b: Float, progress: Float): Float { - // TODO: there is at least 10 mods to many in here lol - val shortestAngle = ((((b.mod(tau) - a.mod(tau)).mod(tau)) + tau + pi).mod(tau)) - pi - return ((a + (shortestAngle) * progress).mod(tau)).toFloat() +val π = Math.PI +val τ = Math.PI * 2 +fun lerpAngle(a: Float, b: Float, progress: Float): Float { + // TODO: there is at least 10 mods to many in here lol + val shortestAngle = ((((b.mod(τ) - a.mod(τ)).mod(τ)) + τ + π).mod(τ)) - π + return ((a + (shortestAngle) * progress).mod(τ)).toFloat() } +fun wrapAngle(angle: Float): Float = (angle.mod(τ) + τ).mod(τ).toFloat() +fun wrapAngle(angle: Double): Double = (angle.mod(τ) + τ).mod(τ) + fun lerp(a: Float, b: Float, progress: Float): Float { - return a + (b - a) * progress + return a + (b - a) * progress } + fun lerp(a: Int, b: Int, progress: Float): Int { - return (a + (b - a) * progress).toInt() + return (a + (b - a) * progress).toInt() } fun ilerp(a: Float, b: Float, value: Float): Float { - return (value - a) / (b - a) + return (value - a) / (b - a) } fun lerp(a: Color, b: Color, progress: Float): Color { - return Color.ofRGBA( - lerp(a.red, b.red, progress), - lerp(a.green, b.green, progress), - lerp(a.blue, b.blue, progress), - lerp(a.alpha, b.alpha, progress), - ) + return Color.ofRGBA( + lerp(a.red, b.red, progress), + lerp(a.green, b.green, progress), + lerp(a.blue, b.blue, progress), + lerp(a.alpha, b.alpha, progress), + ) } diff --git a/src/main/kotlin/util/render/RenderCircleProgress.kt b/src/main/kotlin/util/render/RenderCircleProgress.kt index d759033..81dde6f 100644 --- a/src/main/kotlin/util/render/RenderCircleProgress.kt +++ b/src/main/kotlin/util/render/RenderCircleProgress.kt @@ -1,85 +1,101 @@ package moe.nea.firmament.util.render +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.vertex.VertexFormat import io.github.notenoughupdates.moulconfig.platform.next +import java.util.OptionalInt 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.BufferBuilder +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.util.BufferAllocator import net.minecraft.util.Identifier +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.collections.nonNegligibleSubSectionsAlignedWith +import moe.nea.firmament.util.math.Projections object RenderCircleProgress { - fun renderCircle( + fun renderCircularSlice( drawContext: DrawContext, - texture: Identifier, - progress: Float, + layer: RenderLayer, u1: Float, u2: Float, v1: Float, v2: Float, + angleRadians: ClosedFloatingPointRange<Float>, + color: Int = -1, + innerCutoutRadius: Float = 0F ) { - drawContext.draw { - val bufferBuilder = it.getBuffer(CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture)) - val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix - - val corners = listOf( - Vector2f(0F, -1F), - Vector2f(1F, -1F), - Vector2f(1F, 0F), - Vector2f(1F, 1F), - Vector2f(0F, 1F), - Vector2f(-1F, 1F), - Vector2f(-1F, 0F), - Vector2f(-1F, -1F), - ) + drawContext.draw() + val sections = angleRadians.nonNegligibleSubSectionsAlignedWith((τ / 8f).toFloat()) + .zipWithNext().toList() + BufferAllocator(layer.vertexFormat.vertexSize * sections.size * 3).use { allocator -> - for (i in (0 until 8)) { - if (progress < i / 8F) { - break - } - val second = corners[(i + 1) % 8] - val first = corners[i] - if (progress <= (i + 1) / 8F) { - val internalProgress = 1 - (progress - i / 8F) * 8F - val angle = lerpAngle( - atan2(second.y, second.x), - atan2(first.y, first.x), - internalProgress - ) - if (angle < tau / 8 || angle >= tau * 7 / 8) { - second.set(1F, tan(angle)) - } else if (angle < tau * 3 / 8) { - second.set(1 / tan(angle), 1F) - } else if (angle < tau * 5 / 8) { - second.set(-1F, -tan(angle)) - } else { - second.set(-1 / tan(angle), -1F) - } - } + val bufferBuilder = BufferBuilder(allocator, VertexFormat.DrawMode.TRIANGLES, layer.vertexFormat) + val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix + for ((sectionStart, sectionEnd) in sections) { + val firstPoint = Projections.Two.projectAngleOntoUnitBox(sectionStart.toDouble()) + val secondPoint = Projections.Two.projectAngleOntoUnitBox(sectionEnd.toDouble()) fun ilerp(f: Float): Float = ilerp(-1f, 1f, f) bufferBuilder - .vertex(matrix, second.x, second.y, 0F) - .texture(lerp(u1, u2, ilerp(second.x)), lerp(v1, v2, ilerp(second.y))) - .color(-1) + .vertex(matrix, secondPoint.x, secondPoint.y, 0F) + .texture(lerp(u1, u2, ilerp(secondPoint.x)), lerp(v1, v2, ilerp(secondPoint.y))) + .color(color) .next() bufferBuilder - .vertex(matrix, first.x, first.y, 0F) - .texture(lerp(u1, u2, ilerp(first.x)), lerp(v1, v2, ilerp(first.y))) - .color(-1) + .vertex(matrix, firstPoint.x, firstPoint.y, 0F) + .texture(lerp(u1, u2, ilerp(firstPoint.x)), lerp(v1, v2, ilerp(firstPoint.y))) + .color(color) .next() bufferBuilder .vertex(matrix, 0F, 0F, 0F) .texture(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F))) - .color(-1) + .color(color) .next() } + + bufferBuilder.end().use { buffer -> + // TODO: write a better utility to pass uniforms :sob: ill even take a mixin at this point + if (innerCutoutRadius <= 0) { + layer.draw(buffer) + return + } + val vertexBuffer = layer.vertexFormat.uploadImmediateVertexBuffer(buffer.buffer) + val indexBufferConstructor = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.TRIANGLES) + val indexBuffer = indexBufferConstructor.getIndexBuffer(buffer.drawParameters.indexCount) + RenderSystem.getDevice().createCommandEncoder().createRenderPass( + MC.instance.framebuffer.colorAttachment, + OptionalInt.empty(), + ).use { renderPass -> + renderPass.setPipeline(layer.pipeline) + renderPass.setUniform("InnerCutoutRadius", innerCutoutRadius) + renderPass.setIndexBuffer(indexBuffer, indexBufferConstructor.indexType) + renderPass.setVertexBuffer(0, vertexBuffer) + renderPass.drawIndexed(0, buffer.drawParameters.indexCount) + } + } } } - + fun renderCircle( + drawContext: DrawContext, + texture: Identifier, + progress: Float, + u1: Float, + u2: Float, + v1: Float, + v2: Float, + ) { + renderCircularSlice( + drawContext, + CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture), + u1, u2, v1, v2, + (-τ / 4).toFloat()..(progress * τ - τ / 4).toFloat() + ) + } } diff --git a/src/main/kotlin/util/render/RenderInWorldContext.kt b/src/main/kotlin/util/render/RenderInWorldContext.kt index 98b10ca..4963920 100644 --- a/src/main/kotlin/util/render/RenderInWorldContext.kt +++ b/src/main/kotlin/util/render/RenderInWorldContext.kt @@ -20,6 +20,7 @@ import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Vec3d import moe.nea.firmament.events.WorldRenderLastEvent import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.IntUtil.toRGBA import moe.nea.firmament.util.MC @RenderContextDSL @@ -204,37 +205,39 @@ class RenderInWorldContext private constructor( } } - private fun buildCube(matrix: Matrix4f, buf: VertexConsumer, color: Int) { + private fun buildCube(matrix: Matrix4f, buf: VertexConsumer, colorInt: Int) { + val (r, g, b, a) = colorInt.toRGBA() + // Y- - buf.vertex(matrix, 0F, 0F, 0F).color(color) - buf.vertex(matrix, 0F, 0F, 1F).color(color) - buf.vertex(matrix, 1F, 0F, 1F).color(color) - buf.vertex(matrix, 1F, 0F, 0F).color(color) + buf.vertex(matrix, 0F, 0F, 0F).color(r, g, b, a) + buf.vertex(matrix, 0F, 0F, 1F).color(r, g, b, a) + buf.vertex(matrix, 1F, 0F, 1F).color(r, g, b, a) + buf.vertex(matrix, 1F, 0F, 0F).color(r, g, b, a) // Y+ - buf.vertex(matrix, 0F, 1F, 0F).color(color) - buf.vertex(matrix, 1F, 1F, 0F).color(color) - buf.vertex(matrix, 1F, 1F, 1F).color(color) - buf.vertex(matrix, 0F, 1F, 1F).color(color) + buf.vertex(matrix, 0F, 1F, 0F).color(r, g, b, a) + buf.vertex(matrix, 1F, 1F, 0F).color(r, g, b, a) + buf.vertex(matrix, 1F, 1F, 1F).color(r, g, b, a) + buf.vertex(matrix, 0F, 1F, 1F).color(r, g, b, a) // X- - buf.vertex(matrix, 0F, 0F, 0F).color(color) - buf.vertex(matrix, 0F, 0F, 1F).color(color) - buf.vertex(matrix, 0F, 1F, 1F).color(color) - buf.vertex(matrix, 0F, 1F, 0F).color(color) + buf.vertex(matrix, 0F, 0F, 0F).color(r, g, b, a) + buf.vertex(matrix, 0F, 0F, 1F).color(r, g, b, a) + buf.vertex(matrix, 0F, 1F, 1F).color(r, g, b, a) + buf.vertex(matrix, 0F, 1F, 0F).color(r, g, b, a) // X+ - buf.vertex(matrix, 1F, 0F, 0F).color(color) - buf.vertex(matrix, 1F, 1F, 0F).color(color) - buf.vertex(matrix, 1F, 1F, 1F).color(color) - buf.vertex(matrix, 1F, 0F, 1F).color(color) + buf.vertex(matrix, 1F, 0F, 0F).color(r, g, b, a) + buf.vertex(matrix, 1F, 1F, 0F).color(r, g, b, a) + buf.vertex(matrix, 1F, 1F, 1F).color(r, g, b, a) + buf.vertex(matrix, 1F, 0F, 1F).color(r, g, b, a) // Z- - buf.vertex(matrix, 0F, 0F, 0F).color(color) - buf.vertex(matrix, 1F, 0F, 0F).color(color) - buf.vertex(matrix, 1F, 1F, 0F).color(color) - buf.vertex(matrix, 0F, 1F, 0F).color(color) + buf.vertex(matrix, 0F, 0F, 0F).color(r, g, b, a) + buf.vertex(matrix, 1F, 0F, 0F).color(r, g, b, a) + buf.vertex(matrix, 1F, 1F, 0F).color(r, g, b, a) + buf.vertex(matrix, 0F, 1F, 0F).color(r, g, b, a) // Z+ - buf.vertex(matrix, 0F, 0F, 1F).color(color) - buf.vertex(matrix, 0F, 1F, 1F).color(color) - buf.vertex(matrix, 1F, 1F, 1F).color(color) - buf.vertex(matrix, 1F, 0F, 1F).color(color) + buf.vertex(matrix, 0F, 0F, 1F).color(r, g, b, a) + buf.vertex(matrix, 0F, 1F, 1F).color(r, g, b, a) + buf.vertex(matrix, 1F, 1F, 1F).color(r, g, b, a) + buf.vertex(matrix, 1F, 0F, 1F).color(r, g, b, a) } diff --git a/src/main/kotlin/util/skyblock/SkyBlockItems.kt b/src/main/kotlin/util/skyblock/SkyBlockItems.kt index ca2b17b..4f208dd 100644 --- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt +++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt @@ -3,6 +3,7 @@ package moe.nea.firmament.util.skyblock import moe.nea.firmament.util.SkyblockId object SkyBlockItems { + val COINS = SkyblockId("SKYBLOCK_COIN") val ROTTEN_FLESH = SkyblockId("ROTTEN_FLESH") val ENCHANTED_DIAMOND = SkyblockId("ENCHANTED_DIAMOND") val DIAMOND = SkyblockId("DIAMOND") @@ -13,4 +14,9 @@ object SkyBlockItems { val SLICE_OF_GREEN_VELVET_CAKE = SkyblockId("SLICE_OF_GREEN_VELVET_CAKE") val SLICE_OF_RED_VELVET_CAKE = SkyblockId("SLICE_OF_RED_VELVET_CAKE") val SLICE_OF_STRAWBERRY_SHORTCAKE = SkyblockId("SLICE_OF_STRAWBERRY_SHORTCAKE") + val ASPECT_OF_THE_VOID = SkyblockId("ASPECT_OF_THE_VOID") + val ASPECT_OF_THE_END = SkyblockId("ASPECT_OF_THE_END") + val BONE_BOOMERANG = SkyblockId("BONE_BOOMERANG") + val STARRED_BONE_BOOMERANG = SkyblockId("STARRED_BONE_BOOMERANG") + val TRIBAL_SPEAR = SkyblockId("TRIBAL_SPEAR") } diff --git a/src/main/kotlin/util/skyblock/TabListAPI.kt b/src/main/kotlin/util/skyblock/TabListAPI.kt new file mode 100644 index 0000000..6b937da --- /dev/null +++ b/src/main/kotlin/util/skyblock/TabListAPI.kt @@ -0,0 +1,41 @@ +package moe.nea.firmament.util.skyblock + +import org.intellij.lang.annotations.Language +import net.minecraft.text.Text +import moe.nea.firmament.util.StringUtil.title +import moe.nea.firmament.util.StringUtil.unwords +import moe.nea.firmament.util.mc.MCTabListAPI +import moe.nea.firmament.util.unformattedString + +object TabListAPI { + + fun getWidgetLines(widgetName: WidgetName, includeTitle: Boolean = false, from: MCTabListAPI.CurrentTabList = MCTabListAPI.currentTabList): List<Text> { + return from.body + .dropWhile { !widgetName.matchesTitle(it) } + .takeWhile { it.string.isNotBlank() && !it.string.startsWith(" ") } + .let { if (includeTitle) it else it.drop(1) } + } + + enum class WidgetName(regex: Regex?) { + COMMISSIONS, + SKILLS("Skills:( .*)?"), + PROFILE("Profile: (.*)"), + COLLECTION, + ESSENCE, + PET + ; + + fun matchesTitle(it: Text): Boolean { + return regex.matches(it.unformattedString) + } + + constructor() : this(null) + constructor(@Language("RegExp") regex: String) : this(Regex(regex)) + + val label = + name.split("_").map { it.lowercase().title() }.unwords() + val regex = regex ?: Regex.fromLiteral("$label:") + + } + +} diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt index 4ef7f76..cfda2e9 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -56,6 +56,7 @@ fun OrderedText.reconstitute(): MutableText { return base } + fun StringVisitable.reconstitute(): MutableText { val base = Text.literal("") base.setStyle(Style.EMPTY.withItalic(false)) @@ -82,15 +83,47 @@ val Text.unformattedString: String val Text.directLiteralStringContent: String? get() = (this.content as? PlainTextContent)?.string() -fun Text.getLegacyFormatString() = +fun Text.getLegacyFormatString(trimmed: Boolean = false): String = run { + var lastCode = "§r" val sb = StringBuilder() + fun appendCode(code: String) { + if (code != lastCode || !trimmed) { + sb.append(code) + lastCode = code + } + } for (component in iterator()) { - sb.append(component.style.color?.toChatFormatting()?.toString() ?: "§r") + if (component.directLiteralStringContent.isNullOrEmpty() && component.siblings.isEmpty()) { + continue + } + appendCode(component.style.let { style -> + var color = style.color?.toChatFormatting()?.toString() ?: "§r" + if (style.isBold) + color += LegacyFormattingCode.BOLD.formattingCode + if (style.isItalic) + color += LegacyFormattingCode.ITALIC.formattingCode + if (style.isUnderlined) + color += LegacyFormattingCode.UNDERLINE.formattingCode + if (style.isObfuscated) + color += LegacyFormattingCode.OBFUSCATED.formattingCode + if (style.isStrikethrough) + color += LegacyFormattingCode.STRIKETHROUGH.formattingCode + color + }) sb.append(component.directLiteralStringContent) - sb.append("§r") + if (!trimmed) + appendCode("§r") } sb.toString() + }.also { + var it = it + if (trimmed) { + it = it.removeSuffix("§r") + if (it.length == 2 && it.startsWith("§")) + it = "" + } + it } private val textColorLUT = Formatting.entries @@ -127,7 +160,7 @@ 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.hover(text: Text): MutableText = styled { it.withHoverEvent(HoverEvent.ShowText(text)) } fun MutableText.clickCommand(command: String): MutableText { @@ -164,4 +197,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() } + } +} + |