From 9b1c192f0903695b4f8b0efef9e40db31a1b3810 Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Mon, 31 Mar 2025 09:12:12 +0700
Subject: [PATCH] Provide "enhanced" (count unlimited) item stack codecs

---
 .../dbotthepony/mc/otm/block/entity/Jobs.kt   |  3 ++-
 .../matter/MatterEntanglerBlockEntity.kt      |  3 ++-
 .../matter/MatterReplicatorBlockEntity.kt     |  3 ++-
 .../mc/otm/container/EnhancedContainer.kt     |  5 ++--
 .../mc/otm/container/slotted/ContainerSlot.kt |  5 ++--
 .../otm/container/slotted/SlottedContainer.kt |  7 ++---
 .../otm/data/codec/EnhancedItemStackCodecs.kt | 26 +++++++++++++++++++
 .../condition/ItemInInventoryCondition.kt     |  3 ++-
 .../mc/otm/recipe/MatterEntanglerRecipe.kt    |  3 ++-
 9 files changed, 46 insertions(+), 12 deletions(-)
 create mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/data/codec/EnhancedItemStackCodecs.kt

diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/Jobs.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/Jobs.kt
index 434b79133..c9df0f764 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/Jobs.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/Jobs.kt
@@ -18,6 +18,7 @@ import ru.dbotthepony.mc.otm.util.math.weakGreaterThan
 import ru.dbotthepony.mc.otm.util.math.weakLessThan
 import ru.dbotthepony.mc.otm.util.set
 import ru.dbotthepony.mc.otm.data.codec.DecimalCodec
+import ru.dbotthepony.mc.otm.data.codec.EnhancedItemStackCodecs
 
 private fun isReason(status: Any?, reason: Any) = status == null || status == reason
 private val LOGGER = LogManager.getLogger()
