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,92 @@
package moe.nea.firmament
import com.mojang.brigadier.CommandDispatcher
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import java.nio.file.Files
import java.nio.file.Path
import net.fabricmc.api.ClientModInitializer
import net.fabricmc.api.ModInitializer
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
import net.fabricmc.loader.api.FabricLoader
import net.fabricmc.loader.api.Version
import net.fabricmc.loader.api.metadata.ModMetadata
import org.apache.logging.log4j.LogManager
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder
import kotlinx.coroutines.*
import kotlinx.serialization.json.Json
import kotlin.coroutines.EmptyCoroutineContext
import net.minecraft.command.CommandRegistryAccess
import moe.nea.firmament.commands.registerFirmamentCommand
import moe.nea.firmament.dbus.FirmamentDbusObject
import moe.nea.firmament.features.FeatureManager
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.data.IDataHolder
object Firmament : ModInitializer, ClientModInitializer {
const val MOD_ID = "firmament"
val DEBUG = System.getProperty("firmament.debug") == "true"
val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) }
val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) }
val logger = LogManager.getLogger("Firmament")
val metadata: ModMetadata by lazy { FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata }
val version: Version by lazy { metadata.version }
val json = Json {
prettyPrint = DEBUG
ignoreUnknownKeys = true
encodeDefaults = true
}
val httpClient by lazy {
HttpClient {
install(ContentNegotiation) {
json(json)
}
install(UserAgent) {
agent = "Firmament/$version"
}
}
}
val globalJob = Job()
val dbusConnection = DBusConnectionBuilder.forSessionBus()
.build()
val coroutineScope =
CoroutineScope(EmptyCoroutineContext + CoroutineName("Firmament")) + SupervisorJob(globalJob)
private fun registerCommands(
dispatcher: CommandDispatcher<FabricClientCommandSource>,
@Suppress("UNUSED_PARAMETER")
ctx: CommandRegistryAccess
) {
registerFirmamentCommand(dispatcher)
}
override fun onInitialize() {
dbusConnection.requestBusName("moe.nea.firmament")
dbusConnection.exportObject(FirmamentDbusObject)
IDataHolder.registerEvents()
RepoManager.initialize()
SBData.init()
FeatureManager.autoload()
ClientCommandRegistrationCallback.EVENT.register(this::registerCommands)
ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping {
runBlocking {
logger.info("Shutting down NEU coroutines")
globalJob.cancel()
}
})
}
override fun onInitializeClient() {
}
}

View File

@@ -0,0 +1,81 @@
package moe.nea.firmament.commands
import com.mojang.brigadier.arguments.ArgumentType
import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import java.lang.reflect.ParameterizedType
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import moe.nea.firmament.util.iterate
typealias DefaultSource = FabricClientCommandSource
inline val <T : CommandContext<*>> T.context get() = this
operator fun <T : Any, C : CommandContext<*>> C.get(arg: TypeSafeArg<T>): T {
return arg.get(this)
}
fun literal(
name: String,
block: LiteralArgumentBuilder<DefaultSource>.() -> Unit
): LiteralArgumentBuilder<DefaultSource> =
LiteralArgumentBuilder.literal<DefaultSource>(name).also(block)
data class TypeSafeArg<T : Any>(val name: String, val argument: ArgumentType<T>) {
val argClass by lazy {
argument.javaClass
.iterate<Class<in ArgumentType<T>>> {
it.superclass
}
.map {
it.genericSuperclass
}
.filterIsInstance<ParameterizedType>()
.find { it.rawType == ArgumentType::class.java }!!
.let { it.actualTypeArguments[0] as Class<*> }
}
@JvmName("getWithThis")
fun <S> CommandContext<S>.get(): T =
get(this)
fun <S> get(ctx: CommandContext<S>): T {
return ctx.getArgument(name, argClass) as T
}
}
fun <T : Any> argument(
name: String,
argument: ArgumentType<T>,
block: RequiredArgumentBuilder<DefaultSource, T>.(TypeSafeArg<T>) -> Unit
): RequiredArgumentBuilder<DefaultSource, T> =
RequiredArgumentBuilder.argument<DefaultSource, T>(name, argument).also { block(it, TypeSafeArg(name, argument)) }
fun <T : ArgumentBuilder<DefaultSource, T>, AT : Any> T.thenArgument(
name: String,
argument: ArgumentType<AT>,
block: RequiredArgumentBuilder<DefaultSource, AT>.(TypeSafeArg<AT>) -> Unit
): T = then(argument(name, argument, block))
fun <T : ArgumentBuilder<DefaultSource, T>> T.thenLiteral(
name: String,
block: LiteralArgumentBuilder<DefaultSource>.() -> Unit
): T =
then(literal(name, block))
fun <T : ArgumentBuilder<DefaultSource, T>> T.then(node: ArgumentBuilder<DefaultSource, *>, block: T.() -> Unit): T =
then(node).also(block)
fun <T : ArgumentBuilder<DefaultSource, T>> T.thenExecute(block: CommandContext<DefaultSource>.() -> Unit): T =
executes {
block(it)
1
}

View File

@@ -0,0 +1,66 @@
package moe.nea.firmament.commands
import com.mojang.brigadier.CommandDispatcher
import io.github.cottonmc.cotton.gui.client.CottonClientScreen
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.minecraft.text.Text
import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.gui.repoGui
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.ScreenUtil.setScreenLater
fun firmamentCommand() = literal("firmament") {
thenLiteral("repo") {
thenLiteral("reload") {
thenLiteral("fetch") {
thenExecute {
source.sendFeedback(Text.translatable("firmament.repo.reload.network")) // TODO better reporting
RepoManager.launchAsyncUpdate()
}
}
thenExecute {
source.sendFeedback(Text.translatable("firmament.repo.reload.disk"))
RepoManager.reload()
}
}
thenExecute {
setScreenLater(CottonClientScreen(repoGui()))
}
}
thenLiteral("dev") {
thenLiteral("config") {
thenExecute {
FairySouls.TConfig.showConfigEditor()
}
}
thenLiteral("sbdata") {
thenExecute {
source.sendFeedback(Text.translatable("firmament.sbinfo.profile", SBData.profileCuteName))
val locrawInfo = SBData.locraw
if (locrawInfo == null) {
source.sendFeedback(Text.translatable("firmament.sbinfo.nolocraw"))
} else {
source.sendFeedback(Text.translatable("firmament.sbinfo.server", locrawInfo.server))
source.sendFeedback(Text.translatable("firmament.sbinfo.gametype", locrawInfo.gametype))
source.sendFeedback(Text.translatable("firmament.sbinfo.mode", locrawInfo.mode))
source.sendFeedback(Text.translatable("firmament.sbinfo.map", locrawInfo.map))
}
}
}
}
}
fun registerFirmamentCommand(dispatcher: CommandDispatcher<FabricClientCommandSource>) {
val firmament = dispatcher.register(firmamentCommand())
dispatcher.register(literal("firm") {
redirect(firmament)
})
}

