Inventory

What it is

This is the player’s inventory and the storage in the world around it. The player carries things in a main grid, a backpack and a few equipment slots (hat, watch, hands). Containers are the chests, lockers, shelves and similar storage the player can open. A mod can count, give and take items, see what the player holds in each hand, watch items come and go, and look inside containers.

When to use it

  • Give the player a reward item when a quest step is done.
  • Take a required item (a key, a part) from the player before something happens.
  • Show a message when the player picks up a certain item.
  • Look into a container, for example to show what a shop’s storage holds.

Quick example

This mod waits until a save is loaded, logs how many bottles the player has and how much room is left, then gives two. It also logs every item that is added to or removed from the inventory. Items are found by name with Items.Find and used by their id (see Items).

using ObenseuerCore;

[ModInfo("example.inventorytour", "Inventory Tour", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public sealed class InventoryTour : FrameworkMod
{
    protected override void OnLoad()
    {
        PlayerInventory.ItemAdded += args => Log.Info("added " + args.Count + " of item " + args.ItemId);
        PlayerInventory.ItemRemoved += args => Log.Info("removed " + args.Count + " of item " + args.ItemId);
        Events.WhenReady(Tour);
    }

    private void Tour()
    {
        ItemDef bottle = Items.Find("Empty Glass Bottle");
        if (bottle == null) return;

        Log.Info("bottles: " + PlayerInventory.Count(bottle.Id) + ", free slots: " + PlayerInventory.FreeSlots + "/" + PlayerInventory.Capacity);
        if (PlayerInventory.Give(bottle.Id, 2)) Log.Info("gave 2 bottles");
        else Log.Warn("no room for 2 bottles");
    }
}

Give refuses, and adds nothing, when the whole amount does not fit, so always check what it returns. Count, Give and Take work with item ids, never names. ItemAdded and ItemRemoved are events: the framework calls your code each time they happen. See Using events for more on events and WhenReady.

  • Count counts the stacks whose item id equals itemId, in every inventory slot (backpack and character slots included).
  • Has uses the game’s CheckForItem, which ignores ownership and counts plain item stacks only.
  • Give refuses (returns false, adds nothing) unless the whole amount fits in the main inventory grid. When it fits, it uses the game’s dialogue add (Inventory.AddItemDialogueAmount): the same stack rules and the game’s “Items added!” notification. That add only reaches the main grid, never the equipped backpack or the equipment slots, and drops anything that does not fit on the ground while still returning true. So the fit check covers the main grid only: a full grid with free backpack space makes Give return false. Money ids 0 and 1 are refused; use Wallet.
  • Take returns false unless the whole amount is held. Then it uses the game’s dialogue remove (Inventory.RemoveItemDialogueAmount), which takes one unit at a time from the first matching stack in slot order (main grid, then backpack, then equipment slots). It does not prefer spoiled, low-quality or own items. A stack with ownerId > 0 that is taken while a witness sees it commits theft, following the game’s own crime rule; check Items() first when that matters. Money is not kept as item stacks, so Take returns false for ids 0 and 1; use Wallet.
  • Take counts what the game actually removed. The game’s remove call takes one unit per call and ignores a refusal, so a take can stop short of the amount asked for. The framework reads the player’s count before and after, and when the count did not move by exactly count it returns false and adds the removed units back through the same dialogue add. The give back restores the counted units only: they come back unowned, at the default quality and meta, without the spoilage state they had, and the player sees the “Items added!” notification while ItemAdded fires, so a rolled back take is not lossless. When the give back itself fails, it logs an error with the item id and the units that could not be returned, because those units are lost from the player’s inventory. A rolled back take gives the units back but does not undo a theft the game recorded while it removed them, because the game records that inside the removal itself. Set Debug.InventoryTakeSelfCheck to log a check of that accounting at start.
  • Take refuses an id that is a liquid, or that any of the player’s slots holds as a liquid (ItemLiquidData.liquidItem.ID of a slot’s stack), and logs one warning naming the id. The game’s remove also matches a container whose liquid id equals the id and can match that container before a plain stack, while Count compares plain stacks only, so such a take could destroy a whole container while the counted amount does not move and nothing would be reported. The check is the framework’s own; only an in-game take with a real liquid id can confirm the game’s match order. Use the container facade on this page or StackActions for liquid units.
  • Items() lists the non-empty stacks in every inventory slot, backpack and character slots included.
  • FreeSlots and Capacity count the same slots as Items(), so they include the backpack and the equipment slots (hat, watch, hands, backpack slot). FreeSlots is not the room Give can use, which is the main grid only.
  • GetHeldItemId returns the id of the item in the right or left hand (PlayerHandItems), or -1. The game does not track a stack count for a hand item.
  • EquipHeld(modId, itemId) puts one unowned unit from the main grid into a hand. The item’s category picks the hand: Wieldable Primary is the right hand, Wieldable Secondary the left. It calls the game’s Inventory.EquipItem, the call that ItemEquipable.Invoke makes, so the hand’s OnDropped and OnTaken callbacks run as in the game. It refuses (returns false, changes nothing) when the item has no hand category, when the main grid holds no single unowned unit of it, when the hand’s slot rules refuse the item, when the hand holds an owned item, when the item or the other hand is two-handed and the other hand is full, when the mod id is invalid, during a save or a load, or from an ItemAdded or ItemRemoved handler nested deeper than 8 levels.
  • UnequipHeld(modId, leftHand) moves the item in that hand into the main grid, through the game’s Inventory.Unequip. It refuses an empty hand, an owned item (the game does not let the player take off an owned worn item) and an Unstorable item (the game shows no Unequip button for it). It returns true only when the hand is empty afterwards, so a full main grid gives false and the item stays in the hand.
  • Hats, faces, watches and backpacks are not held items. Use Wardrobe.Equip and Wardrobe.Unequip, which use the same game call.
  • An equip or unequip made through the framework raises ItemAdded for each stack the game adds to a player slot, and no ItemRemoved, because the game clears the source stack without its take method. A swap into an occupied hand raises two ItemAdded. Count does not change, so count the stacks rather than summing the events.
  • EquipHeld does not run the item action’s side effects: ItemEquipable.Invoke first calls the base Item.ItemAction.Invoke, which plays the use sound and raises the ItemConsumed event and the achievement check, and then plays the item sound. The facade skips all of them. Owned stacks are refused, so the owner theft check in the base call does not apply.
  • ItemAdded and ItemRemoved (payload ItemChangedArgs: ItemId, Count) fire from the game’s slot add and take methods, filtered to player slots (main grid, backpack, equipment slots). Give and Take raise one event per item id for the whole operation (Take(id, 5) raises one ItemRemoved(id, 5)). Game code that adds or removes items itself (pickups, crafting, consuming) still raises one event per game call, and crafting or selling removes one unit per call, so sum the counts.
  • Inventory drag and drop, and the item transfers started from the container screen raise no events. Equips and unequips made through EquipHeld, UnequipHeld, Wardrobe.Equip and Wardrobe.Unequip raise ItemAdded only (see the held item bullets above). Shift-click moves out of a container raise ItemAdded, and moves into a container raise nothing. The ground drop from the drag area, removals that bypass the slot take method (quality or liquid removals, Storage.RemoveItem on a backpack) raise no event either. The framework does not gate ItemAdded or ItemRemoved on a save load (see Save loads). Treat the events as a feed of script and gameplay changes and use Count for the truth.
  • Give and Take return false when they are called from an ItemAdded or ItemRemoved handler that is nested deeper than 8 levels. The first refusal is logged once. Give and Take also return false while a trader trade runs (see traders.md).
  • If the game’s own add finds the inventory full it drops the rest on the ground. Those dropped units are not counted: ItemAdded reports only what went into the inventory. Give also checks fit before it adds.

ItemStackInfo

ItemStackInfo one stack of items

PlayerInventory.Items, Containers.GetItems and Containers.TakeStack give you this. It is a snapshot of one stack, taken when you asked.

public sealed class ItemStackInfo
{
    public int ItemId { get; }
    public int Count { get; }
    public int Slot { get; }             // storage slot index for GetItems and TakeStack, -1 when none
    public int OwnerId { get; }          // -1 for the player's own items
    public float Quality { get; }        // -1 when the item has no quality data
    public int IntegrityPercent { get; } // -1 when the item does not spoil
    public int LiquidItemId { get; }     // -1 when there is no liquid
    public float LiquidAmount { get; }
    public bool IsStolen { get; }        // the stack's own stolen flag, see below
    public bool HasOwnerPermission { get; }
    public float Charge { get; }         // -1 when the item has no ammo data
    public float MaxCharge { get; }      // -1 when the item has no ammo data
    public float WalletCoins { get; }    // -1 when the item has no wallet data
    public int WalletOc { get; }         // -1 when the item has no wallet data
    public bool IsWritable { get; }      // true when the item has writable data
    public string WrittenTitle { get; }  // null when none
    public string WrittenText { get; }   // null when none
}
  • Slot is the storage slot index of a stack from GetItems, its position in storageSlots, and is the value to pass as slot to Containers.TakeStack or PlayerShops.Sell. It is -1 for stacks not in a container slot: PlayerInventory.Items, StackActions.StolenStacks and trash piles from Cleaning.TrashItems. Dishwasher contents from Dishwashers.Contents come from GetItems, so they carry the index too. No framework code reads Slot; those two calls take the index as their own argument and refuse -1.
  • The game stores no spoil timer. IntegrityPercent is the game’s own integrity (ItemPerishabledata.GetIntegrityPercent), which falls by the item’s loss per hour.
  • LiquidAmount is the game’s ItemLiquidData.GetLiquidAmount for the stack size.
  • IsStolen is the stack’s own ItemStolenData.playerStolen flag. The game sets it when a stack with an owner id of 2 or more enters the player’s inventory with no owner permission, whether or not a witness saw it; a witness only decides whether the theft crime is also committed. It does not show a crime, a witness or an owner’s claim, so it is not the game’s crime state. The game’s mental-health expectation reads it for a used item.
  • HasOwnerPermission is true when the stack has an ItemOwnerPermission with ownerPermission set. Every meta entry is checked. The class defaults ownerPermission to true, so presence usually means permitted. allowUse is not exposed.
  • Charge and MaxCharge are the game’s ItemAmmodata.ammo and maxAmmo for the stack.
  • WalletCoins is ItemOSCoinWalletData.coins, and WalletOc is its ToOC(), the game’s rounded OC value.
  • IsWritable is true when the stack has ItemWritableData, so a note with no title can be told apart from a stack with no writable data. WrittenTitle and WrittenText are the game’s title and content.

Containers

ContainerInfo a snapshot of one container

Containers.All, Containers.FindByGuid and the Opened and Closed events give you this.

public sealed class ContainerInfo
{
    public string Guid { get; }
    public int InstanceId { get; }
    public string Name { get; }          // the game's GetTitle(), which adds " (Empty)" when empty
    public Vector3 Position { get; }
    public int Apartment { get; }        // -1 when not in an apartment
    public int OwnerId { get; }          // the NPC owner, -1 when none
    public bool NpcOwned { get; }        // owner id > 1, the game's "Steal" condition
    public bool Locked { get; }
}

Containers finding, reading and changing containers

This is the class you start from. All and FindByGuid find containers, GetItems lists what is inside, Give and Take move items, and Opened and Closed tell you when the player opens or closes one.

public static class Containers
{
    public static List<ContainerInfo> All();
    public static ContainerInfo FindByGuid(string guid);
    public static string GuidOf(ContainerInfo container);
    public static List<ItemStackInfo> GetItems(ContainerInfo container);
    public static bool Give(ContainerInfo container, int itemId, int count);
    public static bool Take(ContainerInfo container, int itemId, int count);
    public static ItemStackInfo TakeStack(ContainerInfo container, int slot, int count);
    public static bool IsNotNpcOwned(Storage storage);
    public static bool IsLocked(Storage storage);
    public static bool IsTenantOwned(Storage storage);
    public static bool IsInPlayerTenement(Component component);
    public static bool IsPlayerOwned(Storage storage);
    public static int ApartmentOf(Component component);

    public static event Action<ContainerInfo> Opened;
    public static event Action<ContainerInfo> Closed;
}
  • All finds every active Storage in the active scene with FindObjectsByType, which scans the whole scene, then builds a snapshot per container. Call it once and cache the result; do not call it every frame. Shops, vending machines, prisons and the equipped backpack’s storage are included; check Name. ContainerInfo is a snapshot: Locked and Name are not updated, but Give and Take check the lock again.
  • GetItems reads the game’s storageSlots, skips empty slots, and sets each stack’s Slot to its index there. Pass info.Slot to TakeStack or PlayerShops.Sell to take that exact stack.
  • Give refuses when the container is locked or when no slot can hold the whole amount (checked against every slot). Otherwise it calls the game’s Storage.AddItem, which places into slot 0 and its siblings. In an unusual slot layout that call could still drop the overflow in the world. Giving to an NPC-owned container is allowed, as in the game.
  • Take refuses when the container is locked, when it is NPC-owned and PermissionToTake is false, or when it does not hold the whole amount. Otherwise it calls the game’s Storage.RemoveItem and refreshes the container’s slots. The removed items are destroyed, not moved: nothing is added to the player’s inventory, so a mod that wants a transfer must PlayerInventory.Give the same amount itself, and should do so first. The game shows Steal for NPC-owned containers, but its theft path is not a public method a facade can call, so the facade refuses rather than bypass it.

  • TakeStack(container, slot, count) takes count items from the stack in slot slot, the index in the game’s Slots order, and returns that stack as an ItemStackInfo with its owner and meta. It returns null when the container is locked, is NPC-owned and PermissionToTake is false, the slot is out of range or empty, the stack holds fewer than count, or a save is busy. As with Take, the items are destroyed, not moved to the player. Pass ItemStackInfo.Slot from GetItems as slot; -1 is refused. It also returns null when the slot’s stack is no longer the one GetItems reads (the container’s slots were rebuilt), so list again before taking.
  • IsNotNpcOwned(storage) answers only that no NPC owns the container. It is not a permission check and it does not mean the container belongs to the player. It is the negation of ContainerInfo.NpcOwned: it is true when the container’s StorageOwner is null or its id is 1 or less, which is the game’s own rule for showing Steal on the use prompt, minus the private flag that hides that text. A shop shelf, a vending machine, a prison locker and the player’s own crates have no owner and all report true, so do not use it on its own to decide whether a mod may take items. An NPC-owned container that grants PermissionToTake reports false even though Containers.Take and Containers.TakeStack allow it, and Containers.Give does not read ownership at all. A null storage reports false and throws nothing.
  • IsLocked(storage) is the container’s own lock flag, and false for a null storage. It is the same check Give and Take make, so a hotkey can test it before it calls them.
  • IsTenantOwned(storage) is true when the apartment the container sits in is rented to its resident right now. It reads the live tenement state, so it answers for any rented apartment, not only the player’s. A null storage gives false, and so do the building itself (apartment 0) and apartment 1, the player’s own flat.
  • IsInPlayerTenement(component) is true when the object is in apartment 1, the player’s own flat. Every other apartment gives false, and so does a null object.
  • IsPlayerOwned(storage) is true when the container is not owned by an NPC and sits in the player’s flat. It is a guess built from those two answers, never a permission check, and the game keeps no plain ownership record for a container that would make it exact. Do not use it to decide whether a mod may take items: Take and TakeStack make that decision themselves.
  • IsParkedFurniture does not exist. The game has no parked furniture, so there is nothing for it to report.
  • ApartmentOf(component) returns the apartment number of the tenement prefab above the component, or of its FurnitureManager when the component has no prefab of its own, and -1 when neither exists or the component is null. ContainerInfo.Apartment already carries this value for a container; use ApartmentOf for furniture, doors and other objects.
  • FindByGuid(guid) returns the container whose save GUID is guid, or null. It first uses the game’s GUID registry, the same lookup SaveData.CaptureByGuid and SaveData.ApplyByGuid use. The game fills that registry only for objects it matched to an entry in the loaded save, so when the registry has no live Storage for the GUID, FindByGuid scans the live scene with SaveController.GetObjectByGUID(guid, typeof(Storage)). That fallback calls FindObjectsOfType once, so each miss costs one whole-scene scan; do not call it every frame. It returns null for an empty or null GUID (no scan), for an unknown GUID, and for a destroyed container.
  • GuidOf(container) returns container.Guid, or null for null. An empty string means the container has no GUID.
  • Opened and Closed fire once per open and once per close of a Storage container, and carry a ContainerInfo snapshot built as All builds it. Opened fires when the game opens the container’s storage. Closed fires when the game closes it, or when the container is disabled or destroyed while open (a scene change). The framework tracks the one container it has open, so a repeated close raises nothing, and opening a container while another is open raises Closed for the old one first. Use container.Guid (or GuidOf) to identify it on another PC.
  • Close paths: the menu close (the game’s HideGameMenu, which runs for the “Pause Menu” binding while a menu is open, the menu’s own close buttons and the other close calls) and the container’s own exit (crafting, vending machines, trades and backpacks close their storage themselves) both raise Closed. Walking away is not a separate path: while a container menu is open the game disables movement, so the player cannot walk away from it. A container opened without a menu is closed by its owner, which the exit hook covers.
  • Opened and Closed also cover the storages behind crafting stations, vending machines, trades and the equipped backpack, because the game opens them through the same Storage methods. LiquidStorage (bottles and tanks) has its own enter and exit and raises nothing.
  • Opened and Closed mean the storage was entered or exited, not that a container menu is visible. Storage.EnterExternally runs its side effects, including setting Storage.active and the Opened raise, even when no menu shows. The menu is shown only when the isInteractable | forceOpen check at Storage passes.
  • Backpack, cooler bag and rucksack storages, crafting input and output storages, and trade and vending storages fire them too.
  • With container A open, opening the backpack raises Closed(A), then Opened(backpack). Closing the backpack raises Closed(backpack), then Opened(A), because BackpackStorage.CloseBackpack re-interacts with the last used container. A lock-while-open mod sees A close and reopen on every backpack toggle.
  • A mod that tracks world containers should ignore entries with an empty Guid. Those have no save identity, so FindByGuid cannot return them and another PC cannot match them.
  • Handlers run synchronously on the main thread. A handler that throws is logged and the other handlers still run. The framework does not gate these events on a save load (see Save loads).

Example

This is a whole mod. Once a save has loaded it counts the player’s apples, gives two, takes two back, then reads the stacks, the right hand and the containers. Replace the mod id and the item title with your own.

using ObenseuerCore;

[ModInfo("example.inventoryreport", "Inventory Report", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public sealed class InventoryReport : FrameworkMod
{
    protected override void OnLoad()
    {
        Events.WhenReady(Report);
    }

    private void Report()
    {
        ItemDef apple = Items.Find("Apple");
        if (apple == null)
        {
            Log.Warn("The Apple item was not found, see the limits section of the Item lookup page");
            return;
        }

        Log.Info("count " + PlayerInventory.Count(apple.Id) + " has1 " + PlayerInventory.Has(apple.Id, 1) + " free " + PlayerInventory.FreeSlots + "/" + PlayerInventory.Capacity);
        if (PlayerInventory.Give(apple.Id, 2)) Log.Info("gave 2 apples, count now " + PlayerInventory.Count(apple.Id));
        else Log.Warn("The give was refused, see the limits section of the Player inventory page");
        if (PlayerInventory.Take(apple.Id, 2)) Log.Info("took 2 apples back, count now " + PlayerInventory.Count(apple.Id));
        else Log.Warn("The take was refused, see the limits section of the Player inventory page");
        Log.Info("stacks " + PlayerInventory.Items().Count + " held right " + PlayerInventory.GetHeldItemId(false));
        Log.Info("containers " + Containers.All().Count);
    }
}

Expected: give adds 2 and the count goes up by 2, then take takes them back and the count returns. A false means the call was refused, and the example logs a warning. See Limits for the reasons a call gives false.

Save loads

ItemAdded and ItemRemoved are not gated on a save load, so a slot add or take made while a save loads still fires. A handler that should ignore events raised while a save loads can check SaveData.Busy, which is true while a save is loading or being written.


Table of contents