Inventory_X Update! — v1.1
Major Architecture Update
A ground-up refresh of the fragment data model, replication boundary, and client prediction system. Focused on performance and stronger anti-cheat guarantees.
Highlights
New fragment data model
single source of truth (FragmentEntries asset-or-inline)
Runtime vs Static fragment lifecycle
new introduced static fragments now live on the definition only, eliminating per-item replication for read-only data and reducing network bandwidth on heavy inventories.
Hardened anti-cheat boundary
Closed an item-duplication attack vector.
Handle-preserving rollback
chained predicted operations no longer drop dependent ops during reconcile. Fixes a long-standing prediction edge case in cross-inventory transfers.
+22 new automation tests across registry, prediction, rollback journal, and Layer2 transfer paths.
Cleaner public API
explicit GetRuntime*, GetStatic*, GetEffective* family. Old generalist API removed.
Changelog (UE 5.1 - 5.7)
Data Model
New
- FInventoryItemFragmentEntry — a single entry that is either an asset reference (reusable fragment definition) or inline data (one-off, completely defined in the item).
- EInventoryFragmentLifecycle enum on every fragment, with two values:
- RuntimePerItem — creates a per-item record, replicates, participates in snapshots/merge (legacy default behavior).
- StaticDefinition — definition-side only, never replicated, never per-item — perfect for read-only data shared across all instances of an item type.
- UInventoryItemFragmentDefinition extended with Lifecycle (defaults to RuntimePerItem for backward-compatible behavior).
Removed
- TArray<TObjectPtr<UInventoryItemFragmentDefinition>> FragmentDefinitions on UInventoryItemDefinition.
Why it matters
Static fragments (e.g., ammo type, weapon category, item rarity) no longer pay replication cost or per-item allocation cost. Runtime fragments (e.g., durability, current ammo, condition) keep their existing per-item replicated semantics.
API
Old names have been removed in favor of explicit Runtime/Static/Effective families:
- GetItemFragments -> GetRuntimeItemFragments, GetStaticItemFragments, GetEffectiveItemFragments
- GetFragmentByTag -> GetRuntimeFragmentByTag, GetStaticFragmentByTag, GetEffectiveFragmentByTag
- HasFragment -Z HasRuntimeFragment, HasStaticFragment, HasEffectiveFragment
- TryGetDurabilityFragment -> TryGetRuntimeDurabilityFragment, TryGetStaticDurabilityFragment, TryGetEffectiveDurabilityFragment
- TryGetAmmoFragment -> Same Runtime/Static/Effective split
- TryGetConditionFragment -> Same Runtime/Static/Effective split
- GetFragmentsForItem -> GetRuntimeFragmentHandlesForItem
- Internal_CreateFragment -> Internal_CreateRuntimeFragment
New view types
- FInventoryStaticFragmentView — static fragment data, no handle (handles are runtime-only).
- FInventoryEffectiveFragmentView — unified view with Source = RuntimeRecord | StaticDefinition discriminator and a bIsMutableRuntime flag.
FInventoryFragmentHandle remains valid only for runtime fragments. No fake handles are ever generated for static fragments.
Replication, Snapshot, Drop/Pickup
- Trusted snapshot (ExportItemSubtreeSnapshot/ImportItemSubtreeTrusted) carries only runtime fragments. Static fragment data is derived server-side and client-side from DefinitionId.
- Drop actor PickupableItemComponent replicates only PresentationFragments (runtime-only). UI consumers should query GetEffective* to render full per-item state including static contributions.
- Layer2 cross-inventory bridge snapshots are runtime-only; static data is resolved per-endpoint from the resolved definition.
- Snapshot import now rejects any fragment payload whose FragmentTypeTag does not correspond to a RuntimePerItem spec on the resolved definition (defense against client-crafted snapshots).
Stack Rules & Validation
- InventoryStackValidator::ValidateMerge (Layer1) and InventoryInteractionStackValidator::ValidateMerge (Layer2) compare only runtime fragments when bRequireMatchingFragmentsForMerge is true.
- Two stacks of the same definition with only static fragments always merge cleanly — static fragments are equal by construction.
- Layer2 merge no longer requires both endpoints to resolve the definition: as long as one endpoint resolves and DefinitionId matches, the merge proceeds. Fixes spurious rejections under asset streaming / hot-reload scenarios.
Security & Anti-Cheat
- AddItem and RemoveItem are strictly server-authoritative. The validation gate enforces bServerInitiated == true, and Server_SubmitOperation_Implementation deliberately does not propagate that flag from client RPCs. Client autonomous proxies cannot directly create or destroy items via RPC.
- Closed an item-duplication attack vector. A bypass flag introduced during an early version was identified as a regression and removed. The flag had allowed client-side validation to accept arbitrary AddItem predictions; it has been fully eliminated, and explicit anti-pattern documentation has been added to the architecture guide.
- New security tests confirm that RequestOperation(AddItem) and RequestOperation(RemoveItem) from ROLE_AutonomousProxy:
- return Rejected immediately at local validation;
- never enqueue a pending prediction;
- never emit a Server_SubmitOperation RPC.
Client Prediction
Predicted operations now:
- create only runtime fragment records (static specs do not allocate);
- rollback removes only the runtime records they created (handle-preserving);
- replay correctly preserves source item identity through chained operation reconciliation.
Handle-Preserving Rollback (new in this version)
A subtle bug in Layer2 chained predictions has been fixed. Previously, when a client predicted two transfers in a fast sequence (e.g., move backpack to other inventory, then move weapon into backpack's child container), the authoritative reconciliation of the first operation would re-import the destroyed source items with new handles, breaking the second operation's source resolution and silently invalidating it.
The fix:
- New CreateRecordWithExistingHandle registry primitive that re-uses original handle + generation across destroy/restore cycles.
- Trusted seam variants (ImportItemSubtreePredictedPreservingHandles, ImportItemSubtreeTrustedPreservingHandles) that preserve identity through rollback.
- Symmetric application to both predicted rollback and trusted (server-side transaction failure) rollback for architectural consistency.
- Atomic prevalidation prevents partial registry mutations.
Idempotency Hardening
The rollback idempotency check has been upgraded from a presence-only check to a full tri-state structural verification:
- NotPresent -> proceed with handle-preserving import.
- PresentEquivalent -> idempotent skip (handles + structure match snapshot exactly).
- PresentMismatch -> fail loudly with audit log instead of silent success.
Conservative fallback for runtime fragments: if any item in the snapshot carries fragment data, the idempotency shortcut is bypassed
Performance
- Definition-side fragment cache is array-first: contiguous TArray<FInventoryResolvedFragmentSpec> with linear-scan helpers. No TMap for tag lookup — small fragment counts make linear scan cache-friendlier than hash maps with allocations and pointer chasing.
- Cache prepared in PostLoad and PostEditChangeProperty, not re-resolved on every query.
- Repeated GetEffectiveFragmentSpecs() calls return stable storage (verified by automation test) — zero re-resolution cost on hot paths like tooltip rendering.
- Static-heavy inventories (e.g., shops, world containers with read-only items) see significantly reduced FragmentFastArray size on both server and client.
- Network bandwidth reduction: items with only StaticDefinition fragments replicate zero fragment records.
Test Coverage
Significant suite expansion:
- +11 prediction tests covering Move/Split/Merge/Equip rollback, runtime fragment preservation, and security boundaries.
- +9 rollback journal tests including all 4 structural drift detection cases (Quantity, Placement, ParentContainer, ChildContainerLink), runtime fragment fallback, and idempotency happy path.
- +6 trusted seam tests for handle-preserving import/export.
- +3 registry tests for CreateRecordWithExistingHandle (revive after release, duplicate id rejection, generation preservation).
- Reconciliation suite cleaned: removed orphan tests and updated old tests
Final test status: 117/117 broad Layer2 tests + 9/9 rollback journal + 4/4 handle preserve regression — all green
Bug Fixes
- Chained Layer2 predictions silently invalidated — fixed via handle-preserving rollback re-import.
- Idempotent rollback could mask state corruption — replaced presence-only check with structural verification + audit logging.
- Partial subtree presence treated as success — now correctly classified as PresentMismatch and fails with explicit log.
- Layer2 merge spurious rejection under asset streaming — fallback resolution path uses whichever endpoint resolves the definition (both items must already share DefinitionId).
Internal Improvements
- Internal_CreateFragment renamed to Internal_CreateRuntimeFragment for consistency with the runtime-only contract.
- LookupCache.ItemToFragments is documented and enforced as runtime-only.
- New FInventoryResolvedFragmentSpec cache type unifies runtime, static, and effective spec views on the definition.
- Editor UX: when BaseFragmentAsset is set, inline fields are hidden via EditCondition/EditConditionHides to make the asset-or-inline contract visually unambiguous.
- PostEditChangeProperty clears inline fields when an asset is assigned to prevent zombie data.