**Version:** 5.4.3, Rust build 24587531 (Aug 2026 forced update)
## Problem
Facepunch's Gen2 AI migration means vanilla monument/tunnel scientists now spawn as
`scientist2` — class `Rust.Ai.Gen2.ScientistNPC2 : Rust.Ai.Gen2.BaseNPC2 : BaseCombatEntity`.
They are **no longer BasePlayer-derived**, so Admin Radar's NPC pipeline misses them twice:
1. The entity-add path and cache-rebuild coroutine send all `BaseNPC2` to the **Animals**
cache, so Gen2 scientists only draw (unnamed, wrong color) when the animals filter is on.
2. `CacheNpc()` falls back to `CacheAnimal()` for anything that isn't `BasePlayer`.
Net effect: admins see no scientists at monuments/tunnels populated by Gen2 spawns.
(Old-style `ScientistNPC`, `TunnelDweller`, `UnderwaterDweller` still work — they remain
`BasePlayer`-derived.)
## Discriminator
`ScientistNPC2` is currently the only `BaseNPC2` subtype with `IsAnimal => false`
(crocodile/panther/tiger/wolf2 keep the default `true`), so keying on the concrete
`ScientistNPC2` type keeps the new Gen2 animals correctly bucketed as animals.
## Patch (attached, 5 hunks against stock 5.4.3)
1. Entity-add path: route `ScientistNPC2` into the NPC cache under `config.Core.NPC`
(before the `BaseNPC2` → Animals line; mirrors the TravellingVendor precedent).
2. `CacheNpc()`: new branch → `CacheGen2Scientist()` (structured after `CacheVendor`):
scientist hex color, localized "scientist" name (or ShortPrefabName with the
NpcPrefabName option), health from `BaseCombatEntity.health`, NPC distance gate,
fixed 1.8f box height (Gen2 has no `modelState`).
3. Cache-rebuild coroutine: `AddElementsToCache<ScientistNPC2>` into the NPC cache,
ordered before the `BaseNPC2` animal sweep (the shared entity pool is consumed
in order — the animal sweep would otherwise claim them).
4. Distance classifier: `ScientistNPC2` resolves to the NPC distance instead of Animal.
Running in production since 2026-08-07 on a 5000-size map with ~100 live scientist2;
no errors, Gen2 scientists draw identically to gen1.
--- AdminRadar.cs (5.4.3 stock)
+++ AdminRadar.cs (gen2 support)
@@ -175,6 +175,12 @@
{
return true;
}
+
+ // route to NPC cache before the BaseNpc/BaseNPC2 -> Animals fallback below.
+ if (config.Core.NPC && Add_Internal<ScientistNPC2, EntityInfo>(NPC, entity, EntityType.Npc))
+ {
+ return true;
+ }
if (config.Core.Animals && Add_Internal<BaseNpc, EntityInfo>(Animals, entity, EntityType.Npc))
{
return true;
@@ -2420,6 +2426,14 @@
return;
}
+
+ // branch it falls into the BasePlayer-less CacheAnimal() path below and renders as an animal.
+ if (ei.entity is ScientistNPC2)
+ {
+ CacheGen2Scientist(ei);
+ return;
+ }
+
if (!(ei.entity is BasePlayer))
{
CacheAnimal(ei);
@@ -2467,6 +2481,52 @@
}
}
+
+ // has no modelState/displayName like a BasePlayer NPC, so it needs its own draw path instead of the
+ // BasePlayer branch in CacheNpc(). Structured after CacheVendor(), the other non-BasePlayer NPC-bucket entity.
+ private void CacheGen2Scientist(EntityInfo ei)
+ {
+ try
+ {
+ currType = EntityType.Npc;
+
+ if (IsValid(ei))
+ {
+ var obj = SetDataObject(ei);
+
+ if (SetDistance(ei.from) < config.Distance.NPC)
+ {
+ var target = ei.entity as BaseCombatEntity;
+ var color = __(config.Hex.Scientist);
+ var displayName = config.Options.NpcPrefabName ? target.ShortPrefabName : instance.m("scientist", userid);
+
+ CacheText(obj, color, twoUp, () =>
+ {
+ ei.info = null;
+
+ float dist = Distance(ei.from);
+ if (dist > maxDistance || dist > config.Distance.NPC || !IsAtView(ei))
+ {
+ return;
+ }
+
+ ei.info = Format(displayName, $"<color={config.Hex.Health}>{Mathf.CeilToInt(target.health)}</color> <color={config.Hex.Dist}>{dist}</color>");
+ });
+
+ CacheBox(obj, color, Vector3.up, 1.8f);
+ CacheArrow(obj, color, new(0f, ei.from.y + 10), ei.from, 1, false);
+ }
+ else CacheBox(obj, Color.blue, Vector3.up, 5f, true);
+ }
+
+ checks++;
+ }
+ catch (Exception ex)
+ {
+ HandleException(ex);
+ }
+ }
+
private void DrawVictim(BasePlayer victim, Vector3 from, Vector3 offset, Color color)
{
if (victim != null)
@@ -2841,6 +2901,10 @@
cached += cache.RidableHorse.Count;
}
+
+ // matched entities out of the shared _allEntities pool, so scientist2 (a BaseNPC2 subtype) needs to be
+ // claimed into cache.NPC first or the BaseNPC2 sweep will already have swept it into cache.Animals.
+ yield return CreateCoroutine(AddElementsToCache<ScientistNPC2>(_coroutineTimer, cache.NPC, EntityType.Npc));
yield return CreateCoroutine(AddElementsToCache<BaseNpc>(_coroutineTimer, cache.Animals, EntityType.Npc));
yield return CreateCoroutine(AddElementsToCache<BaseNPC2>(_coroutineTimer, cache.Animals, EntityType.Npc));
yield return CreateCoroutine(AddElementsToCache<WildlifeHazard>(_coroutineTimer, cache.Animals, EntityType.Npc));
@@ -5469,7 +5533,8 @@
case EntityType.Limit:
default:
{
- if (entity is BaseNpc or BaseNPC2 or FarmableAnimal or WildlifeHazard) return Animal;
+
+ if (entity is not ScientistNPC2 && entity is BaseNpc or BaseNPC2 or FarmableAnimal or WildlifeHazard) return Animal;
if (entity is VendingMachine) return VendingMachine;
if (type == EntityType.Box) return Box;
if (type == EntityType.Npc) return NPC;