Skip to content

Auto-Modifier GE System

Overview

MIP uses a family of auto-generating UGameplayEffect subclasses to apply attribute bonuses from equipment and title/knowledge unlocks. Instead of hand-authoring modifier lists in the editor, designers fill in data arrays; the GE regenerates its SetByCaller modifier slots automatically on every save.

At runtime, UMIPAbilitiesBPFL::AccumulateMag collapses the stat arrays into a TMap<FName, float>, which is then injected into a GE spec and applied to the ASC. All GEs are Infinite — they live exactly as long as the source (equipped item or active title) is active.

All paths below are relative to Plugins/ModularInventoryPlus/Source/ModularInventoryPlus/.


Key Classes & Files

Class / Struct File
UMIPBaseAutoModifierGE Public/AbilitySystem/GameplayEffects/MIPBaseAutoModifierGE.h/.cpp
UMIPEquipmentGE Public/AbilitySystem/GameplayEffects/MIPEquipmentGE.h/.cpp
UMIPTitleKnowledgeGE Public/AbilitySystem/GameplayEffects/MIPTitleKnowledgeGE.h/.cpp
UMIPBaseEquipmentAttributesApplierComponent Public/Equipment/MIPBaseEquipmentAttributesApplierComponent.h/.cpp
UMIPEquippedTitleKnowledgeComponent Public/Player/MIPEquippedTitleKnowledgeComponent.h/.cpp
UMIPEquipmentPieceAsset Public/Inventory/DataAsset/MIPEquipmentPieceAsset.h
UItemPiece_EquipmentDefinition Public/Inventory/Pieces/Equipment/ItemPiece_EquipmentDefinition.h/.cpp
UMIPAbilitiesBPFL Public/BPFL/MIPAbilitiesBPFL.h/.cpp
Equipment stat structs Public/Inventory/Structs/EquipmentData.h

UMIPBaseAutoModifierGE

Abstract base for all auto-generating GEs. Subclasses implement OnGenerateModifiers() to call AddModifier() for each stat entry. The four editor lifecycle hooks (PostEditChangeProperty, PostCDOCompiled, PostSaveRoot, PostLoad) all call the same GenerateModifiers() preamble, which empties the modifier array and calls OnGenerateModifiers().

AddModifier(Tag, ModOp) per call:

  • Ability tags (Tag_Attribute_Abilities match) → skipped; handled as loose tags at runtime.
  • Deduplication guard: if an FGameplayModifierInfo for the same FGameplayAttribute + ModifierOp already exists, the call is a no-op. Only one GE modifier slot exists per attribute+op pair.
  • Otherwise: creates a SetByCallerFloat modifier keyed by GetModNameWithPrefix(Tag, ModOp).

Warning

The deduplication means the GE modifier array always has at most one slot per attribute+op. Multiple data entries with the same attribute+op are merged into that slot's magnitude at apply time by AccumulateMag — not in the GE itself.


UMIPEquipmentGE

Concrete subclass used for equipment pieces. OnGenerateModifiers() iterates:

  1. StaticItemAttributeDefinitions from UMIPEquipmentPieceAsset
  2. DynamicItemAttributeDefinitions (range stats)
  3. Class-based trait attributes (sets bHas*TraitAttributes CDO flags for runtime use)

CDO flags (runtime-read, not editor-authored):

Flag Meaning
bHasAddTraitAttributes GE has an Additive trait modifier slot
bHasMultiplicativeTraitAttributes GE has a MultiplyAdditive trait modifier slot
bHasDivisionTraitAttributes GE has a DivideAdditive trait modifier slot
bHasMultiplyCompoundTraitAttributes GE has a MultiplyCompound trait modifier slot
bHasAddFinalTraitAttributes GE has an AddFinal trait modifier slot

UMIPTitleKnowledgeGE

Concrete subclass used for title and knowledge bonuses. OnGenerateModifiers() iterates StaticAttributes (a TArray<FStaticItemAttributeDefinition> defined directly on the GE blueprint).

