From d2f240ff0ca0d27f417f837e706c781a98c31311 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Wed, 28 Aug 2024 19:04:24 +0200 Subject: Refactor source layout Introduce compat source sets and move all kotlin sources to the main directory [no changelog] --- src/main/kotlin/util/Base64Util.kt | 10 + src/main/kotlin/util/BazaarPriceStrategy.kt | 19 ++ src/main/kotlin/util/ClipboardUtils.kt | 24 ++ src/main/kotlin/util/CommonSoundEffects.kt | 26 ++ src/main/kotlin/util/DurabilityBarEvent.kt | 20 ++ src/main/kotlin/util/ErrorBoundary.kt | 10 + src/main/kotlin/util/FirmFormatters.kt | 59 +++++ src/main/kotlin/util/FragmentGuiScreen.kt | 93 +++++++ src/main/kotlin/util/GetRectangle.kt | 17 ++ src/main/kotlin/util/HoveredItemStack.kt | 31 +++ src/main/kotlin/util/IdentifierSerializer.kt | 25 ++ src/main/kotlin/util/IdentityCharacteristics.kt | 15 ++ src/main/kotlin/util/ItemUtil.kt | 26 ++ src/main/kotlin/util/LegacyFormattingCode.kt | 35 +++ src/main/kotlin/util/LegacyTagParser.kt | 245 +++++++++++++++++ src/main/kotlin/util/LoadResource.kt | 20 ++ src/main/kotlin/util/Locraw.kt | 12 + src/main/kotlin/util/LogIfNull.kt | 8 + src/main/kotlin/util/MC.kt | 94 +++++++ src/main/kotlin/util/MinecraftDispatcher.kt | 8 + src/main/kotlin/util/MoulConfigFragment.kt | 44 +++ src/main/kotlin/util/MoulConfigUtils.kt | 230 ++++++++++++++++ src/main/kotlin/util/MutableMapWithMaxSize.kt | 38 +++ src/main/kotlin/util/SBData.kt | 66 +++++ src/main/kotlin/util/ScoreboardUtil.kt | 45 ++++ src/main/kotlin/util/ScreenUtil.kt | 38 +++ src/main/kotlin/util/SequenceUtil.kt | 11 + src/main/kotlin/util/SkyBlockIsland.kt | 42 +++ src/main/kotlin/util/SkyblockId.kt | 149 +++++++++++ src/main/kotlin/util/SortedMapSerializer.kt | 25 ++ src/main/kotlin/util/TemplateUtil.kt | 85 ++++++ src/main/kotlin/util/TimeMark.kt | 44 +++ src/main/kotlin/util/Timer.kt | 25 ++ src/main/kotlin/util/WarpUtil.kt | 75 ++++++ src/main/kotlin/util/assertions.kt | 25 ++ src/main/kotlin/util/async/input.kt | 47 ++++ src/main/kotlin/util/colorconversion.kt | 13 + .../kotlin/util/customgui/CoordRememberingSlot.kt | 14 + src/main/kotlin/util/customgui/CustomGui.kt | 72 +++++ src/main/kotlin/util/customgui/HasCustomGui.kt | 17 ++ src/main/kotlin/util/data/DataHolder.kt | 62 +++++ src/main/kotlin/util/data/IDataHolder.kt | 77 ++++++ .../kotlin/util/data/ProfileSpecificDataHolder.kt | 84 ++++++ src/main/kotlin/util/filter/IteratorFilterSet.kt | 33 +++ src/main/kotlin/util/item/NbtItemData.kt | 24 ++ src/main/kotlin/util/item/SkullItemData.kt | 90 +++++++ src/main/kotlin/util/json/BlockPosSerializer.kt | 25 ++ .../kotlin/util/json/DashlessUUIDSerializer.kt | 29 ++ .../kotlin/util/json/InstantAsLongSerializer.kt | 22 ++ .../kotlin/util/json/SingletonSerializableList.kt | 31 +++ src/main/kotlin/util/listutil.kt | 9 + src/main/kotlin/util/propertyutil.kt | 9 + src/main/kotlin/util/regex.kt | 55 ++++ .../kotlin/util/render/FacingThePlayerContext.kt | 101 +++++++ src/main/kotlin/util/render/LerpUtils.kt | 33 +++ .../kotlin/util/render/RenderCircleProgress.kt | 95 +++++++ src/main/kotlin/util/render/RenderContextDSL.kt | 6 + .../kotlin/util/render/RenderInWorldContext.kt | 294 +++++++++++++++++++++ src/main/kotlin/util/render/TranslatedScissors.kt | 22 ++ src/main/kotlin/util/stringutil.kt | 6 + src/main/kotlin/util/textutil.kt | 117 ++++++++ src/main/kotlin/util/uuid.kt | 12 + 62 files changed, 3138 insertions(+) create mode 100644 src/main/kotlin/util/Base64Util.kt create mode 100644 src/main/kotlin/util/BazaarPriceStrategy.kt create mode 100644 src/main/kotlin/util/ClipboardUtils.kt create mode 100644 src/main/kotlin/util/CommonSoundEffects.kt create mode 100644 src/main/kotlin/util/DurabilityBarEvent.kt create mode 100644 src/main/kotlin/util/ErrorBoundary.kt create mode 100644 src/main/kotlin/util/FirmFormatters.kt create mode 100644 src/main/kotlin/util/FragmentGuiScreen.kt create mode 100644 src/main/kotlin/util/GetRectangle.kt create mode 100644 src/main/kotlin/util/HoveredItemStack.kt create mode 100644 src/main/kotlin/util/IdentifierSerializer.kt create mode 100644 src/main/kotlin/util/IdentityCharacteristics.kt create mode 100644 src/main/kotlin/util/ItemUtil.kt create mode 100644 src/main/kotlin/util/LegacyFormattingCode.kt create mode 100644 src/main/kotlin/util/LegacyTagParser.kt create mode 100644 src/main/kotlin/util/LoadResource.kt create mode 100644 src/main/kotlin/util/Locraw.kt create mode 100644 src/main/kotlin/util/LogIfNull.kt create mode 100644 src/main/kotlin/util/MC.kt create mode 100644 src/main/kotlin/util/MinecraftDispatcher.kt create mode 100644 src/main/kotlin/util/MoulConfigFragment.kt create mode 100644 src/main/kotlin/util/MoulConfigUtils.kt create mode 100644 src/main/kotlin/util/MutableMapWithMaxSize.kt create mode 100644 src/main/kotlin/util/SBData.kt create mode 100644 src/main/kotlin/util/ScoreboardUtil.kt create mode 100644 src/main/kotlin/util/ScreenUtil.kt create mode 100644 src/main/kotlin/util/SequenceUtil.kt create mode 100644 src/main/kotlin/util/SkyBlockIsland.kt create mode 100644 src/main/kotlin/util/SkyblockId.kt create mode 100644 src/main/kotlin/util/SortedMapSerializer.kt create mode 100644 src/main/kotlin/util/TemplateUtil.kt create mode 100644 src/main/kotlin/util/TimeMark.kt create mode 100644 src/main/kotlin/util/Timer.kt create mode 100644 src/main/kotlin/util/WarpUtil.kt create mode 100644 src/main/kotlin/util/assertions.kt create mode 100644 src/main/kotlin/util/async/input.kt create mode 100644 src/main/kotlin/util/colorconversion.kt create mode 100644 src/main/kotlin/util/customgui/CoordRememberingSlot.kt create mode 100644 src/main/kotlin/util/customgui/CustomGui.kt create mode 100644 src/main/kotlin/util/customgui/HasCustomGui.kt create mode 100644 src/main/kotlin/util/data/DataHolder.kt create mode 100644 src/main/kotlin/util/data/IDataHolder.kt create mode 100644 src/main/kotlin/util/data/ProfileSpecificDataHolder.kt create mode 100644 src/main/kotlin/util/filter/IteratorFilterSet.kt create mode 100644 src/main/kotlin/util/item/NbtItemData.kt create mode 100644 src/main/kotlin/util/item/SkullItemData.kt create mode 100644 src/main/kotlin/util/json/BlockPosSerializer.kt create mode 100644 src/main/kotlin/util/json/DashlessUUIDSerializer.kt create mode 100644 src/main/kotlin/util/json/InstantAsLongSerializer.kt create mode 100644 src/main/kotlin/util/json/SingletonSerializableList.kt create mode 100644 src/main/kotlin/util/listutil.kt create mode 100644 src/main/kotlin/util/propertyutil.kt create mode 100644 src/main/kotlin/util/regex.kt create mode 100644 src/main/kotlin/util/render/FacingThePlayerContext.kt create mode 100644 src/main/kotlin/util/render/LerpUtils.kt create mode 100644 src/main/kotlin/util/render/RenderCircleProgress.kt create mode 100644 src/main/kotlin/util/render/RenderContextDSL.kt create mode 100644 src/main/kotlin/util/render/RenderInWorldContext.kt create mode 100644 src/main/kotlin/util/render/TranslatedScissors.kt create mode 100644 src/main/kotlin/util/stringutil.kt create mode 100644 src/main/kotlin/util/textutil.kt create mode 100644 src/main/kotlin/util/uuid.kt (limited to 'src/main/kotlin/util') 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() +} 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 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 { + 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(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) { + if (args.isEmpty()) return + modifyLore { + val loreList = loreAccordingToNbt.toMutableList() + for (arg in args) { + loreList.add(arg) + } + loreList + } +} + +fun ItemStack.modifyLore(update: (List) -> List) { + 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() + + 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() + + 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 { + 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 { + return mapOf("progress" to true, "total" to true, "emptyColor" to true, "fillColor" to true) + } + }) + uni.registerLoader(object : XMLGuiLoader.Basic { + override fun createInstance(context: XMLContext<*>, element: Element): FirmHoverComponent { + return FirmHoverComponent( + context.getChildFragment(element), + context.getPropertyFromAttribute(element, QName("lines"), List::class.java) as Supplier>, + 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 { + return mapOf( + "lines" to true, + "delay" to false, + ) + } + + }) + uni.registerLoader(object : XMLGuiLoader.Basic { + 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 { + return mapOf("onClick" to true, "enabled" to false, "noBackground" to false) + } + }) + uni.registerLoader(object : XMLGuiLoader.Basic { + 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 { + 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 { + 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 { + return mapOf("tick" to true) + } + }) + uni.registerLoader(object : XMLGuiLoader.Basic { + 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 { + 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) { + generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS) + generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl) + File("wrapper.xsd").writeText(""" + + + + + + """.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 mutableMapWithMaxSize(maxSize: Int): MutableMap = object : LinkedHashMap() { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry): Boolean { + return size > maxSize + } +} + +fun ((T) -> R).memoizeIdentity(maxCacheSize: Int): (T) -> R { + val memoized = { it: IdentityCharacteristics -> + 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 MutableMap.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).memoize(maxCacheSize: Int): (T) -> R { + val map = mutableMapWithMaxSize(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 { + 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 { 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.iterate(iterator: (T) -> T?): Sequence = 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 { + 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() + 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(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) +