fix: silence the attach build's entity commands against a same-frame teardown
A unit destroyed or leaving range on the frame its visual is built is despawned by apply_despawn_fade while attach_entity_visuals' plain insert commands are still queued. Bevy's despawn only warns on a dead target, but insert and insert_if_new panic, so the client aborted inside a battleground's stream churn. Every entity-rooted write in attach_entity_visuals, setup_skinned_instance and arm_sequence_clock now uses try_insert / try_insert_if_new, and the mount child's MountChild does too, matching apply_despawn_fade's own try_despawn/try_insert.
This commit is contained in:
@@ -114,7 +114,7 @@ fn setup_skinned_instance(
|
||||
// loops.
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(rig)
|
||||
.try_insert(rig)
|
||||
.remove::<benilla_world::rig_palette::RigStarved>();
|
||||
slot
|
||||
}
|
||||
@@ -125,7 +125,7 @@ fn setup_skinned_instance(
|
||||
// Table full: static bind-pose parts until `heal_rig_starved` rebuilds with a slot.
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(benilla_world::rig_palette::RigStarved);
|
||||
.try_insert(benilla_world::rig_palette::RigStarved);
|
||||
0
|
||||
}
|
||||
};
|
||||
@@ -135,7 +135,7 @@ fn setup_skinned_instance(
|
||||
if let Some(drive) =
|
||||
benilla_world::rig_anim::GlobalSeqDrive::new_rig(&anims.global_bones, nbones)
|
||||
{
|
||||
commands.entity(entity).insert(drive);
|
||||
commands.entity(entity).try_insert(drive);
|
||||
}
|
||||
}
|
||||
Some(RigBuild { pose, slot })
|
||||
@@ -170,14 +170,14 @@ fn arm_sequence_clock(
|
||||
}
|
||||
}
|
||||
}
|
||||
commands.entity(entity).insert((
|
||||
commands.entity(entity).try_insert((
|
||||
player,
|
||||
AnimationGraphHandle(anims.graph.clone()),
|
||||
anims.clone(),
|
||||
));
|
||||
match kind {
|
||||
EntityKind::GameObject if go_state_machine => {
|
||||
commands.entity(entity).insert((
|
||||
commands.entity(entity).try_insert((
|
||||
// Cross-fades each transition over the clip's blend-in time, from the seed.
|
||||
transitions,
|
||||
crate::go_anim::GoAnim::default(),
|
||||
@@ -190,13 +190,13 @@ fn arm_sequence_clock(
|
||||
EntityKind::Corpse => {
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(AnimationTransitions::new())
|
||||
.try_insert(AnimationTransitions::new())
|
||||
.remove::<benilla_world::rig_anim::AnimParked>();
|
||||
}
|
||||
_ => {
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert((
|
||||
.try_insert((
|
||||
// Cross-fades over each clip's blend-in time, so a gait change eases.
|
||||
AnimationTransitions::new(),
|
||||
AnimDriver::default(),
|
||||
@@ -375,14 +375,16 @@ pub(super) fn attach_entity_visuals(
|
||||
let bake_center = dm.map(|d| d.bake_center_local).unwrap_or(Vec3::ZERO);
|
||||
// One ground-shade state per object (the reference's `[obj+0xe0]`), the same 2.5/0.5
|
||||
// MCSH chase for every kind; `insert_if_new` so a rebuild keeps the ramped state.
|
||||
// The `try_` forms: a unit torn down in this frame (common in a battleground's stream
|
||||
// churn) is gone when these apply, and `insert` would panic where `despawn` only warns.
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert_if_new(benilla_world::entity_shade::GroundShade::default());
|
||||
.try_insert_if_new(benilla_world::entity_shade::GroundShade::default());
|
||||
// Held items share the body's interior verdict (the reference aliases the wearer's
|
||||
// collector into each item, `0x718960`), so they fold at the body's centre.
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(benilla_world::interior::BodyBakeCenter(bake_center));
|
||||
.try_insert(benilla_world::interior::BodyBakeCenter(bake_center));
|
||||
// The light node's attach mode (`[node+0x90]` bit 13, set from the TYPEMASK at
|
||||
// `0x613e10`/`0x670db0`, dispatched at `0x6a86d0`): a GameObject by containment at its
|
||||
// bounds centre (`0x6a8c10`), a unit by down-ray at its position (`0x6a8a20`).
|
||||
@@ -390,7 +392,7 @@ pub(super) fn attach_entity_visuals(
|
||||
EntityKind::GameObject => {
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(benilla_world::interior::ContainmentAttach);
|
||||
.try_insert(benilla_world::interior::ContainmentAttach);
|
||||
}
|
||||
_ => {
|
||||
commands
|
||||
@@ -505,7 +507,7 @@ pub(super) fn attach_entity_visuals(
|
||||
for m in &d.markers {
|
||||
markers.entry(m.ident).or_insert((m.bone, m.offset));
|
||||
}
|
||||
commands.entity(entity).insert(super::BoneAttach {
|
||||
commands.entity(entity).try_insert(super::BoneAttach {
|
||||
points: d
|
||||
.attachments
|
||||
.iter()
|
||||
@@ -524,7 +526,7 @@ pub(super) fn attach_entity_visuals(
|
||||
if spine.is_some() || head.is_some() {
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(crate::creature_anim::BodyTwist::new(spine, head));
|
||||
.try_insert(crate::creature_anim::BodyTwist::new(spine, head));
|
||||
}
|
||||
}
|
||||
// A character model carries every hair, facial and body geoset, and is shared by its
|
||||
@@ -590,7 +592,7 @@ pub(super) fn attach_entity_visuals(
|
||||
.map(|c| bevy::camera::primitives::Aabb::from_min_max(c.bounds_min, c.bounds_max))
|
||||
.or_else(|| union_aabb(model.unwrap_or_default().iter().filter_map(|p| p.aabb)));
|
||||
if let Some(aabb) = election_bound {
|
||||
commands.entity(entity).insert(super::ModelBound(aabb));
|
||||
commands.entity(entity).try_insert(super::ModelBound(aabb));
|
||||
}
|
||||
// A billboard card rides its live joint, so card anchors resolve here, over the spawn
|
||||
// loop's geoset predicate: a hidden variant's card bone spawns no anchor.
|
||||
@@ -657,7 +659,7 @@ pub(super) fn attach_entity_visuals(
|
||||
if unit_will_fade {
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(benilla_world::model_fade::UnitAppearFade::Pending { since: now });
|
||||
.try_insert(benilla_world::model_fade::UnitAppearFade::Pending { since: now });
|
||||
} else if reattached {
|
||||
commands
|
||||
.entity(entity)
|
||||
@@ -676,25 +678,25 @@ pub(super) fn attach_entity_visuals(
|
||||
if let (EntityKind::Player, Some(e)) = (net.kind, equipment) {
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(super::equipment::AppliedEquipment(*e));
|
||||
.try_insert(super::equipment::AppliedEquipment(*e));
|
||||
}
|
||||
// The camera's framing pivot, model-local, before scale.
|
||||
commands.entity(entity).insert(CameraPivot {
|
||||
commands.entity(entity).try_insert(CameraPivot {
|
||||
height_local: dm.map(|d| d.pivot_height_local).unwrap_or(0.0),
|
||||
// The pivot's drop while swimming, the reference's third preset `cam+0x124`
|
||||
// (`0x50ccf6`); 0 without a Swim sequence.
|
||||
swim_drop_local: dm.map(|d| d.swim_pivot_drop_local).unwrap_or(0.0),
|
||||
});
|
||||
// The overhead fallback for a model with no PlayerName attachment (`0x608640`).
|
||||
commands.entity(entity).insert(super::OverheadFallback(
|
||||
commands.entity(entity).try_insert(super::OverheadFallback(
|
||||
dm.map(|d| d.bbox_z_local).unwrap_or(0.0),
|
||||
));
|
||||
// The chat bubble's height: the Stand box's Z, not the posed PlayerName attachment.
|
||||
commands.entity(entity).insert(super::StandBoxHeight(
|
||||
commands.entity(entity).try_insert(super::StandBoxHeight(
|
||||
dm.map(|d| d.stand_box_z_local).unwrap_or(0.0),
|
||||
));
|
||||
// The selection ring's model-local radius, scaled by the unit's scale.
|
||||
commands.entity(entity).insert(SelectionRadius(
|
||||
commands.entity(entity).try_insert(SelectionRadius(
|
||||
dm.map(|d| d.ground_radius_local).unwrap_or(0.0),
|
||||
));
|
||||
// Emitters ride their host bone's anchor, origin rebased into the bone frame
|
||||
@@ -778,7 +780,7 @@ pub(super) fn attach_entity_visuals(
|
||||
}
|
||||
// The pose lands last, with every anchor the build resolved into it.
|
||||
if let Some(rb) = skin {
|
||||
commands.entity(entity).insert(rb.pose);
|
||||
commands.entity(entity).try_insert(rb.pose);
|
||||
}
|
||||
// A GameObject with a hull collides; an anchored transport stays Kinematic, as its arm
|
||||
// labelled it, since its body moves every frame.
|
||||
@@ -789,7 +791,7 @@ pub(super) fn attach_entity_visuals(
|
||||
} else {
|
||||
RigidBody::Static
|
||||
};
|
||||
commands.entity(entity).insert((body, col));
|
||||
commands.entity(entity).try_insert((body, col));
|
||||
// This lane bypasses the streamer's attach queue, so it stamps the collider
|
||||
// set itself.
|
||||
collider_epoch.bump();
|
||||
@@ -855,13 +857,13 @@ pub(super) fn attach_entity_visuals(
|
||||
.map_or(0, |s| s.0.unit_mount_display_id());
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(super::mount::AppliedMount(applied));
|
||||
.try_insert(super::mount::AppliedMount(applied));
|
||||
}
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(VisualAttached)
|
||||
.try_insert(VisualAttached)
|
||||
// The display the visual was built with, for `refresh_live_display` to diff.
|
||||
.insert(super::live_display::AppliedDisplay(net.display_id))
|
||||
.try_insert(super::live_display::AppliedDisplay(net.display_id))
|
||||
.remove::<super::equipment::Reattached>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ pub(super) fn spawn_mount_child(
|
||||
commands.entity(child).insert(super::equipment::Reattached);
|
||||
}
|
||||
commands.entity(host).add_child(child);
|
||||
commands.entity(host).insert(MountChild(child));
|
||||
// `try_`: the host may be torn down in the same frame this build runs (a battleground's
|
||||
// stream churn), and a plain insert would panic where `despawn` only warns.
|
||||
commands.entity(host).try_insert(MountChild(child));
|
||||
child
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user