Fix airborne sitting and remote animation playback

This commit is contained in:
Alexander Sellite 2026-08-24 10:36:17 -04:00
parent a1ea341e8c
commit c65176b99f
3 changed files with 105 additions and 9 deletions

View file

@ -2069,13 +2069,13 @@ func _maybe_send_local_animation_action() -> void:
str(action["id"]), str(action["id"]),
int(action["sequence"]), int(action["sequence"]),
bool(action.get("paused", false)), bool(action.get("paused", false)),
avatar.get_network_sitting_state(), avatar.get_network_sitting_intent(),
] ]
if signature == _last_local_animation_action_signature: if signature == _last_local_animation_action_signature:
return return
var encoded: Array = _encode_movement_animation_action( var encoded: Array = _encode_movement_animation_action(
action, action,
avatar.get_network_sitting_state(), avatar.get_network_sitting_intent(),
) )
if encoded.is_empty(): if encoded.is_empty():
return return

View file

@ -1036,7 +1036,13 @@ func _simulate_movement_physics(delta: float) -> void:
or (_network_authoritative_simulation and _network_jump_pending) or (_network_authoritative_simulation and _network_jump_pending)
) )
) )
if _sit_after_landing and is_on_floor(): # Sitting deliberately stops movement processing, so never allow a stale
# grounded flag or a reconciled network state to leave an airborne avatar in
# that early-return path. Preserve the intent and sit once landing is real.
if _sitting and not _can_begin_sitting():
_sit_after_landing = true
_set_sitting(false)
if _sit_after_landing and not jump_requested and _can_begin_sitting():
_sit_after_landing = false _sit_after_landing = false
_set_sitting(true, local_control_enabled) _set_sitting(true, local_control_enabled)
if _sitting: if _sitting:
@ -1489,8 +1495,20 @@ func _update_character_animation() -> void:
_presented_animation_action_id = &"" _presented_animation_action_id = &""
_presented_animation_action_sequence = -1 _presented_animation_action_sequence = -1
_presented_animation_action_paused = false _presented_animation_action_paused = false
if _character_animation_name == next_animation and not action_changed: if (
_character_animation_playback_matches(
next_animation,
action_selected and animation_action_paused,
)
and not action_changed
):
return return
# The animation name is only a selection cache. A remote player's
# AnimationPlayer can occasionally stop or lose its assigned animation while
# its replicated locomotion state remains valid. Reassert the selected
# presentation here instead of waiting for another network state transition;
# movement simulation and replication remain untouched.
_character_animation_player.active = true
_character_animation_player.play(next_animation) _character_animation_player.play(next_animation)
_character_animation_name = next_animation _character_animation_name = next_animation
if action_selected: if action_selected:
@ -1513,6 +1531,26 @@ func _update_character_animation() -> void:
_character_animation_player.pause() _character_animation_player.pause()
func _character_animation_playback_matches(
animation_name: StringName,
paused_action_selected: bool,
) -> bool:
if (
_character_animation_name != animation_name
or _character_animation_player.assigned_animation != animation_name
or not _character_animation_player.active
):
return false
if paused_action_selected:
return true
var animation := _character_animation_player.get_animation(animation_name)
if animation == null or animation.loop_mode == Animation.LOOP_NONE:
# Completed one-shot actions deliberately keep their final pose until the
# authoritative action state advances; they must not be restarted here.
return true
return _character_animation_player.is_playing()
func _on_character_animation_finished(animation_name: StringName) -> void: func _on_character_animation_finished(animation_name: StringName) -> void:
if animation_name == CHARACTER_NET_STRIKE_ANIMATION: if animation_name == CHARACTER_NET_STRIKE_ANIMATION:
if local_control_enabled: if local_control_enabled:
@ -1610,7 +1648,7 @@ func toggle_sitting() -> void:
]) ])
): ):
return return
if not is_on_floor(): if not _can_begin_sitting():
_sit_after_landing = true _sit_after_landing = true
return return
_set_sitting(should_sit, local_control_enabled) _set_sitting(should_sit, local_control_enabled)
@ -1645,7 +1683,15 @@ func is_sitting() -> bool:
return _sitting return _sitting
func _can_begin_sitting() -> bool:
return is_on_floor() and velocity.y <= 0.0
func get_network_sitting_state() -> bool: func get_network_sitting_state() -> bool:
return _sitting
func get_network_sitting_intent() -> bool:
return _sitting or _sit_after_landing return _sitting or _sit_after_landing
@ -1654,7 +1700,9 @@ func apply_network_sitting_state(should_sit: bool) -> void:
func apply_authoritative_network_sitting_state(should_sit: bool) -> void: func apply_authoritative_network_sitting_state(should_sit: bool) -> void:
if should_sit and not is_on_floor(): if should_sit and (
not _can_begin_sitting() or _network_jump_pending
):
_sit_after_landing = true _sit_after_landing = true
_set_sitting(false) _set_sitting(false)
return return
@ -2038,7 +2086,7 @@ func capture_network_input(sequence: int) -> Dictionary:
"sprint": false, "sprint": false,
"sneak": false, "sneak": false,
"slow_walk": false, "slow_walk": false,
"sitting": get_network_sitting_state(), "sitting": get_network_sitting_intent(),
"casting": ( "casting": (
_fishing_visual_phase == FishingVisualPhase.CASTING _fishing_visual_phase == FishingVisualPhase.CASTING
), ),
@ -2063,7 +2111,7 @@ func capture_network_input(sequence: int) -> Dictionary:
"sprint": Input.is_action_pressed("sprint"), "sprint": Input.is_action_pressed("sprint"),
"sneak": Input.is_action_pressed("sneak"), "sneak": Input.is_action_pressed("sneak"),
"slow_walk": Input.is_action_pressed("slow_walk"), "slow_walk": Input.is_action_pressed("slow_walk"),
"sitting": get_network_sitting_state(), "sitting": get_network_sitting_intent(),
"casting": _fishing_visual_phase == FishingVisualPhase.CASTING, "casting": _fishing_visual_phase == FishingVisualPhase.CASTING,
"animation_action": _make_animation_action_state(), "animation_action": _make_animation_action_state(),
} }
@ -2111,7 +2159,7 @@ func get_network_input_state_hash() -> int:
Input.is_action_pressed("sprint"), Input.is_action_pressed("sprint"),
Input.is_action_pressed("sneak"), Input.is_action_pressed("sneak"),
Input.is_action_pressed("slow_walk"), Input.is_action_pressed("slow_walk"),
get_network_sitting_state(), get_network_sitting_intent(),
_fishing_visual_phase == FishingVisualPhase.CASTING, _fishing_visual_phase == FishingVisualPhase.CASTING,
_animation_action_id, _animation_action_id,
_animation_action_sequence, _animation_action_sequence,