Designer workflow:

  1. Create a Blueprint subclass of UMIPTitleKnowledgeGE.
  2. Fill StaticAttributes with attribute entries (non-Ability tags only).
  3. Save — modifiers are auto-generated.
  4. Assign the blueprint to FMIPTitleKnowledgeInfo::StatAttributesGE in the Title & Knowledge settings.

Ability-tagged entries in StaticAttributes are silently skipped during GenerateModifiers but are applied as loose gameplay tags at runtime by UMIPEquippedTitleKnowledgeComponent.


Data Structures

FStaticItemAttributeDefinition

Fixed-value stat entry. Same value on every instance.

Field Type Notes
AttributeTag FGameplayTag e.g. Attribute.Offensive.Attack.PhysicalAttack
AttributeEffect float Raw magnitude value
ModifierOp EGameplayModOp::Type See modifier op table below

FItemAttributeDefinition

Range stat — rolled at item generation time.

Field Type Notes
AttributeTag FGameplayTag
MinAttributeEffect / MaxAttributeEffect float Rolled value stored on UItemPiece_DynamicEquipmentItemStats
ModifierOp EGameplayModOp::Type

FAttributeModifier / FRandomizedAttributeModifier

Runtime representation of a rolled dynamic stat.

struct FAttributeModifier {
    FGameplayTag         AttributeTag;
    float                AttributeEffect;   // actual rolled value
    EGameplayModOp::Type ModifierOp;
    bool                 bIsAbilityStat;    // true → loose tag, skips GE
};

struct FRandomizedAttributeModifier {
    FAttributeModifier AttributeModifier;
    float              Precision;
};

Modifier Ops & GAS Math

GAS evaluates attributes using this formula (from EGameplayModOp):

FinalValue = ((Base + AddBase) * MultiplyAdditive / DivideAdditive * MultiplyCompound) + AddFinal
Op EGameplayModOp GAS bias Behavior when multiple mods exist
AddBase Additive (0) 0.0 Summed and added to base before any multiplier
MultiplyAdditive Multiplicitive (1) 1.0 Percentage bonuses sum: two ×1.2 mods → ×1.4
DivideAdditive Division (2) 1.0 Divisors sum: ÷2 and ÷2 → ÷3 (not ÷4)
MultiplyCompound MultiplyCompound (4) Multipliers chain: ×1.5 then ×2.0 → ×3.0
AddFinal AddFinal (5) 0.0 Summed and added after all multipliers

MultiplyAdditive vs MultiplyCompound

MultiplyAdditive is the standard ARPG flat-% model: two +50% sources give +100% total, not +125%. Use MultiplyCompound only when you intentionally want each bonus to multiply the already-buffed value.

Designer authoring convention — the - 1.0 rule

For MultiplyAdditive and MultiplyCompound, GAS treats 1.0 as the identity (no change). The tooltip always displays AttributeEffect - 1.0 as the visible bonus:

You author Tooltip shows Meaning
1.0 +0% no bonus
1.2 +20% 20% more
2.0 +100% double
5.0 +400% 5× total
6.0 +500% 6× total

Rule: to show +X% in the tooltip, author 1 + X/100 as the AttributeEffect.
e.g. to display +500%, set AttributeEffect = 6.0 (not 5.0).

This applies equally to MultiplyAdditive and MultiplyCompound. AddBase and AddFinal are flat values with no bias — author them as-is.


Magnitude Accumulation (AccumulateMag)

Because the GE has one slot per attribute+op, all stat entries for the same attribute+op are collapsed into one SetByCaller float before the spec is applied. UMIPAbilitiesBPFL::AccumulateMag handles this correctly for every op.

static void UMIPAbilitiesBPFL::AccumulateMag(
    TMap<FName, float>& InMags,
    const FName&        InModName,
    EGameplayModOp::Type InModOp,
    float               InEffect
);

Rules per op:

Op Accumulation Rationale
MultiplyCompound existing *= InEffect Mirrors GAS chain-multiply
MultiplyAdditive, DivideAdditive First entry stored raw; subsequent entries add (InEffect - 1.0) The bias-delta trick: one combined SetByCaller value produces the same result as N separate modifier slots
AddBase, AddFinal existing += InEffect Bias = 0, so raw sum is correct

