Concept: Event Flow & Replication

One event type (FCrimsonCombatTextEvent) travels one direction: server to clients. Everything visual happens client-side after arrival. Understanding the pipeline explains every behavior in the plugin.

The pipeline

StepWhereWhat happens
1. BroadcastServerGameplay code calls BroadcastCombatText. Authority is validated; client calls are ignored with a warning.
2. Resolve locationServerIf the caller didn't set WorldLocation (bHasWorldLocation false and location zero), it is resolved from the Target - socket, then offset, then root + 100 cm (How-To: Position Pops).
3. Stamp the ledgerServerThe component resolves the event's style against its own copy of the registry, finds the row's ledger root, and adds the hit to that attacker's running row on the target. LedgerRoot, InstigatorTotal, InstigatorHitCount and bLedgerScopeOpen are written onto the event.
4. SendServerServerCullDistance = 0: one NetMulticast Unreliable RPC to all clients. Greater than 0: a per-client Client Unreliable RPC to each player whose GetFocalLocation() is in range. A scope payoff takes neither path: it goes on a reliable Client RPC to the one PlayerController whose player raised the total, and skips the range test.
5. ReceiveEach clientThe event reaches UCrimsonCombatTextSubsystem::HandleIncomingEvent - via the GameState component's OnCombatTextReceived delegate (multicast path) or directly (culled path).
6. FilterEach clientUCrimsonCombatTextPlayerSettings::PassesFilter runs, if the component exists: distance cap first (all categories, local-pawn targets exempt), then heals, incoming-damage override, crits-only, own/ally/enemy toggles.
7. DisplayEach clientThe request is queued; the tick dequeues it, resolves a style from the registry, and - when the hit belongs to the local player - either punches the live number it belongs to or starts a pooled widget showing the running total. Anyone else's hit becomes a plain pop of its own value.

Bandwidth choices

  • WorldLocation is a FVector_NetQuantize - quantized to 1 cm, plenty for a floating number and much smaller on the wire.
  • PrefixText and CustomText travel with the event; empty strings cost almost nothing, so pay only for what you use.
  • Instigator and Target are serialized as NetGUID references. On a client where the actor is not net-relevant (or not replicated), they resolve to null - the filter then treats the event as third-party.
  • The six ledger fields are two floats, two ints and two bits - a fixed, tiny cost that buys every client the same running total without a second message.
  • Hit events are unreliable: pops are cosmetic, so a dropped packet just means one missing punch instead of retransmission cost. A scope payoff is reliable and addressed to one player, because a payoff nobody sees is worse than a byte saved.

The server owns the arithmetic

Clients never accumulate. UCrimsonCombatTextReplicationComponent keeps the ledger - per target, per ledger root, one running row for each attacker identity - and every event carries that attacker's total as of that hit. Only the attacker's own client merges it into a climbing number.

That is what makes a number trustworthy: it was computed once, on the server, for one player. A dropped packet or a player who joined mid-fight costs one missing punch rather than a number that is permanently wrong - the next hit carries the correct running total and the display snaps to it.

A dedicated server loads the registry itself
Resolving a style is how the server knows which ledger root a hit belongs to, and the display subsystem that normally async-loads the registry is never created on a dedicated server. The replication component therefore loads RegistryPath on its first authoritative broadcast. Leave that setting unset and the server logs once and stamps nothing - every hit arrives as a plain, unmerged pop.

Display-only by design

UCrimsonCombatTextSubsystem is never created on dedicated servers or in non-game worlds (editor previews, thumbnails), and every entry point re-checks the net mode lazily - so a PIE world running as a dedicated server also displays nothing. The GameState component self-registers with the local subsystem in BeginPlay (RegisterEventSource), so there is no polling and no manual binding.

WorldLocation is a network fallback, not the display anchor
The server resolves FCrimsonCombatTextEvent::WorldLocation before sending so that a client which cannot see the target actor still has somewhere to put the pop.

It is not where the text ends up. Each client runs its own placement, which prefers the live target actor and falls back to this location only when that actor is not net relevant. Two clients can legitimately place the same event differently - see How-To: Position Pops.
Listen servers and singleplayer
A listen server's local player is a client of the multicast like any other, and in singleplayer the 'server' and the only client are the same process - the one GameState-broadcast path covers every net mode. The subsystem's direct RequestCombatText exists for purely local, no-replication use (offline tools, menus).
Never trust pops as gameplay state
Events are fire-and-forget cosmetics. If a system needs to know damage happened, read it from your authoritative gameplay state - not from a combat text delegate.