From 90fa2e09deea5f03e33f9439eddc658d4f6c20e8 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 5 Apr 2024 21:05:16 -0700 Subject: [PATCH] fix: Fixes mount stamina, adds Pub46+ Ethereal stamina (#1716) ### Summary * Makes mount stamina significantly faster. * Reduces absolute reset from 24 hours to 4 hours. * Adds Pub 46+ Ethereal stamina where the stamina is global to all mounts for the player * Removes serialization of stamina entirely because it's ok if it gets reset during shard restarts. --- Projects/UOContent/Misc/StaminaSystem.cs | 310 +++++++++--------- .../Mobiles/Animals/Mounts/BaseMount.cs | 4 +- .../Mobiles/Animals/Mounts/Ethereals.cs | 2 +- Projects/UOContent/Mobiles/BaseCreature.cs | 3 +- .../UOContent/Mobiles/VirtualMountItem.cs | 7 +- 5 files changed, 155 insertions(+), 171 deletions(-) diff --git a/Projects/UOContent/Misc/StaminaSystem.cs b/Projects/UOContent/Misc/StaminaSystem.cs index c13fe15cb8..0ef7215c6d 100644 --- a/Projects/UOContent/Misc/StaminaSystem.cs +++ b/Projects/UOContent/Misc/StaminaSystem.cs @@ -15,14 +15,15 @@ public enum DFAlgorithm PainSpike } -public class StaminaSystem : GenericPersistence +public static class StaminaSystem { private static readonly ILogger logger = LogFactory.GetLogger(typeof(StaminaSystem)); - private static TimeSpan ResetDuration = TimeSpan.FromHours(24); + private static readonly TimeSpan ResetDuration = TimeSpan.FromHours(4); - private static readonly Dictionary _stepsTaken = new(); - private static readonly OrderedHashSet _resetHash = new(); + private static readonly Dictionary _etherealMountStepCounters = []; + private static readonly Dictionary _stepsTaken = []; + private static readonly HashSet _resetHash = []; // TODO: This exploits single thread processing and is not thread safe! public static DFAlgorithm DFA { get; set; } @@ -35,6 +36,9 @@ public class StaminaSystem : GenericPersistence public static bool EnableMountStamina { get; set; } public static bool UseMountStaminaOnlyWhenOverloaded { get; set; } + // Pub 46 (UOML+) + public static bool GlobalEtherealMountStamina { get; set; } + public static void Configure() { CannotMoveWhenFatigued = ServerConfiguration.GetOrUpdateSetting("stamina.cannotMoveWhenFatigued", !Core.AOS); @@ -44,56 +48,7 @@ public static void Configure() AdditionalLossWhenBelow = ServerConfiguration.GetOrUpdateSetting("stamina.additionalLossWhenBelow", 0.10); EnableMountStamina = ServerConfiguration.GetOrUpdateSetting("stamina.enableMountStamina", true); UseMountStaminaOnlyWhenOverloaded = ServerConfiguration.GetSetting("stamina.useMountStaminaOnlyWhenOverloaded", Core.SA); - } - - public StaminaSystem() : base("StaminaSystem", 10) - { - } - - public override void Serialize(IGenericWriter writer) - { - writer.WriteEncodedInt(0); // version - - writer.WriteEncodedInt(_stepsTaken.Count); - foreach (var (m, stepsTaken) in _stepsTaken) - { - writer.Write(m as ISerializable); // To serialize all IHasSteps must be an ISerializable - stepsTaken.Serialize(writer); - } - } - - public override void Deserialize(IGenericReader reader) - { - var version = reader.ReadEncodedInt(); - - var count = reader.ReadEncodedInt(); - _stepsTaken.EnsureCapacity(count); - - var now = Core.Now; - - for (var i = 0; i < count; i++) - { - var m = reader.ReadEntity() as IHasSteps; - var stepsTaken = new StepsTaken(); - stepsTaken.Deserialize(reader); - - if (m == null) - { - continue; - } - - RegenSteps(m, ref stepsTaken, false); - - if (stepsTaken.Steps > 0) - { - _stepsTaken.Add(m, stepsTaken); - - if (m is IMount && now < stepsTaken.IdleStartTime + ResetDuration) - { - _resetHash.Add(m); - } - } - } + GlobalEtherealMountStamina = ServerConfiguration.GetSetting("stamina.globalEtherealMountStamina", Core.ML); } public static void Initialize() @@ -107,14 +62,12 @@ public static void Initialize() using var queue = PooledRefQueue.Create(); foreach (var m in _stepsTaken.Keys) { - ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, m); - if (!Unsafe.IsNullRef(ref stepsTaken)) + // We cannot remove since we are iterating. + ref var stepsTaken = ref RegenSteps(m, out var exists, removeOnInvalidation: false); + + if (exists && stepsTaken.Steps <= 0) { - RegenSteps(m, ref stepsTaken, false); - if (stepsTaken.Steps <= 0) - { - queue.Enqueue(m); - } + queue.Enqueue(m); } } @@ -134,7 +87,7 @@ private static void Login(Mobile m) if (EnableMountStamina) { // Start idle for mount - ref var stepsTaken = ref GetMountStepsTaken(m.Mount, out var exists); + ref var stepsTaken = ref GetStepsTaken(m.Mount, out var exists); if (exists) { if (stepsTaken.Steps <= 0 || Core.Now >= stepsTaken.IdleStartTime + ResetDuration) @@ -152,8 +105,8 @@ private static void Login(Mobile m) if (m is PlayerMobile pm) { - ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, pm); - if (!Unsafe.IsNullRef(ref stepsTaken) && RegenSteps(pm, ref stepsTaken)) + ref var stepsTaken = ref RegenSteps(pm, out var exists); + if (exists) { stepsTaken.IdleStartTime = Core.Now; } @@ -170,45 +123,59 @@ private static void Logout(Mobile m) if (EnableMountStamina) { // Regain mount idle time - ref var stepsTaken = ref GetMountStepsTaken(m.Mount, out var exists); + ref var stepsTaken = ref RegenSteps(m.Mount, out var exists); if (exists) { - if (RegenSteps(m.Mount, ref stepsTaken)) - { - stepsTaken.IdleStartTime = Core.Now; - _resetHash.Add(m.Mount); - } + stepsTaken.IdleStartTime = Core.Now; + _resetHash.Add(m.Mount); } } if (m is PlayerMobile pm) { - ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, pm); - - if (!Unsafe.IsNullRef(ref stepsTaken) && RegenSteps(pm, ref stepsTaken)) + ref var stepsTaken = ref RegenSteps(pm, out var exists); + if (exists) { stepsTaken.IdleStartTime = Core.Now; } } } + private static bool IsEthereal(IHasSteps m, out PlayerMobile pm) + { + if (m is not EtherealMount and not VirtualMountItem) + { + pm = null; + return false; + } + + pm = ((IMount)m).Rider as PlayerMobile; + return pm != null; + } + public static void RemoveEntry(IHasSteps m) { - if (m != null) + if (m == null) + { + return; + } + + if (GlobalEtherealMountStamina && IsEthereal(m, out var pm)) { - _stepsTaken.Remove(m); + _etherealMountStepCounters.Remove(pm); } + + _stepsTaken.Remove(m); } - public static void OnDismount(IHasSteps mount) + public static void OnDismount(IMount mount) { if (!EnableMountStamina) { return; } - ref var stepsTaken = ref GetMountStepsTaken(mount, out var exists); - if (exists && RegenSteps(mount, ref stepsTaken)) + if (RegenSteps(mount)) { _resetHash.Add(mount); return; @@ -217,7 +184,7 @@ public static void OnDismount(IHasSteps mount) _resetHash.Remove(mount); } - private static ref StepsTaken GetMountStepsTaken(IHasSteps m, out bool exists) + private static ref StepsTaken GetStepsTaken(IHasSteps m, out bool exists) { if (m == null) { @@ -225,6 +192,18 @@ private static ref StepsTaken GetMountStepsTaken(IHasSteps m, out bool exists) return ref Unsafe.NullRef(); } + if (GlobalEtherealMountStamina && IsEthereal(m, out var pm)) + { + ref var stepsCounter = ref CollectionsMarshal.GetValueRefOrNullRef(_etherealMountStepCounters, pm); + if (Unsafe.IsNullRef(ref stepsCounter)) + { + exists = false; + return ref Unsafe.NullRef(); + } + + m = stepsCounter; + } + ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, m); exists = !Unsafe.IsNullRef(ref stepsTaken); return ref stepsTaken; @@ -232,43 +211,65 @@ private static ref StepsTaken GetMountStepsTaken(IHasSteps m, out bool exists) private static ref StepsTaken GetOrCreateStepsTaken(IHasSteps m, out bool created) { + if (GlobalEtherealMountStamina && IsEthereal(m, out var pm)) + { + ref var stepsCounter = ref CollectionsMarshal.GetValueRefOrAddDefault(_etherealMountStepCounters, pm, out var stepsCounterExists); + if (!stepsCounterExists) + { + stepsCounter = new EtherealMountStepCounter(); + } + + m = stepsCounter; + } + ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrAddDefault(_stepsTaken, m, out var exists); created = !exists; + if (created) + { + stepsTaken.Entity = m; + } return ref stepsTaken; } - public static void RegenSteps(IHasSteps m, int amount, bool removeOnInvalidation = true) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool RegenSteps(IHasSteps m, int amount = 0, bool removeOnInvalidation = true) { - ref var stepsTaken = ref GetMountStepsTaken(m, out var exists); - if (exists) - { - RegenSteps(m, ref stepsTaken, removeOnInvalidation); - } + RegenSteps(m, out var exists, amount, removeOnInvalidation); + return exists; } // Triggered on logout, dismount, and world load - private static bool RegenSteps(IHasSteps m, ref StepsTaken stepsTaken, bool removeOnInvalidation = true) + private static ref StepsTaken RegenSteps(IHasSteps m, out bool exists, int amount = 0, bool removeOnInvalidation = true) { - var stepsGained = (int)((Core.Now - stepsTaken.IdleStartTime) / m.IdleTimePerStepsGain * m.StepsGainedPerIdleTime); - return RegenSteps(m, stepsGained, ref stepsTaken, removeOnInvalidation); - } + if (m == null) + { + exists = false; + return ref Unsafe.NullRef(); + } - private static bool RegenSteps(IHasSteps m, int amount, ref StepsTaken stepsTaken, bool removeOnInvalidation = true) - { - if (m == null || Unsafe.IsNullRef(ref stepsTaken)) + ref var stepsTaken = ref GetStepsTaken(m, out exists); + if (!exists) { - return false; + return ref Unsafe.NullRef(); } - stepsTaken.Steps -= amount; + exists = RegenSteps(ref stepsTaken, amount, removeOnInvalidation); + return ref stepsTaken; + } + + private static bool RegenSteps(ref StepsTaken stepsTaken, int amount = 0, bool removeOnInvalidation = true) + { + var entity = stepsTaken.Entity; + var stepsGained = (int)((Core.Now - stepsTaken.IdleStartTime) / entity.IdleTimePerStepsGain * entity.StepsGainedPerIdleTime); + + stepsTaken.Steps -= stepsGained + amount; if (stepsTaken.Steps <= 0) { if (removeOnInvalidation) { - _stepsTaken.Remove(m); - stepsTaken = ref Unsafe.NullRef(); + RemoveEntry(entity); return false; } @@ -330,7 +331,7 @@ private static void ProcessPlayerMovement(int overweight, MovementEventArgs e) from.Stam -= stamLoss; - if (from.Stam == 0) + if (from.Stam <= 0) { // You are too fatigued to move, because you are carrying too much weight! from.SendLocalizedMessage(500109); @@ -339,24 +340,29 @@ private static void ProcessPlayerMovement(int overweight, MovementEventArgs e) } } + if (!running) + { + return; + } + if (AdditionalLossWhenBelow > 0 && from.Stam / Math.Max(from.StamMax, 1.0) < AdditionalLossWhenBelow) { --from.Stam; } - if (CannotMoveWhenFatigued && from.Stam == 0) + if (CannotMoveWhenFatigued && from.Stam <= 0) { from.SendLocalizedMessage(500110); // You are too fatigued to move. e.Blocked = true; return; } - if (running && from is PlayerMobile pm) + if (from is PlayerMobile pm) { ref StepsTaken stepsTaken = ref GetOrCreateStepsTaken(pm, out var created); if (!created) { - RegenSteps(pm, ref stepsTaken, false); + RegenSteps(ref stepsTaken, removeOnInvalidation: false); } var steps = ++stepsTaken.Steps; @@ -378,45 +384,26 @@ private static void ProcessMountMovement(IMount mount, int overweight, MovementE { var from = e.Mobile; + var maxSteps = mount.StepsMax; var running = (e.Direction & Direction.Running) != 0; - var stamLoss = overweight > 0 ? GetStamLoss(from, overweight, running) : 0; - - ref var stepsTaken = ref GetOrCreateStepsTaken(mount, out var created); - // Gain any idle steps - if (!created) - { - RegenSteps(mount, ref stepsTaken, false); // Don't delete the entry if it's reset - } + var stamLoss = overweight > 0 ? GetStamLoss(from, overweight, running) : 0; - if (mount is Mobile m && AdditionalLossWhenBelow > 0 && m.Stam / Math.Max(m.StamMax, 1.0) < AdditionalLossWhenBelow) + if (stamLoss <= 0 && (!running || UseMountStaminaOnlyWhenOverloaded)) { - stamLoss++; + return; } - var maxSteps = mount.StepsMax; + ref var stepsTaken = ref GetOrCreateStepsTaken(mount, out var created); - if (stepsTaken.Steps <= maxSteps) + if (!created) { - // Pre-SA mounts would lose stamina while running even when they were not overweight - if (running && !UseMountStaminaOnlyWhenOverloaded) - { - stamLoss++; - } - - if (stamLoss > 0) - { - stepsTaken.Steps += stamLoss; - stepsTaken.IdleStartTime = Core.Now; - - // This only executes when mounted, so we have the player say it since the actual mount is internalized - if ((mount as BaseCreature)?.Debug == true && stepsTaken.Steps % 20 == 0) - { - from.PublicOverheadMessage(MessageType.Regular, 41, false, $"Steps {stepsTaken.Steps}/{mount.StepsMax}"); - } - } + RegenSteps(ref stepsTaken, removeOnInvalidation: false); } + stepsTaken.Steps += stamLoss + 1; + stepsTaken.IdleStartTime = Core.Now; + if (stepsTaken.Steps > maxSteps) { stepsTaken.Steps = maxSteps; @@ -454,29 +441,14 @@ public static bool IsOverloaded(Mobile m) private struct StepsTaken { + public IHasSteps Entity; public int Steps; public DateTime IdleStartTime; - - public void Serialize(IGenericWriter writer) - { - writer.WriteEncodedInt(0); // version - - writer.WriteEncodedInt(Steps); - writer.WriteDeltaTime(IdleStartTime); - } - - public void Deserialize(IGenericReader reader) - { - reader.ReadEncodedInt(); // version - - Steps = reader.ReadEncodedInt(); - IdleStartTime = reader.ReadDeltaTime(); - } } private class ResetTimer : Timer { - private static TimeSpan CheckDuration = TimeSpan.FromHours(1); + private static readonly TimeSpan _checkDuration = TimeSpan.FromHours(1); private DateTime _nextCheck; @@ -499,31 +471,43 @@ protected override void OnTick() return; } - using var queue = PooledRefQueue.Create(); - - ref StepsTaken stepsTaken = ref Unsafe.NullRef(); - foreach (var m in _resetHash) + if (_resetHash.Count > 0) { - stepsTaken = ref GetMountStepsTaken(m, out var exists); - if (!exists || Core.Now >= stepsTaken.IdleStartTime + ResetDuration) + using var queue = PooledRefQueue.Create(); + + ref StepsTaken stepsTaken = ref Unsafe.NullRef(); + foreach (var m in _resetHash) { - queue.Enqueue(m); + stepsTaken = ref GetStepsTaken(m, out var exists); + if (!exists || Core.Now >= stepsTaken.IdleStartTime + ResetDuration) + { + queue.Enqueue(m); + } } - } - if (_resetHash.Count == queue.Count) - { - _resetHash.Clear(); - } - else - { - while (queue.Count > 0) + if (_resetHash.Count == queue.Count) + { + _resetHash.Clear(); + } + else { - _resetHash.Remove(queue.Dequeue()); + while (queue.Count > 0) + { + _resetHash.Remove(queue.Dequeue()); + } } } - _nextCheck = Core.Now + CheckDuration; + _nextCheck = Core.Now + _checkDuration; } } + + // Placeholder for a special case where Ethereal mount steps are global to the player + private class EtherealMountStepCounter : IHasSteps + { + // The properties are not actually used + public int StepsMax => 3840; + public int StepsGainedPerIdleTime => 1; + public TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(1); + } } diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs b/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs index 5181d7de35..40b1278cff 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs @@ -37,10 +37,10 @@ public BaseMount( public virtual bool AllowMaleRider => true; public virtual bool AllowFemaleRider => true; - // Stamina System - 1 step per 10 seconds and 3840 steps max = 9.667 hours + // Stamina System - 1 step per 1 second and 3840 steps max = 64 minutes public virtual int StepsMax => 3840; public virtual int StepsGainedPerIdleTime => 1; - public virtual TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(10); + public virtual TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(1); [Hue] [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs index 43cf00a5b4..f13cebcbe7 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs @@ -145,7 +145,7 @@ public int Steps public virtual int StepsMax => 3840; // Should be same as horse public virtual int StepsGainedPerIdleTime => 1; - public virtual TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(10); + public virtual TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(1); public void OnRiderDamaged(int amount, Mobile from, bool willKill) { diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 1b0a38609e..43684f2573 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -4293,7 +4293,8 @@ public virtual bool CheckFeed(Mobile from, Item dropped) if (stamGain > 0) { Stam += stamGain; - // 64 food = 3,640 steps + + // 61 food = 3,840 steps StaminaSystem.RegenSteps(this as IHasSteps, stamGain * 4); } diff --git a/Projects/UOContent/Mobiles/VirtualMountItem.cs b/Projects/UOContent/Mobiles/VirtualMountItem.cs index bab4d98158..efc3ea2f5b 100644 --- a/Projects/UOContent/Mobiles/VirtualMountItem.cs +++ b/Projects/UOContent/Mobiles/VirtualMountItem.cs @@ -30,11 +30,10 @@ private void AfterDeserialize() } } - public int Steps { get; set; } - - public int StepsMax => 400; + // If this is ever equipped by a player, steps are treated the same as Ethereal mounts + public int StepsMax => 3840; public int StepsGainedPerIdleTime => 1; - public TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(10); + public TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(1); public void OnRiderDamaged(int amount, Mobile from, bool willKill) {