Equipment System¶
Summary: Server-authoritative equipment management using fragment-driven behavior. Equipment slots use GameplayTags, visuals rebuild from the replicated equipment container on every instance, and GAS effects are applied through a unified context pattern.
Table of Contents¶
- Architecture Overview
- Core Concepts
- Equipment Flow
- Fragment Context Pattern
- Slot Management
- Visual Actors
- GAS Integration
- Combat Integration
- Replication Strategy
- Public Contracts
- Related Systems
- Recent Changes
Architecture Overview¶
UEquipmentComponent (on AEternalPlayerState - replicates to all players, survives pawn death)
│
├── EquipmentContainer (UItemContainerComponent - single store for equipped items)
│ └── Slot ↔ grid mapping: GetGridIndexForSlot() / GetSlotForGridIndex()
│ └── Queries: GetEquippedItemBySlot(), GetItemGuidForSlot(), GetAllEquippedItems()
│
├── Character equipment meshes (visual representations)
│ └── Skeletal mesh components created directly on the character per slot
│
└── Fragment Handlers
└── Each fragment type processed via FFragmentEquipContext
Key Design Principles¶
| Principle | Implementation |
|---|---|
| Server Authority | All equip/unequip via Server RPCs |
| Fragment-Driven | Behavior defined by item fragments, not equipment component |
| Context Pattern | Fragments receive FFragmentEquipContext with all needed references |
| Single Store | EquipmentContainer (a UItemContainerComponent) is the sole authoritative store for equipped items; slots map to fixed grid indices |
| Visual Separation | Equipment meshes are visual-only; the container holds the logical state |
Core Concepts¶
PlayerState Ownership¶
Equipment lives on AEternalPlayerState (created in its constructor, exposed via IEquipmentOwner::GetEquipmentComponent) because:
- Equipped gear must replicate to other players, not just the owner
- Equipment persists across pawn death/respawn
- Matches the System Ownership Matrix (long-term player identity data → PlayerState)
Fragment-Driven Architecture¶
The equipment component doesn't know how to equip items. Instead: 1. Component orchestrates the flow 2. Fragments define their own equip/unequip behavior 3. Context object provides all needed references
This means adding new equipment types (e.g., mounts, companions) only requires new fragments.
Equipment Flow¶
Equip Sequence¶
┌──────────────────┐
│ Client: UI Click │
└────────┬─────────┘
│
▼
┌────────────────────────────┐
│ Server_EquipItem(Item,Slot)│ ← Server RPC
└────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Atomic container transaction │ ← EquipTransactionDepth guards
│ ├─ Validate item ownership │ intermediate states
│ ├─ GatherDisplaced() occupants │
│ ├─ Park/move displaced items │
│ └─ Place item in EquipmentContainer│
└────────┬────────────────────────────┘
│ Container contents replicate (FastArray);
│ OnRep drives the same handler on clients
▼
┌──────────────────────────────────┐
│ OnEquipmentContainerChanged() │ ← Runs on server AND all clients
│ ├─ ClearAllVisuals() │
│ ├─ RefreshAllVisuals() │ → ProcessItemEquip per equipped item
│ ├─ RefreshHandWeaponCache() │ → weapon-changed broadcasts
│ └─ ReconcileUnequippedItems() │ → server-only teardown
└────────┬─────────────────────────┘
│
▼
┌────────────────────────────┐
│ ProcessItemEquip() │
│ ├─ BuildEquipContext() │
│ ├─ HandleEquipFragment() │ → Visuals (all instances)
│ ├─ HandleWeaponFragment() │ → Combat setup, hit trace
│ └─ HandleAbilityFragment()│ → Grant abilities (server only)
└────────────────────────────┘
Unequip Sequence¶
There is no explicit unequip path per removal reason. Any route that takes an item out of its slot in the container (Server_UnequipItem, drag to bag, world drop, swap eviction, quick action) converges on the same reconcile:
Item leaves its slot in EquipmentContainer
│ (container change → OnEquipmentContainerChanged)
▼
ReconcileUnequippedItems() ← server-only; AppliedEquipSlots is the source of truth
│
▼
ProcessItemUnequip()
├─ BuildEquipContext()
├─ RemoveEquipmentEffects(Context)
├─ HandleWeaponFragmentOnUnEquip()
└─ HandleAbilityFragmentOnUnEquip()
(Visuals are handled separately: every instance clears and rebuilds
equipment meshes from the container in OnEquipmentContainerChanged.)
Fragment Context Pattern¶
The Problem (Before)¶
Fragments needed to look up references repeatedly:
// Every fragment did this
UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(PC->GetPawn());
The Solution (Now)¶
Context built once, passed to all fragments:
FFragmentEquipContext
├── PlayerController* ← Owning player's controller (resolved from the PlayerState)
├── OwningPawn* ← Current pawn
├── AbilitySystem* ← Cached ASC
├── EquipmentSlot ← Target slot tag
└── EquippedItem* ← The item being equipped
Benefits¶
- Single point of reference lookup (
BuildEquipContext()) - Fragments don't need to know how to get references
- Easier testing - can mock the context
- Future-proof - add more context without changing fragment signatures
Source Reference¶
- Context struct:
ItemFragment.h:27-68 - Context builder:
EquipmentComponent.cpp→BuildEquipContext()
Slot Management¶
Slot Tags¶
Equipment slots are GameplayTags, not enums:
Every slot tag lives under the single flat GameItems.Equipment.Types.* family — there is no per-category
(.Armor., .Weapon.) split in the slot namespace.
| Slot | Tag |
|---|---|
| Head | GameItems.Equipment.Types.Head |
| Chest | GameItems.Equipment.Types.Chest |
| Belt | GameItems.Equipment.Types.Belt |
| Feet | GameItems.Equipment.Types.Feet |
| Hands | GameItems.Equipment.Types.Hands |
| Right Hand | GameItems.Equipment.Types.RightHand |
| Left Hand | GameItems.Equipment.Types.LeftHand |
| Amulet | GameItems.Equipment.Types.Amulet |
| Left Ring | GameItems.Equipment.Types.LeftRing |
| Right Ring | GameItems.Equipment.Types.RightRing |
| Consumable | GameItems.Equipment.Types.Consumable |
| Edible | GameItems.Equipment.Types.Edible |
Do not confuse with
GameItems.Equipment.Armor.Types.*(Helmet, Chest, Legs, Boots, Gloves). That family describes an armor piece's kind, not a slot — it is a separate axis and never appears as a slot tag.
Both families are defined in Source/ProjectEternal/Private/Inventory/Items/ItemTags.cpp.
Item Class Tags¶
Items are classified by a GameItems.Class tag:
| Class | Tag | Description |
|---|---|---|
| Weapon | GameItems.Class.Weapon |
All weapon types |
| Armor | GameItems.Class.Armor |
Body armor pieces |
| Accessory | GameItems.Class.Accessory |
Rings, amulets |
| Consumable | GameItems.Class.Consumable |
Usable items |
These tags drive modifier spawn eligibility and UI filtering.
EquipmentContainer (single-store model)¶
Equipped items live in a single UItemContainerComponent (EquipmentContainer) owned by UEquipmentComponent. Each slot tag maps to a fixed grid index inside the container; there is no separate slot struct.
| Method | Purpose |
|---|---|
GetEquippedItemBySlot(SlotTag) |
Returns item in slot or nullptr |
GetItemGuidForSlot(SlotTag) |
Item instance ID for a slot (works on server + clients) |
GetAllEquippedItems() |
All currently equipped items |
GetGridIndexForSlot(SlotTag) / GetSlotForGridIndex(Index) |
Static slot ↔ grid index mapping |
CanEquipItemInSlot(Item, SlotTag) |
Authoritative slot-compatibility rule (shared by UI, inventory controller, drop preview) |
IsHandBlockedByTwoHander(SlotTag) |
Off-hand reserved by an equipped two-hander |
Because the container is the single source of truth, unequip is universal: ReconcileUnequippedItems() tears down any item that had equip effects applied but is no longer in its slot — drag to bag, world drop, swap eviction, and quick actions all reduce to "the item left the container".
Source Reference¶
- Container property + queries:
EquipmentComponent.h→EquipmentContainer,GetEquipmentContainer() - Tag definitions:
ItemTags.h→GameItems::Equipment::Armor
Visual Actors¶
Character-Attached Meshes (no separate actor)¶
There is no AEquipActor. Equipment visuals are USkeletalMeshComponents created directly on the character, tracked per slot in CharacterEquipmentMeshes (plus CharacterEquipmentVFX for Niagara components). They are local/cosmetic only — never replicated — because every instance rebuilds them from the replicated container:
- Armor uses leader pose (follows character animations)
- Weapons attach to hand sockets (no leader pose)
- Body-part hiding masks (
SlotBodyPartHideMasks/RefreshBodyPartVisibility) prevent the body clipping through armor
Rebuild Flow¶
OnEquipmentContainerChanged() ← server AND all clients
│
├─ ClearAllVisuals()
└─ RefreshAllVisuals()
└─ HandleEquipFragmentOnEquip() per equipped item
├─ CreateCharacterEquipmentMeshes() / SpawnFromVisualConfig()
├─ Attach to skeleton socket for the slot
└─ RefreshBodyPartVisibility()
Source Reference¶
- Mesh tracking:
EquipmentComponent.h→CharacterEquipmentMeshes,CharacterEquipmentVFX - Spawn logic:
EquipmentComponent.cpp→HandleEquipFragmentOnEquip(),SpawnFromVisualConfig()
GAS Integration¶
Dynamic GameplayEffect Pattern¶
Equipment effects are runtime-created UGameplayEffect objects, not Blueprint GE classes. Each modifier becomes a dynamic GE applied to the character's ASC:
FEquipmentFragment::ApplyEquipmentEffects(Context)
│
├─ For each global modifier on the item:
│ ├─ Create dynamic UGameplayEffect (NewObject on ASC)
│ ├─ Set DurationPolicy = Infinite
│ ├─ Add modifier targeting TargetProperty attribute
│ │ ├─ EModifierOperation::Flat → GAS Additive
│ │ └─ EModifierOperation::Increased → GAS Multiplicative
│ └─ ApplyGameplayEffectToSelf → returns ActiveEffectHandle
│
└─ Store handle for cleanup on unequip
Modifier Operations¶
| EModifierOperation | GAS Operation | Stacking Behavior |
|---|---|---|
Flat |
Additive | Direct value addition |
Increased |
Multiplicative (bias 1.0) | Percentages stack additively, not compounding |
The Increased operation uses GAS's bias-adjusted sum so multiple "+10% damage" modifiers yield +20% total, not +21% (compounding). This follows PoE-style "Increased" stacking.
Modifier Source & Apply Gate¶
Each FRolledModifier carries an ECraftingModifierSource tag identifying how it was
acquired. The apply loop consults this before spawning a GE so mechanics like Remnant
Echo Mods can stay attached to an item without applying until their runtime gate allows it.
| Source | Apply Gate |
|---|---|
Drop |
Always applies |
Crafted |
Always applies |
Echo (Remnant) |
Applies only when the item's FRemnantItemState::EchoMods[].bIsActive is true |
Echo Mods are not stored in FEquipmentFragment::Modifiers; they live on the item's
state module (FRemnantItemState::EchoMods, each wrapping a FRolledModifier). The
equipment apply loop treats them uniformly with standard modifiers once the gate passes —
they spawn as regular equipment-owned GE handles, not a parallel handle pool.
RefreshItemModifiers¶
When the gate state changes at runtime (e.g. a Remnant transitioning Sealed → Awakened),
the equipment component re-runs its apply loop for that item via
UEquipmentComponent::RefreshItemModifiers(Item):
- Remove all currently-tracked GE handles for the item
- Re-run the apply loop; modifiers whose gate now passes spawn GEs
This keeps UEquipmentComponent the sole owner of all modifier GE handles for an item,
regardless of source. No dual ownership, no parallel lifecycle management.
Effect Cleanup¶
Effects tracked via handles, removed on unequip:
- ActiveGlobalEffects - Map of modifier ID → active effect handle
Source Reference¶
- Apply effects:
ItemFragment.cpp→FEquipmentFragment::ApplyEquipmentEffects() - Remove effects:
ItemFragment.cpp→FEquipmentFragment::RemoveEquipmentEffects() - Dynamic GE helper:
EternalAbilitySystemLibrary.h→ApplyDynamicModifierEffect() - Modifier operations:
ModifierDefinitions.h→EModifierOperation
Combat Integration¶
Weapon Equip Processing¶
HandleWeaponFragmentOnEquip()
│
├─ Notify PlayerCombatComponent of weapon change
│ └─ Combat component updates its own montages
│
├─ Register hit trace profile
│ └─ Links weapon mesh to trace system
│
├─ Activate animation overlay mode
│ └─ Changes character animation set
│
└─ Add weapon type tags
└─ For animation queries
Key Change: Montage Ownership¶
Before: EquipmentComponent called Combat->SetCombatMontage()
Now: Combat component reacts to OnWeaponChanged() and updates itself
This follows proper component ownership - combat component owns its combat state.
Source Reference¶
- Weapon handling:
EquipmentComponent.cpp→HandleWeaponFragmentOnEquip() - Combat updates:
PlayerCombatComponent.cpp→UpdateCombatMontages()
Replication Strategy¶
What Replicates¶
| Property | Replication |
|---|---|
EquipmentContainer contents |
FastArraySerializer (inside UItemContainerComponent); its OnRep drives client-side visual rebuild |
WeaponTags |
Standard DOREPLIFETIME |
Equipment meshes/VFX do NOT replicate — they are rebuilt locally on every instance from the container.
RPC Pattern¶
There are no multicast RPCs. Clients see equipment changes through container replication:
Client Server All Clients
│ │ │
├─Server_EquipItem()─────▶│ │
│ ├─Validate & atomic container txn │
│ ├─Container FastArray replicates─▶│
│ │ ├─OnRep → OnEquipmentContainerChanged()
│ ├─OnEquipmentContainerChanged() ├─Rebuild visuals, refresh weapon cache
Source Reference¶
- Replication props:
EquipmentComponent.cpp→GetLifetimeReplicatedProps()
Public Contracts¶
Server RPCs¶
| RPC | Parameters | Purpose |
|---|---|---|
Server_EquipItem |
(UItemObject*, FGameplayTag) |
Equip item to slot |
Server_UnequipItem |
(UItemObject*) |
Remove item from slot |
Events¶
| Event | Payload | When Fired |
|---|---|---|
OnItemEquippedEvent |
UItemObject* |
After ProcessItemEquip completes |
OnItemUnEquippedEvent |
UItemObject* |
After ProcessItemUnequip completes |
OnConsumableUsageChangedEvent |
UItemObject* |
When consumable charges change |
Query Methods¶
| Method | Returns | Purpose |
|---|---|---|
GetEquippedItemBySlot(SlotTag) |
UItemObject* |
Get item in specific slot |
GetItemGuidForSlot(SlotTag) |
FGuid |
Item instance ID for a slot (server + clients) |
IsHandBlockedByTwoHander(SlotTag) |
bool |
Off-hand reserved by an equipped two-hander |
GetEquippedWeapons() |
TArray<UItemObject*> |
All equipped weapons |
GetLeftEquippedWeapon() |
UItemObject* |
Left hand weapon |
GetRightEquippedWeapon() |
UItemObject* |
Right hand weapon |
Related Systems¶
- Item Fragments - Fragment types and lifecycle
- Item System - UItemObject and FItemManifest
- Item State Modules - Gate state for Echo Mods and future mechanics
- Remnant Item System - First consumer of the Echo source gate
- Inventory System - Container management
- Combat Overview - Weapon combat integration
- Hit Tracing - Weapon trace registration
- GAS Overview - GameplayEffect application
Recent Changes¶
| Date | Change | Impact |
|---|---|---|
| 2026-08-06 | Slot-tag table corrected to GameItems.Equipment.Types.* |
The table had documented a GameItems.Equipment.Armor.* namespace that no slot uses; added the Edible slot and a warning distinguishing the armor piece-kind family GameItems.Equipment.Armor.Types.* from slots |
| 2026-07-03 | Doc rewrite: ownership + single-store model | Corrected ownership to AEternalPlayerState; replaced deleted FEquippedItemSlots with the EquipmentContainer (UItemContainerComponent) single-store model (slot ↔ grid index mapping); removed nonexistent multicast RPCs and AEquipActor in favor of container-OnRep-driven visual rebuild; refreshed query-method table to the current header API |
| 2026-04 | ECraftingModifierSource-based apply gate + RefreshItemModifiers |
Echo Mods (Remnant) apply only when their state module flag allows. Equipment stays sole owner of all mod GE handles regardless of source. |
| 2026-02 | Dynamic GE pattern | Equipment effects are runtime-created GEs, not Blueprint classes |
| 2026-02 | EModifierOperation (Flat/Increased) | Flat maps to GAS Additive, Increased maps to GAS Multiplicative |
| 2026-02 | Slot refactoring | Leg → Belt, Ring split into LeftRing/RightRing, added item class tags |
| 2025-12-27 | FFragmentEquipContext pattern |
Fragments receive context instead of PlayerController |
| 2025-12-27 | RPC simplification | Separate Server_EquipItem/Server_UnequipItem instead of combined |
| 2025-12-27 | FEquippedItemSlots helpers |
Slot logic encapsulated in struct, not scattered |