Example — two MultiplyAdditive entries (×1.2 and ×1.3), base 10:

Slot 1: Add(ModName, 1.2)           → stored = 1.2
Slot 2: 1.2 += (1.3 - 1.0)  = 0.3  → stored = 1.5

GAS SumMods: 1.0 + (1.5 - 1.0) = 1.5
Result: 10 × 1.5 = 15   ✓  (not 25 as naïve += would give)

Example — two MultiplyCompound entries (×1.5 and ×2.0), base 10:

Slot 1: Add(ModName, 1.5)           → stored = 1.5
Slot 2: 1.5 *= 2.0                  → stored = 3.0

GAS applies single compound ×3.0
Result: 10 × 3.0 = 30   ✓  (not 35 as naïve += would give)

SetByCaller Key Naming

GetModNameWithPrefix(AttributeTag, ModOp) produces a deterministic FName used as the bridge between the GE modifier slot and the runtime magnitude injection. The same name is written to DataName in GenerateModifiers and used as the key when calling SetSetByCallerMagnitude just before spec application.

Prefix map:

Op Prefix
Additive A
Multiplicitive M (or similar)
Division D
MultiplyCompound MC
AddFinal AF

Example: Attribute.Offensive.Attack.PhysicalAttack + MultiplyCompound"MC_Attribute.Offensive.Attack.PhysicalAttack"


Equipment Apply Flow

Client  →  ServerEquipItem (RPC)  →  AddEquippedItemEntry
                                   OnItemEquippedDelegate
                             UMIPBaseEquipmentAttributesApplierComponent
                             ApplyEquipmentItemAttributes_Implementation
                              ┌──────────────┴──────────────┐
                              │                             │
                        Ability stats               Regular stats
                        ApplyAbilityStat()          AccumulateMag() per entry
                        AddLooseGameplayTagStack    → TMap<FName, float> Mags
                              │                             │
                              └──────────────┬──────────────┘
                          ApplyEffectToWithNameCallerMagnitude(ASC, GE, Mags)
                            • MakeOutgoingSpec
                            • SetSetByCallerMagnitude per FName
                            • ApplyGameplayEffectSpecToTarget
                                  FActiveGameplayEffectHandle
                                  stored in ActiveEquipmentEffects

Detailed Call Chain

[Client]
EquipItemOnServer(int32 ItemIndex)
[Server RPC]
ServerEquipItem_Implementation(ItemIndex)
EquipItem → ProcessEquipItem → AddEquippedItemEntry
    • Sets storage tag to "PlayerEquipped"
    • Adds to EquippedItemList (replicated)
    └─► OnItemEquippedDelegate
    ApplyEquipmentItemAttributes_Implementation  [ROLE_Authority only]
        EquipmentDefinition = ItemInstance->GetOrSetEquipmentDefinitionPiece()
        DynamicStatsPiece   = ItemInstance->GetOrSetDynamicEquipmentItemStatsPiece()
    ApplyEquipmentAttributes(PS, EquipDef, DynStats, bApply=true)
        ① Static stat magnitudes
              For each FStaticItemAttributeDefinition:
                bIsAbilityStat → ApplyAbilityStat(ASC, Tag, Value, true)
                otherwise      → AccumulateMag(Mags, ModName, ModOp, Value)

        ② Dynamic stat magnitudes
              For each FRandomizedAttributeModifier:
                bIsAbilityStat → ApplyAbilityStat(ASC, Tag, Value, true)
                otherwise      → AccumulateMag(Mags, ModName, ModOp, Value)

        ③ Class-based Trait magnitudes (if CDO flags set)
              GenerateClassBasedTraitsMags(Op, Mags)

        ④ Apply GE spec
              ApplyEffectToWithNameCallerMagnitude(ASC, GE class, Mags, Level=1)
              ActiveEquipmentEffects.Add(EquipmentDef, Handle)

        ⑤ Equipment set
              TryApplyEquipmentSet(ItemInstance, PC)

