feat: Add more complex entity equipment scraper
This commit is contained in:
@@ -7,6 +7,8 @@ import net.minecraft.entity.LivingEntity
|
||||
import net.minecraft.entity.data.DataTracker
|
||||
import net.minecraft.item.ItemStack
|
||||
import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket
|
||||
import moe.nea.firmament.annotations.Subscribe
|
||||
import moe.nea.firmament.util.MC
|
||||
|
||||
/**
|
||||
* This event is fired when some entity properties are updated.
|
||||
@@ -15,7 +17,27 @@ import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket
|
||||
* *after* the values have been applied to the entity.
|
||||
*/
|
||||
sealed class EntityUpdateEvent : FirmamentEvent() {
|
||||
companion object : FirmamentEventBus<EntityUpdateEvent>()
|
||||
companion object : FirmamentEventBus<EntityUpdateEvent>() {
|
||||
@Subscribe
|
||||
fun onPlayerInventoryUpdate(event: PlayerInventoryUpdate) {
|
||||
val p = MC.player ?: return
|
||||
val updatedSlots = listOf(
|
||||
EquipmentSlot.HEAD to 39,
|
||||
EquipmentSlot.CHEST to 38,
|
||||
EquipmentSlot.LEGS to 37,
|
||||
EquipmentSlot.FEET to 36,
|
||||
EquipmentSlot.OFFHAND to 40,
|
||||
EquipmentSlot.MAINHAND to p.inventory.selectedSlot, // TODO: also equipment update when you swap your selected slot perhaps
|
||||
).mapNotNull { (slot, stackIndex) ->
|
||||
val slotIndex = p.playerScreenHandler.getSlotIndex(p.inventory, stackIndex).asInt
|
||||
event.getOrNull(slotIndex)?.let {
|
||||
Pair.of(slot, it)
|
||||
}
|
||||
}
|
||||
if (updatedSlots.isNotEmpty())
|
||||
publish(EquipmentUpdate(p, updatedSlots))
|
||||
}
|
||||
}
|
||||
|
||||
abstract val entity: Entity
|
||||
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
|
||||
package moe.nea.firmament.events
|
||||
|
||||
import net.minecraft.item.ItemStack
|
||||
|
||||
sealed class PlayerInventoryUpdate : FirmamentEvent() {
|
||||
companion object : FirmamentEventBus<PlayerInventoryUpdate>()
|
||||
data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate()
|
||||
data class Multi(val contents: List<ItemStack>) : PlayerInventoryUpdate()
|
||||
companion object : FirmamentEventBus<PlayerInventoryUpdate>()
|
||||
data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate() {
|
||||
override fun getOrNull(slot: Int): ItemStack? {
|
||||
if (slot == this.slot) return stack
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class Multi(val contents: List<ItemStack>) : PlayerInventoryUpdate() {
|
||||
override fun getOrNull(slot: Int): ItemStack? {
|
||||
return contents.getOrNull(slot)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getOrNull(slot: Int): ItemStack?
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@ package moe.nea.firmament.features.debug
|
||||
|
||||
import net.minecraft.command.argument.RegistryKeyArgumentType
|
||||
import net.minecraft.component.ComponentType
|
||||
import net.minecraft.component.DataComponentTypes
|
||||
import net.minecraft.entity.Entity
|
||||
import net.minecraft.entity.decoration.ArmorStandEntity
|
||||
import net.minecraft.item.ItemStack
|
||||
import net.minecraft.nbt.NbtElement
|
||||
import net.minecraft.nbt.NbtOps
|
||||
import net.minecraft.registry.RegistryKeys
|
||||
import net.minecraft.util.Identifier
|
||||
import moe.nea.firmament.annotations.Subscribe
|
||||
import moe.nea.firmament.commands.get
|
||||
import moe.nea.firmament.commands.thenArgument
|
||||
@@ -16,16 +15,17 @@ import moe.nea.firmament.commands.thenExecute
|
||||
import moe.nea.firmament.commands.thenLiteral
|
||||
import moe.nea.firmament.events.CommandEvent
|
||||
import moe.nea.firmament.events.EntityUpdateEvent
|
||||
import moe.nea.firmament.events.WorldReadyEvent
|
||||
import moe.nea.firmament.util.ClipboardUtils
|
||||
import moe.nea.firmament.util.MC
|
||||
import moe.nea.firmament.util.math.GChainReconciliation
|
||||
import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle
|
||||
import moe.nea.firmament.util.mc.NbtPrism
|
||||
import moe.nea.firmament.util.skyBlockId
|
||||
import moe.nea.firmament.util.tr
|
||||
|
||||
object AnimatedClothingScanner {
|
||||
|
||||
data class SubjectOfFashionTheft<T>(
|
||||
val observedEntity: Entity,
|
||||
data class LensOfFashionTheft<T>(
|
||||
val prism: NbtPrism,
|
||||
val component: ComponentType<T>,
|
||||
) {
|
||||
@@ -36,75 +36,158 @@ object AnimatedClothingScanner {
|
||||
}
|
||||
}
|
||||
|
||||
var subject: SubjectOfFashionTheft<*>? = null
|
||||
var lens: LensOfFashionTheft<*>? = null
|
||||
var subject: Entity? = null
|
||||
var history: MutableList<String> = mutableListOf()
|
||||
val metaHistory: MutableList<List<String>> = mutableListOf()
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Subscribe
|
||||
fun onUpdate(event: EntityUpdateEvent) {
|
||||
val s = subject ?: return
|
||||
if (event.entity != s.observedEntity) return
|
||||
if (event.entity != s) return
|
||||
val l = lens ?: return
|
||||
if (event is EntityUpdateEvent.EquipmentUpdate) {
|
||||
val lines = mutableListOf<String>()
|
||||
event.newEquipment.forEach {
|
||||
val formatted = (s.observe(it.second)).joinToString()
|
||||
lines.add(formatted)
|
||||
MC.sendChat(
|
||||
tr(
|
||||
"firmament.fitstealer.update",
|
||||
"[FIT CHECK][${MC.currentTick}] ${it.first.asString()} => $formatted"
|
||||
)
|
||||
)
|
||||
}
|
||||
if (lines.isNotEmpty()) {
|
||||
val contents = ClipboardUtils.getTextContents()
|
||||
if (contents.startsWith(EXPORT_WATERMARK))
|
||||
ClipboardUtils.setTextContent(
|
||||
contents + "\n" + lines.joinToString("\n")
|
||||
)
|
||||
val formatted = (l.observe(it.second)).joinToString()
|
||||
history.add(formatted)
|
||||
// TODO: add a slot filter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val EXPORT_WATERMARK = "[CLOTHES EXPORT]"
|
||||
fun reduceHistory(reducer: (List<String>, List<String>) -> List<String>): List<String> {
|
||||
return metaHistory.fold(history, reducer).shortenCycle()
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
fun onSubCommand(event: CommandEvent.SubCommand) {
|
||||
event.subcommand("dev") {
|
||||
thenLiteral("stealthisfit") {
|
||||
thenArgument(
|
||||
"component",
|
||||
RegistryKeyArgumentType.registryKey(RegistryKeys.DATA_COMPONENT_TYPE)
|
||||
) { component ->
|
||||
thenArgument("path", NbtPrism.Argument) { path ->
|
||||
thenLiteral("clear") {
|
||||
thenExecute {
|
||||
subject = null
|
||||
metaHistory.clear()
|
||||
history.clear()
|
||||
MC.sendChat(tr("firmament.fitstealer.clear", "Cleared fit stealing history"))
|
||||
}
|
||||
}
|
||||
thenLiteral("copy") {
|
||||
thenExecute {
|
||||
val history = reduceHistory { a, b -> a + b }
|
||||
copyHistory(history)
|
||||
MC.sendChat(tr("firmament.fitstealer.copied", "Copied the history"))
|
||||
}
|
||||
thenLiteral("deduplicated") {
|
||||
thenExecute {
|
||||
subject =
|
||||
if (subject == null) run {
|
||||
val entity = MC.instance.targetedEntity ?: return@run null
|
||||
val clipboard = ClipboardUtils.getTextContents()
|
||||
if (!clipboard.startsWith(EXPORT_WATERMARK)) {
|
||||
ClipboardUtils.setTextContent(EXPORT_WATERMARK)
|
||||
} else {
|
||||
ClipboardUtils.setTextContent("$clipboard\n\n[NEW SCANNER]")
|
||||
}
|
||||
SubjectOfFashionTheft(
|
||||
entity,
|
||||
get(path),
|
||||
MC.unsafeGetRegistryEntry(get(component))!!,
|
||||
)
|
||||
} else null
|
||||
|
||||
val history = reduceHistory { a, b ->
|
||||
(a.toMutableSet() + b).toList()
|
||||
}
|
||||
copyHistory(history)
|
||||
MC.sendChat(
|
||||
subject?.let {
|
||||
tr(
|
||||
"firmament.fitstealer.targeted",
|
||||
"Observing the equipment of ${it.observedEntity.name}."
|
||||
)
|
||||
} ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."),
|
||||
tr(
|
||||
"firmament.fitstealer.copied.deduplicated",
|
||||
"Copied the deduplicated history"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
thenLiteral("merged") {
|
||||
thenExecute {
|
||||
val history = reduceHistory(GChainReconciliation::reconcileCycles)
|
||||
copyHistory(history)
|
||||
MC.sendChat(tr("firmament.fitstealer.copied.merged", "Copied the merged history"))
|
||||
}
|
||||
}
|
||||
}
|
||||
thenLiteral("target") {
|
||||
thenLiteral("self") {
|
||||
thenExecute {
|
||||
toggleObserve(MC.player!!)
|
||||
}
|
||||
}
|
||||
thenLiteral("pet") {
|
||||
thenExecute {
|
||||
source.sendFeedback(
|
||||
tr(
|
||||
"firmament.fitstealer.stealingpet",
|
||||
"Observing nearest marker armourstand"
|
||||
)
|
||||
)
|
||||
val p = MC.player!!
|
||||
val nearestPet = p.world.getEntitiesByClass(
|
||||
ArmorStandEntity::class.java,
|
||||
p.boundingBox.expand(10.0),
|
||||
{ it.isMarker })
|
||||
.minBy { it.squaredDistanceTo(p) }
|
||||
toggleObserve(nearestPet)
|
||||
}
|
||||
}
|
||||
thenExecute {
|
||||
val ent = MC.instance.targetedEntity
|
||||
if (ent == null) {
|
||||
source.sendFeedback(
|
||||
tr(
|
||||
"firmament.fitstealer.notargetundercursor",
|
||||
"No entity under cursor"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
toggleObserve(ent)
|
||||
}
|
||||
}
|
||||
}
|
||||
thenLiteral("path") {
|
||||
thenArgument(
|
||||
"component",
|
||||
RegistryKeyArgumentType.registryKey(RegistryKeys.DATA_COMPONENT_TYPE)
|
||||
) { component ->
|
||||
thenArgument("path", NbtPrism.Argument) { path ->
|
||||
thenExecute {
|
||||
lens = LensOfFashionTheft(
|
||||
get(path),
|
||||
MC.unsafeGetRegistryEntry(get(component))!!,
|
||||
)
|
||||
source.sendFeedback(
|
||||
tr(
|
||||
"firmament.fitstealer.lensset",
|
||||
"Analyzing path ${get(path)} for component ${get(component).value}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyHistory(toCopy: List<String>) {
|
||||
ClipboardUtils.setTextContent(toCopy.joinToString("\n"))
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
fun onWorldSwap(event: WorldReadyEvent) {
|
||||
subject = null
|
||||
if (history.isNotEmpty()) {
|
||||
metaHistory.add(history)
|
||||
history = mutableListOf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleObserve(entity: Entity?) {
|
||||
subject = if (subject == null) entity else null
|
||||
if (subject == null) {
|
||||
metaHistory.add(history)
|
||||
history = mutableListOf()
|
||||
}
|
||||
MC.sendChat(
|
||||
subject?.let {
|
||||
tr(
|
||||
"firmament.fitstealer.targeted",
|
||||
"Observing the equipment of ${it.name}."
|
||||
)
|
||||
} ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
102
src/main/kotlin/util/math/GChainReconciliation.kt
Normal file
102
src/main/kotlin/util/math/GChainReconciliation.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
package moe.nea.firmament.util.math
|
||||
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Algorithm for (sort of) cheap reconciliation of two cycles with missing frames.
|
||||
*/
|
||||
object GChainReconciliation {
|
||||
// Step one: Find the most common element and shift the arrays until it is at the start in both (this could be just rotating until minimal levenshtein distance or smth. that would be way better for cycles with duplicates, but i do not want to implement levenshtein as well)
|
||||
// Step two: Find the first different element.
|
||||
// Step three: Find the next index of both of the elements.
|
||||
// Step four: Insert the element that is further away.
|
||||
|
||||
fun <T> Iterable<T>.frequencies(): Map<T, Int> {
|
||||
val acc = mutableMapOf<T, Int>()
|
||||
for (t in this) {
|
||||
acc.compute(t, { _, old -> (old ?: 0) + 1 })
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
fun <T> findMostCommonlySharedElement(
|
||||
leftChain: List<T>,
|
||||
rightChain: List<T>,
|
||||
): T {
|
||||
val lf = leftChain.frequencies()
|
||||
val rf = rightChain.frequencies()
|
||||
val mostCommonlySharedElement = lf.maxByOrNull { min(it.value, rf[it.key] ?: 0) }?.key
|
||||
if (mostCommonlySharedElement == null || mostCommonlySharedElement !in rf)
|
||||
error("Could not find a shared element")
|
||||
return mostCommonlySharedElement
|
||||
}
|
||||
|
||||
fun <T> List<T>.getMod(index: Int): T {
|
||||
return this[index.mod(size)]
|
||||
}
|
||||
|
||||
fun <T> List<T>.rotated(offset: Int): List<T> {
|
||||
val newList = mutableListOf<T>()
|
||||
for (index in indices) {
|
||||
newList.add(getMod(index - offset))
|
||||
}
|
||||
return newList
|
||||
}
|
||||
|
||||
fun <T> shiftToFront(list: List<T>, element: T): List<T> {
|
||||
val shiftDistance = list.indexOf(element)
|
||||
require(shiftDistance >= 0)
|
||||
return list.rotated(-shiftDistance)
|
||||
}
|
||||
|
||||
fun <T> List<T>.indexOfOrMaxInt(element: T): Int = indexOf(element).takeUnless { it < 0 } ?: Int.MAX_VALUE
|
||||
|
||||
fun <T> reconcileCycles(
|
||||
leftChain: List<T>,
|
||||
rightChain: List<T>,
|
||||
): List<T> {
|
||||
val mostCommonElement = findMostCommonlySharedElement(leftChain, rightChain)
|
||||
val left = shiftToFront(leftChain, mostCommonElement).toMutableList()
|
||||
val right = shiftToFront(rightChain, mostCommonElement).toMutableList()
|
||||
|
||||
var index = 0
|
||||
while (index < left.size && index < right.size) {
|
||||
val leftEl = left[index]
|
||||
val rightEl = right[index]
|
||||
if (leftEl == rightEl) {
|
||||
index++
|
||||
continue
|
||||
}
|
||||
val nextLeftInRight = right.subList(index, right.size)
|
||||
.indexOfOrMaxInt(leftEl)
|
||||
|
||||
val nextRightInLeft = left.subList(index, left.size)
|
||||
.indexOfOrMaxInt(rightEl)
|
||||
if (nextLeftInRight < nextRightInLeft) {
|
||||
left.add(index, rightEl)
|
||||
} else if (nextRightInLeft < nextLeftInRight) {
|
||||
right.add(index, leftEl)
|
||||
} else {
|
||||
index++
|
||||
}
|
||||
}
|
||||
return if (left.size < right.size) right else left
|
||||
}
|
||||
|
||||
fun <T> isValidCycle(longList: List<T>, cycle: List<T>): Boolean {
|
||||
for ((i, value) in longList.withIndex()) {
|
||||
if (cycle.getMod(i) != value)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun <T> List<T>.shortenCycle(): List<T> {
|
||||
for (i in (1..<size)) {
|
||||
if (isValidCycle(this, subList(0, i)))
|
||||
return subList(0, i)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
}
|
||||
75
src/test/kotlin/util/math/GChainReconciliationTest.kt
Normal file
75
src/test/kotlin/util/math/GChainReconciliationTest.kt
Normal file
@@ -0,0 +1,75 @@
|
||||
package moe.nea.firmament.test.util.math
|
||||
|
||||
import io.kotest.core.spec.style.AnnotationSpec
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import moe.nea.firmament.util.math.GChainReconciliation
|
||||
import moe.nea.firmament.util.math.GChainReconciliation.rotated
|
||||
|
||||
class GChainReconciliationTest : AnnotationSpec() {
|
||||
|
||||
fun <T> assertEqualCycles(
|
||||
expected: List<T>,
|
||||
actual: List<T>
|
||||
) {
|
||||
for (offset in expected.indices) {
|
||||
val rotated = expected.rotated(offset)
|
||||
val matchesAtRotation = run {
|
||||
for ((i, v) in actual.withIndex()) {
|
||||
if (rotated[i % rotated.size] != v)
|
||||
return@run false
|
||||
}
|
||||
true
|
||||
}
|
||||
if (matchesAtRotation)
|
||||
return
|
||||
}
|
||||
assertEquals(expected, actual, "Expected arrays to be cycle equivalent")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnfixableCycleNotBeingModified() {
|
||||
assertEquals(
|
||||
listOf(1, 2, 3, 4, 6, 1, 2, 3, 4, 6),
|
||||
GChainReconciliation.reconcileCycles(
|
||||
listOf(1, 2, 3, 4, 6, 1, 2, 3, 4, 6),
|
||||
listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleIndependentHoles() {
|
||||
assertEqualCycles(
|
||||
listOf(1, 2, 3, 4, 5, 6),
|
||||
GChainReconciliation.reconcileCycles(
|
||||
listOf(1, 3, 4, 5, 6, 1, 3, 4, 5, 6),
|
||||
listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBigHole() {
|
||||
assertEqualCycles(
|
||||
listOf(1, 2, 3, 4, 5, 6),
|
||||
GChainReconciliation.reconcileCycles(
|
||||
listOf(1, 4, 5, 6, 1, 4, 5, 6),
|
||||
listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOneMissingBeingDetected() {
|
||||
assertEqualCycles(
|
||||
listOf(1, 2, 3, 4, 5, 6),
|
||||
GChainReconciliation.reconcileCycles(
|
||||
listOf(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6),
|
||||
listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user