fix: Consolidates debug check messages for AI and adds debounce (#2235)

This commit is contained in:
Kamron Batman 2025-07-18 18:08:03 -07:00 committed by GitHub
parent 8f88de68a7
commit f89150735e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 838 additions and 594 deletions

View file

@ -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");
}
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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();
}
}

View file

@ -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.
/// <summary>Expected average length of formatted data used for an individual interpolation expression result.</summary>
/// <remarks>
/// 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.
/// </remarks>
private const int GuessedLengthPerHole = 11;
/// <summary>Minimum size array to rent from the pool.</summary>
/// <remarks>Same as stack-allocation size used today by string.Format.</remarks>
private const int MinimumArrayPoolLength = 256;
/// <summary>Optional provider to pass to IFormattable.ToString or ISpanFormattable.TryFormat calls.</summary>
private readonly IFormatProvider? _provider;
/// <summary>Array rented from the array pool and used to back <see cref="_chars"/>.</summary>
private char[]? _arrayToReturnToPool;
/// <summary>The span to write into.</summary>
private Span<char> _chars;
/// <summary>Position at which to write the next character.</summary>
private int _pos;
/// <summary>Whether <see cref="_provider"/> provides an ICustomFormatter.</summary>
/// <remarks>
/// 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.
/// </remarks>
private readonly bool _hasCustomFormatter;
private readonly bool _debugActive;
/// <summary>Creates a handler used to translate an interpolated string into a <see cref="string"/>.</summary>
/// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
/// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
/// <remarks>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.</remarks>
public DebugInterpolatedStringHandler(int literalLength, int formattedCount, Mobiles.BaseAI ai)
{
_debugActive = ai.Mobile?.Debug == true && Core.TickCount >= ai.NextDebugMessage;
if (_debugActive)
{
_chars = _arrayToReturnToPool = STArrayPool<char>.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
}
else
{
_chars = _arrayToReturnToPool = null;
}
_provider = null;
_pos = 0;
_hasCustomFormatter = false;
}
/// <summary>Creates a handler used to translate an interpolated string into a <see cref="string"/>.</summary>
/// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
/// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
/// <param name="provider">An object that supplies culture-specific formatting information.</param>
/// <remarks>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.</remarks>
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<char>.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
}
else
{
_chars = _arrayToReturnToPool = null;
}
_provider = provider;
_pos = 0;
_hasCustomFormatter = provider is not null && HasCustomFormatter(provider);
}
/// <summary>Derives a default length with which to seed the handler.</summary>
/// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
/// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] // becomes a constant when inputs are constant
internal static int GetDefaultLength(int literalLength, int formattedCount) =>
Math.Max(MinimumArrayPoolLength, literalLength + formattedCount * GuessedLengthPerHole);
/// <summary>Clears the handler, returning any rented array to the pool.</summary>
[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<char>.Shared.Return(toReturn);
}
}
/// <summary>Gets a span of the written characters thus far.</summary>
public ReadOnlySpan<char> Text => _chars[.._pos];
/// <summary>Writes the specified string to the handler.</summary>
/// <param name="value">The string to write.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendLiteral(string value)
{
if (!_debugActive)
{
return;
}
if (value.Length == 1)
{
Span<char> chars = _chars;
int pos = _pos;
if ((uint)pos < (uint)chars.Length)
{
chars[pos] = value[0];
_pos = pos + 1;
}
else
{
GrowThenCopyString(value);
}
return;
}
AppendStringDirect(value);
}
/// <summary>Writes the specified string to the handler.</summary>
/// <param name="value">The string to write.</param>
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<char> 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<char>:
// (T)
// (T, int alignment)
// (T, string? format)
// (T, int alignment, string? format)
// (ReadOnlySpan<char>)
// (ReadOnlySpan<char>, int alignment)
// (ReadOnlySpan<char>, string? format)
// (ReadOnlySpan<char>, 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<T> 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<char>, ...)
// (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<char> 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<char>) and (ReadOnlySpan<char>, 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<char>, 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
/// <summary>Writes the specified value to the handler.</summary>
/// <param name="value">The value to write.</param>
public void AppendFormatted<T>(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);
}
}
/// <summary>Writes the specified value to the handler.</summary>
/// <param name="value">The value to write.</param>
/// <param name="format">The format string.</param>
public void AppendFormatted<T>(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);
}
}
/// <summary>Writes the specified value to the handler.</summary>
/// <param name="value">The value to write.</param>
/// <param name="alignment">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.</param>
public void AppendFormatted<T>(T value, int alignment)
{
if (!_debugActive)
{
return;
}
int startingPos = _pos;
AppendFormatted(value);
if (alignment != 0)
{
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
}
}
/// <summary>Writes the specified value to the handler.</summary>
/// <param name="value">The value to write.</param>
/// <param name="format">The format string.</param>
/// <param name="alignment">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.</param>
public void AppendFormatted<T>(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<char>
/// <summary>Writes the specified character span to the handler.</summary>
/// <param name="value">The span to write.</param>
public void AppendFormatted(ReadOnlySpan<char> 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);
}
}
/// <summary>Writes the specified string of chars to the handler.</summary>
/// <param name="value">The span to write.</param>
/// <param name="alignment">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.</param>
/// <param name="format">The format string.</param>
public void AppendFormatted(ReadOnlySpan<char> 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
/// <summary>Writes the specified value to the handler.</summary>
/// <param name="value">The value to write.</param>
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);
}
}
/// <summary>Writes the specified value to the handler.</summary>
/// <param name="value">The value to write.</param>
/// <remarks>
/// Slow path to handle a custom formatter, potentially null value,
/// or a string that doesn't fit in the current buffer.
/// </remarks>
[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;
}
}
/// <summary>Writes the specified value to the handler.</summary>
/// <param name="value">The value to write.</param>
/// <param name="alignment">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.</param>
/// <param name="format">The format string.</param>
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<char> 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<string?>(value, alignment, format);
#endregion
#region AppendFormatted object
/// <summary>Writes the specified value to the handler.</summary>
/// <param name="value">The value to write.</param>
/// <param name="alignment">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.</param>
/// <param name="format">The format string.</param>
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<object?>(value, alignment, format);
#endregion
#endregion
/// <summary>Gets whether the provider provides a custom formatter.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] // only used in a few hot path call sites
internal static bool HasCustomFormatter(IFormatProvider provider)
{
Debug.Assert(provider is not null);
Debug.Assert(provider is not CultureInfo || provider.GetFormat(typeof(ICustomFormatter)) is null, "Expected CultureInfo to not provide a custom formatter");
return
provider.GetType() != typeof(CultureInfo) && // optimization to avoid GetFormat in the majority case
provider.GetFormat(typeof(ICustomFormatter)) != null;
}
/// <summary>Formats the value using the custom formatter from the provider.</summary>
/// <param name="value">The value to write.</param>
/// <param name="format">The format string.</param>
[MethodImpl(MethodImplOptions.NoInlining)]
private void AppendCustomFormatter<T>(T value, string? format)
{
// This case is very rare, but we need to handle it prior to the other checks in case
// a provider was used that supplied an ICustomFormatter which wanted to intercept the particular value.
// We do the cast here rather than in the ctor, even though this could be executed multiple times per
// formatting, to make the cast pay for play.
Debug.Assert(_hasCustomFormatter);
Debug.Assert(_provider != null);
ICustomFormatter? formatter = (ICustomFormatter?)_provider.GetFormat(typeof(ICustomFormatter));
Debug.Assert(formatter != null, "An incorrectly written provider said it implemented ICustomFormatter, and then didn't");
if (formatter?.Format(format, value, _provider) is string customFormatted)
{
AppendStringDirect(customFormatted);
}
}
/// <summary>Handles adding any padding required for aligning a formatted value in an interpolation expression.</summary>
/// <param name="startingPos">The position at which the written value started.</param>
/// <param name="alignment">Non-zero 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.</param>
private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment)
{
Debug.Assert(startingPos >= 0 && startingPos <= _pos);
Debug.Assert(alignment != 0);
int charsWritten = _pos - startingPos;
bool leftAlign = false;
if (alignment < 0)
{
leftAlign = true;
alignment = -alignment;
}
int paddingNeeded = alignment - charsWritten;
if (paddingNeeded > 0)
{
EnsureCapacityForAdditionalChars(paddingNeeded);
if (leftAlign)
{
_chars.Slice(_pos, paddingNeeded).Fill(' ');
}
else
{
_chars.Slice(startingPos, charsWritten).CopyTo(_chars[(startingPos + paddingNeeded)..]);
_chars.Slice(startingPos, paddingNeeded).Fill(' ');
}
_pos += paddingNeeded;
}
}
/// <summary>Ensures <see cref="_chars"/> has the capacity to store <paramref name="additionalChars"/> beyond <see cref="_pos"/>.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacityForAdditionalChars(int additionalChars)
{
if (_chars.Length - _pos < additionalChars)
{
Grow(additionalChars);
}
}
/// <summary>Fallback for fast path in <see cref="AppendStringDirect"/> when there's not enough space in the destination.</summary>
/// <param name="value">The string to write.</param>
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowThenCopyString(string value)
{
Grow(value.Length);
value.CopyTo(_chars[_pos..]);
_pos += value.Length;
}
/// <summary>Fallback for <see cref="AppendFormatted(ReadOnlySpan{char})"/> for when not enough space exists in the current buffer.</summary>
/// <param name="value">The span to write.</param>
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowThenCopySpan(ReadOnlySpan<char> value)
{
Grow(value.Length);
value.CopyTo(_chars[_pos..]);
_pos += value.Length;
}
/// <summary>Grows <see cref="_chars"/> to have the capacity to store at least <paramref name="additionalChars"/> beyond <see cref="_pos"/>.</summary>
[MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible
private void Grow(int additionalChars)
{
// This method is called when the remaining space (_chars.Length - _pos) is
// insufficient to store a specific number of additional characters. Thus, we
// need to grow to at least that new total. GrowCore will handle growing by more
// than that if possible.
Debug.Assert(additionalChars > _chars.Length - _pos);
GrowCore((uint)_pos + (uint)additionalChars);
}
/// <summary>Grows the size of <see cref="_chars"/>.</summary>
[MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible
private void Grow()
{
// This method is called when the remaining space in _chars isn't sufficient to continue
// the operation. Thus, we need at least one character beyond _chars.Length. GrowCore
// will handle growing by more than that if possible.
GrowCore((uint)_chars.Length + 1);
}
/// <summary>Grow the size of <see cref="_chars"/> to at least the specified <paramref name="requiredMinCapacity"/>.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] // but reuse this grow logic directly in both of the above grow routines
private void GrowCore(uint requiredMinCapacity)
{
// We want the max of how much space we actually required and doubling our capacity (without going beyond the max allowed length). We
// also want to avoid asking for small arrays, to reduce the number of times we need to grow, and since we're working with unsigned
// ints that could technically overflow if someone tried to, for example, append a huge string to a huge string, we also clamp to int.MaxValue.
// Even if the array creation fails in such a case, we may later fail in ToStringAndClear.
uint newCapacity = Math.Max(requiredMinCapacity, Math.Min((uint)_chars.Length * 2, 0x3FFFFFDF));
int arraySize = (int)Math.Clamp(newCapacity, MinimumArrayPoolLength, int.MaxValue);
char[] newArray = STArrayPool<char>.Shared.Rent(arraySize);
_chars[.._pos].CopyTo(newArray);
char[]? toReturn = _arrayToReturnToPool;
_chars = _arrayToReturnToPool = newArray;
if (toReturn is not null)
{
STArrayPool<char>.Shared.Return(toReturn);
}
}
}

View file

@ -8,17 +8,11 @@ public class BerserkAI : BaseAI
public override bool DoActionWander()
{
if (Mobile.Debug)
{
Mobile.DebugSay("I have no combatant");
}
DebugSay("I have no combatant");
if (AcquireFocusMob(Mobile.RangePerception, FightMode.Closest, false, true, 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;
@ -38,10 +32,7 @@ public class BerserkAI : 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;
@ -49,17 +40,11 @@ public class BerserkAI : BaseAI
if (!WalkMobileRange(combatant, 1, true, Mobile.RangeFight, Mobile.RangeFight))
{
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}");
Action = ActionType.Guard;
return true;
@ -72,10 +57,7 @@ public class BerserkAI : 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;
@ -85,10 +67,7 @@ public class BerserkAI : BaseAI
{
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, true, 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;

View file

@ -57,10 +57,7 @@ public class HealerAI : BaseAI
{
if (NeedCure(toHelp))
{
if (Mobile.Debug)
{
Mobile.DebugSay($"{toHelp.Name} needs a cure");
}
this.DebugSayFormatted($"{toHelp.Name} needs a cure");
if (!new CureSpell(Mobile).Cast())
{
@ -69,10 +66,7 @@ public class HealerAI : BaseAI
}
else if (NeedGHeal(toHelp))
{
if (Mobile.Debug)
{
Mobile.DebugSay($"{toHelp.Name} needs a greater heal");
}
this.DebugSayFormatted($"{toHelp.Name} needs a greater heal");
if (!new GreaterHealSpell(Mobile).Cast())
{
@ -81,10 +75,7 @@ public class HealerAI : BaseAI
}
else if (NeedLHeal(toHelp))
{
if (Mobile.Debug)
{
Mobile.DebugSay($"{toHelp.Name} needs a lesser heal");
}
this.DebugSayFormatted($"{toHelp.Name} needs a lesser heal");
new HealSpell(Mobile).Cast();
}
@ -103,10 +94,7 @@ public class HealerAI : BaseAI
// TODO: Should it be able to do this?
if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, Mobile.Combatant))
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I used my abilities on {Mobile.Combatant.Name}!");
}
this.DebugSayFormatted($"I used my abilities on {Mobile.Combatant.Name}!");
}
return true;

View file

@ -83,10 +83,7 @@ public class MageAI : BaseAI
{
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I am going to attack {Mobile.FocusMob.Name}");
}
this.DebugSayFormatted($"I am going to attack {Mobile.FocusMob.Name}");
Mobile.Combatant = Mobile.FocusMob;
Action = ActionType.Combat;
@ -94,19 +91,13 @@ public class MageAI : BaseAI
}
else if (SmartAI && Mobile.Mana < Mobile.ManaMax && !Mobile.Meditating)
{
if (Mobile.Debug)
{
Mobile.DebugSay("I am going to meditate");
}
DebugSay("I am going to meditate");
Mobile.UseSkill(SkillName.Meditation);
}
else
{
if (Mobile.Debug)
{
Mobile.DebugSay("I am wandering");
}
DebugSay("I am wandering");
Mobile.Warmode = false;
@ -225,24 +216,18 @@ public class MageAI : BaseAI
new TeleportSpell(Mobile).Cast();
if (Mobile.Debug)
{
Mobile.DebugSay("I am stuck, I'm going to try teleporting away");
}
DebugSay("I am stuck, I'm going to try teleporting away");
}
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");
}
}
@ -415,20 +400,14 @@ public class MageAI : BaseAI
goto default;
}
if (Mobile.Debug)
{
Mobile.DebugSay("Attempting to poison");
}
DebugSay("Attempting to poison");
spell = new PoisonSpell(Mobile);
break;
}
case 2: // Bless ourselves
{
if (Mobile.Debug)
{
Mobile.DebugSay("Blessing myself");
}
DebugSay("Blessing myself");
spell = new BlessSpell(Mobile);
break;
@ -436,10 +415,7 @@ public class MageAI : BaseAI
case 3:
case 4: // Curse them
{
if (Mobile.Debug)
{
Mobile.DebugSay("Attempting to curse");
}
DebugSay("Attempting to curse");
spell = GetRandomCurseSpell();
break;
@ -451,20 +427,14 @@ public class MageAI : BaseAI
goto default;
}
if (Mobile.Debug)
{
Mobile.DebugSay("Attempting to paralyze");
}
DebugSay("Attempting to paralyze");
spell = new ParalyzeSpell(Mobile);
break;
}
case 6: // Drain mana
{
if (Mobile.Debug)
{
Mobile.DebugSay("Attempting to drain mana");
}
DebugSay("Attempting to drain mana");
spell = GetRandomManaDrainSpell();
break;
@ -476,20 +446,14 @@ public class MageAI : BaseAI
goto default;
}
if (Mobile.Debug)
{
Mobile.DebugSay("Attempting to invis myself");
}
DebugSay("Attempting to invis myself");
spell = new InvisibilitySpell(Mobile);
break;
}
default: // Damage them
{
if (Mobile.Debug)
{
Mobile.DebugSay("Just doing damage");
}
DebugSay("Just doing damage");
spell = GetRandomDamageSpell();
break;
@ -525,10 +489,7 @@ public class MageAI : BaseAI
{
if (c.Paralyzed && !c.Poisoned && !Mobile.Meditating)
{
if (Mobile.Debug)
{
Mobile.DebugSay("I am going to meditate");
}
DebugSay("I am going to meditate");
Mobile.UseSkill(SkillName.Meditation);
}
@ -557,18 +518,7 @@ public class MageAI : BaseAI
}
}
if (Mobile.Debug)
{
if (spell != null)
{
Mobile.DebugSay($"Casting {spell.Name}");
}
else
{
Mobile.DebugSay("I don't have a spell to use!");
}
}
DebugSay(spell != null ? $"Casting {spell.Name}" : "I don't have a spell to use!");
return spell;
}
@ -677,21 +627,12 @@ public class MageAI : BaseAI
{
Mobile.Combatant = c = Mobile.FocusMob!;
if (Mobile.Debug)
{
Mobile.DebugSay(
$"Something happened to my combatant, so I am going to fight {c.Name}"
);
}
this.DebugSayFormatted($"Something happened to my combatant, so I am going to fight {c.Name}");
Mobile.FocusMob = null;
}
else
{
if (Mobile.Debug)
{
Mobile.DebugSay("Something happened to my combatant, and nothing is around. I am on guard.");
}
DebugSay("Something happened to my combatant, and nothing is around. I am on guard.");
Action = ActionType.Guard;
return true;
@ -700,19 +641,13 @@ public class MageAI : BaseAI
if (!Mobile.InLOS(c))
{
if (Mobile.Debug)
{
Mobile.DebugSay("I can't see my target");
}
DebugSay("I can't see my target");
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
Mobile.Combatant = c = Mobile.FocusMob!;
if (Mobile.Debug)
{
Mobile.DebugSay($"I will switch to {c.Name}");
}
this.DebugSayFormatted($"I will switch to {c.Name}");
Mobile.FocusMob = null;
}
@ -742,10 +677,7 @@ public class MageAI : BaseAI
if (c == null)
{
if (Mobile.Debug)
{
Mobile.DebugSay("My combatant has fled, so I am on guard");
}
DebugSay("My combatant has fled, so I am on guard");
Action = ActionType.Guard;
return true;
@ -760,10 +692,7 @@ public class MageAI : BaseAI
if (Utility.Random(0, 100) > fleeChance)
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I am going to flee from {c.Name}");
}
this.DebugSayFormatted($"I am going to flee from {c.Name}");
Action = ActionType.Flee;
return true;
@ -772,10 +701,7 @@ public class MageAI : BaseAI
if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, c))
{
if (Mobile.Debug)
{
Mobile.DebugSay("I used my abilities!");
}
DebugSay("I used my abilities!");
}
else if (Mobile.Spell == null && Core.TickCount - _nextCastTime >= 0)
{
@ -785,19 +711,13 @@ public class MageAI : BaseAI
if (Mobile.Poisoned) // Top cast priority is cure
{
if (Mobile.Debug)
{
Mobile.DebugSay("I am going to cure myself");
}
DebugSay("I am going to cure myself");
spell = new CureSpell(Mobile);
}
else if (toDispel != null) // Something dispellable is attacking us
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I am going to dispel {toDispel}");
}
this.DebugSayFormatted($"I am going to dispel {toDispel}");
spell = DoDispel(toDispel); // May return null if dumb AI and doesn't have enough skill
}
@ -869,10 +789,7 @@ public class MageAI : BaseAI
}
else if (Mobile.Spell == null && Core.TickCount - _nextCastTime >= 0)
{
if (Mobile.Debug)
{
Mobile.DebugSay("I am going to reveal my last target");
}
DebugSay("I am going to reveal my last target");
_revealTarget = new LandTarget(_lastTargetLoc, map);
Spell spell = new RevealSpell(Mobile);
@ -888,10 +805,7 @@ public class MageAI : BaseAI
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I am going to attack {Mobile.FocusMob.Name}");
}
this.DebugSayFormatted($"I am going to attack {Mobile.FocusMob.Name}");
Mobile.Combatant = Mobile.FocusMob;
Action = ActionType.Combat;
@ -919,19 +833,13 @@ public class MageAI : BaseAI
if ((Mobile.Mana > 20 || Mobile.Mana == Mobile.ManaMax) && Mobile.Hits > Mobile.HitsMax / 2)
{
if (Mobile.Debug)
{
Mobile.DebugSay("I am stronger now, my guard is up");
}
DebugSay("I am stronger now, my guard is up");
Action = ActionType.Guard;
}
else if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I am scared of {Mobile.FocusMob.Name}");
}
this.DebugSayFormatted($"I am scared of {Mobile.FocusMob.Name}");
RunFrom(Mobile.FocusMob);
Mobile.FocusMob = null;
@ -943,10 +851,7 @@ public class MageAI : BaseAI
}
else
{
if (Mobile.Debug)
{
Mobile.DebugSay("Area seems clear, but my guard is up");
}
DebugSay("Area seems clear, but my guard is up");
Action = ActionType.Guard;
Mobile.Warmode = true;

View file

@ -12,20 +12,14 @@ public class MeleeAI : 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;
}
else
{
if (Mobile.Debug)
{
Mobile.DebugSay("I am wandering");
}
DebugSay("I am wandering");
Mobile.Warmode = false;
@ -42,10 +36,7 @@ public class MeleeAI : 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;
@ -69,10 +60,7 @@ public class MeleeAI : BaseAI
if (combatant == null)
{
if (Mobile.Debug)
{
Mobile.DebugSay("My combatant has fled, so I am on guard");
}
DebugSay("My combatant has fled, so I am on guard");
Action = ActionType.Guard;
return true;
@ -88,10 +76,7 @@ public class MeleeAI : BaseAI
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;
@ -100,19 +85,13 @@ public class MeleeAI : BaseAI
if (Mobile.GetDistanceToSqrt(combatant) > Mobile.RangePerception + 1)
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I cannot find {combatant.Name}, so my guard is up");
}
this.DebugSayFormatted($"I cannot find {combatant.Name}, so my guard is up");
Action = ActionType.Guard;
return true;
}
if (Mobile.Debug)
{
Mobile.DebugSay($"I cannot find {combatant.Name}, so my guard is up");
}
this.DebugSayFormatted($"I cannot find {combatant.Name}, so my guard is up");
}
else if (Core.TickCount - Mobile.LastMoveTime > 400)
{
@ -128,10 +107,7 @@ public class MeleeAI : BaseAI
var fleeChance = 10 + Math.Max(0, combatant.Hits - Mobile.Hits); // (10 + diff)% chance to flee;
if (Utility.Random(0, 100) < fleeChance)
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I am going to flee from {combatant.Name}");
}
this.DebugSayFormatted($"I am going to flee from {combatant.Name}");
Action = ActionType.Flee;
return true;
@ -141,10 +117,7 @@ public class MeleeAI : BaseAI
if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant))
{
if (Mobile.Debug)
{
Mobile.DebugSay("I used my abilities!");
}
DebugSay("I used my abilities!");
}
return true;
@ -154,10 +127,7 @@ public class MeleeAI : 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;
@ -174,10 +144,7 @@ public class MeleeAI : BaseAI
{
if (Mobile.Hits > Mobile.HitsMax / 2)
{
if (Mobile.Debug)
{
Mobile.DebugSay("I am stronger now, so I will continue fighting");
}
DebugSay("I am stronger now, so I will continue fighting");
Mobile.PlaySound(Mobile.GetAttackSound());
Mobile.CurrentSpeed = Mobile.ActiveSpeed;

View file

@ -10,19 +10,13 @@ public class PredatorAI : BaseAI
{
if (Mobile.Combatant != null)
{
if (Mobile.Debug)
{
Mobile.DebugSay("I am hurt or being attacked, I kill him");
}
DebugSay("I am hurt or being attacked, I kill him");
Action = ActionType.Combat;
}
else if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, true, false, true))
{
if (Mobile.Debug)
{
Mobile.DebugSay("There is something near, I go away");
}
DebugSay("There is something near, I go away");
Action = ActionType.Backoff;
}
@ -41,10 +35,7 @@ public class PredatorAI : 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;
@ -54,27 +45,18 @@ public class PredatorAI : 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}");
}
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;
@ -90,20 +72,14 @@ public class PredatorAI : 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;
}

