_queue = [];
+
+ public StolenItem(Item stolen, Mobile thief, Mobile victim)
+ {
+ Stolen = stolen;
+ Thief = thief;
+ Victim = victim;
+
+ Expires = Core.Now + StealTime;
+ }
+
+ public Item Stolen { get; }
+
+ public Mobile Thief { get; }
+
+ public Mobile Victim { get; }
+
+ public DateTime Expires { get; private set; }
+
+ public bool IsExpired => Core.Now >= Expires;
+
+ public static void Add(Item item, Mobile thief, Mobile victim)
+ {
+ Clean();
+
+ _queue.Enqueue(new StolenItem(item, thief, victim));
+ }
+
+ public static bool IsStolen(Item item)
+ {
+ Mobile victim = null;
+
+ return IsStolen(item, ref victim);
+ }
+
+ public static bool IsStolen(Item item, ref Mobile victim)
+ {
+ Clean();
+
+ foreach (var si in _queue)
+ {
+ if (si.Stolen == item && !si.IsExpired)
+ {
+ victim = si.Victim;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ [OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
+ public static void ReturnOnDeath(Mobile killed)
+ {
+ Clean();
+
+ var corpse = killed.Corpse;
+
+ foreach (var si in _queue)
+ {
+ if (si.Stolen.RootParent != corpse || si.Victim == null || si.IsExpired)
+ {
+ continue;
+ }
+
+ if (si.Victim.AddToBackpack(si.Stolen))
+ {
+ si.Victim.SendLocalizedMessage(1010464); // the item that was stolen is returned to you.
+ }
+ else
+ {
+ si.Victim.SendLocalizedMessage(1010463); // the item that was stolen from you falls to the ground.
+ }
+
+ si.Expires = Core.Now; // such a hack
+ }
+ }
+
+ public static void Clean()
+ {
+ while (_queue.Count > 0)
+ {
+ var si = _queue.Peek();
+
+ if (!si.IsExpired)
+ {
+ break;
+ }
+
+ _queue.Dequeue();
}
}
}
From fff6e19a18e85d7c047f3efff5c70fa930ea26a6 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Mon, 26 May 2025 22:36:23 -0700
Subject: [PATCH 15/34] chore: Fixes name verification test (#2201)
---
Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs b/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
index ab0971904..a9e4f5c27 100644
--- a/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
+++ b/Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
@@ -119,7 +119,7 @@ public class NameVerificationTests
public void Validate_TooManyExceptions_ReturnsFalse()
{
var exceptions = SearchValues.Create(' ', '-', '.');
- Assert.False(NameVerification.Validate("A-B.C D-E", 2, 16, true, false, false, 3, exceptions));
+ Assert.False(NameVerification.Validate("A-B.C D-E", 2, 16, true, false, false, 1, exceptions));
}
[Fact]
From 4e23a8e2051b0ba49d48ed03e13f598b54656faa Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Wed, 28 May 2025 00:09:36 -0700
Subject: [PATCH 16/34] chore: Updates JB logo per Jetbrain's request. (#2203)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index c04c17279..2184b2181 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,6 @@ Thank you for supporting us! You can find out how by visiting the [sponsors](./S
- [Karasho](https://github.com/andreakarasho), [Jaedan](https://github.com/jaedan) and the ClassicUO Community
-Development Tools & Plugins provided with ♥ by
+
Development Tools & Plugins provided with ♥ by

From 7fc98498e4ed7d43dce84abeef5ed981477fd22f Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Wed, 28 May 2025 00:31:58 -0700
Subject: [PATCH 17/34] feat: Adds option for houses to face east with proper
calculations (#2204)
---
Projects/UOContent/Multis/Deeds.cs | 4 +-
Projects/UOContent/Multis/Houses/BaseHouse.cs | 2 +
.../UOContent/Multis/Houses/HousePlacement.cs | 127 +++++++++++-------
.../Multis/Houses/HousePlacementTool.cs | 32 ++---
4 files changed, 101 insertions(+), 64 deletions(-)
diff --git a/Projects/UOContent/Multis/Deeds.cs b/Projects/UOContent/Multis/Deeds.cs
index b537cec5e..1120a0b55 100644
--- a/Projects/UOContent/Multis/Deeds.cs
+++ b/Projects/UOContent/Multis/Deeds.cs
@@ -71,6 +71,8 @@ namespace Server.Multis.Deeds
[CommandProperty(AccessLevel.GameMaster)]
public Point3D Offset { get; set; }
+ public virtual Direction HouseDirection => Direction.South;
+
public abstract Rectangle2D[] Area { get; }
public override void Serialize(IGenericWriter writer)
@@ -153,7 +155,7 @@ namespace Server.Multis.Deeds
else
{
var center = new Point3D(p.X - Offset.X, p.Y - Offset.Y, p.Z - Offset.Z);
- var res = HousePlacement.Check(from, MultiID, center, out var toMove);
+ var res = HousePlacement.Check(from, MultiID, center, out var toMove, HouseDirection);
switch (res)
{
diff --git a/Projects/UOContent/Multis/Houses/BaseHouse.cs b/Projects/UOContent/Multis/Houses/BaseHouse.cs
index d49062d01..f1d3954d9 100644
--- a/Projects/UOContent/Multis/Houses/BaseHouse.cs
+++ b/Projects/UOContent/Multis/Houses/BaseHouse.cs
@@ -100,6 +100,8 @@ namespace Server.Multis
[CommandProperty(AccessLevel.GameMaster)]
public bool RestrictDecay { get; set; }
+ public virtual Direction HouseDirection => Direction.South;
+
public virtual TimeSpan DecayPeriod => TimeSpan.FromDays(5.0);
public virtual DecayType DecayType
diff --git a/Projects/UOContent/Multis/Houses/HousePlacement.cs b/Projects/UOContent/Multis/Houses/HousePlacement.cs
index d97f734ab..972b43f2e 100644
--- a/Projects/UOContent/Multis/Houses/HousePlacement.cs
+++ b/Projects/UOContent/Multis/Houses/HousePlacement.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using Server.Collections;
using Server.Regions;
@@ -37,7 +38,9 @@ namespace Server.Multis
0x0150, 0x015C // Furrows
};
- public static HousePlacementResult Check(Mobile from, int multiID, Point3D center, out List toMove)
+ public static HousePlacementResult Check(
+ Mobile from, int multiID, Point3D center, out List toMove, Direction houseFacing = Direction.South
+ )
{
// If this spot is considered valid, every item and mobile in this list will be moved under the house sign
toMove = new List();
@@ -77,7 +80,7 @@ namespace Server.Multis
HouseFoundation.AddStairsTo(ref mcl); // this is a AOS house, add the stairs
}
- // Location of the nortwest-most corner of the house
+ // Location of the northwest-most corner of the house
var start = new Point3D(center.X + mcl.Min.X, center.Y + mcl.Min.Y, center.Z);
// These are storage lists. They hold items and mobiles found in the map for further processing
@@ -85,7 +88,7 @@ namespace Server.Multis
var mobiles = new List();
// These are also storage lists. They hold location values indicating the yard and border locations.
- List yard = new(), borders = new();
+ List borders = [];
/* RULES:
*
@@ -121,7 +124,7 @@ namespace Server.Multis
return HousePlacementResult.BadRegionTemp;
}
- if (reg.IsPartOf() || reg.IsPartOf())
+ if (reg.IsPartOf())
{
return HousePlacementResult.BadRegionHidden;
}
@@ -218,16 +221,18 @@ namespace Server.Multis
{
var id = item.ItemData;
- if (addTileTop > item.Z && item.Z + id.CalcHeight > addTileZ)
+ if (addTileTop <= item.Z || item.Z + id.CalcHeight <= addTileZ)
{
- if (item.Movable)
- {
- toMove.Add(item);
- }
- else if (id.Impassable || id.Surface && !id.Background)
- {
- return HousePlacementResult.BadItem; // Broke rule #2
- }
+ continue;
+ }
+
+ if (item.Movable)
+ {
+ toMove.Add(item);
+ }
+ else if (id.Impassable || id.Surface && !id.Background)
+ {
+ return HousePlacementResult.BadItem; // Broke rule #2
}
}
@@ -257,17 +262,9 @@ namespace Server.Multis
if (hasFoundation)
{
- for (var xOffset = -1; xOffset <= 1; ++xOffset)
+ if (!CheckYard(map, tileX, tileY, YardSize, houseFacing))
{
- for (var yOffset = -YardSize; yOffset <= YardSize; ++yOffset)
- {
- var yardPoint = new Point2D(tileX + xOffset, tileY + yOffset);
-
- if (!yard.Contains(yardPoint))
- {
- yard.Add(yardPoint);
- }
- }
+ return HousePlacementResult.BadStatic; // Broke rule #3
}
for (var xOffset = -1; xOffset <= 1; ++xOffset)
@@ -364,37 +361,73 @@ namespace Server.Multis
}
}
- for (var i = 0; i < yard.Count; i++)
- {
- var yardPoint = yard[i];
+ return HousePlacementResult.Valid;
+ }
- foreach (var house in map.GetMultisInSector(yardPoint))
+ private static bool CheckYard(Map map, int tileX, int tileY, int yardSize, Direction houseFacing)
+ {
+ var isSouthFacing = (houseFacing & Direction.South) != 0;
+ var isEastFacing = (houseFacing & Direction.East) != 0;
+
+ for (var xOffset = -yardSize; xOffset <= yardSize; ++xOffset)
+ {
+ var absXOffset = Math.Abs(xOffset);
+ for (var yOffset = -yardSize; yOffset <= yardSize; ++yOffset)
{
- if (house.Contains(yard[i]))
+ var absYOffset = Math.Abs(yOffset);
+ var yardPoint = new Point2D(tileX + xOffset, tileY + yOffset);
+
+ bool inSouthYard = yOffset > 0 && yOffset <= yardSize && absXOffset <= 1;
+ bool inEastYard = xOffset > 0 && xOffset <= yardSize && absYOffset <= 1;
+ bool inNorthYard = yOffset < 0 && yOffset >= -yardSize && absXOffset <= 1;
+ bool inWestYard = xOffset < 0 && xOffset >= -yardSize && absYOffset <= 1;
+
+ // Check each house at this point
+ foreach (var house in map.GetMultisInSector(yardPoint))
{
- return HousePlacementResult.BadStatic; // Broke rule #3
+ if (!house.Contains(yardPoint))
+ {
+ continue;
+ }
+
+ var existingHouseFacing = house.HouseDirection;
+ var existingHouseIsSouthFacing = (existingHouseFacing & Direction.South) != 0;
+ var existingHouseIsEastFacing = (existingHouseFacing & Direction.East) != 0;
+
+ // Sub-Rule 1: No houses within immediate proximity (1 tile radius)
+ if (absXOffset <= 1 && absYOffset <= 1)
+ {
+ return false;
+ }
+
+ // Sub-Rule 2: If we're south facing, protect our south yard
+ if (isSouthFacing && inSouthYard)
+ {
+ return false;
+ }
+
+ // Sub-Rule 3: If we're east facing, protect our east yard
+ if (isEastFacing && inEastYard)
+ {
+ return false;
+ }
+
+ // Sub-Rule 4: If there's a south-facing house to our north, respect its yard
+ if (inNorthYard && existingHouseIsSouthFacing)
+ {
+ return false;
+ }
+
+ // Sub-Rule 5: If there's an east-facing house to our west, respect its yard
+ if (inWestYard && existingHouseIsEastFacing)
+ {
+ return false;
+ }
}
}
}
- // TODO: Should we check for MultiTilesAt each yard point?
- // for (var i = 0; i < yard.Count; i++)
- // {
- // var yardPoint = yard[i];
- //
- // foreach (var tiles in map.GetMultiTilesAt(yardPoint))
- // {
- // for (int j = 0; j < tiles.Length; ++j)
- // {
- // if ((TileData.ItemTable[tiles[j].ID & TileData.MaxItemValue].Flags & (TileFlag.Impassable | TileFlag.Surface)) != 0)
- // {
- // return HousePlacementResult.BadStatic; // Broke rule #3
- // }
- // }
- // }
- // }
-
- return HousePlacementResult.Valid;
+ return true;
}
}
}
diff --git a/Projects/UOContent/Multis/Houses/HousePlacementTool.cs b/Projects/UOContent/Multis/Houses/HousePlacementTool.cs
index c0842066c..be254b01c 100644
--- a/Projects/UOContent/Multis/Houses/HousePlacementTool.cs
+++ b/Projects/UOContent/Multis/Houses/HousePlacementTool.cs
@@ -303,7 +303,7 @@ public class HousePlacementEntry
public HousePlacementEntry(
Type type, int description, int storage, int lockdowns, int newStorage, int newLockdowns,
- int vendors, int cost, int xOffset, int yOffset, int zOffset, int multiID
+ int vendors, int cost, int xOffset, int yOffset, int zOffset, int multiID, Direction direction = Direction.South
)
{
Type = type;
@@ -318,6 +318,7 @@ public class HousePlacementEntry
Offset = new Point3D(xOffset, yOffset, zOffset);
MultiID = multiID;
+ HouseDirection = direction;
}
public Type Type { get; }
@@ -334,6 +335,8 @@ public class HousePlacementEntry
public Point3D Offset { get; }
+ public Direction HouseDirection { get; }
+
public static HousePlacementEntry[] ClassicHouses { get; } =
{
new(typeof(SmallOldHouse), 1011303, 425, 212, 489, 244, 10, 37000, 0, 4, 0, 0x0064),
@@ -1981,7 +1984,7 @@ public class HousePlacementEntry
prevHouse.Delete();
- var res = HousePlacement.Check(from, MultiID, center, out var toMove);
+ var res = HousePlacement.Check(from, MultiID, center, out var toMove, HouseDirection);
switch (res)
{
@@ -2008,21 +2011,18 @@ public class HousePlacementEntry
$"{Cost} gold would have been withdrawn from your bank if you were not a GM."
);
}
+ else if (Banker.Withdraw(from, Cost))
+ {
+ // ~1_AMOUNT~ gold has been withdrawn from your bank box.
+ from.SendLocalizedMessage(1060398, Cost.ToString());
+ }
else
{
- if (Banker.Withdraw(from, Cost))
- {
- // ~1_AMOUNT~ gold has been withdrawn from your bank box.
- from.SendLocalizedMessage(1060398, Cost.ToString());
- }
- else
- {
- house.RemoveKeys(from);
- house.Delete();
- // You do not have the funds available in your bank box to purchase this house. Try placing a smaller house, or adding gold or checks to your bank box.
- from.SendLocalizedMessage(1060646);
- return;
- }
+ house.RemoveKeys(from);
+ house.Delete();
+ // You do not have the funds available in your bank box to purchase this house. Try placing a smaller house, or adding gold or checks to your bank box.
+ from.SendLocalizedMessage(1060646);
+ return;
}
house.MoveToWorld(center, from.Map);
@@ -2087,7 +2087,7 @@ public class HousePlacementEntry
}
var center = new Point3D(p.X - Offset.X, p.Y - Offset.Y, p.Z - Offset.Z);
- var res = HousePlacement.Check(from, MultiID, center, out var toMove);
+ var res = HousePlacement.Check(from, MultiID, center, out var toMove, HouseDirection);
switch (res)
{
From 35ff4023c3baad2d2101c6355a8f7428225ebb58 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sun, 1 Jun 2025 10:28:56 -1000
Subject: [PATCH 18/34] fix: Fixes gold trade exploit (#2205)
---
Projects/UOContent/Network/Packets/IncomingMobilePackets.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs b/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs
index 994a2590a..5e05947b4 100644
--- a/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs
+++ b/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs
@@ -158,6 +158,9 @@ public static class IncomingMobilePackets
trade.To.Plat = plat;
trade.UpdateToCurrency();
}
+
+ trade.From.Accepted = false;
+ trade.To.Accepted = false;
}
}
}
From 7695174bccafdbfb77d8d9434d641eff020871a2 Mon Sep 17 00:00:00 2001
From: Bohica <53943479+Bohicatv@users.noreply.github.com>
Date: Sun, 1 Jun 2025 13:33:09 -0700
Subject: [PATCH 19/34] fix: Adds null-conditional checks when stopping timers
for TownCrier (#2206)
---
Projects/UOContent/Mobiles/Townfolk/TownCrier.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/Projects/UOContent/Mobiles/Townfolk/TownCrier.cs b/Projects/UOContent/Mobiles/Townfolk/TownCrier.cs
index a2dd13d14..44eadf509 100644
--- a/Projects/UOContent/Mobiles/Townfolk/TownCrier.cs
+++ b/Projects/UOContent/Mobiles/Townfolk/TownCrier.cs
@@ -118,7 +118,7 @@ public class TownCrierDurationPrompt : Prompt
{
if (!TimeSpan.TryParse(text, out var ts))
{
- from.SendMessage("Value was not properly formatted. Use: ");
+ from.SendMessage("Value was not properly formatted. Use: ");
from.SendGump(new TownCrierGump(from, m_Owner));
return;
}
@@ -272,7 +272,7 @@ public class TownCrierGump : Gump
{
if (info.ButtonID == 1)
{
- m_From.SendMessage("Enter the duration for the new message. Format: ");
+ m_From.SendMessage("Enter the duration for the new message. Format: ");
m_From.Prompt = new TownCrierDurationPrompt(m_Owner);
}
else if (info.ButtonID > 1)
@@ -433,7 +433,7 @@ public partial class TownCrier : Mobile, ITownCrierEntryList
if (tce == null)
{
- _autoShoutTimer.Stop();
+ _autoShoutTimer?.Stop();
_autoShoutTimer = null;
}
else if (_newsTimer == null)
@@ -454,7 +454,7 @@ public partial class TownCrier : Mobile, ITownCrierEntryList
var index = _newsTimer.Index;
if (index >= tce.Lines.Length)
{
- _newsTimer.Stop();
+ _newsTimer?.Stop();
_newsTimer = null;
}
else
From 78e2e24fcd5e3d04f4d475986ec9c6fdaaecf936 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sun, 1 Jun 2025 10:38:28 -1000
Subject: [PATCH 20/34] fix: Fixes gold trade exploit (clearing checks
properly) (#2207)
---
Projects/UOContent/Network/Packets/IncomingMobilePackets.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs b/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs
index 5e05947b4..8dab27f0e 100644
--- a/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs
+++ b/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs
@@ -159,8 +159,7 @@ public static class IncomingMobilePackets
trade.UpdateToCurrency();
}
- trade.From.Accepted = false;
- trade.To.Accepted = false;
+ cont.ClearChecks();
}
}
}
From 0d2ed60fed605dec8c769228600e0a2264e91d13 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Mon, 2 Jun 2025 22:11:40 -1000
Subject: [PATCH 21/34] fix: Fixes null components in BaseAddon (#2208)
---
Projects/UOContent/Items/Addons/AddonComponent.cs | 2 +-
Projects/UOContent/Items/Addons/BaseAddon.cs | 10 +++++++++-
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/Projects/UOContent/Items/Addons/AddonComponent.cs b/Projects/UOContent/Items/Addons/AddonComponent.cs
index ce155cec9..e19914998 100644
--- a/Projects/UOContent/Items/Addons/AddonComponent.cs
+++ b/Projects/UOContent/Items/Addons/AddonComponent.cs
@@ -104,7 +104,7 @@ namespace Server.Items
}
[SerializableField(0)]
- [SerializedCommandProperty(AccessLevel.GameMaster)]
+ [SerializedCommandProperty(AccessLevel.GameMaster, readOnly: true)]
public BaseAddon _addon;
[SerializableField(1)]
diff --git a/Projects/UOContent/Items/Addons/BaseAddon.cs b/Projects/UOContent/Items/Addons/BaseAddon.cs
index 340a93065..6b2abbc66 100644
--- a/Projects/UOContent/Items/Addons/BaseAddon.cs
+++ b/Projects/UOContent/Items/Addons/BaseAddon.cs
@@ -270,7 +270,8 @@ namespace Server.Items
foreach (var c in Components)
{
- c.Delete();
+ // Component can become null if the Addon property is somehow deleted, then the component itself is deleted.
+ c?.Delete();
}
}
@@ -283,5 +284,12 @@ namespace Server.Items
_resource = (CraftResource)reader.ReadEncodedInt();
}
}
+
+ [AfterDeserialization]
+ private void AfterDeserialization()
+ {
+ // We have had issues in the past, so let's tidy it up.
+ _components?.Tidy();
+ }
}
}
From ea4080ce0e2979f68e26acd7344429c1c70f0389 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Mon, 2 Jun 2025 22:48:14 -1000
Subject: [PATCH 22/34] fix: Eliminates allocations in canned evil timer
(#2209)
---
.../Engines/CannedEvil/CannedEvilTimer.cs | 51 +++---
.../CannedEvil/DungeonChampionSpawn.cs | 8 +-
.../Engines/CannedEvil/GenChampEntry.cs | 53 +++---
.../UOContent/Engines/CannedEvil/GenChamps.cs | 171 +++++++++---------
.../Engines/CannedEvil/LLChampionSpawn.cs | 8 +-
5 files changed, 145 insertions(+), 146 deletions(-)
diff --git a/Projects/UOContent/Engines/CannedEvil/CannedEvilTimer.cs b/Projects/UOContent/Engines/CannedEvil/CannedEvilTimer.cs
index d7ca9e1b7..2109f09ec 100644
--- a/Projects/UOContent/Engines/CannedEvil/CannedEvilTimer.cs
+++ b/Projects/UOContent/Engines/CannedEvil/CannedEvilTimer.cs
@@ -15,6 +15,7 @@
using System;
using System.Collections.Generic;
+using Server.Collections;
using Server.Misc;
namespace Server.Engines.CannedEvil
@@ -29,8 +30,8 @@ namespace Server.Engines.CannedEvil
Instance.OnTick();
}
- private static readonly HashSet _dungeonSpawns = new();
- private static readonly HashSet _lostLandsSpawns = new();
+ private static readonly HashSet _dungeonSpawns = new();
+ private static readonly HashSet _lostLandsSpawns = new();
private static DateTime _sliceTime;
public static CannedEvilTimer Instance { get; private set; }
@@ -38,25 +39,25 @@ namespace Server.Engines.CannedEvil
public static void AddSpawn(DungeonChampionSpawn spawn)
{
_dungeonSpawns.Add(spawn);
- Instance?.OnSlice(_dungeonSpawns, false);
+ OnSlice(_dungeonSpawns, false);
}
public static void AddSpawn(LLChampionSpawn spawn)
{
_lostLandsSpawns.Add(spawn);
- Instance?.OnSlice(_lostLandsSpawns, false);
+ OnSlice(_lostLandsSpawns, false);
}
public static void RemoveSpawn(DungeonChampionSpawn spawn)
{
_dungeonSpawns.Remove(spawn);
- Instance?.OnSlice(_dungeonSpawns, false);
+ OnSlice(_dungeonSpawns, false);
}
public static void RemoveSpawn(LLChampionSpawn spawn)
{
_lostLandsSpawns.Remove(spawn);
- Instance?.OnSlice(_lostLandsSpawns, false);
+ OnSlice(_lostLandsSpawns, false);
}
public CannedEvilTimer() : base(TimeSpan.Zero, TimeSpan.FromMinutes(1.0))
@@ -64,32 +65,34 @@ namespace Server.Engines.CannedEvil
_sliceTime = Core.Now;
}
- public void OnSlice(ICollection list, bool rotate = true) where T : ChampionSpawn
+ public static void OnSlice(HashSet spawns, bool rotate = true)
{
- if (list.Count > 0)
+ if (spawns.Count <= 0)
{
- List valid = new List();
+ return;
+ }
- foreach (T spawn in list)
+ using var queue = rotate ? PooledRefQueue- .Create() : default;
+
+ foreach (var spawn in spawns)
+ {
+ if (spawn.AlwaysActive && !spawn.Active)
{
- if (spawn.AlwaysActive && !spawn.Active)
- {
- spawn.ReadyToActivate = true;
- }
- else if (rotate && (!spawn.Active || spawn.Kills == 0 && spawn.Level == 0))
- {
- spawn.Active = false;
- spawn.ReadyToActivate = false;
-
- valid.Add(spawn);
- }
+ spawn.ReadyToActivate = true;
}
-
- if (valid.Count > 0)
+ else if (rotate && (!spawn.Active || spawn.Kills == 0 && spawn.Level == 0))
{
- valid[Utility.Random(valid.Count)].ReadyToActivate = true;
+ spawn.Active = false;
+ spawn.ReadyToActivate = false;
+
+ queue.Enqueue(spawn);
}
}
+
+ if (rotate && queue.Count > 0)
+ {
+ ((ChampionSpawn)queue.PeekRandom()).ReadyToActivate = true;
+ }
}
protected override void OnTick()
diff --git a/Projects/UOContent/Engines/CannedEvil/DungeonChampionSpawn.cs b/Projects/UOContent/Engines/CannedEvil/DungeonChampionSpawn.cs
index 534168848..280afe924 100644
--- a/Projects/UOContent/Engines/CannedEvil/DungeonChampionSpawn.cs
+++ b/Projects/UOContent/Engines/CannedEvil/DungeonChampionSpawn.cs
@@ -26,11 +26,6 @@ public partial class DungeonChampionSpawn : ChampionSpawn
CannedEvilTimer.AddSpawn(this);
}
- public DungeonChampionSpawn(Serial serial) : base(serial)
- {
- CannedEvilTimer.AddSpawn(this);
- }
-
public override bool ProximitySpawn => true;
public override bool AlwaysActive => false;
@@ -39,4 +34,7 @@ public partial class DungeonChampionSpawn : ChampionSpawn
base.OnAfterDelete();
CannedEvilTimer.RemoveSpawn(this);
}
+
+ [AfterDeserialization]
+ private void AfterDeserialization() => CannedEvilTimer.AddSpawn(this);
}
diff --git a/Projects/UOContent/Engines/CannedEvil/GenChampEntry.cs b/Projects/UOContent/Engines/CannedEvil/GenChampEntry.cs
index 148ac59d7..dc106d22c 100644
--- a/Projects/UOContent/Engines/CannedEvil/GenChampEntry.cs
+++ b/Projects/UOContent/Engines/CannedEvil/GenChampEntry.cs
@@ -15,35 +15,34 @@
using System;
-namespace Server.Engines.CannedEvil
+namespace Server.Engines.CannedEvil;
+
+public class ChampionEntry
{
- public record ChampionEntry
+ public readonly bool _randomizeType;
+ public readonly ChampionSpawnType _type;
+ public readonly Point3D _signLocation;
+ public readonly Type _champType;
+ public readonly Map _map;
+ public readonly Point3D _ejectLocation;
+ public readonly Map _ejectMap;
+
+ public ChampionEntry(Type champtype, Point3D signloc, Map map, Point3D ejectloc, Map ejectmap) :
+ this(champtype, ChampionSpawnType.Abyss, signloc, map, ejectloc, ejectmap, true)
{
- public readonly bool m_RandomizeType;
- public readonly ChampionSpawnType m_Type;
- public readonly Point3D m_SignLocation;
- public readonly Type m_ChampType;
- public readonly Map m_Map;
- public readonly Point3D m_EjectLocation;
- public readonly Map m_EjectMap;
+ }
- public ChampionEntry(Type champtype, Point3D signloc, Map map, Point3D ejectloc, Map ejectmap) :
- this(champtype, ChampionSpawnType.Abyss, signloc, map, ejectloc, ejectmap, true)
- {
- }
-
- public ChampionEntry(
- Type champtype, ChampionSpawnType type, Point3D signloc, Map map, Point3D ejectloc, Map ejectmap,
- bool randomizetype = false
- )
- {
- m_ChampType = champtype;
- m_RandomizeType = randomizetype;
- m_Type = type;
- m_SignLocation = signloc;
- m_Map = map;
- m_EjectLocation = ejectloc;
- m_EjectMap = ejectmap;
- }
+ public ChampionEntry(
+ Type champtype, ChampionSpawnType type, Point3D signloc, Map map, Point3D ejectloc, Map ejectmap,
+ bool randomizetype = false
+ )
+ {
+ _champType = champtype;
+ _randomizeType = randomizetype;
+ _type = type;
+ _signLocation = signloc;
+ _map = map;
+ _ejectLocation = ejectloc;
+ _ejectMap = ejectmap;
}
}
diff --git a/Projects/UOContent/Engines/CannedEvil/GenChamps.cs b/Projects/UOContent/Engines/CannedEvil/GenChamps.cs
index 0a3b526ea..22a7f03a0 100644
--- a/Projects/UOContent/Engines/CannedEvil/GenChamps.cs
+++ b/Projects/UOContent/Engines/CannedEvil/GenChamps.cs
@@ -17,105 +17,106 @@ using System;
using System.Collections.Generic;
using Server.Logging;
-namespace Server.Engines.CannedEvil
+namespace Server.Engines.CannedEvil;
+
+public static class ChampionGenerator
{
- public static class ChampionGenerator
+ private static readonly ILogger logger = LogFactory.GetLogger(typeof(ChampionGenerator));
+
+ public static void Configure()
{
- private static readonly ILogger logger = LogFactory.GetLogger(typeof(ChampionGenerator));
+ CommandSystem.Register("GenChamps", AccessLevel.Developer, ChampGen_OnCommand);
+ }
- public static void Configure()
+ private static readonly ChampionEntry[] LLLocations =
+ [
+ new(typeof(LLChampionSpawn), new Point3D(5511, 2360, 42), Map.Felucca, new Point3D(5439, 2323, 26), Map.Felucca),
+ new(typeof(LLChampionSpawn), new Point3D(6038, 2401, 47), Map.Felucca, new Point3D(5988, 2340, 24), Map.Felucca),
+ new(typeof(LLChampionSpawn), new Point3D(5549, 2640, 16), Map.Felucca, new Point3D(5645, 2696, -8), Map.Felucca),
+ new(typeof(LLChampionSpawn), new Point3D(5636, 2916, 37), Map.Felucca, new Point3D(5721, 2949, 28), Map.Felucca),
+ new(typeof(LLChampionSpawn), new Point3D(6035, 2943, 50), Map.Felucca, new Point3D(6098, 2997, 17), Map.Felucca),
+ new(typeof(LLChampionSpawn), new Point3D(5265, 3171, 105), Map.Felucca, new Point3D(5314, 3232, 2), Map.Felucca),
+ new(typeof(LLChampionSpawn), new Point3D(5282, 3368, 50), Map.Felucca, new Point3D(5215, 3318, 3), Map.Felucca),
+ new(typeof(LLChampionSpawn), new Point3D(5207, 3637, 20), Map.Felucca, new Point3D(5263, 3687, 0), Map.Felucca),
+ new(typeof(LLChampionSpawn), new Point3D(5954, 3475, 25), Map.Felucca, new Point3D(6013, 3529, 0), Map.Felucca),
+ new(typeof(LLChampionSpawn), new Point3D(5982, 3882, 20), Map.Felucca, new Point3D(5929, 3820, -1), Map.Felucca),
+ new(typeof(LLChampionSpawn), new Point3D(5724, 3991, 41), Map.Felucca, new Point3D(5774, 4041, 26), Map.Felucca),
+ new(typeof(LLChampionSpawn), ChampionSpawnType.ForestLord, new Point3D(5559, 3757, 21), Map.Felucca, new Point3D(5513, 3878, 3), Map.Felucca)
+ ];
+
+ private static readonly ChampionEntry[] DungeonLocations =
+ [
+ new(typeof(DungeonChampionSpawn), ChampionSpawnType.UnholyTerror, new Point3D(5179, 709, 20), Map.Felucca, new Point3D(4111, 432, 5), Map.Felucca),
+ new(typeof(DungeonChampionSpawn), ChampionSpawnType.VerminHorde, new Point3D(5557, 827, 65), Map.Felucca, new Point3D(5580, 632, 30), Map.Felucca),
+ new(typeof(DungeonChampionSpawn), ChampionSpawnType.ColdBlood, new Point3D(5259, 837, 64), Map.Felucca, new Point3D(1176, 2637, 0), Map.Felucca),
+ new(typeof(DungeonChampionSpawn), ChampionSpawnType.Abyss, new Point3D(5815, 1352, 5), Map.Felucca, new Point3D(2923, 3406, 8), Map.Felucca),
+ new(typeof(DungeonChampionSpawn), ChampionSpawnType.Arachnid, new Point3D(5190, 1607, 20), Map.Felucca, new Point3D(5482, 3161, -54), Map.Felucca)
+ ];
+
+ [Usage("GenChamps")]
+ [Description("Generates champions for Felucca Dungeons & Lost Lands.")]
+ private static void ChampGen_OnCommand(CommandEventArgs e)
+ {
+ /*
+ //We take the assumption that we are spawning managed champions
+ for (int i = CannedEvilTimer.DungeonSpawns.Count - 1; i >= 0; i--)
+ CannedEvilTimer.DungeonSpawns[i].Delete();
+
+ for (int i = CannedEvilTimer.LLSpawns.Count - 1; i >= 0; i--)
+ CannedEvilTimer.LLSpawns[i].Delete();
+ */
+
+ //We assume that all champion spawns are generated here.
+ List spawns = [];
+ foreach (Item item in World.Items.Values)
{
- CommandSystem.Register("GenChamps", AccessLevel.Developer, ChampGen_OnCommand);
+ if (item is ChampionSpawn spawn)
+ {
+ spawns.Add(spawn);
+ }
}
- private static readonly ChampionEntry[] LLLocations = {
- new(typeof(LLChampionSpawn), new Point3D(5511, 2360, 42), Map.Felucca, new Point3D(5439, 2323, 26 ), Map.Felucca),
- new(typeof(LLChampionSpawn), new Point3D(6038, 2401, 47), Map.Felucca, new Point3D(5988, 2340, 24), Map.Felucca),
- new(typeof(LLChampionSpawn), new Point3D(5549, 2640, 16), Map.Felucca, new Point3D(5645, 2696, -8), Map.Felucca),
- new(typeof(LLChampionSpawn), new Point3D(5636, 2916, 37), Map.Felucca, new Point3D(5721, 2949, 28), Map.Felucca),
- new(typeof(LLChampionSpawn), new Point3D(6035, 2943, 50), Map.Felucca, new Point3D(6098, 2997, 17), Map.Felucca),
- new(typeof(LLChampionSpawn), new Point3D(5265, 3171, 105), Map.Felucca, new Point3D(5314, 3232, 2), Map.Felucca),
- new(typeof(LLChampionSpawn), new Point3D(5282, 3368, 50), Map.Felucca, new Point3D(5215, 3318, 3), Map.Felucca),
- new(typeof(LLChampionSpawn), new Point3D(5207, 3637, 20), Map.Felucca, new Point3D(5263, 3687, 0), Map.Felucca),
- new(typeof(LLChampionSpawn), new Point3D(5954, 3475, 25), Map.Felucca, new Point3D(6013, 3529, 0), Map.Felucca),
- new(typeof(LLChampionSpawn), new Point3D(5982, 3882, 20), Map.Felucca, new Point3D(5929, 3820, -1), Map.Felucca),
- new(typeof(LLChampionSpawn), new Point3D(5724, 3991, 41), Map.Felucca, new Point3D(5774, 4041, 26), Map.Felucca),
- new(typeof(LLChampionSpawn), ChampionSpawnType.ForestLord, new Point3D(5559, 3757, 21), Map.Felucca, new Point3D(5513, 3878, 3), Map.Felucca),
- };
-
- private static readonly ChampionEntry[] DungeonLocations = {
- new(typeof(DungeonChampionSpawn), ChampionSpawnType.UnholyTerror, new Point3D(5179, 709, 20), Map.Felucca, new Point3D(4111, 432, 5), Map.Felucca),
- new(typeof(DungeonChampionSpawn), ChampionSpawnType.VerminHorde, new Point3D(5557, 827, 65), Map.Felucca, new Point3D(5580, 632, 30), Map.Felucca),
- new(typeof(DungeonChampionSpawn), ChampionSpawnType.ColdBlood, new Point3D(5259, 837, 64), Map.Felucca, new Point3D(1176, 2637, 0), Map.Felucca),
- new(typeof(DungeonChampionSpawn), ChampionSpawnType.Abyss, new Point3D(5815, 1352, 5), Map.Felucca, new Point3D(2923, 3406, 8), Map.Felucca),
- new(typeof(DungeonChampionSpawn), ChampionSpawnType.Arachnid, new Point3D(5190, 1607, 20), Map.Felucca, new Point3D(5482, 3161, -54), Map.Felucca),
- };
-
- [Usage("GenChamps")]
- [Description("Generates champions for Felucca Dungeons & Lost Lands.")]
- private static void ChampGen_OnCommand(CommandEventArgs e)
+ for (int i = spawns.Count - 1; i >= 0; i--)
{
- /*
- //We take the assumption that we are spawning managed champions
- for (int i = CannedEvilTimer.DungeonSpawns.Count - 1; i >= 0; i--)
- CannedEvilTimer.DungeonSpawns[i].Delete();
-
- for (int i = CannedEvilTimer.LLSpawns.Count - 1; i >= 0; i--)
- CannedEvilTimer.LLSpawns[i].Delete();
- */
-
- //We assume that all champion spawns are generated here.
- List spawns = new List();
- foreach (Item item in World.Items.Values)
- {
- if (item is ChampionSpawn spawn)
- {
- spawns.Add(spawn);
- }
- }
-
- for (int i = spawns.Count - 1; i >= 0; i--)
- {
- spawns[i].Delete();
- }
-
- Process(DungeonLocations);
- Process(LLLocations);
- //ProcessIlshenar();
- //ProcessTokuno();
+ spawns[i].Delete();
}
- private static void Process(ChampionEntry[] entries)
- {
- for (int i = 0; i < entries.Length; i++)
- {
- ChampionEntry entry = entries[i];
+ Process(DungeonLocations);
+ Process(LLLocations);
+ //ProcessIlshenar();
+ //ProcessTokuno();
+ }
- try
+ private static void Process(ChampionEntry[] entries)
+ {
+ for (int i = 0; i < entries.Length; i++)
+ {
+ ChampionEntry entry = entries[i];
+
+ try
+ {
+ if (Activator.CreateInstance(entry._champType) is ChampionSpawn spawn)
{
- if (Activator.CreateInstance(entry.m_ChampType) is ChampionSpawn spawn)
+ spawn.RandomizeType = entry._randomizeType;
+ spawn.Type = entry._type;
+ spawn.MoveToWorld(entry._signLocation, entry._map);
+ spawn.EjectLocation = entry._ejectLocation;
+ spawn.EjectMap = entry._ejectMap;
+ if (spawn.AlwaysActive)
{
- spawn.RandomizeType = entry.m_RandomizeType;
- spawn.Type = entry.m_Type;
- spawn.MoveToWorld(entry.m_SignLocation, entry.m_Map);
- spawn.EjectLocation = entry.m_EjectLocation;
- spawn.EjectMap = entry.m_EjectMap;
- if (spawn.AlwaysActive)
- {
- spawn.ReadyToActivate = true;
- }
+ spawn.ReadyToActivate = true;
}
}
- catch (Exception e)
- {
- logger.Error(
- e,
- "Failed to generate champion \"{Type}\" at {Location} ({Map}).",
- entry.m_ChampType.FullName,
- entry.m_SignLocation,
- entry.m_Map
- );
- }
+ }
+ catch (Exception e)
+ {
+ logger.Error(
+ e,
+ "Failed to generate champion \"{Type}\" at {Location} ({Map}).",
+ entry._champType.FullName,
+ entry._signLocation,
+ entry._map
+ );
}
}
}
diff --git a/Projects/UOContent/Engines/CannedEvil/LLChampionSpawn.cs b/Projects/UOContent/Engines/CannedEvil/LLChampionSpawn.cs
index 17768f81e..d7cb89c4d 100644
--- a/Projects/UOContent/Engines/CannedEvil/LLChampionSpawn.cs
+++ b/Projects/UOContent/Engines/CannedEvil/LLChampionSpawn.cs
@@ -28,11 +28,6 @@ public partial class LLChampionSpawn : ChampionSpawn
CannedEvilTimer.AddSpawn(this);
}
- public LLChampionSpawn(Serial serial) : base(serial)
- {
- CannedEvilTimer.AddSpawn(this);
- }
-
public override bool AlwaysActive => false;
public override void OnAfterDelete()
@@ -40,4 +35,7 @@ public partial class LLChampionSpawn : ChampionSpawn
base.OnAfterDelete();
CannedEvilTimer.RemoveSpawn(this);
}
+
+ [AfterDeserialization]
+ private void AfterDeserialization() => CannedEvilTimer.AddSpawn(this);
}
From 14bc38e3753dc0167b5331ed45e0d5851c5e29b3 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sat, 7 Jun 2025 10:58:01 -1000
Subject: [PATCH 23/34] fix: Removes the absurb 6 hour skill edge case and
fixes target cancellations (#2212)
---
Projects/Server/Mobiles/Mobile.cs | 2 ++
Projects/UOContent/Skills/Begging.cs | 24 +++--------------
Projects/UOContent/Skills/DetectHidden.cs | 7 ++++-
Projects/UOContent/Skills/Peacemaking.cs | 33 ++++++++---------------
Projects/UOContent/Skills/Stealing.cs | 11 ++++++--
5 files changed, 32 insertions(+), 45 deletions(-)
diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs
index 5b4e7a992..f217b0fe1 100644
--- a/Projects/Server/Mobiles/Mobile.cs
+++ b/Projects/Server/Mobiles/Mobile.cs
@@ -661,6 +661,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
}
}
+ [CommandProperty(AccessLevel.Administrator)]
public long NextActionTime { get; set; }
public long NextActionMessage { get; set; }
@@ -675,6 +676,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
public virtual bool CanRegenStam => Alive;
public virtual bool CanRegenMana => Alive;
+ [CommandProperty(AccessLevel.Administrator)]
public long NextSkillTime { get; set; }
public List Aggressors { get; private set; }
diff --git a/Projects/UOContent/Skills/Begging.cs b/Projects/UOContent/Skills/Begging.cs
index 0267aa682..1855050af 100644
--- a/Projects/UOContent/Skills/Begging.cs
+++ b/Projects/UOContent/Skills/Begging.cs
@@ -21,23 +21,18 @@ namespace Server.SkillHandlers
m.SendLocalizedMessage(500397); // To whom do you wish to grovel?
- return TimeSpan.FromHours(6.0);
+ return TimeSpan.FromSeconds(30.0);
}
private class InternalTarget : Target
{
- private bool m_SetSkillTime = true;
-
public InternalTarget() : base(12, false, TargetFlags.None)
{
}
- protected override void OnTargetFinish(Mobile from)
+ protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
{
- if (m_SetSkillTime)
- {
- from.NextSkillTime = Core.TickCount;
- }
+ from.NextSkillTime = Core.TickCount;
}
protected override void OnTarget(Mobile from, object targeted)
@@ -81,8 +76,6 @@ namespace Server.SkillHandlers
from.Animate(32, 5, 1, true, false, 0); // Bow
new InternalTimer(from, targ).Start();
-
- m_SetSkillTime = false;
}
}
else // Not a Mobile
@@ -125,16 +118,7 @@ namespace Server.SkillHandlers
else if (m_From.CheckTargetSkill(SkillName.Begging, m_Target, 0.0, 100.0))
{
var toConsume = theirPack.GetAmount(typeof(Gold)) / 10;
- var max = 10 + m_From.Fame / 2500;
-
- if (max > 14)
- {
- max = 14;
- }
- else if (max < 10)
- {
- max = 10;
- }
+ var max = Math.Clamp(10 + m_From.Fame / 2500, 10, 14);
if (toConsume > max)
{
diff --git a/Projects/UOContent/Skills/DetectHidden.cs b/Projects/UOContent/Skills/DetectHidden.cs
index a0fcecafd..f7f909fc9 100644
--- a/Projects/UOContent/Skills/DetectHidden.cs
+++ b/Projects/UOContent/Skills/DetectHidden.cs
@@ -18,7 +18,7 @@ namespace Server.SkillHandlers
src.SendLocalizedMessage(500819); // Where will you search?
src.Target = new InternalTarget();
- return TimeSpan.FromSeconds(6.0);
+ return TimeSpan.FromSeconds(30.0);
}
private class InternalTarget : Target
@@ -27,6 +27,11 @@ namespace Server.SkillHandlers
{
}
+ protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
+ {
+ from.NextSkillTime = Core.TickCount;
+ }
+
protected override void OnTarget(Mobile src, object targ)
{
var foundAnyone = false;
diff --git a/Projects/UOContent/Skills/Peacemaking.cs b/Projects/UOContent/Skills/Peacemaking.cs
index c963395d5..437249bba 100644
--- a/Projects/UOContent/Skills/Peacemaking.cs
+++ b/Projects/UOContent/Skills/Peacemaking.cs
@@ -27,27 +27,22 @@ namespace Server.SkillHandlers
from.RevealingAction();
from.SendLocalizedMessage(1049525); // Whom do you wish to calm?
from.Target = new InternalTarget(from, instrument);
- from.NextSkillTime = Core.TickCount + 21600000;
+ from.NextSkillTime = Core.TickCount + 30000; // 30s timeout on the targeter
}
private class InternalTarget : Target
{
private readonly BaseInstrument m_Instrument;
- private bool m_SetSkillTime = true;
public InternalTarget(Mobile from, BaseInstrument instrument) : base(
BaseInstrument.GetBardRange(from, SkillName.Peacemaking),
false,
TargetFlags.None
- ) =>
- m_Instrument = instrument;
+ ) => m_Instrument = instrument;
- protected override void OnTargetFinish(Mobile from)
+ protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
{
- if (m_SetSkillTime)
- {
- from.NextSkillTime = Core.TickCount;
- }
+ from.NextSkillTime = Core.TickCount;
}
protected override void OnTarget(Mobile from, object targeted)
@@ -60,21 +55,19 @@ namespace Server.SkillHandlers
}
else if (from.Region.IsPartOf())
{
- from.SendMessage("You may not peacemake in this area.");
+ from.SendMessage("You may not use peacemaking in this area.");
}
else if (targ.Region.IsPartOf())
{
- from.SendMessage("You may not peacemake there.");
+ from.SendMessage("You may not use peacemaking there.");
}
else if (!m_Instrument.IsChildOf(from.Backpack))
{
- from.SendLocalizedMessage(
- 1062488
- ); // The instrument you are trying to play is no longer in your backpack!
+ // The instrument you are trying to play is no longer in your backpack!
+ from.SendLocalizedMessage(1062488);
}
else
{
- m_SetSkillTime = false;
from.NextSkillTime = Core.TickCount + 10000;
if (targeted == from)
@@ -149,17 +142,14 @@ namespace Server.SkillHandlers
if (!from.CanBeHarmful(targ, false))
{
from.SendLocalizedMessage(1049528);
- m_SetSkillTime = true;
}
else if (bc?.Uncalmable == true)
{
from.SendLocalizedMessage(1049526); // You have no chance of calming that creature.
- m_SetSkillTime = true;
}
else if (bc?.BardPacified == true)
{
from.SendLocalizedMessage(1049527); // That creature is already being calmed.
- m_SetSkillTime = true;
}
else if (!BaseInstrument.CheckMusicianship(from))
{
@@ -193,10 +183,10 @@ namespace Server.SkillHandlers
targ.Combatant = null;
targ.Warmode = false;
+ from.SendLocalizedMessage(1049532); // You play hypnotic music, calming your target.
if (bc != null)
{
- from.SendLocalizedMessage(1049532); // You play hypnotic music, calming your target.
-
+ // You play hypnotic music, calming your target.
var seconds = 100 - diff / 1.5;
if (seconds > 120)
@@ -212,8 +202,7 @@ namespace Server.SkillHandlers
}
else
{
- from.SendLocalizedMessage(1049532); // You play hypnotic music, calming your target.
-
+ // You play hypnotic music, calming your target.
// You hear lovely music, and forget to continue battling!
targ.SendLocalizedMessage(500616);
}
diff --git a/Projects/UOContent/Skills/Stealing.cs b/Projects/UOContent/Skills/Stealing.cs
index 6d4c4226b..92cba48cb 100644
--- a/Projects/UOContent/Skills/Stealing.cs
+++ b/Projects/UOContent/Skills/Stealing.cs
@@ -62,7 +62,7 @@ public static class Stealing
m.SendLocalizedMessage(502698); // Which item do you want to steal?
}
- return TimeSpan.FromSeconds(10.0);
+ return TimeSpan.FromSeconds(30.0);
}
private class StealingTarget : Target
@@ -75,6 +75,11 @@ public static class Stealing
AllowNonlocal = true;
}
+ protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
+ {
+ from.NextSkillTime = Core.TickCount;
+ }
+
private Item TryStealItem(Item toSteal, ref bool caught)
{
Item stolen = null;
@@ -83,7 +88,7 @@ public static class Stealing
var mobRoot = root as Mobile;
var rootIsPlayer = mobRoot?.Player == true;
- StealableArtifacts.StealableInstance si = toSteal.Parent == null || !toSteal.Movable
+ var si = toSteal.Parent == null || !toSteal.Movable
? StealableArtifacts.GetStealableInstance(toSteal)
: null;
@@ -404,6 +409,8 @@ public static class Stealing
pm.PermaFlags.Add(mobRoot);
pm.Delta(MobileDelta.Noto);
}
+
+ from.NextSkillTime = Core.TickCount + 10000; // 10 seconds cooldown
}
}
}
From c8d489b72bf96024446bdd25e9955e1b6a5add9a Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sat, 7 Jun 2025 11:05:49 -1000
Subject: [PATCH 24/34] fix: Fixes detect hidden skill cooldown (#2213)
---
Projects/UOContent/Skills/DetectHidden.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Projects/UOContent/Skills/DetectHidden.cs b/Projects/UOContent/Skills/DetectHidden.cs
index f7f909fc9..b0aa0ccbe 100644
--- a/Projects/UOContent/Skills/DetectHidden.cs
+++ b/Projects/UOContent/Skills/DetectHidden.cs
@@ -113,6 +113,8 @@ namespace Server.SkillHandlers
{
src.SendLocalizedMessage(500817); // You can see nothing hidden there.
}
+
+ src.NextSkillTime = Core.TickCount + 6000; // 6 seconds cooldown
}
}
}
From a286e282a96327406801b21d0562c4a6407b224d Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sat, 7 Jun 2025 15:22:54 -1000
Subject: [PATCH 25/34] fix: Fixes fist disarm (pre-aos) (#2214)
---
Projects/UOContent/Items/Weapons/Fists.cs | 93 ++++++++++++-----------
1 file changed, 50 insertions(+), 43 deletions(-)
diff --git a/Projects/UOContent/Items/Weapons/Fists.cs b/Projects/UOContent/Items/Weapons/Fists.cs
index 69f265356..c041b5c82 100644
--- a/Projects/UOContent/Items/Weapons/Fists.cs
+++ b/Projects/UOContent/Items/Weapons/Fists.cs
@@ -50,7 +50,7 @@ namespace Server.Items
return wresValue > incrValue ? wresValue : incrValue;
}
- private void CheckPreAOSMoves(Mobile attacker, Mobile defender)
+ private static void CheckPreAOSMoves(Mobile attacker, Mobile defender)
{
if (!attacker.CanBeginAction())
{
@@ -90,60 +90,67 @@ namespace Server.Items
attacker.SendLocalizedMessage(1004010); // You failed in your attempt to stun.
defender.SendLocalizedMessage(1004011); // Your opponent tried to stun you and failed.
}
+
+ return;
}
- else if (attacker.DisarmReady)
+
+ if (!attacker.DisarmReady)
{
- if (!defender.Player && !defender.Body.IsHuman)
- {
- attacker.SendLocalizedMessage(1004001); // You cannot disarm your opponent.
- return;
- }
+ return;
+ }
- if (attacker.Skills.ArmsLore.Value < 80.0 || attacker.Skills.Wrestling.Value < 80.0)
- {
- attacker.SendLocalizedMessage(1004002); // You are not skilled enough to disarm your opponent.
- attacker.DisarmReady = false;
- return;
- }
+ if (!defender.Player && !defender.Body.IsHuman)
+ {
+ attacker.SendLocalizedMessage(1004001); // You cannot disarm your opponent.
+ return;
+ }
- if (attacker.Stam < 15)
- {
- attacker.SendLocalizedMessage(1004003); // You are too fatigued to attempt anything.
- return;
- }
+ if (attacker.Skills.ArmsLore.Value < 80.0 || attacker.Skills.Wrestling.Value < 80.0)
+ {
+ attacker.SendLocalizedMessage(1004002); // You are not skilled enough to disarm your opponent.
+ attacker.DisarmReady = false;
+ return;
+ }
- var toDisarm = defender.FindItemOnLayer(Layer.OneHanded);
+ if (attacker.Stam < 15)
+ {
+ attacker.SendLocalizedMessage(1004003); // You are too fatigued to attempt anything.
+ return;
+ }
- if (toDisarm?.Movable == false)
- {
- toDisarm = defender.FindItemOnLayer(Layer.TwoHanded);
- }
+ var toDisarm = defender.FindItemOnLayer(Layer.OneHanded);
- var pack = defender.Backpack;
+ if (toDisarm?.Movable != true)
+ {
+ toDisarm = defender.FindItemOnLayer(Layer.TwoHanded);
+ }
- if (pack == null || toDisarm?.Movable == false)
- {
- attacker.SendLocalizedMessage(1004001); // You cannot disarm your opponent.
- }
- else if (CheckMove(attacker, SkillName.ArmsLore))
- {
- StartMoveDelay(attacker);
+ var pack = defender.Backpack;
- attacker.Stam -= 15;
- attacker.DisarmReady = false;
+ if (pack == null || toDisarm?.Movable != true)
+ {
+ attacker.SendLocalizedMessage(1004001); // You cannot disarm your opponent.
+ return;
+ }
- attacker.SendLocalizedMessage(1004006); // You successfully disarm your opponent!
- defender.SendLocalizedMessage(1004007); // You have been disarmed!
+ if (CheckMove(attacker, SkillName.ArmsLore))
+ {
+ StartMoveDelay(attacker);
- pack.DropItem(toDisarm);
- }
- else
- {
- attacker.Stam -= 15;
+ attacker.Stam -= 15;
+ attacker.DisarmReady = false;
- attacker.SendLocalizedMessage(1004004); // You failed in your attempt to disarm.
- defender.SendLocalizedMessage(1004005); // Your opponent tried to disarm you but failed.
- }
+ attacker.SendLocalizedMessage(1004006); // You successfully disarm your opponent!
+ defender.SendLocalizedMessage(1004007); // You have been disarmed!
+
+ pack.DropItem(toDisarm);
+ }
+ else
+ {
+ attacker.Stam -= 15;
+
+ attacker.SendLocalizedMessage(1004004); // You failed in your attempt to disarm.
+ defender.SendLocalizedMessage(1004005); // Your opponent tried to disarm you but failed.
}
}
From 006c5ed03714a431447b2f0547b57806579ebcd7 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Thu, 12 Jun 2025 13:32:00 -1000
Subject: [PATCH 26/34] fix: Fixes boat decay calculation (#2216)
---
Projects/UOContent/Multis/Boats/BaseBoat.cs | 42 +++++----------------
1 file changed, 9 insertions(+), 33 deletions(-)
diff --git a/Projects/UOContent/Multis/Boats/BaseBoat.cs b/Projects/UOContent/Multis/Boats/BaseBoat.cs
index 9e367b455..6a6d68666 100644
--- a/Projects/UOContent/Multis/Boats/BaseBoat.cs
+++ b/Projects/UOContent/Multis/Boats/BaseBoat.cs
@@ -175,40 +175,16 @@ namespace Server.Multis
[CommandProperty(AccessLevel.GameMaster)]
public BoatOrder Order { get; set; }
- public int Status
- {
- get
+ public int Status =>
+ (Core.Now - (TimeOfDecay - BoatDecayDelay)) switch
{
- var start = Core.Now - TimeOfDecay - BoatDecayDelay;
-
- if (start < TimeSpan.FromHours(1.0))
- {
- return 1043010; // This structure is like new.
- }
-
- if (start < TimeSpan.FromDays(2.0))
- {
- return 1043011; // This structure is slightly worn.
- }
-
- if (start < TimeSpan.FromDays(3.0))
- {
- return 1043012; // This structure is somewhat worn.
- }
-
- if (start < TimeSpan.FromDays(4.0))
- {
- return 1043013; // This structure is fairly worn.
- }
-
- if (start < TimeSpan.FromDays(5.0))
- {
- return 1043014; // This structure is greatly worn.
- }
-
- return 1043015; // This structure is in danger of collapsing.
- }
- }
+ var start when start < TimeSpan.FromHours(1) => 1043010, // This structure is like new.
+ var start when start < TimeSpan.FromDays(2) => 1043011, // This structure is slightly worn.
+ var start when start < TimeSpan.FromDays(3) => 1043012, // This structure is somewhat worn.
+ var start when start < TimeSpan.FromDays(4) => 1043013, // This structure is fairly worn.
+ var start when start < TimeSpan.FromDays(5) => 1043014, // This structure is greatly worn.
+ _ => 1043015 // This structure is in danger of collapsing.
+ };
public virtual int NorthID => 0;
public virtual int EastID => 0;
From a2ea4bda37ee5d393ffd2b92b161fda4f5afc120 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Thu, 12 Jun 2025 13:48:02 -1000
Subject: [PATCH 27/34] chore: Removes Qodana. Sad face. (#2217)
---
.github/workflows/code_quality.yml | 24 ------------------------
1 file changed, 24 deletions(-)
delete mode 100644 .github/workflows/code_quality.yml
diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml
deleted file mode 100644
index e7bbc2ebe..000000000
--- a/.github/workflows/code_quality.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-name: Qodana
-on:
- workflow_dispatch:
- pull_request:
- push:
- branches: # Specify your branches here
- - main # The 'main' branch
-
-jobs:
- qodana:
- runs-on: ubuntu-latest
- permissions:
- contents: write
- pull-requests: write
- checks: write
- steps:
- - uses: actions/checkout@v3
- with:
- ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
- fetch-depth: 0 # a full history is required for pull request analysis
- - name: 'Qodana Scan'
- uses: JetBrains/qodana-action@v2025.1
- env:
- QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
From d3e460605092b0b316651440b1e7a00c84284f05 Mon Sep 17 00:00:00 2001
From: Bohica <53943479+Bohicatv@users.noreply.github.com>
Date: Thu, 12 Jun 2025 17:25:32 -0700
Subject: [PATCH 28/34] fix: Fixes SpanReader.Read to fix bounds checks and now
returns bytes written (#2211)
---
Projects/Server/Buffers/SpanReader.cs | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/Projects/Server/Buffers/SpanReader.cs b/Projects/Server/Buffers/SpanReader.cs
index c127327c5..31c5fc51d 100644
--- a/Projects/Server/Buffers/SpanReader.cs
+++ b/Projects/Server/Buffers/SpanReader.cs
@@ -294,13 +294,17 @@ public ref struct SpanReader
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool Read(Span bytes)
+ public int Read(Span bytes)
{
- if (bytes.Length < Length)
+ if (bytes.Length == 0)
{
- throw new ArgumentOutOfRangeException(nameof(bytes));
+ return 0;
}
- return _buffer.TryCopyTo(bytes);
+ var bytesWritten = Math.Min(bytes.Length, Remaining);
+ _buffer.Slice(Position, bytesWritten).CopyTo(bytes);
+
+ Position += bytesWritten;
+ return bytesWritten;
}
}
From 7d748491d0431c896c67c52aa47e37d2e7bb7cb9 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Tue, 24 Jun 2025 10:37:59 -0700
Subject: [PATCH 29/34] fix: Fixes empty/missing bank box prevent withdraw from
account gold (#2219)
---
Projects/UOContent/Mobiles/Townfolk/Banker.cs | 18 ++++++++----------
1 file changed, 8 insertions(+), 10 deletions(-)
diff --git a/Projects/UOContent/Mobiles/Townfolk/Banker.cs b/Projects/UOContent/Mobiles/Townfolk/Banker.cs
index d79cc7055..7bb0c5bd6 100644
--- a/Projects/UOContent/Mobiles/Townfolk/Banker.cs
+++ b/Projects/UOContent/Mobiles/Townfolk/Banker.cs
@@ -12,7 +12,7 @@ namespace Server.Mobiles;
[SerializationGenerator(0, false)]
public partial class Banker : BaseVendor
{
- private readonly List m_SBInfos = new();
+ private readonly List m_SBInfos = [];
[Constructible]
public Banker() : base("the banker")
@@ -41,7 +41,7 @@ public partial class Banker : BaseVendor
}
}
- Container bank = m.FindBankNoCreate();
+ var bank = m.FindBankNoCreate();
if (bank != null)
{
@@ -80,11 +80,11 @@ public partial class Banker : BaseVendor
}
}
- Container bank = m.FindBankNoCreate();
+ var bank = m.FindBankNoCreate();
if (bank != null)
{
- gold = new List();
+ gold = [];
foreach (var g in bank.FindItemsByType())
{
@@ -97,7 +97,7 @@ public partial class Banker : BaseVendor
return int.MaxValue;
}
- checks = new List();
+ checks = [];
foreach (var bc in bank.FindItemsByType())
{
@@ -111,7 +111,7 @@ public partial class Banker : BaseVendor
private static bool HasRequiredBalance(int requiredBalance, Mobile m, out PooledRefList gold, out PooledRefList checks)
{
- Container bank = m.FindBankNoCreate();
+ var bank = m.FindBankNoCreate();
if (bank == null)
{
@@ -380,11 +380,9 @@ public partial class Banker : BaseVendor
{
Say(1048147); // Your backpack can't hold anything else.
}
- else if (amount > 0)
+ else if (amount <= 0)
{
- var box = e.Mobile.FindBankNoCreate();
-
- if (box == null || !Withdraw(e.Mobile, amount))
+ if (!Withdraw(e.Mobile, amount))
{
Say(500384); // Ah, art thou trying to fool me? Thou hast not so much gold!
}
From a37481ea934f9d5189a01f3206a76b789a0667d6 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Wed, 25 Jun 2025 23:31:51 -0700
Subject: [PATCH 30/34] fix: Fixes control target not reset when giving pet
order from context menu (#2220)
---
Projects/UOContent/Mobiles/AI/BaseAI.cs | 21 +++++++++------------
1 file changed, 9 insertions(+), 12 deletions(-)
diff --git a/Projects/UOContent/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs
index d7cce9923..a77e36d8f 100644
--- a/Projects/UOContent/Mobiles/AI/BaseAI.cs
+++ b/Projects/UOContent/Mobiles/AI/BaseAI.cs
@@ -626,15 +626,13 @@ public abstract class BaseAI
{
if (m_Mobile.Summoned || m_Mobile is GrizzledMare)
{
- e.Mobile.SendLocalizedMessage(
- 1005481
- ); // Summoned creatures are loyal only to their summoners.
+ // Summoned creatures are loyal only to their summoners.
+ e.Mobile.SendLocalizedMessage(1005481);
}
else if (e.Mobile.HasTrade)
{
- e.Mobile.SendLocalizedMessage(
- 1070947
- ); // You cannot friend a pet with a trade pending
+ // You cannot friend a pet with a trade pending
+ e.Mobile.SendLocalizedMessage(1070947);
}
else
{
@@ -742,15 +740,13 @@ public abstract class BaseAI
{
if (m_Mobile.Summoned || m_Mobile is GrizzledMare)
{
- e.Mobile.SendLocalizedMessage(
- 1005487
- ); // You cannot transfer ownership of a summoned creature.
+ // You cannot transfer ownership of a summoned creature.
+ e.Mobile.SendLocalizedMessage(1005487);
}
else if (e.Mobile.HasTrade)
{
- e.Mobile.SendLocalizedMessage(
- 1010507
- ); // You cannot transfer a pet with a trade pending
+ // You cannot transfer a pet with a trade pending
+ e.Mobile.SendLocalizedMessage(1010507);
}
else
{
@@ -2975,6 +2971,7 @@ public abstract class BaseAI
{
if (bc.CheckControlChance(from))
{
+ bc.ControlTarget = null;
bc.ControlOrder = _order;
}
From aadcd6db701404e606eed6e89731f53273eb6887 Mon Sep 17 00:00:00 2001
From: Felipe Maya Muniz <67922105+gnai-creator@users.noreply.github.com>
Date: Sat, 28 Jun 2025 15:42:43 -0300
Subject: [PATCH 31/34] fix: Withdraw should have positive amount to work
(#2221)
---
Projects/UOContent/Mobiles/Townfolk/Banker.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Projects/UOContent/Mobiles/Townfolk/Banker.cs b/Projects/UOContent/Mobiles/Townfolk/Banker.cs
index 7bb0c5bd6..7744c1918 100644
--- a/Projects/UOContent/Mobiles/Townfolk/Banker.cs
+++ b/Projects/UOContent/Mobiles/Townfolk/Banker.cs
@@ -380,7 +380,7 @@ public partial class Banker : BaseVendor
{
Say(1048147); // Your backpack can't hold anything else.
}
- else if (amount <= 0)
+ else if (amount > 0)
{
if (!Withdraw(e.Mobile, amount))
{
From f83fc94b05ff616eab85ada157ed7974126c77ee Mon Sep 17 00:00:00 2001
From: mdodkins
Date: Sun, 29 Jun 2025 01:30:18 +0100
Subject: [PATCH 32/34] fix: Do not add an extraneous Open Paperdoll context
menu entry (#2224)
---
Projects/UOContent/Mobiles/Hireables/BaseHire.cs | 5 -----
1 file changed, 5 deletions(-)
diff --git a/Projects/UOContent/Mobiles/Hireables/BaseHire.cs b/Projects/UOContent/Mobiles/Hireables/BaseHire.cs
index 06c569e25..aa4ba1cda 100644
--- a/Projects/UOContent/Mobiles/Hireables/BaseHire.cs
+++ b/Projects/UOContent/Mobiles/Hireables/BaseHire.cs
@@ -228,11 +228,6 @@ public partial class BaseHire : BaseCreature
if (!Controlled)
{
- if (CanPaperdollBeOpenedBy(from))
- {
- list.Add(new PaperdollEntry());
- }
-
list.Add(new HireEntry());
}
else
From c6b5dba83b776b42a456d92b2c37057256c6fc56 Mon Sep 17 00:00:00 2001
From: Felipe Maya Muniz <67922105+gnai-creator@users.noreply.github.com>
Date: Sat, 28 Jun 2025 21:32:13 -0300
Subject: [PATCH 33/34] fix: Adds colored logs to fletching (#2223)
---
Projects/UOContent/Engines/Craft/DefBowFletching.cs | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/Projects/UOContent/Engines/Craft/DefBowFletching.cs b/Projects/UOContent/Engines/Craft/DefBowFletching.cs
index eb6e04166..cd75829a1 100644
--- a/Projects/UOContent/Engines/Craft/DefBowFletching.cs
+++ b/Projects/UOContent/Engines/Craft/DefBowFletching.cs
@@ -258,5 +258,15 @@ public class DefBowFletching : CraftSystem
MarkOption = true;
Repair = Core.AOS;
+
+ SetSubRes(typeof(Log), 1072643);
+
+ AddSubRes(typeof(Log), 1072643, 0.0, 1044041, 1072652);
+ AddSubRes(typeof(OakLog), 1072644, 65.0, 1044041, 1072652);
+ AddSubRes(typeof(AshLog), 1072645, 80.0, 1044041, 1072652);
+ AddSubRes(typeof(YewLog), 1072646, 95.0, 1044041, 1072652);
+ AddSubRes(typeof(HeartwoodLog), 1072647, 100.0, 1044041, 1072652);
+ AddSubRes(typeof(BloodwoodLog), 1072648, 100.0, 1044041, 1072652);
+ AddSubRes(typeof(FrostwoodLog), 1072649, 100.0, 1044041, 1072652);
}
}
From 35faba5087a9ca4d0dd9486f24a02dd2d1bae411 Mon Sep 17 00:00:00 2001
From: Bohica <53943479+Bohicatv@users.noreply.github.com>
Date: Wed, 2 Jul 2025 15:04:48 -0700
Subject: [PATCH 34/34] fix: fixes the id for farmable cabbage (#2227)
---
Projects/UOContent/Items/Farming/FarmableCabbage.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Projects/UOContent/Items/Farming/FarmableCabbage.cs b/Projects/UOContent/Items/Farming/FarmableCabbage.cs
index 3b4e32d59..51ccff825 100644
--- a/Projects/UOContent/Items/Farming/FarmableCabbage.cs
+++ b/Projects/UOContent/Items/Farming/FarmableCabbage.cs
@@ -10,7 +10,7 @@ public partial class FarmableCabbage : FarmableCrop
{
}
- public static int GetCropID() => 3254;
+ public static int GetCropID() => 0x0C7B;
public override Item GetCropObject() =>
new Cabbage