From 5fe42912de73c39dbeec1abc0db8f0e904630234 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Thu, 15 May 2025 19:39:58 -0700
Subject: [PATCH 1/3] fix: Cleans up potential memory leak in monster
abilities.
---
.../UOContent/Engines/Virtues/HonorContext.cs | 1 +
.../Mobiles/Abilities/MonsterAbility.cs | 52 ++++++++++++-------
.../Mobiles/Abilities/MonsterAbilityGroup.cs | 14 +++--
.../Abilities/ReflectPhysicalDamage.cs | 4 ++
Projects/UOContent/Mobiles/BaseCreature.cs | 15 ++++--
5 files changed, 53 insertions(+), 33 deletions(-)
diff --git a/Projects/UOContent/Engines/Virtues/HonorContext.cs b/Projects/UOContent/Engines/Virtues/HonorContext.cs
index 8e2ba1a13..85ebfa8a2 100644
--- a/Projects/UOContent/Engines/Virtues/HonorContext.cs
+++ b/Projects/UOContent/Engines/Virtues/HonorContext.cs
@@ -1,4 +1,5 @@
using System;
+using ModernUO.CodeGeneratedEvents;
using Server.Mobiles;
namespace Server.Engines.Virtues;
diff --git a/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs b/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs
index 72fb61ec3..fc227507e 100644
--- a/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs
+++ b/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs
@@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
+using ModernUO.CodeGeneratedEvents;
namespace Server.Mobiles;
///
/// Abstract class used to build singletons for managing a specific monster ability.
///
-public abstract partial class MonsterAbility
+public abstract class MonsterAbility
{
private Dictionary _nextTriggerTicks;
@@ -18,39 +19,34 @@ public abstract partial class MonsterAbility
public virtual TimeSpan MinTriggerCooldown => TimeSpan.Zero;
public virtual TimeSpan MaxTriggerCooldown => TimeSpan.Zero;
- public bool WillTrigger(MonsterAbilityTrigger trigger) => (AbilityTrigger & trigger) != 0;
-
///
/// Returns true if ability is not on cooldown, and the chance to trigger succeeds.
///
/// Boolean indicating the ability can trigger.
public virtual bool CanTrigger(BaseCreature source, MonsterAbilityTrigger trigger)
{
- if (source is not { Alive: true, Deleted: false })
+ if ((AbilityTrigger & trigger) == 0 || source is not { Alive: true, Deleted: false })
{
return false;
}
- if (_nextTriggerTicks?.TryGetValue(source, out var nextTrigger) == true && nextTrigger - Core.TickCount > 0)
+ if (_nextTriggerTicks?.TryGetValue(source, out var nextTrigger) == true)
{
- return false;
+ if (nextTrigger - Core.TickCount > 0)
+ {
+ return false;
+ }
+
+ _nextTriggerTicks.Remove(source);
+ if (_nextTriggerTicks.Count == 0)
+ {
+ _nextTriggerTicks = null;
+ }
}
var c = ChanceToTrigger;
- if (c >= 1)
- {
- return true;
- }
-
- if (c <= 0)
- {
- return false;
- }
-
- var rnd = Utility.RandomDouble();
-
- return c > rnd;
+ return c >= 1 || c > 0 && c > Utility.RandomDouble();
}
///
@@ -99,7 +95,23 @@ public abstract partial class MonsterAbility
{
}
- public virtual void Move(BaseCreature creature, Direction d)
+ public virtual void Move(BaseCreature source, Direction d)
{
}
+
+ [OnEvent(nameof(BaseCreature.CreatureDeathEvent))]
+ [OnEvent(nameof(BaseCreature.CreatureDeletedEvent))]
+ public static void InvalidateNextAbilityTriggers(BaseCreature source)
+ {
+ var abilities = source.GetMonsterAbilities();
+ if (abilities == null || abilities.Length == 0)
+ {
+ return;
+ }
+
+ for (var i = 0; i < abilities.Length; i++)
+ {
+ abilities[i]._nextTriggerTicks?.Remove(source);
+ }
+ }
}
diff --git a/Projects/UOContent/Mobiles/Abilities/MonsterAbilityGroup.cs b/Projects/UOContent/Mobiles/Abilities/MonsterAbilityGroup.cs
index c431d4d35..6555a3682 100644
--- a/Projects/UOContent/Mobiles/Abilities/MonsterAbilityGroup.cs
+++ b/Projects/UOContent/Mobiles/Abilities/MonsterAbilityGroup.cs
@@ -1,15 +1,14 @@
using System;
-using Server.Random;
using WeightedMonsterAbility = Server.Random.WeightedValue;
namespace Server.Mobiles;
public class MonsterAbilityGroup : MonsterAbility
{
- private WeightedMonsterAbility[] _weightedAbilities;
- private WeightedMonsterAbility[] _availableToTrigger;
+ private readonly WeightedMonsterAbility[] _weightedAbilities;
+ private readonly WeightedMonsterAbility[] _availableToTrigger;
+ private readonly MonsterAbilityTrigger _triggers;
private int _availableToTriggerCount;
- private MonsterAbilityTrigger _triggers;
public MonsterAbilityGroup(params WeightedMonsterAbility[] weightedAbilities)
{
@@ -18,8 +17,7 @@ public class MonsterAbilityGroup : MonsterAbility
for (var i = 0; i < _weightedAbilities.Length; i++)
{
- var weightedAbility = _weightedAbilities[i];
- _triggers |= weightedAbility.Value.AbilityTrigger;
+ _triggers |= _weightedAbilities[i].Value.AbilityTrigger;
}
}
@@ -66,7 +64,7 @@ public class MonsterAbilityGroup : MonsterAbility
for (var i = 0; i < _weightedAbilities.Length; i++)
{
var weightedAbility = _weightedAbilities[i];
- if (weightedAbility.Value.WillTrigger(trigger) && weightedAbility.Value.CanTrigger(source, trigger))
+ if (weightedAbility.Value.CanTrigger(source, trigger))
{
_availableToTrigger[_availableToTriggerCount++] = weightedAbility;
}
@@ -83,7 +81,7 @@ public class MonsterAbilityGroup : MonsterAbility
return;
}
- var slice = new ReadOnlySpan>(_availableToTrigger, 0, _availableToTriggerCount);
+ var slice = new ReadOnlySpan(_availableToTrigger, 0, _availableToTriggerCount);
var chosenAbility = slice.RandomWeightedElement().Value;
// Just in case?
diff --git a/Projects/UOContent/Mobiles/Abilities/ReflectPhysicalDamage.cs b/Projects/UOContent/Mobiles/Abilities/ReflectPhysicalDamage.cs
index ae66734ba..3aa48960f 100644
--- a/Projects/UOContent/Mobiles/Abilities/ReflectPhysicalDamage.cs
+++ b/Projects/UOContent/Mobiles/Abilities/ReflectPhysicalDamage.cs
@@ -8,4 +8,8 @@ public class ReflectPhysicalDamage : MonsterAbility
public override MonsterAbilityType AbilityType => MonsterAbilityType.ReflectPhysicalDamage;
public virtual int PercentReflected => 10;
+
+ public override void Trigger(MonsterAbilityTrigger trigger, BaseCreature source, Mobile target)
+ {
+ }
}
diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs
index ed37b7be1..22c43ebc4 100644
--- a/Projects/UOContent/Mobiles/BaseCreature.cs
+++ b/Projects/UOContent/Mobiles/BaseCreature.cs
@@ -1216,7 +1216,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
- if (ability.WillTrigger(trigger) && ability.CanTrigger(this, trigger))
+ if (ability.CanTrigger(this, trigger))
{
ability.Trigger(trigger, this, defender);
triggered = true;
@@ -1238,7 +1238,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
- if (ability.WillTrigger(trigger) && ability.CanTrigger(this, trigger))
+ if (ability.CanTrigger(this, trigger))
{
ability.Move(this, d);
}
@@ -1257,7 +1257,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
- if (ability.WillTrigger(trigger) && ability.CanTrigger(this, trigger))
+ if (ability.CanTrigger(this, trigger))
{
if ((trigger & MonsterAbilityTrigger.GiveMeleeDamage) != 0)
{
@@ -1298,7 +1298,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
- if (ability.WillTrigger(trigger) && ability.CanTrigger(this, trigger))
+ if (ability.CanTrigger(this, trigger))
{
if ((trigger & MonsterAbilityTrigger.GiveSpellDamage) != 0)
{
@@ -3263,7 +3263,7 @@ namespace Server.Mobiles
}
[GeneratedEvent(nameof(CreatureDeathEvent))]
- public static partial void CreatureDeathEvent(Mobile m);
+ public static partial void CreatureDeathEvent(BaseCreature bc);
public override void OnDeath(Container c)
{
@@ -3454,8 +3454,13 @@ namespace Server.Mobiles
CreatureDeathEvent(this);
}
+ [GeneratedEvent(nameof(CreatureDeletedEvent))]
+ public static partial void CreatureDeletedEvent(BaseCreature bc);
+
public override void OnDelete()
{
+ CreatureDeletedEvent(this);
+
var m = m_ControlMaster;
SetControlMaster(null);
From 25bed78b59a33699d6b81d04c894b511856d6b04 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Thu, 15 May 2025 19:42:44 -0700
Subject: [PATCH 2/3] Revert
---
Projects/UOContent/Engines/Virtues/HonorContext.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/Projects/UOContent/Engines/Virtues/HonorContext.cs b/Projects/UOContent/Engines/Virtues/HonorContext.cs
index 85ebfa8a2..8e2ba1a13 100644
--- a/Projects/UOContent/Engines/Virtues/HonorContext.cs
+++ b/Projects/UOContent/Engines/Virtues/HonorContext.cs
@@ -1,5 +1,4 @@
using System;
-using ModernUO.CodeGeneratedEvents;
using Server.Mobiles;
namespace Server.Engines.Virtues;
From 520a521c9427dc67c2dd26535e6598d082dc63a9 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Thu, 15 May 2025 21:59:01 -0700
Subject: [PATCH 3/3] feat: Adds SpecialAttack trigger for monster abilities.
Fixes fanning fire.
---
.../UOContent/Items/Weapons/BaseWeapon.cs | 7 +-
.../Mobiles/Abilities/FanningFire.cs | 29 ++-
.../Mobiles/Abilities/MonsterAbilities.cs | 2 +-
.../Abilities/MonsterAbilityTrigger.cs | 1 +
Projects/UOContent/Mobiles/BaseCreature.cs | 22 +++
.../Monsters/ML/Bedlam/LadyJennifyr.cs | 187 ++++++++----------
6 files changed, 133 insertions(+), 115 deletions(-)
diff --git a/Projects/UOContent/Items/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs
index 85489edf3..f1d093d5c 100644
--- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs
+++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs
@@ -1350,7 +1350,7 @@ public abstract partial class BaseWeapon
theirValue = Math.Max(0.1, defValue + 50.0);
}
- var chance = ourValue / (theirValue * 2.0) * 1.0 + (double)bonus / 100;;
+ var chance = ourValue / (theirValue * 2.0) * 1.0 + (double)bonus / 100;
if (Core.AOS && chance < 0.02)
{
@@ -1997,6 +1997,11 @@ public abstract partial class BaseWeapon
Bladeweave.BladeWeaving(attacker, out var bladeweavingAbi) &&
bladeweavingAbi is ArmorIgnore;
+ if (bcAtt?.TriggerAbilitySpecialAttack(defender) != true)
+ {
+ return;
+ }
+
var damageGiven = AOS.Damage(
defender,
attacker,
diff --git a/Projects/UOContent/Mobiles/Abilities/FanningFire.cs b/Projects/UOContent/Mobiles/Abilities/FanningFire.cs
index a06bb561f..2bf12722b 100644
--- a/Projects/UOContent/Mobiles/Abilities/FanningFire.cs
+++ b/Projects/UOContent/Mobiles/Abilities/FanningFire.cs
@@ -5,8 +5,23 @@ namespace Server.Mobiles;
public class FanningFire : MonsterAbilitySingleTargetDoT
{
public override MonsterAbilityType AbilityType => MonsterAbilityType.FanningFire;
- public override MonsterAbilityTrigger AbilityTrigger => MonsterAbilityTrigger.GiveDamage;
- public override double ChanceToTrigger => 0.05;
+ public override MonsterAbilityTrigger AbilityTrigger => MonsterAbilityTrigger.SpecialAttack;
+
+ public FanningFire(double chanceToTrigger, int fireResistMod, int minDamage, int maxDamage)
+ {
+ ChanceToTrigger = chanceToTrigger;
+ FireResistMod = fireResistMod;
+ MinDamage = minDamage;
+ MaxDamage = maxDamage;
+ }
+
+ public sealed override double ChanceToTrigger { get; }
+
+ public int FireResistMod { get; }
+
+ public int MinDamage { get; }
+
+ public int MaxDamage { get; }
public const string Name = "FanningFire";
@@ -52,16 +67,12 @@ public class FanningFire : MonsterAbilitySingleTargetDoT
*/
source.DoHarmful(defender);
- var effect = -(defender.FireResistance / 10);
+ defender.AddResistanceMod(new ResistanceMod(ResistanceType.Fire, Name, FireResistMod));
- var mod = new ResistanceMod(ResistanceType.Fire, Name, effect);
- defender.AddResistanceMod(mod);
-
- defender.FixedParticles(0x37B9, 10, 30, 0x34, EffectLayer.RightFoot);
+ defender.FixedParticles(0x3709, 10, 30, 0x34, EffectLayer.RightFoot);
defender.PlaySound(0x208);
- // TODO: Trigger replaces a normal attack.
- AOS.Damage(defender, source, Utility.RandomMinMax(35, 45), 0, 100, 0, 0, 0);
+ AOS.Damage(defender, source, Utility.RandomMinMax(MinDamage, MaxDamage), 0, 100, 0, 0, 0);
}
protected override void EffectTick(BaseCreature source, Mobile defender, ref TimeSpan nextDelay)
diff --git a/Projects/UOContent/Mobiles/Abilities/MonsterAbilities.cs b/Projects/UOContent/Mobiles/Abilities/MonsterAbilities.cs
index 9df7ffd0a..652bc5b05 100644
--- a/Projects/UOContent/Mobiles/Abilities/MonsterAbilities.cs
+++ b/Projects/UOContent/Mobiles/Abilities/MonsterAbilities.cs
@@ -14,7 +14,7 @@ public static class MonsterAbilities
// Resistance Debuffs
public static GraspingClaw GraspingClaw => new();
public static RuneCorruption RuneCorruption => new();
- public static FanningFire FanningFire => new();
+ public static FanningFire FanningFire => new(0.05, -10, 35, 45);
// Summon Undead
public static SummonSkeletonsCounter SummonSkeletonsCounter => new();
diff --git a/Projects/UOContent/Mobiles/Abilities/MonsterAbilityTrigger.cs b/Projects/UOContent/Mobiles/Abilities/MonsterAbilityTrigger.cs
index d30ed4413..96e2a8429 100644
--- a/Projects/UOContent/Mobiles/Abilities/MonsterAbilityTrigger.cs
+++ b/Projects/UOContent/Mobiles/Abilities/MonsterAbilityTrigger.cs
@@ -14,6 +14,7 @@ public enum MonsterAbilityTrigger : ulong
CombatAction = 0x0000000000000020,
Death = 0x0000000000000040,
Movement = 0x0000000000000080,
+ SpecialAttack = 0x0000000000000100, // Triggers instead of a regular attack
GiveDamage = GiveMeleeDamage | GiveSpellDamage,
TakeDamage = TakeMeleeDamage | TakeSpellDamage
diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs
index 22c43ebc4..bca21bdda 100644
--- a/Projects/UOContent/Mobiles/BaseCreature.cs
+++ b/Projects/UOContent/Mobiles/BaseCreature.cs
@@ -1313,6 +1313,28 @@ namespace Server.Mobiles
}
}
+ public virtual bool TriggerAbilitySpecialAttack(Mobile target)
+ {
+ var abilities = GetMonsterAbilities();
+
+ if (abilities == null)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < abilities.Length; i++)
+ {
+ var ability = abilities[i];
+ if (ability.CanTrigger(this, MonsterAbilityTrigger.SpecialAttack))
+ {
+ ability.Trigger(MonsterAbilityTrigger.SpecialAttack, this, target);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
public virtual WeaponAbility GetWeaponAbility() => null;
public virtual bool IsEnemy(Mobile m)
diff --git a/Projects/UOContent/Mobiles/Monsters/ML/Bedlam/LadyJennifyr.cs b/Projects/UOContent/Mobiles/Monsters/ML/Bedlam/LadyJennifyr.cs
index 6b7a89005..e284737de 100644
--- a/Projects/UOContent/Mobiles/Monsters/ML/Bedlam/LadyJennifyr.cs
+++ b/Projects/UOContent/Mobiles/Monsters/ML/Bedlam/LadyJennifyr.cs
@@ -2,121 +2,100 @@ using ModernUO.Serialization;
using System;
using System.Collections.Generic;
-namespace Server.Mobiles
+namespace Server.Mobiles;
+
+[SerializationGenerator(0, false)]
+public partial class LadyJennifyr : SkeletalKnight
{
- [SerializationGenerator(0, false)]
- public partial class LadyJennifyr : SkeletalKnight
+ private static readonly Dictionary _table = new();
+
+ [Constructible]
+ public LadyJennifyr()
{
- private static readonly Dictionary m_Table = new();
+ IsParagon = true;
- [Constructible]
- public LadyJennifyr()
+ Hue = 0x76D;
+
+ SetStr(208, 309);
+ SetDex(91, 118);
+ SetInt(44, 101);
+
+ SetHits(1113, 1285);
+
+ SetDamage(15, 25);
+
+ SetDamageType(ResistanceType.Physical, 40);
+ SetDamageType(ResistanceType.Cold, 60);
+
+ SetResistance(ResistanceType.Physical, 56, 65);
+ SetResistance(ResistanceType.Fire, 41, 49);
+ SetResistance(ResistanceType.Cold, 71, 80);
+ SetResistance(ResistanceType.Poison, 41, 50);
+ SetResistance(ResistanceType.Energy, 50, 58);
+
+ SetSkill(SkillName.Wrestling, 127.9, 137.1);
+ SetSkill(SkillName.Tactics, 128.4, 141.9);
+ SetSkill(SkillName.MagicResist, 102.1, 119.5);
+ SetSkill(SkillName.Anatomy, 129.0, 137.5);
+
+ Fame = 18000;
+ Karma = -18000;
+ }
+
+ public override string CorpseName => "a Lady Jennifyr corpse";
+ public override string DefaultName => "Lady Jennifyr";
+
+ /*
+ // TODO: Uncomment once added
+ public override void OnDeath( Container c )
+ {
+ base.OnDeath( c );
+
+ if (Utility.RandomDouble() < 0.15)
+ c.DropItem( new DisintegratingThesisNotes() );
+
+ if (Utility.RandomDouble() < 0.1)
+ c.DropItem( new ParrotItem() );
+ }
+ */
+
+ public override bool GivesMLMinorArtifact => true;
+
+ public override void GenerateLoot()
+ {
+ AddLoot(LootPack.UltraRich, 3);
+ }
+
+ private static readonly MonsterAbility[] _abilities =
+ [
+ new FanningFire(0.10, -10, 35, 45)
+ ];
+
+ public override MonsterAbility[] GetMonsterAbilities() => _abilities;
+
+ private class ExpireTimer : Timer
+ {
+ private readonly Mobile _mobile;
+ private readonly ResistanceMod _mod;
+
+ public ExpireTimer(Mobile m, ResistanceMod mod) : base(TimeSpan.FromSeconds(10))
{
- IsParagon = true;
-
- Hue = 0x76D;
-
- SetStr(208, 309);
- SetDex(91, 118);
- SetInt(44, 101);
-
- SetHits(1113, 1285);
-
- SetDamage(15, 25);
-
- SetDamageType(ResistanceType.Physical, 40);
- SetDamageType(ResistanceType.Cold, 60);
-
- SetResistance(ResistanceType.Physical, 56, 65);
- SetResistance(ResistanceType.Fire, 41, 49);
- SetResistance(ResistanceType.Cold, 71, 80);
- SetResistance(ResistanceType.Poison, 41, 50);
- SetResistance(ResistanceType.Energy, 50, 58);
-
- SetSkill(SkillName.Wrestling, 127.9, 137.1);
- SetSkill(SkillName.Tactics, 128.4, 141.9);
- SetSkill(SkillName.MagicResist, 102.1, 119.5);
- SetSkill(SkillName.Anatomy, 129.0, 137.5);
-
- Fame = 18000;
- Karma = -18000;
+ _mobile = m;
+ _mod = mod;
}
- public override string CorpseName => "a Lady Jennifyr corpse";
- public override string DefaultName => "Lady Jennifyr";
-
- /*
- // TODO: Uncomment once added
- public override void OnDeath( Container c )
+ public void DoExpire()
{
- base.OnDeath( c );
+ _mobile.RemoveResistanceMod(_mod);
- if (Utility.RandomDouble() < 0.15)
- c.DropItem( new DisintegratingThesisNotes() );
-
- if (Utility.RandomDouble() < 0.1)
- c.DropItem( new ParrotItem() );
- }
- */
-
- public override bool GivesMLMinorArtifact => true;
-
- public override void GenerateLoot()
- {
- AddLoot(LootPack.UltraRich, 3);
+ Stop();
}
- public override void OnGaveMeleeAttack(Mobile defender, int damage)
+ protected override void OnTick()
{
- base.OnGaveMeleeAttack(defender, damage);
-
- if (Utility.RandomDouble() < 0.9)
- {
- return;
- }
-
- if (m_Table.Remove(defender, out var timer))
- {
- timer.DoExpire();
- }
-
- defender.FixedParticles(0x3709, 10, 30, 5052, EffectLayer.LeftFoot);
- defender.PlaySound(0x208);
- // The creature fans you with fire, reducing your resistance to fire attacks.
- defender.SendLocalizedMessage(1070833);
-
- var mod = new ResistanceMod(ResistanceType.Fire, "FireResistFanningFire", -10);
- defender.AddResistanceMod(mod);
-
- m_Table[defender] = timer = new ExpireTimer(defender, mod);
- timer.Start();
- }
-
- private class ExpireTimer : Timer
- {
- private readonly Mobile m_Mobile;
- private readonly ResistanceMod m_Mod;
-
- public ExpireTimer(Mobile m, ResistanceMod mod)
- : base(TimeSpan.FromSeconds(10))
- {
- m_Mobile = m;
- m_Mod = mod;
- }
-
- public void DoExpire()
- {
- m_Mobile.RemoveResistanceMod(m_Mod);
-
- Stop();
- }
-
- protected override void OnTick()
- {
- m_Mobile.SendLocalizedMessage(1070834); // Your resistance to fire attacks has returned.
- DoExpire();
- m_Table.Remove(m_Mobile);
- }
+ _mobile.SendLocalizedMessage(1070834); // Your resistance to fire attacks has returned.
+ DoExpire();
+ _table.Remove(_mobile);
}
}
}