View File

@@ -0,0 +1,11 @@
package moe.nea.firmament.dbus
import org.freedesktop.dbus.annotations.DBusInterfaceName
import org.freedesktop.dbus.interfaces.DBusInterface
@DBusInterfaceName("moe.nea.Firmament")
interface FirmamentDbusInterface : DBusInterface {
fun sayHello(): String
fun getCurrentRepoCommit(): String
fun requestRepoReDownload()
}

View File

@@ -0,0 +1,21 @@
package moe.nea.firmament.dbus
import moe.nea.firmament.repo.RepoManager
object FirmamentDbusObject : FirmamentDbusInterface {
override fun sayHello(): String {
return "Hello from Firmanet"
}
override fun getCurrentRepoCommit(): String {
return RepoManager.currentDownloadedSha ?: "none"
}
override fun requestRepoReDownload() {
RepoManager.launchAsyncUpdate()
}
override fun getObjectPath(): String {
return "/moe/nea/Firmament"
}
}

View File

@@ -0,0 +1,36 @@
package moe.nea.firmament.events
/**
* An event that can be fired by a [NEUEventBus].
*
* Typically, that event bus is implemented as a companion object
*
* ```
* class SomeEvent : NEUEvent() {
* companion object : NEUEventBus<SomeEvent>()
* }
* ```
*/
abstract class NEUEvent {
/**
* A [NEUEvent] that can be [cancelled]
*/
abstract class Cancellable : NEUEvent() {
/**
* Cancels this is event.
*
* @see cancelled
*/
fun cancel() {
cancelled = true
}
/**
* Whether this event is cancelled.
*
* Cancelled events will bypass handlers unless otherwise specified and will prevent the action that this
* event was originally fired for.
*/
var cancelled: Boolean = false
}
}

View File

@@ -0,0 +1,37 @@
package moe.nea.firmament.events
import java.util.concurrent.CopyOnWriteArrayList
import moe.nea.firmament.Firmament
/**
* A pubsub event bus.
*
* [subscribe] to events [publish]ed on this event bus.
* Subscriptions may not necessarily be delivered in the order or registering.
*/
open class NEUEventBus<T : NEUEvent> {
data class Handler<T>(val invocation: (T) -> Unit, val receivesCancelled: Boolean)
private val toHandle: MutableList<Handler<T>> = CopyOnWriteArrayList()
fun subscribe(handle: (T) -> Unit) {
subscribe(handle, false)
}
fun subscribe(handle: (T) -> Unit, receivesCancelled: Boolean) {
toHandle.add(Handler(handle, receivesCancelled))
}
fun publish(event: T): T {
for (function in toHandle) {
if (function.receivesCancelled || event !is NEUEvent.Cancellable || !event.cancelled) {
try {
function.invocation(event)
} catch (e: Exception) {
Firmament.logger.error("Caught exception during processing event $event", e)
}
}
}
return event
}
}

View File

@@ -0,0 +1,13 @@
package moe.nea.firmament.events
import net.minecraft.particle.ParticleEffect
import net.minecraft.util.math.Vec3d
data class ParticleSpawnEvent(
val particleEffect: ParticleEffect,
val position: Vec3d,
val offset: Vec3d,
val longDistance: Boolean,
) : NEUEvent() {
companion object : NEUEventBus<ParticleSpawnEvent>()
}

View File

@@ -0,0 +1,7 @@
package moe.nea.firmament.events
import net.minecraft.client.gui.screen.Screen
data class ScreenOpenEvent(val old: Screen?, val new: Screen?) : NEUEvent.Cancellable() {
companion object : NEUEventBus<ScreenOpenEvent>()
}

View File

@@ -0,0 +1,13 @@
package moe.nea.firmament.events
import net.minecraft.text.Text
import moe.nea.firmament.util.unformattedString
/**
* This event gets published whenever the client receives a chat message from the server.
*/
data class ServerChatLineReceivedEvent(val text: Text) : NEUEvent.Cancellable() {
companion object : NEUEventBus<ServerChatLineReceivedEvent>()
val unformattedString = text.unformattedString
}

View File

@@ -0,0 +1,13 @@
package moe.nea.firmament.events
import moe.nea.firmament.util.Locraw
/**
* This event gets published whenever `/locraw` is queried and HyPixel returns a location different to the old one.
*
* **N.B.:** This event may get fired multiple times while on the server (for example, first to null, then to the
* correct location).
*/
data class SkyblockServerUpdateEvent(val oldLocraw: Locraw?, val newLocraw: Locraw?) : NEUEvent() {
companion object : NEUEventBus<SkyblockServerUpdateEvent>()
}

View File

@@ -0,0 +1,5 @@
package moe.nea.firmament.events
class WorldReadyEvent : NEUEvent() {
companion object : NEUEventBus<WorldReadyEvent>()
}

View File

@@ -0,0 +1,22 @@
package moe.nea.firmament.events
import org.joml.Matrix4f
import net.minecraft.client.render.Camera
import net.minecraft.client.render.GameRenderer
import net.minecraft.client.render.LightmapTextureManager
import net.minecraft.client.util.math.MatrixStack
/**
* This event is called after all world rendering is done, but before any GUI rendering (including hand) has been done.
*/
data class WorldRenderLastEvent(
val matrices: MatrixStack,
val tickDelta: Float,
val renderBlockOutline: Boolean,
val camera: Camera,
val gameRenderer: GameRenderer,
val lightmapTextureManager: LightmapTextureManager,
val positionMatrix: Matrix4f,
) : NEUEvent() {
companion object : NEUEventBus<WorldRenderLastEvent>()
}

View File

@@ -0,0 +1,53 @@
package moe.nea.firmament.features
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import moe.nea.firmament.Firmament
import moe.nea.firmament.features.fishing.FishingWarning
import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.util.data.DataHolder
object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) {
@Serializable
data class Config(
val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf()
)
private val features = mutableMapOf<String, NEUFeature>()
private var hasAutoloaded = false
init {
autoload()
}
fun autoload() {
synchronized(this) {
if (hasAutoloaded) return
loadFeature(FairySouls)
loadFeature(FishingWarning)
hasAutoloaded = true
}
}
fun loadFeature(feature: NEUFeature) {
synchronized(features) {
if (feature.identifier in features) {
Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature")
return
}
features[feature.identifier] = feature
feature.onLoad()
}
}
fun isEnabled(identifier: String): Boolean? =
data.enabledFeatures[identifier]
fun setEnabled(identifier: String, value: Boolean) {
data.enabledFeatures[identifier] = value
markDirty()
}
}

View File

@@ -0,0 +1,18 @@
package moe.nea.firmament.features
import moe.nea.firmament.util.config.ManagedConfig
interface NEUFeature {
val name: String
val identifier: String
val defaultEnabled: Boolean
get() = true
var isEnabled: Boolean
get() = FeatureManager.isEnabled(identifier) ?: defaultEnabled
set(value) {
FeatureManager.setEnabled(identifier, value)
}
val config: ManagedConfig? get() = null
fun onLoad()
}