@@ -67,7 +68,7 @@ open class ItemJob(
 ) : Job(ticks, power, experience) {
 	companion object {
 		fun <T : ItemJob> itemCodec(builder: RecordCodecBuilder.Instance<T>): Products.P4<RecordCodecBuilder.Mu<T>, ItemStack, Double, Decimal, Float> {
-			return builder.group(ItemStack.CODEC.fieldOf("Item").forGetter(ItemJob::itemStack)).and(basicCodec(builder))
+			return builder.group(EnhancedItemStackCodecs.CODEC.fieldOf("Item").forGetter(ItemJob::itemStack)).and(basicCodec(builder))
 		}
 
 		val CODEC: Codec<ItemJob> by lazy {
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/matter/MatterEntanglerBlockEntity.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/matter/MatterEntanglerBlockEntity.kt
index 4b9ba0cc0..79e9bb716 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/matter/MatterEntanglerBlockEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/matter/MatterEntanglerBlockEntity.kt
@@ -38,6 +38,7 @@ import ru.dbotthepony.mc.otm.util.math.Decimal
 import ru.dbotthepony.mc.otm.container.ItemStackKey
 import ru.dbotthepony.mc.otm.container.asKey
 import ru.dbotthepony.mc.otm.data.codec.DecimalCodec
+import ru.dbotthepony.mc.otm.data.codec.EnhancedItemStackCodecs
 import ru.dbotthepony.mc.otm.data.codec.minRange
 import ru.dbotthepony.mc.otm.graph.matter.MatterNode
 import ru.dbotthepony.mc.otm.menu.matter.MatterEntanglerMenu
@@ -53,7 +54,7 @@ class MatterEntanglerBlockEntity(blockPos: BlockPos, blockState: BlockState) : M
 		companion object {
 			val CODEC: Codec<Job> = RecordCodecBuilder.create {
 				it.group(
-					ItemStack.CODEC.fieldOf("itemStack").forGetter(ItemJob::itemStack),
+					EnhancedItemStackCodecs.CODEC.fieldOf("itemStack").forGetter(ItemJob::itemStack),
 					DecimalCodec.minRange(Decimal.ZERO).fieldOf("matter").forGetter(Job::matter),
 					Codec.DOUBLE.minRange(0.0).fieldOf("ticks").forGetter(ItemJob::ticks),
 					Codec.FLOAT.minRange(0f).optionalFieldOf("experience", 0f).forGetter(Job::experience)
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/matter/MatterReplicatorBlockEntity.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/matter/MatterReplicatorBlockEntity.kt
index 0d99da45b..0e362dd67 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/matter/MatterReplicatorBlockEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/matter/MatterReplicatorBlockEntity.kt
@@ -30,6 +30,7 @@ import ru.dbotthepony.mc.otm.container.slotted.SlottedContainer
 import ru.dbotthepony.mc.otm.util.math.Decimal
 import ru.dbotthepony.mc.otm.util.otmRandom
 import ru.dbotthepony.mc.otm.data.codec.DecimalCodec
+import ru.dbotthepony.mc.otm.data.codec.EnhancedItemStackCodecs
 import ru.dbotthepony.mc.otm.data.codec.minRange
 import ru.dbotthepony.mc.otm.graph.matter.MatterNode
 import ru.dbotthepony.mc.otm.matter.MatterManager
@@ -55,7 +56,7 @@ class MatterReplicatorBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) :
 			val CODEC: Codec<ReplicatorJob> by lazy {
 				RecordCodecBuilder.create {
 					it.group(
-						ItemStack.CODEC.fieldOf("Item").forGetter(ReplicatorJob::itemStack),
+						EnhancedItemStackCodecs.CODEC.fieldOf("Item").forGetter(ReplicatorJob::itemStack),
 						DecimalCodec.minRange(Decimal.ZERO).fieldOf("matterPerTick").forGetter(ReplicatorJob::matterPerTick),
 						UUIDUtil.CODEC.fieldOf("task").forGetter(ReplicatorJob::task),
 						DecimalCodec.minRange(Decimal.ZERO).fieldOf("matterValue").forGetter(ReplicatorJob::matterValue),
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/container/EnhancedContainer.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/container/EnhancedContainer.kt
index 9fafb973b..3301ad261 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/container/EnhancedContainer.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/container/EnhancedContainer.kt
@@ -13,6 +13,7 @@ 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.data.codec.EnhancedItemStackCodecs
 import ru.dbotthepony.mc.otm.util.isNotEmpty
 import ru.dbotthepony.mc.otm.util.set
 
@@ -133,7 +134,7 @@ abstract class EnhancedContainer<out S : IContainerSlot>(private val size: Int)
 
 					if (items[i].isNotEmpty) {
 						attached = true
-						tag["item"] = ItemStack.OPTIONAL_CODEC.encodeStart(ops, items[i])
+						tag["item"] = EnhancedItemStackCodecs.OPTIONAL_CODEC.encodeStart(ops, items[i])
 							.getOrThrow { RuntimeException("Unable to serialize item ${items[i]} at slot $i: $it") }
 					}
 
@@ -165,7 +166,7 @@ abstract class EnhancedContainer<out S : IContainerSlot>(private val size: Int)
 				if (!seenSlots.add(slot)) continue
 
 				if ("item" in element) {
-					ItemStack.OPTIONAL_CODEC.decode(ops, element["item"])
+					EnhancedItemStackCodecs.OPTIONAL_CODEC.decode(ops, element["item"])
 						.map { it.first }
 						.ifError { LOGGER.error("Failed to deserialize item stack in slot $slot: ${it.message()}") }
 						.ifSuccess {
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/ContainerSlot.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/ContainerSlot.kt
index ec1139e49..6f629403c 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/ContainerSlot.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/ContainerSlot.kt
@@ -8,6 +8,7 @@ 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.IAutomatedContainerSlot
+import ru.dbotthepony.mc.otm.data.codec.EnhancedItemStackCodecs
 import ru.dbotthepony.mc.otm.util.isNotEmpty
 import ru.dbotthepony.mc.otm.util.set
 import ru.dbotthepony.mc.otm.data.getOrNull
@@ -97,13 +98,13 @@ open class ContainerSlot(
 
 	override fun serializeNBT(provider: HolderLookup.Provider): CompoundTag {
 		return CompoundTag().also {
-			it["item"] = ItemStack.OPTIONAL_CODEC.encodeStart(provider.createSerializationContext(NbtOps.INSTANCE), item)
+			it["item"] = EnhancedItemStackCodecs.OPTIONAL_CODEC.encodeStart(provider.createSerializationContext(NbtOps.INSTANCE), item)
 				.getOrThrow { RuntimeException("Unable to serialize $item in slot $slot: $it") }
 		}
 	}
 
 	override fun deserializeNBT(provider: HolderLookup.Provider, nbt: CompoundTag) {
-		_item = ItemStack.OPTIONAL_CODEC.decode(provider.createSerializationContext(NbtOps.INSTANCE), nbt["item"])
+		_item = EnhancedItemStackCodecs.OPTIONAL_CODEC.decode(provider.createSerializationContext(NbtOps.INSTANCE), nbt["item"])
 			.ifError { LOGGER.error("Unable to deserialize item at slot $slot: ${it.message()}") }
 			.getOrNull()?.first ?: ItemStack.EMPTY
 
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 e1561b3b9..bad20a22b 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
@@ -23,6 +23,7 @@ import ru.dbotthepony.mc.otm.container.IAutomatedContainer
 import ru.dbotthepony.mc.otm.container.IFilteredContainerSlot
 import ru.dbotthepony.mc.otm.container.ItemFilter
 import ru.dbotthepony.mc.otm.container.balance
+import ru.dbotthepony.mc.otm.data.codec.EnhancedItemStackCodecs
 import ru.dbotthepony.mc.otm.util.isNotEmpty
 import ru.dbotthepony.mc.otm.util.set
 import ru.dbotthepony.mc.otm.data.codec.minRange
@@ -183,7 +184,7 @@ class SlottedContainer(
 		companion object {
 			val CODEC: Codec<LegacySerializedItem> = RecordCodecBuilder.create {
 				it.group(
-					ItemStack.OPTIONAL_CODEC.fieldOf("item").forGetter { it.item },
+					EnhancedItemStackCodecs.OPTIONAL_CODEC.fieldOf("item").forGetter { it.item },
 					Codec.INT.minRange(0).fieldOf("slot").forGetter { it.slot },
 				).apply(it, SlottedContainer::LegacySerializedItem)
 			}
@@ -227,7 +228,7 @@ class SlottedContainer(
 
 		for (entry in lostItems) {
 			if ("item" in entry) {
-				ItemStack.OPTIONAL_CODEC.decode(provider, entry["item"])
+				EnhancedItemStackCodecs.OPTIONAL_CODEC.decode(provider, entry["item"])
 					.ifError { LOGGER.warn("Unable to deserialize 'lost' item: ${it.message()}") }
 					.ifSuccess { if (it.first.isNotEmpty) result.add(it.first) }
 			}
@@ -275,7 +276,7 @@ class SlottedContainer(
 							slots[slot].item = item
 							bitmap[slot] = item.isNotEmpty
 						} else if (item.isNotEmpty) {
-							ItemStack.CODEC.encodeStart(provider.createSerializationContext(NbtOps.INSTANCE), item)
+							EnhancedItemStackCodecs.CODEC.encodeStart(provider.createSerializationContext(NbtOps.INSTANCE), item)
 								.ifError { LOGGER.warn("Unable to serialize 'lost' item: ${it.message()}") }
 								.ifSuccess { s ->
 									this.provider = provider
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/data/codec/EnhancedItemStackCodecs.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/data/codec/EnhancedItemStackCodecs.kt
new file mode 100644
index 000000000..a8bc96564
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/data/codec/EnhancedItemStackCodecs.kt
@@ -0,0 +1,26 @@
+package ru.dbotthepony.mc.otm.data.codec
+
+import com.mojang.serialization.Codec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import net.minecraft.core.component.DataComponentPatch
+import net.minecraft.util.ExtraCodecs
+import net.minecraft.world.item.ItemStack
+import java.util.*
+import kotlin.jvm.optionals.getOrElse
+
+object EnhancedItemStackCodecs {
+	val CODEC: Codec<ItemStack> by lazy {
+		RecordCodecBuilder.create {
+			it.group(
+				ItemStack.ITEM_NON_AIR_CODEC.fieldOf("id").forGetter(ItemStack::getItemHolder),
+				Codec.INT.minRange(1).fieldOf("count").orElse(1).forGetter(ItemStack::getCount),
+				DataComponentPatch.CODEC.optionalFieldOf("components", DataComponentPatch.EMPTY).forGetter(ItemStack::getComponentsPatch)
+			).apply(it, ::ItemStack)
+		}
+	}
+
+	val OPTIONAL_CODEC: Codec<ItemStack> by lazy {
+		ExtraCodecs.optionalEmptyMap(CODEC)
+			.xmap({ it.getOrElse { ItemStack.EMPTY } }, { if (it.isEmpty) Optional.empty() else Optional.of(it) })
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/data/condition/ItemInInventoryCondition.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/data/condition/ItemInInventoryCondition.kt
index 5f9c68dde..a18ce2413 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/data/condition/ItemInInventoryCondition.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/data/condition/ItemInInventoryCondition.kt
@@ -10,6 +10,7 @@ import net.minecraft.world.level.storage.loot.predicates.LootItemCondition
 import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType
 import ru.dbotthepony.mc.otm.capability.items
 import ru.dbotthepony.kommons.collect.filter
+import ru.dbotthepony.mc.otm.data.codec.EnhancedItemStackCodecs
 import ru.dbotthepony.mc.otm.data.get
 import ru.dbotthepony.mc.otm.registry.data.MLootItemConditions
 
@@ -48,7 +49,7 @@ data class ItemInInventoryCondition(
 		val CODEC: MapCodec<ItemInInventoryCondition> by lazy {
 			RecordCodecBuilder.mapCodec {
 				it.group(
-					ItemStack.CODEC.fieldOf("item").forGetter(ItemInInventoryCondition::item),
+					EnhancedItemStackCodecs.CODEC.fieldOf("item").forGetter(ItemInInventoryCondition::item),
 					Codec.BOOL.optionalFieldOf("matchComponents", false).forGetter(ItemInInventoryCondition::matchComponents),
 					Codec.BOOL.optionalFieldOf("matchCosmetics", false).forGetter(ItemInInventoryCondition::matchCosmetics),
 				).apply(it, ::ItemInInventoryCondition)
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/MatterEntanglerRecipe.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/MatterEntanglerRecipe.kt
index 0e3cd3295..4001d6454 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/MatterEntanglerRecipe.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/MatterEntanglerRecipe.kt
@@ -22,6 +22,7 @@ import ru.dbotthepony.kommons.collect.filterNotNull
 import ru.dbotthepony.kommons.collect.map
 import ru.dbotthepony.mc.otm.util.math.Decimal
 import ru.dbotthepony.mc.otm.data.codec.DecimalCodec
+import ru.dbotthepony.mc.otm.data.codec.EnhancedItemStackCodecs
 import ru.dbotthepony.mc.otm.data.codec.minRange
 import ru.dbotthepony.mc.otm.network.StreamCodecs
 import ru.dbotthepony.mc.otm.network.optional
@@ -168,7 +169,7 @@ open class MatterEntanglerRecipe(
 				ShapedRecipePattern.MAP_CODEC.fieldOf("ingredients").forGetter(MatterEntanglerRecipe::ingredients),
 				DecimalCodec.minRange(Decimal.ZERO).fieldOf("matter").forGetter(MatterEntanglerRecipe::matter),
 				Codec.DOUBLE.minRange(0.0).fieldOf("ticks").forGetter(MatterEntanglerRecipe::ticks),
-				ItemStack.CODEC.fieldOf("result").forGetter(MatterEntanglerRecipe::result),
+				EnhancedItemStackCodecs.CODEC.fieldOf("result").forGetter(MatterEntanglerRecipe::result),
 				Codec.FLOAT.minRange(0f).optionalFieldOf("experience", 0f).forGetter(MatterEntanglerRecipe::experience),
 				(DataComponentType.CODEC as Codec<DataComponentType<UUID>>).optionalFieldOf("uuidKey").forGetter(MatterEntanglerRecipe::uuidKey),
 				UUIDUtil.STRING_CODEC.optionalFieldOf("fixedUuid").forGetter(MatterEntanglerRecipe::fixedUuid)