From de79019f56b635a82bedaeff14b7ca3170495c37 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Thu, 6 Mar 2025 16:06:04 +0700 Subject: [PATCH] Implement EnhancedContainer --- .../mc/otm/container/EnhancedContainer.kt | 190 ++++++++++++++++++ .../otm/container/slotted/SlottedContainer.kt | 15 +- 2 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/container/EnhancedContainer.kt diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/container/EnhancedContainer.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/container/EnhancedContainer.kt new file mode 100644 index 000000000..f3a4defd5 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/container/EnhancedContainer.kt @@ -0,0 +1,190 @@ +package ru.dbotthepony.mc.otm.container + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet +import net.minecraft.core.HolderLookup.Provider +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.ListTag +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.Tag +import net.minecraft.resources.RegistryOps +import net.minecraft.world.SimpleContainer +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.ItemStack +import net.neoforged.neoforge.common.util.INBTSerializable +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.mc.otm.container.slotted.SlottedContainer +import ru.dbotthepony.mc.otm.core.isNotEmpty +import ru.dbotthepony.mc.otm.core.nbt.set + +/** + * Flexible base implementation of [IEnhancedContainer], designed to be inherited, or used as-is + * if no specific logic is required (this implementation is more efficient than one provided by [SlottedContainer.simple]). + * + * This is supposed to be counterpart to [SimpleContainer] of Minecraft itself, with more features + * and improved performance (inside [IEnhancedContainer] defined methods). + */ +open class EnhancedContainer(private val size: Int) : IEnhancedContainer, INBTSerializable { + private val items = Array(size) { ItemStack.EMPTY } + private val observedItems = Array(size) { ItemStack.EMPTY } + private val slots by lazy(LazyThreadSafetyMode.PUBLICATION) { Array(size) { IContainerSlot.Simple(it, this) } } + + // can be safely overridden in subclasses, very little memory will be wasted + override fun containerSlot(slot: Int): IContainerSlot { + return slots[slot] + } + + protected open fun notifySlotChanged(slot: Int, old: ItemStack) {} + + private fun observeSlotChanges(slot: Int) { + if (items[slot].count != observedItems[slot].count || !ItemStack.isSameItemSameComponents(items[slot], observedItems[slot])) { + notifySlotChanged(slot, observedItems[slot]) + + if (items[slot].isEmpty) { + items[slot] = ItemStack.EMPTY + observedItems[slot] = ItemStack.EMPTY + } else { + notifySlotChanged(slot, observedItems[slot]) + observedItems[slot] = items[slot].copy() + } + } + } + + override fun setChanged(slot: Int) { + observeSlotChanges(slot) + } + + override fun clearContent() { + items.fill(ItemStack.EMPTY) + observedItems.fill(ItemStack.EMPTY) + } + + override fun setChanged() { + for (slot in 0 until size) + observeSlotChanges(slot) + } + + final override fun getContainerSize(): Int { + return size + } + + final override fun getItem(slot: Int): ItemStack { + return items[slot] + } + + final override fun removeItem(slot: Int, amount: Int): ItemStack { + val item = items[slot] + + if (item.isEmpty) + return ItemStack.EMPTY + else if (item.count >= amount) + return removeItemNoUpdate(slot) + else { + val split = item.split(amount) + notifySlotChanged(slot, observedItems[slot]) + observedItems[slot] = item.copy() + return split + } + } + + final override fun removeItemNoUpdate(slot: Int): ItemStack { + val item = items[slot] + + if (item.isEmpty) + return ItemStack.EMPTY + + items[slot] = ItemStack.EMPTY + notifySlotChanged(slot, observedItems[slot]) + observedItems[slot] = ItemStack.EMPTY + + return item + } + + final override fun setItem(slot: Int, itemStack: ItemStack) { + if (itemStack.count != items[slot].count || !ItemStack.isSameItemSameComponents(itemStack, items[slot])) { + items[slot] = itemStack + notifySlotChanged(slot, observedItems[slot]) + observedItems[slot] = itemStack.copy() + } + } + + override fun stillValid(player: Player): Boolean { + return true + } + + /** + * Called from inside [serializeNBT] per slot, return true if you have written something into [nbt]. + * If returning false, and slot does not contain an item, tag entry is discarded + */ + protected open fun attachSlotData(provider: Provider, slot: Int, nbt: CompoundTag, ops: RegistryOps): Boolean { + return false + } + + protected open fun loadSlotData(provider: Provider, slot: Int, nbt: CompoundTag, ops: RegistryOps) { + + } + + override fun serializeNBT(provider: Provider): CompoundTag { + val ops = provider.createSerializationContext(NbtOps.INSTANCE) + + return CompoundTag().also { + it["items"] = ListTag().also { + for (i in 0 until size) { + val tag = CompoundTag() + var attached = attachSlotData(provider, i, tag, ops) + + if (items[i].isNotEmpty) { + attached = true + tag["item"] = ItemStack.OPTIONAL_CODEC.encodeStart(ops, items[i]) + .getOrThrow { RuntimeException("Unable to serialize item ${items[i]} at slot $i: $it") } + } + + tag["slot"] = i + + if (attached) { + it.add(tag) + } + } + } + } + } + + override fun deserializeNBT(provider: Provider, nbt: CompoundTag) { + val copy = observedItems.copyOf() + items.fill(ItemStack.EMPTY) + observedItems.fill(ItemStack.EMPTY) + + val seenSlots = IntOpenHashSet() + val ops = provider.createSerializationContext(NbtOps.INSTANCE) + + if ("items" in nbt) { + for (element in nbt.getList("items", Tag.TAG_COMPOUND.toInt())) { + element as CompoundTag + + if ("slot" !in element) continue + val slot = element.getInt("slot") + if (!seenSlots.add(slot)) continue + + if ("item" in nbt) { + ItemStack.OPTIONAL_CODEC.decode(ops, nbt["item"]) + .map { it.first } + .ifError { LOGGER.error("Failed to deserialize item stack in slot $slot: ${it.message()}") } + .ifSuccess { + if (it.isNotEmpty) { + items[slot] = it + observedItems[slot] = it.copy() + + if (it.count != copy[slot].count || !ItemStack.isSameItemSameComponents(it, copy[slot])) + notifySlotChanged(slot, copy[slot]) + } + } + } + + loadSlotData(provider, slot, element, ops) + } + } + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/SlottedContainer.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/SlottedContainer.kt index f80c8b19e..ca04ebf0b 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/SlottedContainer.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/SlottedContainer.kt @@ -3,9 +3,6 @@ package ru.dbotthepony.mc.otm.container.slotted import com.mojang.serialization.Codec import com.mojang.serialization.codecs.RecordCodecBuilder import it.unimi.dsi.fastutil.ints.IntArrayList -import it.unimi.dsi.fastutil.ints.IntCollection -import it.unimi.dsi.fastutil.ints.IntList -import it.unimi.dsi.fastutil.ints.IntSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import net.minecraft.core.HolderLookup import net.minecraft.core.registries.BuiltInRegistries @@ -20,17 +17,25 @@ import net.minecraft.world.item.ItemStack import net.neoforged.neoforge.common.util.INBTSerializable import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.mc.otm.container.EnhancedContainer import ru.dbotthepony.mc.otm.container.IAutomatedContainer import ru.dbotthepony.mc.otm.container.IAutomatedContainerSlot import ru.dbotthepony.mc.otm.container.IFilteredContainerSlot import ru.dbotthepony.mc.otm.container.balance -import ru.dbotthepony.mc.otm.container.slotRange import ru.dbotthepony.mc.otm.core.isNotEmpty import ru.dbotthepony.mc.otm.core.nbt.set import ru.dbotthepony.mc.otm.data.codec.minRange import java.util.function.Predicate import kotlin.reflect.KClass +/** + * Concrete container implementation, operating on slots. + * + * Due to inflexible constraints, it can not be inherited, and usually it is not required since + * most logic is embedded inside [ContainerSlot]. + * + * If you specifically need to inherit container implementation, use [EnhancedContainer], which was designed to be inherited. + */ class SlottedContainer( slots: Collection>, private val stillValid: Predicate, @@ -381,10 +386,12 @@ class SlottedContainer( return MultiTag(T::class) } + @Deprecated("Consider using EnhancedContainer", replaceWith = ReplaceWith("EnhancedContainer(size)", "ru.dbotthepony.mc.otm.container.EnhancedContainer")) fun simple(size: Int): SlottedContainer { return Builder().add(size, ::ContainerSlot).build() } + @Deprecated("Consider using EnhancedContainer", replaceWith = ReplaceWith("EnhancedContainer.withListener(size, listener)", "ru.dbotthepony.mc.otm.container.EnhancedContainer")) fun simple(size: Int, listener: Runnable): SlottedContainer { return Builder() .add(size, ::ContainerSlot)