From 91b8576385133b528b60bbd468600256c8c1d3d8 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Tue, 1 Apr 2025 18:49:22 +0200 Subject: [PATCH] Completely and totally rework utils --- CykaQuick/Lua/Cyka/utils.lua | 668 ++++++++++++++++++++++------------- 1 file changed, 428 insertions(+), 240 deletions(-) diff --git a/CykaQuick/Lua/Cyka/utils.lua b/CykaQuick/Lua/Cyka/utils.lua index c1392d0..d8e3176 100644 --- a/CykaQuick/Lua/Cyka/utils.lua +++ b/CykaQuick/Lua/Cyka/utils.lua @@ -1,15 +1,172 @@ --- luacheck: globals Character MyModGlobal +-- luacheck: globals Character MyModGlobal Timer _ -- luacheck: max line length 420 ----@class ItemRefs ----@field item Barotrauma.Item ----@field inventory Barotrauma.ItemInventory ----@field slot Barotrauma.ItemInventory.Slot +---@class Barotrauma.Inventory.ItemSlot +---@field items Barotrauma.Item[] +-- local globalInventorySlotCache = {} ---@class InventorySlot ----@field slot Barotrauma.ItemSlot ----@field inventory Barotrauma.ItemInventory ----@field slotIndex number +---@field slot Barotrauma.Inventory.ItemSlot +---@field inventory Barotrauma.Inventory +---@field slotIndex1 number Lua based item slots +---@field slotIndex0 number Barotrauma API based item slots +---@field item Barotrauma.Item +---@field stackSize number +---@field maxStackSize number +-- ---@field lastUpdated number +MyModGlobal.InventorySlot = { + ---@param inventory Barotrauma.Inventory + ---@param slotIndex1 number + ---@return InventorySlot + new = function(inventory, slotIndex1) + local self = setmetatable({}, { + __index = MyModGlobal.InventorySlot + }) + self.inventory = inventory + self.slotIndex1 = slotIndex1 + self.slotIndex0 = slotIndex1 - 1 + -- self:update() + + if inventory and inventory.slots and #inventory.slots > 0 then + self.slot = inventory.slots[slotIndex1] + end + if self.slot and self.slot.items and #self.slot.items > 0 then + self.item = self.slot.items[1] + self.stackSize = #self.slot.items + -- At this point inventory has to exist + -- If it didn't slot wouldn't either and then this wouldn't either + self.maxStackSize = self.item.Prefab.GetMaxStackSize(inventory) + end + + return self + end, + update = function(self) + -- self.lastUpdated = Timer.GetTime() + if not self.inventory then + MyModGlobal.debugPrint("Error updating inventory slot, inventory not found") + return + end + if not self.inventory.slots then + MyModGlobal.debugPrint("Error updating inventory slot, inventory has no slots") + return + end + + local slot = self.inventory.slots[self.slotIndex1] + if not slot then + MyModGlobal.debugPrint("Error updating inventory slot, slot not found") + return + end + self.slot = slot + if not slot.items or #slot.items == 0 then + -- MyModGlobal.debugPrint("Error updating inventory slot, slot is empty") + return + end + self.item = slot.items[1] + self.stackSize = #slot.items + self.maxStackSize = self.item.Prefab.GetMaxStackSize(self.inventory) + end, + __tostring = function(self) + return string.format( + "InventorySlot(inventory=%s, slotIndex1=%d, slotIndex0=%d, item=%s, stackSize=%d, maxStackSize=%d)", + tostring(self.inventory), self.slotIndex1, self.slotIndex0, tostring(self.item), self.stackSize, + self.maxStackSize) + end, + ---@param predicate? fun(slot: InventorySlot): boolean + getNearbySlots = function(self, predicate) + predicate = predicate or function() return true end + + local slotsPerRow = 900 + local ok, err = pcall(function() + slotsPerRow = self.inventory.slotsPerRow + end) + if not ok then + MyModGlobal.debugPrint(string.format("Error getting slots per row: %s", err)) + end + + local getGridPos = function(slotIndex) + local x = slotIndex % slotsPerRow + local y = math.floor(slotIndex / slotsPerRow) + return x, y + end + + local slots = {} + for slotIndex, _ in ipairs(self.inventory.slots) do + local inventorySlot = MyModGlobal.InventorySlot.new(self.inventory, slotIndex) + if predicate(inventorySlot) then + slots[#slots + 1] = inventorySlot + end + end + + local slotx, sloty = getGridPos(self.slotIndex0) + table.sort(slots, function(a, b) + local ax, ay = getGridPos(a.slotIndex0) + local bx, by = getGridPos(b.slotIndex0) + + -- Chebyshev distance + local distA = math.max(math.abs(ax - slotx), math.abs(ay - sloty)) + local distB = math.max(math.abs(bx - slotx), math.abs(by - sloty)) + + if distA == distB then + return a.slotIndex0 < b.slotIndex0 + end + return distA < distB + end) + + return slots + end, + -- hash = function(self) + -- return string.format("%s:%d:%d", tostring(self.inventory), self.slotIndex1, self.slotIndex0) + -- end +} + +---@class ItemMoveRequest +---@field A InventorySlot +---@field B InventorySlot +---@field allowSwap boolean +---@field allowCombine boolean + +local enqueueMove +do + -- A bit of cheeky scoping + local enabled = true + ---@type ItemMoveRequest[] + local itemMoveQueue = {} + local rate = 10 + local function processQueue() + MyModGlobal.debugPrint("Processing queue") + Timer.Wait(processQueue, rate) + if not enabled then return end + if #itemMoveQueue == 0 then return end + ---@type ItemMoveRequest + local moveRequest = table.remove(itemMoveQueue, 1) + + -- TODO: Maybe try and figure out if we CAN put A into B + moveRequest.allowCombine = moveRequest.allowCombine or false + moveRequest.allowSwap = moveRequest.allowSwap or false + local success = moveRequest.B.inventory.TryPutItem(moveRequest.A.item, moveRequest.B.slotIndex0, + moveRequest.allowSwap, moveRequest.allowCombine, Character.Controlled, true) + if not success then + MyModGlobal.debugPrint(string.format("Failed moving item from %s to %s", tostring(moveRequest.A), + tostring(moveRequest.B))) + end + end + processQueue() + + ---@param A InventorySlot + ---@param B InventorySlot + ---@param allowSwap boolean + ---@param allowCombine boolean + enqueueMove = function(A, B, allowSwap, allowCombine) + MyModGlobal.debugPrint(string.format("Enqueuing move from %s to %s", tostring(A), tostring(B))) + table.insert(itemMoveQueue, { + A = A, + B = B, + allowSwap = allowSwap, + allowCombine = allowCombine, + }) + end +end + ---@return Barotrauma.Item[], string? local function getOpenContainers() @@ -23,7 +180,9 @@ end ---@return Barotrauma.Item, string? local function getFirstOpenContainer() local containers, err = getOpenContainers() + ---@diagnostic disable-next-line: return-type-mismatch if err then return nil, err end + ---@diagnostic disable-next-line: return-type-mismatch if #containers == 0 then return nil, "No open containers" end return containers[1], nil end @@ -32,227 +191,261 @@ end -- And enqueueItem calls enqueueInventory -- So unless we define them both before using them -- We will get an error saying either is undefined +-- TODO: Rework these enqueue functions to accept a params object +-- That will house all optional parameters +-- And in that include recurse boolean + +---@class ItemRefs +---@field item Barotrauma.Item +---@field inventory Barotrauma.Inventory +---@field slot Barotrauma.Inventory.ItemSlot +---@field slotIndex1 number + +---@class EnqueueOptions +---@field itemQueue Barotrauma.Item[] +---@field slotQueue Barotrauma.Inventory.ItemSlot[] +---@field inventoryQueue Barotrauma.Inventory[] +---@field itemPredicate fun(item: Barotrauma.Item, itemRef: ItemRefs): boolean +---@field slotPredicate fun(slot: Barotrauma.Inventory.ItemSlot, itemRef: ItemRefs): boolean +---@field inventoryPredicate fun(inventory: Barotrauma.Inventory, itemRef: ItemRefs): boolean +---@field loadRefs boolean +---@field itemRef ItemRefs +---@field recurse boolean + +---@param options EnqueueOptions +---@return EnqueueOptions +local function ensureOptionsDefaults(options) + options = options or {} + options.itemQueue = options.itemQueue or {} + options.slotQueue = options.slotQueue or {} + options.inventoryQueue = options.inventoryQueue or {} + options.itemPredicate = options.itemPredicate or function() return true end + options.slotPredicate = options.slotPredicate or function() return true end + options.inventoryPredicate = options.inventoryPredicate or function() return true end + options.loadRefs = options.loadRefs or false + options.itemRef = options.itemRef or nil + options.recurse = options.recurse or true + return options +end + local enqueueItem local enqueueSlot local enqueueInventory +local enqueuePlayerItems local enqueueOpenContainers -local allPlayerItems -local allSubmarineItems -local allOwnedItems -local _ +local enqueueSubmarineItems +local enqueueAllOwnedItems ----@alias FilterPredicate fun(item: Barotrauma.Item, inventoryRef?: Barotrauma.ItemInventory, slotRef: Barotrauma.ItemInventory.Slot): boolean +do + ---@param item Barotrauma.Item + ---@param options EnqueueOptions + ---@return EnqueueOptions, string? + enqueueItem = function(item, options) + options = ensureOptionsDefaults(options) + if not item then return options.itemQueue, "No item" end --- Loading refs is optional because it MAY have a performance impact - ----@param item Barotrauma.Item ----@param queue Barotrauma.Item[] ----@param predicate? FilterPredicate ----@param loadRefs? boolean ----@param itemRef? ItemRefs ----@return Barotrauma.Item[], string? -enqueueItem = function(item, queue, predicate, loadRefs, itemRef) - queue = queue or {} - predicate = predicate or function() return true end - itemRef = itemRef or {} - -- debugPrint(string.format("Enqueuing item: %s", item.Prefab.Identifier.Value)) - -- local err - -- This should make it breadth first, right...? - -- No, not yet... - if not item then return queue, "No item" end - - local ok, stop = predicate(item, itemRef) - if ok then - queue[#queue + 1] = item - end - if stop then return queue, "Stop" end - if item.OwnInventory then - -- As far as I know every item has only one inventory - -- Only machines have multiple - -- So inventrorY should be fine here - -- debugPrint("Item has its own inventory, enqueuing inventory...") - if loadRefs then - itemRef.item = item - queue, _ = enqueueInventory(item.OwnInventory, queue, predicate, loadRefs, itemRef) - else - queue, _ = enqueueInventory(item.OwnInventory, queue, predicate, itemRef) - end - -- if err then - -- debugPrint(string.format("Error enqueuing inventory: %s", err)) - -- end - end - -- debugPrint(string.format("Item enqueued. Current queue size: %d", #queue)) - return queue, nil -end - ----@param slot Barotrauma.ItemInventory.Slot ----@param queue Barotrauma.Item[] ----@param predicate? FilterPredicate ----@param loadRefs? boolean ----@param itemRef? ItemRefs ----@return Barotrauma.Item[], string? -enqueueSlot = function(slot, queue, predicate, loadRefs, itemRef) - queue = queue or {} - predicate = predicate or function() return true end - itemRef = itemRef or {} - -- debugPrint(string.format("Enqueuing slot with %d items.", #slot.items)) - -- We don't want to shadow queue - local err - -- If the slot is empty there's nothing to iterate - -- And we will naturally return queue as is - if not slot then return queue, "No slot" end - if not slot.items then return queue, "No items" end - - for _, item in ipairs(slot.items) do - -- Only the final leaf nodes decide upon the predicate - if loadRefs then - itemRef.slot = slot - queue, err = enqueueItem(item, queue, predicate, loadRefs, itemRef) - else - queue, err = enqueueItem(item, queue, predicate) - end - if err then - return queue, err - end - end - -- debugPrint(string.format("Finished enqueuing slot. Current queue size: %d", #queue)) - return queue -end - ----@param inventory Barotrauma.ItemInventory ----@param queue Barotrauma.Item[] ----@param predicate? FilterPredicate ----@param loadRefs? boolean ----@param itemRef? ItemRefs ----@return Barotrauma.Item[], string? -enqueueInventory = function(inventory, queue, predicate, loadRefs, itemRef) - queue = queue or {} - predicate = predicate or function() return true end - itemRef = itemRef or {} - -- debugPrint(string.format("Enqueuing inventory with %d slots.", #inventory.slots)) - local err - if not inventory then return queue, "No inventory" end - if not inventory.slots then return queue, "No slots" end - - for _, slot in ipairs(inventory.slots) do - -- Only the final leaf nodes decide upon the predicate - if loadRefs then - itemRef.inventory = inventory - queue, err = enqueueSlot(slot, queue, predicate, loadRefs, itemRef) - else - queue, err = enqueueSlot(slot, queue, predicate) - end - if err then - return queue, err - end - end - -- debugPrint(string.format("Finished enqueuing inventory. Current queue size: %d", #queue)) - return queue -end - -local relevantPlayerInventorySlots = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, MyModGlobal.BAG_SLOT } ----@param queue Barotrauma.Item[] ----@param predicate? FilterPredicate ----@param loadRefs? boolean ----@return Barotrauma.Item[], string? -allPlayerItems = function(queue, predicate, loadRefs) - queue = queue or {} - predicate = predicate or function() return true end - - local character = Character.Controlled - if not character then return queue, "No character" end - - local inventory = character.Inventory - if not inventory then return queue, "No inventory" end - - for _, slotid in ipairs(relevantPlayerInventorySlots) do - local slot = inventory.slots[slotid] - local err - - if not slot then goto continue end - if #slot.items == 0 then goto continue end - - queue, err = enqueueSlot(slot, queue, predicate, loadRefs) - if err then return queue, err end - - ::continue:: - end - - return queue -end - ----@param queue Barotrauma.Item[] ----@param predicate? FilterPredicate ----@param loadRefs? boolean ----@return Barotrauma.Item[], string? -enqueueOpenContainers = function(queue, predicate, loadRefs) - queue = queue or {} - predicate = predicate or function() return true end - - local containers, err = getOpenContainers() - if err then return queue, err end - - for _, container in ipairs(containers) do - local inventories = container.OwnInventories - if not inventories then goto continue end - for containerInventory in inventories do - queue, err = enqueueInventory(containerInventory, queue, predicate, loadRefs) - if err then return queue, err end - end - ::continue:: - end - - return queue -end - ----@param queue Barotrauma.Item[] ----@param predicate? FilterPredicate ----@return Barotrauma.Item[], string? -allSubmarineItems = function(queue, predicate) - queue = queue or {} - predicate = predicate or function() return true end - -- This only exists so predicate does not explode - -- Even if its empty - local itemRef = {} - - local character = Character.Controlled - if not character then return queue, "No character" end - - local submarine = character.Submarine - if not submarine then return queue, "No submarine" end - - for item in submarine.GetItems(false) do - -- We do NOT want to call enqueueItem here because enqueueItem - -- Is recursive - -- And this call (GetItems) already gets all items - -- So we would be doing double the work (at best case) - -- It also means we won't have refs here which sucks - local ok, stop = predicate(item, itemRef) + local ok, stop = options.itemPredicate(item, options.itemRef) if ok then - queue[#queue + 1] = item + options.itemQueue[#options.itemQueue + 1] = item end - if stop then return queue, "Stop" end + if stop then return options.itemQueue, "Stop" end + + local err + if item.OwnInventory then + -- As far as I know every item has only one inventory + -- Only machines have multiple + -- So inventrorY should be fine here + if options.recurse then + if options.loadRefs then + options.itemRef.item = item + options.inventoryQueue, err = enqueueInventory(item.OwnInventory, options) + else + options.inventoryQueue, err = enqueueInventory(item.OwnInventory, options) + end + end + end + return options, err end - return queue -end + ---@param slot Barotrauma.Inventory.ItemSlot + ---@param options EnqueueOptions + ---@return EnqueueOptions, string? + enqueueSlot = function(slot, options) + options = ensureOptionsDefaults(options) + if not slot then return options, "No slot" end + if not slot.items then return options, "No items" end ----@param queue Barotrauma.Item[] ----@param predicate? FilterPredicate ----@param loadRefs? boolean ----@return Barotrauma.Item[], string? -allOwnedItems = function(queue, predicate, loadRefs) - queue = queue or {} - predicate = predicate or function() return true end + local ok, stop = options.slotPredicate(slot, options.itemRef) + if ok then + options.slotQueue[#options.slotQueue + 1] = slot + end + if stop then return options, "Stop" end - local err - queue, err = allPlayerItems(queue, predicate, loadRefs) - if err then return queue, err end + for _, item in ipairs(slot.items) do + -- We redeclare err every iteration so it doesn't spill over + local err + if options.loadRefs then + options.itemRef.slot = slot + options, err = enqueueItem(item, options) + else + options, err = enqueueItem(item, options) + end + if err then + return options, err + end + end + return options + end - queue, err = allSubmarineItems(queue, predicate) - if err then return queue, err end + ---@param inventory Barotrauma.Inventory + ---@param options EnqueueOptions + ---@return EnqueueOptions, string? + enqueueInventory = function(inventory, options) + options = ensureOptionsDefaults(options) + if not inventory then return options, "No inventory" end + if not inventory.slots then return options, "No slots" end - return queue + local ok, stop = options.inventoryPredicate(inventory, options.itemRef) + if ok then + options.inventoryQueue[#options.inventoryQueue + 1] = inventory + end + if stop then return options, "Stop" end + + for i, slot in ipairs(inventory.slots) do + local err + if options.loadRefs then + options.itemRef.inventory = inventory + options.itemRef.slot = slot + options.itemRef.slotIndex1 = i + options, err = enqueueSlot(slot, options) + else + options, err = enqueueSlot(slot, options) + end + if err then + return options, err + end + end + return options + end + + local relevantPlayerInventorySlots = { + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + } + ---@param options EnqueueOptions + ---@return EnqueueOptions, string? + enqueuePlayerItems = function(options) + options = ensureOptionsDefaults(options) + + local character = Character.Controlled + if not character then return options, "No character" end + local inventory = character.Inventory + if not inventory then return options, "No inventory" end + + options.loadRefs = true + options.itemPredicate = function(item) + if not item then return false end + local parentInventory = item.ParentInventory + if not parentInventory then return false end + if not parentInventory.Equals(inventory) then return false end + return true + end + options.slotPredicate = function(slot, itemRef) + if not slot then return false end + if itemRef.slotIndex1 and relevantPlayerInventorySlots[itemRef.slotIndex1] then + return true + end + return false + end + + local err + options, err = enqueueInventory(inventory, options) + if err then return options, err end + + return options + end + + ---@param options EnqueueOptions + ---@return EnqueueOptions, string? + enqueueOpenContainers = function(options) + options = ensureOptionsDefaults(options) + + local containers, err = getOpenContainers() + if err then return options, err end + + for _, container in ipairs(containers) do + local inventories = container.OwnInventories + if not inventories then goto continue end + for containerInventory in inventories do + options, err = enqueueInventory(containerInventory, options) + if err then return options, err end + end + ::continue:: + end + + return options + end + + ---@param options EnqueueOptions + ---@return EnqueueOptions, string? + enqueueSubmarineItems = function(options) + options = ensureOptionsDefaults(options) + + -- This only exists so predicate does not explode + -- Even if its empty + local itemRef = {} + + local character = Character.Controlled + if not character then return options, "No character" end + local submarine = character.Submarine + if not submarine then return options, "No submarine" end + + for item in submarine.GetItems(false) do + -- We do NOT want to call enqueueItem here because enqueueItem + -- Is recursive + -- And this call (GetItems) already gets all items + -- So we would be doing double the work (at best case) + -- It also means we won't have refs here which sucks + local ok, stop = options.itemPredicate(item, itemRef) + if ok then + options.itemQueue[#options.itemQueue + 1] = item + end + if stop then return options, "Stop" end + end + + return options + end + + ---@param options EnqueueOptions + ---@return EnqueueOptions, string? + enqueueAllOwnedItems = function(options) + options = ensureOptionsDefaults(options) + + local err + options, err = enqueuePlayerItems(options) + if err then return options, err end + + options, err = enqueueSubmarineItems(options) + if err then return options, err end + + return options + end end -- There is actually no need to recurse deep @@ -260,8 +453,8 @@ end -- And not an item in an item in the inventory -- So in theory we only need to recurse 1 deep ---@param inventory Barotrauma.Inventory ----@param slots InventorySlot[] ----@param depth number +---@param slots? InventorySlot[] +---@param depth? number ---@return InventorySlot[], string? local function getMouseoverSlots(inventory, slots, depth) slots = slots or {} @@ -269,7 +462,7 @@ local function getMouseoverSlots(inventory, slots, depth) if depth > 1 then return slots, nil end local visualSlots = inventory.visualSlots - if not visualSlots then return nil, "Inventory has no visual slots" end + if not visualSlots then return slots, "Inventory has no visual slots" end for i, visualSlot in ipairs(visualSlots) do local item @@ -305,11 +498,8 @@ local function getMouseoverSlots(inventory, slots, depth) ::mouseover:: if visualSlot:MouseOn() then - slots[#slots + 1] = { - inventory = inventory, - slotIndex = i, - slot = slot - } + local inventorySlot = MyModGlobal.InventorySlot.new(inventory, i) + slots[#slots + 1] = inventorySlot end ::continue:: @@ -320,15 +510,17 @@ end ---@return InventorySlot[], string? local function getSlotsUnderCursor() + local slots = {} -- Make sure we have a controlled character local controlledCharacter = Character.Controlled - if not controlledCharacter then return nil, "No controlled character" end + if not controlledCharacter then return slots, "No controlled character" end local inventory = controlledCharacter.Inventory - if not inventory then return nil, "No inventory" end + if not inventory then return slots, "No inventory" end - local mouseoverSlots, err = getMouseoverSlots(inventory) - if err then return mouseoverSlots, err end + local err + slots, err = getMouseoverSlots(inventory, slots) + if err then return slots, err end -- Even if we don't get them we're still fine local openContainers, _ = getOpenContainers() @@ -337,33 +529,28 @@ local function getSlotsUnderCursor() for _, container in ipairs(openContainers) do local containerInventories = container.OwnInventories for containerInventory in containerInventories do - local slot if not containerInventory or not containerInventory.visualSlots then MyModGlobal.debugPrint("Container inventory has no visual slots") goto continue end for i, visualSlot in ipairs(containerInventory.visualSlots) do if visualSlot:MouseOn() then - slot = containerInventory.slots[i] - mouseoverSlots[#mouseoverSlots + 1] = { - inventory = containerInventory, - slotIndex = i, - slot = slot - } + local inventorySlot = MyModGlobal.InventorySlot.new(containerInventory, i) + slots[#slots + 1] = inventorySlot end end ::continue:: end end - return mouseoverSlots, nil + return slots, nil end ---@return InventorySlot, string? local function getFirstSlotUnderCursor() local slots, err = getSlotsUnderCursor() - if err then return nil, err end - if #slots == 0 then return nil, "No slots found under cursor" end + if err then return slots, err end + if #slots == 0 then return slots, "No slots found under cursor" end for _, slot in ipairs(slots) do if slot.slot.items and #slot.slot.items > 0 then return slot @@ -376,12 +563,13 @@ return { enqueueItem = enqueueItem, enqueueSlot = enqueueSlot, enqueueInventory = enqueueInventory, - enqueueAllPlayerItems = allPlayerItems, - enqueueAllSubmarineItems = allSubmarineItems, - enqueueAllOwnedItems = allOwnedItems, + enqueuePlayerItems = enqueuePlayerItems, + enqueueSubmarineItems = enqueueSubmarineItems, + enqueueAllOwnedItems = enqueueAllOwnedItems, enqueueOpenContainers = enqueueOpenContainers, getOpenContainers = getOpenContainers, getFirstOpenContainer = getFirstOpenContainer, getSlotsUnderCursor = getSlotsUnderCursor, getFirstSlotUnderCursor = getFirstSlotUnderCursor, + enqueueMove = enqueueMove, }