407 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			407 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, predicate
 | |
| 		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))
 | |
| 		predicate = 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
 | |
| 
 | |
| 		utils.enqueueAllSubmarineItems({}, predicate)
 | |
| 		utils.enqueueAllPlayerItems({}, predicate)
 | |
| 
 | |
| 		::continue::
 | |
| 	end
 | |
| end
 | |
| 
 | |
| return {
 | |
| 	buildItemTree = buildItemTree,
 | |
| 	tryBuildCharacterItemTree = tryBuildCharacterItemTree,
 | |
| 	sortItemTree = sortItemTree,
 | |
| 	tryMoveItem = tryMoveItem,
 | |
| 	tryMoveItems = tryMoveItems,
 | |
| 	quickStackItems = quickStackItems,
 | |
| 	stackToCursor = stackToCursor,
 | |
| 	stackAllToCursor = stackAllToCursor,
 | |
| }
 |