Skip to main content
Version: 1.0.0

Items and inventories

Items live in three places: in a character's or NPC's inventory, in a container such as a chest, and on the ground as a dropped item. The SDK keeps all three in sync for every player. Players move items with the game's own menus, and the server checks and applies each move. A server mod can give, take and move items itself, which is what this page is mostly about.

MyMod.Server/GoldIncome.cs
using OblivionMp.Sdk.Common.Archetypes;
using OblivionMp.Sdk.Common.Archetypes.Mixins;
using OblivionMp.Sdk.Serverside.Items;
using ReadyM.Relay.Common.Oblivion.ECS.Values;
using ReadyM.Relay.Common.Oblivion.Items;
using ReadyM.SDK.Attributes;
using ReadyM.SDK.Server.Entities;

namespace MyMod.Server;

// Pays every player 10 gold a minute, straight into their inventory.
[Service]
public sealed partial class GoldIncome(IEntities entities, Inventories inventories)
{
private const float IntervalSeconds = 60f;

private float _elapsed;

private void Update()
{
_elapsed += Time.DeltaTime;
if (_elapsed < IntervalSeconds)
return;

_elapsed -= IntervalSeconds;

foreach (var main in entities.Query<MainCharacter>())
inventories.Add(main.As<Equipment>(), new InventoryEntry(Items.Gold, 10, 0));
}
}

How it works​

Inventories​

An inventory is a list of InventoryEntry values. Two mixins hold one:

MixinOnAlso holds
EquipmentMainCharacter, Npc, ServerSummonCurrentPoison, the poison on the equipped weapon
ContainerItemsContainer (see World and cells)

Both implement IInventory, so the same code works on a player, a corpse and a chest. Ask the entity for the mixin with As<Equipment>() or As<ContainerItems>() and pass that on.

Each entry is one stack:

FieldMeaning
FormIdWhat the item is, as a NetworkFormId. See Identity.
CountHow many.
WornNot 0 while the item is equipped.
HealthThis stack's condition, or InventoryEntry.NoInstanceOverride (-1) when it has the item's default or no condition at all.
ChargeThis stack's enchantment charge, or -1 the same way.

new InventoryEntry(formId, count, worn) makes an entry with the default condition and charge.

To read an inventory, walk it by index:

var arrows = 0;
for (var i = 0; i < main.ItemsCount; i++)
{
var entry = main.GetItems(i);
if (entry.FormId == Items.Ammunition.Arrow1Iron)
arrows += entry.Count;
}

IInventory also has AddItems, InsertItems and RemoveAtItems, which change the list one entry at a time. Inventories (below) is usually what you want instead: it merges stacks and leaves equipped items alone.

Naming items​

Items lists the game's items by their editor ids, as FormId values:

CatalogExample
Items.Goldgold coins
Items.WeaponsItems.Weapons.WeapIronLongsword
Items.Armors, Items.ClothesItems.Armors.DEMHeavyHelmCataclysm1
Items.AmmunitionItems.Ammunition.Arrow1Iron
Items.Potions, Items.Ingredients, Items.Scrolls, Items.SoulGemsItems.Potions.PotionRestoreHealth
Items.Books, Items.KeysItems.Books.DEM01MythicNote1

A FormId turns into a NetworkFormId wherever one is asked for, so new InventoryEntry(Items.Gold, 10, 0) and entry.FormId == Items.Gold both work. For an item the catalogs do not list, such as one from another plugin, write its form id: new FormId(0x0000000F).

Dropped items​

An item on the ground is a DroppedItem:

MemberMeaning
BaseIdWhat the item is.
StackSizeHow many lie there.
Position, RotationWhere, and which way it faces.
Durability, ChargeIts condition and enchantment charge, or -1 for the defaults. These come from ItemExtraData.
RefIdThe reference the game knows it by.
IsDisabledSet while the item is out of the world: it starts disabled in the game files, or a player took it.
ParentCellKind, ParentCellX, ParentCellYThe cell it lies in.

When a player picks up an item someone dropped, the entity is deleted. When they pick up an item that was placed in the game files, the entity stays, with IsDisabled set, so the item can come back when the cell respawns. Items from the game files only exist as entities on a server that seeds the world (see World seeding).

On the client​

Read the local player's inventory from players.LocalMainCharacter, as above. A client cannot change an inventory directly; ask the server to, with an RPC.

IItems drops an item next to the local player:

MyMod.Client/PotionDrop.cs
using System.Numerics;
using OblivionMp.Sdk;
using ReadyM.Relay.Common.Oblivion.Items;
using ReadyM.SDK.Attributes;

namespace MyMod.Client;

[Service]
public sealed partial class PotionDrop(IPlayers players, IItems items)
{
public void DropPotions()
{
if (players.LocalMainCharacter is { } me)
items.SpawnDroppedItem(Items.Potions.PotionRestoreHealth, 3, me.Position + new Vector3(0, 0, 50));
}
}

The item lands in the cell the local player is in. SpawnDroppedItem throws an InvalidOperationException before the local character exists or while the player is in no area.

note

Not in 1.0 yet: dropping an item from the client with a specific condition or enchantment charge. IItems.SpawnDroppedItem takes neither. The server can do it, by creating the DroppedItem itself (below).

On the server​

Ask for Inventories to move stacks with the game's rules:

MethodWhat it does
Add(inventory, entry)Adds the entry, merged into a matching stack that is not equipped.
TryTake(inventory, stack)Removes the stack, unequipped copies first. Returns false and removes nothing if there are too few.
TryMove(from, to, stack)Takes the stack from one inventory and adds it to the other. Returns false and moves nothing if from has too few.
CountCarried(inventory, stack)How many of the stack the inventory holds, not counting equipped ones.

TryTake, TryMove and CountCarried name the stack with an ItemStackData, which matches on the item, its condition and its charge together. Set Durability and Charge to -1 for items in their default state, or they will not match:

var price = new ItemStackData
{
BaseId = Items.Gold,
Count = 100,
Durability = InventoryEntry.NoInstanceOverride,
Charge = InventoryEntry.NoInstanceOverride,
};

var purse = main.As<Equipment>();
if (inventories.TryTake(purse, price))
inventories.Add(purse, new InventoryEntry(Items.Weapons.WeapSilverLongsword, 1, 0));

To put an item on the ground, create a DroppedItem in a cell, with a ref id from ServerRefIds (see World and cells for finding the cell):

var item = entities.Create<DroppedItem>(cell, cell.MasterClient);
item.RefId = refIds.Next();
item.BaseId = Items.Weapons.WeapIronLongsword;
item.StackSize = 1;
item.Position = position;
item.Durability = 50f;
item.Charge = InventoryEntry.NoInstanceOverride;

ContainerRoll fills a container from its template, the way the game rolls a chest's loot: fixed items as they are, leveled lists rolled at a level. Roll(template, level, into) takes the level; Roll(template, visitor, into) uses the level of that player's character, raised for a merchant's container by their Mercantile skill. Both add to what the container already holds. The templates are described in Spawning.

IdWhat it meansHow to fix it
READYM019Client code changes a synced inventory, for example with AddItems.Change it on the server, from an RPC the client sends.
READYM002Create is called inside a foreach over a query, for example to drop an item next to every player.Collect what you need in the loop and create the entities after it.

Reference​