View File

@@ -0,0 +1,117 @@
package moe.nea.firmament.features.fishing
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.acos
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.sqrt
import kotlin.time.Duration.Companion.seconds
import net.minecraft.entity.projectile.FishingBobberEntity
import net.minecraft.particle.ParticleTypes
import net.minecraft.util.math.Vec3d
import moe.nea.firmament.events.ParticleSpawnEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.features.NEUFeature
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.config.ManagedConfig
import moe.nea.firmament.util.render.RenderBlockContext.Companion.renderBlocks
object FishingWarning : NEUFeature {
override val name: String
get() = "Fishing Warning"
override val identifier: String
get() = "fishing-warning"
object TConfig : ManagedConfig("fishing-warning") {
// Display a warning when you are about to hook a fish
val displayWarning by toggle("display-warning") { false }
val highlightWakeChain by toggle("highlight-wake-chain") { false }
}
override val config: ManagedConfig get() = TConfig
data class WakeChain(
val delta: Vec3d,
val momentum: Vec3d,
val lastContinued: TimeMark,
)
val chains = mutableListOf<WakeChain>()
private fun areAnglesClose(a: Double, b: Double, tolerance: Double): Boolean {
var dist = (a - b).absoluteValue
if (180 < dist) dist = 360 - dist;
return dist <= tolerance
}
private fun calculateAngleFromOffsets(xOffset: Double, zOffset: Double): Double {
// See also: Vanilla 1.8.9 Fishing particle code.
var angleX = Math.toDegrees(acos(xOffset / 0.04))
var angleZ = Math.toDegrees(asin(zOffset / 0.04))
if (xOffset < 0) {
// Old: angleZ = 180 - angleZ;
angleZ = 180 - angleZ
}
if (zOffset < 0) {
angleX = 360 - angleX
}
angleX %= 360.0
angleZ %= 360.0
if (angleX < 0) angleX += 360.0
if (angleZ < 0) angleZ += 360.0
var dist = angleX - angleZ
if (dist < -180) dist += 360.0
if (dist > 180) dist -= 360.0
return angleZ + dist / 2
}
private fun toDegrees(d: Double) = d * 180 / Math.PI
fun isHookPossible(hook: FishingBobberEntity, particlePos: Vec3d, angle1: Double, angle2: Double): Boolean {
val dx = particlePos.x - hook.pos.x
val dz = particlePos.z - hook.pos.z
val dist = sqrt(dx * dx + dz * dz)
if (dist < 0.2) return true
val tolerance = toDegrees(atan2(0.03125, dist)) * 1.5
var angleToHook = toDegrees(atan2(dx, dz)) % 360
if (angleToHook < 0) angleToHook += 360
return areAnglesClose(angle1, angleToHook, tolerance) || areAnglesClose(angle2, angleToHook, tolerance)
}
val recentParticles = mutableListOf<Pair<Vec3d, TimeMark>>()
private fun onParticleSpawn(event: ParticleSpawnEvent) {
if (event.particleEffect.type != ParticleTypes.FISHING) return
if (!(abs(event.offset.y - 0.01f) < 0.001f)) return
val hook = MC.player?.fishHook ?: return
val actualOffset = event.offset
val candidate1 = calculateAngleFromOffsets(actualOffset.x, -actualOffset.z)
val candidate2 = calculateAngleFromOffsets(-actualOffset.x, actualOffset.z)
if (isHookPossible(hook, event.position, candidate1, candidate2)) {
recentParticles.add(Pair(event.position, TimeMark.now()))
}
}
override fun onLoad() {
ParticleSpawnEvent.subscribe(::onParticleSpawn)
WorldReadyEvent.subscribe {
recentParticles.clear()
}
WorldRenderLastEvent.subscribe {
recentParticles.removeIf { it.second.passedTime() > 5.seconds }
renderBlocks(it.matrices, it.camera) {
color(0f, 0f, 1f, 1f)
recentParticles.forEach {
tinyBlock(it.first, 0.1F)
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
package moe.nea.firmament.features.world
import io.github.moulberry.repo.data.Coordinate
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import moe.nea.firmament.events.ServerChatLineReceivedEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.features.NEUFeature
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.blockPos
import moe.nea.firmament.util.config.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
import moe.nea.firmament.util.render.RenderBlockContext.Companion.renderBlocks
import moe.nea.firmament.util.unformattedString
object FairySouls : NEUFeature {
@Serializable
data class Data(
val foundSouls: MutableMap<String, MutableSet<Int>> = mutableMapOf()
)
override val config: ManagedConfig
get() = TConfig
object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data)
object TConfig : ManagedConfig("fairy-souls") {
val displaySouls by toggle("show") { false }
val resetSouls by button("reset") {
DConfig.data?.foundSouls?.clear() != null
updateMissingSouls()
}
}
override val name: String get() = "Fairy Souls"
override val identifier: String get() = "fairy-souls"
val playerReach = 5
val playerReachSquared = playerReach * playerReach
var currentLocationName: String? = null
var currentLocationSouls: List<Coordinate> = emptyList()
var currentMissingSouls: List<Coordinate> = emptyList()
fun updateMissingSouls() {
currentMissingSouls = emptyList()
val c = DConfig.data ?: return
val fi = c.foundSouls[currentLocationName] ?: setOf()
val cms = currentLocationSouls.toMutableList()
fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) }
currentMissingSouls = cms
}
fun updateWorldSouls() {
currentLocationSouls = emptyList()
val loc = currentLocationName ?: return
currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc] ?: return
}
fun findNearestClickableSoul(): Coordinate? {
val player = MC.player ?: return null
val pos = player.pos
val location = SBData.skyblockLocation ?: return null
val soulLocations: List<Coordinate> =
RepoManager.neuRepo.constants.fairySouls.soulLocations[location] ?: return null
return soulLocations
.map { it to it.blockPos.getSquaredDistance(pos) }
.filter { it.second < playerReachSquared }
.minByOrNull { it.second }
?.first
}
private fun markNearestSoul() {
val nearestSoul = findNearestClickableSoul() ?: return
val c = DConfig.data ?: return
val loc = currentLocationName ?: return
val idx = currentLocationSouls.indexOf(nearestSoul)
c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx)
DConfig.markDirty()
updateMissingSouls()
}
override fun onLoad() {
SkyblockServerUpdateEvent.subscribe {
currentLocationName = it.newLocraw?.skyblockLocation
updateWorldSouls()
updateMissingSouls()
}
ServerChatLineReceivedEvent.subscribe {
when (it.text.unformattedString) {
"You have already found that Fairy Soul!" -> {
markNearestSoul()
}
"SOUL! You found a Fairy Soul!" -> {
markNearestSoul()
}
}
}
WorldRenderLastEvent.subscribe {
if (!TConfig.displaySouls) return@subscribe
renderBlocks(it.matrices, it.camera) {
color(1F, 1F, 0F, 0.8F)
currentMissingSouls.forEach {
block(it.blockPos)
}
}
}
}
}

