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.
This commit is contained in:
Kamron Batman 2024-04-05 21:05:16 -07:00 • committed by GitHub
parent c63bf84ad4
commit 90fa2e09de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 160 additions and 176 deletions

View file

@ -15,14 +15,15 @@ public enum DFAlgorithm
PainSpike PainSpike
} }
public class StaminaSystem : GenericPersistence public static class StaminaSystem
{ {
private static readonly ILogger logger = LogFactory.GetLogger(typeof(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<IHasSteps, StepsTaken> _stepsTaken = new(); private static readonly Dictionary<PlayerMobile, IHasSteps> _etherealMountStepCounters = [];
private static readonly OrderedHashSet<IHasSteps> _resetHash = new(); private static readonly Dictionary<IHasSteps, StepsTaken> _stepsTaken = [];
private static readonly HashSet<IHasSteps> _resetHash = [];
// TODO: This exploits single thread processing and is not thread safe! // TODO: This exploits single thread processing and is not thread safe!
public static DFAlgorithm DFA { get; set; } public static DFAlgorithm DFA { get; set; }
@ -35,6 +36,9 @@ public class StaminaSystem : GenericPersistence
public static bool EnableMountStamina { get; set; } public static bool EnableMountStamina { get; set; }
public static bool UseMountStaminaOnlyWhenOverloaded { get; set; } public static bool UseMountStaminaOnlyWhenOverloaded { get; set; }
// Pub 46 (UOML+)
public static bool GlobalEtherealMountStamina { get; set; }
public static void Configure() public static void Configure()
{ {
CannotMoveWhenFatigued = ServerConfiguration.GetOrUpdateSetting("stamina.cannotMoveWhenFatigued", !Core.AOS); CannotMoveWhenFatigued = ServerConfiguration.GetOrUpdateSetting("stamina.cannotMoveWhenFatigued", !Core.AOS);
@ -44,56 +48,7 @@ public class StaminaSystem : GenericPersistence
AdditionalLossWhenBelow = ServerConfiguration.GetOrUpdateSetting("stamina.additionalLossWhenBelow", 0.10); AdditionalLossWhenBelow = ServerConfiguration.GetOrUpdateSetting("stamina.additionalLossWhenBelow", 0.10);
EnableMountStamina = ServerConfiguration.GetOrUpdateSetting("stamina.enableMountStamina", true); EnableMountStamina = ServerConfiguration.GetOrUpdateSetting("stamina.enableMountStamina", true);
UseMountStaminaOnlyWhenOverloaded = ServerConfiguration.GetSetting("stamina.useMountStaminaOnlyWhenOverloaded", Core.SA); UseMountStaminaOnlyWhenOverloaded = ServerConfiguration.GetSetting("stamina.useMountStaminaOnlyWhenOverloaded", Core.SA);
} GlobalEtherealMountStamina = ServerConfiguration.GetSetting("stamina.globalEtherealMountStamina", Core.ML);
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<ISerializable>() 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);
}
}
}
} }
public static void Initialize() public static void Initialize()
@ -107,14 +62,12 @@ public class StaminaSystem : GenericPersistence
using var queue = PooledRefQueue<IHasSteps>.Create(); using var queue = PooledRefQueue<IHasSteps>.Create();
foreach (var m in _stepsTaken.Keys) foreach (var m in _stepsTaken.Keys)
{ {
ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, m); // We cannot remove since we are iterating.
if (!Unsafe.IsNullRef(ref stepsTaken)) ref var stepsTaken = ref RegenSteps(m, out var exists, removeOnInvalidation: false);
if (exists && stepsTaken.Steps <= 0)
{ {
RegenSteps(m, ref stepsTaken, false); queue.Enqueue(m);
if (stepsTaken.Steps <= 0)
{
queue.Enqueue(m);
}
} }
} }
@ -134,7 +87,7 @@ public class StaminaSystem : GenericPersistence
if (EnableMountStamina) if (EnableMountStamina)
{ {
// Start idle for mount // 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 (exists)
{ {
if (stepsTaken.Steps <= 0 || Core.Now >= stepsTaken.IdleStartTime + ResetDuration) if (stepsTaken.Steps <= 0 || Core.Now >= stepsTaken.IdleStartTime + ResetDuration)
@ -152,8 +105,8 @@ public class StaminaSystem : GenericPersistence
if (m is PlayerMobile pm) if (m is PlayerMobile pm)
{ {
ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, pm); ref var stepsTaken = ref RegenSteps(pm, out var exists);
if (!Unsafe.IsNullRef(ref stepsTaken) && RegenSteps(pm, ref stepsTaken)) if (exists)
{ {
stepsTaken.IdleStartTime = Core.Now; stepsTaken.IdleStartTime = Core.Now;
} }
@ -170,45 +123,59 @@ public class StaminaSystem : GenericPersistence
if (EnableMountStamina) if (EnableMountStamina)
{ {
// Regain mount idle time // 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 (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) if (m is PlayerMobile pm)
{ {
ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, pm); ref var stepsTaken = ref RegenSteps(pm, out var exists);
if (exists)
if (!Unsafe.IsNullRef(ref stepsTaken) && RegenSteps(pm, ref stepsTaken))
{ {
stepsTaken.IdleStartTime = Core.Now; stepsTaken.IdleStartTime = Core.Now;
} }
} }
} }
public static void RemoveEntry(IHasSteps m) private static bool IsEthereal(IHasSteps m, out PlayerMobile pm)
{ {
if (m != null) if (m is not EtherealMount and not VirtualMountItem)
{ {
_stepsTaken.Remove(m); pm = null;
return false;
} }
pm = ((IMount)m).Rider as PlayerMobile;
return pm != null;
} }
public static void OnDismount(IHasSteps mount) public static void RemoveEntry(IHasSteps m)
{
if (m == null)
{
return;
}
if (GlobalEtherealMountStamina && IsEthereal(m, out var pm))
{
_etherealMountStepCounters.Remove(pm);
}
_stepsTaken.Remove(m);
}
public static void OnDismount(IMount mount)
{ {
if (!EnableMountStamina) if (!EnableMountStamina)
{ {
return; return;
} }
ref var stepsTaken = ref GetMountStepsTaken(mount, out var exists); if (RegenSteps(mount))
if (exists && RegenSteps(mount, ref stepsTaken))
{ {
_resetHash.Add(mount); _resetHash.Add(mount);
return; return;
@ -217,7 +184,7 @@ public class StaminaSystem : GenericPersistence
_resetHash.Remove(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) if (m == null)
{ {
@ -225,6 +192,18 @@ public class StaminaSystem : GenericPersistence
return ref Unsafe.NullRef<StepsTaken>(); return ref Unsafe.NullRef<StepsTaken>();
} }
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<StepsTaken>();
}
m = stepsCounter;
}
ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, m); ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, m);
exists = !Unsafe.IsNullRef(ref stepsTaken); exists = !Unsafe.IsNullRef(ref stepsTaken);
return ref stepsTaken; return ref stepsTaken;
@ -232,43 +211,65 @@ public class StaminaSystem : GenericPersistence
private static ref StepsTaken GetOrCreateStepsTaken(IHasSteps m, out bool created) 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); ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrAddDefault(_stepsTaken, m, out var exists);
created = !exists; created = !exists;
if (created)
{
stepsTaken.Entity = m;
}
return ref stepsTaken; 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); RegenSteps(m, out var exists, amount, removeOnInvalidation);
if (exists) return exists;
{
RegenSteps(m, ref stepsTaken, removeOnInvalidation);
}
} }
// Triggered on logout, dismount, and world load // 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); if (m == null)
return RegenSteps(m, stepsGained, ref stepsTaken, removeOnInvalidation);
}
private static bool RegenSteps(IHasSteps m, int amount, ref StepsTaken stepsTaken, bool removeOnInvalidation = true)
{
if (m == null || Unsafe.IsNullRef(ref stepsTaken))
{ {
return false; exists = false;
return ref Unsafe.NullRef<StepsTaken>();
} }
stepsTaken.Steps -= amount; ref var stepsTaken = ref GetStepsTaken(m, out exists);
if (!exists)
{
return ref Unsafe.NullRef<StepsTaken>();
}
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 (stepsTaken.Steps <= 0)
{ {
if (removeOnInvalidation) if (removeOnInvalidation)
{ {
_stepsTaken.Remove(m); RemoveEntry(entity);
stepsTaken = ref Unsafe.NullRef<StepsTaken>();
return false; return false;
} }
@ -330,7 +331,7 @@ public class StaminaSystem : GenericPersistence
from.Stam -= stamLoss; from.Stam -= stamLoss;
if (from.Stam == 0) if (from.Stam <= 0)
{ {
// You are too fatigued to move, because you are carrying too much weight! // You are too fatigued to move, because you are carrying too much weight!
from.SendLocalizedMessage(500109); from.SendLocalizedMessage(500109);
@ -339,24 +340,29 @@ public class StaminaSystem : GenericPersistence
} }
} }
if (!running)
{
return;
}
if (AdditionalLossWhenBelow > 0 && from.Stam / Math.Max(from.StamMax, 1.0) < AdditionalLossWhenBelow) if (AdditionalLossWhenBelow > 0 && from.Stam / Math.Max(from.StamMax, 1.0) < AdditionalLossWhenBelow)
{ {
--from.Stam; --from.Stam;
} }
if (CannotMoveWhenFatigued && from.Stam == 0) if (CannotMoveWhenFatigued && from.Stam <= 0)
{ {
from.SendLocalizedMessage(500110); // You are too fatigued to move. from.SendLocalizedMessage(500110); // You are too fatigued to move.
e.Blocked = true; e.Blocked = true;
return; return;
} }
if (running && from is PlayerMobile pm) if (from is PlayerMobile pm)
{ {
ref StepsTaken stepsTaken = ref GetOrCreateStepsTaken(pm, out var created); ref StepsTaken stepsTaken = ref GetOrCreateStepsTaken(pm, out var created);
if (!created) if (!created)
{ {
RegenSteps(pm, ref stepsTaken, false); RegenSteps(ref stepsTaken, removeOnInvalidation: false);
} }
var steps = ++stepsTaken.Steps; var steps = ++stepsTaken.Steps;
@ -378,44 +384,25 @@ public class StaminaSystem : GenericPersistence
{ {
var from = e.Mobile; var from = e.Mobile;
var maxSteps = mount.StepsMax;
var running = (e.Direction & Direction.Running) != 0; var running = (e.Direction & Direction.Running) != 0;
var stamLoss = overweight > 0 ? GetStamLoss(from, overweight, running) : 0; var stamLoss = overweight > 0 ? GetStamLoss(from, overweight, running) : 0;
if (stamLoss <= 0 && (!running || UseMountStaminaOnlyWhenOverloaded))
{
return;
}
ref var stepsTaken = ref GetOrCreateStepsTaken(mount, out var created); ref var stepsTaken = ref GetOrCreateStepsTaken(mount, out var created);
// Gain any idle steps
if (!created) if (!created)
{ {
RegenSteps(mount, ref stepsTaken, false); // Don't delete the entry if it's reset RegenSteps(ref stepsTaken, removeOnInvalidation: false);
} }
if (mount is Mobile m && AdditionalLossWhenBelow > 0 && m.Stam / Math.Max(m.StamMax, 1.0) < AdditionalLossWhenBelow) stepsTaken.Steps += stamLoss + 1;
{ stepsTaken.IdleStartTime = Core.Now;
stamLoss++;
}
var maxSteps = mount.StepsMax;
if (stepsTaken.Steps <= maxSteps)
{
// 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}");
}
}
}
if (stepsTaken.Steps > maxSteps) if (stepsTaken.Steps > maxSteps)
{ {
@ -454,29 +441,14 @@ public class StaminaSystem : GenericPersistence
private struct StepsTaken private struct StepsTaken
{ {
public IHasSteps Entity;
public int Steps; public int Steps;
public DateTime IdleStartTime; 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 class ResetTimer : Timer
{ {
private static TimeSpan CheckDuration = TimeSpan.FromHours(1); private static readonly TimeSpan _checkDuration = TimeSpan.FromHours(1);
private DateTime _nextCheck; private DateTime _nextCheck;
@ -499,31 +471,43 @@ public class StaminaSystem : GenericPersistence
return; return;
} }
using var queue = PooledRefQueue<IHasSteps>.Create(); if (_resetHash.Count > 0)
ref StepsTaken stepsTaken = ref Unsafe.NullRef<StepsTaken>();
foreach (var m in _resetHash)
{ {
stepsTaken = ref GetMountStepsTaken(m, out var exists); using var queue = PooledRefQueue<IHasSteps>.Create();
if (!exists || Core.Now >= stepsTaken.IdleStartTime + ResetDuration)
ref StepsTaken stepsTaken = ref Unsafe.NullRef<StepsTaken>();
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)
{
_resetHash.Remove(queue.Dequeue());
}
} }
} }
if (_resetHash.Count == queue.Count) _nextCheck = Core.Now + _checkDuration;
{
_resetHash.Clear();
}
else
{
while (queue.Count > 0)
{
_resetHash.Remove(queue.Dequeue());
}
}
_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);
}
} }

