From 2cd11be465fb97f63e639b2e894a1add640b886a Mon Sep 17 00:00:00 2001
From: Constructor <fractalminds@protonmail.com>
Date: Tue, 13 Dec 2022 01:45:00 +0100
Subject: [PATCH] feature: Persistent container cache and renderer

---
 .../managers/CachedContainerManager.kt        | 194 ++++++++++++++++++
 .../module/modules/render/ContainerPreview.kt |  73 ++++++-
 2 files changed, 261 insertions(+), 6 deletions(-)
 create mode 100644 src/main/kotlin/com/lambda/client/manager/managers/CachedContainerManager.kt

diff --git a/src/main/kotlin/com/lambda/client/manager/managers/CachedContainerManager.kt b/src/main/kotlin/com/lambda/client/manager/managers/CachedContainerManager.kt
new file mode 100644
index 000000000..7e9fe9839
--- /dev/null
+++ b/src/main/kotlin/com/lambda/client/manager/managers/CachedContainerManager.kt
@@ -0,0 +1,194 @@
+package com.lambda.client.manager.managers
+
+import com.lambda.client.LambdaMod
+import com.lambda.client.event.listener.listener
+import com.lambda.client.manager.Manager
+import com.lambda.client.module.modules.player.PacketLogger
+import com.lambda.client.module.modules.render.ContainerPreview.cacheContainers
+import com.lambda.client.util.FolderUtils
+import com.lambda.client.util.math.Direction
+import com.lambda.client.util.threads.defaultScope
+import com.lambda.client.util.threads.safeListener
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import net.minecraft.inventory.ItemStackHelper
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.CompressedStreamTools
+import net.minecraft.nbt.NBTTagByte
+import net.minecraft.nbt.NBTTagCompound
+import net.minecraft.nbt.NBTTagInt
+import net.minecraft.nbt.NBTTagList
+import net.minecraft.tileentity.TileEntityChest
+import net.minecraft.tileentity.TileEntityDispenser
+import net.minecraft.tileentity.TileEntityHopper
+import net.minecraft.tileentity.TileEntityLockableLoot
+import net.minecraft.tileentity.TileEntityShulkerBox
+import net.minecraft.util.EnumFacing
+import net.minecraft.util.NonNullList
+import net.minecraft.util.math.BlockPos
+import net.minecraftforge.event.entity.player.PlayerContainerEvent
+import net.minecraftforge.event.entity.player.PlayerInteractEvent
+import net.minecraftforge.event.world.WorldEvent
+import java.io.*
+import java.nio.file.Paths
+import kotlin.collections.HashMap
+
+object CachedContainerManager : Manager {
+    private val directory = Paths.get(FolderUtils.lambdaFolder, "cached-containers").toFile()
+    private val containerWorlds = HashMap<File, NBTTagCompound>()
+    private var currentFile: File? = null
+    private var currentTileEntityLockableLoot: TileEntityLockableLoot? = null
+
+    init {
+        listener<WorldEvent.Load> {
+            if (!cacheContainers) return@listener
+
+            val serverDirectory = mc.currentServerData?.serverIP?.replace(":", "_") ?: return@listener
+            val folder = File(directory, serverDirectory)
+            currentFile = File(folder, "${it.world.provider.dimension}.nbt")
+
+            currentFile?.let { file ->
+                if (containerWorlds[file] != null) return@listener
+
+                defaultScope.launch(Dispatchers.IO) {
+                    try {
+                        CompressedStreamTools.read(file)?.let { compound ->
+                            containerWorlds[file] = compound
+                            LambdaMod.LOG.info("Container DB loaded from $file")
+                        } ?: run {
+                            if (!folder.exists()) folder.mkdirs()
+                            val containerDB = NBTTagCompound().apply {
+                                setTag("Containers", NBTTagList())
+                            }
+                            containerWorlds[file] = containerDB
+                            CompressedStreamTools.write(containerDB, file)
+                            LambdaMod.LOG.info("New container DB created in $file")
+                        }
+                    } catch (e: IOException) {
+                        LambdaMod.LOG.error("Failed to load container DB from $file", e)
+                    }
+                }
+            }
+        }
+
+        safeListener<PlayerInteractEvent.RightClickBlock> {
+            if (!cacheContainers) return@safeListener
+
+            currentTileEntityLockableLoot = (world.getTileEntity(it.pos) as? TileEntityLockableLoot) ?: return@safeListener
+        }
+
+        safeListener<PlayerContainerEvent.Close> { event ->
+            if (!cacheContainers) return@safeListener
+
+            val tileEntityLockableLoot = currentTileEntityLockableLoot ?: return@safeListener
+            currentTileEntityLockableLoot = null
+
+            val tileEntityTag = tileEntityLockableLoot.serializeNBT()
+
+            val matrix = getContainerMatrix(tileEntityLockableLoot)
+
+            if (tileEntityLockableLoot is TileEntityChest && event.container.inventory.size == 90) {
+                var otherChest: TileEntityChest? = null
+                var facing: EnumFacing? = null
+
+                tileEntityLockableLoot.adjacentChestXNeg?.let {
+                    otherChest = it
+                    facing = EnumFacing.WEST
+                }
+
+                tileEntityLockableLoot.adjacentChestXPos?.let {
+                    otherChest = it
+                    facing = EnumFacing.EAST
+                }
+
+                tileEntityLockableLoot.adjacentChestZNeg?.let {
+                    otherChest = it
+                    facing = EnumFacing.NORTH
+                }
+
+                tileEntityLockableLoot.adjacentChestZPos?.let {
+                    otherChest = it
+                    facing = EnumFacing.SOUTH
+                }
+
+                otherChest?.let { other ->
+                    facing?.let { face ->
+                        val slotCount = matrix.first * matrix.second * 2
+                        val inventory = event.container.inventory.take(slotCount)
+
+                        safeInventoryToDB(inventory, tileEntityTag, tileEntityLockableLoot.pos, face)
+                        safeInventoryToDB(inventory, other.serializeNBT(), other.pos, face.opposite)
+                    }
+                }
+
+            } else {
+                val slotCount = matrix.first * matrix.second
+                val inventory = event.container.inventory.take(slotCount)
+
+                safeInventoryToDB(inventory, tileEntityTag, tileEntityLockableLoot.pos, null)
+            }
+        }
+    }
+
+    private fun safeInventoryToDB(inventory: List<ItemStack>, tileEntityTag: NBTTagCompound, pos: BlockPos, facing: EnumFacing?) {
+        val file = currentFile ?: return
+        val containerDB = containerWorlds[file] ?: return
+        val containerList = containerDB.getContainerList() ?: return
+
+        val nonNullList = NonNullList.withSize(inventory.size, ItemStack.EMPTY)
+
+        inventory.forEachIndexed { index, itemStack ->
+            nonNullList[index] = itemStack
+        }
+
+        ItemStackHelper.saveAllItems(tileEntityTag, nonNullList)
+
+        facing?.let {
+            tileEntityTag.setTag("adjacentChest", NBTTagByte(it.index.toByte()))
+        }
+
+        findContainer(pos)?.let { containerTag ->
+            containerList.removeAll { containerTag == it }
+            containerList.appendTag(tileEntityTag)
+        } ?: run {
+            containerList.appendTag(tileEntityTag)
+        }
+
+        defaultScope.launch(Dispatchers.IO) {
+            try {
+                CompressedStreamTools.write(containerDB, file)
+            } catch (e: IOException) {
+                LambdaMod.LOG.warn("${PacketLogger.chatName} Failed saving containers!", e)
+            }
+        }
+    }
+
+    fun findContainer(pos: BlockPos) = containerWorlds[currentFile]
+        ?.getContainerList()
+        ?.filterIsInstance<NBTTagCompound>()
+        ?.firstOrNull {
+            pos.x == it.getInteger("x")
+                && pos.y == it.getInteger("y")
+                && pos.z == it.getInteger("z")
+        }
+
+    fun getInventoryOfContainer(tag: NBTTagCompound): NonNullList<ItemStack>? {
+        val inventory = NonNullList.withSize(54, ItemStack.EMPTY)
+        ItemStackHelper.loadAllItems(tag, inventory)
+        return inventory
+    }
+
+    fun getAllContainers() = containerWorlds[currentFile]?.getContainerList()?.filterIsInstance<NBTTagCompound>()
+
+    fun getContainerMatrix(type: TileEntityLockableLoot): Pair<Int, Int> {
+        return when (type) {
+            is TileEntityChest -> Pair(9, 3)
+            is TileEntityDispenser -> Pair(3, 3)
+            is TileEntityHopper -> Pair(5, 1)
+            is TileEntityShulkerBox -> Pair(9, 3)
+            else -> Pair(0, 0) // Should never happen
+        }
+    }
+
+    private fun NBTTagCompound.getContainerList() = getTag("Containers") as? NBTTagList
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/client/module/modules/render/ContainerPreview.kt b/src/main/kotlin/com/lambda/client/module/modules/render/ContainerPreview.kt
index 53ac4006d..f470feb0d 100644
--- a/src/main/kotlin/com/lambda/client/module/modules/render/ContainerPreview.kt
+++ b/src/main/kotlin/com/lambda/client/module/modules/render/ContainerPreview.kt
@@ -1,21 +1,29 @@
 package com.lambda.client.module.modules.render
 
 import com.lambda.client.commons.extension.ceilToInt
+import com.lambda.client.commons.extension.floorToInt
+import com.lambda.client.event.events.RenderOverlayEvent
+import com.lambda.client.manager.managers.CachedContainerManager
 import com.lambda.client.module.Category
 import com.lambda.client.module.Module
 import com.lambda.client.util.color.ColorHolder
-import com.lambda.client.util.graphics.GlStateUtils
-import com.lambda.client.util.graphics.RenderUtils2D
-import com.lambda.client.util.graphics.VertexHelper
+import com.lambda.client.util.graphics.*
 import com.lambda.client.util.graphics.font.FontRenderAdapter
 import com.lambda.client.util.items.block
 import com.lambda.client.util.math.Vec2d
+import com.lambda.client.util.math.VectorUtils.toVec3dCenter
+import com.lambda.client.util.threads.safeListener
+import com.lambda.client.util.world.getHitVec
 import net.minecraft.client.renderer.GlStateManager
 import net.minecraft.init.Blocks
 import net.minecraft.inventory.IInventory
 import net.minecraft.item.ItemShulkerBox
 import net.minecraft.item.ItemStack
-import net.minecraft.nbt.NBTTagCompound
+import net.minecraft.nbt.*
+import net.minecraft.tileentity.TileEntity
+import net.minecraft.tileentity.TileEntityLockableLoot
+import net.minecraft.util.EnumFacing
+import net.minecraft.util.math.BlockPos
 import org.lwjgl.opengl.GL11.GL_LINE_LOOP
 import org.lwjgl.opengl.GL11.glLineWidth
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo
@@ -25,6 +33,8 @@ object ContainerPreview : Module(
     description = "Previews shulkers and ender chests in the game GUI",
     category = Category.RENDER
 ) {
+    val cacheContainers by setting("Cache Containers", true)
+    private val renderCachedContainers by setting("Render Cached Containers", true, { cacheContainers })
     private val useCustomFont by setting("Use Custom Font", false)
     private val backgroundColor by setting("Background Color", ColorHolder(16, 0, 16, 255))
     private val borderTopColor by setting("Top Border Color", ColorHolder(144, 101, 237, 54))
@@ -32,6 +42,57 @@ object ContainerPreview : Module(
 
     var enderChest: IInventory? = null
 
+    init {
+        safeListener<RenderOverlayEvent> {
+            if (!renderCachedContainers) return@safeListener
+
+            var indexH = 0
+
+            // Preprocessing needs to be done in the manager to reduce strain on the render thread
+            CachedContainerManager.getAllContainers()?.forEach { tag ->
+                CachedContainerManager.getInventoryOfContainer(tag)?.let { container ->
+                    val thisPos = BlockPos(tag.getInteger("x"), tag.getInteger("y"), tag.getInteger("z"))
+                    val type = (TileEntity.create(world, tag) as? TileEntityLockableLoot) ?: return@safeListener
+                    var matrix = CachedContainerManager.getContainerMatrix(type)
+
+                    var renderPos = thisPos.toVec3dCenter()
+
+                    (tag.getTag("adjacentChest") as? NBTTagByte)?.byte?.toInt()?.let { index ->
+                        renderPos = getHitVec(thisPos, EnumFacing.byIndex(index))
+                        matrix = Pair(9, 6)
+                    }
+
+                    val screenPos = ProjectionUtils.toScaledScreenPos(renderPos)
+
+                    val width = matrix.first * 16
+                    val height = matrix.second * 16
+
+                    val vertexHelper = VertexHelper(GlStateUtils.useVbo())
+
+                    val color = backgroundColor.clone().apply { a = 50 }
+
+                    val newX = screenPos.x - width / 2
+                    val newY = screenPos.y - height / 2
+
+                    RenderUtils2D.drawRoundedRectFilled(
+                        vertexHelper,
+                        Vec2d(newX, newY),
+                        Vec2d(newX + width, newY + height),
+                        1.0,
+                        color = color
+                    )
+
+                    container.forEachIndexed { index, itemStack ->
+                        val x = newX + (index % matrix.first) * 16
+                        val y = newY + (index / matrix.first) * 16
+                        RenderUtils2D.drawItem(itemStack, x.floorToInt(), y.floorToInt())
+                    }
+                }
+                indexH += 60
+            }
+        }
+    }
+
     fun renderTooltips(itemStack: ItemStack, x: Int, y: Int, ci: CallbackInfo) {
         val item = itemStack.item
 
@@ -110,12 +171,12 @@ object ContainerPreview : Module(
             color = backgroundColor
         )
 
-        drawRectOutline(vertexHelper, x, y, width, height)
+        drawRectOutline(vertexHelper, x, y, width, height.floorToInt())
 
         FontRenderAdapter.drawString(stack.displayName, x.toFloat(), y.toFloat() - 2.0f, customFont = useCustomFont)
     }
 
-    private fun drawRectOutline(vertexHelper: VertexHelper, x: Double, y: Double, width: Int, height: Float) {
+    private fun drawRectOutline(vertexHelper: VertexHelper, x: Double, y: Double, width: Int, height: Int) {
         RenderUtils2D.prepareGl()
         glLineWidth(5.0f)