Replace references to NEU with Firmament

This commit is contained in:
nea
2023-05-16 01:23:43 +02:00
parent 96c546cc73
commit ead6762eb1
70 changed files with 354 additions and 360 deletions

View File

@@ -0,0 +1,24 @@
package moe.nea.firmament.util
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtString
import net.minecraft.text.Text
fun ItemStack.appendLore(args: List<Text>) {
val compoundTag = getOrCreateSubNbt("display")
val loreList = compoundTag.getOrCreateList("Lore", NbtString.STRING_TYPE)
for (arg in args) {
loreList.add(NbtString.of(Text.Serializer.toJson(arg)))
}
}
fun NbtCompound.getOrCreateList(label: String, tag: Byte): NbtList = getList(label, tag.toInt()).also {
put(label, it)
}
fun NbtCompound.getOrCreateCompoundTag(label: String): NbtCompound = getCompound(label).also {
put(label, it)
}

View File

@@ -0,0 +1,243 @@
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 doubleMatch = Patterns.DOUBLE.matchEntire(textForm) ?: Patterns.DOUBLE_UNTYPED.matchEntire(textForm)
if (doubleMatch != null) {
return NbtDouble.of(doubleMatch.groups[1]!!.value.toDouble())
}
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())
}
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)
}
}
}

View File

@@ -0,0 +1,8 @@
package moe.nea.firmament.util
import kotlinx.serialization.Serializable
@Serializable
data class Locraw(val server: String, val gametype: String? = null, val mode: String? = null, val map: String? = null) {
val skyblockLocation = if (gametype == "SKYBLOCK") mode else null
}

View File

@@ -0,0 +1,13 @@
package moe.nea.firmament.util
import io.github.moulberry.repo.data.Coordinate
import net.minecraft.client.MinecraftClient
import net.minecraft.util.math.BlockPos
object MC {
inline val player get() = MinecraftClient.getInstance().player
inline val world get() = MinecraftClient.getInstance().world
}
val Coordinate.blockPos: BlockPos
get() = BlockPos(x, y, z)

View File

@@ -0,0 +1,22 @@
package moe.nea.firmament.util
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Runnable
import kotlin.coroutines.CoroutineContext
import net.minecraft.client.MinecraftClient
object MinecraftDispatcher : CoroutineDispatcher() {
@ExperimentalCoroutinesApi
override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
throw UnsupportedOperationException("limitedParallelism is not supported for MinecraftDispatcher")
}
override fun isDispatchNeeded(context: CoroutineContext): Boolean =
!MinecraftClient.getInstance().isOnThread
override fun dispatch(context: CoroutineContext, block: Runnable) {
MinecraftClient.getInstance().execute(block)
}
}

View File

@@ -0,0 +1,62 @@
package moe.nea.firmament.util
import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import moe.nea.firmament.Firmament
import moe.nea.firmament.events.ServerChatLineReceivedEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.events.WorldReadyEvent
object SBData {
val profileRegex = "(?:Your profile was changed to: |You are playing on profile: )(.+)".toRegex()
var profileCuteName: String? = null
private var lastLocrawSent = Timer()
private val locrawRoundtripTime: Duration = 5.seconds
var locraw: Locraw? = null
val skyblockLocation get() = locraw?.skyblockLocation
fun init() {
ServerChatLineReceivedEvent.subscribe { event ->
val profileMatch = profileRegex.matchEntire(event.unformattedString)
if (profileMatch != null) {
profileCuteName = profileMatch.groupValues[1]
}
if (event.unformattedString.startsWith("{")) {
if (tryReceiveLocraw(event.unformattedString) && lastLocrawSent.timePassed() < locrawRoundtripTime) {
lastLocrawSent.markFarPast()
event.cancel()
}
}
}
WorldReadyEvent.subscribe {
sendLocraw()
locraw = null
}
}
private fun tryReceiveLocraw(unformattedString: String): Boolean = try {
val lastLocraw = locraw
locraw = Firmament.json.decodeFromString<Locraw>(unformattedString)
SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw))
true
} catch (e: SerializationException) {
e.printStackTrace()
false
} catch (e: IllegalArgumentException) {
e.printStackTrace()
false
}
fun sendLocraw() {
lastLocrawSent.markNow()
val nh = MC.player?.networkHandler ?: return
nh.sendChatCommand("locraw")
}
}

View File

@@ -0,0 +1,36 @@
package moe.nea.firmament.util
import moe.nea.firmament.Firmament
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.screen.Screen
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 ${nextScreen::class.qualifiedName} to be opened later, but ${nog::class.qualifiedName} is already queued.")
return
}
nextOpenedGui = nextScreen
}
}

View File

@@ -0,0 +1,9 @@
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)
}
}

View File

@@ -0,0 +1,48 @@
package moe.nea.firmament.util
import io.github.moulberry.repo.data.NEUItem
import io.github.moulberry.repo.data.Rarity
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.util.Identifier
@JvmInline
value class SkyblockId(val neuItem: String) {
val identifier get() = Identifier("skyblockitem", neuItem.lowercase().replace(";", "__"))
}
val NEUItem.skyblockId get() = SkyblockId(skyblockItemId)
@Serializable
data class HypixelPetInfo(
val type: String,
val tier: Rarity,
) {
val skyblockId get() = SkyblockId("${type.uppercase()};${tier.ordinal}")
}
private val jsonparser = Json { ignoreUnknownKeys = true }
val ItemStack.extraAttributes: NbtCompound
get() = getOrCreateSubNbt("ExtraAttributes")
val ItemStack.skyBlockId: SkyblockId?
get() {
when (val id = extraAttributes.getString("id")) {
"PET" -> {
val jsonString = extraAttributes.getString("petInfo")
if (jsonString.isNullOrBlank()) return null
val petInfo =
runCatching { jsonparser.decodeFromString<HypixelPetInfo>(jsonString) }
.getOrElse { return null }
return petInfo.skyblockId
}
// TODO: RUNE, ENCHANTED_BOOK, PARTY_HAT_CRAB{,_ANIMATED}, ABICASE
else -> {
return SkyblockId(id)
}
}
}

View File

@@ -0,0 +1,15 @@
package moe.nea.firmament.util
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.TimeSource
@OptIn(ExperimentalTime::class)
class TimeMark private constructor(private val timeMark: TimeSource.Monotonic.ValueTimeMark?) {
fun passedTime() = timeMark?.elapsedNow() ?: Duration.INFINITE
companion object {
fun now() = TimeMark(TimeSource.Monotonic.markNow())
fun farPast() = TimeMark(null)
}
}

View File

@@ -0,0 +1,23 @@
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
}
}

View File

