fix: the attach and mount teardown path uses try_ commands and child-side ChildOf

A parent-side add_child panics against an entity a same-frame teardown already
despawned; spawn with ChildOf on the child and use try_insert/try_remove/
try_despawn so the same-frame churn warns instead of aborting.
This commit is contained in:
chairmanpow
2026-10-12 17:02:33 +01:00
parent 3a5eb088ca
commit fa02975473
3 changed files with 49 additions and 40 deletions
+28 -22
View File
@@ -115,7 +115,7 @@ fn setup_skinned_instance(
commands
.entity(entity)
.try_insert(rig)
.remove::<benilla_world::rig_palette::RigStarved>();
.try_remove::<benilla_world::rig_palette::RigStarved>();
slot
}
// Nothing to skin: no starvation marker, or `heal_rig_starved` would rebuild the body for a
@@ -191,7 +191,7 @@ fn arm_sequence_clock(
commands
.entity(entity)
.try_insert(AnimationTransitions::new())
.remove::<benilla_world::rig_anim::AnimParked>();
.try_remove::<benilla_world::rig_anim::AnimParked>();
}
_ => {
commands
@@ -203,7 +203,7 @@ fn arm_sequence_clock(
))
// A rebuilt rig is born live: a stale park marker would desync the LOD gate's
// edge-triggered bookkeeping.
.remove::<benilla_world::rig_anim::AnimParked>();
.try_remove::<benilla_world::rig_anim::AnimParked>();
}
}
}
@@ -361,9 +361,11 @@ pub(super) fn attach_entity_visuals(
} else if let Some(&super::mount::MountChild(child)) = mount_child {
// Dismounted while pending: drop the mount it ordered.
if let Ok(mut ec) = commands.get_entity(child) {
ec.despawn();
ec.try_despawn();
}
commands.entity(entity).remove::<super::mount::MountChild>();
commands
.entity(entity)
.try_remove::<super::mount::MountChild>();
}
let kind = match net.kind {
EntityKind::GameObject => ModelKind::GameObject,
@@ -397,7 +399,7 @@ pub(super) fn attach_entity_visuals(
_ => {
commands
.entity(entity)
.remove::<benilla_world::interior::ContainmentAttach>();
.try_remove::<benilla_world::interior::ContainmentAttach>();
}
}
let object = WorldObject {
@@ -425,8 +427,11 @@ pub(super) fn attach_entity_visuals(
// dispatches on the mount's model (`0x7106c0`) and tilts through its node.
let mut joints_root = rider_root;
if d.terrain_tilt != 0 && rider_root == entity {
// Child-side `ChildOf`, not `add_child`: a parent-side add panics against a
// unit that a same-frame teardown despawned mid-build.
let node = commands
.spawn((
ChildOf(entity),
super::conform::ConformNode {
// The ground and yaw source: the unit, or a mount child's
// host.
@@ -437,7 +442,6 @@ pub(super) fn attach_entity_visuals(
Visibility::default(),
))
.id();
commands.entity(entity).add_child(node);
joints_root = node;
}
setup_skinned_instance(
@@ -663,7 +667,7 @@ pub(super) fn attach_entity_visuals(
} else if reattached {
commands
.entity(entity)
.remove::<benilla_world::model_fade::UnitAppearFade>();
.try_remove::<benilla_world::model_fade::UnitAppearFade>();
}
// Size is `OBJECT_FIELD_SCALE_X` alone: the server already folds the DBC scale into
// it (vmangos `Unit::GetScaleForDisplayId`, `Unit.cpp:9579`), and the reference
@@ -834,19 +838,21 @@ pub(super) fn attach_entity_visuals(
id: net.display_id.unwrap_or(0),
detail: String::new(),
};
commands.entity(entity).with_children(|parent| {
parent.spawn((
Mesh3d(mesh),
MeshMaterial3d(material),
Transform::from_translation(Vec3::Y * lift),
object,
benilla_world::interact::CreaturePickPart,
// No `RenderSubmesh`: the picker casts against the cube's box.
benilla_world::interact::PickBox,
// Counted by `WOW_UNIT_VISUALS`.
super::FallbackCube,
));
});
// Child-side `ChildOf`, like every model part in `dress`: the parent-side
// `with_children` would `add_related` against a unit that a same-frame teardown
// already despawned, and panic where the child spawn only warns.
commands.spawn((
ChildOf(entity),
Mesh3d(mesh),
MeshMaterial3d(material),
Transform::from_translation(Vec3::Y * lift),
object,
benilla_world::interact::CreaturePickPart,
// No `RenderSubmesh`: the picker casts against the cube's box.
benilla_world::interact::PickBox,
// Counted by `WOW_UNIT_VISUALS`.
super::FallbackCube,
));
}
}
// The mount the visual was built on, for `reseat_mounts` to diff; stamped on a cube too,
@@ -864,7 +870,7 @@ pub(super) fn attach_entity_visuals(
.try_insert(VisualAttached)
// The display the visual was built with, for `refresh_live_display` to diff.
.try_insert(super::live_display::AppliedDisplay(net.display_id))
.remove::<super::equipment::Reattached>();
.try_remove::<super::equipment::Reattached>();
}
}
@@ -191,10 +191,10 @@ pub(in crate::entities) fn redress_player_looks(
// A billboard card is a world root, so it is reaped by name.
if let Some(card) = dp.card {
if let Ok(mut ec) = commands.get_entity(card) {
ec.despawn();
ec.try_despawn();
}
}
commands.entity(child).despawn();
commands.entity(child).try_despawn();
hidden += 1;
continue;
}
+19 -16
View File
@@ -45,8 +45,11 @@ pub(super) fn spawn_mount_child(
scale: f32,
fade_skip: bool,
) -> Entity {
// Child-side `ChildOf(host)`, not `add_child`: a parent-side add panics against a host that a
// same-frame teardown despawned mid-build.
let child = commands
.spawn((
ChildOf(host),
NetEntity {
kind: EntityKind::Unit,
display_id: Some(display),
@@ -60,7 +63,6 @@ pub(super) fn spawn_mount_child(
if fade_skip {
commands.entity(child).insert(super::equipment::Reattached);
}
commands.entity(host).add_child(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));
@@ -79,13 +81,13 @@ fn spawn_seat_anchor(
) -> Entity {
let anchor = commands
.spawn((
ChildOf(joint),
Transform::from_translation(offset)
.with_scale(Vec3::splat(1.0 / mount_scale.max(0.001))),
Visibility::default(),
benilla_world::rig_anim::RigFrame(rider),
))
.id();
commands.entity(joint).add_child(anchor);
anchor
}
@@ -135,9 +137,9 @@ pub(super) fn seat_or_spawn_mount(
let built = children.get(child).map(|(_, _, n)| n.display_id).ok();
if built != Some(Some(mount_display)) {
if let Ok(mut ec) = commands.get_entity(child) {
ec.despawn();
ec.try_despawn();
}
commands.entity(unit).remove::<MountChild>();
commands.entity(unit).try_remove::<MountChild>();
return Seat::Wait;
}
let Ok((true, Some(bones), _)) = children.get(child) else {
@@ -185,12 +187,12 @@ fn ground_frame(
}
let node = commands
.spawn((
ChildOf(unit),
super::conform::ConformNode { unit, mode: tilt },
Transform::default(),
Visibility::default(),
))
.id();
commands.entity(unit).add_child(node);
node
}
@@ -202,15 +204,16 @@ fn reseat_rig(commands: &mut Commands, rig: &mut RigPose, frame: Entity, conform
return;
}
let anchors: Vec<Entity> = rig.anchors.iter().map(|&(_, a)| a).collect();
if !anchors.is_empty() {
commands.entity(frame).add_children(&anchors);
for anchor in anchors {
// Child-side, so a frame despawned in the same frame warns rather than panicking.
commands.entity(anchor).try_insert(ChildOf(frame));
}
let old = std::mem::replace(&mut rig.joints_root, frame);
rig.pose_dirty = true;
// A conform node the rig just left is empty, so it goes; a seat anchor dies with its mount.
if conform {
if let Ok(mut ec) = commands.get_entity(old) {
ec.despawn();
ec.try_despawn();
}
}
}
@@ -254,11 +257,11 @@ pub(super) fn reseat_mounts(
let Ok(seated_root) = poses.get(entity).map(|r| r.joints_root) else {
if let Some(&MountChild(child)) = child {
if let Ok(mut ec) = commands.get_entity(child) {
ec.despawn();
ec.try_despawn();
}
commands.entity(entity).remove::<MountChild>();
commands.entity(entity).try_remove::<MountChild>();
}
commands.entity(entity).insert(AppliedMount(live));
commands.entity(entity).try_insert(AppliedMount(live));
continue;
};
let child = child.map(|&MountChild(c)| c);
@@ -276,13 +279,13 @@ pub(super) fn reseat_mounts(
}
if let Some(child) = child {
if let Ok(mut ec) = commands.get_entity(child) {
ec.despawn();
ec.try_despawn();
}
}
commands
.entity(entity)
.remove::<MountChild>()
.insert(AppliedMount(0));
.try_remove::<MountChild>()
.try_insert(AppliedMount(0));
continue;
}
// ── Leg 2 · `0x607a00`: build the mount model and attach the body to its seat.
@@ -302,10 +305,10 @@ pub(super) fn reseat_mounts(
let leaving_conform = conforms.contains(seated_root);
reseat_rig(&mut commands, &mut rig, anchor, leaving_conform);
}
commands.entity(entity).insert(AppliedMount(live));
commands.entity(entity).try_insert(AppliedMount(live));
}
Seat::UnitMatrix => {
commands.entity(entity).insert(AppliedMount(live));
commands.entity(entity).try_insert(AppliedMount(live));
}
}
}