View file

@ -13,17 +13,11 @@ public class ThiefAI : 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}, attacking");
}
this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking");
Mobile.Combatant = Mobile.FocusMob;
Action = ActionType.Combat;
@ -43,10 +37,7 @@ public class ThiefAI : 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;
@ -54,10 +45,7 @@ public class ThiefAI : BaseAI
if (!WalkMobileRange(combatant, 1, true, Mobile.RangeFight, Mobile.RangeFight))
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I should be closer to {combatant.Name}");
}
this.DebugSayFormatted($"I should be closer to {combatant.Name}");
}
else
{
@ -76,20 +64,14 @@ public class ThiefAI : BaseAI
Core.TickCount - Mobile.NextSkillTime >= 0 && _toDisarm.LootType != LootType.Blessed &&
_toDisarm.LootType != LootType.Newbied)
{
if (Mobile.Debug)
{
Mobile.DebugSay("Trying to steal from combatant.");
}
DebugSay("Trying to steal from combatant.");
Mobile.UseSkill(SkillName.Stealing);
Mobile.Target?.Invoke(Mobile, _toDisarm);
}
else if (_toDisarm == null && Core.TickCount - Mobile.NextSkillTime >= 0)
{
if (Mobile.Debug)
{
Mobile.DebugSay($"Trying to steal from {combatant.Name}.");
}
this.DebugSayFormatted($"Trying to steal from {combatant.Name}.");
bool didSteal = TryStealFrom<Bandage>(combatant);
didSteal = TryStealFrom<Nightshade>(combatant) || didSteal;
@ -98,10 +80,7 @@ public class ThiefAI : BaseAI
if (!didSteal)
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I am going to flee from {combatant.Name}");
}
this.DebugSayFormatted($"I am going to flee from {combatant.Name}");
Action = ActionType.Flee;
return true;
@ -117,10 +96,7 @@ public class ThiefAI : BaseAI
if (Utility.Random(0, 100) > fleeChance)
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I am going to flee from {combatant.Name}");
}
this.DebugSayFormatted($"I am going to flee from {combatant.Name}");
Action = ActionType.Flee;
}
@ -130,10 +106,7 @@ public class ThiefAI : BaseAI
if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, Mobile.Combatant))
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I used my abilities on {Mobile.Combatant.Name}!");
}
this.DebugSayFormatted($"I used my abilities on {Mobile.Combatant.Name}!");
}
return true;
}
@ -155,10 +128,7 @@ public class ThiefAI : 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;
@ -175,10 +145,7 @@ public class ThiefAI : BaseAI
{
if (Mobile.Hits > Mobile.HitsMax / 2)
{
if (Mobile.Debug)
{
Mobile.DebugSay("I am stronger now, so I will continue fighting");
}
DebugSay("I am stronger now, so I will continue fighting");
Action = ActionType.Combat;
}

View file

@ -15,17 +15,11 @@ public class VendorAI : BaseAI
public override bool DoActionWander()
{
if (Mobile.Debug)
{
Mobile.DebugSay("I'm fine");
}
DebugSay("I'm fine");
if (Mobile.Combatant != null)
{
if (Mobile.Debug)
{
Mobile.DebugSay($"{Mobile.Combatant.Name} is attacking me");
}
this.DebugSayFormatted($"{Mobile.Combatant.Name} is attacking me");
Mobile.Say(GetRandomGuardMessage());
Action = ActionType.Flee;
@ -34,10 +28,7 @@ public class VendorAI : BaseAI
{
if (Mobile.FocusMob != null)
{
if (Mobile.Debug)
{
Mobile.DebugSay($"{Mobile.FocusMob.Name} has talked to me");
}
this.DebugSayFormatted($"{Mobile.FocusMob.Name} has talked to me");
Action = ActionType.Interact;
}
@ -58,10 +49,7 @@ public class VendorAI : BaseAI
if (Mobile.Combatant != null)
{
if (Mobile.Debug)
{
Mobile.DebugSay($"{Mobile.Combatant.Name} is attacking me");
}
this.DebugSayFormatted($"{Mobile.Combatant.Name} is attacking me");
Mobile.Say(GetRandomGuardMessage());
@ -72,10 +60,7 @@ public class VendorAI : BaseAI
if (customer?.Deleted != false || customer.Map != Mobile.Map)
{
if (Mobile.Debug)
{
Mobile.DebugSay("My customer have disapeared");
}
DebugSay("My customer have disapeared");
Mobile.FocusMob = null;
@ -83,19 +68,13 @@ public class VendorAI : BaseAI
}
else if (customer.InRange(Mobile, Mobile.RangeFight))
{
if (Mobile.Debug)
{
Mobile.DebugSay($"I am with {customer.Name}");
}
this.DebugSayFormatted($"I am with {customer.Name}");
Mobile.Direction = Mobile.GetDirectionTo(customer);
}
else
{
if (Mobile.Debug)
{
Mobile.DebugSay($"{customer.Name} is gone");
}
this.DebugSayFormatted($"{customer.Name} is gone");
Mobile.FocusMob = null;
Action = ActionType.Wander;

View file

@ -2414,13 +2414,6 @@ namespace Server.Mobiles
base.OnAfterDelete();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void DebugSay(string text)
{
// Moved the debug check to implementation layer so we can avoid string formatting when we do not need it
PublicOverheadMessage(MessageType.Regular, 41, false, text);
}
/*
* This function can be overridden.. so a "Strongest" mobile, can have a different definition depending
* on who check for value
@ -2498,7 +2491,7 @@ namespace Server.Mobiles
}
else
{
DebugSay("I'm being attacked but my master told me not to fight.");
AIObject.DebugSay("I'm being attacked but my master told me not to fight.");
Warmode = false;
return;
}