View File

@@ -0,0 +1,94 @@
package moe.nea.firmament.gui
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.WTextField
import io.github.cottonmc.cotton.gui.widget.WToggleButton
import io.github.cottonmc.cotton.gui.widget.data.HorizontalAlignment
import io.github.cottonmc.cotton.gui.widget.data.Insets
import io.github.cottonmc.cotton.gui.widget.data.VerticalAlignment
import moe.nea.firmament.Firmament
import moe.nea.firmament.util.data.DataHolder
import net.minecraft.text.Text
import kotlin.reflect.KMutableProperty1
class ConfigGui<K>(val holder: DataHolder<K>, val build: ConfigGui<K>.() -> Unit) : LightweightGuiDescription() {
private val root = WGridPanelWithPadding(verticalPadding = 4)
private val reloadables = mutableListOf<(() -> Unit)>()
init {
setRootPanel(root)
root.insets = Insets.ROOT_PANEL
build()
reload()
}
fun title(text: Text) {
if (col != 0) {
Firmament.logger.warn("Set title not at the top of the ConfigGui")
}
val label = WLabel(text)
label.verticalAlignment = VerticalAlignment.TOP
label.horizontalAlignment = HorizontalAlignment.CENTER
root.add(label, 0, col, 11, 1)
col++
}
private fun label(text: Text) {
val label = WLabel(text)
label.verticalAlignment = VerticalAlignment.CENTER
root.add(label, 0, col, 5, 1)
}
fun toggle(text: Text, prop: KMutableProperty1<K, Boolean>) {
val toggle = WToggleButton(text)
reloadables.add { toggle.toggle = prop.get(holder.data) }
toggle.setOnToggle {
prop.set(holder.data, true)
holder.markDirty()
}
root.add(toggle, 5, col, 6, 1)
label(text)
col++
}
fun button(text: Text, buttonText: Text, runnable: () -> Unit) {
val button = WButton(buttonText)
button.setOnClick {
runnable.invoke()
}
root.add(button, 5, col, 6, 1)
label(text)
col++
}
fun textfield(
text: Text,
background: Text,
prop: KMutableProperty1<K, String>,
maxLength: Int = 255
) {
val textfield = WTextField(background)
textfield.isEditable = true
reloadables.add {
textfield.text = prop.get(holder.data)
}
textfield.maxLength = maxLength
textfield.setChangedListener {
prop.set(holder.data, it)
holder.markDirty()
}
root.add(textfield, 5, col, 6, 11)
label(text)
col++
}
fun reload() {
reloadables.forEach { it.invoke() }
}
private var col = 0
}

View File

@@ -0,0 +1,33 @@
package moe.nea.firmament.gui
import io.github.cottonmc.cotton.gui.widget.WPanelWithInsets
import io.github.cottonmc.cotton.gui.widget.WWidget
import io.github.cottonmc.cotton.gui.widget.data.Insets
class WGridPanelWithPadding(
val grid: Int = 18,
val verticalPadding: Int = 0,
val horizontalPadding: Int = 0,
) : WPanelWithInsets() {
private inline val vertOffset get() = grid + verticalPadding
private inline val horiOffset get() = grid + horizontalPadding
fun add(w: WWidget, x: Int, y: Int, width: Int = 1, height: Int = 1) {
children.add(w)
w.parent = this
w.setLocation(x * horiOffset + insets.left, y * vertOffset + insets.top)
if (w.canResize())
w.setSize(
grid + (horiOffset * (width - 1)),
grid + (vertOffset * (height - 1)),
)
expandToFit(w, insets)
}
override fun setInsets(insets: Insets): WGridPanelWithPadding {
super.setInsets(insets)
return this
}
}

View File

@@ -0,0 +1,36 @@
package moe.nea.firmament.gui
import net.minecraft.text.Text
import moe.nea.firmament.repo.RepoManager
fun repoGui(): ConfigGui<RepoManager.Config> {
return ConfigGui(RepoManager) {
title(Text.translatable("firmament.gui.repo.title"))
toggle(Text.translatable("firmament.gui.repo.autoupdate"), RepoManager.Config::autoUpdate)
textfield(
Text.translatable("firmament.gui.repo.username"),
Text.translatable("firmament.gui.repo.hint.username"),
RepoManager.Config::user,
maxLength = 255
)
textfield(
Text.translatable("firmament.gui.repo.reponame"),
Text.translatable("firmament.gui.repo.hint.reponame"),
RepoManager.Config::repo
)
textfield(
Text.translatable("firmament.gui.repo.branch"),
Text.translatable("firmament.gui.repo.hint.branch"),
RepoManager.Config::branch
)
button(
Text.translatable("firmament.gui.repo.reset.label"),
Text.translatable("firmament.gui.repo.reset"),
) {
RepoManager.data.user = "NotEnoughUpdates"
RepoManager.data.repo = "NotEnoughUpdates-REPO"
RepoManager.data.branch = "dangerous"
reload()
}
}
}

View File

@@ -0,0 +1,63 @@
package moe.nea.firmament.hud
import io.github.cottonmc.cotton.gui.client.ScreenDrawing
import io.github.cottonmc.cotton.gui.widget.WWidget
import io.github.cottonmc.cotton.gui.widget.data.HorizontalAlignment
import io.github.cottonmc.cotton.gui.widget.data.Insets
import net.minecraft.client.util.math.MatrixStack
import kotlin.math.roundToInt
import kotlin.math.sin
val Insets.vertical get() = bottom + top
val Insets.horizontal get() = left + right
class ProgressBar(
var label: String,
var total: Int?, // If total is null, then make it a bouncy rectangle
var progress: Int = 0,
) : WWidget() {
var insets: Insets = Insets(7)
override fun canResize(): Boolean = true
fun reportProgress(label: String, progress: Int, total: Int?) {
synchronized(this) {
this.label = label
this.progress = progress
this.total = total
}
}
override fun paint(matrices: MatrixStack, x: Int, y: Int, mouseX: Int, mouseY: Int) {
ScreenDrawing.coloredRect(matrices, x, y, width, height, 0xFF808080.toInt())
val (l, prog) = synchronized(this) {
label to (progress to total)
}
val (p, t) = prog
if (t == null) {
ScreenDrawing.coloredRect(
matrices,
(x + (1 + sin(System.currentTimeMillis().toDouble() / 1000)) * width * 3 / 4 / 2).roundToInt(),
y,
width / 4,
height,
0xFF00FF00.toInt()
)
} else {
ScreenDrawing.coloredRect(matrices, x, y, width * p / t, height, 0xFF00FF00.toInt())
}
ScreenDrawing.drawString(
matrices,
if (t != null) "$l ($p/$t)" else l,
HorizontalAlignment.CENTER,
x + insets.left,
y + insets.top,
width - insets.horizontal,
height - insets.vertical,
)
}
}

View File

