Files
barotrauma-localmods/QuickStackToBag/Lua/Autorun/init.lua
2025-03-30 14:36:16 +02:00

485 lines
20 KiB
Lua

if SERVER then return end
-- Docs: https://evilfactory.github.io/LuaCsForBarotrauma/lua-docs/manual/common-questions/
---@class MyModGlobal
---@field CONFIG {QUICKSTACK_KEYS: Keys, FABRICATOR_KEY: Keys, MAX_BUY: Keys, NESTED_CONTAINERS: boolean, DEBUG_MODE: boolean}
---@field MOD_NAME string
---@field MOD_VERSION string
---@field DumpTable fun(table: table, depth?: number)
---@field debugPrint fun(message: string)
MyModGlobal = {
CONFIG = {
QUICKSTACK_KEYS = Keys.F,
FABRICATOR_KEY = Keys.V,
MAX_BUY = Keys.B,
NESTED_CONTAINERS = true,
DEBUG_MODE = true,
},
MOD_NAME = "Quick Stack To Containers",
MOD_VERSION = "1.1.0",
BAG_SLOT = 8,
}
---@param table table
---@param depth number?
MyModGlobal.DumpTable = function(table, depth)
if depth == nil then
depth = 0
end
if (depth > 200) then
print("Error: Depth > 200 in dumpTable()")
return
end
for k, v in pairs(table) do
if (type(v) == "table") then
print(string.rep(" ", depth) .. k .. ":")
MyModGlobal.DumpTable(v, depth + 1)
else
print(string.rep(" ", depth) .. k .. ": ", v)
end
end
end
-- Debugging helper function
MyModGlobal.debugPrint = function(message)
if MyModGlobal.CONFIG.DEBUG_MODE then
print("[" .. MyModGlobal.MOD_NAME .. "] " .. message)
end
end
require("Cyka.quickstack")
print(MyModGlobal.MOD_NAME .. " v" .. MyModGlobal.MOD_VERSION .. " loaded!")
-- Register necessary types and make fields accessible
LuaUserData.RegisterType("Barotrauma.Items.Components.ItemContainer+SlotRestrictions")
LuaUserData.RegisterType(
'System.Collections.Immutable.ImmutableArray`1[[Barotrauma.Items.Components.ItemContainer+SlotRestrictions, Barotrauma]]')
LuaUserData.MakeFieldAccessible(Descriptors['Barotrauma.Items.Components.ItemContainer'], 'slotRestrictions')
LuaUserData.MakeFieldAccessible(Descriptors['Barotrauma.ItemInventory'], 'slots')
LuaUserData.MakeFieldAccessible(Descriptors["Barotrauma.CharacterInventory"], "slots")
LuaUserData.RegisterType("Barotrauma.Store")
LuaUserData.RegisterType("Barotrauma.GUIComponent")
LuaUserData.RegisterType("Barotrauma.PurchasedItem")
LuaUserData.RegisterType("Barotrauma.ItemPrefab")
LuaUserData.RegisterType("Barotrauma.Location+StoreInfo")
LuaUserData.MakeMethodAccessible(Descriptors["Barotrauma.CargoManager"], "GetConfirmedSoldEntities")
-- -- Register necessary type to access VisualSlot
-- LuaUserData.RegisterType("Barotrauma.VisualSlot")
--
-- ---@return Barotrauma.VisualSlot|nil, Barotrauma.ItemInventory|nil, Barotrauma.Item|nil
-- local function getInventorySlotUnderCursor()
-- -- Make sure we have a controlled character
-- local controlledCharacter = Character.Controlled
-- if not controlledCharacter then return nil, nil, nil end
--
-- -- Check player inventory first
-- local charInventory = controlledCharacter.Inventory
-- if charInventory and charInventory.visualSlots then
-- for i, visualSlot in ipairs(charInventory.visualSlots) do
-- -- Check if mouse is over this slot
-- if visualSlot:MouseOn() then
-- local slot = charInventory.slots[i]
-- local item = nil
-- if #slot.items > 0 then
-- item = slot.items[1]
-- end
-- return visualSlot, charInventory, item
-- end
-- end
-- end
--
-- -- Check if selected item has inventory (containers, etc.)
-- local selectedItem = controlledCharacter.SelectedItem
-- if selectedItem and selectedItem.OwnInventory and selectedItem.OwnInventory.visualSlots then
-- local itemInv = selectedItem.OwnInventory
-- for i, visualSlot in ipairs(itemInv.visualSlots) do
-- if visualSlot:MouseOn() then
-- local slot = itemInv.slots[i]
-- local item = nil
-- if #slot.items > 0 then
-- item = slot.items[1]
-- end
-- return visualSlot, itemInv, item
-- end
-- end
-- end
--
-- -- Check open containers or other items with visible inventories
-- for item in Item.ItemList do
-- if item and item.OwnInventory and item.OwnInventory.visualSlots then
-- local itemInv = item.OwnInventory
-- for i, visualSlot in ipairs(itemInv.visualSlots) do
-- if visualSlot:MouseOn() then
-- local slot = itemInv.slots[i]
-- local slotItem = nil
-- if #slot.items > 0 then
-- slotItem = slot.items[1]
-- end
-- return visualSlot, itemInv, slotItem
-- end
-- end
-- end
-- end
--
-- return nil, nil, nil
-- end
-- -- Register necessary types for detecting repairable objects
-- LuaUserData.RegisterType("Barotrauma.Items.Components.Repairable")
-- LuaUserData.MakeFieldAccessible(Descriptors["Barotrauma.Items.Components.Repairable"], "RepairButton")
--
-- ---@return Barotrauma.Item|nil, Barotrauma.Items.Components.Repairable|nil
-- local function getRepairableObjectInFocus()
-- -- Make sure we have a controlled character
-- local controlledCharacter = Character.Controlled
-- if not controlledCharacter then return nil, nil end
--
-- -- Check if we have a selected item - this is necessary for repair interfaces to show
-- local selectedItem = controlledCharacter.SelectedItem
-- if not selectedItem then return nil, nil end
--
-- -- Check if the selected item is in fact the repairable object itself
-- for _, component in pairs(selectedItem.Components) do
-- if component.name == "Repairable" then
-- -- Check if repair interface should be shown
-- if component:ShouldDrawHUD(controlledCharacter) then
-- return selectedItem, component
-- end
-- end
-- end
--
-- -- Nothing found
-- return nil, nil
-- end
--
-- ---@return boolean
-- local function isRepairButtonVisible()
-- local _, repairableComponent = getRepairableObjectInFocus()
-- if not repairableComponent then return false end
--
-- -- Check if the repair button exists and is visible
-- local repairButton = repairableComponent.RepairButton
-- return repairButton ~= nil and repairButton.Visible
-- end
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
---@return {item: Barotrauma.Item, fabricator: Barotrauma.FabricatorComponent}, string?
local function getOpenFabricator()
-- Get the controlled character
local controlledCharacter = Character.Controlled
if not controlledCharacter then return nil, "No controlled character found" end
-- Check if the character has a selected item
local selectedItem = controlledCharacter.SelectedItem
if not selectedItem then return nil, "No selected item found" end
-- Check if the selected item has a Fabricator component
local fabricator = Game.GetFabricatorComponent(selectedItem)
if not fabricator then return nil, "No fabricator component found" end
return {
item = selectedItem,
fabricator = fabricator
}
end
--- Recipes can have multiple inputs, for example ammo
--- Can be made either out of copper iron or steel, 1 of either
---@class RecipeInfo
---@field targetItem {identifier: string, name: string, amount: number}
---@field requiredItems {amount: number, minCondition: number, maxCondition: number, prefabs: string[]}[]
---@param fabricator Barotrauma.FabricatorComponent
---@return RecipeInfo, string?
local function getSelectedRecipeRequirements(fabricator)
-- local openFabricator, err = getOpenFabricator()
-- if err then return nil, err end
-- local fabricator = openFabricator.fabricator
local selectedRecipe = fabricator.SelectedItem
if not selectedRecipe then return nil, "No selected recipe found" end
local requiredItems = {}
for requiredItem in selectedRecipe.RequiredItems do
local itemInfo = {
amount = tonumber(requiredItem.Amount),
minCondition = tonumber(requiredItem.MinCondition),
maxCondition = tonumber(requiredItem.MaxCondition),
prefabs = {}
}
for prefab in requiredItem.ItemPrefabs do
itemInfo.prefabs[#itemInfo.prefabs + 1] = prefab.Identifier.Value
end
requiredItems[#requiredItems + 1] = itemInfo
end
return {
targetItem = {
identifier = selectedRecipe.TargetItem.Identifier,
name = selectedRecipe.TargetItem.Name,
amount = selectedRecipe.Amount
},
requiredItems = requiredItems
}
end
-- Hook into player control to listen for key press
Hook.Patch("Barotrauma.Character", "ControlLocalPlayer", function(instance, ptable)
if not PlayerInput.KeyHit(CONFIG.FABRICATOR_KEY) then return end
-- TODO: Maybe get items from entire sub...?
-- There's no point getting recipes if we don't have all of this bullshit
---@type Barotrauma.Character
local character = instance
if not character then
debugPrint("Character instance is nil.")
return
end
---@type Barotrauma.CharacterInventory
local inventory = character.Inventory
if not inventory then
debugPrint("Character inventory is nil.")
return
end
---@type Barotrauma.ItemInventory.Slot
local bagSlot = inventory.slots[BAG_SLOT]
if not bagSlot then
debugPrint("Bag slot not found.")
return
end
if #bagSlot.items == 0 then
debugPrint("Bag slot is empty.")
return
end
---@type Barotrauma.Item
local bagItem = bagSlot.items[1]
if not bagItem then
debugPrint("Bag item not found.")
return
end
local fabricator, err = getOpenFabricator()
if err then
print(string.format("Error getting open fabricator: %s", err))
return
end
local recipe
recipe, err = getSelectedRecipeRequirements(fabricator.fabricator)
if err then
print(string.format("Error getting selected recipe requirements: %s", err))
return
end
-- DumpTable(recipe)
-- TODO: Maybe make it so every press cycles the input
-- For recipes that have multiple prefabs
-- But then again what if it has 3 items with 4 prefabs each..
-- Is that 4 iterations or 3*4 iterations?
-- We can not use #toFind because we can remove 0th item
-- Which means it's no longer contiguous
-- Which means #toFind returns 0
local toFind = recipe.requiredItems
local remaining = #toFind
---@type Barotrauma.Item[]
local toGet = {}
---@type fun(item: Barotrauma.Item): boolean, boolean
local filter = function(item)
local found = false
-- DumpTable(toFind)
-- toFind are all items we need to find
for i, itemInfo in pairs(toFind) do
-- prefabs are all items that satisfy the requirements
for _, prefab in ipairs(itemInfo.prefabs) do
-- debugPrint(string.format("Checking %s against %s", item.Prefab.Identifier.Value, prefab))
if item.Prefab.Identifier.Value == prefab then
-- debugPrint(string.format("That'll do %s %s", item.Prefab.Identifier.Value, prefab))
toGet[#toGet + 1] = item
itemInfo.amount = itemInfo.amount - 1
found = true
break
end
end
if itemInfo.amount <= 0 then
-- debugPrint(string.format("Removing %s from toFind", itemInfo.prefabs[1]))
toFind[i] = nil
remaining = remaining - 1
end
if found then break end
end
-- DumpTable(toFind)
-- debugPrint(string.format("Found %s %s", item.Prefab.Identifier.Value, tostring(remaining)))
return found, remaining == 0
end
-- DumpTable(toGet)
local items = enqueueInventory(bagItem.OwnInventory, {}, filter)
-- DumpTable(items)
-- TODO: This might explode... Oh well?
local inputInventory = fabricator.item.OwnInventories[1]
for iinventory in fabricator.item.OwnInventories do
if #iinventory.slots > 1 then
inputInventory = iinventory
break
end
end
local slot = -1
local previous = nil
for _, item in ipairs(items) do
if previous ~= item.Prefab.Identifier then slot = slot + 1 end
inputInventory.TryPutItem(item, slot, false, true, nil)
previous = item.Prefab.Identifier
end
DumpTable(items)
end, Hook.HookMethodType.After)
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
---@return Barotrauma.Location.StoreInfo[], string?
local function getCurrentStore()
if not Game or not Game.GameSession or not Game.GameSession.Campaign then
return nil, "No game session found"
end
local map = Game.GameSession.Campaign.Map
if not map or not map.CurrentLocation or not map.CurrentLocation.Stores then
return nil, "No map found"
end
local location = map.CurrentLocation
-- Otherwise, determine which store is active by checking the cargo manager
local cargoManager = Game.GameSession.Campaign.CargoManager
if not cargoManager then
return nil, "No cargo manager found"
end
-- Find which store has items in the cart
local stores = {}
for _, store in pairs(location.Stores) do
if #cargoManager:GetBuyCrateItems(store) > 0 then
stores[#stores + 1] = store
end
end
return stores, nil
end
-- Example: Add a key binding to buy all items in the current store
-- when the 'B' key is pressed
Hook.Patch("Barotrauma.Character", "ControlLocalPlayer", function(instance, ptable)
if not PlayerInput.KeyHit(CONFIG.MAX_BUY) then return end
local cargoManager = Game.GameSession.Campaign.CargoManager
if not cargoManager then
print("No cargo manager available")
return
end
local stores, err = getCurrentStore()
if err then
print(string.format("Error getting current store: %s", err))
return
end
for _, store in ipairs(stores) do
local toAdd = {}
-- Get items available at the store
local items = cargoManager:GetBuyCrateItems(store)
for item in items do
-- We have already added this many of item
toAdd[item.ItemPrefab.Identifier.Value] = {
quantity = -item.Quantity,
prefab = item.ItemPrefab -- Store the ItemPrefab object
}
end
for item in store.Stock do
-- So if we add the total amount available
-- We get the amount we have to add to buy entire stock
local idValue = item.ItemPrefab.Identifier.Value
if toAdd[idValue] then
toAdd[idValue].quantity = toAdd[idValue].quantity + item.Quantity
end
end
for idValue, info in pairs(toAdd) do
if info.quantity > 0 then
print(string.format("Adding %d of %s to the buy crate", info.quantity, idValue))
-- Use the stored ItemPrefab object, not the string identifier
cargoManager:ModifyItemQuantityInBuyCrate(store.Identifier, info.prefab, info.quantity)
end
end
end
end, Hook.HookMethodType.After)
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
local amountExperience = 600
local passiveExperienceDelay = 2
local passiveExperienceTimer = 0
Hook.Add("think", "examples.passiveExperience", function()
if Timer.GetTime() < passiveExperienceTimer then return end
for k, v in pairs(Character.CharacterList) do
if not v.IsDead and v.Info ~= nil then
v.Info.GiveExperience(amountExperience)
end
end
passiveExperienceTimer = Timer.GetTime() + passiveExperienceDelay
end)