aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/gui
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
committerLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
commitd2f240ff0ca0d27f417f837e706c781a98c31311 (patch)
tree0db7aff6cc14deaf36eed83889d59fd6b3a6f599 /src/main/kotlin/gui
parenta6906308163aa3b2d18fa1dc1aa71ac9bbcc83ab (diff)
downloadFirmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.gz
Firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.bz2
Firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.zip
Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
Diffstat (limited to 'src/main/kotlin/gui')
-rw-r--r--src/main/kotlin/gui/BarComponent.kt125
-rw-r--r--src/main/kotlin/gui/FirmButtonComponent.kt81
-rw-r--r--src/main/kotlin/gui/FirmHoverComponent.kt59
-rw-r--r--src/main/kotlin/gui/FixedComponent.kt38
-rw-r--r--src/main/kotlin/gui/ImageComponent.kt33
-rw-r--r--src/main/kotlin/gui/TickComponent.kt18
-rw-r--r--src/main/kotlin/gui/config/AllConfigsGui.kt46
-rw-r--r--src/main/kotlin/gui/config/BooleanHandler.kt37
-rw-r--r--src/main/kotlin/gui/config/ClickHandler.kt24
-rw-r--r--src/main/kotlin/gui/config/DurationHandler.kt58
-rw-r--r--src/main/kotlin/gui/config/GuiAppender.kt40
-rw-r--r--src/main/kotlin/gui/config/HudMetaHandler.kt39
-rw-r--r--src/main/kotlin/gui/config/IntegerHandler.kt54
-rw-r--r--src/main/kotlin/gui/config/JAnyHud.kt48
-rw-r--r--src/main/kotlin/gui/config/KeyBindingHandler.kt149
-rw-r--r--src/main/kotlin/gui/config/ManagedConfig.kt181
-rw-r--r--src/main/kotlin/gui/config/ManagedConfigElement.kt8
-rw-r--r--src/main/kotlin/gui/config/ManagedOption.kt62
-rw-r--r--src/main/kotlin/gui/config/StringHandler.kt36
-rw-r--r--src/main/kotlin/gui/entity/EntityModifier.kt9
-rw-r--r--src/main/kotlin/gui/entity/EntityRenderer.kt197
-rw-r--r--src/main/kotlin/gui/entity/EntityWidget.kt35
-rw-r--r--src/main/kotlin/gui/entity/FakeWorld.kt488
-rw-r--r--src/main/kotlin/gui/entity/GuiPlayer.kt54
-rw-r--r--src/main/kotlin/gui/entity/ModifyAge.kt25
-rw-r--r--src/main/kotlin/gui/entity/ModifyCharged.kt14
-rw-r--r--src/main/kotlin/gui/entity/ModifyEquipment.kt55
-rw-r--r--src/main/kotlin/gui/entity/ModifyHorse.kt61
-rw-r--r--src/main/kotlin/gui/entity/ModifyInvisible.kt13
-rw-r--r--src/main/kotlin/gui/entity/ModifyName.kt14
-rw-r--r--src/main/kotlin/gui/entity/ModifyPlayerSkin.kt47
-rw-r--r--src/main/kotlin/gui/entity/ModifyRiding.kt15
-rw-r--r--src/main/kotlin/gui/entity/ModifyWither.kt20
-rw-r--r--src/main/kotlin/gui/hud/MoulConfigHud.kt66
34 files changed, 2249 insertions, 0 deletions
diff --git a/src/main/kotlin/gui/BarComponent.kt b/src/main/kotlin/gui/BarComponent.kt
new file mode 100644
index 0000000..8ef0753
--- /dev/null
+++ b/src/main/kotlin/gui/BarComponent.kt
@@ -0,0 +1,125 @@
+
+package moe.nea.firmament.gui
+
+import com.mojang.blaze3d.systems.RenderSystem
+import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
+import io.github.notenoughupdates.moulconfig.common.RenderContext
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext
+import me.shedaniel.math.Color
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+
+class BarComponent(
+ val progress: GetSetter<Double>, val total: GetSetter<Double>,
+ val fillColor: Color,
+ val emptyColor: Color,
+) : GuiComponent() {
+ override fun getWidth(): Int {
+ return 80
+ }
+
+ override fun getHeight(): Int {
+ return 8
+ }
+
+ data class Texture(
+ val identifier: Identifier,
+ val u1: Float, val v1: Float,
+ val u2: Float, val v2: Float,
+ ) {
+ fun draw(context: DrawContext, x: Int, y: Int, width: Int, height: Int, color: Color) {
+ context.drawTexturedQuad(
+ identifier,
+ x, y, x + width, x + height, 0,
+ u1, u2, v1, v2,
+ color.red / 255F,
+ color.green / 255F,
+ color.blue / 255F,
+ color.alpha / 255F,
+ )
+ }
+ }
+
+ companion object {
+ val resource = Firmament.identifier("textures/gui/bar.png")
+ val left = Texture(resource, 0 / 64F, 0 / 64F, 4 / 64F, 8 / 64F)
+ val middle = Texture(resource, 4 / 64F, 0 / 64F, 8 / 64F, 8 / 64F)
+ val right = Texture(resource, 8 / 64F, 0 / 64F, 12 / 64F, 8 / 64F)
+ val segmentOverlay = Texture(resource, 12 / 64F, 0 / 64F, 15 / 64F, 8 / 64F)
+ }
+
+ private fun drawSection(
+ context: DrawContext,
+ texture: Texture,
+ x: Int,
+ y: Int,
+ width: Int,
+ sectionStart: Double,
+ sectionEnd: Double
+ ) {
+ if (sectionEnd < progress.get() && width == 4) {
+ texture.draw(context, x, y, 4, 8, fillColor)
+ return
+ }
+ if (sectionStart > progress.get() && width == 4) {
+ texture.draw(context, x, y, 4, 8, emptyColor)
+ return
+ }
+ val increasePerPixel = (sectionEnd - sectionStart) / width
+ var valueAtPixel = sectionStart
+ for (i in (0 until width)) {
+ val newTex =
+ Texture(texture.identifier, texture.u1 + i / 64F, texture.v1, texture.u1 + (i + 1) / 64F, texture.v2)
+ newTex.draw(
+ context, x + i, y, 1, 8,
+ if (valueAtPixel < progress.get()) fillColor else emptyColor
+ )
+ valueAtPixel += increasePerPixel
+ }
+ }
+
+ override fun render(context: GuiImmediateContext) {
+ val renderContext = (context.renderContext as ModernRenderContext).drawContext
+ var i = 0
+ val x = 0
+ val y = 0
+ while (i < context.width - 4) {
+ drawSection(
+ renderContext,
+ if (i == 0) left else middle,
+ x + i, y,
+ (context.width - (i + 4)).coerceAtMost(4),
+ i * total.get() / context.width, (i + 4) * total.get() / context.width
+ )
+ i += 4
+ }
+ drawSection(
+ renderContext,
+ right,
+ x + context.width - 4,
+ y,
+ 4,
+ (context.width - 4) * total.get() / context.width,
+ total.get()
+ )
+ RenderSystem.setShaderColor(1F, 1F, 1F, 1F)
+
+ }
+
+}
+
+fun Identifier.toMoulConfig(): MyResourceLocation {
+ return MyResourceLocation(this.namespace, this.path)
+}
+
+fun RenderContext.color(color: Color) {
+ color(color.red, color.green, color.blue, color.alpha)
+}
+
+fun RenderContext.color(red: Int, green: Int, blue: Int, alpha: Int) {
+ color(red / 255f, green / 255f, blue / 255f, alpha / 255f)
+}
diff --git a/src/main/kotlin/gui/FirmButtonComponent.kt b/src/main/kotlin/gui/FirmButtonComponent.kt
new file mode 100644
index 0000000..82e5b05
--- /dev/null
+++ b/src/main/kotlin/gui/FirmButtonComponent.kt
@@ -0,0 +1,81 @@
+
+package moe.nea.firmament.gui
+
+import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
+import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.gui.MouseEvent
+import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+
+
+open class FirmButtonComponent(
+ child: GuiComponent,
+ val isEnabled: GetSetter<Boolean> = GetSetter.constant(true),
+ val noBackground: Boolean = false,
+ val action: Runnable,
+) : PanelComponent(child, if (noBackground) 0 else 2, DefaultBackgroundRenderer.TRANSPARENT) {
+
+ /* TODO: make use of vanillas built in nine slicer */
+ val hoveredBg =
+ NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button_highlighted.png"))
+ .cornerSize(5)
+ .cornerUv(5 / 200F, 5 / 20F)
+ .mode(NinePatch.Mode.STRETCHING)
+ .build()
+ val unhoveredBg = NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button.png"))
+ .cornerSize(5)
+ .cornerUv(5 / 200F, 5 / 20F)
+ .mode(NinePatch.Mode.STRETCHING)
+ .build()
+ val disabledBg =
+ NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button_disabled.png"))
+ .cornerSize(5)
+ .cornerUv(5 / 200F, 5 / 20F)
+ .mode(NinePatch.Mode.STRETCHING)
+ .build()
+ val activeBg = NinePatch.builder(MyResourceLocation("firmament", "textures/gui/sprites/widget/button_active.png"))
+ .cornerSize(5)
+ .cornerUv(5 / 200F, 5 / 20F)
+ .mode(NinePatch.Mode.STRETCHING)
+ .build()
+ var isClicking = false
+ override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean {
+ if (!isEnabled.get()) return false
+ if (isClicking) {
+ if (mouseEvent is MouseEvent.Click && !mouseEvent.mouseState && mouseEvent.mouseButton == 0) {
+ isClicking = false
+ if (context.isHovered) {
+ action.run()
+ }
+ return true
+ }
+ }
+ if (!context.isHovered) return false
+ if (mouseEvent !is MouseEvent.Click) return false
+ if (mouseEvent.mouseState && mouseEvent.mouseButton == 0) {
+ requestFocus()
+ isClicking = true
+ return true
+ }
+ return false
+ }
+
+ open fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> =
+ if (!isEnabled.get()) disabledBg
+ else if (context.isHovered || isClicking) hoveredBg
+ else unhoveredBg
+
+ override fun render(context: GuiImmediateContext) {
+ context.renderContext.pushMatrix()
+ if (!noBackground)
+ context.renderContext.drawNinePatch(
+ getBackground(context),
+ 0f, 0f, context.width, context.height
+ )
+ context.renderContext.translate(insets.toFloat(), insets.toFloat(), 0f)
+ element.render(getChildContext(context))
+ context.renderContext.popMatrix()
+ }
+}
diff --git a/src/main/kotlin/gui/FirmHoverComponent.kt b/src/main/kotlin/gui/FirmHoverComponent.kt
new file mode 100644
index 0000000..b1792ce
--- /dev/null
+++ b/src/main/kotlin/gui/FirmHoverComponent.kt
@@ -0,0 +1,59 @@
+package moe.nea.firmament.gui
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
+import io.github.notenoughupdates.moulconfig.gui.MouseEvent
+import java.util.function.BiFunction
+import java.util.function.Supplier
+import kotlin.time.Duration
+import moe.nea.firmament.util.TimeMark
+
+class FirmHoverComponent(
+ val child: GuiComponent,
+ val hoverLines: Supplier<List<String>>,
+ val hoverDelay: Duration,
+) : GuiComponent() {
+ override fun getWidth(): Int {
+ return child.width
+ }
+
+ override fun getHeight(): Int {
+ return child.height
+ }
+
+ override fun <T : Any?> foldChildren(
+ initial: T,
+ visitor: BiFunction<GuiComponent, T, T>
+ ): T {
+ return visitor.apply(child, initial)
+ }
+
+ override fun render(context: GuiImmediateContext) {
+ if (context.isHovered && (permaHover || lastMouseMove.passedTime() > hoverDelay)) {
+ context.renderContext.scheduleDrawTooltip(hoverLines.get())
+ permaHover = true
+ } else {
+ permaHover = false
+ }
+ if (!context.isHovered) {
+ lastMouseMove = TimeMark.now()
+ }
+ child.render(context)
+
+ }
+
+ var permaHover = false
+ var lastMouseMove = TimeMark.farPast()
+
+ override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean {
+ if (mouseEvent is MouseEvent.Move) {
+ lastMouseMove = TimeMark.now()
+ }
+ return child.mouseEvent(mouseEvent, context)
+ }
+
+ override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
+ return child.keyboardEvent(event, context)
+ }
+}
diff --git a/src/main/kotlin/gui/FixedComponent.kt b/src/main/kotlin/gui/FixedComponent.kt
new file mode 100644
index 0000000..ae1da2d
--- /dev/null
+++ b/src/main/kotlin/gui/FixedComponent.kt
@@ -0,0 +1,38 @@
+
+package moe.nea.firmament.gui
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
+import io.github.notenoughupdates.moulconfig.gui.MouseEvent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import java.util.function.BiFunction
+
+class FixedComponent(
+ val fixedWidth: GetSetter<Int>?,
+ val fixedHeight: GetSetter<Int>?,
+ val component: GuiComponent,
+) : GuiComponent() {
+ override fun getWidth(): Int = fixedWidth?.get() ?: component.width
+
+ override fun getHeight(): Int = fixedHeight?.get() ?: component.height
+
+ override fun <T : Any?> foldChildren(initial: T, visitor: BiFunction<GuiComponent, T, T>): T {
+ return visitor.apply(component, initial)
+ }
+
+ fun fixContext(context: GuiImmediateContext): GuiImmediateContext =
+ context.translated(0, 0, width, height)
+
+ override fun render(context: GuiImmediateContext) {
+ component.render(fixContext(context))
+ }
+
+ override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean {
+ return component.mouseEvent(mouseEvent, fixContext(context))
+ }
+
+ override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
+ return component.keyboardEvent(event, fixContext(context))
+ }
+}
diff --git a/src/main/kotlin/gui/ImageComponent.kt b/src/main/kotlin/gui/ImageComponent.kt
new file mode 100644
index 0000000..bba7dee
--- /dev/null
+++ b/src/main/kotlin/gui/ImageComponent.kt
@@ -0,0 +1,33 @@
+package moe.nea.firmament.gui
+
+import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import java.util.function.Supplier
+
+class ImageComponent(
+ private val width: Int,
+ private val height: Int,
+ val resourceLocation: Supplier<MyResourceLocation>,
+ val u1: Float,
+ val u2: Float,
+ val v1: Float,
+ val v2: Float,
+) : GuiComponent() {
+ override fun getWidth(): Int {
+ return width
+ }
+
+ override fun getHeight(): Int {
+ return height
+ }
+
+ override fun render(context: GuiImmediateContext) {
+ context.renderContext.bindTexture(resourceLocation.get())
+ context.renderContext.drawTexturedRect(
+ 0f, 0f,
+ context.width.toFloat(), context.height.toFloat(),
+ u1, v1, u2, v2
+ )
+ }
+}
diff --git a/src/main/kotlin/gui/TickComponent.kt b/src/main/kotlin/gui/TickComponent.kt
new file mode 100644
index 0000000..d1879b1
--- /dev/null
+++ b/src/main/kotlin/gui/TickComponent.kt
@@ -0,0 +1,18 @@
+package moe.nea.firmament.gui
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+
+class TickComponent(val onTick: Runnable) : GuiComponent() {
+ override fun getWidth(): Int {
+ return 0
+ }
+
+ override fun getHeight(): Int {
+ return 0
+ }
+
+ override fun render(context: GuiImmediateContext) {
+ onTick.run()
+ }
+}
diff --git a/src/main/kotlin/gui/config/AllConfigsGui.kt b/src/main/kotlin/gui/config/AllConfigsGui.kt
new file mode 100644
index 0000000..4f7731c
--- /dev/null
+++ b/src/main/kotlin/gui/config/AllConfigsGui.kt
@@ -0,0 +1,46 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.text.Text
+import moe.nea.firmament.features.FeatureManager
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil.setScreenLater
+
+object AllConfigsGui {
+
+ val allConfigs
+ get() = listOf(
+ RepoManager.Config
+ ) + FeatureManager.allFeatures.mapNotNull { it.config }
+
+ fun <T> List<T>.toObservableList(): ObservableList<T> = ObservableList(this)
+
+ class MainMapping(val allConfigs: List<ManagedConfig>) {
+ @get:Bind("configs")
+ val configs = allConfigs.map { EntryMapping(it) }.toObservableList()
+
+ class EntryMapping(val config: ManagedConfig) {
+ @Bind
+ fun name() = Text.translatable("firmament.config.${config.name}").string
+
+ @Bind
+ fun openEditor() {
+ config.showConfigEditor(MC.screen)
+ }
+ }
+ }
+
+ fun makeScreen(parent: Screen? = null): Screen {
+ return MoulConfigUtils.loadScreen("config/main", MainMapping(allConfigs), parent)
+ }
+
+ fun showAllGuis() {
+ setScreenLater(makeScreen())
+ }
+}
diff --git a/src/main/kotlin/gui/config/BooleanHandler.kt b/src/main/kotlin/gui/config/BooleanHandler.kt
new file mode 100644
index 0000000..8592777
--- /dev/null
+++ b/src/main/kotlin/gui/config/BooleanHandler.kt
@@ -0,0 +1,37 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent
+import io.github.notenoughupdates.moulconfig.gui.component.SwitchComponent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.boolean
+import kotlinx.serialization.json.jsonPrimitive
+
+class BooleanHandler(val config: ManagedConfig) : 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: ManagedOption<Boolean>, guiAppender: GuiAppender) {
+ guiAppender.appendLabeledRow(
+ opt.labelText,
+ CenterComponent(SwitchComponent(object : GetSetter<Boolean> {
+ override fun get(): Boolean {
+ return opt.get()
+ }
+
+ override fun set(newValue: Boolean) {
+ opt.set(newValue)
+ config.save()
+ }
+ }, 200)
+ ))
+ }
+}
diff --git a/src/main/kotlin/gui/config/ClickHandler.kt b/src/main/kotlin/gui/config/ClickHandler.kt
new file mode 100644
index 0000000..fa1c621
--- /dev/null
+++ b/src/main/kotlin/gui/config/ClickHandler.kt
@@ -0,0 +1,24 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import kotlinx.serialization.json.JsonElement
+import moe.nea.firmament.gui.FirmButtonComponent
+
+class ClickHandler(val config: ManagedConfig, val runnable: () -> Unit) : ManagedConfig.OptionHandler<Unit> {
+ override fun toJson(element: Unit): JsonElement? {
+ return null
+ }
+
+ override fun fromJson(element: JsonElement) {}
+
+ override fun emitGuiElements(opt: ManagedOption<Unit>, guiAppender: GuiAppender) {
+ guiAppender.appendLabeledRow(
+ opt.labelText,
+ FirmButtonComponent(
+ TextComponent(opt.labelText.string),
+ action = runnable),
+ )
+ }
+}
diff --git a/src/main/kotlin/gui/config/DurationHandler.kt b/src/main/kotlin/gui/config/DurationHandler.kt
new file mode 100644
index 0000000..8d485b1
--- /dev/null
+++ b/src/main/kotlin/gui/config/DurationHandler.kt
@@ -0,0 +1,58 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.common.IMinecraft
+import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
+import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.long
+import kotlin.time.Duration
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+import net.minecraft.text.Text
+import moe.nea.firmament.util.FirmFormatters
+
+class DurationHandler(val config: ManagedConfig, val min: Duration, val max: Duration) :
+ ManagedConfig.OptionHandler<Duration> {
+ override fun toJson(element: Duration): JsonElement? {
+ return JsonPrimitive(element.inWholeMilliseconds)
+ }
+
+ override fun fromJson(element: JsonElement): Duration {
+ return element.jsonPrimitive.long.toDuration(DurationUnit.MILLISECONDS)
+ }
+
+ override fun emitGuiElements(opt: ManagedOption<Duration>, guiAppender: GuiAppender) {
+ guiAppender.appendLabeledRow(
+ opt.labelText,
+ RowComponent(
+ TextComponent(IMinecraft.instance.defaultFontRenderer,
+ { FirmFormatters.formatTimespan(opt.value) },
+ 40,
+ TextComponent.TextAlignment.CENTER,
+ true,
+ false),
+ SliderComponent(
+ object : GetSetter<Float> {
+ override fun get(): Float {
+ return opt.value.toDouble(DurationUnit.SECONDS).toFloat()
+ }
+
+ override fun set(newValue: Float) {
+ opt.value = newValue.toDouble().toDuration(DurationUnit.SECONDS)
+ }
+ },
+ min.toDouble(DurationUnit.SECONDS).toFloat(),
+ max.toDouble(DurationUnit.SECONDS).toFloat(),
+ 0.1F,
+ 130
+ )
+ ))
+ }
+
+}
diff --git a/src/main/kotlin/gui/config/GuiAppender.kt b/src/main/kotlin/gui/config/GuiAppender.kt
new file mode 100644
index 0000000..329319d
--- /dev/null
+++ b/src/main/kotlin/gui/config/GuiAppender.kt
@@ -0,0 +1,40 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.text.Text
+import moe.nea.firmament.gui.FixedComponent
+
+class GuiAppender(val width: Int, val screenAccessor: () -> Screen) {
+ val panel = mutableListOf<GuiComponent>()
+ internal val reloadables = mutableListOf<(() -> Unit)>()
+
+ fun onReload(reloadable: () -> Unit) {
+ reloadables.add(reloadable)
+ }
+
+ fun appendLabeledRow(label: Text, right: GuiComponent) {
+ appendSplitRow(
+ TextComponent(label.string),
+ right
+ )
+ }
+
+ fun appendSplitRow(left: GuiComponent, right: GuiComponent) {
+ // TODO: make this more dynamic
+ // i could just make a component that allows for using half the available size
+ appendFullRow(RowComponent(
+ FixedComponent(GetSetter.constant(width / 2), null, left),
+ FixedComponent(GetSetter.constant(width / 2), null, right),
+ ))
+ }
+
+ fun appendFullRow(widget: GuiComponent) {
+ panel.add(widget)
+ }
+}
diff --git a/src/main/kotlin/gui/config/HudMetaHandler.kt b/src/main/kotlin/gui/config/HudMetaHandler.kt
new file mode 100644
index 0000000..35c9d51
--- /dev/null
+++ b/src/main/kotlin/gui/config/HudMetaHandler.kt
@@ -0,0 +1,39 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.decodeFromJsonElement
+import kotlinx.serialization.json.encodeToJsonElement
+import net.minecraft.text.MutableText
+import net.minecraft.text.Text
+import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.jarvis.JarvisIntegration
+import moe.nea.firmament.util.MC
+
+class HudMetaHandler(val config: ManagedConfig, val label: MutableText, val width: Int, val height: Int) :
+ ManagedConfig.OptionHandler<HudMeta> {
+ override fun toJson(element: HudMeta): JsonElement? {
+ return Json.encodeToJsonElement(element.position)
+ }
+
+ override fun fromJson(element: JsonElement): HudMeta {
+ return HudMeta(Json.decodeFromJsonElement(element), label, width, height)
+ }
+
+ override fun emitGuiElements(opt: ManagedOption<HudMeta>, guiAppender: GuiAppender) {
+ guiAppender.appendLabeledRow(
+ opt.labelText,
+ FirmButtonComponent(
+ TextComponent(
+ Text.stringifiedTranslatable("firmament.hud.edit", label).string),
+ ) {
+ MC.screen = JarvisIntegration.jarvis.getHudEditor(
+ guiAppender.screenAccessor.invoke(),
+ listOf(opt.value)
+ )
+ })
+ }
+}
diff --git a/src/main/kotlin/gui/config/IntegerHandler.kt b/src/main/kotlin/gui/config/IntegerHandler.kt
new file mode 100644
index 0000000..31ce90f
--- /dev/null
+++ b/src/main/kotlin/gui/config/IntegerHandler.kt
@@ -0,0 +1,54 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.common.IMinecraft
+import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
+import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.jsonPrimitive
+import moe.nea.firmament.util.FirmFormatters
+
+class IntegerHandler(val config: ManagedConfig, val min: Int, val max: Int) : ManagedConfig.OptionHandler<Int> {
+ override fun toJson(element: Int): JsonElement? {
+ return JsonPrimitive(element)
+ }
+
+ override fun fromJson(element: JsonElement): Int {
+ return element.jsonPrimitive.int
+ }
+
+ override fun emitGuiElements(opt: ManagedOption<Int>, guiAppender: GuiAppender) {
+ guiAppender.appendLabeledRow(
+ opt.labelText,
+ RowComponent(
+ TextComponent(IMinecraft.instance.defaultFontRenderer,
+ { FirmFormatters.formatCommas(opt.value, 0) },
+ 40,
+ TextComponent.TextAlignment.CENTER,
+ true,
+ false),
+ SliderComponent(
+ object : GetSetter<Float> {
+ override fun get(): Float {
+ return opt.value.toFloat()
+ }
+
+ override fun set(newValue: Float) {
+ opt.value = newValue.toInt()
+ }
+ },
+ min.toFloat(),
+ max.toFloat(),
+ 0.1F,
+ 130
+ )
+ ))
+
+ }
+
+}
diff --git a/src/main/kotlin/gui/config/JAnyHud.kt b/src/main/kotlin/gui/config/JAnyHud.kt
new file mode 100644
index 0000000..35c4eb2
--- /dev/null
+++ b/src/main/kotlin/gui/config/JAnyHud.kt
@@ -0,0 +1,48 @@
+
+
+package moe.nea.firmament.gui.config
+
+import moe.nea.jarvis.api.JarvisHud
+import moe.nea.jarvis.api.JarvisScalable
+import kotlinx.serialization.Serializable
+import net.minecraft.text.Text
+
+@Serializable
+data class HudPosition(
+ var x: Double,
+ var y: Double,
+ var scale: Float,
+)
+
+
+data class HudMeta(
+ val position: HudPosition,
+ private val label: Text,
+ private val width: Int,
+ private val height: Int,
+) : JarvisScalable, JarvisHud {
+ override fun getX(): Double = position.x
+
+ override fun setX(newX: Double) {
+ position.x = newX
+ }
+
+ override fun getY(): Double = position.y
+
+ override fun setY(newY: Double) {
+ position.y = newY
+ }
+
+ override fun getLabel(): Text = label
+
+ override fun getWidth(): Int = width
+
+ override fun getHeight(): Int = height
+
+ override fun getScale(): Float = position.scale
+
+ override fun setScale(newScale: Float) {
+ position.scale = newScale
+ }
+
+}
diff --git a/src/main/kotlin/gui/config/KeyBindingHandler.kt b/src/main/kotlin/gui/config/KeyBindingHandler.kt
new file mode 100644
index 0000000..c389cc9
--- /dev/null
+++ b/src/main/kotlin/gui/config/KeyBindingHandler.kt
@@ -0,0 +1,149 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.common.IMinecraft
+import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
+import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import org.lwjgl.glfw.GLFW
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.decodeFromJsonElement
+import kotlinx.serialization.json.encodeToJsonElement
+import net.minecraft.text.Text
+import net.minecraft.util.Formatting
+import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.keybindings.FirmamentKeyBindings
+import moe.nea.firmament.keybindings.SavedKeyBinding
+
+class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
+ ManagedConfig.OptionHandler<SavedKeyBinding> {
+
+ override fun initOption(opt: ManagedOption<SavedKeyBinding>) {
+ FirmamentKeyBindings.registerKeyBinding(name, opt)
+ }
+
+ override fun toJson(element: SavedKeyBinding): JsonElement? {
+ return Json.encodeToJsonElement(element)
+ }
+
+ override fun fromJson(element: JsonElement): SavedKeyBinding {
+ return Json.decodeFromJsonElement(element)
+ }
+
+ override fun emitGuiElements(opt: ManagedOption<SavedKeyBinding>, guiAppender: GuiAppender) {
+ var editing = false
+ var lastPressed = 0
+ var lastPressedNonModifier = 0
+ var label: String = ""
+ var button: FirmButtonComponent? = null
+ fun updateLabel() {
+ var stroke = opt.value.format()
+ if (editing) {
+ stroke = Text.literal("")
+ val (shift, alt, ctrl) = SavedKeyBinding.getMods(SavedKeyBinding.getModInt())
+ if (shift) {
+ stroke.append("SHIFT + ")
+ }
+ if (alt) {
+ stroke.append("ALT + ")
+ }
+ if (ctrl) {
+ stroke.append("CTRL + ")
+ }
+ stroke.append("???")
+ stroke.styled { it.withColor(Formatting.YELLOW) }
+ }
+ label = (stroke).string
+ managedConfig.save()
+ }
+ button = object : FirmButtonComponent(
+ TextComponent(
+ IMinecraft.instance.defaultFontRenderer,
+ { label },
+ 130,
+ TextComponent.TextAlignment.LEFT,
+ false,
+ false
+ ), action = {
+ if (editing) {
+ button!!.blur()
+ } else {
+ editing = true
+ button!!.requestFocus()
+ updateLabel()
+ }
+ }) {
+ override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
+ if (event is KeyboardEvent.KeyPressed) {
+ return if (event.pressed) onKeyPressed(event.keycode, SavedKeyBinding.getModInt())
+ else onKeyReleased(event.keycode, SavedKeyBinding.getModInt())
+ }
+ return super.keyboardEvent(event, context)
+ }
+
+ override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> {
+ if (editing) return activeBg
+ return super.getBackground(context)
+ }
+
+ fun onKeyPressed(ch: Int, modifiers: Int): Boolean {
+ if (!editing) {
+ return false
+ }
+ if (ch == GLFW.GLFW_KEY_ESCAPE) {
+ lastPressedNonModifier = 0
+ editing = false
+ lastPressed = 0
+ opt.value = SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN)
+ updateLabel()
+ blur()
+ return true
+ }
+ if (ch == GLFW.GLFW_KEY_LEFT_SHIFT || ch == GLFW.GLFW_KEY_RIGHT_SHIFT
+ || ch == GLFW.GLFW_KEY_LEFT_ALT || ch == GLFW.GLFW_KEY_RIGHT_ALT
+ || ch == GLFW.GLFW_KEY_LEFT_CONTROL || ch == GLFW.GLFW_KEY_RIGHT_CONTROL
+ ) {
+ lastPressed = ch
+ } else {
+ opt.value = SavedKeyBinding(
+ ch, modifiers
+ )
+ editing = false
+ blur()
+ lastPressed = 0
+ lastPressedNonModifier = 0
+ }
+ updateLabel()
+ return true
+ }
+
+ override fun onLostFocus() {
+ lastPressedNonModifier = 0
+ editing = false
+ lastPressed = 0
+ updateLabel()
+ }
+
+ fun onKeyReleased(ch: Int, modifiers: Int): Boolean {
+ if (!editing)
+ return false
+ if (lastPressedNonModifier == ch || (lastPressedNonModifier == 0 && ch == lastPressed)) {
+ opt.value = SavedKeyBinding(ch, modifiers)
+ editing = false
+ blur()
+ lastPressed = 0
+ lastPressedNonModifier = 0
+ }
+ updateLabel()
+ return true
+ }
+ }
+ updateLabel()
+ guiAppender.appendLabeledRow(opt.labelText, button)
+ }
+
+}
diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt
new file mode 100644
index 0000000..aa6e3c8
--- /dev/null
+++ b/src/main/kotlin/gui/config/ManagedConfig.kt
@@ -0,0 +1,181 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
+import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
+import io.github.notenoughupdates.moulconfig.gui.GuiContext
+import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent
+import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent
+import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
+import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
+import io.github.notenoughupdates.moulconfig.gui.component.ScrollPanelComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import moe.nea.jarvis.api.Point
+import org.lwjgl.glfw.GLFW
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlin.io.path.createDirectories
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+import kotlin.time.Duration
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.ScreenUtil.setScreenLater
+
+abstract class ManagedConfig(override val name: String) : ManagedConfigElement() {
+
+ interface OptionHandler<T : Any> {
+ fun initOption(opt: ManagedOption<T>) {}
+ fun toJson(element: T): JsonElement?
+ fun fromJson(element: JsonElement): T
+ fun emitGuiElements(opt: ManagedOption<T>, guiAppender: GuiAppender)
+ }
+
+ 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, ManagedOption<*>>()
+ val sortedOptions = mutableListOf<ManagedOption<*>>()
+
+ private var latestGuiAppender: GuiAppender? = null
+
+ protected fun <T : Any> option(
+ propertyName: String,
+ default: () -> T,
+ handler: OptionHandler<T>
+ ): ManagedOption<T> {
+ if (propertyName in allOptions) error("Cannot register the same name twice")
+ return ManagedOption(this, propertyName, default, handler).also {
+ it.handler.initOption(it)
+ it.load(data)
+ allOptions[propertyName] = it
+ sortedOptions.add(it)
+ }
+ }
+
+ protected fun toggle(propertyName: String, default: () -> Boolean): ManagedOption<Boolean> {
+ return option(propertyName, default, BooleanHandler(this))
+ }
+
+ protected fun duration(
+ propertyName: String,
+ min: Duration,
+ max: Duration,
+ default: () -> Duration,
+ ): ManagedOption<Duration> {
+ return option(propertyName, default, DurationHandler(this, min, max))
+ }
+
+
+ protected fun position(
+ propertyName: String,
+ width: Int,
+ height: Int,
+ default: () -> Point,
+ ): ManagedOption<HudMeta> {
+ val label = Text.translatable("firmament.config.${name}.${propertyName}")
+ return option(propertyName, {
+ val p = default()
+ HudMeta(HudPosition(p.x, p.y, 1F), label, width, height)
+ }, HudMetaHandler(this, label, width, height))
+ }
+
+ protected fun keyBinding(
+ propertyName: String,
+ default: () -> Int,
+ ): ManagedOption<SavedKeyBinding> = keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding(default()) }
+
+ protected fun keyBindingWithOutDefaultModifiers(
+ propertyName: String,
+ default: () -> SavedKeyBinding,
+ ): ManagedOption<SavedKeyBinding> {
+ return option(propertyName, default, KeyBindingHandler("firmament.config.${name}.${propertyName}", this))
+ }
+
+ protected fun keyBindingWithDefaultUnbound(
+ propertyName: String,
+ ): ManagedOption<SavedKeyBinding> {
+ return keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN) }
+ }
+
+ protected fun integer(
+ propertyName: String,
+ min: Int,
+ max: Int,
+ default: () -> Int,
+ ): ManagedOption<Int> {
+ return option(propertyName, default, IntegerHandler(this, min, max))
+ }
+
+ protected fun button(propertyName: String, runnable: () -> Unit): ManagedOption<Unit> {
+ return option(propertyName, { }, ClickHandler(this, runnable))
+ }
+
+ protected fun string(propertyName: String, default: () -> String): ManagedOption<String> {
+ return option(propertyName, default, StringHandler(this))
+ }
+
+
+ fun reloadGui() {
+ latestGuiAppender?.reloadables?.forEach { it() }
+ }
+
+ val labelText = Text.translatable("firmament.config.${name}")
+
+ fun getConfigEditor(parent: Screen? = null): Screen {
+ var screen: Screen? = null
+ val guiapp = GuiAppender(400) { requireNotNull(screen) { "Screen Accessor called too early" } }
+ latestGuiAppender = guiapp
+ guiapp.appendFullRow(RowComponent(
+ FirmButtonComponent(TextComponent("←")) {
+ if (parent != null) {
+ save()
+ setScreenLater(parent)
+ } else {
+ AllConfigsGui.showAllGuis()
+ }
+ }
+ ))
+ sortedOptions.forEach { it.appendToGui(guiapp) }
+ guiapp.reloadables.forEach { it() }
+ val component = CenterComponent(PanelComponent(ScrollPanelComponent(400, 300, ColumnComponent(guiapp.panel)), 10, PanelComponent.DefaultBackgroundRenderer.VANILLA))
+ screen = object : GuiComponentWrapper(GuiContext(component)) {
+ override fun close() {
+ if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) {
+ client!!.setScreen(parent)
+ }
+ }
+ }
+ return screen
+ }
+
+ fun showConfigEditor(parent: Screen? = null) {
+ setScreenLater(getConfigEditor(parent))
+ }
+
+}
diff --git a/src/main/kotlin/gui/config/ManagedConfigElement.kt b/src/main/kotlin/gui/config/ManagedConfigElement.kt
new file mode 100644
index 0000000..28cd6b8
--- /dev/null
+++ b/src/main/kotlin/gui/config/ManagedConfigElement.kt
@@ -0,0 +1,8 @@
+
+
+package moe.nea.firmament.gui.config
+
+abstract class ManagedConfigElement {
+ abstract val name: String
+
+}
diff --git a/src/main/kotlin/gui/config/ManagedOption.kt b/src/main/kotlin/gui/config/ManagedOption.kt
new file mode 100644
index 0000000..b7264e8
--- /dev/null
+++ b/src/main/kotlin/gui/config/ManagedOption.kt
@@ -0,0 +1,62 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
+
+class ManagedOption<T : Any>(
+ val element: ManagedConfigElement,
+ val propertyName: String,
+ val default: () -> T,
+ val handler: ManagedConfig.OptionHandler<T>
+) : ReadWriteProperty<Any?, T>, GetSetter<T> {
+ override fun set(newValue: T) {
+ this.value = newValue
+ }
+
+ override fun get(): T {
+ return this.value
+ }
+
+ val rawLabelText = "firmament.config.${element.name}.${propertyName}"
+ val labelText = Text.translatable(rawLabelText)
+
+ lateinit var value: T
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+ this.value = value
+ }
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): T {
+ return value
+ }
+
+ fun load(root: JsonElement) {
+ if (root is JsonObject && root.containsKey(propertyName)) {
+ try {
+ value = handler.fromJson(root[propertyName]!!)
+ return
+ } catch (e: Exception) {
+ Firmament.logger.error(
+ "Exception during loading of config file ${element.name}. This will reset this config.",
+ e
+ )
+ }
+ }
+ value = default()
+ }
+
+ fun toJson(): JsonElement? {
+ return handler.toJson(value)
+ }
+
+ fun appendToGui(guiapp: GuiAppender) {
+ handler.emitGuiElements(this, guiapp)
+ }
+}
diff --git a/src/main/kotlin/gui/config/StringHandler.kt b/src/main/kotlin/gui/config/StringHandler.kt
new file mode 100644
index 0000000..a326abb
--- /dev/null
+++ b/src/main/kotlin/gui/config/StringHandler.kt
@@ -0,0 +1,36 @@
+
+
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonPrimitive
+import net.minecraft.text.Text
+
+class StringHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<String> {
+ override fun toJson(element: String): JsonElement? {
+ return JsonPrimitive(element)
+ }
+
+ override fun fromJson(element: JsonElement): String {
+ return element.jsonPrimitive.content
+ }
+
+ override fun emitGuiElements(opt: ManagedOption<String>, guiAppender: GuiAppender) {
+ guiAppender.appendLabeledRow(
+ opt.labelText,
+ TextFieldComponent(
+ object : GetSetter<String> by opt {
+ override fun set(newValue: String) {
+ opt.set(newValue)
+ config.save()
+ }
+ },
+ 130,
+ suggestion = Text.translatableWithFallback(opt.rawLabelText + ".hint", "").string
+ ),
+ )
+ }
+}
diff --git a/src/main/kotlin/gui/entity/EntityModifier.kt b/src/main/kotlin/gui/entity/EntityModifier.kt
new file mode 100644
index 0000000..9623070
--- /dev/null
+++ b/src/main/kotlin/gui/entity/EntityModifier.kt
@@ -0,0 +1,9 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+
+fun interface EntityModifier {
+ fun apply(entity: LivingEntity, info: JsonObject): LivingEntity
+}
diff --git a/src/main/kotlin/gui/entity/EntityRenderer.kt b/src/main/kotlin/gui/entity/EntityRenderer.kt
new file mode 100644
index 0000000..8c7428d
--- /dev/null
+++ b/src/main/kotlin/gui/entity/EntityRenderer.kt
@@ -0,0 +1,197 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.Gson
+import com.google.gson.JsonArray
+import com.google.gson.JsonObject
+import org.apache.logging.log4j.LogManager
+import org.joml.Quaternionf
+import org.joml.Vector3f
+import kotlin.math.atan
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.screen.ingame.InventoryScreen
+import net.minecraft.entity.Entity
+import net.minecraft.entity.EntityType
+import net.minecraft.entity.LivingEntity
+import net.minecraft.util.Identifier
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.assertNotNullOr
+import moe.nea.firmament.util.iterate
+import moe.nea.firmament.util.openFirmamentResource
+import moe.nea.firmament.util.render.enableScissorWithTranslation
+
+object EntityRenderer {
+ val fakeWorld = FakeWorld()
+ private fun <T : Entity> t(entityType: EntityType<T>): () -> T {
+ return { entityType.create(fakeWorld)!! }
+ }
+
+ val entityIds: Map<String, () -> LivingEntity> = mapOf(
+ "Zombie" to t(EntityType.ZOMBIE),
+ "Chicken" to t(EntityType.CHICKEN),
+ "Slime" to t(EntityType.SLIME),
+ "Wolf" to t(EntityType.WOLF),
+ "Skeleton" to t(EntityType.SKELETON),
+ "Creeper" to t(EntityType.CREEPER),
+ "Ocelot" to t(EntityType.OCELOT),
+ "Blaze" to t(EntityType.BLAZE),
+ "Rabbit" to t(EntityType.RABBIT),
+ "Sheep" to t(EntityType.SHEEP),
+ "Horse" to t(EntityType.HORSE),
+ "Eisengolem" to t(EntityType.IRON_GOLEM),
+ "Silverfish" to t(EntityType.SILVERFISH),
+ "Witch" to t(EntityType.WITCH),
+ "Endermite" to t(EntityType.ENDERMITE),
+ "Snowman" to t(EntityType.SNOW_GOLEM),
+ "Villager" to t(EntityType.VILLAGER),
+ "Guardian" to t(EntityType.GUARDIAN),
+ "ArmorStand" to t(EntityType.ARMOR_STAND),
+ "Squid" to t(EntityType.SQUID),
+ "Bat" to t(EntityType.BAT),
+ "Spider" to t(EntityType.SPIDER),
+ "CaveSpider" to t(EntityType.CAVE_SPIDER),
+ "Pigman" to t(EntityType.ZOMBIFIED_PIGLIN),
+ "Ghast" to t(EntityType.GHAST),
+ "MagmaCube" to t(EntityType.MAGMA_CUBE),
+ "Wither" to t(EntityType.WITHER),
+ "Enderman" to t(EntityType.ENDERMAN),
+ "Mooshroom" to t(EntityType.MOOSHROOM),
+ "WitherSkeleton" to t(EntityType.WITHER_SKELETON),
+ "Cow" to t(EntityType.COW),
+ "Dragon" to t(EntityType.ENDER_DRAGON),
+ "Player" to { makeGuiPlayer(fakeWorld) },
+ "Pig" to t(EntityType.PIG),
+ "Giant" to t(EntityType.GIANT),
+ )
+ val entityModifiers: Map<String, EntityModifier> = mapOf(
+ "playerdata" to ModifyPlayerSkin,
+ "equipment" to ModifyEquipment,
+ "riding" to ModifyRiding,
+ "charged" to ModifyCharged,
+ "witherdata" to ModifyWither,
+ "invisible" to ModifyInvisible,
+ "age" to ModifyAge,
+ "horse" to ModifyHorse,
+ "name" to ModifyName,
+ )
+
+ val logger = LogManager.getLogger("Firmament.Entity")
+ fun applyModifiers(entityId: String, modifiers: List<JsonObject>): LivingEntity? {
+ val entityType = assertNotNullOr(entityIds[entityId]) {
+ logger.error("Could not create entity with id $entityId")
+ return null
+ }
+ var entity = entityType()
+ for (modifierJson in modifiers) {
+ val modifier = assertNotNullOr(modifierJson["type"]?.asString?.let(entityModifiers::get)) {
+ logger.error("Unknown modifier $modifierJson")
+ return null
+ }
+ entity = modifier.apply(entity, modifierJson)
+ }
+ return entity
+ }
+
+ fun constructEntity(info: JsonObject): LivingEntity? {
+ val modifiers = (info["modifiers"] as JsonArray?)?.map { it.asJsonObject } ?: emptyList()
+ val entityType = assertNotNullOr(info["entity"]?.asString) {
+ logger.error("Missing entity type on entity object")
+ return null
+ }
+ return applyModifiers(entityType, modifiers)
+ }
+
+ private val gson = Gson()
+ fun constructEntity(location: Identifier): LivingEntity? {
+ return constructEntity(
+ gson.fromJson(
+ location.openFirmamentResource().bufferedReader(), JsonObject::class.java
+ )
+ )
+ }
+
+ fun renderEntity(
+ entity: LivingEntity,
+ renderContext: DrawContext,
+ posX: Int,
+ posY: Int,
+ mouseX: Float,
+ mouseY: Float
+ ) {
+ var bottomOffset = 0.0F
+ var currentEntity = entity
+ val maxSize = entity.iterate { it.firstPassenger as? LivingEntity }
+ .map { it.height }
+ .sum()
+ while (true) {
+ currentEntity.age = MC.player?.age ?: 0
+ drawEntity(
+ renderContext,
+ posX,
+ posY,
+ posX + 50,
+ posY + 80,
+ minOf(2F / maxSize, 1F) * 30,
+ -bottomOffset,
+ mouseX,
+ mouseY,
+ currentEntity
+ )
+ val next = currentEntity.firstPassenger as? LivingEntity ?: break
+ bottomOffset += currentEntity.getPassengerRidingPos(next).y.toFloat() * 0.75F
+ currentEntity = next
+ }
+ }
+
+
+ fun drawEntity(
+ context: DrawContext,
+ x1: Int,
+ y1: Int,
+ x2: Int,
+ y2: Int,
+ size: Float,
+ bottomOffset: Float,
+ mouseX: Float,
+ mouseY: Float,
+ entity: LivingEntity
+ ) {
+ context.enableScissorWithTranslation(x1.toFloat(), y1.toFloat(), x2.toFloat(), y2.toFloat())
+ val centerX = (x1 + x2) / 2f
+ val centerY = (y1 + y2) / 2f
+ val targetYaw = atan(((centerX - mouseX) / 40.0f).toDouble()).toFloat()
+ val targetPitch = atan(((centerY - mouseY) / 40.0f).toDouble()).toFloat()
+ val rotateToFaceTheFront = Quaternionf().rotateZ(Math.PI.toFloat())
+ val rotateToFaceTheCamera = Quaternionf().rotateX(targetPitch * 20.0f * (Math.PI.toFloat() / 180))
+ rotateToFaceTheFront.mul(rotateToFaceTheCamera)
+ val oldBodyYaw = entity.bodyYaw
+ val oldYaw = entity.yaw
+ val oldPitch = entity.pitch
+ val oldPrevHeadYaw = entity.prevHeadYaw
+ val oldHeadYaw = entity.headYaw
+ entity.bodyYaw = 180.0f + targetYaw * 20.0f
+ entity.yaw = 180.0f + targetYaw * 40.0f
+ entity.pitch = -targetPitch * 20.0f
+ entity.headYaw = entity.yaw
+ entity.prevHeadYaw = entity.yaw
+ val vector3f = Vector3f(0.0f, entity.height / 2.0f + bottomOffset, 0.0f)
+ InventoryScreen.drawEntity(
+ context,
+ centerX,
+ centerY,
+ size,
+ vector3f,
+ rotateToFaceTheFront,
+ rotateToFaceTheCamera,
+ entity
+ )
+ entity.bodyYaw = oldBodyYaw
+ entity.yaw = oldYaw
+ entity.pitch = oldPitch
+ entity.prevHeadYaw = oldPrevHeadYaw
+ entity.headYaw = oldHeadYaw
+ context.disableScissor()
+ }
+
+
+}
diff --git a/src/main/kotlin/gui/entity/EntityWidget.kt b/src/main/kotlin/gui/entity/EntityWidget.kt
new file mode 100644
index 0000000..2e49072
--- /dev/null
+++ b/src/main/kotlin/gui/entity/EntityWidget.kt
@@ -0,0 +1,35 @@
+
+package moe.nea.firmament.gui.entity
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.Element
+import net.minecraft.entity.LivingEntity
+
+class EntityWidget(val entity: LivingEntity, val point: Point) : WidgetWithBounds() {
+ override fun children(): List<Element> {
+ return emptyList()
+ }
+
+ var hasErrored = false
+
+ override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ try {
+ if (!hasErrored)
+ EntityRenderer.renderEntity(entity, context, point.x, point.y, mouseX.toFloat(), mouseY.toFloat())
+ } catch (ex: Exception) {
+ EntityRenderer.logger.error("Failed to render constructed entity: $entity", ex)
+ hasErrored = true
+ }
+ if (hasErrored) {
+ context.fill(point.x, point.y, point.x + 50, point.y + 80, 0xFFAA2222.toInt())
+ }
+ }
+
+ override fun getBounds(): Rectangle {
+ return Rectangle(point, Dimension(50, 80))
+ }
+}
diff --git a/src/main/kotlin/gui/entity/FakeWorld.kt b/src/main/kotlin/gui/entity/FakeWorld.kt
new file mode 100644
index 0000000..f354d5a
--- /dev/null
+++ b/src/main/kotlin/gui/entity/FakeWorld.kt
@@ -0,0 +1,488 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.mojang.datafixers.util.Pair
+import com.mojang.serialization.Lifecycle
+import java.util.*
+import java.util.function.BooleanSupplier
+import java.util.function.Consumer
+import java.util.stream.Stream
+import kotlin.jvm.optionals.getOrNull
+import kotlin.streams.asSequence
+import net.minecraft.block.Block
+import net.minecraft.block.BlockState
+import net.minecraft.component.type.MapIdComponent
+import net.minecraft.entity.Entity
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.fluid.Fluid
+import net.minecraft.item.map.MapState
+import net.minecraft.recipe.BrewingRecipeRegistry
+import net.minecraft.recipe.Ingredient
+import net.minecraft.recipe.RecipeManager
+import net.minecraft.registry.BuiltinRegistries
+import net.minecraft.registry.DynamicRegistryManager
+import net.minecraft.registry.Registry
+import net.minecraft.registry.RegistryKey
+import net.minecraft.registry.RegistryKeys
+import net.minecraft.registry.RegistryWrapper
+import net.minecraft.registry.entry.RegistryEntry
+import net.minecraft.registry.entry.RegistryEntryInfo
+import net.minecraft.registry.entry.RegistryEntryList
+import net.minecraft.registry.entry.RegistryEntryOwner
+import net.minecraft.registry.tag.TagKey
+import net.minecraft.resource.featuretoggle.FeatureFlags
+import net.minecraft.resource.featuretoggle.FeatureSet
+import net.minecraft.scoreboard.Scoreboard
+import net.minecraft.sound.SoundCategory
+import net.minecraft.sound.SoundEvent
+import net.minecraft.util.Identifier
+import net.minecraft.util.TypeFilter
+import net.minecraft.util.function.LazyIterationConsumer
+import net.minecraft.util.math.BlockPos
+import net.minecraft.util.math.Box
+import net.minecraft.util.math.ChunkPos
+import net.minecraft.util.math.Direction
+import net.minecraft.util.math.Vec3d
+import net.minecraft.util.math.random.Random
+import net.minecraft.util.profiler.DummyProfiler
+import net.minecraft.world.BlockView
+import net.minecraft.world.Difficulty
+import net.minecraft.world.GameRules
+import net.minecraft.world.MutableWorldProperties
+import net.minecraft.world.World
+import net.minecraft.world.biome.Biome
+import net.minecraft.world.biome.BiomeKeys
+import net.minecraft.world.chunk.Chunk
+import net.minecraft.world.chunk.ChunkManager
+import net.minecraft.world.chunk.ChunkStatus
+import net.minecraft.world.chunk.EmptyChunk
+import net.minecraft.world.chunk.light.LightingProvider
+import net.minecraft.world.entity.EntityLookup
+import net.minecraft.world.event.GameEvent
+import net.minecraft.world.tick.OrderedTick
+import net.minecraft.world.tick.QueryableTickScheduler
+import net.minecraft.world.tick.TickManager
+
+fun <T> makeRegistry(registryWrapper: RegistryWrapper.Impl<T>, key: RegistryKey<out Registry<T>>): Registry<T> {
+ val inverseLookup = registryWrapper.streamEntries()
+ .asSequence().map { it.value() to it.registryKey() }
+ .toMap()
+ val idLookup = registryWrapper.streamEntries()
+ .asSequence()
+ .map { it.registryKey() }
+ .withIndex()
+ .associate { it.value to it.index }
+ val map = registryWrapper.streamEntries().asSequence().map { it.registryKey() to it.value() }.toMap(mutableMapOf())
+ val inverseIdLookup = idLookup.asIterable().associate { (k, v) -> v to k }
+ return object : Registry<T> {
+ override fun get(key: RegistryKey<T>?): T? {
+ return registryWrapper.getOptional(key).getOrNull()?.value()
+ }
+
+ override fun iterator(): MutableIterator<T> {
+ return object : MutableIterator<T> {
+ val iterator = registryWrapper.streamEntries().iterator()
+ override fun hasNext(): Boolean {
+ return iterator.hasNext()
+ }
+
+ override fun next(): T {
+ return iterator.next().value()
+ }
+
+ override fun remove() {
+ TODO("Not yet implemented")
+ }
+ }
+ }
+
+ override fun getRawId(value: T?): Int {
+ return idLookup[inverseLookup[value ?: return -1] ?: return -1] ?: return -1
+ }
+
+ override fun get(id: Identifier?): T? {
+ return get(RegistryKey.of(key, id))
+ }
+
+ override fun get(index: Int): T? {
+ return get(inverseIdLookup[index] ?: return null)
+ }
+
+ override fun size(): Int {
+ return idLookup.size
+ }
+
+ override fun getKey(): RegistryKey<out Registry<T>> {
+ return key
+ }
+
+ override fun getEntryInfo(key: RegistryKey<T>?): Optional<RegistryEntryInfo> {
+ TODO("Not yet implemented")
+ }
+
+ override fun getLifecycle(): Lifecycle {
+ return Lifecycle.stable()
+ }
+
+ override fun getDefaultEntry(): Optional<RegistryEntry.Reference<T>> {
+ return Optional.empty()
+ }
+
+ override fun getIds(): MutableSet<Identifier> {
+ return idLookup.keys.mapTo(mutableSetOf()) { it.value }
+ }
+
+ override fun getEntrySet(): MutableSet<MutableMap.MutableEntry<RegistryKey<T>, T>> {
+ return map.entries
+ }
+
+ override fun getKeys(): MutableSet<RegistryKey<T>> {
+ return map.keys
+ }
+
+ override fun getRandom(random: Random?): Optional<RegistryEntry.Reference<T>> {
+ return registryWrapper.streamEntries().findFirst()
+ }
+
+ override fun containsId(id: Identifier?): Boolean {
+ return idLookup.containsKey(RegistryKey.of(key, id ?: return false))
+ }
+
+ override fun freeze(): Registry<T> {
+ return this
+ }
+
+ override fun getEntry(rawId: Int): Optional<RegistryEntry.Reference<T>> {
+ val x = inverseIdLookup[rawId] ?: return Optional.empty()
+ return Optional.of(RegistryEntry.Reference.standAlone(registryWrapper, x))
+ }
+
+ override fun streamEntries(): Stream<RegistryEntry.Reference<T>> {
+ return registryWrapper.streamEntries()
+ }
+
+ override fun streamTagsAndEntries(): Stream<Pair<TagKey<T>, RegistryEntryList.Named<T>>> {
+ return streamTags().map { Pair(it, getOrCreateEntryList(it)) }
+ }
+
+ override fun streamTags(): Stream<TagKey<T>> {
+ return registryWrapper.streamTagKeys()
+ }
+
+ override fun clearTags() {
+ }
+
+ override fun getEntryOwner(): RegistryEntryOwner<T> {
+ return registryWrapper
+ }
+
+ override fun getReadOnlyWrapper(): RegistryWrapper.Impl<T> {
+ return registryWrapper
+ }
+
+ override fun populateTags(tagEntries: MutableMap<TagKey<T>, MutableList<RegistryEntry<T>>>?) {
+ }
+
+ override fun getOrCreateEntryList(tag: TagKey<T>?): RegistryEntryList.Named<T> {
+ return getEntryList(tag).orElseGet { RegistryEntryList.of(registryWrapper, tag) }
+ }
+
+ override fun getEntryList(tag: TagKey<T>?): Optional<RegistryEntryList.Named<T>> {
+ return registryWrapper.getOptional(tag ?: return Optional.empty())
+ }
+
+ override fun getEntry(value: T): RegistryEntry<T> {
+ return registryWrapper.getOptional(inverseLookup[value]!!).get()
+ }
+
+ override fun getEntry(key: RegistryKey<T>?): Optional<RegistryEntry.Reference<T>> {
+ return registryWrapper.getOptional(key ?: return Optional.empty())
+ }
+
+ override fun getEntry(id: Identifier?): Optional<RegistryEntry.Reference<T>> {
+ TODO("Not yet implemented")
+ }
+
+ override fun createEntry(value: T): RegistryEntry.Reference<T> {
+ TODO("Not yet implemented")
+ }
+
+ override fun contains(key: RegistryKey<T>?): Boolean {
+ return getEntry(key).isPresent
+ }
+
+ override fun getId(value: T): Identifier? {
+ return (inverseLookup[value] ?: return null).value
+ }
+
+ override fun getKey(entry: T): Optional<RegistryKey<T>> {
+ return Optional.ofNullable(inverseLookup[entry ?: return Optional.empty()])
+ }
+ }
+}
+
+fun createDynamicRegistry(): DynamicRegistryManager.Immutable {
+ val wrapperLookup = BuiltinRegistries.createWrapperLookup()
+ return object : DynamicRegistryManager.Immutable {
+ override fun <E : Any?> getOptional(key: RegistryKey<out Registry<out E>>): Optional<Registry<E>> {
+ val lookup = wrapperLookup.getOptionalWrapper(key).getOrNull() ?: return Optional.empty()
+ val registry = makeRegistry(lookup, key as RegistryKey<out Registry<E>>)
+ return Optional.of(registry)
+ }
+
+ fun <T> entry(reg: RegistryKey<out Registry<T>>): DynamicRegistryManager.Entry<T> {
+ return DynamicRegistryManager.Entry(reg, getOptional(reg).get())
+ }
+
+ override fun streamAllRegistries(): Stream<DynamicRegistryManager.Entry<*>> {
+ return wrapperLookup.streamAllRegistryKeys()
+ .map { entry(it as RegistryKey<out Registry<Any>>) }
+ }
+ }
+}
+
+class FakeWorld(
+ registries: DynamicRegistryManager.Immutable = createDynamicRegistry(),
+) : World(
+ Properties,
+ RegistryKey.of(RegistryKeys.WORLD, Identifier.of("firmament", "fakeworld")),
+ registries,
+ registries[RegistryKeys.DIMENSION_TYPE].entryOf(
+ RegistryKey.of(
+ RegistryKeys.DIMENSION_TYPE,
+ Identifier.of("minecraft", "overworld")
+ )
+ ),
+ { DummyProfiler.INSTANCE },
+ true,
+ false,
+ 0, 0
+) {
+ object Properties : MutableWorldProperties {
+ override fun getSpawnPos(): BlockPos {
+ return BlockPos.ORIGIN
+ }
+
+ override fun getSpawnAngle(): Float {
+ return 0F
+ }
+
+ override fun getTime(): Long {
+ return 0
+ }
+
+ override fun getTimeOfDay(): Long {
+ return 0
+ }
+
+ override fun isThundering(): Boolean {
+ return false
+ }
+
+ override fun isRaining(): Boolean {
+ return false
+ }
+
+ override fun setRaining(raining: Boolean) {
+ }
+
+ override fun isHardcore(): Boolean {
+ return false
+ }
+
+ override fun getGameRules(): GameRules {
+ return GameRules()
+ }
+
+ override fun getDifficulty(): Difficulty {
+ return Difficulty.HARD
+ }
+
+ override fun isDifficultyLocked(): Boolean {
+ return false
+ }
+
+ override fun setSpawnPos(pos: BlockPos?, angle: Float) {}
+ }
+
+ override fun getPlayers(): List<PlayerEntity> {
+ return emptyList()
+ }
+
+ override fun getBrightness(direction: Direction?, shaded: Boolean): Float {
+ return 1f
+ }
+
+ override fun getGeneratorStoredBiome(biomeX: Int, biomeY: Int, biomeZ: Int): RegistryEntry<Biome> {
+ return registryManager.get(RegistryKeys.BIOME).entryOf(BiomeKeys.PLAINS)
+ }
+
+ override fun getEnabledFeatures(): FeatureSet {
+ return FeatureFlags.VANILLA_FEATURES
+ }
+
+ class FakeTickScheduler<T> : QueryableTickScheduler<T> {
+ override fun scheduleTick(orderedTick: OrderedTick<T>?) {
+ }
+
+ override fun isQueued(pos: BlockPos?, type: T): Boolean {
+ return true
+ }
+
+ override fun getTickCount(): Int {
+ return 0
+ }
+
+ override fun isTicking(pos: BlockPos?, type: T): Boolean {
+ return true
+ }
+
+ }
+
+ override fun getBlockTickScheduler(): QueryableTickScheduler<Block> {
+ return FakeTickScheduler()
+ }
+
+ override fun getFluidTickScheduler(): QueryableTickScheduler<Fluid> {
+ return FakeTickScheduler()
+ }
+
+
+ class FakeChunkManager(val world: FakeWorld) : ChunkManager() {
+ override fun getChunk(x: Int, z: Int, leastStatus: ChunkStatus?, create: Boolean): Chunk {
+ return EmptyChunk(
+ world,
+ ChunkPos(x, z),
+ world.registryManager.get(RegistryKeys.BIOME).entryOf(BiomeKeys.PLAINS)
+ )
+ }
+
+ override fun getWorld(): BlockView {
+ return world
+ }
+
+ override fun tick(shouldKeepTicking: BooleanSupplier?, tickChunks: Boolean) {
+ }
+
+ override fun getDebugString(): String {
+ return "FakeChunkManager"
+ }
+
+ override fun getLoadedChunkCount(): Int {
+ return 0
+ }
+
+ override fun getLightingProvider(): LightingProvider {
+ return FakeLightingProvider(this)
+ }
+ }
+
+ class FakeLightingProvider(chunkManager: FakeChunkManager) : LightingProvider(chunkManager, false, false)
+
+ override fun getChunkManager(): ChunkManager {
+ return FakeChunkManager(this)
+ }
+
+ override fun playSound(
+ source: PlayerEntity?,
+ x: Double,
+ y: Double,
+ z: Double,
+ sound: RegistryEntry<SoundEvent>?,
+ category: SoundCategory?,
+ volume: Float,
+ pitch: Float,
+ seed: Long
+ ) {
+ }
+
+ override fun syncWorldEvent(player: PlayerEntity?, eventId: Int, pos: BlockPos?, data: Int) {
+ }
+
+ override fun emitGameEvent(event: RegistryEntry<GameEvent>?, emitterPos: Vec3d?, emitter: GameEvent.Emitter?) {
+ }
+
+ override fun updateListeners(pos: BlockPos?, oldState: BlockState?, newState: BlockState?, flags: Int) {
+ }
+
+ override fun playSoundFromEntity(
+ source: PlayerEntity?,
+ entity: Entity?,
+ sound: RegistryEntry<SoundEvent>?,
+ category: SoundCategory?,
+ volume: Float,
+ pitch: Float,
+ seed: Long
+ ) {
+ }
+
+ override fun asString(): String {
+ return "FakeWorld"
+ }
+
+ override fun getEntityById(id: Int): Entity? {
+ return null
+ }
+
+ override fun getTickManager(): TickManager {
+ return TickManager()
+ }
+
+ override fun getMapState(id: MapIdComponent?): MapState? {
+ return null
+ }
+
+ override fun putMapState(id: MapIdComponent?, state: MapState?) {
+ }
+
+ override fun increaseAndGetMapId(): MapIdComponent {
+ return MapIdComponent(0)
+ }
+
+ override fun setBlockBreakingInfo(entityId: Int, pos: BlockPos?, progress: Int) {
+ }
+
+ override fun getScoreboard(): Scoreboard {
+ return Scoreboard()
+ }
+
+ override fun getRecipeManager(): RecipeManager {
+ return RecipeManager(registryManager)
+ }
+
+ object FakeEntityLookup : EntityLookup<Entity> {
+ override fun get(id: Int): Entity? {
+ return null
+ }
+
+ override fun get(uuid: UUID?): Entity? {
+ return null
+ }
+
+ override fun iterate(): MutableIterable<Entity> {
+ return mutableListOf()
+ }
+
+ override fun <U : Entity?> forEachIntersects(
+ filter: TypeFilter<Entity, U>?,
+ box: Box?,
+ consumer: LazyIterationConsumer<U>?
+ ) {
+ }
+
+ override fun forEachIntersects(box: Box?, action: Consumer<Entity>?) {
+ }
+
+ override fun <U : Entity?> forEach(filter: TypeFilter<Entity, U>?, consumer: LazyIterationConsumer<U>?) {
+ }
+
+ }
+
+ override fun getEntityLookup(): EntityLookup<Entity> {
+ return FakeEntityLookup
+ }
+
+ override fun getBrewingRecipeRegistry(): BrewingRecipeRegistry {
+ return BrewingRecipeRegistry.EMPTY
+ }
+}
diff --git a/src/main/kotlin/gui/entity/GuiPlayer.kt b/src/main/kotlin/gui/entity/GuiPlayer.kt
new file mode 100644
index 0000000..d00b44d
--- /dev/null
+++ b/src/main/kotlin/gui/entity/GuiPlayer.kt
@@ -0,0 +1,54 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.mojang.authlib.GameProfile
+import java.util.*
+import net.minecraft.client.network.AbstractClientPlayerEntity
+import net.minecraft.client.util.DefaultSkinHelper
+import net.minecraft.client.util.SkinTextures
+import net.minecraft.client.util.SkinTextures.Model
+import net.minecraft.client.world.ClientWorld
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.BlockPos
+import net.minecraft.world.World
+
+/**
+ * @see moe.nea.firmament.init.EarlyRiser
+ */
+fun makeGuiPlayer(world: FakeWorld): GuiPlayer {
+ val constructor = GuiPlayer::class.java.getDeclaredConstructor(
+ World::class.java,
+ BlockPos::class.java,
+ Float::class.javaPrimitiveType,
+ GameProfile::class.java
+ )
+ return constructor.newInstance(world, BlockPos.ORIGIN, 0F, GameProfile(UUID.randomUUID(), "Linnea"))
+}
+
+class GuiPlayer(world: ClientWorld?, profile: GameProfile?) : AbstractClientPlayerEntity(world, profile) {
+ override fun isSpectator(): Boolean {
+ return false
+ }
+
+ override fun isCreative(): Boolean {
+ return false
+ }
+
+ override fun shouldRenderName(): Boolean {
+ return false
+ }
+
+ var skinTexture: Identifier = DefaultSkinHelper.getSkinTextures(this.getUuid()).texture
+ var capeTexture: Identifier? = null
+ var model: Model = Model.WIDE
+ override fun getSkinTextures(): SkinTextures {
+ return SkinTextures(
+ skinTexture,
+ null,
+ capeTexture,
+ null,
+ model,
+ true
+ )
+ }
+}
diff --git a/src/main/kotlin/gui/entity/ModifyAge.kt b/src/main/kotlin/gui/entity/ModifyAge.kt
new file mode 100644
index 0000000..a65c368
--- /dev/null
+++ b/src/main/kotlin/gui/entity/ModifyAge.kt
@@ -0,0 +1,25 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.decoration.ArmorStandEntity
+import net.minecraft.entity.mob.ZombieEntity
+import net.minecraft.entity.passive.PassiveEntity
+
+object ModifyAge : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ val isBaby = info["baby"]?.asBoolean ?: false
+ if (entity is PassiveEntity) {
+ entity.breedingAge = if (isBaby) -1 else 1
+ } else if (entity is ZombieEntity) {
+ entity.isBaby = isBaby
+ } else if (entity is ArmorStandEntity) {
+ entity.isSmall = isBaby
+ } else {
+ error("Cannot set age for $entity")
+ }
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/gui/entity/ModifyCharged.kt b/src/main/kotlin/gui/entity/ModifyCharged.kt
new file mode 100644
index 0000000..d22f6e3
--- /dev/null
+++ b/src/main/kotlin/gui/entity/ModifyCharged.kt
@@ -0,0 +1,14 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.mob.CreeperEntity
+
+object ModifyCharged : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ require(entity is CreeperEntity)
+ entity.dataTracker.set(CreeperEntity.CHARGED, true)
+ return entity
+ }
+}
diff --git a/src/main/kotlin/gui/entity/ModifyEquipment.kt b/src/main/kotlin/gui/entity/ModifyEquipment.kt
new file mode 100644
index 0000000..73e450e
--- /dev/null
+++ b/src/main/kotlin/gui/entity/ModifyEquipment.kt
@@ -0,0 +1,55 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.component.type.DyedColorComponent
+import net.minecraft.entity.EquipmentSlot
+import net.minecraft.entity.LivingEntity
+import net.minecraft.item.ArmorItem
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import moe.nea.firmament.rei.SBItemStack
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.item.setEncodedSkullOwner
+import moe.nea.firmament.util.item.zeroUUID
+
+object ModifyEquipment : EntityModifier {
+ val names = mapOf(
+ "hand" to EquipmentSlot.MAINHAND,
+ "helmet" to EquipmentSlot.HEAD,
+ "chestplate" to EquipmentSlot.CHEST,
+ "leggings" to EquipmentSlot.LEGS,
+ "feet" to EquipmentSlot.FEET,
+ )
+
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ names.forEach { (key, slot) ->
+ info[key]?.let {
+ entity.equipStack(slot, createItem(it.asString))
+ }
+ }
+ return entity
+ }
+
+ private fun createItem(item: String): ItemStack {
+ val split = item.split("#")
+ if (split.size != 2) return SBItemStack(SkyblockId(item)).asImmutableItemStack()
+ val (type, data) = split
+ return when (type) {
+ "SKULL" -> ItemStack(Items.PLAYER_HEAD).also { it.setEncodedSkullOwner(zeroUUID, data) }
+ "LEATHER_LEGGINGS" -> coloredLeatherArmor(Items.LEATHER_LEGGINGS, data)
+ "LEATHER_BOOTS" -> coloredLeatherArmor(Items.LEATHER_BOOTS, data)
+ "LEATHER_HELMET" -> coloredLeatherArmor(Items.LEATHER_HELMET, data)
+ "LEATHER_CHESTPLATE" -> coloredLeatherArmor(Items.LEATHER_CHESTPLATE, data)
+ else -> error("Unknown leather piece: $type")
+ }
+ }
+
+ private fun coloredLeatherArmor(leatherArmor: Item, data: String): ItemStack {
+ val stack = ItemStack(leatherArmor)
+ stack.set(DataComponentTypes.DYED_COLOR, DyedColorComponent(data.toInt(16), false))
+ return stack
+ }
+}
diff --git a/src/main/kotlin/gui/entity/ModifyHorse.kt b/src/main/kotlin/gui/entity/ModifyHorse.kt
new file mode 100644
index 0000000..8ac011b
--- /dev/null
+++ b/src/main/kotlin/gui/entity/ModifyHorse.kt
@@ -0,0 +1,61 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonNull
+import com.google.gson.JsonObject
+import kotlin.experimental.and
+import kotlin.experimental.inv
+import kotlin.experimental.or
+import net.minecraft.entity.EntityType
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.passive.AbstractHorseEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import moe.nea.firmament.gui.entity.EntityRenderer.fakeWorld
+
+object ModifyHorse : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ require(entity is AbstractHorseEntity)
+ var entity: AbstractHorseEntity = entity
+ info["kind"]?.let {
+ entity = when (it.asString) {
+ "skeleton" -> EntityType.SKELETON_HORSE.create(fakeWorld)!!
+ "zombie" -> EntityType.ZOMBIE_HORSE.create(fakeWorld)!!
+ "mule" -> EntityType.MULE.create(fakeWorld)!!
+ "donkey" -> EntityType.DONKEY.create(fakeWorld)!!
+ "horse" -> EntityType.HORSE.create(fakeWorld)!!
+ else -> error("Unknown horse kind $it")
+ }
+ }
+ info["armor"]?.let {
+ if (it is JsonNull) {
+ entity.setHorseArmor(ItemStack.EMPTY)
+ } else {
+ when (it.asString) {
+ "iron" -> entity.setHorseArmor(ItemStack(Items.IRON_HORSE_ARMOR))
+ "golden" -> entity.setHorseArmor(ItemStack(Items.GOLDEN_HORSE_ARMOR))
+ "diamond" -> entity.setHorseArmor(ItemStack(Items.DIAMOND_HORSE_ARMOR))
+ else -> error("Unknown horse armor $it")
+ }
+ }
+ }
+ info["saddled"]?.let {
+ entity.setIsSaddled(it.asBoolean)
+ }
+ return entity
+ }
+
+}
+
+fun AbstractHorseEntity.setIsSaddled(shouldBeSaddled: Boolean) {
+ val oldFlag = dataTracker.get(AbstractHorseEntity.HORSE_FLAGS)
+ dataTracker.set(
+ AbstractHorseEntity.HORSE_FLAGS,
+ if (shouldBeSaddled) oldFlag or AbstractHorseEntity.SADDLED_FLAG.toByte()
+ else oldFlag and AbstractHorseEntity.SADDLED_FLAG.toByte().inv()
+ )
+}
+
+fun AbstractHorseEntity.setHorseArmor(itemStack: ItemStack) {
+ items.setStack(1, itemStack)
+}
diff --git a/src/main/kotlin/gui/entity/ModifyInvisible.kt b/src/main/kotlin/gui/entity/ModifyInvisible.kt
new file mode 100644
index 0000000..8d36991
--- /dev/null
+++ b/src/main/kotlin/gui/entity/ModifyInvisible.kt
@@ -0,0 +1,13 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+
+object ModifyInvisible : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ entity.isInvisible = info.get("invisible")?.asBoolean ?: true
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/gui/entity/ModifyName.kt b/src/main/kotlin/gui/entity/ModifyName.kt
new file mode 100644
index 0000000..a03da96
--- /dev/null
+++ b/src/main/kotlin/gui/entity/ModifyName.kt
@@ -0,0 +1,14 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+import net.minecraft.text.Text
+
+object ModifyName : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ entity.customName = Text.literal(info.get("name").asString)
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/gui/entity/ModifyPlayerSkin.kt b/src/main/kotlin/gui/entity/ModifyPlayerSkin.kt
new file mode 100644
index 0000000..28f0070
--- /dev/null
+++ b/src/main/kotlin/gui/entity/ModifyPlayerSkin.kt
@@ -0,0 +1,47 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import com.google.gson.JsonPrimitive
+import kotlin.experimental.and
+import kotlin.experimental.or
+import net.minecraft.client.util.SkinTextures
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.entity.player.PlayerModelPart
+import net.minecraft.util.Identifier
+
+object ModifyPlayerSkin : EntityModifier {
+ val playerModelPartIndex = PlayerModelPart.entries.associateBy { it.getName() }
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ require(entity is GuiPlayer)
+ info["cape"]?.let {
+ entity.capeTexture = Identifier.of(it.asString)
+ }
+ info["skin"]?.let {
+ entity.skinTexture = Identifier.of(it.asString)
+ }
+ info["slim"]?.let {
+ entity.model = if (it.asBoolean) SkinTextures.Model.SLIM else SkinTextures.Model.WIDE
+ }
+ info["parts"]?.let {
+ var trackedData = entity.dataTracker.get(PlayerEntity.PLAYER_MODEL_PARTS)
+ if (it is JsonPrimitive && it.isBoolean) {
+ trackedData = (if (it.asBoolean) -1 else 0).toByte()
+ } else {
+ val obj = it.asJsonObject
+ for ((k, v) in obj.entrySet()) {
+ val part = playerModelPartIndex[k]!!
+ trackedData = if (v.asBoolean) {
+ trackedData and (part.bitFlag.inv().toByte())
+ } else {
+ trackedData or (part.bitFlag.toByte())
+ }
+ }
+ }
+ entity.dataTracker.set(PlayerEntity.PLAYER_MODEL_PARTS, trackedData)
+ }
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/gui/entity/ModifyRiding.kt b/src/main/kotlin/gui/entity/ModifyRiding.kt
new file mode 100644
index 0000000..5c4c78d
--- /dev/null
+++ b/src/main/kotlin/gui/entity/ModifyRiding.kt
@@ -0,0 +1,15 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+
+object ModifyRiding : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ val newEntity = EntityRenderer.constructEntity(info)
+ require(newEntity != null)
+ newEntity.startRiding(entity, true)
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/gui/entity/ModifyWither.kt b/src/main/kotlin/gui/entity/ModifyWither.kt
new file mode 100644
index 0000000..6083d88
--- /dev/null
+++ b/src/main/kotlin/gui/entity/ModifyWither.kt
@@ -0,0 +1,20 @@
+
+package moe.nea.firmament.gui.entity
+
+import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.boss.WitherEntity
+
+object ModifyWither : EntityModifier {
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ require(entity is WitherEntity)
+ info["tiny"]?.let {
+ entity.setInvulTimer(if (it.asBoolean) 800 else 0)
+ }
+ info["armored"]?.let {
+ entity.health = if (it.asBoolean) 1F else entity.maxHealth
+ }
+ return entity
+ }
+
+}
diff --git a/src/main/kotlin/gui/hud/MoulConfigHud.kt b/src/main/kotlin/gui/hud/MoulConfigHud.kt
new file mode 100644
index 0000000..e77d9af
--- /dev/null
+++ b/src/main/kotlin/gui/hud/MoulConfigHud.kt
@@ -0,0 +1,66 @@
+
+package moe.nea.firmament.gui.hud
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
+import io.github.notenoughupdates.moulconfig.gui.GuiContext
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import net.minecraft.resource.ResourceManager
+import net.minecraft.resource.SynchronousResourceReloader
+import moe.nea.firmament.events.FinalizeResourceManagerEvent
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.gui.config.HudMeta
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+
+abstract class MoulConfigHud(
+ val name: String,
+ val hudMeta: HudMeta,
+) {
+ companion object {
+ private val componentWrapper by lazy {
+ object : GuiComponentWrapper(GuiContext(TextComponent("§cERROR"))) {
+ init {
+ this.client = MC.instance
+ }
+ }
+ }
+ }
+
+ private var fragment: GuiContext? = null
+
+ fun forceInit() {
+ }
+
+ open fun shouldRender(): Boolean {
+ return true
+ }
+
+ init {
+ require(name.matches("^[a-z_/]+$".toRegex()))
+ HudRenderEvent.subscribe {
+ if (!shouldRender()) return@subscribe
+ val renderContext = componentWrapper.createContext(it.context)
+ if (fragment == null)
+ loadFragment()
+ it.context.matrices.push()
+ hudMeta.applyTransformations(it.context.matrices)
+ val renderContextTranslated =
+ renderContext.translated(hudMeta.absoluteX, hudMeta.absoluteY, hudMeta.width, hudMeta.height)
+ .scaled(hudMeta.scale)
+ fragment!!.root.render(renderContextTranslated)
+ it.context.matrices.pop()
+ }
+ FinalizeResourceManagerEvent.subscribe {
+ MC.resourceManager.registerReloader(object : SynchronousResourceReloader {
+ override fun reload(manager: ResourceManager?) {
+ fragment = null
+ }
+ })
+ }
+ }
+
+ fun loadFragment() {
+ fragment = MoulConfigUtils.loadGui(name, this)
+ }
+
+}