aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/util')
-rw-r--r--src/main/kotlin/util/BazaarPriceStrategy.kt2
-rw-r--r--src/main/kotlin/util/ChromaColourUtil.kt10
-rw-r--r--src/main/kotlin/util/ErrorUtil.kt25
-rw-r--r--src/main/kotlin/util/HoveredItemStack.kt2
-rw-r--r--src/main/kotlin/util/IntUtil.kt12
-rw-r--r--src/main/kotlin/util/LegacyTagWriter.kt103
-rw-r--r--src/main/kotlin/util/MC.kt9
-rw-r--r--src/main/kotlin/util/MoulConfigUtils.kt67
-rw-r--r--src/main/kotlin/util/SkyblockId.kt107
-rw-r--r--src/main/kotlin/util/StringUtil.kt2
-rw-r--r--src/main/kotlin/util/TestUtil.kt1
-rw-r--r--src/main/kotlin/util/async/input.kt108
-rw-r--r--src/main/kotlin/util/collections/RangeUtil.kt40
-rw-r--r--src/main/kotlin/util/json/KJsonUtils.kt11
-rw-r--r--src/main/kotlin/util/math/Projections.kt46
-rw-r--r--src/main/kotlin/util/mc/InitLevel.kt25
-rw-r--r--src/main/kotlin/util/mc/MCTabListAPI.kt96
-rw-r--r--src/main/kotlin/util/mc/NbtUtil.kt10
-rw-r--r--src/main/kotlin/util/mc/SNbtFormatter.kt7
-rw-r--r--src/main/kotlin/util/mc/SkullItemData.kt3
-rw-r--r--src/main/kotlin/util/mc/SlotUtils.kt18
-rw-r--r--src/main/kotlin/util/regex.kt7
-rw-r--r--src/main/kotlin/util/render/CustomRenderLayers.kt47
-rw-r--r--src/main/kotlin/util/render/DrawContextExt.kt11
-rw-r--r--src/main/kotlin/util/render/LerpUtils.kt35
-rw-r--r--src/main/kotlin/util/render/RenderCircleProgress.kt118
-rw-r--r--src/main/kotlin/util/render/RenderInWorldContext.kt53
-rw-r--r--src/main/kotlin/util/skyblock/SkyBlockItems.kt6
-rw-r--r--src/main/kotlin/util/skyblock/TabListAPI.kt41
-rw-r--r--src/main/kotlin/util/textutil.kt41
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 {