View file

@ -37,10 +37,10 @@ public abstract partial class BaseMount : BaseCreature, IMount
public virtual bool AllowMaleRider => true; public virtual bool AllowMaleRider => true;
public virtual bool AllowFemaleRider => 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 StepsMax => 3840;
public virtual int StepsGainedPerIdleTime => 1; public virtual int StepsGainedPerIdleTime => 1;
public virtual TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(10); public virtual TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(1);
[Hue] [Hue]
[CommandProperty(AccessLevel.GameMaster)] [CommandProperty(AccessLevel.GameMaster)]

View file

@ -145,7 +145,7 @@ namespace Server.Mobiles
public virtual int StepsMax => 3840; // Should be same as horse public virtual int StepsMax => 3840; // Should be same as horse
public virtual int StepsGainedPerIdleTime => 1; 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) public void OnRiderDamaged(int amount, Mobile from, bool willKill)
{ {

View file

@ -4293,7 +4293,8 @@ namespace Server.Mobiles
if (stamGain > 0) if (stamGain > 0)
{ {
Stam += stamGain; Stam += stamGain;
// 64 food = 3,640 steps
// 61 food = 3,840 steps
StaminaSystem.RegenSteps(this as IHasSteps, stamGain * 4); StaminaSystem.RegenSteps(this as IHasSteps, stamGain * 4);
} }

View file

@ -30,11 +30,10 @@ public partial class VirtualMountItem : Item, IMountItem, IMount
} }
} }
public int Steps { get; set; } // If this is ever equipped by a player, steps are treated the same as Ethereal mounts
public int StepsMax => 3840;
public int StepsMax => 400;
public int StepsGainedPerIdleTime => 1; 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) public void OnRiderDamaged(int amount, Mobile from, bool willKill)
{ {