diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt index 4cb2dd53e..91983250f 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/Rebar.kt @@ -219,6 +219,8 @@ object Rebar : JavaPlugin(), RebarAddon { RebarTool.register(this, pm) RebarWeapon.register(this, pm) VanillaCookingFuel.register(this, pm) + RebarPickupable.register(this, pm) + RebarDroppable.register(this, pm) // Rebar Entities EntityListener.register(this, pm) diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/BlockStorage.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/BlockStorage.kt index 9be319784..24881ef07 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/BlockStorage.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/block/BlockStorage.kt @@ -66,6 +66,7 @@ import kotlin.random.Random object BlockStorage : Listener { val rebarBlocksKey = rebarKey("blocks") + val rebarBlocksType = RebarSerializers.LIST.listTypeFrom(RebarSerializers.TAG_CONTAINER) // Access to blocks, blocksByChunk, blocksById fields must be synchronized // to prevent them briefly going out of sync @@ -510,8 +511,7 @@ object BlockStorage : Listener { } private fun load(world: World, chunk: Chunk): List { - val type = RebarSerializers.LIST.listTypeFrom(RebarSerializers.TAG_CONTAINER) - val chunkBlocks = chunk.persistentDataContainer.get(rebarBlocksKey, type)?.mapNotNull { element -> + val chunkBlocks = chunk.persistentDataContainer.get(rebarBlocksKey, rebarBlocksType)?.mapNotNull { element -> RebarBlock.deserialize(world, element) }?.toMutableList() ?: mutableListOf() @@ -522,9 +522,7 @@ object BlockStorage : Listener { val serializedBlocks = chunkBlocks.mapNotNull { RebarBlock.serialize(it, chunk.persistentDataContainer.adapterContext) } - - val type = RebarSerializers.LIST.listTypeFrom(RebarSerializers.TAG_CONTAINER) - chunk.persistentDataContainer.set(rebarBlocksKey, type, serializedBlocks) + chunk.persistentDataContainer.set(rebarBlocksKey, rebarBlocksType, serializedBlocks) } @EventHandler diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidIntersectionDisplay.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidIntersectionDisplay.kt index c52eccb52..cb7e8682c 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidIntersectionDisplay.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidIntersectionDisplay.kt @@ -1,5 +1,6 @@ package io.github.pylonmc.rebar.content.fluid +import io.github.pylonmc.rebar.block.BlockStorage import io.github.pylonmc.rebar.datatypes.RebarSerializers import io.github.pylonmc.rebar.entity.EntityStorage import io.github.pylonmc.rebar.entity.RebarEntity @@ -11,7 +12,12 @@ import io.github.pylonmc.rebar.fluid.FluidManager import io.github.pylonmc.rebar.fluid.FluidPointType import io.github.pylonmc.rebar.fluid.VirtualFluidPoint import io.github.pylonmc.rebar.item.builder.ItemStackBuilder +import io.github.pylonmc.rebar.util.IMMEDIATE_FACES import io.github.pylonmc.rebar.util.rebarKey +import io.papermc.paper.datacomponent.DataComponentTypes +import io.papermc.paper.datacomponent.item.CustomModelData +import jdk.internal.org.jline.keymap.KeyMap.display +import org.bukkit.NamespacedKey import org.bukkit.block.Block import org.bukkit.entity.ItemDisplay import org.bukkit.event.EventPriority @@ -63,13 +69,42 @@ class FluidIntersectionDisplay : RebarEntity, RebarDeathEntity, Flu pdc.set(CONNECTED_PIPE_DISPLAYS_KEY, RebarSerializers.TAG_CONTAINER, connectedPdc) } + @Suppress("UnstableApiUsage") + fun updateItemDisplay() { + if (connectedPipeDisplays.isEmpty()) return + + val marker = BlockStorage.getAs(FluidIntersectionMarker::class.java, entity.location.block) ?: return + val modelData = CustomModelData.customModelData() + modelData.addString("fluid_point_display:${FluidPointType.INTERSECTION.name.lowercase()}") + modelData.addString("fluid_point_display:${marker.pipe.key}") + + val from = this.entity.location + for (face in IMMEDIATE_FACES) { + var hasFace = false + for (displayId in this.connectedPipeDisplays) { + val display = EntityStorage.getAs(FluidPipeDisplay::class.java, displayId) ?: continue + val towards = display.entity.location.subtract(from).toVector().normalize() + if (face.direction == towards) { + hasFace = true + break + } + } + modelData.addString("${face.name.lowercase()}=$hasFace") + } + this.entity.setItemStack(this.entity.itemStack.apply { + setData(DataComponentTypes.CUSTOM_MODEL_DATA, modelData) + }) + } + override fun connectPipeDisplay(uuid: UUID) { this.connectedPipeDisplays.add(uuid) + updateItemDisplay() } override fun disconnectPipeDisplay(uuid: UUID) { check(uuid in this.connectedPipeDisplays) { "$uuid is not connected" } this.connectedPipeDisplays.remove(uuid) + updateItemDisplay() } override fun onDeath(event: RebarEntityDeathEvent, priority: EventPriority) { diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidPipeDisplay.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidPipeDisplay.kt index bae3d841d..1e7e56c22 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidPipeDisplay.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidPipeDisplay.kt @@ -138,7 +138,7 @@ class FluidPipeDisplay : RebarEntity { .buildForItemDisplay() ) .itemStack(ItemStackBuilder.of(pipe.material) - .addCustomModelDataString("fluid_pipe_display:${pipe.key.key}") + .addCustomModelDataString("fluid_pipe_display:${pipe.key}") .addCustomModelDataString("fluid_pipe_length:${pipeAmount}") ) .build(centerLocation) diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/EntityStorage.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/EntityStorage.kt index cb6af620a..c725d4409 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/EntityStorage.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/entity/EntityStorage.kt @@ -14,6 +14,7 @@ import io.github.pylonmc.rebar.util.isFromAddon import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.bukkit.Bukkit import org.bukkit.NamespacedKey import org.bukkit.entity.Entity import org.bukkit.event.EventHandler @@ -37,6 +38,7 @@ object EntityStorage : Listener { private val entitiesByKey: MutableMap>> = ConcurrentHashMap() private val entityAutosaveTasks: MutableMap = ConcurrentHashMap() private val whenEntityLoadsTasks: MutableMap>>> = ConcurrentHashMap() + private val whenVanillaEntityLoadsTasks: MutableMap>> = ConcurrentHashMap() // Access to entities, entitiesById fields must be synchronized to prevent them // briefly going out of sync @@ -130,7 +132,6 @@ object EntityStorage : Listener { consumer.accept(it) } } - } /** @@ -161,6 +162,52 @@ object EntityStorage : Listener { inline fun whenEntityLoads(uuid: UUID, crossinline consumer: (T) -> Unit) = whenEntityLoads(uuid, T::class.java) { t -> consumer(t) } + /** + * Schedules a task to run when the vanilla entity with id [uuid] is loaded, or runs the task immediately + * if the entity is already loaded. + * + * Useful for when you don't know whether a block or one of its associated entity will be loaded first. + */ + @JvmStatic + fun whenVanillaEntityLoads(uuid: UUID, consumer: Consumer) { + val entity = Bukkit.getEntity(uuid) + if (entity != null) { + consumer.accept(entity) + } else { + whenVanillaEntityLoadsTasks.getOrPut(uuid) { mutableSetOf() }.add { + consumer.accept(it) + } + } + } + + /** + * Schedules a task to run when the vanilla entity with id [uuid] is loaded, or runs the task immediately + * if the entity is already loaded. + * + * Useful for when you don't know whether a block or one of its associated entity will be loaded first. + */ + @JvmStatic + fun whenVanillaEntityLoads(uuid: UUID, clazz: Class, consumer: Consumer) { + val entity = Bukkit.getEntity(uuid) + if (entity != null && clazz.isInstance(entity)) { + consumer.accept(clazz.cast(entity)) + } else { + whenVanillaEntityLoadsTasks.getOrPut(uuid) { mutableSetOf() }.add { + consumer.accept(if (clazz.isInstance(it)) clazz.cast(it) else throw IllegalStateException("Entity $uuid was not of expected type ${clazz.simpleName}")) + } + } + } + + /** + * Schedules a task to run when the vanilla entity with id [uuid] is loaded, or runs the task immediately + * if the entity is already loaded + * + * Useful for when you don't know whether a block or one of its associated entity will be loaded first. + */ + @JvmStatic + inline fun whenVanillaEntityLoads(uuid: UUID, crossinline consumer: (T) -> Unit) + = whenVanillaEntityLoads(uuid, T::class.java) { t -> consumer(t) } + /** * Returns false if the entity is not a [RebarEntity] or does not exist. */ @@ -203,10 +250,21 @@ object EntityStorage : Listener { @EventHandler private fun onEntityLoad(event: EntitiesLoadEvent) { for (entity in event.entities) { + val vanillaTasks = whenVanillaEntityLoadsTasks.remove(entity.uniqueId) + if (vanillaTasks != null) { + for (task in vanillaTasks) { + try { + task.accept(entity) + } catch (t: Throwable) { + t.printStackTrace() + } + } + } + val rebarEntity = RebarEntity.deserialize(entity) ?: continue add(rebarEntity) - val tasks = whenEntityLoadsTasks[rebarEntity.uuid] + val tasks = whenEntityLoadsTasks.remove(rebarEntity.uuid) if (tasks != null) { for (task in tasks) { try { @@ -215,7 +273,6 @@ object EntityStorage : Listener { t.printStackTrace() } } - whenEntityLoadsTasks.remove(rebarEntity.uuid) } RebarEntityLoadEvent(rebarEntity).callEvent() diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/BackButton.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/BackButton.kt index 4a553d721..9e0fb6535 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/BackButton.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/BackButton.kt @@ -2,6 +2,7 @@ package io.github.pylonmc.rebar.guide.button import io.github.pylonmc.rebar.content.guide.RebarGuide import io.github.pylonmc.rebar.item.builder.ItemStackBuilder +import io.github.pylonmc.rebar.item.research.Research.Companion.guideHints import io.github.pylonmc.rebar.util.rebarKey import io.papermc.paper.datacomponent.DataComponentTypes import net.kyori.adventure.text.Component @@ -20,7 +21,11 @@ class BackButton : AbstractItem() { override fun getItemProvider(player: Player) = ItemStackBuilder.gui(Material.ENCHANTED_BOOK, rebarKey("guide_back")) .set(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, false) - .name(Component.translatable("rebar.guide.button.back.name")) + .name(Component.translatable("rebar.guide.button.back.name")).apply { + if (player.guideHints) { + lore(Component.translatable("rebar.guide.button.back.hints")) + } + } override fun handleClick(clickType: ClickType, player: Player, click: Click) { val history = RebarGuide.history.getOrPut(player.uniqueId) { mutableListOf() } diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/PageButton.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/PageButton.kt index 529d91a14..912fe3550 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/PageButton.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/PageButton.kt @@ -28,7 +28,7 @@ open class PageButton(val stack: ItemStack, val page: GuidePage) : GuideButton() constructor(material: Material, page: GuidePage) : this(ItemStack(material), page) - override fun getItemProvider(viewer: Player): ItemProvider = ItemStackBuilder.gui(stack, "${rebarKey("guide_page")}:${page.key}") + override fun getItemProvider(viewer: Player): ItemProvider = ItemStackBuilder.gui(stack.clone(), "${rebarKey("guide_page")}:${page.key}") .name(Component.translatable("${page.key.namespace}.guide.page.${page.key.key}")) .clearLore() .lore(Component.translatable("${page.key.namespace}.guide.button.${page.key.key}.lore", "")) diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/base/SearchPage.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/base/SearchPage.kt index 6c3dbf964..bd8c5faed 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/base/SearchPage.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/pages/base/SearchPage.kt @@ -52,7 +52,7 @@ abstract class SearchPage(key: NamespacedKey) : SimpleStaticGuidePage(key) { val upperGui = Gui.builder() .setStructure("# S #") .addIngredient('S', searchSpecifiersStack) - .addIngredient('#', GuiItems.background(search)) + .addIngredient('#', GuiItems.background()) .build() loadCurrentPage(player, lowerGui) diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemListener.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemListener.kt index 9b06e563b..e228ad8b9 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemListener.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/RebarItemListener.kt @@ -3,13 +3,11 @@ package io.github.pylonmc.rebar.item import com.destroystokyo.paper.event.player.PlayerReadyArrowEvent import io.github.pylonmc.rebar.Rebar import io.github.pylonmc.rebar.block.BlockStorage -import io.github.pylonmc.rebar.entity.EntityListener.logEventHandleErr import io.github.pylonmc.rebar.item.base.* import io.github.pylonmc.rebar.item.research.Research.Companion.canUse import io.github.pylonmc.rebar.util.findRebarItemInInventory -import io.papermc.paper.event.player.PlayerPickItemEvent +import io.papermc.paper.event.player.PlayerPickBlockEvent import org.bukkit.GameMode -import org.bukkit.attribute.Attribute import org.bukkit.entity.Player import org.bukkit.event.Event import org.bukkit.event.EventHandler @@ -17,14 +15,9 @@ import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.event.block.BlockBreakEvent import org.bukkit.event.block.BlockDamageEvent -import org.bukkit.event.block.BlockDispenseEvent import org.bukkit.event.entity.EntityDamageByEntityEvent import org.bukkit.event.entity.EntityDeathEvent -import org.bukkit.event.entity.EntityShootBowEvent -import org.bukkit.event.inventory.BrewingStandFuelEvent -import org.bukkit.event.inventory.FurnaceBurnEvent import org.bukkit.event.player.* -import kotlin.math.ceil internal object RebarItemListener : Listener { @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) @@ -167,20 +160,18 @@ internal object RebarItemListener : Listener { } } - @EventHandler - private fun handle(event: PlayerPickItemEvent) { - val reachDistance = event.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE)?.value ?: 4.5 - val block = event.player.getTargetBlockExact(ceil(reachDistance).toInt()) ?: return - val rebarBlock = BlockStorage.get(block) ?: return + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + private fun handle(event: PlayerPickBlockEvent) { + val rebarBlock = BlockStorage.get(event.block) ?: return val blockItem = rebarBlock.getPickItem() ?: return - val blockRebarItem = RebarItem.fromStack(blockItem) ?: return + val blockSchema = RebarItemSchema.fromStack(blockItem) ?: return val sourceSlot = event.sourceSlot if (sourceSlot != -1) { val sourceItem = event.player.inventory.getItem(event.sourceSlot) if (sourceItem != null) { - val sourceRebarItem = RebarItem.fromStack(sourceItem) - if (sourceRebarItem != null) { + val sourceSchema = RebarItemSchema.fromStack(sourceItem) + if (sourceSchema == blockSchema) { // The source item is already of the correct Rebar type, so we shouldn't interfere with the event return } @@ -189,7 +180,7 @@ internal object RebarItemListener : Listener { // If we reach this point, the source item is not of the correct type // So we're going to search the inventory for a block of the correct type - val existingSlot = findRebarItemInInventory(event.player.inventory, blockRebarItem) + val existingSlot = findRebarItemInInventory(event.player.inventory, blockSchema) if (existingSlot != null) { // If we find one, we'll set the source to that slot event.sourceSlot = existingSlot @@ -209,7 +200,7 @@ internal object RebarItemListener : Listener { } } - val newSourceSlot = findRebarItemInInventory(event.player.inventory, blockRebarItem) + val newSourceSlot = findRebarItemInInventory(event.player.inventory, blockSchema) if (newSourceSlot == null) { // should never happen but you never know event.isCancelled = true @@ -218,14 +209,6 @@ internal object RebarItemListener : Listener { event.sourceSlot = newSourceSlot event.targetSlot = event.player.inventory.heldItemSlot - - // don't question this idk wtf is going on - seems we have to manually do the swap in the hotbar - if (sourceSlot <= 8) { - val source = event.player.inventory.getItem(event.sourceSlot) - val target = event.player.inventory.getItem(event.targetSlot) - event.player.inventory.setItem(event.sourceSlot, target) - event.player.inventory.setItem(event.targetSlot, source) - } } @JvmSynthetic diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarDroppable.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarDroppable.kt new file mode 100644 index 000000000..bb9d0bfd0 --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarDroppable.kt @@ -0,0 +1,29 @@ +package io.github.pylonmc.rebar.item.base + +import io.github.pylonmc.rebar.event.api.MultiListener +import io.github.pylonmc.rebar.event.api.annotation.MultiHandlers +import io.github.pylonmc.rebar.event.api.annotation.UniversalHandler +import io.github.pylonmc.rebar.item.RebarItem +import io.github.pylonmc.rebar.item.RebarItemListener +import org.bukkit.event.EventPriority +import org.bukkit.event.player.PlayerDropItemEvent +import org.jetbrains.annotations.ApiStatus + +interface RebarDroppable { + fun onDropped(event: PlayerDropItemEvent, priority: EventPriority) + + @ApiStatus.Internal + companion object : MultiListener { + @UniversalHandler + private fun onDrop(event: PlayerDropItemEvent, priority: EventPriority) { + val rebarItem = RebarItem.fromStack(event.itemDrop.itemStack, RebarDroppable::class.java) + val droppable = rebarItem as? RebarItem ?: return + + try { + MultiHandlers.handleEvent(droppable, "onDropped", event, priority) + } catch (e: Exception) { + RebarItemListener.logEventHandleErr(event, e, rebarItem) + } + } + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarPickupable.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarPickupable.kt new file mode 100644 index 000000000..7986de75e --- /dev/null +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/item/base/RebarPickupable.kt @@ -0,0 +1,29 @@ +package io.github.pylonmc.rebar.item.base + +import io.github.pylonmc.rebar.event.api.MultiListener +import io.github.pylonmc.rebar.event.api.annotation.MultiHandlers +import io.github.pylonmc.rebar.event.api.annotation.UniversalHandler +import io.github.pylonmc.rebar.item.RebarItem +import io.github.pylonmc.rebar.item.RebarItemListener +import org.bukkit.event.EventPriority +import org.bukkit.event.player.PlayerAttemptPickupItemEvent +import org.jetbrains.annotations.ApiStatus + +interface RebarPickupable { + fun onPickupAttempt(event: PlayerAttemptPickupItemEvent, priority: EventPriority) + + @ApiStatus.Internal + companion object : MultiListener { + @UniversalHandler + private fun onPickupAttempt(event: PlayerAttemptPickupItemEvent, priority: EventPriority) { + val rebarItem = RebarItem.fromStack(event.item.itemStack, RebarPickupable::class.java) + val pickupable = rebarItem as? RebarItem ?: return + + try { + MultiHandlers.handleEvent(pickupable, "onPickupAttempt", event, priority) + } catch (e: Exception) { + RebarItemListener.logEventHandleErr(event, e, rebarItem) + } + } + } +} \ No newline at end of file diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/metrics/RebarMetrics.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/metrics/RebarMetrics.kt index 0466225bd..e768f20b1 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/metrics/RebarMetrics.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/metrics/RebarMetrics.kt @@ -30,13 +30,15 @@ internal object RebarMetrics { metrics.addCustomChart(AdvancedPie("addons") { val values = mutableMapOf() for (addon in RebarRegistry.ADDONS) { - values.put(addon.javaClass.simpleName, 1) + if (addon is Rebar) continue + values[addon.javaClass.simpleName] = 1 } values }) metrics.addCustomChart(AdvancedPie("number_of_addons") { - mutableMapOf(RebarRegistry.ADDONS.count().toString() to 1) + // Subtract 1 to not include Rebar itself + mutableMapOf((RebarRegistry.ADDONS.count() - 1).toString() to 1) }) metrics.addCustomChart(AdvancedPie("disabled_items") { diff --git a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt index fda9cfff8..bcd4de511 100644 --- a/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt +++ b/rebar/src/main/kotlin/io/github/pylonmc/rebar/util/RebarUtils.kt @@ -314,7 +314,7 @@ fun fromMiniMessage(string: String): Component = customMiniMessage.deserialize(s * @param targetItem The item to find. Items will be compared by their Rebar ID * @return The slot containing the item, or null if no item was found */ -fun findRebarItemInInventory(inventory: Inventory, targetItem: RebarItem): Int? { +fun findRebarItemInInventory(inventory: Inventory, targetItem: RebarItemSchema): Int? { for (i in 0..Back" + hints: |- + + Hints + Shift left click to go to the main menu item-recipes: "Recipe" machine-recipes: "Recipes made with this machine" search-specifiers: