diff --git a/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs b/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs
index 85662a887..c89dd6e98 100644
--- a/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs
+++ b/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs
@@ -398,17 +398,14 @@ namespace Server.Factions
else*/
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"My move is blocked, so I am going to attack {Mobile.FocusMob.Name}");
- }
+ this.DebugSayFormatted($"My move is blocked, so I am going to attack {Mobile.FocusMob.Name}");
Mobile.Combatant = Mobile.FocusMob;
Action = ActionType.Combat;
}
- else if (Mobile.Debug)
+ else
{
- Mobile.DebugSay("I am stuck");
+ DebugSay("I am stuck");
}
}
diff --git a/Projects/UOContent/Mobiles/AI/AnimalAI.cs b/Projects/UOContent/Mobiles/AI/AnimalAI.cs
index 481b22747..9e33414c7 100644
--- a/Projects/UOContent/Mobiles/AI/AnimalAI.cs
+++ b/Projects/UOContent/Mobiles/AI/AnimalAI.cs
@@ -23,19 +23,13 @@ public class AnimalAI : BaseAI
if (!Mobile.Summoned && !Mobile.Controlled && hitPercent < 0.1 && Mobile.CanFlee) // Less than 10% health
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I am low on health!");
- }
+ DebugSay("I am low on health!");
Action = ActionType.Flee;
}
else if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"I have detected {Mobile.FocusMob.Name}, attacking");
- }
+ this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking");
Mobile.Combatant = Mobile.FocusMob;
Action = ActionType.Combat;
@@ -55,10 +49,7 @@ public class AnimalAI : BaseAI
if (combatant == null || combatant.Deleted || combatant.Map != Mobile.Map || !combatant.Alive ||
combatant.IsDeadBondedPet)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("My combatant is gone!");
- }
+ DebugSay("My combatant is gone!");
Action = ActionType.Wander;
return true;
@@ -68,19 +59,13 @@ public class AnimalAI : BaseAI
{
if (Mobile.GetDistanceToSqrt(combatant) > Mobile.RangePerception + 1)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"I cannot find {combatant.Name}");
- }
+ this.DebugSayFormatted($"I cannot find {combatant.Name}");
Action = ActionType.Wander;
return true;
}
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"I should be closer to {combatant.Name}");
- }
+ this.DebugSayFormatted($"I should be closer to {combatant.Name}");
}
else if (Core.TickCount - Mobile.LastMoveTime > 400)
{
@@ -93,10 +78,7 @@ public class AnimalAI : BaseAI
if (hitPercent <= 0.1)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I am low on health!");
- }
+ DebugSay("I am low on health!");
Action = ActionType.Flee;
return true;
@@ -105,10 +87,7 @@ public class AnimalAI : BaseAI
if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"I used my abilities on {combatant.Name}!");
- }
+ this.DebugSayFormatted($"I used my abilities on {combatant.Name}!");
}
return true;
@@ -126,20 +105,14 @@ public class AnimalAI : BaseAI
{
if (WalkMobileRange(Mobile.FocusMob, 1, false, Mobile.RangePerception, Mobile.RangePerception * 2))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Well, here I am safe");
- }
+ DebugSay("Well, here I am safe");
Action = ActionType.Wander;
}
}
else
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have lost my focus, lets relax");
- }
+ DebugSay("I have lost my focus, lets relax");
Action = ActionType.Wander;
}
diff --git a/Projects/UOContent/Mobiles/AI/ArcherAI.cs b/Projects/UOContent/Mobiles/AI/ArcherAI.cs
index 660ca702f..6491c42d5 100644
--- a/Projects/UOContent/Mobiles/AI/ArcherAI.cs
+++ b/Projects/UOContent/Mobiles/AI/ArcherAI.cs
@@ -11,17 +11,11 @@ public class ArcherAI : BaseAI
public override bool DoActionWander()
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have no combatant");
- }
+ DebugSay("I have no combatant");
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"I have detected {Mobile.FocusMob.Name} and I will attack");
- }
+ this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name} and I will attack");
Mobile.Combatant = Mobile.FocusMob;
Action = ActionType.Combat;
@@ -41,10 +35,7 @@ public class ArcherAI : BaseAI
if (combatant == null || combatant.Deleted || combatant.Map != Mobile.Map || !combatant.Alive ||
combatant.IsDeadBondedPet)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("My combatant is gone, so my guard is up");
- }
+ DebugSay("My combatant is gone, so my guard is up");
Action = ActionType.Guard;
return true;
@@ -58,17 +49,11 @@ public class ArcherAI : BaseAI
Mobile.Weapon.MaxRange
))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"I am still not in range of {combatant.Name}");
- }
+ this.DebugSayFormatted($"I am still not in range of {combatant.Name}");
if ((int)Mobile.GetDistanceToSqrt(combatant) > Mobile.RangePerception + 1)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"I have lost {combatant.Name}");
- }
+ this.DebugSayFormatted($"I have lost {combatant.Name}");
Mobile.Combatant = null;
Action = ActionType.Guard;
@@ -82,10 +67,7 @@ public class ArcherAI : BaseAI
if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"I used my abilities on {combatant.Name}!");
- }
+ this.DebugSayFormatted($"I used my abilities on {combatant.Name}!");
return true;
}
@@ -118,10 +100,7 @@ public class ArcherAI : BaseAI
{
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"I have detected {Mobile.FocusMob.Name}, attacking");
- }
+ this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking");
Mobile.Combatant = Mobile.FocusMob;
Action = ActionType.Combat;
diff --git a/Projects/UOContent/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs
index 8ddf00d32..cc8ee14c3 100644
--- a/Projects/UOContent/Mobiles/AI/BaseAI.cs
+++ b/Projects/UOContent/Mobiles/AI/BaseAI.cs
@@ -102,6 +102,7 @@ public abstract class BaseAI
private long _nextDetectHidden;
private long _nextStopGuard;
+ public long NextDebugMessage { get; set; }
protected PathFollower _path;
public Timer _timer;
@@ -463,20 +464,14 @@ public abstract class BaseAI
if (Mobile.Controlled && Mobile.Commandable)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Listening...");
- }
+ DebugSay("Listening...");
var isOwner = e.Mobile == Mobile.ControlMaster;
var isFriend = !isOwner && Mobile.IsPetFriend(e.Mobile);
if (e.Mobile.Alive && (isOwner || isFriend))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("It's from my master");
- }
+ DebugSay("It's from my master");
var keywords = e.Keywords;
var speech = e.Speech;
@@ -774,10 +769,7 @@ public abstract class BaseAI
{
if (e.Mobile.AccessLevel >= AccessLevel.GameMaster)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("It's from a GM");
- }
+ DebugSay("It's from a GM");
if (Mobile.FindMyName(e.Speech, true))
{
@@ -805,6 +797,15 @@ public abstract class BaseAI
}
}
+ public void DebugSay(string message, int cooldownMs = 5000)
+ {
+ if (Mobile.Debug && Core.TickCount >= NextDebugMessage)
+ {
+ Mobile.PublicOverheadMessage(MessageType.Regular, 41, false, message);
+ NextDebugMessage = Core.TickCount + cooldownMs;
+ }
+ }
+
public virtual bool Think()
{
if (Mobile.Deleted)
@@ -923,10 +924,7 @@ public abstract class BaseAI
{
if (CheckHerding())
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Praise the shepherd!");
- }
+ DebugSay("Praise the shepherd!");
}
else if (Mobile.CurrentWayPoint != null)
{
@@ -934,19 +932,13 @@ public abstract class BaseAI
if ((point.X != Mobile.Location.X || point.Y != Mobile.Location.Y) && point.Map == Mobile.Map &&
point.Parent == null && !point.Deleted)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I will move towards my waypoint.");
- }
+ DebugSay("I will move towards my waypoint.");
DoMove(Mobile.GetDirectionTo(Mobile.CurrentWayPoint));
}
else if (OnAtWayPoint())
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I will go to the next waypoint");
- }
+ DebugSay("I will go to the next waypoint");
Mobile.CurrentWayPoint = point.NextPoint;
if (point.NextPoint?.Deleted == true)
@@ -987,10 +979,7 @@ public abstract class BaseAI
{
if (Core.AOS && CheckHerding())
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Praise the shepherd!");
- }
+ DebugSay("Praise the shepherd!");
return true;
}
@@ -1000,10 +989,7 @@ public abstract class BaseAI
if (combatant == null || combatant.Deleted || combatant.Map != Mobile.Map || !combatant.Alive ||
combatant.IsDeadBondedPet)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("My combatant is gone!");
- }
+ DebugSay("My combatant is gone!");
Action = ActionType.Wander;
return true;
@@ -1012,10 +998,7 @@ public abstract class BaseAI
Mobile.Direction = Mobile.GetDirectionTo(combatant);
if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"I used my abilities on {combatant.Name}!");
- }
+ this.DebugSayFormatted($"I used my abilities on {combatant.Name}!");
}
return true;
@@ -1025,25 +1008,16 @@ public abstract class BaseAI
{
if (Core.AOS && CheckHerding())
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Praise the shepherd!");
- }
+ DebugSay("Praise the shepherd!");
}
else if (Core.TickCount - _nextStopGuard < 0)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I am on guard");
- }
+ DebugSay("I am on guard");
// m_Mobile.Turn( Utility.Random(0, 2) - 1 );
}
else
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I stopped being on guard");
- }
+ DebugSay("I stopped being on guard");
Action = ActionType.Wander;
}
@@ -1057,10 +1031,7 @@ public abstract class BaseAI
if (from?.Deleted != false || from.Map != Mobile.Map)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have lost him");
- }
+ DebugSay("I have lost him");
Action = ActionType.Guard;
return true;
@@ -1068,19 +1039,13 @@ public abstract class BaseAI
if (WalkMobileRange(from, 1, true, Mobile.RangePerception * 2, Mobile.RangePerception * 3))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have fled");
- }
+ DebugSay("I have fled");
Action = ActionType.Guard;
return true;
}
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I am fleeing!");
- }
+ DebugSay("I am fleeing!");
return true;
}
@@ -1244,10 +1209,7 @@ public abstract class BaseAI
public virtual bool DoOrderNone()
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have no order");
- }
+ DebugSay("I have no order");
WalkRandomInHome(3, 2, 1);
@@ -1276,20 +1238,14 @@ public abstract class BaseAI
if (iCurrDist > Mobile.RangePerception)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have lost my master. I stay here");
- }
+ DebugSay("I have lost my master. I stay here");
Mobile.ControlTarget = null;
Mobile.ControlOrder = OrderType.None;
}
else
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("My master told me come");
- }
+ DebugSay("My master told me come");
// Not exactly OSI style, but better than nothing.
var bRun = iCurrDist > 5;
@@ -1319,10 +1275,7 @@ public abstract class BaseAI
return true;
}
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I drop my stuff for my master");
- }
+ DebugSay("I drop my stuff for my master");
var pack = Mobile.Backpack;
@@ -1389,10 +1342,7 @@ public abstract class BaseAI
{
if (CheckHerding())
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Praise the shepherd!");
- }
+ DebugSay("Praise the shepherd!");
}
else if (Mobile.ControlTarget?.Deleted == false && Mobile.ControlTarget != Mobile)
{
@@ -1400,10 +1350,7 @@ public abstract class BaseAI
if (iCurrDist > Mobile.RangePerception)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have lost the one to follow. I stay here");
- }
+ DebugSay("I have lost the one to follow. I stay here");
if (Mobile.Combatant?.Deleted == false && Mobile.Combatant.Alive &&
!Mobile.Combatant.IsDeadBondedPet)
@@ -1418,10 +1365,7 @@ public abstract class BaseAI
}
else
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"My master told me to follow: {Mobile.ControlTarget.Name}");
- }
+ this.DebugSayFormatted($"My master told me to follow: {Mobile.ControlTarget.Name}");
// Not exactly OSI style, but better than nothing.
var bRun = iCurrDist > 5;
@@ -1447,10 +1391,7 @@ public abstract class BaseAI
}
else
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have nobody to follow");
- }
+ DebugSay("I have nobody to follow");
Mobile.ControlTarget = null;
Mobile.ControlOrder = OrderType.None;
@@ -1600,9 +1541,9 @@ public abstract class BaseAI
}
}
- if (combatant != null && Mobile.Debug)
+ if (combatant != null)
{
- Mobile.DebugSay("Crap, my master has been attacked! I will attack one of those bastards!");
+ DebugSay("Crap, my master has been attacked! I will attack one of those bastards!");
}
}
@@ -1610,10 +1551,7 @@ public abstract class BaseAI
combatant.Alive && !combatant.IsDeadBondedPet && Mobile.CanSee(combatant) &&
Mobile.CanBeHarmful(combatant, false) && combatant.Map == Mobile.Map)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Guarding from target...");
- }
+ DebugSay("Guarding from target...");
Mobile.Combatant = combatant;
Mobile.FocusMob = combatant;
@@ -1627,10 +1565,7 @@ public abstract class BaseAI
}
else
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Nothing to guard from");
- }
+ DebugSay("Nothing to guard from");
Mobile.Warmode = false;
if (Core.AOS)
@@ -1654,12 +1589,7 @@ public abstract class BaseAI
if (Mobile.ControlTarget?.Deleted != false || Mobile.ControlTarget.Map != Mobile.Map ||
!Mobile.ControlTarget.Alive || Mobile.ControlTarget.IsDeadBondedPet)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay(
- "I think he might be dead. He's not anywhere around here at least. That's cool. I'm glad he's dead."
- );
- }
+ DebugSay("I think he might be dead. He's not anywhere around here at least. That's cool. I'm glad he's dead.");
if (Core.AOS)
{
@@ -1703,10 +1633,7 @@ public abstract class BaseAI
Mobile.ControlTarget = newCombatant;
Mobile.ControlOrder = OrderType.Attack;
Mobile.Combatant = newCombatant;
- if (Mobile.Debug)
- {
- Mobile.DebugSay("But -that- is not dead. Here we go again...");
- }
+ DebugSay("But -that- is not dead. Here we go again...");
Think();
}
@@ -1714,10 +1641,7 @@ public abstract class BaseAI
}
else
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Attacking target...");
- }
+ DebugSay("Attacking target...");
Think();
}
@@ -1727,20 +1651,14 @@ public abstract class BaseAI
public virtual bool DoOrderPatrol()
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("This order is not yet coded");
- }
+ DebugSay("This order is not yet coded");
return true;
}
public virtual bool DoOrderRelease()
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have been released");
- }
+ DebugSay("I have been released");
Mobile.PlaySound(Mobile.GetAngerSound());
@@ -1776,17 +1694,7 @@ public abstract class BaseAI
public virtual bool DoOrderStay()
{
- if (Mobile.Debug)
- {
- if (CheckHerding())
- {
- Mobile.DebugSay("Praise the shepherd!");
- }
- else
- {
- Mobile.DebugSay("My master told me to stay");
- }
- }
+ DebugSay(CheckHerding() ? "Praise the shepherd!" : "My master told me to stay");
return true;
}
@@ -1798,10 +1706,7 @@ public abstract class BaseAI
return true;
}
- if (Mobile.Debug)
- {
- Mobile.DebugSay("My master told me to stop.");
- }
+ DebugSay("My master told me to stop.");
Mobile.Direction = Mobile.GetDirectionTo(Mobile.ControlMaster);
Mobile.Home = Mobile.Location;
@@ -1832,10 +1737,7 @@ public abstract class BaseAI
if (from?.Deleted == false && to?.Deleted == false && from != to && to.Player)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"Begin transfer with {to.Name}");
- }
+ this.DebugSayFormatted($"Begin transfer with {to.Name}");
var youngFrom = from is PlayerMobile mobile && mobile.Young;
var youngTo = to is PlayerMobile playerMobile && playerMobile.Young;
@@ -1911,20 +1813,14 @@ public abstract class BaseAI
{
if (Core.Now < Mobile.BardEndTime)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I am pacified, I wait");
- }
+ DebugSay("I am pacified, I wait");
Mobile.Combatant = null;
Mobile.Warmode = false;
}
else
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I'm not pacified any longer");
- }
+ DebugSay("I'm not pacified any longer");
Mobile.BardPacified = false;
}
@@ -1939,10 +1835,7 @@ public abstract class BaseAI
Mobile.BardMaster.Map != Mobile.Map || Mobile.GetDistanceToSqrt(Mobile.BardMaster) >
Mobile.RangePerception))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have lost my provoker");
- }
+ DebugSay("I have lost my provoker");
Mobile.BardProvoked = false;
Mobile.BardMaster = null;
@@ -1954,10 +1847,7 @@ public abstract class BaseAI
else if (Mobile.BardTarget?.Deleted != false || Mobile.BardTarget.Map != Mobile.Map ||
Mobile.GetDistanceToSqrt(Mobile.BardTarget) > Mobile.RangePerception)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("I have lost my provoke target");
- }
+ DebugSay("I have lost my provoke target");
Mobile.BardProvoked = false;
Mobile.BardMaster = null;
@@ -2172,10 +2062,7 @@ public abstract class BaseAI
if (canOpenDoors || canDestroyObstacles)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("My movement was blocked, I will try to clear some obstacles.");
- }
+ DebugSay("My movement was blocked, I will try to clear some obstacles.");
var map = Mobile.Map;
@@ -2236,28 +2123,15 @@ public abstract class BaseAI
if (item is BaseDoor door)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay(
- "Little do they expect, I've learned how to open doors. Didn't they read the script??"
- );
- }
+ DebugSay("Little do they expect, I've learned how to open doors. Didn't they read the script??");
- if (Mobile.Debug)
- {
- Mobile.DebugSay("*twist*");
- }
+ DebugSay("*twist*");
door.Use(Mobile);
}
else
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay(
- $"Ugabooga. I'm so big and tough I can destroy it: {item.GetType().Name}"
- );
- }
+ this.DebugSayFormatted($"Ugabooga. I'm so big and tough I can destroy it: {item.GetType().Name}");
if (item is Container cont)
{
@@ -2410,10 +2284,7 @@ public abstract class BaseAI
{
if (_path != null)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Teleported; repathing");
- }
+ DebugSay("Teleported; repathing");
_path.ForceRepath();
}
@@ -2578,10 +2449,7 @@ public abstract class BaseAI
if (Mobile.ConstantFocus != null)
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Acquired my constant focus");
- }
+ DebugSay("Acquired my constant focus");
Mobile.FocusMob = Mobile.ConstantFocus;
return true;
@@ -2608,10 +2476,7 @@ public abstract class BaseAI
Mobile.NextReacquireTime = Core.TickCount + (int)Mobile.ReacquireDelay.TotalMilliseconds;
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Acquiring...");
- }
+ DebugSay("Acquiring...");
var map = Mobile.Map;
@@ -2806,10 +2671,7 @@ public abstract class BaseAI
return;
}
- if (Mobile.Debug)
- {
- Mobile.DebugSay("Checking for hidden players");
- }
+ DebugSay("Checking for hidden players");
var srcSkill = Mobile.Skills.DetectHidden.Value;
@@ -2823,10 +2685,7 @@ public abstract class BaseAI
if (trg != Mobile && trg.Player && trg.Alive && trg.Hidden && trg.AccessLevel == AccessLevel.Player &&
Mobile.InLOS(trg))
{
- if (Mobile.Debug)
- {
- Mobile.DebugSay($"Trying to detect {trg.Name}");
- }
+ this.DebugSayFormatted($"Trying to detect {trg.Name}");
var trgHiding = trg.Skills.Hiding.Value / 2.9;
var trgStealth = trg.Skills.Stealth.Value / 1.8;
diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAIExtensions.cs b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAIExtensions.cs
new file mode 100644
index 000000000..e46181c00
--- /dev/null
+++ b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAIExtensions.cs
@@ -0,0 +1,20 @@
+using System.Runtime.CompilerServices;
+using Server.Mobiles.AI.BaseAI;
+
+namespace Server.Mobiles;
+
+public static class BaseAIExtensions
+{
+ public static void DebugSayFormatted(this BaseAI ai,
+ [InterpolatedStringHandlerArgument("ai")] ref DebugInterpolatedStringHandler handler, int cooldownMs = 5000)
+ {
+ var message = handler.Text;
+ if (message.Length > 0)
+ {
+ ai.Mobile.PublicOverheadMessage(MessageType.Regular, 41, false, message.ToString());
+ ai.NextDebugMessage = Core.TickCount + cooldownMs;
+ }
+
+ handler.Clear();
+ }
+}
diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs b/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs
new file mode 100644
index 000000000..c2d5dcf15
--- /dev/null
+++ b/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs
@@ -0,0 +1,662 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using Server.Buffers;
+
+namespace Server.Mobiles.AI.BaseAI;
+
+[InterpolatedStringHandler]
+public ref struct DebugInterpolatedStringHandler
+{
+ // Implementation note:
+ // As this type lives in CompilerServices and is only intended to be targeted by the compiler,
+ // public APIs eschew argument validation logic in a variety of places, e.g. allowing a null input
+ // when one isn't expected to produce a NullReferenceException rather than an ArgumentNullException.
+
+ /// Expected average length of formatted data used for an individual interpolation expression result.
+ ///
+ /// This is inherited from string.Format, and could be changed based on further data.
+ /// string.Format actually uses `format.Length + args.Length * 8`, but format.Length
+ /// includes the format items themselves, e.g. "{0}", and since it's rare to have double-digit
+ /// numbers of items, we bump the 8 up to 11 to account for the three extra characters in "{d}",
+ /// since the compiler-provided base length won't include the equivalent character count.
+ ///
+ private const int GuessedLengthPerHole = 11;
+ /// Minimum size array to rent from the pool.
+ /// Same as stack-allocation size used today by string.Format.
+ private const int MinimumArrayPoolLength = 256;
+
+ /// Optional provider to pass to IFormattable.ToString or ISpanFormattable.TryFormat calls.
+ private readonly IFormatProvider? _provider;
+ /// Array rented from the array pool and used to back .
+ private char[]? _arrayToReturnToPool;
+ /// The span to write into.
+ private Span _chars;
+ /// Position at which to write the next character.
+ private int _pos;
+ /// Whether provides an ICustomFormatter.
+ ///
+ /// Custom formatters are very rare. We want to support them, but it's ok if we make them more expensive
+ /// in order to make them as pay-for-play as possible. So, we avoid adding another reference type field
+ /// to reduce the size of the handler and to reduce required zero'ing, by only storing whether the provider
+ /// provides a formatter, rather than actually storing the formatter. This in turn means, if there is a
+ /// formatter, we pay for the extra interface call on each AppendFormatted that needs it.
+ ///
+ private readonly bool _hasCustomFormatter;
+
+ private readonly bool _debugActive;
+
+ /// Creates a handler used to translate an interpolated string into a .
+ /// The number of constant characters outside of interpolation expressions in the interpolated string.
+ /// The number of interpolation expressions in the interpolated string.
+ /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.
+ public DebugInterpolatedStringHandler(int literalLength, int formattedCount, Mobiles.BaseAI ai)
+ {
+ _debugActive = ai.Mobile?.Debug == true && Core.TickCount >= ai.NextDebugMessage;
+
+ if (_debugActive)
+ {
+ _chars = _arrayToReturnToPool = STArrayPool.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
+ }
+ else
+ {
+ _chars = _arrayToReturnToPool = null;
+ }
+
+ _provider = null;
+ _pos = 0;
+ _hasCustomFormatter = false;
+ }
+
+ /// Creates a handler used to translate an interpolated string into a .
+ /// The number of constant characters outside of interpolation expressions in the interpolated string.
+ /// The number of interpolation expressions in the interpolated string.
+ /// An object that supplies culture-specific formatting information.
+ /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.
+ public DebugInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider, Mobiles.BaseAI ai)
+ {
+ _debugActive = (ai.Mobile as BaseCreature)?.Debug == true && Core.TickCount >= ai.NextDebugMessage;
+
+ if (_debugActive)
+ {
+ _chars = _arrayToReturnToPool = STArrayPool.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
+ }
+ else
+ {
+ _chars = _arrayToReturnToPool = null;
+ }
+
+ _provider = provider;
+ _pos = 0;
+ _hasCustomFormatter = provider is not null && HasCustomFormatter(provider);
+ }
+
+ /// Derives a default length with which to seed the handler.
+ /// The number of constant characters outside of interpolation expressions in the interpolated string.
+ /// The number of interpolation expressions in the interpolated string.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] // becomes a constant when inputs are constant
+ internal static int GetDefaultLength(int literalLength, int formattedCount) =>
+ Math.Max(MinimumArrayPoolLength, literalLength + formattedCount * GuessedLengthPerHole);
+
+ /// Clears the handler, returning any rented array to the pool.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] // used only on a few hot paths
+ public void Clear()
+ {
+ char[]? toReturn = _arrayToReturnToPool;
+ this = default; // defensive clear
+ if (toReturn is not null)
+ {
+ STArrayPool.Shared.Return(toReturn);
+ }
+ }
+
+ /// Gets a span of the written characters thus far.
+ public ReadOnlySpan Text => _chars[.._pos];
+
+ /// Writes the specified string to the handler.
+ /// The string to write.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AppendLiteral(string value)
+ {
+ if (!_debugActive)
+ {
+ return;
+ }
+
+ if (value.Length == 1)
+ {
+ Span chars = _chars;
+ int pos = _pos;
+ if ((uint)pos < (uint)chars.Length)
+ {
+ chars[pos] = value[0];
+ _pos = pos + 1;
+ }
+ else
+ {
+ GrowThenCopyString(value);
+ }
+ return;
+ }
+
+ AppendStringDirect(value);
+ }
+
+ /// Writes the specified string to the handler.
+ /// The string to write.
+ private void AppendStringDirect(string value)
+ {
+ if (value.TryCopyTo(_chars[_pos..]))
+ {
+ _pos += value.Length;
+ }
+ else
+ {
+ GrowThenCopyString(value);
+ }
+ }
+
+ #region AppendFormatted
+ // Design note:
+ // The compiler requires a AppendFormatted overload for anything that might be within an interpolation expression;
+ // if it can't find an appropriate overload, for handlers in general it'll simply fail to compile.
+ // (For target-typing to string where it uses DefaultInterpolatedStringHandler implicitly, it'll instead fall back to
+ // its other mechanisms, e.g. using string.Format. This fallback has the benefit that if we miss a case,
+ // interpolated strings will still work, but it has the downside that a developer generally won't know
+ // if the fallback is happening and they're paying more.)
+ //
+ // At a minimum, then, we would need an overload that accepts:
+ // (object value, int alignment = 0, string? format = null)
+ // Such an overload would provide the same expressiveness as string.Format. However, this has several
+ // shortcomings:
+ // - Every value type in an interpolation expression would be boxed.
+ // - ReadOnlySpan could not be used in interpolation expressions.
+ // - Every AppendFormatted call would have three arguments at the call site, bloating the IL further.
+ // - Every invocation would be more expensive, due to lack of specialization, every call needing to account
+ // for alignment and format, etc.
+ //
+ // To address that, we could just have overloads for T and ReadOnlySpan:
+ // (T)
+ // (T, int alignment)
+ // (T, string? format)
+ // (T, int alignment, string? format)
+ // (ReadOnlySpan)
+ // (ReadOnlySpan, int alignment)
+ // (ReadOnlySpan, string? format)
+ // (ReadOnlySpan, int alignment, string? format)
+ // but this also has shortcomings:
+ // - Some expressions that would have worked with an object overload will now force a fallback to string.Format
+ // (or fail to compile if the handler is used in places where the fallback isn't provided), because the compiler
+ // can't always target type to T, e.g. `b switch { true => 1, false => null }` where `b` is a bool can successfully
+ // be passed as an argument of type `object` but not of type `T`.
+ // - Reference types get no benefit from going through the generic code paths, and actually incur some overheads
+ // from doing so.
+ // - Nullable value types also pay a heavy price, in particular around interface checks that would generally evaporate
+ // at compile time for value types but don't (currently) if the Nullable goes through the same code paths
+ // (see https://github.com/dotnet/runtime/issues/50915).
+ //
+ // We could try to take a more elaborate approach for DefaultInterpolatedStringHandler, since it is the most common handler
+ // and we want to minimize overheads both at runtime and in IL size, e.g. have a complete set of overloads for each of:
+ // (T, ...) where T : struct
+ // (T?, ...) where T : struct
+ // (object, ...)
+ // (ReadOnlySpan, ...)
+ // (string, ...)
+ // but this also has shortcomings, most importantly:
+ // - If you have an unconstrained T that happens to be a value type, it'll now end up getting boxed to use the object overload.
+ // This also necessitates the T? overload, since nullable value types don't meet a T : struct constraint, so without those
+ // they'd all map to the object overloads as well.
+ // - Any reference type with an implicit cast to ROS will fail to compile due to ambiguities between the overloads. string
+ // is one such type, hence needing dedicated overloads for it that can be bound to more tightly.
+ //
+ // A middle ground we've settled on, which is likely to be the right approach for most other handlers as well, would be the set:
+ // (T, ...) with no constraint
+ // (ReadOnlySpan) and (ReadOnlySpan, int)
+ // (object, int alignment = 0, string? format = null)
+ // (string) and (string, int)
+ // This would address most of the concerns, at the expense of:
+ // - Most reference types going through the generic code paths and so being a bit more expensive.
+ // - Nullable types being more expensive until https://github.com/dotnet/runtime/issues/50915 is addressed.
+ // We could choose to add a T? where T : struct set of overloads if necessary.
+ // Strings don't require their own overloads here, but as they're expected to be very common and as we can
+ // optimize them in several ways (can copy the contents directly, don't need to do any interface checks, don't
+ // need to pay the shared generic overheads, etc.) we can add overloads specifically to optimize for them.
+ //
+ // Hole values are formatted according to the following policy:
+ // 1. If an IFormatProvider was supplied and it provides an ICustomFormatter, use ICustomFormatter.Format (even if the value is null).
+ // 2. If the type implements ISpanFormattable, use ISpanFormattable.TryFormat.
+ // 3. If the type implements IFormattable, use IFormattable.ToString.
+ // 4. Otherwise, use object.ToString.
+ // This matches the behavior of string.Format, StringBuilder.AppendFormat, etc. The only overloads for which this doesn't
+ // apply is ReadOnlySpan, which isn't supported by either string.Format nor StringBuilder.AppendFormat, but more
+ // importantly which can't be boxed to be passed to ICustomFormatter.Format.
+
+ #region AppendFormatted T
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ public void AppendFormatted(T value)
+ {
+ if (!_debugActive)
+ {
+ return;
+ }
+
+ // This method could delegate to AppendFormatted with a null format, but explicitly passing
+ // default as the format to TryFormat helps to improve code quality in some cases when TryFormat is inlined,
+ // e.g. for Int32 it enables the JIT to eliminate code in the inlined method based on a length check on the format.
+
+ // If there's a custom formatter, always use it.
+ if (_hasCustomFormatter)
+ {
+ AppendCustomFormatter(value, format: null);
+ return;
+ }
+
+ // Check first for IFormattable, even though we'll prefer to use ISpanFormattable, as the latter
+ // requires the former. For value types, it won't matter as the type checks devolve into
+ // JIT-time constants. For reference types, they're more likely to implement IFormattable
+ // than they are to implement ISpanFormattable: if they don't implement either, we save an
+ // interface check over first checking for ISpanFormattable and then for IFormattable, and
+ // if it only implements IFormattable, we come out even: only if it implements both do we
+ // end up paying for an extra interface check.
+ string? s;
+ if (value is IFormattable)
+ {
+ // If the value can format itself directly into our buffer, do so.
+ if (value is ISpanFormattable)
+ {
+ int charsWritten;
+ while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, default, _provider)) // constrained call avoiding boxing for value types
+ {
+ Grow();
+ }
+
+ _pos += charsWritten;
+ return;
+ }
+
+ s = ((IFormattable)value).ToString(format: null, _provider); // constrained call avoiding boxing for value types
+ }
+ else
+ {
+ s = value?.ToString();
+ }
+
+ if (s is not null)
+ {
+ AppendStringDirect(s);
+ }
+ }
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ /// The format string.
+ public void AppendFormatted(T value, string? format)
+ {
+ if (!_debugActive)
+ {
+ return;
+ }
+
+ // If there's a custom formatter, always use it.
+ if (_hasCustomFormatter)
+ {
+ AppendCustomFormatter(value, format);
+ return;
+ }
+
+ // Check first for IFormattable, even though we'll prefer to use ISpanFormattable, as the latter
+ // requires the former. For value types, it won't matter as the type checks devolve into
+ // JIT-time constants. For reference types, they're more likely to implement IFormattable
+ // than they are to implement ISpanFormattable: if they don't implement either, we save an
+ // interface check over first checking for ISpanFormattable and then for IFormattable, and
+ // if it only implements IFormattable, we come out even: only if it implements both do we
+ // end up paying for an extra interface check.
+ string? s;
+ if (value is IFormattable)
+ {
+ // If the value can format itself directly into our buffer, do so.
+ if (value is ISpanFormattable)
+ {
+ int charsWritten;
+ while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, format, _provider)) // constrained call avoiding boxing for value types
+ {
+ Grow();
+ }
+
+ _pos += charsWritten;
+ return;
+ }
+
+ s = ((IFormattable)value).ToString(format, _provider); // constrained call avoiding boxing for value types
+ }
+ else
+ {
+ s = value?.ToString();
+ }
+
+ if (s is not null)
+ {
+ AppendStringDirect(s);
+ }
+ }
+
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ /// Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.
+ public void AppendFormatted(T value, int alignment)
+ {
+ if (!_debugActive)
+ {
+ return;
+ }
+
+ int startingPos = _pos;
+ AppendFormatted(value);
+ if (alignment != 0)
+ {
+ AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
+ }
+ }
+
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ /// The format string.
+ /// Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.
+ public void AppendFormatted(T value, int alignment, string? format)
+ {
+ if (!_debugActive)
+ {
+ return;
+ }
+
+ int startingPos = _pos;
+ AppendFormatted(value, format);
+ if (alignment != 0)
+ {
+ AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
+ }
+ }
+ #endregion
+
+ #region AppendFormatted ReadOnlySpan
+ /// Writes the specified character span to the handler.
+ /// The span to write.
+ public void AppendFormatted(ReadOnlySpan value)
+ {
+ if (!_debugActive)
+ {
+ return;
+ }
+
+ // Fast path for when the value fits in the current buffer
+ if (value.TryCopyTo(_chars[_pos..]))
+ {
+ _pos += value.Length;
+ }
+ else
+ {
+ GrowThenCopySpan(value);
+ }
+ }
+
+ /// Writes the specified string of chars to the handler.
+ /// The span to write.
+ /// Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.
+ /// The format string.
+ public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null)
+ {
+ if (!_debugActive)
+ {
+ return;
+ }
+
+ bool leftAlign = false;
+ if (alignment < 0)
+ {
+ leftAlign = true;
+ alignment = -alignment;
+ }
+
+ int paddingRequired = alignment - value.Length;
+ if (paddingRequired <= 0)
+ {
+ // The value is as large or larger than the required amount of padding,
+ // so just write the value.
+ AppendFormatted(value);
+ return;
+ }
+
+ // Write the value along with the appropriate padding.
+ EnsureCapacityForAdditionalChars(value.Length + paddingRequired);
+ if (leftAlign)
+ {
+ value.CopyTo(_chars[_pos..]);
+ _pos += value.Length;
+ _chars.Slice(_pos, paddingRequired).Fill(' ');
+ _pos += paddingRequired;
+ }
+ else
+ {
+ _chars.Slice(_pos, paddingRequired).Fill(' ');
+ _pos += paddingRequired;
+ value.CopyTo(_chars[_pos..]);
+ _pos += value.Length;
+ }
+ }
+ #endregion
+
+ #region AppendFormatted string
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ public void AppendFormatted(string? value)
+ {
+ if (!_debugActive)
+ {
+ return;
+ }
+
+ // Fast-path for no custom formatter and a non-null string that fits in the current destination buffer.
+ if (!_hasCustomFormatter && value?.TryCopyTo(_chars[_pos..]) == true)
+ {
+ _pos += value.Length;
+ }
+ else
+ {
+ AppendFormattedSlow(value);
+ }
+ }
+
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ ///
+ /// Slow path to handle a custom formatter, potentially null value,
+ /// or a string that doesn't fit in the current buffer.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void AppendFormattedSlow(string? value)
+ {
+ if (_hasCustomFormatter)
+ {
+ AppendCustomFormatter(value, format: null);
+ }
+ else if (value is not null)
+ {
+ EnsureCapacityForAdditionalChars(value.Length);
+ value.CopyTo(_chars[_pos..]);
+ _pos += value.Length;
+ }
+ }
+
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ /// Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.
+ /// The format string.
+ public void AppendFormatted(string? value, int alignment = 0, string? format = null) =>
+ // Format is meaningless for strings and doesn't make sense for someone to specify. We have the overload
+ // simply to disambiguate between ROS and object, just in case someone does specify a format, as
+ // string is implicitly convertible to both. Just delegate to the T-based implementation.
+ AppendFormatted(value, alignment, format);
+ #endregion
+
+ #region AppendFormatted object
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ /// Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value.
+ /// The format string.
+ public void AppendFormatted(object? value, int alignment = 0, string? format = null) =>
+ // This overload is expected to be used rarely, only if either a) something strongly typed as object is
+ // formatted with both an alignment and a format, or b) the compiler is unable to target type to T. It
+ // exists purely to help make cases from (b) compile. Just delegate to the T-based implementation.
+ AppendFormatted