@@ -0,0 +1,53 @@
package moe.nea.firmament.recipes
import io.github.moulberry.repo.data.NEUCraftingRecipe
import io.github.moulberry.repo.data.NEUIngredient
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import me.shedaniel.rei.api.client.gui.Renderer
import me.shedaniel.rei.api.client.gui.widgets.Widget
import me.shedaniel.rei.api.client.gui.widgets.Widgets
import me.shedaniel.rei.api.client.registry.display.DisplayCategory
import me.shedaniel.rei.api.common.category.CategoryIdentifier
import me.shedaniel.rei.api.common.util.EntryStacks
import net.minecraft.block.Blocks
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.rei.SBItemEntryDefinition
class SBCraftingRecipe(override val neuRecipe: NEUCraftingRecipe) : SBRecipe() {
override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.catIdentifier
object Category : DisplayCategory<SBCraftingRecipe> {
val catIdentifier = CategoryIdentifier.of<SBCraftingRecipe>(Firmament.MOD_ID, "crafing_recipe")
override fun getCategoryIdentifier(): CategoryIdentifier<out SBCraftingRecipe> = catIdentifier
override fun getTitle(): Text = Text.literal("SkyBlock Crafting")
override fun getIcon(): Renderer = EntryStacks.of(Blocks.CRAFTING_TABLE)
override fun setupDisplay(display: SBCraftingRecipe, bounds: Rectangle): List<Widget> {
val point = Point(bounds.centerX - 58, bounds.centerY - 27)
return buildList {
add(Widgets.createRecipeBase(bounds))
add(Widgets.createArrow(Point(point.x + 60, point.y + 18)))
add(Widgets.createResultSlotBackground(Point(point.x + 95, point.y + 19)))
for (i in 0 until 3) {
for (j in 0 until 3) {
val slot = Widgets.createSlot(Point(point.x + 1 + i * 18, point.y + 1 + j * 18)).markInput()
add(slot)
val item = display.neuRecipe.inputs[i + j * 3]
if (item == NEUIngredient.SENTINEL_EMPTY) continue
slot.entry(SBItemEntryDefinition.getEntry(item)) // TODO: make use of stackable item entries
}
}
add(
Widgets.createSlot(Point(point.x + 95, point.y + 19))
.entry(SBItemEntryDefinition.getEntry(display.neuRecipe.output))
.disableBackground().markOutput()
)
}
}
}
}

View File

@@ -0,0 +1,53 @@
package moe.nea.firmament.recipes
import io.github.moulberry.repo.data.NEUForgeRecipe
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import me.shedaniel.rei.api.client.gui.Renderer
import me.shedaniel.rei.api.client.gui.widgets.Widget
import me.shedaniel.rei.api.client.gui.widgets.Widgets
import me.shedaniel.rei.api.client.registry.display.DisplayCategory
import me.shedaniel.rei.api.common.category.CategoryIdentifier
import me.shedaniel.rei.api.common.util.EntryStacks
import net.minecraft.block.Blocks
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.rei.SBItemEntryDefinition
class SBForgeRecipe(override val neuRecipe: NEUForgeRecipe) : SBRecipe() {
override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.categoryIdentifier
object Category : DisplayCategory<SBForgeRecipe> {
override fun getCategoryIdentifier(): CategoryIdentifier<SBForgeRecipe> =
CategoryIdentifier.of(Firmament.MOD_ID, "forge_recipe")
override fun getTitle(): Text = Text.literal("Forge Recipes")
override fun getDisplayHeight(): Int {
return super.getDisplayHeight()
}
override fun getIcon(): Renderer = EntryStacks.of(Blocks.ANVIL)
override fun setupDisplay(display: SBForgeRecipe, bounds: Rectangle): List<Widget> {
return buildList {
// TODO: proper gui for this (possibly inspired by the old circular gui)
add(Widgets.createRecipeBase(bounds))
val resultSlot = Point(bounds.centerX, bounds.centerY + 5)
add(Widgets.createResultSlotBackground(resultSlot))
val ingredientsCenter = Point(bounds.centerX, bounds.centerY - 20)
val count = display.neuRecipe.inputs.size
display.neuRecipe.inputs.forEachIndexed { idx, ingredient ->
add(
Widgets.createSlot(
Point(ingredientsCenter.x + 12 - count * 24 / 2 + idx * 24, ingredientsCenter.y)
).markInput().entry(SBItemEntryDefinition.getEntry(ingredient))
)
}
add(
Widgets.createSlot(resultSlot).markOutput().disableBackground()
.entry(SBItemEntryDefinition.getEntry(display.neuRecipe.outputStack))
)
}
}
}
}

View File

@@ -0,0 +1,24 @@
package moe.nea.firmament.recipes
import io.github.moulberry.repo.data.NEURecipe
import me.shedaniel.rei.api.common.display.Display
import me.shedaniel.rei.api.common.entry.EntryIngredient
import moe.nea.firmament.rei.SBItemEntryDefinition
import moe.nea.firmament.util.SkyblockId
abstract class SBRecipe() : Display {
abstract val neuRecipe: NEURecipe
override fun getInputEntries(): List<EntryIngredient> {
return neuRecipe.allInputs.map {
val entryStack = SBItemEntryDefinition.getEntry(SkyblockId(it.itemId))
EntryIngredient.of(entryStack)
}
}
override fun getOutputEntries(): List<EntryIngredient> {
return neuRecipe.allOutputs.map {
val entryStack = SBItemEntryDefinition.getEntry(SkyblockId(it.itemId))
EntryIngredient.of(entryStack)
}
}
}

View File

@@ -0,0 +1,73 @@
package moe.nea.firmament.rei
import io.github.moulberry.repo.data.NEUItem
import me.shedaniel.rei.api.client.plugins.REIClientPlugin
import me.shedaniel.rei.api.client.registry.category.CategoryRegistry
import me.shedaniel.rei.api.client.registry.display.DisplayRegistry
import me.shedaniel.rei.api.client.registry.entry.CollapsibleEntryRegistry
import me.shedaniel.rei.api.client.registry.entry.EntryRegistry
import me.shedaniel.rei.api.client.registry.screen.ScreenRegistry
import me.shedaniel.rei.api.common.entry.EntryStack
import me.shedaniel.rei.api.common.entry.type.EntryTypeRegistry
import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes
import net.minecraft.item.ItemStack
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import moe.nea.firmament.recipes.SBCraftingRecipe
import moe.nea.firmament.recipes.SBForgeRecipe
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SkyblockId
class FirmamentReiPlugin : REIClientPlugin {
companion object {
fun EntryStack<NEUItem>.asItemEntry(): EntryStack<ItemStack> {
return EntryStack.of(VanillaEntryTypes.ITEM, value.asItemStack())
}
val SKYBLOCK_ITEM_TYPE_ID = Identifier("firmament", "skyblockitems")
}
override fun registerEntryTypes(registry: EntryTypeRegistry) {
registry.register(SKYBLOCK_ITEM_TYPE_ID, SBItemEntryDefinition)
}
override fun registerCategories(registry: CategoryRegistry) {
registry.add(SBCraftingRecipe.Category)
registry.add(SBForgeRecipe.Category)
}
override fun registerDisplays(registry: DisplayRegistry) {
registry.registerDisplayGenerator(
SBCraftingRecipe.Category.catIdentifier,
SkyblockCraftingRecipeDynamicGenerator
)
registry.registerDisplayGenerator(
SBForgeRecipe.Category.categoryIdentifier,
SkyblockForgeRecipeDynamicGenerator
)
}
override fun registerCollapsibleEntries(registry: CollapsibleEntryRegistry) {
RepoManager.neuRepo.constants.parents.parents
.forEach { (parent, children) ->
registry.group(
SkyblockId(parent).identifier,
Text.literal(RepoManager.getNEUItem(SkyblockId(parent))?.displayName ?: parent),
(children + parent).map { SBItemEntryDefinition.getEntry(RepoManager.getNEUItem(SkyblockId(it))) })
}
}
override fun registerScreens(registry: ScreenRegistry) {
registry.registerFocusedStack(SkyblockItemIdFocusedStackProvider)
}
override fun registerEntries(registry: EntryRegistry) {
RepoManager.neuRepo.items?.items?.values?.forEach {
if (!it.isVanilla)
registry.addEntry(EntryStack.of(SBItemEntryDefinition, it))
}
}
}

