& π";
+ const string expected = "Hello δΈη <test> & π";
+ var result = input.EscapeHtml();
+ Assert.Equal(expected, result);
+ }
+
+ [Fact(DisplayName = "String overload matches ReadOnlySpan overload")]
+ public void EscapeHtml_StringVsReadOnlySpan_ProduceSameResult()
+ {
+ const string input = "Tom & Jerry 'in' \"quotes\"
";
+
+ var resultString = input.EscapeHtml();
+ var resultSpan = input.AsSpan().EscapeHtml();
+
+ Assert.Equal(resultString, resultSpan);
+ }
+}
diff --git a/Projects/Server/Utilities/Html.cs b/Projects/Server/Utilities/Html.cs
index 91d2b6f3b..9377c7014 100644
--- a/Projects/Server/Utilities/Html.cs
+++ b/Projects/Server/Utilities/Html.cs
@@ -14,9 +14,11 @@
*************************************************************************/
using System;
+using System.Buffers;
using System.Runtime.CompilerServices;
using System.Text;
using Server.Buffers;
+using Server.Text;
namespace Server;
@@ -237,13 +239,70 @@ public static class Html
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RawInterpolatedStringHandler Right(this ReadOnlySpan text) => text.Right(-1);
+ private static readonly SearchValues _htmlSearchValues = SearchValues.Create('<', '>', '&', '"', '\'');
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static string EscapeHtml(this string input) =>
- new StringBuilder(input.Length).Append(input)
- .Replace("<", "<")
- .Replace(">", ">")
- .Replace("&", "&")
- .Replace("\"", """)
- .Replace("'", "'")
- .ToString();
+ public static string EscapeHtml(this string input)
+ {
+ if (string.IsNullOrEmpty(input))
+ {
+ return input ?? "";
+ }
+
+ return EscapeHtml(input.AsSpan());
+ }
+
+ public static string EscapeHtml(this ReadOnlySpan input)
+ {
+ if (input.IsEmpty)
+ {
+ return string.Empty;
+ }
+
+ int indexOfAny = input.IndexOfAny(_htmlSearchValues);
+ if (indexOfAny < 0)
+ {
+ return input.ToString();
+ }
+
+ using var builder = ValueStringBuilder.Create(input.Length * 2);
+ int lastIndex = 0;
+
+ while (indexOfAny >= 0)
+ {
+ if (indexOfAny > lastIndex)
+ {
+ builder.Append(input[lastIndex..indexOfAny]);
+ }
+
+ char c = input[indexOfAny];
+ var replacement = c switch
+ {
+ '&' => "&",
+ '<' => "<",
+ '>' => ">",
+ '"' => """,
+ '\'' => "'"
+ };
+ builder.Append(replacement);
+
+ lastIndex = indexOfAny + 1;
+ indexOfAny = input[lastIndex..].IndexOfAny(_htmlSearchValues);
+ if (indexOfAny < 0)
+ {
+ break;
+ }
+
+ indexOfAny += lastIndex;
+ }
+
+ if (lastIndex < input.Length)
+ {
+ builder.Append(input[lastIndex..]);
+ }
+
+ var result = builder.ToString();
+ builder.Dispose();
+ return result;
+ }
}
diff --git a/Projects/UOContent/Gumps/AdminGump.cs b/Projects/UOContent/Gumps/AdminGump.cs
index e9c91dba3..00e65887d 100644
--- a/Projects/UOContent/Gumps/AdminGump.cs
+++ b/Projects/UOContent/Gumps/AdminGump.cs
@@ -4095,15 +4095,15 @@ namespace Server.Gumps
{
if (x is not KeyValuePair> a)
{
- return -1;
+ return 1;
}
if (y is not KeyValuePair> b)
{
- return 1;
+ return -1;
}
- return a.Value.Count - b.Value.Count;
+ return b.Value.Count - a.Value.Count;
}
}
diff --git a/Projects/UOContent/Mobiles/AI/AnimalAI.cs b/Projects/UOContent/Mobiles/AI/AnimalAI.cs
index 9e33414c7..5107b57fe 100644
--- a/Projects/UOContent/Mobiles/AI/AnimalAI.cs
+++ b/Projects/UOContent/Mobiles/AI/AnimalAI.cs
@@ -55,7 +55,7 @@ public class AnimalAI : BaseAI
return true;
}
- if (!WalkMobileRange(combatant, 1, true, Mobile.RangeFight, Mobile.RangeFight))
+ if (!WalkMobileRange(combatant, 1, false, Mobile.RangeFight, Mobile.RangeFight))
{
if (Mobile.GetDistanceToSqrt(combatant) > Mobile.RangePerception + 1)
{
diff --git a/Projects/UOContent/Mobiles/AI/ArcherAI.cs b/Projects/UOContent/Mobiles/AI/ArcherAI.cs
index 6491c42d5..19e0375c4 100644
--- a/Projects/UOContent/Mobiles/AI/ArcherAI.cs
+++ b/Projects/UOContent/Mobiles/AI/ArcherAI.cs
@@ -41,13 +41,7 @@ public class ArcherAI : BaseAI
return true;
}
- if (Core.TickCount - Mobile.LastMoveTime > 1000 && !WalkMobileRange(
- combatant,
- 1,
- true,
- Mobile.RangeFight,
- Mobile.Weapon.MaxRange
- ))
+ if (!WalkMobileRange(combatant, 1, false, Mobile.RangeFight, Mobile.Weapon.MaxRange))
{
this.DebugSayFormatted($"I am still not in range of {combatant.Name}");
diff --git a/Projects/UOContent/Mobiles/AI/BerserkAI.cs b/Projects/UOContent/Mobiles/AI/BerserkAI.cs
index f882d6fb5..4663ae8d0 100644
--- a/Projects/UOContent/Mobiles/AI/BerserkAI.cs
+++ b/Projects/UOContent/Mobiles/AI/BerserkAI.cs
@@ -38,7 +38,7 @@ public class BerserkAI : BaseAI
return true;
}
- if (!WalkMobileRange(combatant, 1, true, Mobile.RangeFight, Mobile.RangeFight))
+ if (!WalkMobileRange(combatant, 1, false, Mobile.RangeFight, Mobile.RangeFight))
{
this.DebugSayFormatted($"I am still not in range of {combatant.Name}");
diff --git a/Projects/UOContent/Mobiles/AI/MageAI.cs b/Projects/UOContent/Mobiles/AI/MageAI.cs
index 03fe60a6a..0ca4893e5 100644
--- a/Projects/UOContent/Mobiles/AI/MageAI.cs
+++ b/Projects/UOContent/Mobiles/AI/MageAI.cs
@@ -168,7 +168,7 @@ public class MageAI : BaseAI
{
if (!SmartAI)
{
- if (!MoveTo(m, true, Mobile.RangeFight))
+ if (!MoveTo(m, false, Mobile.RangeFight))
{
OnFailedMove();
}
@@ -182,14 +182,14 @@ public class MageAI : BaseAI
{
RunFrom(m);
}
- else if (!Mobile.InRange(m, Math.Max(Mobile.RangeFight, 2)) && !MoveTo(m, true, 1))
+ else if (!Mobile.InRange(m, Math.Max(Mobile.RangeFight, 2)) && !MoveTo(m, false, 1))
{
OnFailedMove();
}
}
else if (!Mobile.InRange(m, Mobile.RangeFight))
{
- if (!MoveTo(m, true, 1))
+ if (!MoveTo(m, false, 1))
{
OnFailedMove();
}
@@ -713,6 +713,14 @@ public class MageAI : BaseAI
}
else if (Mobile.Spell == null && Core.TickCount - _nextCastTime >= 0)
{
+ if (Mobile.Controlled && c == Mobile)
+ {
+ DebugSay("I should not attack myself!");
+ Mobile.Combatant = null;
+ Action = ActionType.Guard;
+ return true;
+ }
+
// We are ready to cast a spell
Spell spell;
var toDispel = FindDispelTarget(true);
diff --git a/Projects/UOContent/Mobiles/AI/MeleeAI.cs b/Projects/UOContent/Mobiles/AI/MeleeAI.cs
index edf0b108e..01ef3c06f 100644
--- a/Projects/UOContent/Mobiles/AI/MeleeAI.cs
+++ b/Projects/UOContent/Mobiles/AI/MeleeAI.cs
@@ -67,7 +67,7 @@ public class MeleeAI : BaseAI
}
}
- if (!MoveTo(combatant, true, Mobile.RangeFight))
+ if (!MoveTo(combatant, false, Mobile.RangeFight))
{
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
diff --git a/Projects/UOContent/Mobiles/AI/PredatorAI.cs b/Projects/UOContent/Mobiles/AI/PredatorAI.cs
index fe5b06a6f..5e0e01520 100644
--- a/Projects/UOContent/Mobiles/AI/PredatorAI.cs
+++ b/Projects/UOContent/Mobiles/AI/PredatorAI.cs
@@ -41,7 +41,7 @@ public class PredatorAI : BaseAI
return true;
}
- if (!WalkMobileRange(combatant, 1, true, Mobile.RangeFight, Mobile.RangeFight))
+ if (!WalkMobileRange(combatant, 1, false, Mobile.RangeFight, Mobile.RangeFight))
{
if (Mobile.GetDistanceToSqrt(combatant) > Mobile.RangePerception + 1)
{
diff --git a/Projects/UOContent/Mobiles/AI/ThiefAI.cs b/Projects/UOContent/Mobiles/AI/ThiefAI.cs
index ff707f3df..2d67de801 100644
--- a/Projects/UOContent/Mobiles/AI/ThiefAI.cs
+++ b/Projects/UOContent/Mobiles/AI/ThiefAI.cs
@@ -43,7 +43,7 @@ public class ThiefAI : BaseAI
return true;
}
- if (!WalkMobileRange(combatant, 1, true, Mobile.RangeFight, Mobile.RangeFight))
+ if (!WalkMobileRange(combatant, 1, false, Mobile.RangeFight, Mobile.RangeFight))
{
this.DebugSayFormatted($"I should be closer to {combatant.Name}");
}
diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs
index f9898d6cd..eb70e8454 100644
--- a/Projects/UOContent/Mobiles/BaseCreature.cs
+++ b/Projects/UOContent/Mobiles/BaseCreature.cs
@@ -2676,19 +2676,7 @@ namespace Server.Mobiles
if (Body.IsHuman)
{
- switch (Utility.Random(2))
- {
- case 0:
- {
- CheckedAnimate(5, 5, 1, true, true, 1);
- break;
- }
- case 1:
- {
- CheckedAnimate(6, 5, 1, true, false, 1);
- break;
- }
- }
+ CheckedAnimate(Utility.RandomBool() ? 5 : 6, 5, 1, true, false, 1);
}
else if (Body.IsAnimal)
{
@@ -2713,19 +2701,7 @@ namespace Server.Mobiles
}
else if (Body.IsMonster)
{
- switch (Utility.Random(2))
- {
- case 0:
- {
- CheckedAnimate(17, 5, 1, true, false, 1);
- break;
- }
- case 1:
- {
- CheckedAnimate(18, 5, 1, true, false, 1);
- break;
- }
- }
+ CheckedAnimate(Utility.RandomBool() ? 17 : 18, 5, 1, true, false, 1);
}
PlaySound(GetIdleSound());
diff --git a/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs b/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs
index 3997a3856..3ecc94db2 100644
--- a/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs
+++ b/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs
@@ -93,7 +93,7 @@ public abstract partial class BaseFamiliar : BaseCreature
Hidden = m_LastHidden = master.Hidden;
}
- if (AIObject?.WalkMobileRange(master, 5, true, 1, 1) == true)
+ if (AIObject?.WalkMobileRange(master, 5, false, 1, 1) == true)
{
Warmode = master.Warmode;
Combatant = master.Combatant;