How-To: Write Your Own Filter (Optional)

Goal: add a visibility rule the three built-in classes cannot express - "hide chip damage", "only show text for my current target", "hide numbers from my own summons". One override in Blueprint or C++, and the rule picks up a settings row, a dev-tools checkbox and MCP visibility with no other wiring.

Prerequisites
You completed How-To: Filter Which Pops a Player Sees and have a UCrimsonCombatTextFilterSet assigned on your registry.

1. Declare a tag for it

Add one tag under CrimsonCombatText.Filter - Project Settings > Project > GameplayTags, or your own Config/Tags ini. The tag is the row's identity: it is the settings storage key and the key for the player's overrides, so renaming it resets whatever the player had chosen.

ini
GameplayTagList=(Tag="CrimsonCombatText.Filter.HideChipDamage",DevComment="Hides damage numbers below the player's threshold.")

2. Subclass and override Matches

Answer one question: does this rule describe this event? Return true and the chain applies your MatchResult (or DisabledResult when the player has the row off). Return false and evaluation moves to the next row. You never return a verdict yourself.

Content Browser > Blueprint Class > search CrimsonCombatTextFilter. Open it, override Matches, and read what you need off the Context pin.

A Blueprint filter overriding Matches: break the Context, compare Value against Scalar Parameter, return the result.
Rows must be stateless
A filter instance is shared by every pop that runs through the set - the same rule placements live by. Matches is const for that reason. Do not cache anything about one event on the object.

3. Fill in the row's identity

Add your class as a row on the filter set and set these. Everything except FilterId has a usable default.

PropertyWhy it matters
FilterIdRequired. The tag from step 1. Also the settings key and the override key
DisplayNameLabel shown when no Tag Labels entry exists. Prefer the Tag Labels route below
DescriptionTooltip on the settings row, and the blurb in the dev tools
MatchResult / DisabledResultWhat a match means when the row is on, and when it is off
bUserConfigurableClear it for policy the player should not be able to switch off. The row then stays pinned to bEnabledByDefault
bHasScalarParameter + ScalarRange / ScalarStep / ScalarDefaultAdds a slider beneath the toggle. Read it in Matches from Context.ScalarParameter
Label it without touching the asset
Add an entry for your tag under Project Settings > Crimson > Tag Labels. It wins over DisplayName, is localizable, and lets you rename the setting later without opening the filter set.

4. What the Context gives you

FCrimsonCombatTextFilterContext is built once per event, before the chain runs, and handed to every row unchanged.

FieldMeaning
InstigatorActor / TargetActorWeak pointers, already normalized to the avatar pawn. Frequently null on clients - always check
InstigatorRelationship / TargetRelationshipSelf / Ally / Enemy / Unknown, resolved once via your team hook. A null side is passed to that hook like any other and reads as Ally; Unknown means there was no viewing pawn at all
bViewerIsInstigator / bViewerIsTargetDirect identity checks against the viewing pawn
ContextTagsThe tags the pop was requested with, before style resolution
ValueThe pop's value, sign as the sender used it
EventWorldLocation / bHasEventWorldLocationServer-resolved anchor. Survives a null target
ViewingPawn / ViewingPlayerWho the text is being drawn for
ScalarParameterYour row's tunable - the player's override, or your ScalarDefault
Null instigators are normal
A null actor is not classified separately - it goes to the team hook like any other, and CrimsonTeams answers Friendly for a null side, so those events read as Ally and stay visible. This matters: a GAS effect context carries the ASC's OwnerActor as its instigator, which is null on any client where that actor is not relevant, and null outright for effects built without one. Treating those as their own relationship would make them match no row in the default chain and vanish into DefaultResult - silently, and indistinguishably from a broken plugin. Unknown is produced only when there is no viewing pawn at all.
Actors arrive normalized to the avatar
Both sides are run through UCrimsonCombatTextPlayerSettings::ResolveEventActor before the context is built. A GAS effect context carries the ASC's owner as its instigator - the PlayerState in the standard setup - while the target side is the avatar pawn. Comparing those directly would mean a player's own damage never reads as Self, so both are resolved to the pawn first, via IAbilitySystemInterface with a Controller/PlayerState fallback.
Keep it cheap
Matches runs on every incoming pop, on the game thread, before anything is spawned. The expensive work - the team query - is already done for you and cached in the Context; do not re-query it.

5. Place it in the chain

Add the row to the set and drag it to where it belongs. Position is behavior: a Hide rule near the top pre-empts everything below it, and a Show rule near the top wins before any restriction gets a look.

Verify
Run Crimson.CombatText.DevTools in PIE and open the Filters tab. Your row appears in order with its class name and description, a checkbox, and a slider if it has a scalar - with no change to the dev tools. It also shows up in the settings screen and in the GetFilterChain MCP tool.

See also

  • How-To: Filter Which Pops a Player Sees - the chain, the verdicts, and the built-in rows.
  • How-To: Expose Pop Options as User Settings - how your row reaches the options menu.
  • API Reference - the full UCrimsonCombatTextFilter surface.