View File

@@ -0,0 +1,31 @@
package moe.nea.firmament.rei
import io.github.moulberry.repo.data.NEUItem
import me.shedaniel.math.Rectangle
import me.shedaniel.rei.api.client.entry.renderer.EntryRenderer
import me.shedaniel.rei.api.client.gui.widgets.Tooltip
import me.shedaniel.rei.api.client.gui.widgets.TooltipContext
import me.shedaniel.rei.api.common.entry.EntryStack
import net.minecraft.client.util.math.MatrixStack
import moe.nea.firmament.rei.FirmamentReiPlugin.Companion.asItemEntry
object NEUItemEntryRenderer : EntryRenderer<NEUItem> {
override fun render(
entry: EntryStack<NEUItem>,
matrices: MatrixStack,
bounds: Rectangle,
mouseX: Int,
mouseY: Int,
delta: Float
) {
matrices.push()
matrices.translate(0F, 0F, 100F)
entry.asItemEntry().render(matrices, bounds, mouseX, mouseY, delta)
matrices.pop()
}
override fun getTooltip(entry: EntryStack<NEUItem>, tooltipContext: TooltipContext): Tooltip? {
return entry.asItemEntry().getTooltip(tooltipContext, false)
}
}

View File

@@ -0,0 +1,25 @@
package moe.nea.firmament.rei
import io.github.moulberry.repo.data.NEUItem
import me.shedaniel.rei.api.common.entry.EntrySerializer
import me.shedaniel.rei.api.common.entry.EntryStack
import net.minecraft.nbt.NbtCompound
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SkyblockId
object NEUItemEntrySerializer : EntrySerializer<NEUItem?> {
const val SKYBLOCK_ID_ENTRY = "SKYBLOCK_ID"
override fun supportSaving(): Boolean = true
override fun supportReading(): Boolean = true
override fun read(tag: NbtCompound): NEUItem? {
return RepoManager.getNEUItem(SkyblockId(tag.getString(SKYBLOCK_ID_ENTRY)))
}
override fun save(entry: EntryStack<NEUItem?>, value: NEUItem?): NbtCompound {
return NbtCompound().apply {
putString(SKYBLOCK_ID_ENTRY, value?.skyblockItemId ?: "null")
}
}
}

View File

@@ -0,0 +1,85 @@
package moe.nea.firmament.rei
import io.github.moulberry.repo.data.NEUIngredient
import io.github.moulberry.repo.data.NEUItem
import java.util.stream.Stream
import me.shedaniel.rei.api.client.entry.renderer.EntryRenderer
import me.shedaniel.rei.api.common.entry.EntrySerializer
import me.shedaniel.rei.api.common.entry.EntryStack
import me.shedaniel.rei.api.common.entry.comparison.ComparisonContext
import me.shedaniel.rei.api.common.entry.type.EntryDefinition
import me.shedaniel.rei.api.common.entry.type.EntryType
import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes
import net.minecraft.item.ItemStack
import net.minecraft.registry.tag.TagKey
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import moe.nea.firmament.rei.FirmamentReiPlugin.Companion.asItemEntry
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.ItemCache.getIdentifier
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SkyblockId
// TODO: allow stackable entries
object SBItemEntryDefinition : EntryDefinition<NEUItem> {
override fun equals(o1: NEUItem?, o2: NEUItem?, context: ComparisonContext?): Boolean {
return o1 === o2
}
override fun cheatsAs(entry: EntryStack<NEUItem>?, value: NEUItem?): ItemStack {
return value.asItemStack()
}
override fun getValueType(): Class<NEUItem> = NEUItem::class.java
override fun getType(): EntryType<NEUItem> = EntryType.deferred(FirmamentReiPlugin.SKYBLOCK_ITEM_TYPE_ID)
override fun getRenderer(): EntryRenderer<NEUItem> = NEUItemEntryRenderer
override fun getSerializer(): EntrySerializer<NEUItem?> {
return NEUItemEntrySerializer
}
override fun getTagsFor(entry: EntryStack<NEUItem>?, value: NEUItem?): Stream<out TagKey<*>>? {
return Stream.empty()
}
override fun asFormattedText(entry: EntryStack<NEUItem>, value: NEUItem): Text {
return VanillaEntryTypes.ITEM.definition.asFormattedText(entry.asItemEntry(), value.asItemStack())
}
override fun hash(entry: EntryStack<NEUItem>, value: NEUItem?, context: ComparisonContext): Long {
// Repo items are immutable, and get replaced entirely when loaded from disk
return System.identityHashCode(value) * 31L
}
override fun wildcard(entry: EntryStack<NEUItem>?, value: NEUItem?): NEUItem? {
return value
}
override fun normalize(entry: EntryStack<NEUItem>?, value: NEUItem?): NEUItem? {
return value
}
override fun copy(entry: EntryStack<NEUItem>?, value: NEUItem?): NEUItem? {
return value
}
override fun isEmpty(entry: EntryStack<NEUItem>?, value: NEUItem?): Boolean {
return false
}
override fun getIdentifier(entry: EntryStack<NEUItem>?, value: NEUItem?): Identifier {
return value?.getIdentifier() ?: Identifier.of("skyblockitem", "null")!!
}
fun getEntry(neuItem: NEUItem?): EntryStack<NEUItem> =
EntryStack.of(this, neuItem)
fun getEntry(skyblockId: SkyblockId?): EntryStack<NEUItem> =
EntryStack.of(this, skyblockId?.let { RepoManager.getNEUItem(it) })
fun getEntry(ingredient: NEUIngredient?): EntryStack<NEUItem> =
getEntry(ingredient?.itemId?.let { SkyblockId(it) })
}

View File

