mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-26 02:11:39 -04:00
Play a body's death animation once instead of restarting it as it falls
The client kept a corpse's animation tree only while the corpse's tr_time stayed the same. The server changes tr_time whenever a body's fall changes: when it goes over an edge, when it slides down a wall, and when it lands. Each change copied the tree again from the victim, whose tree is already gone because a dead player is not posed, so the corpse got a fresh tree with the death clip back at its first frame. Bodies flew off standing upright in the clip's first 50 ms, stood against walls, and then played the whole fall again where they stopped. The server now stamps each corpse slot with the level time of the death (spawn_time_ms, sent with the corpse pool), and the client keys the tree on that time and the victim. Both stay the same for one body's life and change when the slot holds another body. When there is no victim tree to copy (a body first seen late, or an entity number that now holds another body), the fresh tree starts as far into the death clip as the body is old, so an old body shows the pose it already lies in instead of dying again. The corpse pool's wire format changes, so PROTOCOL_VERSION is 117. Seen on the Mac (mp_favela, point-blank SPAS-12 kills of a bot on an open terrace, with temporary logging): every body copies its tree once and its death clip plays once, from the victim's pose to the end. Before, the same kill copied it 18 times and drew the body upright in the clip's first 50 ms for about 0.8 s. On an iPhone build, point-blank shotgun kills on mp_favela no longer glided off upright, stood pinned to walls or replayed their fall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -335,6 +335,7 @@ pub fn corpse_slot_to_entity_state(slot: &PlayerCorpseSlot) -> EntityState {
|
||||
es.number = slot.entnum;
|
||||
es.e_type = ET_PLAYER_CORPSE;
|
||||
es.client_num = i32::try_from(slot.victim.0).unwrap_or(0);
|
||||
es.time2 = slot.spawn_time_ms;
|
||||
es.e_flags = slot.e_flags;
|
||||
es.tr_type = slot.tr_type;
|
||||
es.tr_time = slot.tr_time;
|
||||
|
||||
@@ -175,6 +175,6 @@ pub use transport::wire::{WireError, WireReader, WireWriter};
|
||||
|
||||
pub use svc_script_audio::SvcScriptAudio;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 116;
|
||||
pub const PROTOCOL_VERSION: u32 = 117;
|
||||
|
||||
pub use client::fire_verdict::{FireVerdictState, PredictedFireStatus};
|
||||
|
||||
@@ -2210,6 +2210,7 @@ fn encode_corpse_pool(out: &mut WireWriter, pool: &PlayerCorpsePool) {
|
||||
}
|
||||
out.put_u8(u8::from(slot.falling));
|
||||
out.put_i32(slot.ground_entity_num);
|
||||
out.put_i32(slot.spawn_time_ms);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2243,6 +2244,7 @@ fn decode_corpse_pool(input: &mut WireReader<'_>) -> Result<PlayerCorpsePool, Wi
|
||||
tr_base: [input.get_f32()?, input.get_f32()?, input.get_f32()?],
|
||||
falling: input.get_u8()? != 0,
|
||||
ground_entity_num: input.get_i32()?,
|
||||
spawn_time_ms: input.get_i32()?,
|
||||
};
|
||||
}
|
||||
_ => return Err(WireError::Malformed("bad corpse slot tag")),
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct PersistentRemoteTree {
|
||||
pub(crate) legs_restart: bool,
|
||||
pub(crate) torso_restart: bool,
|
||||
|
||||
pub(crate) occupation_tr_time: Option<i32>,
|
||||
pub(crate) occupation_key: Option<CorpseOccupationKey>,
|
||||
|
||||
pub(crate) legs_rate_sample: ClientAnimSample,
|
||||
|
||||
@@ -88,16 +88,36 @@ pub fn skip_frozen_corpse_dobj(
|
||||
is_corpse && tree_reused && last_was_cache_hit && has_cache
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct CorpseOccupationKey {
|
||||
pub spawn_time_ms: i32,
|
||||
pub victim: i32,
|
||||
}
|
||||
|
||||
pub fn corpse_occupation_key(es: &entity_iw4::EntityState) -> CorpseOccupationKey {
|
||||
CorpseOccupationKey {
|
||||
spawn_time_ms: es.time2,
|
||||
victim: es.client_num,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fresh_corpse_clip_time(age_ms: i32, clip_length_ms: i32) -> f32 {
|
||||
if clip_length_ms <= 0 || age_ms <= 0 {
|
||||
return 0.0;
|
||||
}
|
||||
(age_ms as f32 / clip_length_ms as f32).min(1.0)
|
||||
}
|
||||
|
||||
pub fn clone_corpse_tree_from_victim(
|
||||
trees: &mut RemoteBodyTrees,
|
||||
corpse_ent: u32,
|
||||
victim: u32,
|
||||
tr_time: i32,
|
||||
key: CorpseOccupationKey,
|
||||
) {
|
||||
if trees
|
||||
.by_ent
|
||||
.get(&corpse_ent)
|
||||
.is_some_and(|slot| slot.occupation_tr_time == Some(tr_time))
|
||||
.is_some_and(|slot| slot.occupation_key == Some(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +128,7 @@ pub fn clone_corpse_tree_from_victim(
|
||||
let mut dst = src;
|
||||
dst.dobj = None;
|
||||
dst.reuse_key = None;
|
||||
dst.occupation_tr_time = Some(tr_time);
|
||||
dst.occupation_key = Some(key);
|
||||
trees.by_ent.insert(corpse_ent, dst);
|
||||
}
|
||||
|
||||
@@ -129,6 +149,7 @@ pub fn advance_remote_tree(
|
||||
dt: f32,
|
||||
origin: [f32; 3],
|
||||
pose_time_ms: i32,
|
||||
corpse_age_ms: Option<i32>,
|
||||
trees: &mut RemoteBodyTrees,
|
||||
) -> Result<AdvancedRemoteTree, String> {
|
||||
let tree = binding.tree();
|
||||
@@ -139,7 +160,7 @@ pub fn advance_remote_tree(
|
||||
let legs_restart = legs.restart_toggle();
|
||||
let torso_restart = torso.restart_toggle();
|
||||
let previous = trees.by_ent.remove(&persist_key);
|
||||
let occupation_tr_time = previous.as_ref().and_then(|slot| slot.occupation_tr_time);
|
||||
let occupation_key = previous.as_ref().and_then(|slot| slot.occupation_key);
|
||||
let prev = previous.filter(|slot| Arc::ptr_eq(&slot.binding, binding));
|
||||
let prev_restart_changed_legs = prev
|
||||
.as_ref()
|
||||
@@ -178,6 +199,7 @@ pub fn advance_remote_tree(
|
||||
let overlayed = overlay_legs_clip(&clip, &torso_clip);
|
||||
(Arc::new(overlayed), Some(torso_clip))
|
||||
};
|
||||
let fresh = prev.is_none();
|
||||
let old_legs = prev.as_ref().map(|slot| slot.legs).unwrap_or(0);
|
||||
let old_torso = prev.as_ref().map(|slot| slot.torso).unwrap_or(0);
|
||||
let old_legs_moving = prev
|
||||
@@ -225,7 +247,7 @@ pub fn advance_remote_tree(
|
||||
torso: 0,
|
||||
legs_restart: false,
|
||||
torso_restart: false,
|
||||
occupation_tr_time,
|
||||
occupation_key,
|
||||
legs_rate_sample: ClientAnimSample::default(),
|
||||
torso_rate_sample: ClientAnimSample::default(),
|
||||
},
|
||||
@@ -292,6 +314,23 @@ pub fn advance_remote_tree(
|
||||
.set_state(id, state)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
if fresh && let Some(age_ms) = corpse_age_ms {
|
||||
for (index, leaf_clip) in [
|
||||
(legs_index, Some(&clip)),
|
||||
(torso_index, torso_clip.as_ref()),
|
||||
] {
|
||||
let Some(leaf_clip) = leaf_clip else {
|
||||
continue;
|
||||
};
|
||||
let time = fresh_corpse_clip_time(age_ms, leaf_clip.length_msec());
|
||||
let mut state = slot.runtime.states()[index as usize];
|
||||
state.time = time;
|
||||
state.old_time = time;
|
||||
slot.runtime
|
||||
.set_state(xmodel_runtime::XAnimNodeId(index), state)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
}
|
||||
clips = Some(PoseClips {
|
||||
clip,
|
||||
torso_clip,
|
||||
|
||||
@@ -3,10 +3,10 @@ use crate::anim::remote_body::{
|
||||
RemoteBodySkinnedItem, RemoteBodySkinnedQueue, RemoteBodyTrees, RemoteModelLods,
|
||||
RemoteSkinModels, RemoteSkinPoseHashes, SkinAfterPose, WorldGunGap, advance_remote_tree,
|
||||
bind_remote_skin_models, clone_corpse_tree_from_victim, commit_assembled_body,
|
||||
ensure_remote_dobj, hash_skin_matrices, occupy_lod_byte, occupy_remote_kit_dobj, packed_anim,
|
||||
pose_remote_dobj, push_cached_surfaces, radii, remote_player_controller, select_remote_lods,
|
||||
select_remote_models, skin_after_pose, skin_slot_need, skip_frozen_corpse_dobj,
|
||||
take_unique_geom, validate_remote_tracks, zero_anim,
|
||||
corpse_occupation_key, ensure_remote_dobj, hash_skin_matrices, occupy_lod_byte,
|
||||
occupy_remote_kit_dobj, packed_anim, pose_remote_dobj, push_cached_surfaces, radii,
|
||||
remote_player_controller, select_remote_lods, select_remote_models, skin_after_pose,
|
||||
skin_slot_need, skip_frozen_corpse_dobj, take_unique_geom, validate_remote_tracks, zero_anim,
|
||||
};
|
||||
use crate::anim::scene_submission::{AnimDObjSceneSkels, AnimDObjSceneSubmission, AnimSceneSubmit};
|
||||
use crate::anim::world_weapon::PreparedItemCompositions;
|
||||
@@ -868,16 +868,13 @@ impl<'a> RemotePoseFrame<'a> {
|
||||
let persist_key = u32::from(identity.number());
|
||||
let is_corpse = runtime.next_state.e_type == ET_PLAYER_CORPSE
|
||||
|| runtime.pose_e_type == ET_PLAYER_CORPSE as u8;
|
||||
let occupation_tr_time = is_corpse.then_some(runtime.next_state.tr_time);
|
||||
if is_corpse {
|
||||
let occupation_key = is_corpse.then(|| corpse_occupation_key(&runtime.next_state));
|
||||
if let Some(key) = occupation_key {
|
||||
let victim = u32::try_from(runtime.next_state.client_num).unwrap_or(0);
|
||||
clone_corpse_tree_from_victim(
|
||||
self.trees,
|
||||
persist_key,
|
||||
victim,
|
||||
runtime.next_state.tr_time,
|
||||
);
|
||||
clone_corpse_tree_from_victim(self.trees, persist_key, victim, key);
|
||||
}
|
||||
let corpse_age_ms =
|
||||
occupation_key.map(|key| runtime.pose_time_ms.wrapping_sub(key.spawn_time_ms));
|
||||
|
||||
if runtime.previous_pose.is_none() {
|
||||
if let Some(slot) = self.trees.get_mut(persist_key) {
|
||||
@@ -955,6 +952,7 @@ impl<'a> RemotePoseFrame<'a> {
|
||||
dt,
|
||||
sample.rate_origin,
|
||||
sample.rate_time_ms,
|
||||
corpse_age_ms,
|
||||
trees,
|
||||
)?;
|
||||
|
||||
@@ -1051,11 +1049,11 @@ impl<'a> RemotePoseFrame<'a> {
|
||||
}
|
||||
Ok(PoseOneOutcome::Posed)
|
||||
})();
|
||||
if let Some(tr_time) = occupation_tr_time
|
||||
if let Some(key) = occupation_key
|
||||
&& let Some(slot) = trees.get_mut(persist_key)
|
||||
&& slot.occupation_tr_time.is_none()
|
||||
&& slot.occupation_key.is_none()
|
||||
{
|
||||
slot.occupation_tr_time = Some(tr_time);
|
||||
slot.occupation_key = Some(key);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ pub struct PlayerCorpseSlot {
|
||||
pub falling: bool,
|
||||
|
||||
pub ground_entity_num: i32,
|
||||
|
||||
pub spawn_time_ms: i32,
|
||||
}
|
||||
|
||||
impl Default for PlayerCorpseSlot {
|
||||
@@ -57,6 +59,7 @@ impl Default for PlayerCorpseSlot {
|
||||
tr_base: [0.0; 3],
|
||||
falling: false,
|
||||
ground_entity_num: ENTITYNUM_NONE,
|
||||
spawn_time_ms: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,6 +181,7 @@ pub(crate) fn occupy_player_clone(
|
||||
tr_delta: clamp_clone_tr_delta(ps.velocity),
|
||||
tr_base: ps.origin,
|
||||
falling: true,
|
||||
spawn_time_ms: time_ms,
|
||||
..PlayerCorpseSlot::default()
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user