aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/util
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
committerLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
commitd2f240ff0ca0d27f417f837e706c781a98c31311 (patch)
tree0db7aff6cc14deaf36eed83889d59fd6b3a6f599 /src/main/kotlin/util
parenta6906308163aa3b2d18fa1dc1aa71ac9bbcc83ab (diff)
downloadfirmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.gz
firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.bz2
firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.zip
Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
Diffstat (limited to 'src/main/kotlin/util')
-rw-r--r--src/main/kotlin/util/Base64Util.kt10
-rw-r--r--src/main/kotlin/util/BazaarPriceStrategy.kt19
-rw-r--r--src/main/kotlin/util/ClipboardUtils.kt24
-rw-r--r--src/main/kotlin/util/CommonSoundEffects.kt26
-rw-r--r--src/main/kotlin/util/DurabilityBarEvent.kt20
-rw-r--r--src/main/kotlin/util/ErrorBoundary.kt10
-rw-r--r--src/main/kotlin/util/FirmFormatters.kt59
-rw-r--r--src/main/kotlin/util/FragmentGuiScreen.kt93
-rw-r--r--src/main/kotlin/util/GetRectangle.kt17
-rw-r--r--src/main/kotlin/util/HoveredItemStack.kt31
-rw-r--r--src/main/kotlin/util/IdentifierSerializer.kt25
-rw-r--r--src/main/kotlin/util/IdentityCharacteristics.kt15
-rw-r--r--src/main/kotlin/util/ItemUtil.kt26
-rw-r--r--src/main/kotlin/util/LegacyFormattingCode.kt35
-rw-r--r--src/main/kotlin/util/LegacyTagParser.kt245
-rw-r--r--src/main/kotlin/util/LoadResource.kt20
-rw-r--r--src/main/kotlin/util/Locraw.kt12
-rw-r--r--src/main/kotlin/util/LogIfNull.kt8
-rw-r--r--src/main/kotlin/util/MC.kt94
-rw-r--r--src/main/kotlin/util/MinecraftDispatcher.kt8
-rw-r--r--src/main/kotlin/util/MoulConfigFragment.kt44
-rw-r--r--src/main/kotlin/util/MoulConfigUtils.kt230
-rw-r--r--src/main/kotlin/util/MutableMapWithMaxSize.kt38
-rw-r--r--src/main/kotlin/util/SBData.kt66
-rw-r--r--src/main/kotlin/util/ScoreboardUtil.kt45
-rw-r--r--src/main/kotlin/util/ScreenUtil.kt38
-rw-r--r--src/main/kotlin/util/SequenceUtil.kt11
-rw-r--r--src/main/kotlin/util/SkyBlockIsland.kt42
-rw-r--r--src/main/kotlin/util/SkyblockId.kt149
-rw-r--r--src/main/kotlin/util/SortedMapSerializer.kt25
-rw-r--r--src/main/kotlin/util/TemplateUtil.kt85
-rw-r--r--src/main/kotlin/util/TimeMark.kt44
-rw-r--r--src/main/kotlin/util/Timer.kt25
-rw-r--r--src/main/kotlin/util/WarpUtil.kt75
-rw-r--r--src/main/kotlin/util/assertions.kt25
-rw-r--r--src/main/kotlin/util/async/input.kt47
-rw-r--r--src/main/kotlin/util/colorconversion.kt13
-rw-r--r--src/main/kotlin/util/customgui/CoordRememberingSlot.kt14
-rw-r--r--src/main/kotlin/util/customgui/CustomGui.kt72
-rw-r--r--src/main/kotlin/util/customgui/HasCustomGui.kt17
-rw-r--r--src/main/kotlin/util/data/DataHolder.kt62
-rw-r--r--src/main/kotlin/util/data/IDataHolder.kt77
-rw-r--r--src/main/kotlin/util/data/ProfileSpecificDataHolder.kt84
-rw-r--r--src/main/kotlin/util/filter/IteratorFilterSet.kt33
-rw-r--r--src/main/kotlin/util/item/NbtItemData.kt24
-rw-r--r--src/main/kotlin/util/item/SkullItemData.kt90
-rw-r--r--src/main/kotlin/util/json/BlockPosSerializer.kt25
-rw-r--r--src/main/kotlin/util/json/DashlessUUIDSerializer.kt29
-rw-r--r--src/main/kotlin/util/json/InstantAsLongSerializer.kt22
-rw-r--r--src/main/kotlin/util/json/SingletonSerializableList.kt31
-rw-r--r--src/main/kotlin/util/listutil.kt9
-rw-r--r--src/main/kotlin/util/propertyutil.kt9
-rw-r--r--src/main/kotlin/util/regex.kt55
-rw-r--r--src/main/kotlin/util/render/FacingThePlayerContext.kt101
-rw-r--r--src/main/kotlin/util/render/LerpUtils.kt33
-rw-r--r--src/main/kotlin/util/render/RenderCircleProgress.kt95
-rw-r--r--src/main/kotlin/util/render/RenderContextDSL.kt6
-rw-r--r--src/main/kotlin/util/render/RenderInWorldContext.kt294
-rw-r--r--src/main/kotlin/util/render/TranslatedScissors.kt22
-rw-r--r--src/main/kotlin/util/stringutil.kt6
-rw-r--r--src/main/kotlin/util/textutil.kt117
-rw-r--r--src/main/kotlin/util/uuid.kt12
62 files changed, 3138 insertions, 0 deletions
diff --git a/src/main/kotlin/util/Base64Util.kt b/src/main/kotlin/util/Base64Util.kt
new file mode 100644
index 0000000..44bcdfd
--- /dev/null
+++ b/src/main/kotlin/util/Base64Util.kt
@@ -0,0 +1,10 @@
+
+package moe.nea.firmament.util
+
+object Base64Util {
+ fun String.padToValidBase64(): String {
+ val align = this.length % 4
+ if (align == 0) return this
+ return this + "=".repeat(4 - align)
+ }
+}
diff --git a/src/main/kotlin/util/BazaarPriceStrategy.kt b/src/main/kotlin/util/BazaarPriceStrategy.kt
new file mode 100644
index 0000000..002eedb
--- /dev/null
+++ b/src/main/kotlin/util/BazaarPriceStrategy.kt
@@ -0,0 +1,19 @@
+
+package moe.nea.firmament.util
+
+import moe.nea.firmament.repo.HypixelStaticData
+
+enum class BazaarPriceStrategy {
+ BUY_ORDER,
+ SELL_ORDER,
+ NPC_SELL;
+
+ fun getSellPrice(skyblockId: SkyblockId): Double {
+ val bazaarEntry = HypixelStaticData.bazaarData[skyblockId] ?: return 0.0
+ return when (this) {
+ BUY_ORDER -> bazaarEntry.quickStatus.sellPrice
+ SELL_ORDER -> bazaarEntry.quickStatus.buyPrice
+ NPC_SELL -> TODO()
+ }
+ }
+}
diff --git a/src/main/kotlin/util/ClipboardUtils.kt b/src/main/kotlin/util/ClipboardUtils.kt
new file mode 100644
index 0000000..7b9b836
--- /dev/null
+++ b/src/main/kotlin/util/ClipboardUtils.kt
@@ -0,0 +1,24 @@
+
+
+package moe.nea.firmament.util
+
+import moe.nea.firmament.Firmament
+
+object ClipboardUtils {
+ fun setTextContent(string: String) {
+ try {
+ MC.keyboard.clipboard = string.ifEmpty { " " }
+ } catch (e: Exception) {
+ Firmament.logger.error("Could not write clipboard", e)
+ }
+ }
+
+ fun getTextContents(): String {
+ try {
+ return MC.keyboard.clipboard ?: ""
+ } catch (e: Exception) {
+ Firmament.logger.error("Could not read clipboard", e)
+ return ""
+ }
+ }
+}
diff --git a/src/main/kotlin/util/CommonSoundEffects.kt b/src/main/kotlin/util/CommonSoundEffects.kt
new file mode 100644
index 0000000..a97a2cb
--- /dev/null
+++ b/src/main/kotlin/util/CommonSoundEffects.kt
@@ -0,0 +1,26 @@
+
+
+package moe.nea.firmament.util
+
+import net.minecraft.client.sound.PositionedSoundInstance
+import net.minecraft.sound.SoundEvent
+import net.minecraft.util.Identifier
+
+// TODO: Replace these with custom sound events that just re use the vanilla ogg s
+object CommonSoundEffects {
+ fun playSound(identifier: Identifier) {
+ MC.soundManager.play(PositionedSoundInstance.master(SoundEvent.of(identifier), 1F))
+ }
+
+ fun playFailure() {
+ playSound(Identifier.of("minecraft", "block.anvil.place"))
+ }
+
+ fun playSuccess() {
+ playDing()
+ }
+
+ fun playDing() {
+ playSound(Identifier.of("minecraft", "entity.arrow.hit_player"))
+ }
+}
diff --git a/src/main/kotlin/util/DurabilityBarEvent.kt b/src/main/kotlin/util/DurabilityBarEvent.kt
new file mode 100644
index 0000000..993462c
--- /dev/null
+++ b/src/main/kotlin/util/DurabilityBarEvent.kt
@@ -0,0 +1,20 @@
+
+package moe.nea.firmament.util
+
+import me.shedaniel.math.Color
+import net.minecraft.item.ItemStack
+import moe.nea.firmament.events.FirmamentEvent
+import moe.nea.firmament.events.FirmamentEventBus
+
+data class DurabilityBarEvent(
+ val item: ItemStack,
+) : FirmamentEvent() {
+ data class DurabilityBar(
+ val color: Color,
+ val percentage: Float,
+ )
+
+ var barOverride: DurabilityBar? = null
+
+ companion object : FirmamentEventBus<DurabilityBarEvent>()
+}
diff --git a/src/main/kotlin/util/ErrorBoundary.kt b/src/main/kotlin/util/ErrorBoundary.kt
new file mode 100644
index 0000000..fbc5b37
--- /dev/null
+++ b/src/main/kotlin/util/ErrorBoundary.kt
@@ -0,0 +1,10 @@
+
+
+package moe.nea.firmament.util
+
+
+fun <T> errorBoundary(block: () -> T): T? {
+ // TODO: implement a proper error boundary here to avoid crashing minecraft code
+ return block()
+}
+
diff --git a/src/main/kotlin/util/FirmFormatters.kt b/src/main/kotlin/util/FirmFormatters.kt
new file mode 100644
index 0000000..c3bdd16
--- /dev/null
+++ b/src/main/kotlin/util/FirmFormatters.kt
@@ -0,0 +1,59 @@
+
+
+package moe.nea.firmament.util
+
+import com.google.common.math.IntMath.pow
+import kotlin.math.absoluteValue
+import kotlin.time.Duration
+
+object FirmFormatters {
+ fun formatCommas(int: Int, segments: Int = 3): String = formatCommas(int.toLong(), segments)
+ fun formatCommas(long: Long, segments: Int = 3): String {
+ val α = long / 1000
+ if (α != 0L) {
+ return formatCommas(α, segments) + "," + (long - α * 1000).toString().padStart(3, '0')
+ }
+ return long.toString()
+ }
+
+ fun formatCommas(float: Float, fractionalDigits: Int): String = formatCommas(float.toDouble(), fractionalDigits)
+ fun formatCommas(double: Double, fractionalDigits: Int): String {
+ val long = double.toLong()
+ val δ = (double - long).absoluteValue
+ val μ = pow(10, fractionalDigits)
+ val digits = (μ * δ).toInt().toString().padStart(fractionalDigits, '0').trimEnd('0')
+ return formatCommas(long) + (if (digits.isEmpty()) "" else ".$digits")
+ }
+
+ fun formatDistance(distance: Double): String {
+ if (distance < 10)
+ return "%.1fm".format(distance)
+ return "%dm".format(distance.toInt())
+ }
+
+ fun formatTimespan(duration: Duration, millis: Boolean = false): String {
+ if (duration.isInfinite()) {
+ return if (duration.isPositive()) "∞"
+ else "-∞"
+ }
+ val sb = StringBuilder()
+ if (duration.isNegative()) sb.append("-")
+ duration.toComponents { days, hours, minutes, seconds, nanoseconds ->
+ if (days > 0) {
+ sb.append(days).append("d")
+ }
+ if (hours > 0) {
+ sb.append(hours).append("h")
+ }
+ if (minutes > 0) {
+ sb.append(minutes).append("m")
+ }
+ sb.append(seconds).append("s")
+ if (millis) {
+ sb.append(nanoseconds / 1_000_000).append("ms")
+ }
+ }
+ return sb.toString()
+ }
+
+}
diff --git a/src/main/kotlin/util/FragmentGuiScreen.kt b/src/main/kotlin/util/FragmentGuiScreen.kt
new file mode 100644
index 0000000..5e13d51
--- /dev/null
+++ b/src/main/kotlin/util/FragmentGuiScreen.kt
@@ -0,0 +1,93 @@
+
+
+package moe.nea.firmament.util
+
+import io.github.notenoughupdates.moulconfig.gui.GuiContext
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.text.Text
+
+abstract class FragmentGuiScreen(
+ val dismissOnOutOfBounds: Boolean = true
+) : Screen(Text.literal("")) {
+ var popup: MoulConfigFragment? = null
+
+ fun createPopup(context: GuiContext, position: Point) {
+ popup = MoulConfigFragment(context, position) { popup = null }
+ }
+
+ override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ super.render(context, mouseX, mouseY, delta)
+ context.matrices.push()
+ context.matrices.translate(0f, 0f, 1000f)
+ popup?.render(context, mouseX, mouseY, delta)
+ context.matrices.pop()
+ }
+
+ private inline fun ifPopup(ifYes: (MoulConfigFragment) -> Unit): Boolean {
+ val p = popup ?: return false
+ ifYes(p)
+ return true
+ }
+
+ override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ return ifPopup {
+ it.keyPressed(keyCode, scanCode, modifiers)
+ }
+ }
+
+ override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ return ifPopup {
+ it.keyReleased(keyCode, scanCode, modifiers)
+ }
+ }
+
+ override fun mouseMoved(mouseX: Double, mouseY: Double) {
+ ifPopup { it.mouseMoved(mouseX, mouseY) }
+ }
+
+ override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ return ifPopup {
+ it.mouseReleased(mouseX, mouseY, button)
+ }
+ }
+
+ override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
+ return ifPopup {
+ it.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
+ }
+ }
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ return ifPopup {
+ if (!Rectangle(
+ it.position,
+ Dimension(it.context.root.width, it.context.root.height)
+ ).contains(Point(mouseX, mouseY))
+ && dismissOnOutOfBounds
+ ) {
+ popup = null
+ } else {
+ it.mouseClicked(mouseX, mouseY, button)
+ }
+ }|| super.mouseClicked(mouseX, mouseY, button)
+ }
+
+ override fun charTyped(chr: Char, modifiers: Int): Boolean {
+ return ifPopup { it.charTyped(chr, modifiers) }
+ }
+
+ override fun mouseScrolled(
+ mouseX: Double,
+ mouseY: Double,
+ horizontalAmount: Double,
+ verticalAmount: Double
+ ): Boolean {
+ return ifPopup {
+ it.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
+ }
+ }
+}
diff --git a/src/main/kotlin/util/GetRectangle.kt b/src/main/kotlin/util/GetRectangle.kt
new file mode 100644
index 0000000..ec64f31
--- /dev/null
+++ b/src/main/kotlin/util/GetRectangle.kt
@@ -0,0 +1,17 @@
+
+
+package moe.nea.firmament.util
+
+import me.shedaniel.math.Rectangle
+import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
+import net.minecraft.client.gui.screen.ingame.HandledScreen
+
+fun HandledScreen<*>.getRectangle(): Rectangle {
+ this as AccessorHandledScreen
+ return Rectangle(
+ getX_Firmament(),
+ getY_Firmament(),
+ getBackgroundWidth_Firmament(),
+ getBackgroundHeight_Firmament()
+ )
+}
diff --git a/src/main/kotlin/util/HoveredItemStack.kt b/src/main/kotlin/util/HoveredItemStack.kt
new file mode 100644
index 0000000..47a59d0
--- /dev/null
+++ b/src/main/kotlin/util/HoveredItemStack.kt
@@ -0,0 +1,31 @@
+
+
+package moe.nea.firmament.util
+
+import me.shedaniel.math.impl.PointHelper
+import me.shedaniel.rei.api.client.REIRuntime
+import me.shedaniel.rei.api.client.gui.widgets.Slot
+import me.shedaniel.rei.api.client.registry.screen.ScreenRegistry
+import net.minecraft.client.gui.Element
+import net.minecraft.client.gui.ParentElement
+import net.minecraft.client.gui.screen.ingame.HandledScreen
+import net.minecraft.item.ItemStack
+import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
+
+
+val HandledScreen<*>.focusedItemStack: ItemStack?
+ get() {
+ this as AccessorHandledScreen
+ val vanillaSlot = this.focusedSlot_Firmament?.stack
+ if (vanillaSlot != null) return vanillaSlot
+ val focusedSlot = ScreenRegistry.getInstance().getFocusedStack(this, PointHelper.ofMouse())
+ if (focusedSlot != null) return focusedSlot.cheatsAs().value
+ var baseElement: Element? = REIRuntime.getInstance().overlay.orElse(null)
+ val mx = PointHelper.getMouseFloatingX()
+ val my = PointHelper.getMouseFloatingY()
+ while (true) {
+ if (baseElement is Slot) return baseElement.currentEntry.cheatsAs().value
+ if (baseElement !is ParentElement) return null
+ baseElement = baseElement.hoveredElement(mx, my).orElse(null)
+ }
+ }
diff --git a/src/main/kotlin/util/IdentifierSerializer.kt b/src/main/kotlin/util/IdentifierSerializer.kt
new file mode 100644
index 0000000..65c5b1c
--- /dev/null
+++ b/src/main/kotlin/util/IdentifierSerializer.kt
@@ -0,0 +1,25 @@
+
+package moe.nea.firmament.util
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import net.minecraft.util.Identifier
+
+object IdentifierSerializer : KSerializer<Identifier> {
+ val delegateSerializer = String.serializer()
+ override val descriptor: SerialDescriptor
+ get() = PrimitiveSerialDescriptor("Identifier", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): Identifier {
+ return Identifier.of(decoder.decodeSerializableValue(delegateSerializer))
+ }
+
+ override fun serialize(encoder: Encoder, value: Identifier) {
+ encoder.encodeSerializableValue(delegateSerializer, value.toString())
+ }
+}
diff --git a/src/main/kotlin/util/IdentityCharacteristics.kt b/src/main/kotlin/util/IdentityCharacteristics.kt
new file mode 100644
index 0000000..f6054c4
--- /dev/null
+++ b/src/main/kotlin/util/IdentityCharacteristics.kt
@@ -0,0 +1,15 @@
+
+
+package moe.nea.firmament.util
+
+class IdentityCharacteristics<T>(val value: T) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is IdentityCharacteristics<*>) return false
+ return value === other.value
+ }
+
+ override fun hashCode(): Int {
+ return System.identityHashCode(value)
+ }
+}
diff --git a/src/main/kotlin/util/ItemUtil.kt b/src/main/kotlin/util/ItemUtil.kt
new file mode 100644
index 0000000..40d6198
--- /dev/null
+++ b/src/main/kotlin/util/ItemUtil.kt
@@ -0,0 +1,26 @@
+
+
+package moe.nea.firmament.util
+
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtList
+import net.minecraft.text.Text
+import moe.nea.firmament.util.item.loreAccordingToNbt
+
+
+fun ItemStack.appendLore(args: List<Text>) {
+ if (args.isEmpty()) return
+ modifyLore {
+ val loreList = loreAccordingToNbt.toMutableList()
+ for (arg in args) {
+ loreList.add(arg)
+ }
+ loreList
+ }
+}
+
+fun ItemStack.modifyLore(update: (List<Text>) -> List<Text>) {
+ val loreList = loreAccordingToNbt
+ loreAccordingToNbt = update(loreList)
+}
diff --git a/src/main/kotlin/util/LegacyFormattingCode.kt b/src/main/kotlin/util/LegacyFormattingCode.kt
new file mode 100644
index 0000000..44bacfc
--- /dev/null
+++ b/src/main/kotlin/util/LegacyFormattingCode.kt
@@ -0,0 +1,35 @@
+
+
+package moe.nea.firmament.util
+
+import net.minecraft.util.Formatting
+
+enum class LegacyFormattingCode(val label: String, val char: Char, val index: Int) {
+ BLACK("BLACK", '0', 0),
+ DARK_BLUE("DARK_BLUE", '1', 1),
+ DARK_GREEN("DARK_GREEN", '2', 2),
+ DARK_AQUA("DARK_AQUA", '3', 3),
+ DARK_RED("DARK_RED", '4', 4),
+ DARK_PURPLE("DARK_PURPLE", '5', 5),
+ GOLD("GOLD", '6', 6),
+ GRAY("GRAY", '7', 7),
+ DARK_GRAY("DARK_GRAY", '8', 8),
+ BLUE("BLUE", '9', 9),
+ GREEN("GREEN", 'a', 10),
+ AQUA("AQUA", 'b', 11),
+ RED("RED", 'c', 12),
+ LIGHT_PURPLE("LIGHT_PURPLE", 'd', 13),
+ YELLOW("YELLOW", 'e', 14),
+ WHITE("WHITE", 'f', 15),
+ OBFUSCATED("OBFUSCATED", 'k', -1),
+ BOLD("BOLD", 'l', -1),
+ STRIKETHROUGH("STRIKETHROUGH", 'm', -1),
+ UNDERLINE("UNDERLINE", 'n', -1),
+ ITALIC("ITALIC", 'o', -1),
+ RESET("RESET", 'r', -1);
+
+ val modern = Formatting.byCode(char)!!
+
+ val formattingCode = "§$char"
+
+}
diff --git a/src/main/kotlin/util/LegacyTagParser.kt b/src/main/kotlin/util/LegacyTagParser.kt
new file mode 100644
index 0000000..4e08da1
--- /dev/null
+++ b/src/main/kotlin/util/LegacyTagParser.kt
@@ -0,0 +1,245 @@
+
+
+package moe.nea.firmament.util
+
+import java.util.*
+import net.minecraft.nbt.AbstractNbtNumber
+import net.minecraft.nbt.NbtByte
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtDouble
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtFloat
+import net.minecraft.nbt.NbtInt
+import net.minecraft.nbt.NbtList
+import net.minecraft.nbt.NbtLong
+import net.minecraft.nbt.NbtShort
+import net.minecraft.nbt.NbtString
+
+class LegacyTagParser private constructor(string: String) {
+ data class TagParsingException(val baseString: String, val offset: Int, val mes0: String) :
+ Exception("$mes0 at $offset in `$baseString`.")
+
+ class StringRacer(val backing: String) {
+ var idx = 0
+ val stack = Stack<Int>()
+
+ fun pushState() {
+ stack.push(idx)
+ }
+
+ fun popState() {
+ idx = stack.pop()
+ }
+
+ fun discardState() {
+ stack.pop()
+ }
+
+ fun peek(count: Int): String {
+ return backing.substring(minOf(idx, backing.length), minOf(idx + count, backing.length))
+ }
+
+ fun finished(): Boolean {
+ return peek(1).isEmpty()
+ }
+
+ fun peekReq(count: Int): String? {
+ val p = peek(count)
+ if (p.length != count)
+ return null
+ return p
+ }
+
+ fun consumeCountReq(count: Int): String? {
+ val p = peekReq(count)
+ if (p != null)
+ idx += count
+ return p
+ }
+
+ fun tryConsume(string: String): Boolean {
+ val p = peek(string.length)
+ if (p != string)
+ return false
+ idx += p.length
+ return true
+ }
+
+ fun consumeWhile(shouldConsumeThisString: (String) -> Boolean): String {
+ var lastString: String = ""
+ while (true) {
+ val nextString = lastString + peek(1)
+ if (!shouldConsumeThisString(nextString)) {
+ return lastString
+ }
+ idx++
+ lastString = nextString
+ }
+ }
+
+ fun expect(search: String, errorMessage: String) {
+ if (!tryConsume(search))
+ error(errorMessage)
+ }
+
+ fun error(errorMessage: String): Nothing {
+ throw TagParsingException(backing, idx, errorMessage)
+ }
+
+ }
+
+ val racer = StringRacer(string)
+ val baseTag = parseTag()
+
+ companion object {
+ val digitRange = "0123456789-"
+ fun parse(string: String): NbtCompound {
+ return LegacyTagParser(string).baseTag
+ }
+ }
+
+ fun skipWhitespace() {
+ racer.consumeWhile { Character.isWhitespace(it.last()) } // Only check last since other chars are always checked before.
+ }
+
+ fun parseTag(): NbtCompound {
+ skipWhitespace()
+ racer.expect("{", "Expected '{’ at start of tag")
+ skipWhitespace()
+ val tag = NbtCompound()
+ while (!racer.tryConsume("}")) {
+ skipWhitespace()
+ val lhs = parseIdentifier()
+ skipWhitespace()
+ racer.expect(":", "Expected ':' after identifier in tag")
+ skipWhitespace()
+ val rhs = parseAny()
+ tag.put(lhs, rhs)
+ racer.tryConsume(",")
+ skipWhitespace()
+ }
+ return tag
+ }
+
+ private fun parseAny(): NbtElement {
+ skipWhitespace()
+ val nextChar = racer.peekReq(1) ?: racer.error("Expected new object, found EOF")
+ return when {
+ nextChar == "{" -> parseTag()
+ nextChar == "[" -> parseList()
+ nextChar == "\"" -> parseStringTag()
+ nextChar.first() in (digitRange) -> parseNumericTag()
+ else -> racer.error("Unexpected token found. Expected start of new element")
+ }
+ }
+
+ fun parseList(): NbtList {
+ skipWhitespace()
+ racer.expect("[", "Expected '[' at start of tag")
+ skipWhitespace()
+ val list = NbtList()
+ while (!racer.tryConsume("]")) {
+ skipWhitespace()
+ racer.pushState()
+ val lhs = racer.consumeWhile { it.all { it in digitRange } }
+ skipWhitespace()
+ if (!racer.tryConsume(":") || lhs.isEmpty()) { // No prefixed 0:
+ racer.popState()
+ list.add(parseAny()) // Reparse our number (or not a number) as actual tag
+ } else {
+ racer.discardState()
+ skipWhitespace()
+ list.add(parseAny()) // Ignore prefix indexes. They should not be generated out of order by any vanilla implementation (which is what NEU should export). Instead append where it appears in order.
+ }
+ skipWhitespace()
+ racer.tryConsume(",")
+ }
+ return list
+ }
+
+ fun parseQuotedString(): String {
+ skipWhitespace()
+ racer.expect("\"", "Expected '\"' at string start")
+ val sb = StringBuilder()
+ while (true) {
+ when (val peek = racer.consumeCountReq(1)) {
+ "\"" -> break
+ "\\" -> {
+ val escaped = racer.consumeCountReq(1) ?: racer.error("Unfinished backslash escape")
+ if (escaped != "\"" && escaped != "\\") {
+ // Surprisingly i couldn't find unicode escapes to be generated by the original minecraft 1.8.9 implementation
+ racer.idx--
+ racer.error("Invalid backslash escape '$escaped'")
+ }
+ sb.append(escaped)
+ }
+
+ null -> racer.error("Unfinished string")
+ else -> {
+ sb.append(peek)
+ }
+ }
+ }
+ return sb.toString()
+ }
+
+ fun parseStringTag(): NbtString {
+ return NbtString.of(parseQuotedString())
+ }
+
+ object Patterns {
+ val DOUBLE = "([-+]?[0-9]*\\.?[0-9]+)[d|D]".toRegex()
+ val FLOAT = "([-+]?[0-9]*\\.?[0-9]+)[f|F]".toRegex()
+ val BYTE = "([-+]?[0-9]+)[b|B]".toRegex()
+ val LONG = "([-+]?[0-9]+)[l|L]".toRegex()
+ val SHORT = "([-+]?[0-9]+)[s|S]".toRegex()
+ val INTEGER = "([-+]?[0-9]+)".toRegex()
+ val DOUBLE_UNTYPED = "([-+]?[0-9]*\\.?[0-9]+)".toRegex()
+ val ROUGH_PATTERN = "[-+]?[0-9]*\\.?[0-9]*[dDbBfFlLsS]?".toRegex()
+ }
+
+ fun parseNumericTag(): AbstractNbtNumber {
+ skipWhitespace()
+ val textForm = racer.consumeWhile { Patterns.ROUGH_PATTERN.matchEntire(it) != null }
+ if (textForm.isEmpty()) {
+ racer.error("Expected numeric tag (starting with either -, +, . or a digit")
+ }
+ val floatMatch = Patterns.FLOAT.matchEntire(textForm)
+ if (floatMatch != null) {
+ return NbtFloat.of(floatMatch.groups[1]!!.value.toFloat())
+ }
+ val byteMatch = Patterns.BYTE.matchEntire(textForm)
+ if (byteMatch != null) {
+ return NbtByte.of(byteMatch.groups[1]!!.value.toByte())
+ }
+ val longMatch = Patterns.LONG.matchEntire(textForm)
+ if (longMatch != null) {
+ return NbtLong.of(longMatch.groups[1]!!.value.toLong())
+ }
+ val shortMatch = Patterns.SHORT.matchEntire(textForm)
+ if (shortMatch != null) {
+ return NbtShort.of(shortMatch.groups[1]!!.value.toShort())
+ }
+ val integerMatch = Patterns.INTEGER.matchEntire(textForm)
+ if (integerMatch != null) {
+ return NbtInt.of(integerMatch.groups[1]!!.value.toInt())
+ }
+ val doubleMatch = Patterns.DOUBLE.matchEntire(textForm) ?: Patterns.DOUBLE_UNTYPED.matchEntire(textForm)
+ if (doubleMatch != null) {
+ return NbtDouble.of(doubleMatch.groups[1]!!.value.toDouble())
+ }
+ throw IllegalStateException("Could not properly parse numeric tag '$textForm', despite passing rough verification. This is a bug in the LegacyTagParser")
+ }
+
+ private fun parseIdentifier(): String {
+ skipWhitespace()
+ if (racer.peek(1) == "\"") {
+ return parseQuotedString()
+ }
+ return racer.consumeWhile {
+ val x = it.last()
+ x != ':' && !Character.isWhitespace(x)
+ }
+ }
+
+}
diff --git a/src/main/kotlin/util/LoadResource.kt b/src/main/kotlin/util/LoadResource.kt
new file mode 100644
index 0000000..4bc8704
--- /dev/null
+++ b/src/main/kotlin/util/LoadResource.kt
@@ -0,0 +1,20 @@
+
+package moe.nea.firmament.util
+
+import java.io.InputStream
+import kotlin.io.path.inputStream
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.util.Identifier
+import moe.nea.firmament.repo.RepoDownloadManager
+
+
+fun Identifier.openFirmamentResource(): InputStream {
+ val resource = MC.resourceManager.getResource(this).getOrNull()
+ if (resource == null) {
+ if (namespace == "neurepo")
+ return RepoDownloadManager.repoSavedLocation.resolve(path).inputStream()
+ error("Could not read resource $this")
+ }
+ return resource.inputStream
+}
+
diff --git a/src/main/kotlin/util/Locraw.kt b/src/main/kotlin/util/Locraw.kt
new file mode 100644
index 0000000..9778bc7
--- /dev/null
+++ b/src/main/kotlin/util/Locraw.kt
@@ -0,0 +1,12 @@
+
+
+package moe.nea.firmament.util
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+
+@Serializable
+data class Locraw(val server: String, val gametype: String? = null, val mode: String? = null, val map: String? = null) {
+ @Transient
+ val skyblockLocation = if (gametype == "SKYBLOCK") mode?.let(SkyBlockIsland::forMode) else null
+}
diff --git a/src/main/kotlin/util/LogIfNull.kt b/src/main/kotlin/util/LogIfNull.kt
new file mode 100644
index 0000000..600c5e6
--- /dev/null
+++ b/src/main/kotlin/util/LogIfNull.kt
@@ -0,0 +1,8 @@
+
+package moe.nea.firmament.util
+
+
+fun runNull(block: () -> Unit): Nothing? {
+ block()
+ return null
+}
diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt
new file mode 100644
index 0000000..b0d3056
--- /dev/null
+++ b/src/main/kotlin/util/MC.kt
@@ -0,0 +1,94 @@
+package moe.nea.firmament.util
+
+import io.github.moulberry.repo.data.Coordinate
+import java.util.concurrent.ConcurrentLinkedQueue
+import net.minecraft.client.MinecraftClient
+import net.minecraft.client.gui.screen.ingame.HandledScreen
+import net.minecraft.client.render.WorldRenderer
+import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket
+import net.minecraft.registry.BuiltinRegistries
+import net.minecraft.registry.RegistryKeys
+import net.minecraft.registry.RegistryWrapper
+import net.minecraft.resource.ReloadableResourceManagerImpl
+import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.events.TickEvent
+
+object MC {
+
+ private val messageQueue = ConcurrentLinkedQueue<Text>()
+
+ init {
+ TickEvent.subscribe {
+ while (true) {
+ inGameHud.chatHud.addMessage(messageQueue.poll() ?: break)
+ }
+ while (true) {
+ (nextTickTodos.poll() ?: break).invoke()
+ }
+ }
+ }
+
+ fun sendChat(text: Text) {
+ if (instance.isOnThread)
+ inGameHud.chatHud.addMessage(text)
+ else
+ messageQueue.add(text)
+ }
+
+ fun sendServerCommand(command: String) {
+ val nh = player?.networkHandler ?: return
+ nh.sendPacket(
+ CommandExecutionC2SPacket(
+ command,
+ )
+ )
+ }
+
+ fun sendServerChat(text: String) {
+ player?.networkHandler?.sendChatMessage(text)
+ }
+
+ fun sendCommand(command: String) {
+ player?.networkHandler?.sendCommand(command)
+ }
+
+ fun onMainThread(block: () -> Unit) {
+ if (instance.isOnThread)
+ block()
+ else
+ instance.send(block)
+ }
+
+ private val nextTickTodos = ConcurrentLinkedQueue<() -> Unit>()
+ fun nextTick(function: () -> Unit) {
+ nextTickTodos.add(function)
+ }
+
+
+ inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl)
+ inline val worldRenderer: WorldRenderer get() = instance.worldRenderer
+ inline val networkHandler get() = player?.networkHandler
+ inline val instance get() = MinecraftClient.getInstance()
+ inline val keyboard get() = instance.keyboard
+ inline val textureManager get() = instance.textureManager
+ inline val inGameHud get() = instance.inGameHud
+ inline val font get() = instance.textRenderer
+ inline val soundManager get() = instance.soundManager
+ inline val player get() = instance.player
+ inline val camera get() = instance.cameraEntity
+ inline val guiAtlasManager get() = instance.guiAtlasManager
+ inline val world get() = instance.world
+ inline var screen
+ get() = instance.currentScreen
+ set(value) = instance.setScreen(value)
+ inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*>
+ inline val window get() = instance.window
+ inline val currentRegistries: RegistryWrapper.WrapperLookup? get() = world?.registryManager
+ val defaultRegistries: RegistryWrapper.WrapperLookup = BuiltinRegistries.createWrapperLookup()
+ val defaultItems = defaultRegistries.getWrapperOrThrow(RegistryKeys.ITEM)
+}
+
+
+val Coordinate.blockPos: BlockPos
+ get() = BlockPos(x, y, z)
diff --git a/src/main/kotlin/util/MinecraftDispatcher.kt b/src/main/kotlin/util/MinecraftDispatcher.kt
new file mode 100644
index 0000000..d1f22a9
--- /dev/null
+++ b/src/main/kotlin/util/MinecraftDispatcher.kt
@@ -0,0 +1,8 @@
+
+
+package moe.nea.firmament.util
+
+import kotlinx.coroutines.asCoroutineDispatcher
+import net.minecraft.client.MinecraftClient
+
+val MinecraftDispatcher by lazy { MinecraftClient.getInstance().asCoroutineDispatcher() }
diff --git a/src/main/kotlin/util/MoulConfigFragment.kt b/src/main/kotlin/util/MoulConfigFragment.kt
new file mode 100644
index 0000000..36132cd
--- /dev/null
+++ b/src/main/kotlin/util/MoulConfigFragment.kt
@@ -0,0 +1,44 @@
+
+
+package moe.nea.firmament.util
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
+import io.github.notenoughupdates.moulconfig.gui.GuiContext
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import me.shedaniel.math.Point
+import net.minecraft.client.gui.DrawContext
+
+class MoulConfigFragment(
+ context: GuiContext,
+ val position: Point,
+ val dismiss: () -> Unit
+) : GuiComponentWrapper(context) {
+ init {
+ this.init(MC.instance, MC.screen!!.width, MC.screen!!.height)
+ }
+
+ override fun createContext(drawContext: DrawContext?): GuiImmediateContext {
+ val oldContext = super.createContext(drawContext)
+ return oldContext.translated(
+ position.x,
+ position.y,
+ context.root.width,
+ context.root.height,
+ )
+ }
+
+
+ override fun render(drawContext: DrawContext?, i: Int, j: Int, f: Float) {
+ val ctx = createContext(drawContext)
+ val m = drawContext!!.matrices
+ m.push()
+ m.translate(position.x.toFloat(), position.y.toFloat(), 0F)
+ context.root.render(ctx)
+ m.pop()
+ ctx.renderContext.doDrawTooltip()
+ }
+
+ override fun close() {
+ dismiss()
+ }
+}
diff --git a/src/main/kotlin/util/MoulConfigUtils.kt b/src/main/kotlin/util/MoulConfigUtils.kt
new file mode 100644
index 0000000..00561d1
--- /dev/null
+++ b/src/main/kotlin/util/MoulConfigUtils.kt
@@ -0,0 +1,230 @@
+
+
+package moe.nea.firmament.util
+
+import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
+import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
+import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
+import io.github.notenoughupdates.moulconfig.gui.GuiContext
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import io.github.notenoughupdates.moulconfig.xml.ChildCount
+import io.github.notenoughupdates.moulconfig.xml.XMLContext
+import io.github.notenoughupdates.moulconfig.xml.XMLGuiLoader
+import io.github.notenoughupdates.moulconfig.xml.XMLUniverse
+import io.github.notenoughupdates.moulconfig.xml.XSDGenerator
+import java.io.File
+import java.util.function.Supplier
+import javax.xml.namespace.QName
+import me.shedaniel.math.Color
+import org.w3c.dom.Element
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.gui.screen.Screen
+import moe.nea.firmament.gui.BarComponent
+import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.gui.FirmHoverComponent
+import moe.nea.firmament.gui.FixedComponent
+import moe.nea.firmament.gui.ImageComponent
+import moe.nea.firmament.gui.TickComponent
+
+object MoulConfigUtils {
+ val firmUrl = "http://firmament.nea.moe/moulconfig"
+ val universe = XMLUniverse.getDefaultUniverse().also { uni ->
+ uni.registerMapper(java.awt.Color::class.java) {
+ if (it.startsWith("#")) {
+ val hexString = it.substring(1)
+ val hex = hexString.toInt(16)
+ if (hexString.length == 6) {
+ return@registerMapper java.awt.Color(hex)
+ }
+ if (hexString.length == 8) {
+ return@registerMapper java.awt.Color(hex, true)
+ }
+ error("Hexcolor $it needs to be exactly 6 or 8 hex digits long")
+ }
+ return@registerMapper java.awt.Color(it.toInt(), true)
+ }
+ uni.registerMapper(Color::class.java) {
+ val color = uni.mapXMLObject(it, java.awt.Color::class.java)
+ Color.ofRGBA(color.red, color.green, color.blue, color.alpha)
+ }
+ uni.registerLoader(object : XMLGuiLoader.Basic<BarComponent> {
+ override fun getName(): QName {
+ return QName(firmUrl, "Bar")
+ }
+
+ override fun createInstance(context: XMLContext<*>, element: Element): BarComponent {
+ return BarComponent(
+ context.getPropertyFromAttribute(element, QName("progress"), Double::class.java)!!,
+ context.getPropertyFromAttribute(element, QName("total"), Double::class.java)!!,
+ context.getPropertyFromAttribute(element, QName("fillColor"), Color::class.java)!!.get(),
+ context.getPropertyFromAttribute(element, QName("emptyColor"), Color::class.java)!!.get(),
+ )
+ }
+
+ override fun getChildCount(): ChildCount {
+ return ChildCount.NONE
+ }
+
+ override fun getAttributeNames(): Map<String, Boolean> {
+ return mapOf("progress" to true, "total" to true, "emptyColor" to true, "fillColor" to true)
+ }
+ })
+ uni.registerLoader(object : XMLGuiLoader.Basic<FirmHoverComponent> {
+ 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("delay"), Duration::class.java, 0.6.seconds),
+ )
+ }
+
+ override fun getName(): QName {
+ return QName(firmUrl, "Hover")
+ }
+
+ override fun getChildCount(): ChildCount {
+ return ChildCount.ONE
+ }
+
+ override fun getAttributeNames(): Map<String, Boolean> {
+ return mapOf(
+ "lines" to true,
+ "delay" to false,
+ )
+ }
+
+ })
+ uni.registerLoader(object : XMLGuiLoader.Basic<FirmButtonComponent> {
+ override fun getName(): QName {
+ return QName(firmUrl, "Button")
+ }
+
+ override fun createInstance(context: XMLContext<*>, element: Element): FirmButtonComponent {
+ return FirmButtonComponent(
+ context.getChildFragment(element),
+ context.getPropertyFromAttribute(element, QName("enabled"), Boolean::class.java)
+ ?: GetSetter.constant(true),
+ context.getPropertyFromAttribute(element, QName("noBackground"), Boolean::class.java, false),
+ context.getMethodFromAttribute(element, QName("onClick")),
+ )
+ }
+
+ override fun getChildCount(): ChildCount {
+ return ChildCount.ONE
+ }
+
+ override fun getAttributeNames(): Map<String, Boolean> {
+ return mapOf("onClick" to true, "enabled" to false, "noBackground" to false)
+ }
+ })
+ uni.registerLoader(object : XMLGuiLoader.Basic<ImageComponent> {
+ override fun createInstance(context: XMLContext<*>, element: Element): ImageComponent {
+ return ImageComponent(
+ context.getPropertyFromAttribute(element, QName("width"), Int::class.java)!!.get(),
+ context.getPropertyFromAttribute(element, QName("height"), Int::class.java)!!.get(),
+ context.getPropertyFromAttribute(element, QName("resource"), MyResourceLocation::class.java)!!,
+ context.getPropertyFromAttribute(element, QName("u1"), Float::class.java, 0f),
+ context.getPropertyFromAttribute(element, QName("u2"), Float::class.java, 1f),
+ context.getPropertyFromAttribute(element, QName("v1"), Float::class.java, 0f),
+ context.getPropertyFromAttribute(element, QName("v2"), Float::class.java, 1f),
+ )
+ }
+
+ override fun getName(): QName {
+ return QName(firmUrl, "Image")
+ }
+
+ override fun getChildCount(): ChildCount {
+ return ChildCount.NONE
+ }
+
+ override fun getAttributeNames(): Map<String, Boolean> {
+ return mapOf(
+ "width" to true, "height" to true,
+ "resource" to true,
+ "u1" to false,
+ "u2" to false,
+ "v1" to false,
+ "v2" to false,
+ )
+ }
+ })
+ uni.registerLoader(object : XMLGuiLoader.Basic<TickComponent> {
+ override fun createInstance(context: XMLContext<*>, element: Element): TickComponent {
+ return TickComponent(context.getMethodFromAttribute(element, QName("tick")))
+ }
+
+ override fun getName(): QName {
+ return QName(firmUrl, "Tick")
+ }
+
+ override fun getChildCount(): ChildCount {
+ return ChildCount.NONE
+ }
+
+ override fun getAttributeNames(): Map<String, Boolean> {
+ return mapOf("tick" to true)
+ }
+ })
+ 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.getChildFragment(element)
+ )
+ }
+
+ override fun getName(): QName {
+ return QName(firmUrl, "Fixed")
+ }
+
+ override fun getChildCount(): ChildCount {
+ return ChildCount.ONE
+ }
+
+ override fun getAttributeNames(): Map<String, Boolean> {
+ return mapOf("width" to true, "height" to true)
+ }
+ })
+ }
+
+ fun generateXSD(
+ file: File,
+ namespace: String
+ ) {
+ val generator = XSDGenerator(universe, namespace)
+ generator.writeAll()
+ generator.dumpToFile(file)
+ }
+
+ @JvmStatic
+ fun main(args: Array<out String>) {
+ generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS)
+ generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl)
+ File("wrapper.xsd").writeText("""
+<?xml version="1.0" encoding="UTF-8" ?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/>
+ <xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/>
+</xs:schema>
+ """.trimIndent())
+ }
+
+ fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen {
+ return object : GuiComponentWrapper(loadGui(name, bindTo)) {
+ override fun close() {
+ if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) {
+ client!!.setScreen(parent)
+ }
+ }
+ }
+ }
+
+ fun loadGui(name: String, bindTo: Any): GuiContext {
+ return GuiContext(universe.load(bindTo, MyResourceLocation("firmament", "gui/$name.xml")))
+ }
+}
diff --git a/src/main/kotlin/util/MutableMapWithMaxSize.kt b/src/main/kotlin/util/MutableMapWithMaxSize.kt
new file mode 100644
index 0000000..067e652
--- /dev/null
+++ b/src/main/kotlin/util/MutableMapWithMaxSize.kt
@@ -0,0 +1,38 @@
+
+package moe.nea.firmament.util
+
+fun <K, V> mutableMapWithMaxSize(maxSize: Int): MutableMap<K, V> = object : LinkedHashMap<K, V>() {
+ override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>): Boolean {
+ return size > maxSize
+ }
+}
+
+fun <T, R> ((T) -> R).memoizeIdentity(maxCacheSize: Int): (T) -> R {
+ val memoized = { it: IdentityCharacteristics<T> ->
+ this(it.value)
+ }.memoize(maxCacheSize)
+ return { memoized(IdentityCharacteristics(it)) }
+}
+
+@PublishedApi
+internal val SENTINEL_NULL = java.lang.Object()
+
+/**
+ * Requires the map to only contain values of type [R] or [SENTINEL_NULL]. This is ensured if the map is only ever
+ * accessed via this function.
+ */
+inline fun <T, R> MutableMap<T, Any>.computeNullableFunction(key: T, crossinline func: () -> R): R {
+ val value = this.getOrPut(key) {
+ func() ?: SENTINEL_NULL
+ }
+ @Suppress("UNCHECKED_CAST")
+ return if (value === SENTINEL_NULL) null as R
+ else value as R
+}
+
+fun <T, R> ((T) -> R).memoize(maxCacheSize: Int): (T) -> R {
+ val map = mutableMapWithMaxSize<T, Any>(maxCacheSize)
+ return {
+ map.computeNullableFunction(it) { this@memoize(it) }
+ }
+}
diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt
new file mode 100644
index 0000000..b30c6fb
--- /dev/null
+++ b/src/main/kotlin/util/SBData.kt
@@ -0,0 +1,66 @@
+package moe.nea.firmament.util
+
+import java.util.UUID
+import net.hypixel.modapi.HypixelModAPI
+import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket
+import kotlin.jvm.optionals.getOrNull
+import kotlin.time.Duration.Companion.seconds
+import moe.nea.firmament.events.AllowChatEvent
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.ServerConnectedEvent
+import moe.nea.firmament.events.SkyblockServerUpdateEvent
+import moe.nea.firmament.events.WorldReadyEvent
+
+object SBData {
+ private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex()
+ val profileSuggestTexts = listOf(
+ "CLICK THIS TO SUGGEST IT IN CHAT [DASHES]",
+ "CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]",
+ )
+ var profileId: UUID? = null
+
+ private var hasReceivedProfile = false
+ var locraw: Locraw? = null
+ val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation
+ val hasValidLocraw get() = locraw?.server !in listOf("limbo", null)
+ val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK"
+ var lastProfileIdRequest = TimeMark.farPast()
+ fun init() {
+ ServerConnectedEvent.subscribe {
+ HypixelModAPI.getInstance().subscribeToEventPacket(ClientboundLocationPacket::class.java)
+ }
+ HypixelModAPI.getInstance().createHandler(ClientboundLocationPacket::class.java) {
+ MC.onMainThread {
+ val lastLocraw = locraw
+ locraw = Locraw(it.serverName,
+ it.serverType.getOrNull()?.name?.uppercase(),
+ it.mode.getOrNull(),
+ it.map.getOrNull())
+ SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, null))
+ }
+ }
+ SkyblockServerUpdateEvent.subscribe {
+ if (!hasReceivedProfile && isOnSkyblock && lastProfileIdRequest.passedTime() > 30.seconds) {
+ lastProfileIdRequest = TimeMark.now()
+ MC.sendServerCommand("profileid")
+ }
+ }
+ AllowChatEvent.subscribe { event ->
+ if (event.unformattedString in profileSuggestTexts && lastProfileIdRequest.passedTime() < 5.seconds) {
+ event.cancel()
+ }
+ }
+ ProcessChatEvent.subscribe(receivesCancelled = true) { event ->
+ val profileMatch = profileRegex.matchEntire(event.unformattedString)
+ if (profileMatch != null) {
+ try {
+ profileId = UUID.fromString(profileMatch.groupValues[1])
+ hasReceivedProfile = true
+ } catch (e: IllegalArgumentException) {
+ profileId = null
+ e.printStackTrace()
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/util/ScoreboardUtil.kt b/src/main/kotlin/util/ScoreboardUtil.kt
new file mode 100644
index 0000000..4311971
--- /dev/null
+++ b/src/main/kotlin/util/ScoreboardUtil.kt
@@ -0,0 +1,45 @@
+
+
+package moe.nea.firmament.util
+
+import java.util.*
+import net.minecraft.client.gui.hud.InGameHud
+import net.minecraft.scoreboard.ScoreboardDisplaySlot
+import net.minecraft.scoreboard.Team
+import net.minecraft.text.StringVisitable
+import net.minecraft.text.Style
+import net.minecraft.text.Text
+import net.minecraft.util.Formatting
+
+fun getScoreboardLines(): List<Text> {
+ val scoreboard = MC.player?.scoreboard ?: return listOf()
+ val activeObjective = scoreboard.getObjectiveForSlot(ScoreboardDisplaySlot.SIDEBAR) ?: return listOf()
+ return scoreboard.getScoreboardEntries(activeObjective)
+ .filter { !it.hidden() }
+ .sortedWith(InGameHud.SCOREBOARD_ENTRY_COMPARATOR)
+ .take(15).map {
+ val team = scoreboard.getScoreHolderTeam(it.owner)
+ val text = it.name()
+ Team.decorateName(team, text)
+ }
+}
+
+
+fun Text.formattedString(): String {
+ val sb = StringBuilder()
+ visit(StringVisitable.StyledVisitor<Unit> { style, string ->
+ val c = Formatting.byName(style.color?.name)
+ if (c != null) {
+ sb.append("§${c.code}")
+ }
+ if (style.isUnderlined) {
+ sb.append("§n")
+ }
+ if (style.isBold) {
+ sb.append("§l")
+ }
+ sb.append(string)
+ Optional.empty()
+ }, Style.EMPTY)
+ return sb.toString().replace("§[^a-f0-9]".toRegex(), "")
+}
diff --git a/src/main/kotlin/util/ScreenUtil.kt b/src/main/kotlin/util/ScreenUtil.kt
new file mode 100644
index 0000000..99d77fb
--- /dev/null
+++ b/src/main/kotlin/util/ScreenUtil.kt
@@ -0,0 +1,38 @@
+
+
+package moe.nea.firmament.util
+
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
+import net.minecraft.client.MinecraftClient
+import net.minecraft.client.gui.screen.Screen
+import moe.nea.firmament.Firmament
+
+object ScreenUtil {
+ init {
+ ClientTickEvents.START_CLIENT_TICK.register(::onTick)
+ }
+
+ private fun onTick(minecraft: MinecraftClient) {
+ if (nextOpenedGui != null) {
+ val p = minecraft.player
+ if (p?.currentScreenHandler != null) {
+ p.closeHandledScreen()
+ }
+ minecraft.setScreen(nextOpenedGui)
+ nextOpenedGui = null
+ }
+ }
+
+ private var nextOpenedGui: Screen? = null
+
+ fun setScreenLater(nextScreen: Screen?) {
+ val nog = nextOpenedGui
+ if (nog != null) {
+ Firmament.logger.warn("Setting screen ${if (nextScreen == null) "null" else nextScreen::class.qualifiedName} to be opened later, but ${nog::class.qualifiedName} is already queued.")
+ return
+ }
+ nextOpenedGui = nextScreen
+ }
+
+
+}
diff --git a/src/main/kotlin/util/SequenceUtil.kt b/src/main/kotlin/util/SequenceUtil.kt
new file mode 100644
index 0000000..7b5bad0
--- /dev/null
+++ b/src/main/kotlin/util/SequenceUtil.kt
@@ -0,0 +1,11 @@
+
+
+package moe.nea.firmament.util
+
+fun <T : Any> T.iterate(iterator: (T) -> T?): Sequence<T> = sequence {
+ var x: T? = this@iterate
+ while (x != null) {
+ yield(x)
+ x = iterator(x)
+ }
+}
diff --git a/src/main/kotlin/util/SkyBlockIsland.kt b/src/main/kotlin/util/SkyBlockIsland.kt
new file mode 100644
index 0000000..bd0567d
--- /dev/null
+++ b/src/main/kotlin/util/SkyBlockIsland.kt
@@ -0,0 +1,42 @@
+
+package moe.nea.firmament.util
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import moe.nea.firmament.repo.RepoManager
+
+@Serializable(with = SkyBlockIsland.Serializer::class)
+class SkyBlockIsland
+private constructor(
+ val locrawMode: String,
+) {
+
+ object Serializer : KSerializer<SkyBlockIsland> {
+ override val descriptor: SerialDescriptor
+ get() = PrimitiveSerialDescriptor("SkyBlockIsland", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): SkyBlockIsland {
+ return forMode(decoder.decodeString())
+ }
+
+ override fun serialize(encoder: Encoder, value: SkyBlockIsland) {
+ encoder.encodeString(value.locrawMode)
+ }
+ }
+ companion object {
+ private val allIslands = mutableMapOf<String, SkyBlockIsland>()
+ fun forMode(mode: String): SkyBlockIsland = allIslands.computeIfAbsent(mode, ::SkyBlockIsland)
+ val HUB = forMode("hub")
+ val PRIVATE_ISLAND = forMode("dynamic")
+ val RIFT = forMode("rift")
+ }
+
+ val userFriendlyName
+ get() = RepoManager.neuRepo.constants.islands.areaNames
+ .getOrDefault(locrawMode, locrawMode)
+}
diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt
new file mode 100644
index 0000000..59b1d2c
--- /dev/null
+++ b/src/main/kotlin/util/SkyblockId.kt
@@ -0,0 +1,149 @@
+
+
+@file:UseSerializers(DashlessUUIDSerializer::class)
+
+package moe.nea.firmament.util
+
+import io.github.moulberry.repo.data.NEUItem
+import io.github.moulberry.repo.data.Rarity
+import java.util.UUID
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+import kotlinx.serialization.json.Json
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.component.type.NbtComponent
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.util.Identifier
+import moe.nea.firmament.repo.set
+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`,
+ * with those values extracted from other metadata.
+ */
+@JvmInline
+@Serializable
+value class SkyblockId(val neuItem: String) {
+ val identifier
+ 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
+ }
+
+ /**
+ * 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.
+ */
+ @JvmInline
+ @Serializable
+ value class BazaarStock(val bazaarId: String) {
+ fun toRepoId(): SkyblockId {
+ bazaarEnchantmentRegex.matchEntire(bazaarId)?.let {
+ return SkyblockId("${it.groupValues[1]};${it.groupValues[2]}")
+ }
+ return SkyblockId(bazaarId.replace(":", "-"))
+ }
+ }
+
+ companion object {
+ val COINS: SkyblockId = SkyblockId("SKYBLOCK_COIN")
+ private val bazaarEnchantmentRegex = "ENCHANTMENT_(\\D*)_(\\d+)".toRegex()
+ val NULL: SkyblockId = SkyblockId("null")
+ val PET_NULL: SkyblockId = SkyblockId("null_pet")
+ private val illlegalPathRegex = "[^a-z0-9_.-/]".toRegex()
+ }
+}
+
+val NEUItem.skyblockId get() = SkyblockId(skyblockItemId)
+
+@Serializable
+data class HypixelPetInfo(
+ val type: String,
+ val tier: Rarity,
+ val exp: Double = 0.0,
+ val candyUsed: Int = 0,
+ val uuid: UUID? = null,
+) {
+ val skyblockId get() = SkyblockId("${type.uppercase()};${tier.ordinal}")
+}
+
+private val jsonparser = Json { ignoreUnknownKeys = true }
+
+val ItemStack.extraAttributes: NbtCompound
+ get() {
+ val customData = get(DataComponentTypes.CUSTOM_DATA) ?: run {
+ val component = NbtComponent.of(NbtCompound())
+ set(DataComponentTypes.CUSTOM_DATA, component)
+ component
+ }
+ return customData.nbt
+ }
+
+val ItemStack.skyblockUUIDString: String?
+ get() = extraAttributes.getString("uuid")?.takeIf { it.isNotBlank() }
+
+val ItemStack.skyblockUUID: UUID?
+ get() = skyblockUUIDString?.let { UUID.fromString(it) }
+
+val ItemStack.petData: HypixelPetInfo?
+ get() {
+ val jsonString = extraAttributes.getString("petInfo")
+ if (jsonString.isNullOrBlank()) return null
+ return runCatching { jsonparser.decodeFromString<HypixelPetInfo>(jsonString) }
+ .getOrElse { return null }
+ }
+
+fun ItemStack.setSkyBlockFirmamentUiId(uiId: String) = setSkyBlockId(SkyblockId("FIRMAMENT_UI_$uiId"))
+fun ItemStack.setSkyBlockId(skyblockId: SkyblockId): ItemStack {
+ this.extraAttributes["id"] = skyblockId.neuItem
+ return this
+}
+
+val ItemStack.skyBlockId: SkyblockId?
+ get() {
+ return when (val id = extraAttributes.getString("id")) {
+ "" -> {
+ null
+ }
+
+ "PET" -> {
+ petData?.skyblockId ?: SkyblockId.PET_NULL
+ }
+
+ "RUNE", "UNIQUE_RUNE" -> {
+ val runeData = extraAttributes.getCompound("runes")
+ val runeKind = runeData.keys.singleOrNull()
+ if (runeKind == null) SkyblockId("RUNE")
+ else SkyblockId("${runeKind.uppercase()}_RUNE;${runeData.getInt(runeKind)}")
+ }
+
+ "ABICASE" -> {
+ SkyblockId("ABICASE_${extraAttributes.getString("model").uppercase()}")
+ }
+
+ "ENCHANTED_BOOK" -> {
+ val enchantmentData = extraAttributes.getCompound("enchantments")
+ val enchantName = enchantmentData.keys.singleOrNull()
+ if (enchantName == null) SkyblockId("ENCHANTED_BOOK")
+ else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName)}")
+ }
+
+ // TODO: PARTY_HAT_CRAB{,_ANIMATED,_SLOTH},POTION
+ else -> {
+ SkyblockId(id)
+ }
+ }
+ }
+
diff --git a/src/main/kotlin/util/SortedMapSerializer.kt b/src/main/kotlin/util/SortedMapSerializer.kt
new file mode 100644
index 0000000..baa10ad
--- /dev/null
+++ b/src/main/kotlin/util/SortedMapSerializer.kt
@@ -0,0 +1,25 @@
+
+
+package moe.nea.firmament.util
+
+import java.util.SortedMap
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+class SortedMapSerializer<K : Comparable<K>, V>(val keyDelegate: KSerializer<K>, val valueDelegate: KSerializer<V>) :
+ KSerializer<SortedMap<K, V>> {
+ val mapSerializer = MapSerializer(keyDelegate, valueDelegate)
+ override val descriptor: SerialDescriptor
+ get() = mapSerializer.descriptor
+
+ override fun deserialize(decoder: Decoder): SortedMap<K, V> {
+ return (mapSerializer.deserialize(decoder).toSortedMap(Comparator.naturalOrder()))
+ }
+
+ override fun serialize(encoder: Encoder, value: SortedMap<K, V>) {
+ mapSerializer.serialize(encoder, value)
+ }
+}
diff --git a/src/main/kotlin/util/TemplateUtil.kt b/src/main/kotlin/util/TemplateUtil.kt
new file mode 100644
index 0000000..11100e9
--- /dev/null
+++ b/src/main/kotlin/util/TemplateUtil.kt
@@ -0,0 +1,85 @@
+
+
+package moe.nea.firmament.util
+
+import java.util.*
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.SerializationStrategy
+import kotlinx.serialization.serializer
+import moe.nea.firmament.Firmament
+
+object TemplateUtil {
+
+ @JvmStatic
+ fun getTemplatePrefix(data: String): String? {
+ val decoded = maybeFromBase64Encoded(data) ?: return null
+ return decoded.replaceAfter("/", "", "").ifBlank { null }
+ }
+
+ @JvmStatic
+ fun intoBase64Encoded(raw: String): String {
+ return Base64.getEncoder().encodeToString(raw.encodeToByteArray())
+ }
+
+ private val base64Alphabet = charArrayOf(
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', '='
+ )
+
+ @JvmStatic
+ fun maybeFromBase64Encoded(raw: String): String? {
+ val raw = raw.trim()
+ if (raw.any { it !in base64Alphabet }) {
+ return null
+ }
+ return try {
+ Base64.getDecoder().decode(raw).decodeToString()
+ } catch (ex: Exception) {
+ null
+ }
+ }
+
+
+ /**
+ * Returns a base64 encoded string, truncated such that for all `x`, `x.startsWith(prefix)` implies
+ * `base64Encoded(x).startsWith(getPrefixComparisonSafeBase64Encoding(prefix))`
+ * (however, the inverse may not always be true).
+ */
+ @JvmStatic
+ fun getPrefixComparisonSafeBase64Encoding(prefix: String): String {
+ val rawEncoded =
+ Base64.getEncoder().encodeToString(prefix.encodeToByteArray())
+ .replace("=", "")
+ return rawEncoded.substring(0, rawEncoded.length - rawEncoded.length % 4)
+ }
+
+ inline fun <reified T> encodeTemplate(sharePrefix: String, data: T): String =
+ encodeTemplate(sharePrefix, data, serializer())
+
+ fun <T> encodeTemplate(sharePrefix: String, data: T, serializer: SerializationStrategy<T>): String {
+ require(sharePrefix.endsWith("/"))
+ return intoBase64Encoded(sharePrefix + Firmament.json.encodeToString(serializer, data))
+ }
+
+ inline fun <reified T : Any> maybeDecodeTemplate(sharePrefix: String, data: String): T? =
+ maybeDecodeTemplate(sharePrefix, data, serializer())
+
+ fun <T : Any> maybeDecodeTemplate(sharePrefix: String, data: String, serializer: DeserializationStrategy<T>): T? {
+ require(sharePrefix.endsWith("/"))
+ val data = data.trim()
+ if (!data.startsWith(getPrefixComparisonSafeBase64Encoding(sharePrefix)))
+ return null
+ val decoded = maybeFromBase64Encoded(data) ?: return null
+ if (!decoded.startsWith(sharePrefix))
+ return null
+ return try {
+ Firmament.json.decodeFromString<T>(serializer, decoded.substring(sharePrefix.length))
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+}
diff --git a/src/main/kotlin/util/TimeMark.kt b/src/main/kotlin/util/TimeMark.kt
new file mode 100644
index 0000000..1264212
--- /dev/null
+++ b/src/main/kotlin/util/TimeMark.kt
@@ -0,0 +1,44 @@
+
+
+package moe.nea.firmament.util
+
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+
+class TimeMark private constructor(private val timeMark: Long) : Comparable<TimeMark> {
+ fun passedTime() = if (timeMark == 0L) Duration.INFINITE else (System.currentTimeMillis() - timeMark).milliseconds
+
+ operator fun minus(other: TimeMark): Duration {
+ if (other.timeMark == timeMark)
+ return 0.milliseconds
+ if (other.timeMark == 0L)
+ return Duration.INFINITE
+ if (timeMark == 0L)
+ return -Duration.INFINITE
+ return (timeMark - other.timeMark).milliseconds
+ }
+
+ companion object {
+ fun now() = TimeMark(System.currentTimeMillis())
+ fun farPast() = TimeMark(0L)
+ fun ago(timeDelta: Duration): TimeMark {
+ if (timeDelta.isFinite()) {
+ return TimeMark(System.currentTimeMillis() - timeDelta.inWholeMilliseconds)
+ }
+ require(timeDelta.isPositive())
+ return farPast()
+ }
+ }
+
+ override fun hashCode(): Int {
+ return timeMark.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is TimeMark && other.timeMark == timeMark
+ }
+
+ override fun compareTo(other: TimeMark): Int {
+ return this.timeMark.compareTo(other.timeMark)
+ }
+}
diff --git a/src/main/kotlin/util/Timer.kt b/src/main/kotlin/util/Timer.kt
new file mode 100644
index 0000000..6e9b467
--- /dev/null
+++ b/src/main/kotlin/util/Timer.kt
@@ -0,0 +1,25 @@
+
+
+package moe.nea.firmament.util
+
+import kotlin.time.Duration
+import kotlin.time.ExperimentalTime
+import kotlin.time.TimeSource
+
+@OptIn(ExperimentalTime::class)
+class Timer {
+ private var mark: TimeSource.Monotonic.ValueTimeMark? = null
+
+ fun timePassed(): Duration {
+ return mark?.elapsedNow() ?: Duration.INFINITE
+ }
+
+ fun markNow() {
+ mark = TimeSource.Monotonic.markNow()
+ }
+
+ fun markFarPast() {
+ mark = null
+ }
+
+}
diff --git a/src/main/kotlin/util/WarpUtil.kt b/src/main/kotlin/util/WarpUtil.kt
new file mode 100644
index 0000000..8fca6f3
--- /dev/null
+++ b/src/main/kotlin/util/WarpUtil.kt
@@ -0,0 +1,75 @@
+
+package moe.nea.firmament.util
+
+import io.github.moulberry.repo.constants.Islands
+import io.github.moulberry.repo.constants.Islands.Warp
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
+import kotlin.math.sqrt
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.text.Text
+import net.minecraft.util.math.Position
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.data.ProfileSpecificDataHolder
+
+object WarpUtil {
+ val warps: List<Islands.Warp> get() = RepoManager.neuRepo.constants.islands.warps
+
+ @Serializable
+ data class Data(
+ val excludedWarps: MutableSet<String> = mutableSetOf(),
+ )
+
+ object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "warp-util", ::Data)
+
+ private var lastAttemptedWarp = ""
+ private var lastWarpAttempt = TimeMark.farPast()
+ fun findNearestWarp(island: SkyBlockIsland, pos: Position): Islands.Warp? {
+ return warps.asSequence().filter { it.mode == island.locrawMode }.minByOrNull {
+ if (DConfig.data?.excludedWarps?.contains(it.warp) == true) {
+ return@minByOrNull Double.MAX_VALUE
+ } else {
+ return@minByOrNull squaredDist(pos, it)
+ }
+ }
+ }
+
+ private fun squaredDist(pos: Position, warp: Warp): Double {
+ val dx = pos.x - warp.x
+ val dy = pos.y - warp.y
+ val dz = pos.z - warp.z
+ return dx * dx + dy * dy + dz * dz
+ }
+
+ fun teleportToNearestWarp(island: SkyBlockIsland, pos: Position) {
+ val nearestWarp = findNearestWarp(island, pos)
+ if (nearestWarp == null) {
+ MC.sendChat(Text.literal("Could not find an unlocked warp in ${island.userFriendlyName}"))
+ return
+ }
+ if (island == SBData.skyblockLocation
+ && sqrt(squaredDist(pos, nearestWarp)) > 1.1 * sqrt(squaredDist((MC.player ?: return).pos, nearestWarp))
+ ) {
+ return
+ }
+ MC.sendServerCommand("warp ${nearestWarp.warp}")
+ }
+
+ init {
+ ProcessChatEvent.subscribe {
+ if (it.unformattedString == "You haven't unlocked this fast travel destination!"
+ && lastWarpAttempt.passedTime() < 2.seconds
+ ) {
+ DConfig.data?.excludedWarps?.add(lastAttemptedWarp)
+ DConfig.markDirty()
+ MC.sendChat(Text.stringifiedTranslatable("firmament.warp-util.mark-excluded", lastAttemptedWarp))
+ lastWarpAttempt = TimeMark.farPast()
+ }
+ if (it.unformattedString == "You may now fast travel to") {
+ DConfig.data?.excludedWarps?.clear()
+ DConfig.markDirty()
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/util/assertions.kt b/src/main/kotlin/util/assertions.kt
new file mode 100644
index 0000000..6f2ed19
--- /dev/null
+++ b/src/main/kotlin/util/assertions.kt
@@ -0,0 +1,25 @@
+
+
+package moe.nea.firmament.util
+
+/**
+ * Less aggressive version of `require(obj != null)`, which fails in devenv but continues at runtime.
+ */
+inline fun <T : Any> assertNotNullOr(obj: T?, message: String? = null, block: () -> T): T {
+ if (message == null)
+ assert(obj != null)
+ else
+ assert(obj != null) { message }
+ return obj ?: block()
+}
+
+
+/**
+ * Less aggressive version of `require(condition)`, which fails in devenv but continues at runtime.
+ */
+inline fun assertTrueOr(condition: Boolean, block: () -> Unit) {
+ assert(condition)
+ if (!condition) block()
+}
+
+
diff --git a/src/main/kotlin/util/async/input.kt b/src/main/kotlin/util/async/input.kt
new file mode 100644
index 0000000..9aab5cf
--- /dev/null
+++ b/src/main/kotlin/util/async/input.kt
@@ -0,0 +1,47 @@
+
+
+package moe.nea.firmament.util.async
+
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.keybindings.IKeyBinding
+
+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 { 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()
+ }
+}
+
+
diff --git a/src/main/kotlin/util/colorconversion.kt b/src/main/kotlin/util/colorconversion.kt
new file mode 100644
index 0000000..d7a5dad
--- /dev/null
+++ b/src/main/kotlin/util/colorconversion.kt
@@ -0,0 +1,13 @@
+
+
+package moe.nea.firmament.util
+
+import net.minecraft.text.TextColor
+import net.minecraft.util.DyeColor
+
+fun DyeColor.toShedaniel(): me.shedaniel.math.Color =
+ me.shedaniel.math.Color.ofOpaque(this.signColor)
+
+fun DyeColor.toTextColor(): TextColor =
+ TextColor.fromRgb(this.signColor)
+
diff --git a/src/main/kotlin/util/customgui/CoordRememberingSlot.kt b/src/main/kotlin/util/customgui/CoordRememberingSlot.kt
new file mode 100644
index 0000000..c61c711
--- /dev/null
+++ b/src/main/kotlin/util/customgui/CoordRememberingSlot.kt
@@ -0,0 +1,14 @@
+
+package moe.nea.firmament.util.customgui
+
+import net.minecraft.screen.slot.Slot
+
+interface CoordRememberingSlot {
+ fun rememberCoords_firmament()
+ fun restoreCoords_firmament()
+ fun getOriginalX_firmament(): Int
+ fun getOriginalY_firmament(): Int
+}
+
+val Slot.originalX get() = (this as CoordRememberingSlot).getOriginalX_firmament()
+val Slot.originalY get() = (this as CoordRememberingSlot).getOriginalY_firmament()
diff --git a/src/main/kotlin/util/customgui/CustomGui.kt b/src/main/kotlin/util/customgui/CustomGui.kt
new file mode 100644
index 0000000..f9094b2
--- /dev/null
+++ b/src/main/kotlin/util/customgui/CustomGui.kt
@@ -0,0 +1,72 @@
+
+package moe.nea.firmament.util.customgui
+
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.screen.slot.Slot
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HandledScreenPushREIEvent
+
+abstract class CustomGui {
+
+ abstract fun getBounds(): List<Rectangle>
+
+ open fun moveSlot(slot: Slot) {
+ // TODO: return a Pair maybe? worth an investigation
+ }
+
+ companion object {
+ @Subscribe
+ fun onExclusionZone(event: HandledScreenPushREIEvent) {
+ val customGui = event.screen.customGui ?: return
+ event.rectangles.addAll(customGui.getBounds())
+ }
+ }
+
+ open fun render(
+ drawContext: DrawContext,
+ delta: Float,
+ mouseX: Int,
+ mouseY: Int
+ ) {
+ }
+
+ open fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ return false
+ }
+
+ open fun afterSlotRender(context: DrawContext, slot: Slot) {}
+ open fun beforeSlotRender(context: DrawContext, slot: Slot) {}
+ open fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
+ return false
+ }
+
+ open fun isClickOutsideBounds(mouseX: Double, mouseY: Double): Boolean {
+ return getBounds().none { it.contains(mouseX, mouseY) }
+ }
+
+ open fun isPointWithinBounds(
+ x: Int,
+ y: Int,
+ width: Int,
+ height: Int,
+ pointX: Double,
+ pointY: Double,
+ ): Boolean {
+ return getBounds().any { it.contains(pointX, pointY) } &&
+ Rectangle(x, y, width, height).contains(pointX, pointY)
+ }
+
+ open fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean {
+ return isPointWithinBounds(slot.x + xOffset, slot.y + yOffset, 16, 16, pointX, pointY)
+ }
+
+ open fun onInit() {}
+ open fun shouldDrawForeground(): Boolean {
+ return true
+ }
+
+ open fun onVoluntaryExit(): Boolean {
+ return true
+ }
+}
diff --git a/src/main/kotlin/util/customgui/HasCustomGui.kt b/src/main/kotlin/util/customgui/HasCustomGui.kt
new file mode 100644
index 0000000..edead2e
--- /dev/null
+++ b/src/main/kotlin/util/customgui/HasCustomGui.kt
@@ -0,0 +1,17 @@
+
+package moe.nea.firmament.util.customgui
+
+import net.minecraft.client.gui.screen.ingame.HandledScreen
+
+@Suppress("FunctionName")
+interface HasCustomGui {
+ fun getCustomGui_Firmament(): CustomGui?
+ fun setCustomGui_Firmament(gui: CustomGui?)
+}
+
+var <T : HandledScreen<*>> T.customGui: CustomGui?
+ get() = (this as HasCustomGui).getCustomGui_Firmament()
+ set(value) {
+ (this as HasCustomGui).setCustomGui_Firmament(value)
+ }
+
diff --git a/src/main/kotlin/util/data/DataHolder.kt b/src/main/kotlin/util/data/DataHolder.kt
new file mode 100644
index 0000000..21a6014
--- /dev/null
+++ b/src/main/kotlin/util/data/DataHolder.kt
@@ -0,0 +1,62 @@
+
+
+package moe.nea.firmament.util.data
+
+import java.nio.file.Path
+import kotlinx.serialization.KSerializer
+import kotlin.io.path.exists
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+import moe.nea.firmament.Firmament
+
+abstract class DataHolder<T>(
+ val serializer: KSerializer<T>,
+ val name: String,
+ val default: () -> T
+) : IDataHolder<T> {
+
+
+ final override var data: T
+ private set
+
+ init {
+ data = readValueOrDefault()
+ IDataHolder.putDataHolder(this::class, this)
+ }
+
+ private val file: Path get() = Firmament.CONFIG_DIR.resolve("$name.json")
+
+ protected fun readValueOrDefault(): T {
+ if (file.exists())
+ try {
+ return Firmament.json.decodeFromString(
+ serializer,
+ file.readText()
+ )
+ } catch (e: Exception) {/* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/
+ IDataHolder.badLoads.add(name)
+ Firmament.logger.error(
+ "Exception during loading of config file $name. This will reset this config.",
+ e
+ )
+ }
+ return default()
+ }
+
+ private fun writeValue(t: T) {
+ file.writeText(Firmament.json.encodeToString(serializer, t))
+ }
+
+ override fun save() {
+ writeValue(data)
+ }
+
+ override fun load() {
+ data = readValueOrDefault()
+ }
+
+ override fun markDirty() {
+ IDataHolder.markDirty(this::class)
+ }
+
+}
diff --git a/src/main/kotlin/util/data/IDataHolder.kt b/src/main/kotlin/util/data/IDataHolder.kt
new file mode 100644
index 0000000..5d09bcd
--- /dev/null
+++ b/src/main/kotlin/util/data/IDataHolder.kt
@@ -0,0 +1,77 @@
+
+
+package moe.nea.firmament.util.data
+
+import java.util.concurrent.CopyOnWriteArrayList
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
+import kotlin.reflect.KClass
+import net.minecraft.client.MinecraftClient
+import net.minecraft.server.command.CommandOutput
+import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.events.ScreenChangeEvent
+
+interface IDataHolder<T> {
+ companion object {
+ internal var badLoads: MutableList<String> = CopyOnWriteArrayList()
+ private val allConfigs: MutableMap<KClass<out IDataHolder<*>>, IDataHolder<*>> = mutableMapOf()
+ private val dirty: MutableSet<KClass<out IDataHolder<*>>> = mutableSetOf()
+
+ internal fun <T : IDataHolder<K>, K> putDataHolder(kClass: KClass<T>, inst: IDataHolder<K>) {
+ allConfigs[kClass] = inst
+ }
+
+ fun <T : IDataHolder<K>, K> markDirty(kClass: KClass<T>) {
+ if (kClass !in allConfigs) {
+ Firmament.logger.error("Tried to markDirty '${kClass.qualifiedName}', which isn't registered as 'IConfigHolder'")
+ return
+ }
+ dirty.add(kClass)
+ }
+
+ private fun performSaves() {
+ val toSave = dirty.toList().also {
+ dirty.clear()
+ }
+ for (it in toSave) {
+ val obj = allConfigs[it]
+ if (obj == null) {
+ Firmament.logger.error("Tried to save '${it}', which isn't registered as 'ConfigHolder'")
+ continue
+ }
+ obj.save()
+ }
+ }
+
+ private fun warnForResetConfigs(player: CommandOutput) {
+ if (badLoads.isNotEmpty()) {
+ player.sendMessage(
+ Text.literal(
+ "The following configs have been reset: ${badLoads.joinToString(", ")}. " +
+ "This can be intentional, but probably isn't."
+ )
+ )
+ badLoads.clear()
+ }
+ }
+
+ fun registerEvents() {
+ ScreenChangeEvent.subscribe { event ->
+ performSaves()
+ val p = MinecraftClient.getInstance().player
+ if (p != null) {
+ warnForResetConfigs(p)
+ }
+ }
+ ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping {
+ performSaves()
+ })
+ }
+
+ }
+
+ val data: T
+ fun save()
+ fun markDirty()
+ fun load()
+}
diff --git a/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt b/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt
new file mode 100644
index 0000000..1cd4f22
--- /dev/null
+++ b/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt
@@ -0,0 +1,84 @@
+
+
+package moe.nea.firmament.util.data
+
+import java.nio.file.Path
+import java.util.UUID
+import kotlinx.serialization.KSerializer
+import kotlin.io.path.createDirectories
+import kotlin.io.path.deleteExisting
+import kotlin.io.path.exists
+import kotlin.io.path.extension
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.SBData
+
+abstract class ProfileSpecificDataHolder<S>(
+ private val dataSerializer: KSerializer<S>,
+ val configName: String,
+ private val configDefault: () -> S
+) : IDataHolder<S?> {
+
+ var allConfigs: MutableMap<UUID, S>
+
+ override val data: S?
+ get() = SBData.profileId?.let {
+ allConfigs.computeIfAbsent(it) { configDefault() }
+ }
+
+ init {
+ allConfigs = readValues()
+ IDataHolder.putDataHolder(this::class, this)
+ }
+
+ private val configDirectory: Path get() = Firmament.CONFIG_DIR.resolve("profiles").resolve(configName)
+
+ private fun readValues(): MutableMap<UUID, S> {
+ if (!configDirectory.exists()) {
+ configDirectory.createDirectories()
+ }
+ val profileFiles = configDirectory.listDirectoryEntries()
+ return profileFiles
+ .filter { it.extension == "json" }
+ .mapNotNull {
+ try {
+ UUID.fromString(it.nameWithoutExtension) to Firmament.json.decodeFromString(dataSerializer, it.readText())
+ } catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/
+ IDataHolder.badLoads.add(configName)
+ Firmament.logger.error(
+ "Exception during loading of profile specific config file $it ($configName). This will reset that profiles config.",
+ e
+ )
+ null
+ }
+ }.toMap().toMutableMap()
+ }
+
+ override fun save() {
+ if (!configDirectory.exists()) {
+ configDirectory.createDirectories()
+ }
+ val c = allConfigs
+ configDirectory.listDirectoryEntries().forEach {
+ if (it.nameWithoutExtension !in c.mapKeys { it.toString() }) {
+ it.deleteExisting()
+ }
+ }
+ c.forEach { (name, value) ->
+ val f = configDirectory.resolve("$name.json")
+ f.writeText(Firmament.json.encodeToString(dataSerializer, value))
+ }
+ }
+
+ override fun markDirty() {
+ IDataHolder.markDirty(this::class)
+ }
+
+ override fun load() {
+ allConfigs = readValues()
+ }
+
+}
diff --git a/src/main/kotlin/util/filter/IteratorFilterSet.kt b/src/main/kotlin/util/filter/IteratorFilterSet.kt
new file mode 100644
index 0000000..483b8d9
--- /dev/null
+++ b/src/main/kotlin/util/filter/IteratorFilterSet.kt
@@ -0,0 +1,33 @@
+
+package moe.nea.firmament.util.filter
+
+abstract class IteratorFilterSet<K>(val original: java.util.Set<K>) : java.util.Set<K> by original {
+ abstract fun shouldKeepElement(element: K): Boolean
+
+ override fun iterator(): MutableIterator<K> {
+ val parentIterator = original.iterator()
+ return object : MutableIterator<K> {
+ var lastEntry: K? = null
+ override fun hasNext(): Boolean {
+ while (lastEntry == null) {
+ if (!parentIterator.hasNext())
+ break
+ val element = parentIterator.next()
+ if (!shouldKeepElement(element)) continue
+ lastEntry = element
+ }
+ return lastEntry != null
+ }
+
+ override fun next(): K {
+ if (!hasNext()) throw NoSuchElementException()
+ return lastEntry ?: throw NoSuchElementException()
+ }
+
+ override fun remove() {
+ TODO("Not yet implemented")
+ }
+ }
+ }
+}
+
diff --git a/src/main/kotlin/util/item/NbtItemData.kt b/src/main/kotlin/util/item/NbtItemData.kt
new file mode 100644
index 0000000..f7f259d
--- /dev/null
+++ b/src/main/kotlin/util/item/NbtItemData.kt
@@ -0,0 +1,24 @@
+
+
+package moe.nea.firmament.util.item
+
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.component.type.LoreComponent
+import net.minecraft.item.ItemStack
+import net.minecraft.text.Text
+
+var ItemStack.loreAccordingToNbt
+ get() = get(DataComponentTypes.LORE)?.lines ?: listOf()
+ set(value) {
+ set(DataComponentTypes.LORE, LoreComponent(value))
+ }
+
+var ItemStack.displayNameAccordingToNbt: Text
+ get() = get(DataComponentTypes.CUSTOM_NAME) ?: get(DataComponentTypes.ITEM_NAME) ?: item.name
+ set(value) {
+ set(DataComponentTypes.CUSTOM_NAME, value)
+ }
+
+fun ItemStack.setCustomName(text: Text) {
+ set(DataComponentTypes.CUSTOM_NAME, text)
+}
diff --git a/src/main/kotlin/util/item/SkullItemData.kt b/src/main/kotlin/util/item/SkullItemData.kt
new file mode 100644
index 0000000..ddab88e
--- /dev/null
+++ b/src/main/kotlin/util/item/SkullItemData.kt
@@ -0,0 +1,90 @@
+
+
+@file:UseSerializers(DashlessUUIDSerializer::class, InstantAsLongSerializer::class)
+
+package moe.nea.firmament.util.item
+
+import com.mojang.authlib.GameProfile
+import com.mojang.authlib.minecraft.MinecraftProfileTexture
+import com.mojang.authlib.properties.Property
+import java.util.UUID
+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
+import net.minecraft.item.Items
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.Base64Util.padToValidBase64
+import moe.nea.firmament.util.assertTrueOr
+import moe.nea.firmament.util.json.DashlessUUIDSerializer
+import moe.nea.firmament.util.json.InstantAsLongSerializer
+
+@Serializable
+data class MinecraftProfileTextureKt(
+ val url: String,
+ val metadata: Map<String, String> = mapOf(),
+)
+
+@Serializable
+data class MinecraftTexturesPayloadKt(
+ val textures: Map<MinecraftProfileTexture.Type, MinecraftProfileTextureKt> = mapOf(),
+ val profileId: UUID? = null,
+ val profileName: String? = null,
+ val isPublic: Boolean = true,
+ val timestamp: Instant = Clock.System.now(),
+)
+
+fun GameProfile.setTextures(textures: MinecraftTexturesPayloadKt) {
+ val json = Firmament.json.encodeToString(textures)
+ val encoded = java.util.Base64.getEncoder().encodeToString(json.encodeToByteArray())
+ properties.put(propertyTextures, Property(propertyTextures, encoded))
+}
+
+private val propertyTextures = "textures"
+
+fun ItemStack.setEncodedSkullOwner(uuid: UUID, encodedData: String) {
+ assert(this.item == Items.PLAYER_HEAD)
+ val gameProfile = GameProfile(uuid, "LameGuy123")
+ gameProfile.properties.put(propertyTextures, Property(propertyTextures, encodedData.padToValidBase64()))
+ this.set(DataComponentTypes.PROFILE, ProfileComponent(gameProfile))
+}
+
+val zeroUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1")
+fun createSkullItem(uuid: UUID, url: String) = ItemStack(Items.PLAYER_HEAD)
+ .also { it.setSkullOwner(uuid, url) }
+
+fun ItemStack.setSkullOwner(uuid: UUID, url: String) {
+ assert(this.item == Items.PLAYER_HEAD)
+ val gameProfile = GameProfile(uuid, "nea89")
+ gameProfile.setTextures(
+ MinecraftTexturesPayloadKt(
+ textures = mapOf(MinecraftProfileTexture.Type.SKIN to MinecraftProfileTextureKt(url)),
+ profileId = uuid,
+ profileName = "nea89",
+ )
+ )
+ this.set(DataComponentTypes.PROFILE, ProfileComponent(gameProfile))
+}
+
+
+fun decodeProfileTextureProperty(property: Property): MinecraftTexturesPayloadKt? {
+ assertTrueOr(property.name == propertyTextures) { return null }
+ return try {
+ var encodedF: String = property.value
+ while (encodedF.length % 4 != 0 && encodedF.last() == '=') {
+ encodedF = encodedF.substring(0, encodedF.length - 1)
+ }
+ val json = java.util.Base64.getDecoder().decode(encodedF).decodeToString()
+ Firmament.json.decodeFromString<MinecraftTexturesPayloadKt>(json)
+ } catch (e: Exception) {
+ // Malformed profile data
+ if (Firmament.DEBUG)
+ e.printStackTrace()
+ null
+ }
+}
+
diff --git a/src/main/kotlin/util/json/BlockPosSerializer.kt b/src/main/kotlin/util/json/BlockPosSerializer.kt
new file mode 100644
index 0000000..144b0a0
--- /dev/null
+++ b/src/main/kotlin/util/json/BlockPosSerializer.kt
@@ -0,0 +1,25 @@
+package moe.nea.firmament.util.json
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.serializer
+import net.minecraft.util.math.BlockPos
+
+object BlockPosSerializer : KSerializer<BlockPos> {
+ val delegate = serializer<List<Int>>()
+
+ override val descriptor: SerialDescriptor
+ get() = SerialDescriptor("BlockPos", delegate.descriptor)
+
+ override fun deserialize(decoder: Decoder): BlockPos {
+ val list = decoder.decodeSerializableValue(delegate)
+ require(list.size == 3)
+ return BlockPos(list[0], list[1], list[2])
+ }
+
+ override fun serialize(encoder: Encoder, value: BlockPos) {
+ encoder.encodeSerializableValue(delegate, listOf(value.x, value.y, value.z))
+ }
+}
diff --git a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
new file mode 100644
index 0000000..acb1dc8
--- /dev/null
+++ b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
@@ -0,0 +1,29 @@
+
+
+package moe.nea.firmament.util.json
+
+import java.util.UUID
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import moe.nea.firmament.util.parseDashlessUUID
+
+object DashlessUUIDSerializer : KSerializer<UUID> {
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("DashlessUUIDSerializer", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): UUID {
+ val str = decoder.decodeString()
+ if ("-" in str) {
+ return UUID.fromString(str)
+ }
+ return parseDashlessUUID(str)
+ }
+
+ override fun serialize(encoder: Encoder, value: UUID) {
+ encoder.encodeString(value.toString().replace("-", ""))
+ }
+}
diff --git a/src/main/kotlin/util/json/InstantAsLongSerializer.kt b/src/main/kotlin/util/json/InstantAsLongSerializer.kt
new file mode 100644
index 0000000..ad738dc
--- /dev/null
+++ b/src/main/kotlin/util/json/InstantAsLongSerializer.kt
@@ -0,0 +1,22 @@
+
+
+package moe.nea.firmament.util.json
+
+import kotlinx.datetime.Instant
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+object InstantAsLongSerializer : KSerializer<Instant> {
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("InstantAsLongSerializer", PrimitiveKind.LONG)
+ override fun deserialize(decoder: Decoder): Instant {
+ return Instant.fromEpochMilliseconds(decoder.decodeLong())
+ }
+
+ override fun serialize(encoder: Encoder, value: Instant) {
+ encoder.encodeLong(value.toEpochMilliseconds())
+ }
+}
diff --git a/src/main/kotlin/util/json/SingletonSerializableList.kt b/src/main/kotlin/util/json/SingletonSerializableList.kt
new file mode 100644
index 0000000..aa543d6
--- /dev/null
+++ b/src/main/kotlin/util/json/SingletonSerializableList.kt
@@ -0,0 +1,31 @@
+
+package moe.nea.firmament.util.json
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.builtins.ListSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+
+class SingletonSerializableList<T>(val child: KSerializer<T>) : KSerializer<List<T>> {
+ override val descriptor: SerialDescriptor
+ get() = JsonElement.serializer().descriptor
+
+ override fun deserialize(decoder: Decoder): List<T> {
+ decoder as JsonDecoder
+ val list = JsonElement.serializer().deserialize(decoder)
+ if (list is JsonArray) {
+ return list.map {
+ decoder.json.decodeFromJsonElement(child, it)
+ }
+ }
+ return listOf(decoder.json.decodeFromJsonElement(child, list))
+ }
+
+ override fun serialize(encoder: Encoder, value: List<T>) {
+ ListSerializer(child).serialize(encoder, value)
+ }
+}
diff --git a/src/main/kotlin/util/listutil.kt b/src/main/kotlin/util/listutil.kt
new file mode 100644
index 0000000..73cb23e
--- /dev/null
+++ b/src/main/kotlin/util/listutil.kt
@@ -0,0 +1,9 @@
+
+package moe.nea.firmament.util
+
+fun <T, R> List<T>.lastNotNullOfOrNull(func: (T) -> R?): R? {
+ for (i in indices.reversed()) {
+ return func(this[i]) ?: continue
+ }
+ return null
+}
diff --git a/src/main/kotlin/util/propertyutil.kt b/src/main/kotlin/util/propertyutil.kt
new file mode 100644
index 0000000..795a0d2
--- /dev/null
+++ b/src/main/kotlin/util/propertyutil.kt
@@ -0,0 +1,9 @@
+
+
+package moe.nea.firmament.util
+
+import kotlin.properties.ReadOnlyProperty
+
+fun <T, V, M> ReadOnlyProperty<T, V>.map(mapper: (V) -> M): ReadOnlyProperty<T, M> {
+ return ReadOnlyProperty { thisRef, property -> mapper(this@map.getValue(thisRef, property)) }
+}
diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt
new file mode 100644
index 0000000..3ce5bd8
--- /dev/null
+++ b/src/main/kotlin/util/regex.kt
@@ -0,0 +1,55 @@
+
+
+package moe.nea.firmament.util
+
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+import org.intellij.lang.annotations.Language
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+inline fun <T> String.ifMatches(regex: Regex, block: (MatchResult) -> T): T? =
+ regex.matchEntire(this)?.let(block)
+
+inline fun <T> Pattern.useMatch(string: String, block: Matcher.() -> T): T? =
+ matcher(string)
+ .takeIf(Matcher::matches)
+ ?.let(block)
+
+@Language("RegExp")
+val TIME_PATTERN = "[0-9]+[ms]"
+
+@Language("RegExp")
+val SHORT_NUMBER_FORMAT = "[0-9]+(?:,[0-9]+)*(?:\\.[0-9]+)?[kKmMbB]?"
+
+
+val siScalars = mapOf(
+ 'k' to 1_000.0,
+ 'K' to 1_000.0,
+ 'm' to 1_000_000.0,
+ 'M' to 1_000_000.0,
+ 'b' to 1_000_000_000.0,
+ 'B' to 1_000_000_000.0,
+)
+
+fun parseTimePattern(text: String): Duration {
+ val length = text.dropLast(1).toInt()
+ return when (text.last()) {
+ 'm' -> length.minutes
+ 's' -> length.seconds
+ else -> error("Invalid pattern for time $text")
+ }
+}
+
+fun parseShortNumber(string: String): Double {
+ var k = string.replace(",", "")
+ val scalar = k.last()
+ var scalarMultiplier = siScalars[scalar]
+ if (scalarMultiplier == null) {
+ scalarMultiplier = 1.0
+ } else {
+ k = k.dropLast(1)
+ }
+ return k.toDouble() * scalarMultiplier
+}
diff --git a/src/main/kotlin/util/render/FacingThePlayerContext.kt b/src/main/kotlin/util/render/FacingThePlayerContext.kt
new file mode 100644
index 0000000..eb37e35
--- /dev/null
+++ b/src/main/kotlin/util/render/FacingThePlayerContext.kt
@@ -0,0 +1,101 @@
+
+package moe.nea.firmament.util.render
+
+import com.mojang.blaze3d.systems.RenderSystem
+import io.github.notenoughupdates.moulconfig.platform.next
+import org.joml.Matrix4f
+import net.minecraft.client.font.TextRenderer
+import net.minecraft.client.render.BufferRenderer
+import net.minecraft.client.render.GameRenderer
+import net.minecraft.client.render.LightmapTextureManager
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.render.Tessellator
+import net.minecraft.client.render.VertexConsumer
+import net.minecraft.client.render.VertexFormat
+import net.minecraft.client.render.VertexFormats
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.assertTrueOr
+
+@RenderContextDSL
+class FacingThePlayerContext(val worldContext: RenderInWorldContext) {
+ val matrixStack by worldContext::matrixStack
+ fun waypoint(position: BlockPos, label: Text) {
+ text(
+ label,
+ Text.literal("§e${FirmFormatters.formatDistance(MC.player?.pos?.distanceTo(position.toCenterPos()) ?: 42069.0)}")
+ )
+ }
+
+ fun text(
+ vararg texts: Text,
+ verticalAlign: RenderInWorldContext.VerticalAlign = RenderInWorldContext.VerticalAlign.CENTER,
+ background: Int = 0x70808080,
+ ) {
+ assertTrueOr(texts.isNotEmpty()) { return@text }
+ for ((index, text) in texts.withIndex()) {
+ worldContext.matrixStack.push()
+ val width = MC.font.getWidth(text)
+ worldContext.matrixStack.translate(-width / 2F, verticalAlign.align(index, texts.size), 0F)
+ val vertexConsumer: VertexConsumer =
+ worldContext.vertexConsumers.getBuffer(RenderLayer.getTextBackgroundSeeThrough())
+ val matrix4f = worldContext.matrixStack.peek().positionMatrix
+ vertexConsumer.vertex(matrix4f, -1.0f, -1.0f, 0.0f).color(background)
+ .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next()
+ vertexConsumer.vertex(matrix4f, -1.0f, MC.font.fontHeight.toFloat(), 0.0f).color(background)
+ .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next()
+ vertexConsumer.vertex(matrix4f, width.toFloat(), MC.font.fontHeight.toFloat(), 0.0f)
+ .color(background)
+ .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next()
+ vertexConsumer.vertex(matrix4f, width.toFloat(), -1.0f, 0.0f).color(background)
+ .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next()
+ worldContext.matrixStack.translate(0F, 0F, 0.01F)
+
+ MC.font.draw(
+ text,
+ 0F,
+ 0F,
+ -1,
+ false,
+ worldContext.matrixStack.peek().positionMatrix,
+ worldContext.vertexConsumers,
+ TextRenderer.TextLayerType.SEE_THROUGH,
+ 0,
+ LightmapTextureManager.MAX_LIGHT_COORDINATE
+ )
+ worldContext.matrixStack.pop()
+ }
+ }
+
+
+ fun texture(
+ texture: Identifier, width: Int, height: Int,
+ u1: Float, v1: Float,
+ u2: Float, v2: Float,
+ ) {
+ RenderSystem.setShaderTexture(0, texture)
+ RenderSystem.setShader(GameRenderer::getPositionTexColorProgram)
+ val hw = width / 2F
+ val hh = height / 2F
+ val matrix4f: Matrix4f = worldContext.matrixStack.peek().positionMatrix
+ val buf = Tessellator.getInstance()
+ .begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_TEXTURE_COLOR)
+ buf.vertex(matrix4f, -hw, -hh, 0F)
+ .color(-1)
+ .texture(u1, v1).next()
+ buf.vertex(matrix4f, -hw, +hh, 0F)
+ .color(-1)
+ .texture(u1, v2).next()
+ buf.vertex(matrix4f, +hw, +hh, 0F)
+ .color(-1)
+ .texture(u2, v2).next()
+ buf.vertex(matrix4f, +hw, -hh, 0F)
+ .color(-1)
+ .texture(u2, v1).next()
+ BufferRenderer.drawWithGlobalProgram(buf.end())
+ }
+
+}
diff --git a/src/main/kotlin/util/render/LerpUtils.kt b/src/main/kotlin/util/render/LerpUtils.kt
new file mode 100644
index 0000000..f2c2f25
--- /dev/null
+++ b/src/main/kotlin/util/render/LerpUtils.kt
@@ -0,0 +1,33 @@
+
+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()
+}
+
+fun lerp(a: Float, b: Float, progress: Float): Float {
+ return a + (b - a) * progress
+}
+fun lerp(a: Int, b: Int, progress: Float): Int {
+ return (a + (b - a) * progress).toInt()
+}
+
+fun ilerp(a: Float, b: Float, value: Float): Float {
+ 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),
+ )
+}
+
diff --git a/src/main/kotlin/util/render/RenderCircleProgress.kt b/src/main/kotlin/util/render/RenderCircleProgress.kt
new file mode 100644
index 0000000..a2f42b5
--- /dev/null
+++ b/src/main/kotlin/util/render/RenderCircleProgress.kt
@@ -0,0 +1,95 @@
+
+package moe.nea.firmament.util.render
+
+import com.mojang.blaze3d.systems.RenderSystem
+import io.github.notenoughupdates.moulconfig.platform.next
+import org.joml.Matrix4f
+import org.joml.Vector2f
+import kotlin.math.atan2
+import kotlin.math.tan
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.render.BufferRenderer
+import net.minecraft.client.render.GameRenderer
+import net.minecraft.client.render.Tessellator
+import net.minecraft.client.render.VertexFormat.DrawMode
+import net.minecraft.client.render.VertexFormats
+import net.minecraft.util.Identifier
+
+object RenderCircleProgress {
+
+ fun renderCircle(
+ drawContext: DrawContext,
+ texture: Identifier,
+ progress: Float,
+ u1: Float,
+ u2: Float,
+ v1: Float,
+ v2: Float,
+ ) {
+ RenderSystem.setShaderTexture(0, texture)
+ RenderSystem.setShader(GameRenderer::getPositionTexColorProgram)
+ RenderSystem.enableBlend()
+ val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix
+ val bufferBuilder = Tessellator.getInstance().begin(DrawMode.TRIANGLES, VertexFormats.POSITION_TEXTURE_COLOR)
+
+ 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),
+ )
+
+ 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)
+ }
+ }
+
+ 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)
+ .next()
+ bufferBuilder
+ .vertex(matrix, first.x, first.y, 0F)
+ .texture(lerp(u1, u2, ilerp(first.x)), lerp(v1, v2, ilerp(first.y)))
+ .color(-1)
+ .next()
+ bufferBuilder
+ .vertex(matrix, 0F, 0F, 0F)
+ .texture(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F)))
+ .color(-1)
+ .next()
+ }
+ BufferRenderer.drawWithGlobalProgram(bufferBuilder.end())
+ RenderSystem.disableBlend()
+ }
+
+
+
+}
diff --git a/src/main/kotlin/util/render/RenderContextDSL.kt b/src/main/kotlin/util/render/RenderContextDSL.kt
new file mode 100644
index 0000000..9bb4431
--- /dev/null
+++ b/src/main/kotlin/util/render/RenderContextDSL.kt
@@ -0,0 +1,6 @@
+
+package moe.nea.firmament.util.render
+
+@DslMarker
+annotation class RenderContextDSL {
+}
diff --git a/src/main/kotlin/util/render/RenderInWorldContext.kt b/src/main/kotlin/util/render/RenderInWorldContext.kt
new file mode 100644
index 0000000..7faa499
--- /dev/null
+++ b/src/main/kotlin/util/render/RenderInWorldContext.kt
@@ -0,0 +1,294 @@
+
+
+package moe.nea.firmament.util.render
+
+import com.mojang.blaze3d.systems.RenderSystem
+import io.github.notenoughupdates.moulconfig.platform.next
+import java.lang.Math.pow
+import org.joml.Matrix4f
+import org.joml.Vector3f
+import net.minecraft.client.gl.VertexBuffer
+import net.minecraft.client.render.BufferBuilder
+import net.minecraft.client.render.BufferRenderer
+import net.minecraft.client.render.Camera
+import net.minecraft.client.render.GameRenderer
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.render.RenderPhase
+import net.minecraft.client.render.RenderTickCounter
+import net.minecraft.client.render.Tessellator
+import net.minecraft.client.render.VertexConsumerProvider
+import net.minecraft.client.render.VertexFormat
+import net.minecraft.client.render.VertexFormats
+import net.minecraft.client.texture.Sprite
+import net.minecraft.client.util.math.MatrixStack
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+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.MC
+
+@RenderContextDSL
+class RenderInWorldContext private constructor(
+ private val tesselator: Tessellator,
+ val matrixStack: MatrixStack,
+ private val camera: Camera,
+ private val tickCounter: RenderTickCounter,
+ val vertexConsumers: VertexConsumerProvider.Immediate,
+) {
+
+ object RenderLayers {
+ val TRANSLUCENT_TRIS = RenderLayer.of("firmament_translucent_tris",
+ VertexFormats.POSITION_COLOR,
+ VertexFormat.DrawMode.TRIANGLES,
+ RenderLayer.DEFAULT_BUFFER_SIZE,
+ false, true,
+ RenderLayer.MultiPhaseParameters.builder()
+ .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
+ .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
+ .program(RenderPhase.COLOR_PROGRAM)
+ .build(false))
+ }
+
+ fun color(color: me.shedaniel.math.Color) {
+ color(color.red / 255F, color.green / 255f, color.blue / 255f, color.alpha / 255f)
+ }
+
+ fun color(red: Float, green: Float, blue: Float, alpha: Float) {
+ RenderSystem.setShaderColor(red, green, blue, alpha)
+ }
+
+ fun block(blockPos: BlockPos) {
+ matrixStack.push()
+ matrixStack.translate(blockPos.x.toFloat(), blockPos.y.toFloat(), blockPos.z.toFloat())
+ buildCube(matrixStack.peek().positionMatrix, tesselator)
+ matrixStack.pop()
+ }
+
+ enum class VerticalAlign {
+ TOP, BOTTOM, CENTER;
+
+ fun align(index: Int, count: Int): Float {
+ return when (this) {
+ CENTER -> (index - count / 2F) * (1 + MC.font.fontHeight.toFloat())
+ BOTTOM -> (index - count) * (1 + MC.font.fontHeight.toFloat())
+ TOP -> (index) * (1 + MC.font.fontHeight.toFloat())
+ }
+ }
+ }
+
+ fun waypoint(position: BlockPos, vararg label: Text) {
+ text(
+ position.toCenterPos(),
+ *label,
+ Text.literal("§e${FirmFormatters.formatDistance(MC.player?.pos?.distanceTo(position.toCenterPos()) ?: 42069.0)}"),
+ background = 0xAA202020.toInt()
+ )
+ }
+
+ fun withFacingThePlayer(position: Vec3d, block: FacingThePlayerContext.() -> Unit) {
+ matrixStack.push()
+ matrixStack.translate(position.x, position.y, position.z)
+ val actualCameraDistance = position.distanceTo(camera.pos)
+ val distanceToMoveTowardsCamera = if (actualCameraDistance < 10) 0.0 else -(actualCameraDistance - 10.0)
+ val vec = position.subtract(camera.pos).multiply(distanceToMoveTowardsCamera / actualCameraDistance)
+ matrixStack.translate(vec.x, vec.y, vec.z)
+ matrixStack.multiply(camera.rotation)
+ matrixStack.scale(0.025F, -0.025F, 1F)
+
+ FacingThePlayerContext(this).run(block)
+
+ matrixStack.pop()
+ vertexConsumers.drawCurrentLayer()
+ }
+
+ fun sprite(position: Vec3d, sprite: Sprite, width: Int, height: Int) {
+ texture(
+ position, sprite.atlasId, width, height, sprite.minU, sprite.minV, sprite.maxU, sprite.maxV
+ )
+ }
+
+ fun texture(
+ position: Vec3d, texture: Identifier, width: Int, height: Int,
+ u1: Float, v1: Float,
+ u2: Float, v2: Float,
+ ) {
+ withFacingThePlayer(position) {
+ texture(texture, width, height, u1, v1, u2, v2)
+ }
+ }
+
+ fun text(position: Vec3d, vararg texts: Text, verticalAlign: VerticalAlign = VerticalAlign.CENTER, background: Int = 0x70808080) {
+ withFacingThePlayer(position) {
+ text(*texts, verticalAlign = verticalAlign, background = background)
+ }
+ }
+
+ fun tinyBlock(vec3d: Vec3d, size: Float) {
+ RenderSystem.setShader(GameRenderer::getPositionColorProgram)
+ matrixStack.push()
+ matrixStack.translate(vec3d.x, vec3d.y, vec3d.z)
+ matrixStack.scale(size, size, size)
+ matrixStack.translate(-.5, -.5, -.5)
+ buildCube(matrixStack.peek().positionMatrix, tesselator)
+ matrixStack.pop()
+ }
+
+ fun wireframeCube(blockPos: BlockPos, lineWidth: Float = 10F) {
+ RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram)
+ matrixStack.push()
+ RenderSystem.lineWidth(lineWidth / pow(camera.pos.squaredDistanceTo(blockPos.toCenterPos()), 0.25).toFloat())
+ matrixStack.translate(blockPos.x.toFloat(), blockPos.y.toFloat(), blockPos.z.toFloat())
+ buildWireFrameCube(matrixStack.peek(), tesselator)
+ matrixStack.pop()
+ }
+
+ fun line(vararg points: Vec3d, lineWidth: Float = 10F) {
+ line(points.toList(), lineWidth)
+ }
+
+ fun tracer(toWhere: Vec3d, lineWidth: Float = 3f) {
+ val cameraForward = Vector3f(0f, 0f, 1f).rotate(camera.rotation)
+ line(camera.pos.add(Vec3d(cameraForward)), toWhere, lineWidth = lineWidth)
+ }
+
+ fun line(points: List<Vec3d>, lineWidth: Float = 10F) {
+ RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram)
+ RenderSystem.lineWidth(lineWidth)
+ val buffer = tesselator.begin(VertexFormat.DrawMode.LINES, VertexFormats.LINES)
+
+ val matrix = matrixStack.peek()
+ var lastNormal: Vector3f? = null
+ points.zipWithNext().forEach { (a, b) ->
+ val normal = Vector3f(b.x.toFloat(), b.y.toFloat(), b.z.toFloat())
+ .sub(a.x.toFloat(), a.y.toFloat(), a.z.toFloat())
+ .normalize()
+ val lastNormal0 = lastNormal ?: normal
+ lastNormal = normal
+ buffer.vertex(matrix.positionMatrix, a.x.toFloat(), a.y.toFloat(), a.z.toFloat())
+ .color(-1)
+ .normal(matrix, lastNormal0.x, lastNormal0.y, lastNormal0.z)
+ .next()
+ buffer.vertex(matrix.positionMatrix, b.x.toFloat(), b.y.toFloat(), b.z.toFloat())
+ .color(-1)
+ .normal(matrix, normal.x, normal.y, normal.z)
+ .next()
+ }
+
+ BufferRenderer.drawWithGlobalProgram(buffer.end())
+ }
+
+ companion object {
+ private fun doLine(
+ matrix: MatrixStack.Entry,
+ buf: BufferBuilder,
+ i: Float,
+ j: Float,
+ k: Float,
+ x: Float,
+ y: Float,
+ z: Float
+ ) {
+ val normal = Vector3f(x, y, z)
+ .sub(i, j, k)
+ .normalize()
+ buf.vertex(matrix.positionMatrix, i, j, k)
+ .normal(matrix, normal.x, normal.y, normal.z)
+ .color(-1)
+ .next()
+ buf.vertex(matrix.positionMatrix, x, y, z)
+ .normal(matrix, normal.x, normal.y, normal.z)
+ .color(-1)
+ .next()
+ }
+
+
+ private fun buildWireFrameCube(matrix: MatrixStack.Entry, tessellator: Tessellator) {
+ val buf = tessellator.begin(VertexFormat.DrawMode.LINES, VertexFormats.LINES)
+
+ for (i in 0..1) {
+ for (j in 0..1) {
+ val i = i.toFloat()
+ val j = j.toFloat()
+ doLine(matrix, buf, 0F, i, j, 1F, i, j)
+ doLine(matrix, buf, i, 0F, j, i, 1F, j)
+ doLine(matrix, buf, i, j, 0F, i, j, 1F)
+ }
+ }
+ BufferRenderer.drawWithGlobalProgram(buf.end())
+ }
+
+ private fun buildCube(matrix: Matrix4f, tessellator: Tessellator) {
+ val buf = tessellator.begin(VertexFormat.DrawMode.TRIANGLES, VertexFormats.POSITION_COLOR)
+ buf.vertex(matrix, 0.0F, 0.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 0.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 1.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 1.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 0.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 1.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 0.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 0.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 0.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 1.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 0.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 0.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 0.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 1.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 1.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 0.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 0.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 0.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 1.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 0.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 0.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 1.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 0.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 1.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 0.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 1.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 0.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 1.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 1.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 1.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 1.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 1.0F, 0.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 1.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 1.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 0.0F, 1.0F, 1.0F).color(-1).next()
+ buf.vertex(matrix, 1.0F, 0.0F, 1.0F).color(-1).next()
+ RenderLayers.TRANSLUCENT_TRIS.draw(buf.end())
+ }
+
+
+ fun renderInWorld(event: WorldRenderLastEvent, block: RenderInWorldContext. () -> Unit) {
+ RenderSystem.disableDepthTest()
+ RenderSystem.enableBlend()
+ RenderSystem.defaultBlendFunc()
+ RenderSystem.disableCull()
+
+ event.matrices.push()
+ event.matrices.translate(-event.camera.pos.x, -event.camera.pos.y, -event.camera.pos.z)
+
+ val ctx = RenderInWorldContext(
+ RenderSystem.renderThreadTesselator(),
+ event.matrices,
+ event.camera,
+ event.tickCounter,
+ event.vertexConsumers
+ )
+
+ block(ctx)
+
+ event.matrices.pop()
+
+ RenderSystem.setShaderColor(1F, 1F, 1F, 1F)
+ VertexBuffer.unbind()
+ RenderSystem.enableDepthTest()
+ RenderSystem.enableCull()
+ RenderSystem.disableBlend()
+ }
+ }
+}
+
+
diff --git a/src/main/kotlin/util/render/TranslatedScissors.kt b/src/main/kotlin/util/render/TranslatedScissors.kt
new file mode 100644
index 0000000..c1e6544
--- /dev/null
+++ b/src/main/kotlin/util/render/TranslatedScissors.kt
@@ -0,0 +1,22 @@
+
+package moe.nea.firmament.util.render
+
+import org.joml.Vector4f
+import net.minecraft.client.gui.DrawContext
+
+fun DrawContext.enableScissorWithTranslation(x1: Float, y1: Float, x2: Float, y2: Float) {
+ val pMat = matrices.peek().positionMatrix
+ val target = Vector4f()
+
+ target.set(x1, y1, 0f, 1f)
+ target.mul(pMat)
+ val scissorX1 = target.x
+ val scissorY1 = target.y
+
+ target.set(x2, y2, 0f, 1f)
+ target.mul(pMat)
+ val scissorX2 = target.x
+ val scissorY2 = target.y
+
+ enableScissor(scissorX1.toInt(), scissorY1.toInt(), scissorX2.toInt(), scissorY2.toInt())
+}
diff --git a/src/main/kotlin/util/stringutil.kt b/src/main/kotlin/util/stringutil.kt
new file mode 100644
index 0000000..56f8dbe
--- /dev/null
+++ b/src/main/kotlin/util/stringutil.kt
@@ -0,0 +1,6 @@
+
+package moe.nea.firmament.util
+
+fun parseIntWithComma(string: String): Int {
+ return string.replace(",", "").toInt()
+}
diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt
new file mode 100644
index 0000000..a05733c
--- /dev/null
+++ b/src/main/kotlin/util/textutil.kt
@@ -0,0 +1,117 @@
+
+
+package moe.nea.firmament.util
+
+import net.minecraft.text.MutableText
+import net.minecraft.text.PlainTextContent
+import net.minecraft.text.Style
+import net.minecraft.text.Text
+import net.minecraft.text.TranslatableTextContent
+import net.minecraft.util.Formatting
+import moe.nea.firmament.Firmament
+
+
+class TextMatcher(text: Text) {
+ data class State(
+ var iterator: MutableList<Text>,
+ var currentText: Text?,
+ var offset: Int,
+ var textContent: String,
+ )
+
+ var state = State(
+ mutableListOf(text),
+ null,
+ 0,
+ ""
+ )
+
+ fun pollChunk(): Boolean {
+ val firstOrNull = state.iterator.removeFirstOrNull() ?: return false
+ state.offset = 0
+ state.currentText = firstOrNull
+ state.textContent = when (val content = firstOrNull.content) {
+ is PlainTextContent.Literal -> content.string
+ else -> {
+ Firmament.logger.warn("TextContent of type ${content.javaClass} not understood.")
+ return false
+ }
+ }
+ state.iterator.addAll(0, firstOrNull.siblings)
+ return true
+ }
+
+ fun pollChunks(): Boolean {
+ while (state.offset !in state.textContent.indices) {
+ if (!pollChunk()) {
+ return false
+ }
+ }
+ return true
+ }
+
+ fun pollChar(): Char? {
+ if (!pollChunks()) return null
+ return state.textContent[state.offset++]
+ }
+
+
+ fun expectString(string: String): Boolean {
+ var found = ""
+ while (found.length < string.length) {
+ if (!pollChunks()) return false
+ val takeable = state.textContent.drop(state.offset).take(string.length - found.length)
+ state.offset += takeable.length
+ found += takeable
+ }
+ return found == string
+ }
+}
+
+val formattingChars = "kmolnrKMOLNR".toSet()
+fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String {
+ var nextParagraph = indexOf('§')
+ if (nextParagraph < 0) return this.toString()
+ val stringBuffer = StringBuilder(this.length)
+ var readIndex = 0
+ while (nextParagraph >= 0) {
+ stringBuffer.append(this, readIndex, nextParagraph)
+ if (keepNonColorCodes && nextParagraph + 1 < length && this[nextParagraph + 1] in formattingChars) {
+ readIndex = nextParagraph
+ nextParagraph = indexOf('§', startIndex = readIndex + 1)
+ } else {
+ readIndex = nextParagraph + 2
+ nextParagraph = indexOf('§', startIndex = readIndex)
+ }
+ if (readIndex > this.length)
+ readIndex = this.length
+ }
+ stringBuffer.append(this, readIndex, this.length)
+ return stringBuffer.toString()
+}
+
+val Text.unformattedString: String
+ get() = string.removeColorCodes()
+
+
+fun MutableText.withColor(formatting: Formatting) = this.styled { it.withColor(formatting).withItalic(false) }
+
+fun Text.transformEachRecursively(function: (Text) -> Text): Text {
+ val c = this.content
+ if (c is TranslatableTextContent) {
+ return Text.translatableWithFallback(c.key, c.fallback, *c.args.map {
+ (if (it is Text) it else Text.literal(it.toString())).transformEachRecursively(function)
+ }.toTypedArray()).also { new ->
+ new.style = this.style
+ new.siblings.clear()
+ this.siblings.forEach { child ->
+ new.siblings.add(child.transformEachRecursively(function))
+ }
+ }
+ }
+ return function(this.copy().also { it.siblings.clear() }).also { tt ->
+ this.siblings.forEach {
+ tt.siblings.add(it.transformEachRecursively(function))
+ }
+ }
+}
diff --git a/src/main/kotlin/util/uuid.kt b/src/main/kotlin/util/uuid.kt
new file mode 100644
index 0000000..4aa0749
--- /dev/null
+++ b/src/main/kotlin/util/uuid.kt
@@ -0,0 +1,12 @@
+
+
+package moe.nea.firmament.util
+
+import java.math.BigInteger
+import java.util.UUID
+
+fun parseDashlessUUID(dashlessUuid: String): UUID {
+ val most = BigInteger(dashlessUuid.substring(0, 16), 16)
+ val least = BigInteger(dashlessUuid.substring(16, 32), 16)
+ return UUID(most.toLong(), least.toLong())
+}