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_Abilitiesmatch) → skipped; handled as loose tags at runtime. - Deduplication guard: if an
FGameplayModifierInfofor the sameFGameplayAttribute+ModifierOpalready exists, the call is a no-op. Only one GE modifier slot exists per attribute+op pair. - Otherwise: creates a
SetByCallerFloatmodifier keyed byGetModNameWithPrefix(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:
StaticItemAttributeDefinitionsfromUMIPEquipmentPieceAssetDynamicItemAttributeDefinitions(range stats)- Class-based trait attributes (sets
bHas*TraitAttributesCDO 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:
- Create a Blueprint subclass of
UMIPTitleKnowledgeGE. - Fill
StaticAttributeswith attribute entries (non-Ability tags only). - Save — modifiers are auto-generated.
- Assign the blueprint to
FMIPTitleKnowledgeInfo::StatAttributesGEin 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):
| 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:
- Reads the set tag from
EquipmentSetDataAsset. - Increments the set tag count (
AddLooseGameplayTag). - Looks up the bonus info for the current piece-count threshold.
- Applies set-bonus
GameplayEffect(s) viaApplyEffectTo. - Adds loose tags for any set ability stats.
Removing — TryRemoveEquipmentSet:
- Reads piece count before decrement.
- Decrements the set tag count.
- Removes set-bonus GEs (
ASC->RemoveActiveGameplayEffectBySourceEffect). - 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¶
- Ability System Overview — ASC, attribute sets, GAS basics used throughout.
- Equipment Component — equip/unequip RPC entry points and item validation.
- UMIPEquipmentPieceAsset — designer data source for equipment stats.
- Settings & BPFL —
UMIPTitleAndKnowledgeSettingsandUMIPAbilitiesBPFL.