Equipment Unequip Flow

Client  →  ServerUnequipItem (RPC)  →  RemoveEquippedItemEntry
                                     OnItemUnequippedDelegate
                             UMIPBaseEquipmentAttributesApplierComponent
                             ReverseEquipmentItemAttributes_Implementation
                               ActiveHandle = ActiveEquipmentEffects[EquipDef]
                               ASC->RemoveActiveGameplayEffect(Handle)
                               ActiveEquipmentEffects.Remove(EquipDef)
                               RemoveAbilityStats()         ← loose tags
                               TryRemoveEquipmentSet()

Title / Knowledge Apply Flow

UMIPEquippedTitleKnowledgeComponent mirrors the equipment applier pattern exactly.

ServerEquipTitleKnowledge_Implementation(Tag)
    ├─► Spam guard (1 s cooldown)
    ├─► Validate ownership (HasTitle or HasKnowledge)
    ├─► RemoveStats()     ← remove previously active GE + ability tags
    ├─► ApplyStats(Tag)
    │       Info = UMIPTitleAndKnowledgeSettings::Get().FindInfo(Tag)
    │       GECDO = Info->StatAttributesGE->GetDefaultObject<UMIPTitleKnowledgeGE>()
    │       For each FStaticItemAttributeDefinition in GECDO->StaticAttributes:
    │           Ability tag  → ApplyAbilityStat(ASC, Tag, Value, true)
    │                          AppliedAbilityStats.FindOrAdd(Tag) += Value
    │           otherwise    → AccumulateMag(Mags, ModName, ModOp, Value)
    │       ActiveGEHandle = ApplyEffectToWithNameCallerMagnitude(ASC, GE, Mags, 1)
    ├─► SyncTagToPlayerInfo(Tag)    ← nameplate replication
    ├─► MarkComponentForSave()
    └─► ClientOnEquippedChanged(Tag)

RemoveStats() calls ASC->RemoveActiveGameplayEffect(ActiveGEHandle) and subtracts all AppliedAbilityStats loose tag values.


Ability Stats (Loose Gameplay Tags)

Stats whose AttributeTag matches Tag_Attribute_Abilities never go through the GE. They are applied as loose gameplay tag stacks directly on the ASC:

UMIPAbilitiesBPFL::ApplyAbilityStat(ASC, Tag, Value, bApply);
// bApply = true  → AddLooseGameplayTagStack(Tag, Value)
// bApply = false → RemoveLooseGameplayTagStack(Tag, Value)

Both systems (equipment and title/knowledge) track the applied values explicitly so they can be subtracted correctly on remove.


Equipment Sets

Equipment pieces can belong to a set (UMIPEquipmentSetDataAsset). The component tracks the count of equipped set pieces via loose gameplay tag stacks.

Applying — TryApplyEquipmentSet:

  1. Reads the set tag from EquipmentSetDataAsset.
  2. Increments the set tag count (AddLooseGameplayTag).
  3. Looks up the bonus info for the current piece-count threshold.
  4. Applies set-bonus GameplayEffect(s) via ApplyEffectTo.
  5. Adds loose tags for any set ability stats.

Removing — TryRemoveEquipmentSet:

  1. Reads piece count before decrement.
  2. Decrements the set tag count.
  3. Removes set-bonus GEs (ASC->RemoveActiveGameplayEffectBySourceEffect).
  4. Removes loose tags for set ability stats.

Client Replication

GE application is ROLE_Authority only. Attribute changes replicate automatically via GAS. Loose ability stat tags need explicit client-side sync:

Delegate Handler Action
ClientEquipmentEntryAddedDelegate OnEquippedItem_Client Applies loose ability stat tags
ClientEquipmentEntryRemovedDelegate OnUnequippedItem_Client Removes loose ability stat tags

UMIPEquippedTitleKnowledgeComponent uses ClientOnEquippedChanged (a ClientRPC) to keep EquippedTitleKnowledgeTag in sync on the owning client for UI purposes.


Integration