Replication
How MovesetComponent stays correct under server-only, client-only, and parallel Apply Moveset calls
The plugin is designed so all three call patterns of Apply Moveset — server-only, client-only, both-parallel — work without disconnects, double-initialisation, or special-casing in user code. This page lays out exactly how that's achieved, and the failure modes the design protects against.
Subobject replication, the right way
MovesetComponent is a CDO/BP-added stably-named replicated sub-object. It's added via Add Component → MovesetComponent in the Character Blueprint, which makes it part of the actor's CDO. It's not spawned at runtime.
Why this matters
A runtime-created replicated sub-object causes ContentBlockHeaderInvalidCreate disconnects when both authority and an autonomous proxy create it locally — the names don't match, the actor's replication channel mismatches what authority expects, and the connection is dropped.
A CDO/BP-added stably-named component sidesteps that entirely:
- Authority and all proxies have the component baked into the actor at construction.
- Names match across the wire.
Apply Movesetonly initialises an existing component — it never spawns one.
This is non-negotiable: Apply Moveset will fail with Moveset.Event.Failed if the component doesn't exist on the actor.
Replicated state
MovesetComponent replicates exactly the minimum needed for remote viewers to play the cosmetic montage and for autonomous proxies to stay in sync:
| Field | Replication |
|---|---|
ActiveSelection { Definition, Mode } |
Replicated atomically. OnRep_ActiveSelection auto-initialises the local component on remote sides. |
CurrentAttackTag |
Replicated with COND_SkipOwner. Simulated proxies play the montage from OnRep_CurrentAttack; the autonomous proxy keeps its own predicted value. |
| All other state (cursor, buffer, hitboxes) | Local. Re-derived from inputs + ActiveSelection + CurrentAttackTag on each side. |
OnRep_ActiveSelection is what makes server-only Apply Moveset work for clients — when the selection arrives on a remote side, the OnRep handler auto-initialises the local component, sets up the chooser, attaches the input bindings, and starts the cursor at root. The client doesn't need to call Apply Moveset itself.
RPCs
| RPC | Direction | Purpose |
|---|---|---|
Server_DispatchInput |
Autonomous → Authority | Mirrors autoproxy presses to the server. Necessary because the server's view of a client-controlled pawn has no UEnhancedInputComponent. |
Server_InitializeFromClient |
Autonomous → Authority | Ensures the authority initialises when Apply Moveset only ran on the client. |
Both are reliable, both validate inputs server-side (gate by OwningPawn, validate the input action is one of the active graph's edges).
Hitbox sweeps and authority
Hitbox sweeps run on Authority and AutonomousProxy. Simulated proxies don't simulate sweeps — they're cosmetic only and rely on replicated state to drive their visuals.
The autonomous proxy runs sweeps for client-side prediction (immediate hit feedback for the player who pressed the button). The authority runs them for the truth value that gates damage and replicated state changes.
This means:
- Damage belongs on Authority NetMode Action Modules (server is the only side that should apply damage).
- Hit VFX / sound belongs on
AllNetMode (every viewer needs to see / hear it). - Camera shake belongs on LocallyControlled (only the attacker's screen).
See Action Modules for the full table.
The coalescing problem (and the fix)
On Authority, the server's pose tick can be coarser than animation-frame resolution. A hitbox AnimNotifyState that lasts 8 animation frames can collapse into 8 atomic NotifyBegin → NotifyEnd pairs instead of one long-lived registration. Each atomic registration would have zero Prev → Curr socket delta (registered and unregistered on the same tick), so naive sweep code would never accumulate any movement to sweep through.
The component coalesces same-key registrations into one long-lived hitbox and defers unregister by a couple of TickComponent passes. The sweep pipeline accumulates real Prev → Curr deltas across pose ticks, so authority-side hits register correctly even when NotifyBegin/NotifyEnd collapse.
This is automatic. No project-side wiring required.
The one project-side requirement: set the character mesh's VisibilityBasedAnimTickOption to Always Tick Pose, Refresh Bones in multiplayer scenarios. Otherwise the server's pose tick doesn't fire NotifyBegin at all for off-screen / non-rendered actors and authority-side hit detection silently breaks for unobserved characters.
The teleport guard
If an actor warps (e.g. blink ability, root motion teleport), the next sweep would have a Prev → Curr socket delta of hundreds of metres, sweeping through the entire level and hitting everything between origin and destination. That's wrong.
UMovesetSettings::MaxHitboxStepDistance (default 500cm) caps per-tick prev → curr socket distance. Sweeps exceeding the cap skip the current tick — the hitbox stays registered, the next tick re-baselines from the new socket position. Set to 0 to disable the guard.
Three call patterns
Concrete examples of how each shape ends up correctly initialised everywhere.
Server-only
Server: Apply Moveset(Self, MD_Sword, Mode=, InitialInput=IA_PrimaryAttack)
- Authority initialises the local component.
ActiveSelectionreplicates →OnRep_ActiveSelectionfires on autoproxy + simproxies.- Each remote side initialises symmetrically through the OnRep.
- Authority dispatches
InitialInputonceMoveset.Event.Readyfires.
Client-only
Autonomous Proxy: Apply Moveset(Self, MD_Sword, Mode=, InitialInput=IA_PrimaryAttack)
- Autoproxy initialises the local component.
- Autoproxy fires
Server_InitializeFromClientto the authority. - Authority initialises symmetrically + replicates
ActiveSelection. OnRep_ActiveSelectionfires on simproxies — they initialise.- Autoproxy dispatches
InitialInputonce its localMoveset.Event.Readyfires.
Both-parallel
Server + Autonomous Proxy: Apply Moveset(Self, MD_Sword, Mode=, InitialInput=IA_PrimaryAttack)
- Authority and autoproxy each initialise their local component (idempotent on first call).
ActiveSelectionalready matches on both — replication is a no-op.- Subsequent same-selection calls on either side are no-ops aside from re-dispatching the input.
- Simproxies still initialise via
OnRep_ActiveSelection.
Idempotency rules
Apply Moveset follows these rules on subsequent calls:
| Subsequent call shape | Behaviour |
|---|---|
| Same Definition + Mode as current | No-op aside from re-dispatching the optional InitialInput. |
| Different Mode, same Definition | Routed to RequestSelectionSwap — graph-aware combo-state preservation, respects active Combo Window. |
| Different Definition + same Mode | Routed to RequestSelectionSwap. |
| Different Definition + different Mode | Routed to RequestSelectionSwap (atomic). |
This is what makes Apply Moveset safe to call from any code path (equipment swap, ability activation, AI behaviour) without thinking about whether it's already been called.
Diagnostic logging
When something goes wrong, the component logs at Warning (operational issues — missing chooser entry, no MovesetComponent on target) or Error (corruption — bad replicated state, RPC validation failure). All log lines are prefixed LogMoveset: so a single -LogCmds="LogMoveset Verbose" cmdline gets the full picture.
Moveset.Event.Failed payload's FailureReason carries a tag describing what went wrong:
Moveset.Failure.NoComponent— actor has noUMovesetComponent.Moveset.Failure.BadDefinition— Definition is null or has no DefaultMode.Moveset.Failure.NoChooser— Mode's AnimationChooser is null.Moveset.Failure.BadComponentClass—OverrideComponentClasswas set on Apply Moveset and the actual component is not a subclass of it.