404 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			404 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
-- luacheck: globals MyModGlobal Character
 | 
						|
-- luacheck: max line length 420
 | 
						|
local utils = require("Cyka.utils")
 | 
						|
 | 
						|
---@class ItemLocation
 | 
						|
---@field inventory Barotrauma.ItemInventory
 | 
						|
---@field slotIndex number
 | 
						|
---@field depth number
 | 
						|
---@field maxFits number
 | 
						|
 | 
						|
-- The resulting item tree is a table where the key is an ID of an item
 | 
						|
-- And the value is an object that represents where that item is located
 | 
						|
-- In our inventory
 | 
						|
-- Special case are empty slots where any item fits
 | 
						|
---@param inventory Barotrauma.ItemInventory
 | 
						|
---@param itemTree table<string, ItemLocation[]>
 | 
						|
---@param depth number
 | 
						|
---@return table<string, ItemLocation[]>
 | 
						|
local function buildItemTree(inventory, itemTree, depth)
 | 
						|
	itemTree = itemTree or {}
 | 
						|
	depth = depth or 0
 | 
						|
	if not inventory or not inventory.slots then
 | 
						|
		-- MyModGlobal.debugPrint(string.format("Inventory is nil or has no slots, returning empty itemTree"))
 | 
						|
		return itemTree
 | 
						|
	end
 | 
						|
 | 
						|
	-- One slot can have one item but multiple of it
 | 
						|
	-- The number of an item in a slot is #slot.items
 | 
						|
	for slotIndex, slot in ipairs(inventory.slots) do
 | 
						|
		-- MyModGlobal.debugPrint(string.format("Building item tree for inventory at slot index: %d", slotIndex))
 | 
						|
		-- MyModGlobal.debugPrint(string.format("Slot %d has %d items", slotIndex, #slot.items))
 | 
						|
		if #slot.items == 0 then
 | 
						|
			-- MyModGlobal.debugPrint(string.format("Slot %d is empty, adding to itemTree as 'empty'", slotIndex))
 | 
						|
			itemTree['empty'] = itemTree['empty'] or {}
 | 
						|
			itemTree['empty'][#itemTree['empty'] + 1] = {
 | 
						|
				inventory = inventory,
 | 
						|
				slotIndex = slotIndex - 1,
 | 
						|
				maxFits = 60,
 | 
						|
				depth = depth
 | 
						|
			}
 | 
						|
			-- MyModGlobal.debugPrint(string.format("Added empty slot to itemTree at index: %d", slotIndex))
 | 
						|
		else
 | 
						|
			---@type Barotrauma.Item
 | 
						|
			local item = slot.items[1]
 | 
						|
			local identifier = item.Prefab.Identifier.Value
 | 
						|
			-- MyModGlobal.debugPrint(string.format("Found item: %s with identifier: %s", item.Name, identifier))
 | 
						|
			itemTree[identifier] = itemTree[identifier] or {}
 | 
						|
			-- We DO want even slots with maxFits = 0
 | 
						|
			-- Because that indicates that we DO HAVE the item
 | 
						|
			-- At all
 | 
						|
			-- And based on that we decide to move it
 | 
						|
			itemTree[identifier][#itemTree[identifier] + 1] = {
 | 
						|
				inventory = inventory,
 | 
						|
				slotIndex = slotIndex - 1,
 | 
						|
				maxFits = slot.HowManyCanBePut(item.Prefab),
 | 
						|
				depth = depth
 | 
						|
			}
 | 
						|
			-- MyModGlobal.debugPrint(string.format("Added item to itemTree under identifier: %s", identifier))
 | 
						|
 | 
						|
			local tags = item.Prefab.Tags
 | 
						|
			local shouldSuss = false
 | 
						|
			for tag in tags do
 | 
						|
				if tag.value:find("container") then
 | 
						|
					shouldSuss = true
 | 
						|
					break
 | 
						|
				end
 | 
						|
			end
 | 
						|
 | 
						|
			if shouldSuss then
 | 
						|
				-- MyModGlobal.debugPrint(string.format("Searching inside %s for nested containers", item.Name))
 | 
						|
				buildItemTree(item.OwnInventory, itemTree, depth + 1)
 | 
						|
			end
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	-- MyModGlobal.debugPrint("Completed building item tree")
 | 
						|
	return itemTree
 | 
						|
end
 | 
						|
 | 
						|
-- We would like to fill larger stacks first
 | 
						|
---@param itemTree table<string, ItemLocation[]>
 | 
						|
---@return table<string, ItemLocation[]>
 | 
						|
local function sortItemTree(itemTree)
 | 
						|
	for _, item in pairs(itemTree) do
 | 
						|
		table.sort(item, function(a, b)
 | 
						|
			---@cast a ItemLocation
 | 
						|
			---@cast b ItemLocation
 | 
						|
			if a.depth ~= b.depth then
 | 
						|
				return a.depth < b.depth
 | 
						|
			elseif a.maxFits ~= b.maxFits then
 | 
						|
				return a.maxFits > b.maxFits
 | 
						|
			else
 | 
						|
				return a.slotIndex < b.slotIndex
 | 
						|
			end
 | 
						|
		end)
 | 
						|
	end
 | 
						|
 | 
						|
	return itemTree
 | 
						|
end
 | 
						|
 | 
						|
---@param item Barotrauma.Item
 | 
						|
---@param itemTree table<string, ItemLocation[]>
 | 
						|
---@param force boolean
 | 
						|
---@return string
 | 
						|
local function tryMoveItem(item, itemTree, force)
 | 
						|
	-- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value))
 | 
						|
	force = force or false
 | 
						|
	local location = itemTree[item.Prefab.Identifier.Value]
 | 
						|
	if not location and not force then
 | 
						|
		-- MyModGlobal.debugPrint("No locations for item, not stacking (not forced)")
 | 
						|
		return nil, "No locations for item, not stacking (not forced)"
 | 
						|
	end
 | 
						|
	-- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value))
 | 
						|
	-- MyModGlobal.DumpTable(location)
 | 
						|
 | 
						|
	local moved = false
 | 
						|
	if location then
 | 
						|
		-- First try to move to existing stacks
 | 
						|
		for _, itemLocation in ipairs(location) do
 | 
						|
			-- We cannot stack items with decreased condition
 | 
						|
			local canBePut = itemLocation.inventory.CanBePutInSlot(item.Prefab, itemLocation.slotIndex, item.Condition)
 | 
						|
			-- MyModGlobal.debugPrint(string.format("Can be put in slot %d: %s", itemLocation.slotIndex, tostring(canBePut)))
 | 
						|
 | 
						|
			if itemLocation.maxFits > 0 and canBePut then
 | 
						|
				moved = moved or itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, false, true, nil)
 | 
						|
				if moved then
 | 
						|
					itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
 | 
						|
				end
 | 
						|
				-- if moved then
 | 
						|
				-- 	MyModGlobal.debugPrint(string.format("Moved item to existing stack at slot index: %d", itemLocation .slotIndex))
 | 
						|
				-- else
 | 
						|
				-- 	MyModGlobal.debugPrint(string.format("Failed to move item to existing stack at slot index: %d", itemLocation .slotIndex))
 | 
						|
				-- end
 | 
						|
			end
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	-- If we can not find an existing stack
 | 
						|
	-- Then move to any of the empty slots
 | 
						|
	if not moved then
 | 
						|
		-- MyModGlobal.debugPrint("No existing stacks found, trying empty slots...")
 | 
						|
		for _, itemLocation in ipairs(itemTree['empty']) do
 | 
						|
			local maxFits = itemLocation.maxFits
 | 
						|
			-- These empty slots are not guranteed to be empty, ironically
 | 
						|
			-- After we insert an item into one it's no longer empty
 | 
						|
			-- But it still is in the empty table
 | 
						|
			-- So we want to make sure we can insert our item
 | 
						|
			-- Into the maybe empty slots
 | 
						|
			itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
 | 
						|
 | 
						|
			if maxFits > 0 then
 | 
						|
				-- MyModGlobal.debugPrint(string.format("Trying to move item to empty slot at index: %d", itemLocation.slotIndex))
 | 
						|
				moved = moved or itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, true, false, nil)
 | 
						|
				if moved then
 | 
						|
					itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
 | 
						|
				end
 | 
						|
				-- if moved then
 | 
						|
				-- 	MyModGlobal.debugPrint(string.format("Moved item to empty slot at index: %d", itemLocation.slotIndex))
 | 
						|
				-- else
 | 
						|
				-- 	MyModGlobal.debugPrint(string.format("Failed to move item to empty slot at index: %d", itemLocation.slotIndex))
 | 
						|
				-- end
 | 
						|
			end
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	-- If we still can not move the item give up
 | 
						|
	if not moved then
 | 
						|
		-- MyModGlobal.debugPrint("Failed to find valid location for item")
 | 
						|
		return "Failed to find valid location for item"
 | 
						|
	end
 | 
						|
	-- MyModGlobal.debugPrint("Item moved successfully")
 | 
						|
	return nil
 | 
						|
