diff --git a/plugin/RunicNPC.cs b/plugin/RunicNPC.cs
index f11a9be..12a3701 100644
--- a/plugin/RunicNPC.cs
+++ b/plugin/RunicNPC.cs
@@ -3212,6 +3212,10 @@ namespace Oxide.Plugins
Index(npc, false);
if (npc.IsBoss)
ClearBar(npc);
+ if (_addsOf.Count > 0)
+ foreach (ulong boss in _addsOf.Keys.ToList())
+ if (_addsOf[boss].Remove(npc) && _addsOf[boss].Count == 0)
+ _addsOf.Remove(boss);
if (npc.PlacementId != null)
{
@@ -3542,6 +3546,12 @@ namespace Oxide.Plugins
/// The bosses alive now.
private readonly HashSet _bosses = new HashSet();
+ ///
+ /// Each boss's live adds, by the boss's net id, kept after the boss dies (D287) so that despawning
+ /// the boss's id removes them too. An add leaves its set when it dies or despawns.
+ ///
+ private readonly Dictionary> _addsOf = new Dictionary>();
+
/// Whose bar each player sees, by Steam id: one boss at a time, the nearest that qualifies (D274).
private readonly Dictionary _barShown = new Dictionary();
@@ -3692,6 +3702,7 @@ namespace Oxide.Plugins
Vector3 centre = boss.transform.position;
string owner = boss.Owner;
+ ulong bossId = boss.NetId;
var attackers = boss.Contributors.Keys.ToList();
float turn = UnityEngine.Random.value * Mathf.PI * 2f;
int i = 0;
@@ -3712,12 +3723,18 @@ namespace Oxide.Plugins
if (add == null)
PrintWarning($"{boss.RnName} ({boss.ProfileName}): add {i + 1} of {adds.Count} ('{adds.Profile}') not placed: {error}");
else
+ {
+ HashSet mine;
+ if (!_addsOf.TryGetValue(bossId, out mine))
+ _addsOf[bossId] = mine = new HashSet();
+ mine.Add(add);
foreach (ulong id in attackers)
{
BasePlayer who = BasePlayer.FindByID(id);
if (who != null && who.IsAlive() && Vector3.Distance(who.transform.position, add.transform.position) <= 100f)
Learn(add, who, true);
}
+ }
i++;
if (i < adds.Count)
@@ -4251,10 +4268,25 @@ namespace Oxide.Plugins
return npc;
}
+ ///
+ /// Removes one NPC, and for a boss the adds it summoned, alive or not (D287): an event's teardown
+ /// removes what its step placed by net id, and a boss's adds are in no step's ledger. The boss may
+ /// already be dead; its adds are still found by its net id.
+ ///
private int RunicNpc_Despawn(ulong netId)
{
+ var gone = new List();
RunicNpcPlayer npc;
- return _npcs.TryGetValue(netId, out npc) ? Despawn(new[] { npc }) : 0;
+ if (_npcs.TryGetValue(netId, out npc))
+ gone.Add(npc);
+ HashSet adds;
+ if (_addsOf.TryGetValue(netId, out adds))
+ {
+ gone.AddRange(adds);
+ _addsOf.Remove(netId);
+ }
+
+ return Despawn(gone);
}
private int RunicNpc_DespawnOwner(string owner) => owner == null ? 0 : Despawn(_npcs.Values.Where(n => n.Owner == owner));
diff --git a/tools/RunicNpcTest.cs b/tools/RunicNpcTest.cs
index b2d36a7..1c3972a 100644
--- a/tools/RunicNpcTest.cs
+++ b/tools/RunicNpcTest.cs
@@ -2218,12 +2218,17 @@ namespace Oxide.Plugins
if (second != null) Api("RunicNpc_Despawn", Id(second));
// The kill: the death hook with every player who hurt it; the adds fight on (D287).
+ ulong bossId = Id(boss);
boss.Hurt(new HitInfo(shooter, boss, Rust.DamageType.Bullet, 5000f));
yield return new WaitForSeconds(1f);
Check("s6.boss.died", _bossEvents.Contains("died:t_boss") && _bossContributors.ContainsKey(StandInId + 64) && _bossContributors[StandInId + 64] > 990f,
$"events {string.Join(", ", _bossEvents.ToArray())}, contributors {JsonConvert.SerializeObject(_bossContributors)}");
int addsLeft = List(Owner).Count(d => (string)d["profile"] == "t_roam");
Check("s6.boss.addsFightOn", addsLeft == 2, $"{addsLeft} add(s) alive after the boss died (D287)");
+ // An event's teardown despawns what its step placed, by net id: the dead boss's id takes its adds with it.
+ int removed = Api("RunicNpc_Despawn", bossId);
+ int addsAfter = List(Owner).Count(d => (string)d["profile"] == "t_roam");
+ Check("s6.boss.despawnTakesAdds", removed == 2 && addsAfter == 0, $"RunicNpc_Despawn(the dead boss's id) removed {removed}; adds left {addsAfter}");
Check("s6.boss.unlisted", List(Owner).All(d => (string)d["profile"] != "t_boss"), "");
// ---- the examples (D280, D284, D289) ----