feat: Add npc location exporter

This commit is contained in:
Linnea Gräf
2025-06-22 22:45:16 +02:00
parent fbc44e2139
commit 1ab9094bde
8 changed files with 176 additions and 55 deletions

View File

@@ -58,6 +58,7 @@ object PowerUserTools : FirmamentFeature {
val copyTitle by keyBindingWithDefaultUnbound("copy-title") val copyTitle by keyBindingWithDefaultUnbound("copy-title")
val exportItemStackToRepo by keyBindingWithDefaultUnbound("export-item-stack") val exportItemStackToRepo by keyBindingWithDefaultUnbound("export-item-stack")
val exportUIRecipes by keyBindingWithDefaultUnbound("export-recipe") val exportUIRecipes by keyBindingWithDefaultUnbound("export-recipe")
val exportNpcLocation by keyBindingWithDefaultUnbound("export-npc-location")
} }
override val config override val config

View File

@@ -1,19 +1,27 @@
package moe.nea.firmament.features.debug.itemeditor package moe.nea.firmament.features.debug.itemeditor
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import net.minecraft.client.network.ClientPlayerEntity
import net.minecraft.entity.decoration.ArmorStandEntity
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenKeyPressedEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.features.debug.PowerUserTools import moe.nea.firmament.features.debug.PowerUserTools
import moe.nea.firmament.repo.ItemNameLookup import moe.nea.firmament.repo.ItemNameLookup
import moe.nea.firmament.util.MC import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SHORT_NUMBER_FORMAT import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.async.waitForTextInput
import moe.nea.firmament.util.ifDropLast import moe.nea.firmament.util.ifDropLast
import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex
import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.mc.setSkullOwner
import moe.nea.firmament.util.parseShortNumber import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.red import moe.nea.firmament.util.red
import moe.nea.firmament.util.removeColorCodes import moe.nea.firmament.util.removeColorCodes
@@ -32,10 +40,46 @@ object ExportRecipe {
val x = it % 3 val x = it % 3
val y = it / 3 val y = it / 3
(xNames[x].toString() + yNames[y]) to x + y * 9 + 10 (yNames[y].toString() + xNames[x].toString()) to x + y * 9 + 10
} }
val resultSlot = 25 val resultSlot = 25
@Subscribe
fun exportNpcLocation(event: WorldKeyboardEvent) {
if (!event.matches(PowerUserTools.TConfig.exportNpcLocation)) {
return
}
val entity = MC.instance.targetedEntity
if (entity == null) {
MC.sendChat(tr("firmament.repo.export.npc.noentity", "Could not find entity to export"))
return
}
Firmament.coroutineScope.launch {
val guessName = entity.world.getEntitiesByClass(
ArmorStandEntity::class.java,
entity.boundingBox.expand(0.1),
{ !it.name.string.contains("CLICK") })
.firstOrNull()?.customName?.string
?: ""
val reply = waitForTextInput("$guessName (NPC)", "Export stub")
val id = generateName(reply)
ItemExporter.exportStub(id, reply) {
val playerEntity = entity as? ClientPlayerEntity
val textureUrl = playerEntity?.skinTextures?.textureUrl
if (textureUrl != null)
it.setSkullOwner(playerEntity.uuid, textureUrl)
}
ItemExporter.modifyJson(id) {
val mutJson = it.toMutableMap()
mutJson["island"] = JsonPrimitive(SBData.skyblockLocation?.locrawMode ?: "unknown")
mutJson["x"] = JsonPrimitive(entity.blockX)
mutJson["y"] = JsonPrimitive(entity.blockY)
mutJson["z"] = JsonPrimitive(entity.blockZ)
JsonObject(mutJson)
}
}
}
@Subscribe @Subscribe
fun onRecipeKeyBind(event: HandledScreenKeyPressedEvent) { fun onRecipeKeyBind(event: HandledScreenKeyPressedEvent) {
if (!event.matches(PowerUserTools.TConfig.exportUIRecipes)) { if (!event.matches(PowerUserTools.TConfig.exportUIRecipes)) {
@@ -138,7 +182,7 @@ object ExportRecipe {
} }
fun generateName(name: String): SkyblockId { fun generateName(name: String): SkyblockId {
return SkyblockId(name.uppercase().replace(" ", "_")) return SkyblockId(name.uppercase().replace(" ", "_").replace("(", "").replace(")", ""))
} }
fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair<SkyblockId, Double>? { fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair<SkyblockId, Double>? {

View File

@@ -65,13 +65,20 @@ object ItemExporter {
exportItem(itemStack) exportItem(itemStack)
} }
fun appendRecipe(skyblockId: SkyblockId, recipe: JsonObject) { fun modifyJson(skyblockId: SkyblockId, modify: (JsonObject) -> JsonObject) {
val oldJson = Firmament.json.decodeFromString<JsonObject>(pathFor(skyblockId).readText()) val oldJson = Firmament.json.decodeFromString<JsonObject>(pathFor(skyblockId).readText())
val mutableJson = oldJson.toMutableMap() val newJson = modify(oldJson)
val recipes = ((mutableJson["recipes"] as JsonArray?) ?: listOf()).toMutableList() pathFor(skyblockId).writeText(Firmament.twoSpaceJson.encodeToString(JsonObject(newJson)))
recipes.add(recipe) }
mutableJson["recipes"] = JsonArray(recipes)
pathFor(skyblockId).writeText(Firmament.twoSpaceJson.encodeToString(JsonObject(mutableJson))) fun appendRecipe(skyblockId: SkyblockId, recipe: JsonObject) {
modifyJson(skyblockId) { oldJson ->
val mutableJson = oldJson.toMutableMap()
val recipes = ((mutableJson["recipes"] as JsonArray?) ?: listOf()).toMutableList()
recipes.add(recipe)
mutableJson["recipes"] = JsonArray(recipes)
JsonObject(mutableJson)
}
} }
@Subscribe @Subscribe
@@ -82,7 +89,7 @@ object ItemExporter {
} }
} }
fun exportStub(skyblockId: SkyblockId, title: String) { fun exportStub(skyblockId: SkyblockId, title: String, extra: (ItemStack) -> Unit = {}) {
exportItem(ItemStack(Items.PLAYER_HEAD).also { exportItem(ItemStack(Items.PLAYER_HEAD).also {
it.displayNameAccordingToNbt = Text.literal(title) it.displayNameAccordingToNbt = Text.literal(title)
it.loreAccordingToNbt = listOf(Text.literal("")) it.loreAccordingToNbt = listOf(Text.literal(""))

View File

@@ -35,6 +35,9 @@ import moe.nea.firmament.util.transformEachRecursively
import moe.nea.firmament.util.unformattedString import moe.nea.firmament.util.unformattedString
class LegacyItemExporter private constructor(var itemStack: ItemStack) { class LegacyItemExporter private constructor(var itemStack: ItemStack) {
init {
require(!itemStack.isEmpty)
}
var lore = itemStack.loreAccordingToNbt var lore = itemStack.loreAccordingToNbt
var name = itemStack.displayNameAccordingToNbt var name = itemStack.displayNameAccordingToNbt
val extraAttribs = itemStack.extraAttributes.copy() val extraAttribs = itemStack.extraAttributes.copy()

View File

@@ -0,0 +1,15 @@
package moe.nea.firmament.features.debug.itemeditor
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.TextComponent
import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent
import io.github.notenoughupdates.moulconfig.observer.GetSetter
import kotlin.reflect.KMutableProperty0
import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.util.MoulConfigUtils

View File

@@ -24,4 +24,4 @@ class VanillaScreenProvider : HoveredItemStackProvider {
val HandledScreen<*>.focusedItemStack: ItemStack? val HandledScreen<*>.focusedItemStack: ItemStack?
get() = get() =
HoveredItemStackProvider.allValidInstances HoveredItemStackProvider.allValidInstances
.firstNotNullOfOrNull { it.provideHoveredItemStack(this) } .firstNotNullOfOrNull { it.provideHoveredItemStack(this)?.takeIf { !it.isEmpty } }

View File

@@ -9,7 +9,6 @@ import io.github.notenoughupdates.moulconfig.gui.GuiContext
import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
import io.github.notenoughupdates.moulconfig.gui.MouseEvent import io.github.notenoughupdates.moulconfig.gui.MouseEvent
import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
import io.github.notenoughupdates.moulconfig.observer.GetSetter import io.github.notenoughupdates.moulconfig.observer.GetSetter
import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext
import io.github.notenoughupdates.moulconfig.xml.ChildCount import io.github.notenoughupdates.moulconfig.xml.ChildCount
@@ -21,7 +20,6 @@ import java.io.File
import java.util.function.Supplier import java.util.function.Supplier
import javax.xml.namespace.QName import javax.xml.namespace.QName
import me.shedaniel.math.Color import me.shedaniel.math.Color
import org.jetbrains.annotations.Unmodifiable
import org.w3c.dom.Element import org.w3c.dom.Element
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -41,13 +39,15 @@ object MoulConfigUtils {
fun main(args: Array<out String>) { fun main(args: Array<out String>) {
generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS) generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS)
generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl) generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl)
File("wrapper.xsd").writeText(""" File("wrapper.xsd").writeText(
"""
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/> <xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/>
<xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/> <xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/>
</xs:schema> </xs:schema>
""".trimIndent()) """.trimIndent()
)
} }
val firmUrl = "http://firmament.nea.moe/moulconfig" val firmUrl = "http://firmament.nea.moe/moulconfig"
@@ -96,9 +96,11 @@ object MoulConfigUtils {
override fun createInstance(context: XMLContext<*>, element: Element): FirmHoverComponent { override fun createInstance(context: XMLContext<*>, element: Element): FirmHoverComponent {
return FirmHoverComponent( return FirmHoverComponent(
context.getChildFragment(element), context.getChildFragment(element),
context.getPropertyFromAttribute(element, context.getPropertyFromAttribute(
QName("lines"), element,
List::class.java) as Supplier<List<String>>, QName("lines"),
List::class.java
) as Supplier<List<String>>,
context.getPropertyFromAttribute(element, QName("delay"), Duration::class.java, 0.6.seconds), context.getPropertyFromAttribute(element, QName("delay"), Duration::class.java, 0.6.seconds),
) )
} }
@@ -223,16 +225,21 @@ object MoulConfigUtils {
generator.dumpToFile(file) generator.dumpToFile(file)
} }
fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen { fun wrapScreen(guiContext: GuiContext, parent: Screen?, onClose: () -> Unit = {}): Screen {
return object : GuiComponentWrapper(loadGui(name, bindTo)) { return object : GuiComponentWrapper(guiContext) {
override fun close() { override fun close() {
if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) { if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) {
client!!.setScreen(parent) client!!.setScreen(parent)
onClose()
} }
} }
} }
} }
fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen {
return wrapScreen(loadGui(name, bindTo), parent)
}
// TODO: move this utility into moulconfig (also rework guicontext into an interface so i can make this mesh better into vanilla) // TODO: move this utility into moulconfig (also rework guicontext into an interface so i can make this mesh better into vanilla)
fun GuiContext.adopt(element: GuiComponent) = element.foldRecursive(Unit, { comp, unit -> comp.context = this }) fun GuiContext.adopt(element: GuiComponent) = element.foldRecursive(Unit, { comp, unit -> comp.context = this })
@@ -288,12 +295,14 @@ object MoulConfigUtils {
assert(drawContext?.isUntranslatedGuiDrawContext() != false) assert(drawContext?.isUntranslatedGuiDrawContext() != false)
val context = drawContext?.let(::ModernRenderContext) val context = drawContext?.let(::ModernRenderContext)
?: IMinecraft.instance.provideTopLevelRenderContext() ?: IMinecraft.instance.provideTopLevelRenderContext()
val immContext = GuiImmediateContext(context, val immContext = GuiImmediateContext(
0, 0, 0, 0, context,
mouseX, mouseY, 0, 0, 0, 0,
mouseX, mouseY, mouseX, mouseY,
mouseX.toFloat(), mouseX, mouseY,
mouseY.toFloat()) mouseX.toFloat(),
mouseY.toFloat()
)
return immContext return immContext
} }

View File

@@ -1,47 +1,89 @@
package moe.nea.firmament.util.async package moe.nea.firmament.util.async
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.TextComponent
import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent
import io.github.notenoughupdates.moulconfig.observer.GetSetter
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume import kotlin.coroutines.resume
import net.minecraft.client.gui.screen.Screen
import moe.nea.firmament.events.HandledScreenKeyPressedEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.keybindings.IKeyBinding import moe.nea.firmament.keybindings.IKeyBinding
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil
private object InputHandler { private object InputHandler {
data class KeyInputContinuation(val keybind: IKeyBinding, val onContinue: () -> Unit) data class KeyInputContinuation(val keybind: IKeyBinding, val onContinue: () -> Unit)
private val activeContinuations = mutableListOf<KeyInputContinuation>() private val activeContinuations = mutableListOf<KeyInputContinuation>()
fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit { fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit {
synchronized(InputHandler) { synchronized(InputHandler) {
activeContinuations.add(keyInputContinuation) activeContinuations.add(keyInputContinuation)
} }
return { return {
synchronized(this) { synchronized(this) {
activeContinuations.remove(keyInputContinuation) activeContinuations.remove(keyInputContinuation)
} }
} }
} }
init { init {
HandledScreenKeyPressedEvent.subscribe("Input:resumeAfterInput") { event -> HandledScreenKeyPressedEvent.subscribe("Input:resumeAfterInput") { event ->
synchronized(InputHandler) { synchronized(InputHandler) {
val toRemove = activeContinuations.filter { val toRemove = activeContinuations.filter {
event.matches(it.keybind) event.matches(it.keybind)
} }
toRemove.forEach { it.onContinue() } toRemove.forEach { it.onContinue() }
activeContinuations.removeAll(toRemove) activeContinuations.removeAll(toRemove)
} }
} }
} }
} }
suspend fun waitForInput(keybind: IKeyBinding): Unit = suspendCancellableCoroutine { cont -> suspend fun waitForInput(keybind: IKeyBinding): Unit = suspendCancellableCoroutine { cont ->
val unregister = val unregister =
InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) }) InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) })
cont.invokeOnCancellation { cont.invokeOnCancellation {
unregister() unregister()
} }
} }
fun createPromptScreenGuiComponent(suggestion: String, prompt: String, action: Runnable) = (run {
val text = GetSetter.floating(suggestion)
GuiContext(
CenterComponent(
PanelComponent(
ColumnComponent(
TextFieldComponent(text, 120),
FirmButtonComponent(TextComponent(prompt), action = action)
)
)
)
) to text
})
suspend fun waitForTextInput(suggestion: String, prompt: String) =
suspendCancellableCoroutine<String> { cont ->
lateinit var screen: Screen
lateinit var text: GetSetter<String>
val action = {
if (MC.screen === screen)
MC.screen = null
// TODO: should this exit
cont.resume(text.get())
}
val (gui, text_) = createPromptScreenGuiComponent(suggestion, prompt, action)
text = text_
screen = MoulConfigUtils.wrapScreen(gui, null, onClose = action)
ScreenUtil.setScreenLater(screen)
cont.invokeOnCancellation {
action()
}
}