end
 | 
						|
 | 
						|
---@param items Barotrauma.Item[]
 | 
						|
---@param itemTree table<string, ItemLocation[]>
 | 
						|
---@param force boolean
 | 
						|
---@return string[]
 | 
						|
local function tryMoveItems(items, itemTree, force)
 | 
						|
	force = force or false
 | 
						|
	local errs = {}
 | 
						|
	for _, item in ipairs(items) do
 | 
						|
		local err = tryMoveItem(item, itemTree, force)
 | 
						|
		-- oops, this one failed, continue...
 | 
						|
		if err then
 | 
						|
			errs[#errs + 1] = string.format("Failed to move item: %s", item.Prefab.Identifier.Value)
 | 
						|
		end
 | 
						|
	end
 | 
						|
	return errs
 | 
						|
end
 | 
						|
 | 
						|
---@param character Barotrauma.Character
 | 
						|
---@return table<string, ItemLocation[]>, string
 | 
						|
local function tryBuildCharacterItemTree(character)
 | 
						|
	local itemTree = {}
 | 
						|
	-- MyModGlobal.debugPrint(string.format("Preparing to stack items into the bag..."))
 | 
						|
	local inventory = character.Inventory
 | 
						|
	if not inventory or not inventory.slots then
 | 
						|
		return itemTree, "Character has no inventory"
 | 
						|
	end
 | 
						|
 | 
						|
	local bagSlot = inventory.slots[MyModGlobal.BAG_SLOT]
 | 
						|
	if bagSlot then
 | 
						|
		-- MyModGlobal.debugPrint(string.format("Bag slot found at index 8 with %d items.", #bagSlot.items))
 | 
						|
		if #bagSlot.items > 0 then
 | 
						|
			local item = bagSlot.items[1]
 | 
						|
			-- MyModGlobal.debugPrint(string.format("Found item in bag slot: %s", item.Name))
 | 
						|
			if item and item.OwnInventory then
 | 
						|
				-- MyModGlobal.debugPrint(string.format("Item has its own inventory, building item tree for it..."))
 | 
						|
				itemTree = buildItemTree(item.OwnInventory, itemTree)
 | 
						|
			else
 | 
						|
				return itemTree, "Bag does not have its own inventory"
 | 
						|
			end
 | 
						|
		else
 | 
						|
			return itemTree, "Bag slot is empty"
 | 
						|
		end
 | 
						|
	else
 | 
						|
		return itemTree, "No bag slot found at index " .. tostring(MyModGlobal.BAG_SLOT)
 | 
						|
	end
 | 
						|
	return itemTree, nil
 | 
						|
end
 | 
						|
 | 
						|
-- Function to quickly stack items from inventory to containers
 | 
						|
-- 6 and 7 are hands
 | 
						|
-- 9..18 are main slots
 | 
						|
local inventorySlotsToStack = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }
 | 
						|
local function quickStackItems(character)
 | 
						|
	if not character then
 | 
						|
		MyModGlobal.debugPrint("No character found")
 | 
						|
		return
 | 
						|
	end
 | 
						|
 | 
						|
	local inventory = character.Inventory
 | 
						|
	if not inventory or not inventory.slots then
 | 
						|
		MyModGlobal.debugPrint("Character has no inventory")
 | 
						|
		return
 | 
						|
	end
 | 
						|
 | 
						|
	-- for i, slot in ipairs(inventory.slots) do
 | 
						|
	-- 	if #slot.items > 0 then
 | 
						|
	-- 		local item = slot.items[1]
 | 
						|
	-- 		local identifier = item.Prefab.Identifier.Value
 | 
						|
	-- 		print(string.format("Item at slot %d is %s", i, identifier))
 | 
						|
	-- 	end
 | 
						|
	-- end
 | 
						|
 | 
						|
	MyModGlobal.debugPrint("Quick stack function called")
 | 
						|
 | 
						|
	local itemTree, err = tryBuildCharacterItemTree(character)
 | 
						|
	if err then
 | 
						|
		MyModGlobal.debugPrint(string.format("Error building item tree: %s", err))
 | 
						|
		return
 | 
						|
	end
 | 
						|
	itemTree = sortItemTree(itemTree)
 | 
						|
	--DumpTable(itemTree)
 | 
						|
	local toMove = {}
 | 
						|
 | 
						|
	for _, slotid in ipairs(inventorySlotsToStack) do
 | 
						|
		MyModGlobal.debugPrint(string.format("Processing inventory slot: %d", slotid))
 | 
						|
		local slot = inventory.slots[slotid]
 | 
						|
		if #slot.items > 0 then
 | 
						|
			local item = slot.items[1]
 | 
						|
			local tags = item.Prefab.Tags
 | 
						|
			local shouldSuss = true
 | 
						|
			for tag in tags do
 | 
						|
				if tag.value:find("tool") or tag.value:find("weapon") then
 | 
						|
					MyModGlobal.debugPrint(string.format("Item '%s' is a tool or weapon, skipping", item.Name))
 | 
						|
					shouldSuss = false
 | 
						|
					break
 | 
						|
				end
 | 
						|
			end
 | 
						|
 | 
						|
			if shouldSuss then
 | 
						|
				local before = #toMove
 | 
						|
				toMove = utils.enqueueSlot(slot, toMove)
 | 
						|
				local after = #toMove
 | 
						|
				MyModGlobal.debugPrint(string.format("Enqueued %d items from the inventory slot %d", after - before,
 | 
						|
					slotid))
 | 
						|
			end
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	local openContainers = utils.getOpenContainers()
 | 
						|
	for _, container in ipairs(openContainers) do
 | 
						|
		local inventories = container.OwnInventories
 | 
						|
		MyModGlobal.debugPrint(string.format("Found %d inventories in the open container", #inventories))
 | 
						|
		for containerInventory in inventories do
 | 
						|
			MyModGlobal.debugPrint(string.format("Enqueuing inventory with %d slots", #containerInventory.slots))
 | 
						|
			local before = #toMove
 | 
						|
			toMove = utils.enqueueInventory(containerInventory, toMove)
 | 
						|
			local after = #toMove
 | 
						|
			MyModGlobal.debugPrint(string.format("Enqueued %d items from the open container", after - before))
 | 
						|
		end
 | 
						|
	end
 | 
						|
 | 
						|
	local errors = tryMoveItems(toMove, itemTree)
 | 
						|
	for _, error in ipairs(errors) do
 | 
						|
		MyModGlobal.debugPrint(string.format("Error stacking item: %s", error))
 | 
						|
	end
 | 
						|
end
 | 
						|
 | 
						|
local function stackToCursor()
 | 
						|
	local slots, err = utils.getSlotsUnderCursor()
 | 
						|
	if err then
 | 
						|
		MyModGlobal.debugPrint(string.format("Error getting slots under cursor: %s", err))
 | 
						|
		return
 | 
						|
	end
 | 
						|
 | 
						|
	for _, slot in ipairs(slots) do
 | 
						|
		local item
 | 
						|
		if not slot.slot.items or #slot.slot.items == 0 then
 | 
						|
			MyModGlobal.debugPrint("No items in slot")
 | 
						|
			goto continue
 | 
						|
		end
 | 
						|
 | 
						|
		item = slot.slot.items[1]
 | 
						|
		MyModGlobal.debugPrint(string.format("Stacking all player items to %s", item.Prefab.Identifier.Value))
 | 
						|
		utils.enqueueAllPlayerItems({}, function(ititem)
 | 
						|
			if ititem.Prefab.Identifier.Value == item.Prefab.Identifier.Value then
 | 
						|
				if item == ititem then return false end
 | 
						|
				-- We are moving items in the predicate because we expect to only
 | 
						|
				-- Select a small subset of all items
 | 
						|
				-- And it is much easier to let the game decide when we can not move
 | 
						|
				-- Any more items (via return value of TryPutItem)
 | 
						|
				-- And we then know that we can safely stop
 | 
						|
				-- UPDATE: OK well that was a stupid idea, it returns an error for other shit as well
 | 
						|
				-- What other shit? Wish I knew
 | 
						|
				-- So we'll use HowManyCanBePut instead...
 | 
						|
				local moved = slot.inventory.TryPutItem(ititem, slot.slotIndex - 1, false, true, nil)
 | 
						|
				if not moved then
 | 
						|
					MyModGlobal.debugPrint(string.format("Failed to move item %s to slot %d", ititem.Name, slot
 | 
						|
						.slotIndex - 1))
 | 
						|
					-- return false, true
 | 
						|
				end
 | 
						|
				local maxFits = slot.inventory.HowManyCanBePut(ititem.Prefab, slot.slotIndex - 1)
 | 
						|
				if maxFits <= 0 then
 | 
						|
					MyModGlobal.debugPrint(string.format("Item %s has no more fits in slot %d", ititem.Name, slot
 | 
						|
						.slotIndex - 1))
 | 
						|
					return false, true
 | 
						|
				end
 | 
						|
			end
 | 
						|
		end)
 | 
						|
 | 
						|
		::continue::
 | 
						|
	end
 | 
						|
end
 | 
						|
 | 
						|
local function stackAllToCursor()
 | 
						|
	local slots, err = utils.getSlotsUnderCursor()
 | 
						|
	if err then
 | 
						|
		MyModGlobal.debugPrint(string.format("Error getting slots under cursor: %s", err))
 | 
						|
		return
 | 
						|
	end
 | 
						|
 | 
						|
	for _, slot in ipairs(slots) do
 | 
						|
		local item
 | 
						|
		if not slot.slot.items or #slot.slot.items == 0 then
 | 
						|
			MyModGlobal.debugPrint("No items in slot")
 | 
						|
			goto continue
 | 
						|
		end
 | 
						|
 | 
						|
		item = slot.slot.items[1]
 | 
						|
		MyModGlobal.debugPrint(string.format("Stacking all items to %s", item.Prefab.Identifier.Value))
 | 
						|
		utils.enqueueAllOwnedItems({}, function(ititem)
 | 
						|
			if ititem.Prefab.Identifier.Value == item.Prefab.Identifier.Value then
 | 
						|
				if item == ititem then return false end
 | 
						|
				-- We are moving items in the predicate because we expect to only
 | 
						|
				-- Select a small subset of all items
 | 
						|
				-- And it is much easier to let the game decide when we can not move
 | 
						|
				-- Any more items (via return value of TryPutItem)
 | 
						|
				-- And we then know that we can safely stop
 | 
						|
				-- UPDATE: OK well that was a stupid idea, it returns an error for other shit as well
 | 
						|
				-- What other shit? Wish I knew
 | 
						|
				-- So we'll use HowManyCanBePut instead...
 | 
						|
				local moved = slot.inventory.TryPutItem(ititem, slot.slotIndex - 1, false, true, nil)
 | 
						|
				if not moved then
 | 
						|
					MyModGlobal.debugPrint(string.format("Failed to move item %s to slot %d", ititem.Name, slot
 | 
						|
						.slotIndex - 1))
 | 
						|
					-- return false, true
 | 
						|
				end
 | 
						|
				local maxFits = slot.inventory.HowManyCanBePut(ititem.Prefab, slot.slotIndex - 1)
 | 
						|
				if maxFits <= 0 then
 | 
						|
					MyModGlobal.debugPrint(string.format("Item %s has no more fits in slot %d", ititem.Name, slot
 | 
						|
						.slotIndex - 1))
 | 
						|
					return false, true
 | 
						|
				end
 | 
						|
			end
 | 
						|
		end)
 | 
						|
 | 
						|
		::continue::
 | 
						|
	end
 | 
						|
end
 | 
						|
 | 
						|
return {
 | 
						|
	buildItemTree = buildItemTree,
 | 
						|
	tryBuildCharacterItemTree = tryBuildCharacterItemTree,
 | 
						|
	sortItemTree = sortItemTree,
 | 
						|
	tryMoveItem = tryMoveItem,
 | 
						|
	tryMoveItems = tryMoveItems,
 | 
						|
	quickStackItems = quickStackItems,
 | 
						|
	stackToCursor = stackToCursor,
 | 
						|
	stackAllToCursor = stackAllToCursor,
 | 
						|
}
 |