[UE 5.8.3][Gameplay] ACharacter::CheckJumpInput: "CanJump() && UseLegacyDoJump ? A : B" parses as "(CanJump() && UseLegacyDoJump) ? A : B", so DoJump is called when CanJump() is false

Summary

ACharacter::CheckJumpInput (Character.cpp:1591 in 5.8.3) reads:

const bool bDidJump = CanJump() && (CharacterCVars::UseLegacyDoJump)? CharacterMovement->DoJump(bClientUpdating) : CharacterMovement->DoJump(bClientUpdating, DeltaTime);

&& binds tighter than ?:, so this is (CanJump() && UseLegacyDoJump) ? DoJump(a) : DoJump(a, DeltaTime). CanJump() does not gate the call. It only picks a branch, and both branches call a DoJump. Two effects:

  1. DoJump(bool, float) is called when CanJump() is false.
  2. With the default p.UseLegacyDoJump 1, the deprecated one-argument overload runs when the jump is allowed and forwards DeltaTime = 0. The two-argument overload gets the real DeltaTime only when the jump is refused.

What Type of Bug are you experiencing?

Gameplay

Steps to Reproduce

Measured in an automation test in a game module, using only engine classes plus two small subclasses. The movement component’s DoJump only logs and calls Super:

UCLASS() class UReproCMC : public UCharacterMovementComponent { GENERATED_BODY()
public:
  virtual bool DoJump(bool bReplayingMoves, float DeltaTime) override {
    const bool bCan = CharacterOwner && CharacterOwner->CanJump();
    const bool bResult = Super::DoJump(bReplayingMoves, DeltaTime);
    UE_LOG(LogTemp, Display, TEXT("DoJump DeltaTime=%.3f CanJump()=%d returned=%d"), DeltaTime, bCan, bResult);
    return bResult; } };
UCLASS() class AReproCharacter : public ACharacter { GENERATED_BODY()
public:
  AReproCharacter(const FObjectInitializer& OI) : Super(OI.SetDefaultSubobjectClass<UReproCMC>(ACharacter::CharacterMovementComponentName))
  { GetCharacterMovement()->GetNavAgentPropertiesRef().bCanCrouch = true; } };
  1. Spawn AReproCharacter on a floor, possess it with a locally controlled APlayerController, and let it settle on the ground.
  2. Standing: call Jump() and tick the movement component once at 0.05 s.
  3. Crouched: call Crouch(), tick, then Jump() and tick once at 0.05 s. CanJump() is false while crouched.
  4. Repeat both with p.UseLegacyDoJump 0.

Expected Result

When CanJump() is false, DoJump is not called, which is what CanJump() && reads as. The overload and DeltaTime do not depend on whether the jump is allowed.

Observed Result

(one 0.05 s tick each)

  • p.UseLegacyDoJump 1 (default), standing: CanJump() true, DoJump called, DeltaTime 0.000, launched.
  • p.UseLegacyDoJump 1, crouched: CanJump() false, DoJump called, DeltaTime 0.050, returned false.
  • p.UseLegacyDoJump 0, standing: DoJump called, DeltaTime 0.050, launched.
  • p.UseLegacyDoJump 0, crouched: CanJump() false, DoJump still called, DeltaTime 0.050, returned false.

Affects Versions

5.8

Platform(s)

Mac

Additional Notes

Low severity. The stock UCharacterMovementComponent::DoJump re-checks CanJump(), so a stock Character behaves correctly, and the doc comment says DoJump checks it. It matters for an override that acts before calling Super. I found it while overriding DoJump to stand a crouched character up before a jump: the override has to re-check CanJump() itself, because it runs on refused jumps too. Suggested fix: parenthesise as CanJump() && (UseLegacyDoJump ? DoJump(a) : DoJump(a, DeltaTime)). Verified in the 5.8.3 source and at runtime on macOS, Apple Silicon. I have not checked other versions.