@@ -0,0 +1,204 @@
package moe.nea.firmament.util.config
import io.github.cottonmc.cotton.gui.client.CottonClientScreen
import io.github.cottonmc.cotton.gui.client.LightweightGuiDescription
import io.github.cottonmc.cotton.gui.widget.WButton
import io.github.cottonmc.cotton.gui.widget.WLabel
import io.github.cottonmc.cotton.gui.widget.WToggleButton
import io.github.cottonmc.cotton.gui.widget.WWidget
import io.github.cottonmc.cotton.gui.widget.data.Insets
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonPrimitive
import kotlin.io.path.createDirectories
import kotlin.io.path.readText
import kotlin.io.path.writeText
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.WGridPanelWithPadding
import moe.nea.firmament.util.ScreenUtil.setScreenLater
abstract class ManagedConfig(val name: String) {
class GuiAppender(val width: Int) {
private var row = 0
internal val panel = WGridPanelWithPadding(verticalPadding = 4, horizontalPadding = 4)
internal val reloadables = mutableListOf<(() -> Unit)>()
fun set(x: Int, y: Int, w: Int, h: Int, widget: WWidget) {
panel.add(widget, x, y, w, h)
}
fun onReload(reloadable: () -> Unit) {
reloadables.add(reloadable)
}
fun skipRows(r: Int) {
row += r
}
fun appendSplitRow(left: WWidget, right: WWidget) {
val lw = width / 2
set(0, row, lw, 1, left)
set(lw, row, width - lw, 1, right)
skipRows(1)
}
fun appendFullRow(widget: WWidget) {
set(0, row, width, 1, widget)
skipRows(1)
}
}
interface OptionHandler<T : Any> {
fun toJson(element: T): JsonElement?
fun fromJson(element: JsonElement): T
fun emitGuiElements(opt: Option<T>, guiAppender: GuiAppender)
}
inner class Option<T : Any> internal constructor(
val propertyName: String,
val default: () -> T,
val handler: OptionHandler<T>
) : ReadOnlyProperty<Any?, T> {
private lateinit var _value: T
private var loaded = false
var value: T
get() {
if (!loaded)
load()
return _value
}
set(value) {
loaded = true
_value = value
}
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
private fun load() {
if (data.containsKey(propertyName)) {
try {
value = handler.fromJson(data[propertyName]!!)
} catch (e: Exception) {
Firmament.logger.error(
"Exception during loading of config file $name. This will reset this config.",
e
)
}
}
value = default()
}
internal fun toJson(): JsonElement? {
return handler.toJson(value)
}
fun appendToGui(guiapp: GuiAppender) {
handler.emitGuiElements(this, guiapp)
}
}
val file = Firmament.CONFIG_DIR.resolve("$name.json")
val data: JsonObject by lazy {
try {
Firmament.json.decodeFromString(
file.readText()
)
} catch (e: Exception) {
Firmament.logger.info("Could not read config $name. Loading empty config.")
JsonObject(mutableMapOf())
}
}
fun save() {
val data = JsonObject(allOptions.mapNotNull { (key, value) ->
value.toJson()?.let {
key to it
}
}.toMap())
file.parent.createDirectories()
file.writeText(Firmament.json.encodeToString(data))
}
val allOptions = mutableMapOf<String, Option<*>>()
val sortedOptions = mutableListOf<Option<*>>()
protected fun <T : Any> option(propertyName: String, default: () -> T, handler: OptionHandler<T>): Option<T> {
if (propertyName in allOptions) error("Cannot register the same name twice")
return Option(propertyName, default, handler).also {
allOptions[propertyName] = it
sortedOptions.add(it)
}
}
class BooleanHandler(val config: ManagedConfig) : OptionHandler<Boolean> {
override fun toJson(element: Boolean): JsonElement? {
return JsonPrimitive(element)
}
override fun fromJson(element: JsonElement): Boolean {
return element.jsonPrimitive.boolean
}
override fun emitGuiElements(opt: Option<Boolean>, guiAppender: GuiAppender) {
guiAppender.appendFullRow(
WToggleButton(Text.translatable("firmament.config.${config.name}.${opt.propertyName}")).apply {
guiAppender.onReload { toggle = opt.value }
setOnToggle {
opt.value = it
config.save()
}
}
)
}
}
class ClickHandler(val config: ManagedConfig, val runnable: () -> Unit) : OptionHandler<Unit> {
override fun toJson(element: Unit): JsonElement? {
return null
}
override fun fromJson(element: JsonElement) {}
override fun emitGuiElements(opt: Option<Unit>, guiAppender: GuiAppender) {
guiAppender.appendSplitRow(
WLabel(Text.translatable("firmament.config.${config.name}.${opt.propertyName}")),
WButton(Text.translatable("firmament.config.${config.name}.${opt.propertyName}")).apply {
setOnClick {
runnable()
}
},
)
}
}
protected fun toggle(propertyName: String, default: () -> Boolean): Option<Boolean> {
return option(propertyName, default, BooleanHandler(this))
}
fun showConfigEditor() {
val lwgd = LightweightGuiDescription()
val guiapp = GuiAppender(20)
guiapp.panel.insets = Insets.ROOT_PANEL
sortedOptions.forEach { it.appendToGui(guiapp) }
guiapp.reloadables.forEach { it() }
lwgd.setRootPanel(guiapp.panel)
setScreenLater(CottonClientScreen(lwgd))
}
protected fun button(propertyName: String, runnable: () -> Unit): Option<Unit> {
return option(propertyName, { }, ClickHandler(this, runnable))
}
}

View File

@@ -0,0 +1,60 @@
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)
}
}

View File

@@ -0,0 +1,75 @@
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.ScreenOpenEvent
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() {
ScreenOpenEvent.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()
}

View File

