diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/triggers/MatteryInventoryChangeTrigger.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/triggers/MatteryInventoryChangeTrigger.kt index cd2fccf4f..841b2d276 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/triggers/MatteryInventoryChangeTrigger.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/triggers/MatteryInventoryChangeTrigger.kt @@ -1,204 +1,110 @@ package ru.dbotthepony.mc.otm.triggers import com.google.gson.JsonObject -import it.unimi.dsi.fastutil.Hash.Strategy -import it.unimi.dsi.fastutil.objects.Object2ObjectFunction -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap +import com.mojang.serialization.Codec import it.unimi.dsi.fastutil.objects.ObjectArrayList -import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.Reference2ObjectFunction import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap import net.minecraft.advancements.CriteriaTriggers import net.minecraft.advancements.CriterionTrigger import net.minecraft.advancements.critereon.InventoryChangeTrigger -import net.minecraft.advancements.critereon.MinMaxBounds import net.minecraft.server.PlayerAdvancements import net.minecraft.server.level.ServerPlayer import net.minecraft.world.Container import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.Item import net.minecraft.world.item.ItemStack import ru.dbotthepony.mc.otm.capability.matteryPlayer import ru.dbotthepony.mc.otm.container.get import ru.dbotthepony.mc.otm.container.util.iterator -import ru.dbotthepony.mc.otm.core.collect.flatMap -import ru.dbotthepony.mc.otm.core.collect.toList import ru.dbotthepony.mc.otm.core.isNotEmpty -import java.util.stream.Collectors +import java.util.stream.Stream + +private typealias TriggerSet = ObjectOpenHashSet> + +private fun items(of: CriterionTrigger.Listener): List { + return of.trigger.items + .stream() + .flatMap { it.items.map { it.stream() }.orElse(Stream.empty()) } + .map { it.value() } + .toList() +} /** * This object detours all necessary InventoryChangeTrigger methods * - * Reason behind this is to support arbitrary containers (not just [Inventory], done for Exopack) and to improve performance by using search tree + * Reason behind this is to support arbitrary containers (not just [Inventory], done for Exopack) and to improve performance */ object MatteryInventoryChangeTrigger : CriterionTrigger { - private object BoundsStrategy : Strategy { - override fun equals(a: MinMaxBounds.Ints?, b: MinMaxBounds.Ints?): Boolean { - return a?.min == b?.min && a?.max == b?.max - } - - override fun hashCode(o: MinMaxBounds.Ints?): Int { - return o?.let { Integer.rotateLeft(it.min.hashCode(), 16) xor it.max.hashCode() } ?: 0 - } - } - - private object DefaultStrategy : Strategy { - override fun equals(a: Any?, b: Any?): Boolean { - return a == b - } - - override fun hashCode(o: Any?): Int { - return o.hashCode() - } - } - - private fun interface Tester { - fun test(value: T, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int): Boolean - } - - private fun interface Hint { - fun getHints(inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int): Collection? - } - - private class Node( - val strategy: Strategy, - private val getter: InventoryChangeTrigger.TriggerInstance.() -> Collection, - val test: Tester, // if hint is present, tester is always going to be called with `null` value once - val hint: Hint? = null - ) { - fun getValues(instance: InventoryChangeTrigger.TriggerInstance): Set { - val result = ObjectArraySet() - result.addAll(getter.invoke(instance)) - return result - } - } - - private val nodes = ArrayList>() - - init { - nodes.add(Node(BoundsStrategy, { listOf(slots.occupied) }, { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v!!.matches(slotsOccupied) })) - nodes.add(Node(BoundsStrategy, { listOf(slots.full) }, { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v!!.matches(slotsFull) })) - nodes.add(Node(BoundsStrategy, { listOf(slots.empty) }, { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v!!.matches(slotsEmpty) })) - - nodes.add(Node( - DefaultStrategy, - { items.iterator().flatMap { (it.items.map { it.map { it.value() } }.orElse(listOf(null))).iterator() }.toList() }, - { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v == null || item.item == v }, - { inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> mutableListOf(item.item) })) - - nodes.add(Node( - DefaultStrategy, - { items.map { it.tag.orElse(null) } }, - { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v == null || item.`is`(v) }, - { inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> item.tags.collect(Collectors.toCollection(::ArrayList)) })) - - nodes.add(Node(BoundsStrategy, { predicates.map { it.count } }, { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v!!.matches(item.count) })) - - nodes.add(Node( - BoundsStrategy, - { items.map { it.durability } }, - { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v!!.isAny || item.isDamageableItem && v.matches(item.maxDamage - item.damageValue) })) - } - private class ListenerTree(private val advancements: PlayerAdvancements) { - private val set = ObjectOpenHashSet>() - private val tree = Object2ObjectOpenCustomHashMap(nodes.first().strategy as Strategy) - - private fun search(instance: InventoryChangeTrigger.TriggerInstance, tree: MutableMap, nodeId: Int): Collection>> { - val node = nodes[nodeId] - - if (nodeId + 1 != nodes.size) { - val result = ArrayList>>() - - for (v in node.getValues(instance)) { - result.addAll( - search( - instance, - tree.computeIfAbsent(v, Object2ObjectFunction { Object2ObjectOpenCustomHashMap(4, nodes[nodeId + 1].strategy as Strategy) }) as MutableMap, - nodeId + 1)) - } - - return result - } else { - val result = ArrayList>>() - - for (v in node.getValues(instance)) { - result.add(tree.computeIfAbsent(v, Object2ObjectFunction { ObjectOpenHashSet>(4) }) as MutableSet>) - } - - return result - } - } + // fastutil for lesser memory footprint, even if it causes lesser performance + private val sorted = Reference2ObjectOpenHashMap() + private val unsorted = TriggerSet() + private val tracked = TriggerSet() fun add(listener: CriterionTrigger.Listener) { - if (set.add(listener)) { - search(listener.trigger, tree, 0).forEach { it.add(listener) } + if (tracked.add(listener)) { + val items = items(listener) + + if (items.isEmpty()) { + unsorted.add(listener) + } else { + for (item in items) { + sorted.computeIfAbsent(item, Reference2ObjectFunction { TriggerSet() }).add(listener) + } + } } } fun remove(listener: CriterionTrigger.Listener) { - if (set.remove(listener)) { - search(listener.trigger, tree, 0).forEach { it.remove(listener) } + if (tracked.remove(listener)) { + unsorted.remove(listener) + + for (item in items(listener)) { + val set = sorted[item] + + if (set != null && set.remove(listener) && set.isEmpty()) { + sorted.remove(item) + } + } } } - private fun addNull(input: Collection): Collection { - return if (null in input) - input - else if (input is ArrayList) { - input.add(null) - input - } else { - ArrayList(input).also { it.add(null) } - } - } + private fun trigger(inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int, elements: TriggerSet, matches: TriggerSet) { + for (l in elements) { + with (l.trigger) { + if (slots.matches(slotsFull, slotsEmpty, slotsOccupied)) { + if (items.isEmpty() || items.size == 1 && item.isNotEmpty && this.items[0].test(item)) { + matches.add(l) + } else if (items.size > 1) { + val unsatisfied = ObjectArrayList(items) + unsatisfied.removeIf { it.test(ItemStack.EMPTY) } - private fun trigger(inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int, tree: MutableMap, nodeId: Int, set: MutableSet>) { - val node = nodes[nodeId] as Node - val keys = node.hint?.getHints(inventory, item, slotsFull, slotsEmpty, slotsOccupied)?.let(::addNull) ?: tree.keys + for (inventoryItem in inventory) { + unsatisfied.removeIf { it.test(inventoryItem) } + if (unsatisfied.isEmpty) break + } - for (k in keys) { - val v = tree[k] ?: continue - - if (node.test.test(k, inventory, item, slotsFull, slotsEmpty, slotsOccupied)) { - if (nodeId + 1 == nodes.size) { - for (l in v as Set>) { - // переделываем matches у InventoryTriggerInstance - with (l.trigger) { - if ( - this.slotsFull.matches(slotsFull) && - this.slotsEmpty.matches(slotsEmpty) && - this.slotsOccupied.matches(slotsOccupied) - ) { - if (this.predicates.isEmpty() || this.predicates.size == 1 && item.isNotEmpty && this.predicates[0].matches(item)) { - set.add(l) - } else if (this.predicates.size > 1) { - val unsatisfied = ObjectArrayList(this.predicates) - unsatisfied.removeIf { it.matches(ItemStack.EMPTY) } - - for (inventoryItem in inventory) { - unsatisfied.removeIf { it.matches(inventoryItem) } - if (unsatisfied.isEmpty) break - } - - if (unsatisfied.isEmpty) { - set.add(l) - } - } - } + if (unsatisfied.isEmpty) { + matches.add(l) } } - } else { - trigger(inventory, item, slotsFull, slotsEmpty, slotsOccupied, v as MutableMap, nodeId + 1, set) } } } } fun trigger(inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int) { - val matches = ObjectOpenHashSet>() + val matches = TriggerSet() - trigger(inventory, item, slotsFull, slotsEmpty, slotsOccupied, tree, 0, matches) + trigger(inventory, item, slotsFull, slotsEmpty, slotsOccupied, unsorted, matches) + + val sorted = sorted[item.item] + + if (sorted != null) { + trigger(inventory, item, slotsFull, slotsEmpty, slotsOccupied, sorted, matches) + } for (l in matches) { l.run(advancements) @@ -220,18 +126,14 @@ object MatteryInventoryChangeTrigger : CriterionTrigger { + return CriteriaTriggers.INVENTORY_CHANGED.codec() } // реплицирует ванильный метод fun trigger(player: ServerPlayer, inventory: Container, item: ItemStack) { if (inventory === player.inventory) { - val mattery = player.matteryPlayer - - if (mattery != null) { - return trigger(player, mattery.combinedInventory, item) - } + return trigger(player, player.matteryPlayer.combinedInventory, item) } var slotsFull = 0