@@ -0,0 +1,52 @@
package moe.nea.firmament.rei
import io.github.moulberry.repo.data.NEUCraftingRecipe
import io.github.moulberry.repo.data.NEUForgeRecipe
import io.github.moulberry.repo.data.NEUItem
import io.github.moulberry.repo.data.NEURecipe
import java.util.*
import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator
import me.shedaniel.rei.api.client.view.ViewSearchBuilder
import me.shedaniel.rei.api.common.display.Display
import me.shedaniel.rei.api.common.entry.EntryStack
import moe.nea.firmament.recipes.SBCraftingRecipe
import moe.nea.firmament.recipes.SBForgeRecipe
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.skyblockId
val SkyblockCraftingRecipeDynamicGenerator = neuDisplayGenerator<SBCraftingRecipe, NEUCraftingRecipe> {
SBCraftingRecipe(it)
}
val SkyblockForgeRecipeDynamicGenerator = neuDisplayGenerator<SBForgeRecipe, NEUForgeRecipe> {
SBForgeRecipe(it)
}
inline fun <D : Display, reified T : NEURecipe> neuDisplayGenerator(noinline mapper: (T) -> D) =
object : DynamicDisplayGenerator<D> {
override fun getRecipeFor(entry: EntryStack<*>): Optional<List<D>> {
if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
val item = entry.castValue<NEUItem>()
val recipes = RepoManager.getRecipesFor(item.skyblockId)
val craftingRecipes = recipes.filterIsInstance<T>()
return Optional.of(craftingRecipes.map(mapper))
}
override fun generate(builder: ViewSearchBuilder): Optional<List<D>> {
if (SBCraftingRecipe.Category.catIdentifier !in builder.categories) return Optional.empty()
return Optional.of(
RepoManager.getAllRecipes().filterIsInstance<T>().map(mapper)
.toList()
)
}
override fun getUsageFor(entry: EntryStack<*>): Optional<List<D>> {
if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
val item = entry.castValue<NEUItem>()
val recipes = RepoManager.getUsagesFor(item.skyblockId)
val craftingRecipes = recipes.filterIsInstance<T>()
return Optional.of(craftingRecipes.map(mapper))
}
}

View File

@@ -0,0 +1,25 @@
package moe.nea.firmament.rei
import dev.architectury.event.CompoundEventResult
import me.shedaniel.math.Point
import me.shedaniel.rei.api.client.registry.screen.FocusedStackProvider
import me.shedaniel.rei.api.common.entry.EntryStack
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.skyBlockId
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.screen.ingame.HandledScreen
object SkyblockItemIdFocusedStackProvider : FocusedStackProvider {
override fun provide(screen: Screen?, mouse: Point?): CompoundEventResult<EntryStack<*>> {
if (screen !is HandledScreen<*>) return CompoundEventResult.pass()
screen as AccessorHandledScreen
val focusedSlot = screen.focusedSlot_NEU ?: return CompoundEventResult.pass()
val item = focusedSlot.stack ?: return CompoundEventResult.pass()
val skyblockId = item.skyBlockId ?: return CompoundEventResult.pass()
val neuItem = RepoManager.getNEUItem(skyblockId) ?: return CompoundEventResult.interrupt(false, null)
return CompoundEventResult.interruptTrue(EntryStack.of(SBItemEntryDefinition, neuItem))
}
override fun getPriority(): Double = 1_000_000.0
}

View File

@@ -0,0 +1,123 @@
package moe.nea.firmament.repo
import com.mojang.serialization.Dynamic
import io.github.cottonmc.cotton.gui.client.CottonHud
import io.github.moulberry.repo.IReloadable
import io.github.moulberry.repo.NEURepository
import io.github.moulberry.repo.data.NEUItem
import java.io.PrintWriter
import java.nio.file.Path
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.io.path.absolutePathString
import kotlin.io.path.writer
import net.minecraft.SharedConstants
import net.minecraft.client.resource.language.I18n
import net.minecraft.datafixer.Schemas
import net.minecraft.datafixer.TypeReferences
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtOps
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.util.LegacyTagParser
import moe.nea.firmament.util.appendLore
import moe.nea.firmament.util.skyblockId
object ItemCache : IReloadable {
val dfuLog = Path.of("logs/dfulog.txt")
private val cache: MutableMap<String, ItemStack> = ConcurrentHashMap()
private val df = Schemas.getFixer()
private val dfuHandle = PrintWriter(dfuLog.writer())
var isFlawless = true
private set
private fun NEUItem.get10809CompoundTag(): NbtCompound = NbtCompound().apply {
put("tag", LegacyTagParser.parse(nbttag))
putString("id", minecraftItemId)
putByte("Count", 1)
putShort("Damage", damage.toShort())
}
private fun NbtCompound.transformFrom10809ToModern(): NbtCompound? =
try {
df.update(
TypeReferences.ITEM_STACK,
Dynamic(NbtOps.INSTANCE, this),
-1,
SharedConstants.getGameVersion().saveVersion.id
).value as NbtCompound
} catch (e: Exception) {
if (isFlawless)
Firmament.logger.error("Failed to run data fixer an item. Check ${dfuLog.absolutePathString()} for more information")
isFlawless = false
e.printStackTrace(dfuHandle)
null
}
fun brokenItemStack(neuItem: NEUItem?): ItemStack {
return ItemStack(Items.PAINTING).apply {
setCustomName(Text.literal(neuItem?.displayName ?: "null"))
appendLore(listOf(Text.translatable("firmament.repo.brokenitem", neuItem?.skyblockItemId)))
}
}
private fun NEUItem.asItemStackNow(): ItemStack {
try {
val oldItemTag = get10809CompoundTag()
val modernItemTag = oldItemTag.transformFrom10809ToModern()
?: return brokenItemStack(this)
val itemInstance = ItemStack.fromNbt(modernItemTag)
if (itemInstance.nbt?.contains("Enchantments") == true) {
itemInstance.enchantments.add(NbtCompound())
}
return itemInstance
} catch (e: Exception) {
e.printStackTrace()
return brokenItemStack(this)
}
}
fun NEUItem?.asItemStack(): ItemStack {
if (this == null) return brokenItemStack(null)
var s = cache[this.skyblockItemId]
if (s == null) {
s = asItemStackNow()
cache[this.skyblockItemId] = s
}
return s
}
fun NEUItem.getIdentifier() = skyblockId.identifier
var job: Job? = null
override fun reload(repository: NEURepository) {
val j = job
if (j != null && j.isActive) {
j.cancel()
}
cache.clear()
isFlawless = true
job = Firmament.coroutineScope.launch {
val items = repository.items?.items
if (items == null) {
CottonHud.remove(RepoManager.progressBar)
return@launch
}
val recacheItems = I18n.translate("firmament.repo.cache")
RepoManager.progressBar.reportProgress(recacheItems, 0, items.size)
CottonHud.add(RepoManager.progressBar)
var i = 0
items.values.forEach {
it.asItemStack() // Rebuild cache
RepoManager.progressBar.reportProgress(recacheItems, i++, items.size)
}
CottonHud.remove(RepoManager.progressBar)
}
}
}