View file

@ -38,7 +38,9 @@ func _validate_latency_smoothing() -> void:
_validate_animation_action_ordering(avatar) _validate_animation_action_ordering(avatar)
_validate_transit_estimation() _validate_transit_estimation()
_validate_remote_snapshot_smoothing(avatar) _validate_remote_snapshot_smoothing(avatar)
_validate_remote_locomotion_playback_recovery(avatar)
_validate_reliable_jump_intent(avatar) _validate_reliable_jump_intent(avatar)
_validate_airborne_sitting(avatar)
_validate_stale_input_expiry(avatar) _validate_stale_input_expiry(avatar)
avatar.queue_free() avatar.queue_free()
await process_frame await process_frame
@ -141,6 +143,26 @@ func _validate_compact_animation_encoding() -> void:
) )
func _validate_airborne_sitting(avatar: Player) -> void:
avatar.reset_network_movement_state()
avatar.set("_sit_after_landing", true)
assert(avatar.get_network_sitting_intent())
assert(not avatar.get_network_sitting_state())
assert(not bool(avatar.make_network_snapshot(2)["sitting"]))
assert(bool(avatar.capture_network_input(1)["sitting"]))
# Even a malformed or stale reconciliation that marks an airborne avatar as
# seated must not bypass gravity and freeze it in place.
avatar.set("_sit_after_landing", false)
avatar.call("_set_sitting", true)
avatar.velocity = Vector3(0.0, 4.0, 0.0)
avatar.call("_simulate_movement_physics", 0.1)
assert(not avatar.is_sitting())
assert(bool(avatar.get("_sit_after_landing")))
assert(avatar.velocity.y < 4.0)
avatar.reset_network_movement_state()
func _validate_animation_action_ordering(avatar: Player) -> void: func _validate_animation_action_ordering(avatar: Player) -> void:
var draw := NetworkPlayerAnimationProtocol.make_action_state( var draw := NetworkPlayerAnimationProtocol.make_action_state(
&"draw", 5, 0.2 &"draw", 5, 0.2
@ -352,6 +374,32 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
) )
func _validate_remote_locomotion_playback_recovery(avatar: Player) -> void:
avatar.configure_network_remote(false)
avatar.push_network_snapshot(
_network_snapshot(Vector3.ZERO, Vector3(4.5, 0.0, 0.0), 21)
)
avatar.call("_update_character_animation")
var animation_player := avatar.get_node(
"Visuals/CharacterRig/AnimationPlayer"
) as AnimationPlayer
assert(animation_player.assigned_animation == &"running")
assert(animation_player.is_playing())
# Reproduce issue #111: locomotion replication still says RUNNING (and the
# dust therefore still emits), but the rig playback has fallen idle. The
# local presentation pass must repair that state without another packet.
animation_player.stop()
assert(not animation_player.is_playing())
assert(
int(avatar.get("_network_target_locomotion_state"))
== Player.LocomotionState.RUNNING
)
avatar.call("_update_character_animation")
assert(animation_player.assigned_animation == &"running")
assert(animation_player.is_playing())
func _validate_reliable_jump_intent(avatar: Player) -> void: func _validate_reliable_jump_intent(avatar: Player) -> void:
avatar.set_local_control(true) avatar.set_local_control(true)
avatar.call("_queue_local_network_jump_intent") avatar.call("_queue_local_network_jump_intent")