Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
This commit is contained in:
10
src/main/kotlin/util/Base64Util.kt
Normal file
10
src/main/kotlin/util/Base64Util.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
19
src/main/kotlin/util/BazaarPriceStrategy.kt
Normal file
19
src/main/kotlin/util/BazaarPriceStrategy.kt
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/main/kotlin/util/ClipboardUtils.kt
Normal file
24
src/main/kotlin/util/ClipboardUtils.kt
Normal file
@@ -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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/main/kotlin/util/CommonSoundEffects.kt
Normal file
26
src/main/kotlin/util/CommonSoundEffects.kt
Normal file
@@ -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"))
|
||||
}
|
||||
}
|
||||
20
src/main/kotlin/util/DurabilityBarEvent.kt
Normal file
20
src/main/kotlin/util/DurabilityBarEvent.kt
Normal file
@@ -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>()
|
||||
}
|
||||
10
src/main/kotlin/util/ErrorBoundary.kt
Normal file
10
src/main/kotlin/util/ErrorBoundary.kt
Normal file
@@ -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()
|
||||
}
|
||||
|
||||
59
src/main/kotlin/util/FirmFormatters.kt
Normal file
59
src/main/kotlin/util/FirmFormatters.kt
Normal file
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
93
src/main/kotlin/util/FragmentGuiScreen.kt
Normal file
93
src/main/kotlin/util/FragmentGuiScreen.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/main/kotlin/util/GetRectangle.kt
Normal file
17
src/main/kotlin/util/GetRectangle.kt
Normal file
@@ -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()
|
||||
)
|
||||
}
|
||||
31
src/main/kotlin/util/HoveredItemStack.kt
Normal file
31
src/main/kotlin/util/HoveredItemStack.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
25
src/main/kotlin/util/IdentifierSerializer.kt
Normal file
25
src/main/kotlin/util/IdentifierSerializer.kt
Normal file
@@ -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())
|
||||
}
|
||||
}
|
||||
15
src/main/kotlin/util/IdentityCharacteristics.kt
Normal file
15
src/main/kotlin/util/IdentityCharacteristics.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
26
src/main/kotlin/util/ItemUtil.kt
Normal file
26
src/main/kotlin/util/ItemUtil.kt
Normal file
@@ -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)
|
||||
}
|
||||
35
src/main/kotlin/util/LegacyFormattingCode.kt
Normal file
35
src/main/kotlin/util/LegacyFormattingCode.kt
Normal file
@@ -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"
|
||||
|
||||
}
|
||||
245
src/main/kotlin/util/LegacyTagParser.kt
Normal file
245
src/main/kotlin/util/LegacyTagParser.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
20
src/main/kotlin/util/LoadResource.kt
Normal file
20
src/main/kotlin/util/LoadResource.kt
Normal file
@@ -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
|
||||
}
|
||||
|
||||
12
src/main/kotlin/util/Locraw.kt
Normal file
12
src/main/kotlin/util/Locraw.kt
Normal file
@@ -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
|
||||
}
|
||||
8
src/main/kotlin/util/LogIfNull.kt
Normal file
8
src/main/kotlin/util/LogIfNull.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
package moe.nea.firmament.util
|
||||
|
||||
|
||||
fun runNull(block: () -> Unit): Nothing? {
|
||||
block()
|
||||
return null
|
||||
}
|
||||
94
src/main/kotlin/util/MC.kt
Normal file
94
src/main/kotlin/util/MC.kt
Normal file
@@ -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)
|
||||
8
src/main/kotlin/util/MinecraftDispatcher.kt
Normal file
8
src/main/kotlin/util/MinecraftDispatcher.kt
Normal file
@@ -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() }
|
||||
44
src/main/kotlin/util/MoulConfigFragment.kt
Normal file
44
src/main/kotlin/util/MoulConfigFragment.kt
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
230
src/main/kotlin/util/MoulConfigUtils.kt
Normal file
230
src/main/kotlin/util/MoulConfigUtils.kt
Normal file
@@ -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")))
|
||||
}
|
||||
}
|
||||
38
src/main/kotlin/util/MutableMapWithMaxSize.kt
Normal file
38
src/main/kotlin/util/MutableMapWithMaxSize.kt
Normal file
@@ -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) }
|
||||
}
|
||||
}
|
||||
66
src/main/kotlin/util/SBData.kt
Normal file
66
src/main/kotlin/util/SBData.kt
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/main/kotlin/util/ScoreboardUtil.kt
Normal file
45
src/main/kotlin/util/ScoreboardUtil.kt
Normal file
@@ -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(), "")
|
||||
}
|
||||
38
src/main/kotlin/util/ScreenUtil.kt
Normal file
38
src/main/kotlin/util/ScreenUtil.kt
Normal file
@@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
11
src/main/kotlin/util/SequenceUtil.kt
Normal file
11
src/main/kotlin/util/SequenceUtil.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
42
src/main/kotlin/util/SkyBlockIsland.kt
Normal file
42
src/main/kotlin/util/SkyBlockIsland.kt
Normal file
@@ -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)
|
||||
}
|
||||
149
src/main/kotlin/util/SkyblockId.kt
Normal file
149
src/main/kotlin/util/SkyblockId.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
src/main/kotlin/util/SortedMapSerializer.kt
Normal file
25
src/main/kotlin/util/SortedMapSerializer.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
85
src/main/kotlin/util/TemplateUtil.kt
Normal file
85
src/main/kotlin/util/TemplateUtil.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
44
src/main/kotlin/util/TimeMark.kt
Normal file
44
src/main/kotlin/util/TimeMark.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
25
src/main/kotlin/util/Timer.kt
Normal file
25
src/main/kotlin/util/Timer.kt
Normal file
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
75
src/main/kotlin/util/WarpUtil.kt
Normal file
75
src/main/kotlin/util/WarpUtil.kt
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/main/kotlin/util/assertions.kt
Normal file
25
src/main/kotlin/util/assertions.kt
Normal file
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
47
src/main/kotlin/util/async/input.kt
Normal file
47
src/main/kotlin/util/async/input.kt
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
src/main/kotlin/util/colorconversion.kt
Normal file
13
src/main/kotlin/util/colorconversion.kt
Normal file
@@ -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)
|
||||
|
||||
14
src/main/kotlin/util/customgui/CoordRememberingSlot.kt
Normal file
14
src/main/kotlin/util/customgui/CoordRememberingSlot.kt
Normal file
@@ -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()
|
||||
72
src/main/kotlin/util/customgui/CustomGui.kt
Normal file
72
src/main/kotlin/util/customgui/CustomGui.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
17
src/main/kotlin/util/customgui/HasCustomGui.kt
Normal file
17
src/main/kotlin/util/customgui/HasCustomGui.kt
Normal file
@@ -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)
|
||||
}
|
||||
|
||||
62
src/main/kotlin/util/data/DataHolder.kt
Normal file
62
src/main/kotlin/util/data/DataHolder.kt
Normal file
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
77
src/main/kotlin/util/data/IDataHolder.kt
Normal file
77
src/main/kotlin/util/data/IDataHolder.kt
Normal file
@@ -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()
|
||||
}
|
||||
84
src/main/kotlin/util/data/ProfileSpecificDataHolder.kt
Normal file
84
src/main/kotlin/util/data/ProfileSpecificDataHolder.kt
Normal file
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
33
src/main/kotlin/util/filter/IteratorFilterSet.kt
Normal file
33
src/main/kotlin/util/filter/IteratorFilterSet.kt
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
src/main/kotlin/util/item/NbtItemData.kt
Normal file
24
src/main/kotlin/util/item/NbtItemData.kt
Normal file
@@ -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)
|
||||
}
|
||||
90
src/main/kotlin/util/item/SkullItemData.kt
Normal file
90
src/main/kotlin/util/item/SkullItemData.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
25
src/main/kotlin/util/json/BlockPosSerializer.kt
Normal file
25
src/main/kotlin/util/json/BlockPosSerializer.kt
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
29
src/main/kotlin/util/json/DashlessUUIDSerializer.kt
Normal file
29
src/main/kotlin/util/json/DashlessUUIDSerializer.kt
Normal file
@@ -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("-", ""))
|
||||
}
|
||||
}
|
||||
22
src/main/kotlin/util/json/InstantAsLongSerializer.kt
Normal file
22
src/main/kotlin/util/json/InstantAsLongSerializer.kt
Normal file
@@ -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())
|
||||
}
|
||||
}
|
||||
31
src/main/kotlin/util/json/SingletonSerializableList.kt
Normal file
31
src/main/kotlin/util/json/SingletonSerializableList.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
9
src/main/kotlin/util/listutil.kt
Normal file
9
src/main/kotlin/util/listutil.kt
Normal file
@@ -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
|
||||
}
|
||||
9
src/main/kotlin/util/propertyutil.kt
Normal file
9
src/main/kotlin/util/propertyutil.kt
Normal file
@@ -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)) }
|
||||
}
|
||||
55
src/main/kotlin/util/regex.kt
Normal file
55
src/main/kotlin/util/regex.kt
Normal file
@@ -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
|
||||
}
|
||||
101
src/main/kotlin/util/render/FacingThePlayerContext.kt
Normal file
101
src/main/kotlin/util/render/FacingThePlayerContext.kt
Normal file
@@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
33
src/main/kotlin/util/render/LerpUtils.kt
Normal file
33
src/main/kotlin/util/render/LerpUtils.kt
Normal file
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
95
src/main/kotlin/util/render/RenderCircleProgress.kt
Normal file
95
src/main/kotlin/util/render/RenderCircleProgress.kt
Normal file
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
6
src/main/kotlin/util/render/RenderContextDSL.kt
Normal file
6
src/main/kotlin/util/render/RenderContextDSL.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
package moe.nea.firmament.util.render
|
||||
|
||||
@DslMarker
|
||||
annotation class RenderContextDSL {
|
||||
}
|
||||
294
src/main/kotlin/util/render/RenderInWorldContext.kt
Normal file
294
src/main/kotlin/util/render/RenderInWorldContext.kt
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
src/main/kotlin/util/render/TranslatedScissors.kt
Normal file
22
src/main/kotlin/util/render/TranslatedScissors.kt
Normal file
@@ -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())
|
||||
}
|
||||
6
src/main/kotlin/util/stringutil.kt
Normal file
6
src/main/kotlin/util/stringutil.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
package moe.nea.firmament.util
|
||||
|
||||
fun parseIntWithComma(string: String): Int {
|
||||
return string.replace(",", "").toInt()
|
||||
}
|
||||
117
src/main/kotlin/util/textutil.kt
Normal file
117
src/main/kotlin/util/textutil.kt
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/main/kotlin/util/uuid.kt
Normal file
12
src/main/kotlin/util/uuid.kt
Normal file
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user