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.
Countcounts the stacks whose item id equalsitemId, in every inventory slot (backpack and character slots included).Hasuses the game’sCheckForItem, which ignores ownership and counts plain item stacks only.Giverefuses (returnsfalse, 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 makesGivereturnfalse. Money ids0and1are refused; useWallet.Takereturnsfalseunless 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 withownerId > 0that is taken while a witness sees it commits theft, following the game’s own crime rule; checkItems()first when that matters. Money is not kept as item stacks, soTakereturnsfalsefor ids0and1; useWallet.Takecounts 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 exactlycountit returnsfalseand 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 whileItemAddedfires, 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. SetDebug.InventoryTakeSelfCheckto log a check of that accounting at start.Takerefuses an id that is a liquid, or that any of the player’s slots holds as a liquid (ItemLiquidData.liquidItem.IDof 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, whileCountcompares 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 orStackActionsfor liquid units.Items()lists the non-empty stacks in every inventory slot, backpack and character slots included.FreeSlotsandCapacitycount the same slots asItems(), so they include the backpack and the equipment slots (hat, watch, hands, backpack slot).FreeSlotsis not the roomGivecan use, which is the main grid only.GetHeldItemIdreturns 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 Primaryis the right hand,Wieldable Secondarythe left. It calls the game’sInventory.EquipItem, the call thatItemEquipable.Invokemakes, so the hand’sOnDroppedandOnTakencallbacks run as in the game. It refuses (returnsfalse, 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 anItemAddedorItemRemovedhandler nested deeper than 8 levels.UnequipHeld(modId, leftHand)moves the item in that hand into the main grid, through the game’sInventory.Unequip. It refuses an empty hand, an owned item (the game does not let the player take off an owned worn item) and anUnstorableitem (the game shows no Unequip button for it). It returnstrueonly when the hand is empty afterwards, so a full main grid givesfalseand the item stays in the hand.- Hats, faces, watches and backpacks are not held items. Use
Wardrobe.EquipandWardrobe.Unequip, which use the same game call. - An equip or unequip made through the framework raises
ItemAddedfor each stack the game adds to a player slot, and noItemRemoved, because the game clears the source stack without its take method. A swap into an occupied hand raises twoItemAdded.Countdoes not change, so count the stacks rather than summing the events. EquipHelddoes not run the item action’s side effects:ItemEquipable.Invokefirst calls the baseItem.ItemAction.Invoke, which plays the use sound and raises theItemConsumedevent 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.ItemAddedandItemRemoved(payloadItemChangedArgs:ItemId,Count) fire from the game’s slot add and take methods, filtered to player slots (main grid, backpack, equipment slots).GiveandTakeraise one event per item id for the whole operation (Take(id, 5)raises oneItemRemoved(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.EquipandWardrobe.UnequipraiseItemAddedonly (see the held item bullets above). Shift-click moves out of a container raiseItemAdded, 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.RemoveItemon a backpack) raise no event either. The framework does not gateItemAddedorItemRemovedon a save load (see Save loads). Treat the events as a feed of script and gameplay changes and useCountfor the truth. GiveandTakereturnfalsewhen they are called from anItemAddedorItemRemovedhandler that is nested deeper than 8 levels. The first refusal is logged once.GiveandTakealso returnfalsewhile 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:
ItemAddedreports only what went into the inventory.Givealso 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
}
Slotis the storage slot index of a stack fromGetItems, its position instorageSlots, and is the value to pass asslottoContainers.TakeStackorPlayerShops.Sell. It is -1 for stacks not in a container slot:PlayerInventory.Items,StackActions.StolenStacksand trash piles fromCleaning.TrashItems. Dishwasher contents fromDishwashers.Contentscome fromGetItems, so they carry the index too. No framework code readsSlot; those two calls take the index as their own argument and refuse -1.- The game stores no spoil timer.
IntegrityPercentis the game’s own integrity (ItemPerishabledata.GetIntegrityPercent), which falls by the item’s loss per hour. LiquidAmountis the game’sItemLiquidData.GetLiquidAmountfor the stack size.IsStolenis the stack’s ownItemStolenData.playerStolenflag. 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.HasOwnerPermissionis true when the stack has anItemOwnerPermissionwithownerPermissionset. Every meta entry is checked. The class defaultsownerPermissionto true, so presence usually means permitted.allowUseis not exposed.ChargeandMaxChargeare the game’sItemAmmodata.ammoandmaxAmmofor the stack.WalletCoinsisItemOSCoinWalletData.coins, andWalletOcis itsToOC(), the game’s rounded OC value.IsWritableis true when the stack hasItemWritableData, so a note with no title can be told apart from a stack with no writable data.WrittenTitleandWrittenTextare the game’stitleandcontent.
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;
}
Allfinds every activeStoragein the active scene withFindObjectsByType, 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; checkName.ContainerInfois a snapshot:LockedandNameare not updated, butGiveandTakecheck the lock again.GetItemsreads the game’sstorageSlots, skips empty slots, and sets each stack’sSlotto its index there. Passinfo.SlottoTakeStackorPlayerShops.Sellto take that exact stack.Giverefuses when the container is locked or when no slot can hold the whole amount (checked against every slot). Otherwise it calls the game’sStorage.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.-
Takerefuses when the container is locked, when it is NPC-owned andPermissionToTakeis false, or when it does not hold the whole amount. Otherwise it calls the game’sStorage.RemoveItemand 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 mustPlayerInventory.Givethe same amount itself, and should do so first. The game showsStealfor 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)takescountitems from the stack in slotslot, the index in the game’sSlotsorder, and returns that stack as anItemStackInfowith its owner and meta. It returnsnullwhen the container is locked, is NPC-owned andPermissionToTakeis false, the slot is out of range or empty, the stack holds fewer thancount, or a save is busy. As withTake, the items are destroyed, not moved to the player. PassItemStackInfo.SlotfromGetItemsasslot; -1 is refused. It also returnsnullwhen the slot’s stack is no longer the oneGetItemsreads (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 ofContainerInfo.NpcOwned: it istruewhen the container’sStorageOwneris null or its id is 1 or less, which is the game’s own rule for showingStealon 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 reporttrue, so do not use it on its own to decide whether a mod may take items. An NPC-owned container that grantsPermissionToTakereportsfalseeven thoughContainers.TakeandContainers.TakeStackallow it, andContainers.Givedoes not read ownership at all. A null storage reportsfalseand throws nothing.IsLocked(storage)is the container’s own lock flag, andfalsefor a null storage. It is the same checkGiveandTakemake, so a hotkey can test it before it calls them.IsTenantOwned(storage)istruewhen 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 givesfalse, and so do the building itself (apartment0) and apartment1, the player’s own flat.IsInPlayerTenement(component)istruewhen the object is in apartment1, the player’s own flat. Every other apartment givesfalse, and so does a null object.IsPlayerOwned(storage)istruewhen 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:TakeandTakeStackmake that decision themselves.IsParkedFurnituredoes 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 itsFurnitureManagerwhen the component has no prefab of its own, and-1when neither exists or the component is null.ContainerInfo.Apartmentalready carries this value for a container; useApartmentOffor furniture, doors and other objects.FindByGuid(guid)returns the container whose save GUID isguid, ornull. It first uses the game’s GUID registry, the same lookupSaveData.CaptureByGuidandSaveData.ApplyByGuiduse. The game fills that registry only for objects it matched to an entry in the loaded save, so when the registry has no liveStoragefor the GUID,FindByGuidscans the live scene withSaveController.GetObjectByGUID(guid, typeof(Storage)). That fallback callsFindObjectsOfTypeonce, so each miss costs one whole-scene scan; do not call it every frame. It returnsnullfor an empty or null GUID (no scan), for an unknown GUID, and for a destroyed container.GuidOf(container)returnscontainer.Guid, ornullfornull. An empty string means the container has no GUID.OpenedandClosedfire once per open and once per close of aStoragecontainer, and carry aContainerInfosnapshot built asAllbuilds it.Openedfires when the game opens the container’s storage.Closedfires 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 raisesClosedfor the old one first. Usecontainer.Guid(orGuidOf) 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 raiseClosed. 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. OpenedandClosedalso cover the storages behind crafting stations, vending machines, trades and the equipped backpack, because the game opens them through the sameStoragemethods.LiquidStorage(bottles and tanks) has its own enter and exit and raises nothing.OpenedandClosedmean the storage was entered or exited, not that a container menu is visible.Storage.EnterExternallyruns its side effects, including settingStorage.activeand theOpenedraise, even when no menu shows. The menu is shown only when theisInteractable | forceOpencheck atStoragepasses.- 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), thenOpened(backpack). Closing the backpack raisesClosed(backpack), thenOpened(A), becauseBackpackStorage.CloseBackpackre-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, soFindByGuidcannot 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.