diff options
Diffstat (limited to 'src/main/kotlin/util')
30 files changed, 860 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/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 42d9a89..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 @@ -22,28 +27,31 @@ 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 @@ -54,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. @@ -62,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(":", "-")) } } @@ -85,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() @@ -104,7 +113,7 @@ 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? @@ -135,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) } @@ -202,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/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 2458891..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 { |