View File

@@ -0,0 +1,118 @@
package moe.nea.firmament.repo
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.utils.io.jvm.nio.*
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import moe.nea.firmament.Firmament
import moe.nea.firmament.Firmament.logger
import moe.nea.firmament.util.iterate
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.zip.ZipInputStream
import kotlin.io.path.*
object RepoDownloadManager {
val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted")
val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt")
private fun loadSavedVersionHash(): String? =
if (repoSavedLocation.exists()) {
if (repoMetadataLocation.exists()) {
try {
repoMetadataLocation.readText().trim()
} catch (e: IOException) {
null
}
} else {
null
}
} else null
private fun saveVersionHash(versionHash: String) {
latestSavedVersionHash = versionHash
repoMetadataLocation.writeText(versionHash)
}
var latestSavedVersionHash: String? = loadSavedVersionHash()
private set
@Serializable
private class GithubCommitsResponse(val sha: String)
private suspend fun requestLatestGithubSha(): String? {
val response =
Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.data.user}/${RepoManager.data.repo}/commits/${RepoManager.data.branch}")
if (response.status.value != 200) {
return null
}
return response.body<GithubCommitsResponse>().sha
}
private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) {
val response = Firmament.httpClient.get(url)
val targetFile = Files.createTempFile("firmament-repo", ".zip")
val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
response.bodyAsChannel().copyTo(outputChannel)
targetFile
}
/**
* Downloads the latest repository from github, setting [latestSavedVersionHash].
* @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update)
*/
suspend fun downloadUpdate(force: Boolean): Boolean = withContext(CoroutineName("Repo Update Check")) {
val latestSha = requestLatestGithubSha()
if (latestSha == null) {
logger.warn("Could not request github API to retrieve latest REPO sha.")
return@withContext false
}
val currentSha = loadSavedVersionHash()
if (latestSha != currentSha || force) {
val requestUrl = "https://github.com/${RepoManager.data.user}/${RepoManager.data.repo}/archive/$latestSha.zip"
logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl")
val zipFile = downloadGithubArchive(requestUrl)
logger.info("Download repository zip file to $zipFile. Deleting old repository")
withContext(IO) { repoSavedLocation.toFile().deleteRecursively() }
logger.info("Extracting new repository")
withContext(IO) { extractNewRepository(zipFile) }
logger.info("Repository loaded on disk.")
saveVersionHash(latestSha)
return@withContext true
} else {
logger.debug("Repository on latest sha $currentSha. Not performing update")
return@withContext false
}
}
private fun extractNewRepository(zipFile: Path) {
repoSavedLocation.createDirectories()
ZipInputStream(zipFile.inputStream()).use { cis ->
while (true) {
val entry = cis.nextEntry ?: break
if (entry.isDirectory) continue
val extractedLocation =
repoSavedLocation.resolve(
entry.name.substringAfter('/', missingDelimiterValue = "")
)
if (repoSavedLocation !in extractedLocation.iterate { it.parent }) {
logger.error("Not Enough Updates detected an invalid zip file. This is a potential security risk, please report this in the Moulberry discord.")
throw RuntimeException("Not Enough Updates detected an invalid zip file. This is a potential security risk, please report this in the Moulberry discord.")
}
extractedLocation.parent.createDirectories()
cis.copyTo(extractedLocation.outputStream())
cis.closeEntry()
}
}
}
}

View File

@@ -0,0 +1,102 @@
package moe.nea.firmament.repo
import io.github.cottonmc.cotton.gui.client.CottonHud
import io.github.moulberry.repo.NEURecipeCache
import io.github.moulberry.repo.NEURepository
import io.github.moulberry.repo.NEURepositoryException
import io.github.moulberry.repo.data.NEURecipe
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import net.minecraft.client.MinecraftClient
import net.minecraft.network.packet.s2c.play.SynchronizeRecipesS2CPacket
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.Firmament.logger
import moe.nea.firmament.hud.ProgressBar
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.data.DataHolder
object RepoManager : DataHolder<RepoManager.Config>(serializer(), "repo", ::Config) {
@Serializable
data class Config(
var user: String = "NotEnoughUpdates",
var repo: String = "NotEnoughUpdates-REPO",
var autoUpdate: Boolean = true,
var branch: String = "dangerous",
)
val currentDownloadedSha by RepoDownloadManager::latestSavedVersionHash
var recentlyFailedToUpdateItemList = false
val progressBar = ProgressBar("", null, 0).also {
it.setSize(180, 22)
}
val neuRepo: NEURepository = NEURepository.of(RepoDownloadManager.repoSavedLocation).apply {
registerReloadListener(ItemCache)
registerReloadListener {
if (!trySendClientboundUpdateRecipesPacket()) {
logger.warn("Failed to issue a ClientboundUpdateRecipesPacket (to reload REI). This may lead to an outdated item list.")
recentlyFailedToUpdateItemList = true
}
}
}
private val recipeCache = NEURecipeCache.forRepo(neuRepo)
fun getAllRecipes() = neuRepo.items.items.values.asSequence().flatMap { it.recipes }
fun getRecipesFor(skyblockId: SkyblockId): Set<NEURecipe> = recipeCache.recipes[skyblockId.neuItem] ?: setOf()
fun getUsagesFor(skyblockId: SkyblockId): Set<NEURecipe> = recipeCache.usages[skyblockId.neuItem] ?: setOf()
private fun trySendClientboundUpdateRecipesPacket(): Boolean {
return MinecraftClient.getInstance().world != null && MinecraftClient.getInstance().networkHandler?.onSynchronizeRecipes(
SynchronizeRecipesS2CPacket(mutableListOf())
) != null
}
init {
ClientTickEvents.START_WORLD_TICK.register(ClientTickEvents.StartWorldTick {
if (recentlyFailedToUpdateItemList && trySendClientboundUpdateRecipesPacket())
recentlyFailedToUpdateItemList = false
})
}
fun getNEUItem(skyblockId: SkyblockId) = neuRepo.items.getItemBySkyblockId(skyblockId.neuItem)
fun launchAsyncUpdate(force: Boolean = false) {
Firmament.coroutineScope.launch {
progressBar.reportProgress("Downloading", 0, null)
CottonHud.add(progressBar)
RepoDownloadManager.downloadUpdate(force)
progressBar.reportProgress("Download complete", 1, 1)
reload()
}
}
fun reload() {
try {
progressBar.reportProgress("Reloading from Disk", 0, null)
CottonHud.add(progressBar)
neuRepo.reload()
} catch (exc: NEURepositoryException) {
MinecraftClient.getInstance().player?.sendMessage(
Text.literal("Failed to reload repository. This will result in some mod features not working.")
)
CottonHud.remove(progressBar)
exc.printStackTrace()
}
}
fun initialize() {
if (data.autoUpdate) {
launchAsyncUpdate()
} else {
reload()
}
}
}

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(), "")