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