diff --git a/plugin/RunicNPC.cs b/plugin/RunicNPC.cs
index 25a8737..5b0de53 100644
--- a/plugin/RunicNPC.cs
+++ b/plugin/RunicNPC.cs
@@ -5179,6 +5179,39 @@ namespace Oxide.Plugins
// ---- where an admin stands and looks ----
/// The ground, a floor or a roof the admin is looking at, within .
+ /// D325: how far in front of the admin rnpc here stands the NPC, centre to centre.
+ private const float HereAhead = 1f;
+
+ ///
+ /// D325: the spot in front of the admin, on the ground, facing them. False, with the
+ /// admin's own spot and facing, when it does not fit: something in the way at chest height, no ground within
+ /// below their feet, or, for one that walks, no navmesh there.
+ ///
+ private static bool HereSpot(BasePlayer player, bool sentry, out Vector3 spot, out float yaw)
+ {
+ Vector3 feet = player.transform.position;
+ Vector3 ahead = player.eyes.HeadForward();
+ ahead.y = 0f;
+ if (ahead.sqrMagnitude < 0.01f)
+ ahead = new Vector3(player.transform.forward.x, 0f, player.transform.forward.z);
+ ahead.Normalize();
+
+ Vector3 chest = feet + Vector3.up;
+ RaycastHit hit;
+ if (!Physics.SphereCast(chest, 0.3f, ahead, out hit, HereAhead, GroundMask, QueryTriggerInteraction.Ignore) &&
+ Physics.Raycast(chest + ahead * HereAhead, Vector3.down, out hit, 1f + MaxDrop, GroundMask, QueryTriggerInteraction.Ignore) &&
+ (sentry || OffMeshReason(hit.point) == null))
+ {
+ spot = hit.point;
+ yaw = Quaternion.LookRotation(-ahead).eulerAngles.y;
+ return true;
+ }
+
+ spot = feet;
+ yaw = player.eyes.rotation.eulerAngles.y;
+ return false;
+ }
+
private static bool LookSpot(BasePlayer player, out Vector3 spot)
{
RaycastHit hit;
@@ -5433,6 +5466,7 @@ namespace Oxide.Plugins
Vector3 spot;
float yaw;
+ string hereNote = null;
if (o.At.HasValue)
{
spot = o.At.Value;
@@ -5445,8 +5479,8 @@ namespace Oxide.Plugins
}
else if (here)
{
- spot = c.Player.transform.position;
- yaw = c.Player.eyes.rotation.eulerAngles.y;
+ if (!HereSpot(c.Player, p.IsSentry, out spot, out yaw))
+ hereNote = $"There is no room {HereAhead:0} m in front of you, so it stands where you do.";
}
else
{
@@ -5511,6 +5545,8 @@ namespace Oxide.Plugins
Movement m = movement ?? p.Movement;
c.Say($"Placed {id}: {data.Count} × '{name}' ({p.Role}{(p.IsSentry ? "" : ", " + m.Mode + (kind == "wander" ? $" {m.Radius:0} m" : ""))}), " +
$"respawn {data.Respawn:0} s, {data.RespawnMode}{(data.Tether != null ? $", inside zone {data.Tether}" : "")}. Rename it with rnpc rename {id} .");
+ if (hereNote != null)
+ c.Say(hereNote);
if (built)
c.Say("This spot is on a player-built structure. If it is destroyed, the NPC falls back to the nearest navmesh.");
c.Say(CostWarning(PlannedTotal()));
diff --git a/tools/RunicNpcTest.cs b/tools/RunicNpcTest.cs
index 1a7f52b..ed7b7ba 100644
--- a/tools/RunicNpcTest.cs
+++ b/tools/RunicNpcTest.cs
@@ -1297,7 +1297,9 @@ namespace Oxide.Plugins
permission.GrantUserPermission(uid, "runicnpc.place", null);
RunicNPC.Call("CmdChat", admin, "rnpc", new[] { "here", "t_roam", "count=1" });
JObject chatPlaced = PlacementData("t_roam-2");
- Check("cmd.chat.here", chatPlaced != null && Flat(V(chatPlaced["position"].ToObject>()), b) < 1f, "" + chatPlaced?.ToString(Formatting.None));
+ // D325: a metre in front of the admin, not at their feet.
+ float hereAway = chatPlaced == null ? -1f : Flat(V(chatPlaced["position"].ToObject>()), admin.transform.position);
+ Check("cmd.chat.here", hereAway > 0.8f && hereAway < 1.2f, $"{hereAway:0.00} m from the admin; " + chatPlaced?.ToString(Formatting.None));
RunicNPC.Call("CmdChat", admin, "rnpc", new[] { "respawn", "t_roam-2" });
Check("cmd.chat.adminNeedsAdmin", Row("t_roam-2") != null, "respawn is refused with runicnpc.place alone (no error)");
RunicNPC.Call("CmdChat", admin, "rnpc", new[] { "remove", "t_roam-2" });