Skip to main content

Saving and loading

EDInventorySave serialises a container to bytes and reads it back, with versioning and migration so a save written by an older build still loads.

Saving​

TArray<uint8> Blob;
EDInventorySave::SaveContainer(*Container, Blob);

That captures the entries, their stack counts, per-stack state, dynamic tags and slot addresses. Promoted item instances are saved too.

Loading​

FEDDemoSubsystemAllocator Allocator(UEDInventorySubsystem::Get(this));
EDInventorySave::LoadContainer(*Container, Blob, Allocator);

Loading needs a handle allocator because handles are server-allocated identity. Passing the world subsystem's allocator means loaded items get handles that cannot collide with anything allocated since. Restoring raw handles instead is how you end up with two items that are the same item.

Loading replaces the container's contents and broadcasts a Reset change, so observers re-read rather than trying to diff.

A whole inventory, bags included​

For anything that owns an inventory — a player, an NPC, a corpse — save the inventory, not its containers:

EDInventorySave::SaveInventory(*Inventory, Save->InventoryBytes);
// ... later
EDInventorySave::LoadInventory(*Inventory, Save->InventoryBytes, Allocator);
Container-at-a-time saving loses bags

This is not a convenience wrapper. A container an item brings with it — the grid inside a worn backpack — does not exist at the moment you load. It appears only once the backpack itself has been restored. Code that collects containers up front, saves each one, and later walks the same list to restore them writes the bag's contents to disk and then has nowhere to put them back. The player loses exactly what they took the trouble to pack, and nothing reports an error.

Keying by container tag has a second failure on top: two backpacks of the same kind are two containers with the same tag, so one of them silently overwrites the other.

SaveInventory records each container's owning item by position within the same save rather than by handle — handles are deliberately re-minted on load, so they cannot survive one — and orders the records so an owner is always written before the container it brings. A bag inside a bag works for the same reason, with no special case.

On load, containers the inventory owns outright are matched by type tag; the ones items bring are found through the restored owning item, which by then has re-opened them.

One item on its own​

SaveItem / LoadItem for something that is not in a container at all — lying on the ground, in flight between systems, attached to something that is not an inventory:

EDInventorySave::SaveItem(WorldItem->ToItemEntry(Handle), Record.Bytes);
EDInventorySave::LoadItem(Record.Bytes, Allocator, RestoredEntry);

It writes the whole entry, so a dropped backpack comes back packed and a worn sword comes back worn. Saving a definition and a count instead is how a world reload hands everybody new equipment.

One container on its own​

SaveContainer / LoadContainer remain the right call for a container that belongs to nothing else — a chest, a stash, a vendor's stock, a loot container that has to persist between sessions:

EDInventorySave::SaveContainer(*Chest->GetContainer(), Blob.Bytes);
EDInventorySave::LoadContainer(*Chest->GetContainer(), Blob.Bytes, Allocator);

Pass a TArray<FEDItemHandle>* as the last argument if you need to reconnect anything that referred to those items by handle — it reports the new handle of every restored entry, in save order.

Loading into a container that already has a type​

LoadContainer restores the type stored in the blob. An older save may have none, and blanking a container's type would silently detach every filter and lookup keyed on it — so an empty saved tag leaves the configured one alone.

If you see containers losing their identity after a load, that is the case to check first.

Versioning​

Blobs carry a version, and migration runs on load. Adding a field to an entry means bumping the version and handling the old shape; the header for EDInventorySave documents the current version and what changed.

Replicated formats — the transaction error enum, NetSerialize layouts, the FastArray entry shape — get the same scrutiny for the same reason: changing one breaks saves and network compatibility with running clients.

JSON export​

There is a debug JSON exporter as well. It is for reading a container in a bug report, not for persistence — it is not versioned and not intended to be loaded back.

Where next​