@@ -0,0 +1,82 @@
package moe.nea.firmament.util.data
import java.nio.file.Path
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<String, S>
override val data: S?
get() = SBData.profileCuteName?.let {
allConfigs.computeIfAbsent(it) { configDefault() }
}
init {
allConfigs = readValues()
readValues()
IDataHolder.putDataHolder(this::class, this)
}
private val configDirectory: Path get() = Firmament.CONFIG_DIR.resolve("profiles").resolve(configName)
private fun readValues(): MutableMap<String, S> {
if (!configDirectory.exists()) {
configDirectory.createDirectories()
}
val profileFiles = configDirectory.listDirectoryEntries()
return profileFiles
.filter { it.extension == "json" }
.mapNotNull {
try {
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) {
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()
}
}

View File

@@ -0,0 +1,105 @@
package moe.nea.firmament.util.render
import com.mojang.blaze3d.systems.RenderSystem
import org.joml.Matrix4f
import net.minecraft.client.gl.VertexBuffer
import net.minecraft.client.render.BufferBuilder
import net.minecraft.client.render.Camera
import net.minecraft.client.render.GameRenderer
import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexFormat
import net.minecraft.client.render.VertexFormats
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3d
class RenderBlockContext private constructor(private val tesselator: Tessellator, private val matrixStack: MatrixStack) {
private val buffer = tesselator.buffer
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, buffer)
tesselator.draw()
matrixStack.pop()
}
fun tinyBlock(vec3d: Vec3d, size: Float) {
matrixStack.push()
matrixStack.translate(vec3d.x, vec3d.y, vec3d.z)
matrixStack.scale(size, size, size)
matrixStack.translate(-.5, -.5, -.5)
buildCube(matrixStack.peek().positionMatrix, buffer)
tesselator.draw()
matrixStack.pop()
}
companion object {
private fun buildCube(matrix: Matrix4f, buf: BufferBuilder) {
buf.begin(VertexFormat.DrawMode.TRIANGLES, VertexFormats.POSITION_COLOR)
buf.fixedColor(255, 255, 255, 255)
buf.vertex(matrix, 0.0F, 0.0F, 0.0F).next()
buf.vertex(matrix, 0.0F, 0.0F, 1.0F).next()
buf.vertex(matrix, 0.0F, 1.0F, 1.0F).next()
buf.vertex(matrix, 1.0F, 1.0F, 0.0F).next()
buf.vertex(matrix, 0.0F, 0.0F, 0.0F).next()
buf.vertex(matrix, 0.0F, 1.0F, 0.0F).next()
buf.vertex(matrix, 1.0F, 0.0F, 1.0F).next()
buf.vertex(matrix, 0.0F, 0.0F, 0.0F).next()
buf.vertex(matrix, 1.0F, 0.0F, 0.0F).next()
buf.vertex(matrix, 1.0F, 1.0F, 0.0F).next()
buf.vertex(matrix, 1.0F, 0.0F, 0.0F).next()
buf.vertex(matrix, 0.0F, 0.0F, 0.0F).next()
buf.vertex(matrix, 0.0F, 0.0F, 0.0F).next()
buf.vertex(matrix, 0.0F, 1.0F, 1.0F).next()
buf.vertex(matrix, 0.0F, 1.0F, 0.0F).next()
buf.vertex(matrix, 1.0F, 0.0F, 1.0F).next()
buf.vertex(matrix, 0.0F, 0.0F, 1.0F).next()
buf.vertex(matrix, 0.0F, 0.0F, 0.0F).next()
buf.vertex(matrix, 0.0F, 1.0F, 1.0F).next()
buf.vertex(matrix, 0.0F, 0.0F, 1.0F).next()
buf.vertex(matrix, 1.0F, 0.0F, 1.0F).next()
buf.vertex(matrix, 1.0F, 1.0F, 1.0F).next()
buf.vertex(matrix, 1.0F, 0.0F, 0.0F).next()
buf.vertex(matrix, 1.0F, 1.0F, 0.0F).next()
buf.vertex(matrix, 1.0F, 0.0F, 0.0F).next()
buf.vertex(matrix, 1.0F, 1.0F, 1.0F).next()
buf.vertex(matrix, 1.0F, 0.0F, 1.0F).next()
buf.vertex(matrix, 1.0F, 1.0F, 1.0F).next()
buf.vertex(matrix, 1.0F, 1.0F, 0.0F).next()
buf.vertex(matrix, 0.0F, 1.0F, 0.0F).next()
buf.vertex(matrix, 1.0F, 1.0F, 1.0F).next()
buf.vertex(matrix, 0.0F, 1.0F, 0.0F).next()
buf.vertex(matrix, 0.0F, 1.0F, 1.0F).next()
buf.vertex(matrix, 1.0F, 1.0F, 1.0F).next()
buf.vertex(matrix, 0.0F, 1.0F, 1.0F).next()
buf.vertex(matrix, 1.0F, 0.0F, 1.0F).next()
buf.unfixColor()
}
fun renderBlocks(matrices: MatrixStack, camera: Camera, block: RenderBlockContext. () -> Unit) {
RenderSystem.disableDepthTest()
RenderSystem.enableBlend()
RenderSystem.defaultBlendFunc()
RenderSystem.setShader(GameRenderer::getPositionColorProgram)
matrices.push()
matrices.translate(-camera.pos.x, -camera.pos.y, -camera.pos.z)
val ctx = RenderBlockContext(Tessellator.getInstance(), matrices)
block(ctx)
matrices.pop()
RenderSystem.setShaderColor(1F,1F,1F,1F)
VertexBuffer.unbind()
RenderSystem.enableDepthTest()
RenderSystem.disableBlend()
}
}
}

View File

@@ -0,0 +1,70 @@
package moe.nea.firmament.util
import net.minecraft.text.LiteralTextContent
import net.minecraft.text.Text
import net.minecraft.text.TextContent
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 LiteralTextContent -> content.string
TextContent.EMPTY -> ""
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 Text.unformattedString
get() = string.replace("§.".toRegex(), "")