diff --git a/Projects/Server/Mobiles/IHasSteps.cs b/Projects/Server/Mobiles/IHasSteps.cs
new file mode 100644
index 0000000000..15bd9a9599
--- /dev/null
+++ b/Projects/Server/Mobiles/IHasSteps.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Server.Mobiles;
+
+public interface IHasSteps
+{
+ int StepsMax { get; }
+
+ int StepsGainedPerIdleTime { get; }
+
+ TimeSpan IdleTimePerStepsGain { get; }
+}
diff --git a/Projects/Server/Mobiles/IMount.cs b/Projects/Server/Mobiles/IMount.cs
index 78c6424389..9ce95e6927 100644
--- a/Projects/Server/Mobiles/IMount.cs
+++ b/Projects/Server/Mobiles/IMount.cs
@@ -13,9 +13,11 @@
* along with this program. If not, see . *
*************************************************************************/
+using System;
+
namespace Server.Mobiles;
-public interface IMount
+public interface IMount : IHasSteps
{
Mobile Rider { get; set; }
void OnRiderDamaged(int amount, Mobile from, bool willKill);
diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs
index e93aac4be7..37f3f4c873 100644
--- a/Projects/Server/Mobiles/Mobile.cs
+++ b/Projects/Server/Mobiles/Mobile.cs
@@ -4081,33 +4081,49 @@ private bool CanMove(Direction d, Point3D oldLocation, ref Point3D newLocation)
switch (d & Direction.Mask)
{
case Direction.North:
- --y;
- break;
+ {
+ --y;
+ break;
+ }
case Direction.Right:
- ++x;
- --y;
- break;
+ {
+ ++x;
+ --y;
+ break;
+ }
case Direction.East:
- ++x;
- break;
+ {
+ ++x;
+ break;
+ }
case Direction.Down:
- ++x;
- ++y;
- break;
+ {
+ ++x;
+ ++y;
+ break;
+ }
case Direction.South:
- ++y;
- break;
+ {
+ ++y;
+ break;
+ }
case Direction.Left:
- --x;
- ++y;
- break;
+ {
+ --x;
+ ++y;
+ break;
+ }
case Direction.West:
- --x;
- break;
+ {
+ --x;
+ break;
+ }
case Direction.Up:
- --x;
- --y;
- break;
+ {
+ --x;
+ --y;
+ break;
+ }
}
newLocation.m_X = x;
@@ -5428,38 +5444,64 @@ public virtual void DoSpeech(string text, int[] keywords, MessageType type, int
switch (type)
{
case MessageType.Regular:
- SpeechHue = hue;
- break;
+ {
+ SpeechHue = hue;
+ break;
+ }
case MessageType.Emote:
- EmoteHue = hue;
- break;
+ {
+ EmoteHue = hue;
+ break;
+ }
case MessageType.Whisper:
- WhisperHue = hue;
- range = 1;
- break;
+ {
+ WhisperHue = hue;
+ range = 1;
+ break;
+ }
case MessageType.Yell:
- YellHue = hue;
- range = 18;
- break;
+ {
+ YellHue = hue;
+ range = 18;
+ break;
+ }
case MessageType.System:
- break;
+ {
+ break;
+ }
case MessageType.Label:
- break;
+ {
+ break;
+ }
case MessageType.Focus:
- break;
+ {
+ break;
+ }
case MessageType.Spell:
- break;
+ {
+ break;
+ }
case MessageType.Guild:
- break;
+ {
+ break;
+ }
case MessageType.Alliance:
- break;
+ {
+ break;
+ }
case MessageType.Command:
- break;
+ {
+ break;
+ }
case MessageType.Encoded:
- break;
+ {
+ break;
+ }
default:
- type = MessageType.Regular;
- break;
+ {
+ type = MessageType.Regular;
+ break;
+ }
}
var regArgs = new SpeechEventArgs(this, text, type, hue, keywords);
diff --git a/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholySteed.cs b/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholySteed.cs
index 3388af2239..5e06a24229 100644
--- a/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholySteed.cs
+++ b/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholySteed.cs
@@ -44,6 +44,7 @@ public UnholySteed(Serial serial) : base(serial)
{
}
+ public override int StepsMax => 6400;
public override string CorpseName => "an unholy corpse";
public override bool IsDispellable => false;
public override bool IsBondable => false;
diff --git a/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolySteed.cs b/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolySteed.cs
index 1999d3f70e..a5fb4c6021 100644
--- a/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolySteed.cs
+++ b/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolySteed.cs
@@ -43,6 +43,7 @@ public HolySteed(Serial serial) : base(serial)
{
}
+ public override int StepsMax => 6400;
public override string CorpseName => "a holy corpse";
public override bool IsDispellable => false;
public override bool IsBondable => false;
diff --git a/Projects/UOContent/Engines/Factions/Mobiles/FactionWarHorse.cs b/Projects/UOContent/Engines/Factions/Mobiles/FactionWarHorse.cs
index 896c79546e..f6a19ccca1 100644
--- a/Projects/UOContent/Engines/Factions/Mobiles/FactionWarHorse.cs
+++ b/Projects/UOContent/Engines/Factions/Mobiles/FactionWarHorse.cs
@@ -49,6 +49,7 @@ public FactionWarHorse(Serial serial) : base(serial)
{
}
+ public override int StepsMax => 6400;
public override string CorpseName => "a war horse corpse";
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
diff --git a/Projects/UOContent/Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs b/Projects/UOContent/Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs
index 598ddb7732..9ce69ddeed 100644
--- a/Projects/UOContent/Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs
+++ b/Projects/UOContent/Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs
@@ -468,61 +468,4 @@ public override void Deserialize(IGenericReader reader)
Timer.StartTimer(Register);
}
}
-
- public class VirtualMount : IMount
- {
- private readonly VirtualMountItem m_Item;
-
- public VirtualMount(VirtualMountItem item) => m_Item = item;
-
- Mobile IMount.Rider
- {
- get => m_Item.Rider;
- set { }
- }
-
- public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill)
- {
- }
- }
-
- [SerializationGenerator(0, false)]
- public partial class VirtualMountItem : Item, IMountItem
- {
- private VirtualMount _mount;
-
- [SerializableField(0)]
- private Mobile _rider;
-
- public VirtualMountItem(Mobile mob) : base(0x3EA0)
- {
- Layer = Layer.Mount;
-
- Rider = mob;
- _mount = new VirtualMount(this);
- }
-
- public IMount Mount => _mount;
-
- [AfterDeserialization]
- private void AfterDeserialization()
- {
- if (_rider?.Deleted != false)
- {
- Delete();
- }
- else
- {
- _mount = new VirtualMount(this);
- }
- }
-
- public override DeathMoveResult OnParentDeath(Mobile parent)
- {
- _mount = null;
- Delete();
-
- return DeathMoveResult.RemainEquipped;
- }
- }
}
diff --git a/Projects/UOContent/Engines/Factions/Mobiles/Guards/Types/FactionDragoon.cs b/Projects/UOContent/Engines/Factions/Mobiles/Guards/Types/FactionDragoon.cs
index 3811c675f0..f78faf3c76 100644
--- a/Projects/UOContent/Engines/Factions/Mobiles/Guards/Types/FactionDragoon.cs
+++ b/Projects/UOContent/Engines/Factions/Mobiles/Guards/Types/FactionDragoon.cs
@@ -1,4 +1,5 @@
using Server.Items;
+using Server.Mobiles;
namespace Server.Factions
{
diff --git a/Projects/UOContent/Engines/Factions/Mobiles/Guards/Types/FactionPaladin.cs b/Projects/UOContent/Engines/Factions/Mobiles/Guards/Types/FactionPaladin.cs
index b74f74b7e7..9e0f96bc54 100644
--- a/Projects/UOContent/Engines/Factions/Mobiles/Guards/Types/FactionPaladin.cs
+++ b/Projects/UOContent/Engines/Factions/Mobiles/Guards/Types/FactionPaladin.cs
@@ -1,4 +1,5 @@
using Server.Items;
+using Server.Mobiles;
namespace Server.Factions
{
diff --git a/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs b/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs
index 0f6dc9f58f..d997a11f22 100644
--- a/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs
+++ b/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs
@@ -18,8 +18,6 @@ public static class PlayerMurderSystem
// Only the players that are online
private static readonly HashSet _contextTerms = new(MurderContext.EqualityComparer.Default);
- private static readonly Timer _murdererTimer = new MurdererTimer();
-
private static TimeSpan _shortTermMurderDuration;
private static TimeSpan _longTermMurderDuration;
@@ -41,8 +39,6 @@ public static void Initialize()
EventSink.Disconnected += OnDisconnected;
EventSink.Login += OnLogin;
EventSink.PlayerDeleted += OnPlayerDeleted;
-
- _murdererTimer.Start();
}
private static void OnPlayerDeleted(Mobile m)
@@ -182,6 +178,11 @@ public MurdererTimer() : base(TimeSpan.FromMinutes(5.0), TimeSpan.FromMinutes(5.
{
}
+ public static void Initialize()
+ {
+ new MurdererTimer().Start();
+ }
+
protected override void OnTick()
{
if (_contextTerms.Count == 0)
@@ -208,5 +209,10 @@ protected override void OnTick()
}
}
}
+
+ ~MurdererTimer()
+ {
+ PlayerMurderSystem.logger.Error($"{nameof(MurdererTimer)} is no longer running!");
+ }
}
}
diff --git a/Projects/UOContent/Engines/Virtues/VirtueSystem.cs b/Projects/UOContent/Engines/Virtues/VirtueSystem.cs
index ebc7d57a37..12bde4fb3a 100644
--- a/Projects/UOContent/Engines/Virtues/VirtueSystem.cs
+++ b/Projects/UOContent/Engines/Virtues/VirtueSystem.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Server.Collections;
+using Server.Logging;
using Server.Mobiles;
namespace Server.Engines.Virtues;
@@ -29,9 +30,9 @@ public enum VirtueName
public static class VirtueSystem
{
- private static readonly Dictionary _playerVirtues = new();
+ private static readonly ILogger logger = LogFactory.GetLogger(typeof(VirtueSystem));
- private static readonly Timer _virtueTimer = new VirtueTimer();
+ private static readonly Dictionary _playerVirtues = new();
private static void FixVirtue(Mobile m, int[] virtueValues)
{
@@ -66,8 +67,6 @@ public static void Initialize()
FixVirtue(m, values);
}
}
-
- _virtueTimer.Start();
}
private static void Serialize(IGenericWriter writer)
@@ -346,6 +345,11 @@ public VirtueTimer() : base(TimeSpan.FromMinutes(5.0), TimeSpan.FromMinutes(5.0)
{
}
+ public static void Initialize()
+ {
+ new VirtueTimer().Start();
+ }
+
protected override void OnTick()
{
if (_playerVirtues.Count == 0)
@@ -371,5 +375,10 @@ protected override void OnTick()
_playerVirtues.Remove((PlayerMobile)queue.Dequeue());
}
}
+
+ ~VirtueTimer()
+ {
+ VirtueSystem.logger.Error($"{nameof(VirtueTimer)} is no longer running!");
+ }
}
}
diff --git a/Projects/UOContent/Migrations/Server.Mobiles.EtherealMount.v3.json b/Projects/UOContent/Migrations/Server.Mobiles.EtherealMount.v4.json
similarity index 74%
rename from Projects/UOContent/Migrations/Server.Mobiles.EtherealMount.v3.json
rename to Projects/UOContent/Migrations/Server.Mobiles.EtherealMount.v4.json
index d5f63f9ab0..b3fb60bd10 100644
--- a/Projects/UOContent/Migrations/Server.Mobiles.EtherealMount.v3.json
+++ b/Projects/UOContent/Migrations/Server.Mobiles.EtherealMount.v4.json
@@ -1,10 +1,11 @@
{
- "version": 3,
+ "version": 4,
"type": "Server.Mobiles.EtherealMount",
"properties": [
{
"name": "IsDonationItem",
"type": "bool",
+ "usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
@@ -13,6 +14,7 @@
{
"name": "IsRewardItem",
"type": "bool",
+ "usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
@@ -37,7 +39,17 @@
{
"name": "Rider",
"type": "Server.Mobile",
+ "usesSaveFlag": true,
"rule": "SerializableInterfaceMigrationRule"
+ },
+ {
+ "name": "Steps",
+ "type": "int",
+ "usesSaveFlag": true,
+ "rule": "PrimitiveTypeMigrationRule",
+ "ruleArguments": [
+ ""
+ ]
}
]
}
\ No newline at end of file
diff --git a/Projects/UOContent/Migrations/Server.Mobiles.VirtualMountItem.v0.json b/Projects/UOContent/Migrations/Server.Mobiles.VirtualMountItem.v0.json
new file mode 100644
index 0000000000..2b50a2479b
--- /dev/null
+++ b/Projects/UOContent/Migrations/Server.Mobiles.VirtualMountItem.v0.json
@@ -0,0 +1,11 @@
+{
+ "version": 0,
+ "type": "Server.Mobiles.VirtualMountItem",
+ "properties": [
+ {
+ "name": "Rider",
+ "type": "Server.Mobile",
+ "rule": "SerializableInterfaceMigrationRule"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Projects/UOContent/Misc/StaminaSystem.cs b/Projects/UOContent/Misc/StaminaSystem.cs
index e033c6a7d9..fec730a453 100644
--- a/Projects/UOContent/Misc/StaminaSystem.cs
+++ b/Projects/UOContent/Misc/StaminaSystem.cs
@@ -1,4 +1,9 @@
using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Server.Collections;
+using Server.Logging;
using Server.Mobiles;
using Server.Spells.Ninjitsu;
@@ -12,13 +17,263 @@ public enum DFAlgorithm
public static class StaminaSystem
{
- public const int OverloadAllowance = 4; // We can be four stones overweight without getting fatigued
+ private static readonly ILogger logger = LogFactory.GetLogger(typeof(StaminaSystem));
+ private static TimeSpan ResetDuration = TimeSpan.FromHours(24);
+
+ private static readonly Dictionary _stepsTaken = new();
+ private static readonly OrderedHashSet _resetHash = new();
+
+ // TODO: This exploits single thread processing and is not thread safe!
public static DFAlgorithm DFA { get; set; }
+ public static int StonesOverweightAllowance { get; set; }
+ public static bool CannotMoveWhenFatigued { get; set; }
+ public static int StonesPerOverweightLoss { get; set; }
+ public static int BaseOverweightLoss { get; set; }
+ public static double AdditionalLossWhenBelow { get; set; }
+ public static bool EnableMountStamina { get; set; }
+ public static bool UseMountStaminaWhenOverweight { get; set; }
+
+ public static void Configure()
+ {
+ CannotMoveWhenFatigued = ServerConfiguration.GetOrUpdateSetting("stamina.cannotMoveWhenFatigued", !Core.AOS);
+ StonesPerOverweightLoss = ServerConfiguration.GetOrUpdateSetting("stamina.stonesPerOverweightLoss", 25);
+ StonesOverweightAllowance = ServerConfiguration.GetOrUpdateSetting("stamina.stonesOverweightAllowance", 4);
+ BaseOverweightLoss = ServerConfiguration.GetOrUpdateSetting("stamina.baseOverweightLoss", 5);
+ AdditionalLossWhenBelow = ServerConfiguration.GetOrUpdateSetting("stamina.additionalLossWhenBelow", 0.10);
+ EnableMountStamina = ServerConfiguration.GetOrUpdateSetting("stamina.enableMountStamina", true);
+ UseMountStaminaWhenOverweight = ServerConfiguration.GetSetting("stamina.useStamina", Core.SA);
+
+ GenericPersistence.Register("StaminaSystem", Serialize, Deserialize);
+ }
+
+ private static 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);
+ }
+ }
+
+ private static 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);
+ }
+ }
+ }
+ }
+
public static void Initialize()
{
EventSink.Movement += EventSink_Movement;
+ EventSink.Login += Login;
+ EventSink.Logout += Logout;
+ EventSink.PlayerDeleted += OnPlayerDeleted;
+
+ // Credit idle time
+ using var queue = PooledRefQueue.Create();
+ foreach (var m in _stepsTaken.Keys)
+ {
+ ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, m);
+ if (!Unsafe.IsNullRef(ref stepsTaken))
+ {
+ RegenSteps(m, ref stepsTaken, false);
+ if (stepsTaken.Steps <= 0)
+ {
+ queue.Enqueue(m);
+ }
+ }
+ }
+
+ while (queue.Count > 0)
+ {
+ _stepsTaken.Remove(queue.Dequeue());
+ }
+ }
+
+ private static void OnPlayerDeleted(Mobile m)
+ {
+ RemoveEntry(m as IHasSteps);
+ }
+
+ private static void Login(Mobile m)
+ {
+ if (EnableMountStamina)
+ {
+ // Start idle for mount
+ ref var stepsTaken = ref GetMountStepsTaken(m.Mount, out var exists);
+ if (exists)
+ {
+ if (stepsTaken.Steps <= 0 || Core.Now >= stepsTaken.IdleStartTime + ResetDuration)
+ {
+ _stepsTaken.Remove(m.Mount);
+ }
+ else
+ {
+ stepsTaken.IdleStartTime = Core.Now;
+ }
+ }
+
+ _resetHash.Remove(m.Mount);
+ }
+
+ if (m is PlayerMobile pm)
+ {
+ ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, pm);
+ if (!Unsafe.IsNullRef(ref stepsTaken) && RegenSteps(pm, ref stepsTaken))
+ {
+ stepsTaken.IdleStartTime = Core.Now;
+ }
+ }
+ }
+
+ private static void Logout(Mobile m)
+ {
+ if (_stepsTaken == null || _stepsTaken.Count == 0)
+ {
+ return;
+ }
+
+ if (EnableMountStamina)
+ {
+ // Regain mount idle time
+ ref var stepsTaken = ref GetMountStepsTaken(m.Mount, out var exists);
+ if (exists)
+ {
+ if (RegenSteps(m.Mount, ref stepsTaken))
+ {
+ 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))
+ {
+ stepsTaken.IdleStartTime = Core.Now;
+ }
+ }
+ }
+
+ public static void RemoveEntry(IHasSteps m)
+ {
+ if (m != null)
+ {
+ _stepsTaken.Remove(m);
+ }
+ }
+
+ public static void OnDismount(IHasSteps mount)
+ {
+ if (!EnableMountStamina)
+ {
+ return;
+ }
+
+ ref var stepsTaken = ref GetMountStepsTaken(mount, out var exists);
+ if (exists && RegenSteps(mount, ref stepsTaken))
+ {
+ _resetHash.Add(mount);
+ return;
+ }
+
+ _resetHash.Remove(mount);
+ }
+
+ private static ref StepsTaken GetMountStepsTaken(IHasSteps m, out bool exists)
+ {
+ if (m == null)
+ {
+ exists = false;
+ return ref Unsafe.NullRef();
+ }
+
+ ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, m);
+ exists = !Unsafe.IsNullRef(ref stepsTaken);
+ return ref stepsTaken;
+ }
+
+ private static ref StepsTaken GetOrCreateStepsTaken(IHasSteps m, out bool created)
+ {
+ ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrAddDefault(_stepsTaken, m, out var exists);
+ created = !exists;
+
+ return ref stepsTaken;
+ }
+
+ public static void RegenSteps(IHasSteps m, int amount, bool removeOnInvalidation = true)
+ {
+ ref var stepsTaken = ref GetMountStepsTaken(m, out var exists);
+ if (exists)
+ {
+ RegenSteps(m, ref stepsTaken, removeOnInvalidation);
+ }
+ }
+
+ // Triggered on logout, dismount, and world load
+ private static bool RegenSteps(IHasSteps m, ref StepsTaken stepsTaken, bool removeOnInvalidation = true)
+ {
+ var stepsGained = (int)((Core.Now - stepsTaken.IdleStartTime) / m.IdleTimePerStepsGain * m.StepsGainedPerIdleTime);
+ 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;
+ }
+
+ stepsTaken.Steps -= amount;
+
+ if (stepsTaken.Steps <= 0)
+ {
+ if (removeOnInvalidation)
+ {
+ _stepsTaken.Remove(m);
+ stepsTaken = ref Unsafe.NullRef();
+ return false;
+ }
+
+ stepsTaken.Steps = 0;
+ }
+
+ return true;
}
public static void FatigueOnDamage(Mobile m, int damage)
@@ -42,63 +297,135 @@ public static void EventSink_Movement(MovementEventArgs e)
{
var from = e.Mobile;
- if (!from.Alive || from.AccessLevel > AccessLevel.Player)
+ if (!from.Player || !from.Alive || from.AccessLevel > AccessLevel.Player)
{
return;
}
- if (!from.Player)
+ var maxWeight = GetMaxWeight(from) + StonesOverweightAllowance;
+ var overweight = Mobile.BodyWeight + from.TotalWeight - maxWeight;
+
+ if (EnableMountStamina && from.Mount != null)
{
- // Else it won't work on monsters.
- DeathStrike.AddStep(from);
- return;
+ ProcessMountMovement(from.Mount, overweight, e);
+ }
+ else
+ {
+ ProcessPlayerMovement(overweight, e);
}
- var maxWeight = GetMaxWeight(from) + OverloadAllowance;
- var overWeight = Mobile.BodyWeight + from.TotalWeight - maxWeight;
+ DeathStrike.AddStep(from);
+ }
- if (overWeight > 0)
+ private static void ProcessPlayerMovement(int overweight, MovementEventArgs e)
+ {
+ var from = e.Mobile;
+ var running = (e.Direction & Direction.Running) != 0;
+
+ if (overweight > 0)
{
- from.Stam -= GetStamLoss(from, overWeight, (e.Direction & Direction.Running) != 0);
+ var stamLoss = GetStamLoss(from, overweight, running);
+
+ from.Stam -= stamLoss;
if (from.Stam == 0)
{
- from.SendLocalizedMessage(
- 500109
- ); // 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);
e.Blocked = true;
return;
}
}
- if (from.Stam * 100 / Math.Max(from.StamMax, 1) < 10)
+ if (AdditionalLossWhenBelow > 0 && from.Stam / Math.Max(from.StamMax, 1.0) < AdditionalLossWhenBelow)
{
--from.Stam;
}
- if (!Core.AOS && from.Stam == 0)
+ if (CannotMoveWhenFatigued && from.Stam == 0)
{
from.SendLocalizedMessage(500110); // You are too fatigued to move.
e.Blocked = true;
return;
}
- if (from is PlayerMobile pm)
+ if (running && from is PlayerMobile pm)
{
- var amt = pm.Mounted ? 48 : 16;
+ ref StepsTaken stepsTaken = ref GetOrCreateStepsTaken(pm, out var created);
+ if (!created)
+ {
+ RegenSteps(pm, ref stepsTaken, false);
+ }
- if (++pm.StepsTaken % amt == 0)
+ var steps = ++stepsTaken.Steps;
+
+ // 3x steps if mounted is used when EnableMountStamina is false
+ var maxSteps = pm.StepsMax * (from.Mount != null ? 3 : 1);
+
+ if (steps > maxSteps)
{
--pm.Stam;
+ stepsTaken.Steps = 0;
}
+
+ stepsTaken.IdleStartTime = Core.Now;
}
+ }
- DeathStrike.AddStep(from);
+ private static void ProcessMountMovement(IMount mount, int overweight, MovementEventArgs e)
+ {
+ var from = e.Mobile;
+
+ 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
+ }
+
+ if (mount is Mobile m && AdditionalLossWhenBelow > 0 && m.Stam / Math.Max(m.StamMax, 1.0) < AdditionalLossWhenBelow)
+ {
+ 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 && !UseMountStaminaWhenOverweight)
+ {
+ 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)
+ {
+ stepsTaken.Steps = maxSteps;
+ from.SendLocalizedMessage(500108); // Your mount is too fatigued to move.
+ e.Blocked = true;
+ }
}
public static int GetStamLoss(Mobile from, int overWeight, bool running)
{
- var loss = 5 + overWeight / 25;
+ var loss = BaseOverweightLoss + overWeight / StonesPerOverweightLoss;
if (from.Mounted)
{
@@ -120,6 +447,81 @@ public static bool IsOverloaded(Mobile m)
return false;
}
- return Mobile.BodyWeight + m.TotalWeight > GetMaxWeight(m) + OverloadAllowance;
+ return Mobile.BodyWeight + m.TotalWeight > GetMaxWeight(m) + StonesOverweightAllowance;
+ }
+
+ private struct StepsTaken
+ {
+ 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 DateTime _nextCheck;
+
+ public ResetTimer() : base(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)) => _nextCheck = Core.Now;
+
+ public static void Initialize()
+ {
+ new ResetTimer().Start();
+ }
+
+ ~ResetTimer()
+ {
+ StaminaSystem.logger.Error($"{nameof(ResetTimer)} is no longer running!");
+ }
+
+ protected override void OnTick()
+ {
+ if (Core.Now < _nextCheck)
+ {
+ return;
+ }
+
+ using var queue = PooledRefQueue.Create();
+
+ ref StepsTaken stepsTaken = ref Unsafe.NullRef();
+ foreach (var m in _resetHash)
+ {
+ stepsTaken = ref GetMountStepsTaken(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());
+ }
+ }
+
+ _nextCheck = Core.Now + CheckDuration;
+ }
}
}
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs b/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs
index aa0edf8e83..5181d7de35 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs
@@ -1,343 +1,352 @@
using ModernUO.Serialization;
using System;
using Server.Items;
+using Server.Misc;
using Server.Multis;
using Server.Targeting;
-namespace Server.Mobiles
+namespace Server.Mobiles;
+
+[SerializationGenerator(0, false)]
+public abstract partial class BaseMount : BaseCreature, IMount
{
- [SerializationGenerator(0, false)]
- public abstract partial class BaseMount : BaseCreature, IMount
+ public BaseMount(
+ int bodyID,
+ int itemID,
+ AIType aiType,
+ FightMode fightMode = FightMode.Closest,
+ int rangePerception = 10,
+ int rangeFight = 1
+ ) : base(aiType, fightMode, rangePerception, rangeFight)
{
- public BaseMount(
- int bodyID, int itemID, AIType aiType, FightMode fightMode = FightMode.Closest,
- int rangePerception = 10, int rangeFight = 1
- ) : base(aiType, fightMode, rangePerception, rangeFight)
- {
- Body = bodyID;
+ Body = bodyID;
- InternalItem = new MountItem(this, itemID);
- }
+ InternalItem = new MountItem(this, itemID);
+ }
+
+ public virtual TimeSpan MountAbilityDelay => TimeSpan.Zero;
- public virtual TimeSpan MountAbilityDelay => TimeSpan.Zero;
+ [SerializableField(0)]
+ [SerializedCommandProperty(AccessLevel.GameMaster)]
+ public DateTime _nextMountAbility;
- [SerializedCommandProperty(AccessLevel.GameMaster)]
- [SerializableField(0)]
- public DateTime _nextMountAbility;
+ [SerializableField(2)]
+ [SerializedCommandProperty(AccessLevel.GameMaster)]
+ protected Item _internalItem;
- [SerializedCommandProperty(AccessLevel.GameMaster)]
- [SerializableField(2)]
- protected Item _internalItem;
+ public virtual bool AllowMaleRider => true;
+ public virtual bool AllowFemaleRider => true;
- public virtual bool AllowMaleRider => true;
- public virtual bool AllowFemaleRider => true;
+ // Stamina System - 1 step per 10 seconds and 3840 steps max = 9.667 hours
+ public virtual int StepsMax => 3840;
+ public virtual int StepsGainedPerIdleTime => 1;
+ public virtual TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(10);
- [Hue]
- [CommandProperty(AccessLevel.GameMaster)]
- public override int Hue
+ [Hue]
+ [CommandProperty(AccessLevel.GameMaster)]
+ public override int Hue
+ {
+ get => base.Hue;
+ set
{
- get => base.Hue;
- set
- {
- base.Hue = value;
+ base.Hue = value;
- if (InternalItem != null)
- {
- InternalItem.Hue = value;
- }
+ if (InternalItem != null)
+ {
+ InternalItem.Hue = value;
}
}
+ }
- [CommandProperty(AccessLevel.GameMaster)]
- public int ItemID
+ [CommandProperty(AccessLevel.GameMaster)]
+ public int ItemID
+ {
+ get => InternalItem?.ItemID ?? 0;
+ set
{
- get => InternalItem?.ItemID ?? 0;
- set
+ if (InternalItem != null)
{
- if (InternalItem != null)
- {
- InternalItem.ItemID = value;
- }
+ InternalItem.ItemID = value;
}
}
+ }
- [CommandProperty(AccessLevel.GameMaster)]
- [SerializableProperty(1)]
- public Mobile Rider
+ [CommandProperty(AccessLevel.GameMaster)]
+ [SerializableProperty(1)]
+ public Mobile Rider
+ {
+ get => _rider;
+ set
{
- get => _rider;
- set
+ if (_rider == value)
+ {
+ return;
+ }
+
+ if (value == null)
{
- if (_rider != value)
+ var loc = _rider.Location;
+ var map = _rider.Map;
+
+ if (map == null || map == Map.Internal)
{
- if (value == null)
- {
- var loc = _rider.Location;
- var map = _rider.Map;
-
- if (map == null || map == Map.Internal)
- {
- loc = _rider.LogoutLocation;
- map = _rider.LogoutMap;
- }
-
- Direction = _rider.Direction;
- Location = loc;
- Map = map;
-
- InternalItem?.Internalize();
- }
- else
- {
- if (_rider != null)
- {
- Dismount(_rider);
- }
-
- Dismount(value);
-
- if (InternalItem != null)
- {
- value.AddItem(InternalItem);
- }
-
- value.Direction = Direction;
-
- Internalize();
-
- if (value.Target is Bola.BolaTarget)
- {
- Target.Cancel(value);
- }
- }
-
- _rider = value;
- this.MarkDirty();
+ loc = _rider.LogoutLocation;
+ map = _rider.LogoutMap;
}
- }
- }
- public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill)
- {
- if (_rider == null)
+ Direction = _rider.Direction;
+ Location = loc;
+ Map = map;
+
+ InternalItem?.Internalize();
+ }
+ else
{
- return;
+ if (_rider != null)
+ {
+ Dismount(_rider);
+ }
+
+ Dismount(value);
+
+ if (InternalItem != null)
+ {
+ value.AddItem(InternalItem);
+ }
+
+ value.Direction = Direction;
+
+ Internalize();
+
+ if (value.Target is Bola.BolaTarget)
+ {
+ Target.Cancel(value);
+ }
}
- var attacker = from ?? _rider.FindMostRecentDamager(true);
+ _rider = value;
- if (!(attacker == this || attacker == _rider || willKill || Core.Now < NextMountAbility)
- && DoMountAbility(amount, from))
+ if (value == null)
{
- NextMountAbility = Core.Now + MountAbilityDelay;
+ StaminaSystem.OnDismount(this);
}
+
+ this.MarkDirty();
}
+ }
- public override bool OnBeforeDeath()
+ public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill)
+ {
+ if (_rider == null)
{
- Rider = null;
- return base.OnBeforeDeath();
+ return;
}
- public override void OnAfterDelete()
- {
- InternalItem?.Delete();
- InternalItem = null;
+ var attacker = from ?? _rider.FindMostRecentDamager(true);
- base.OnAfterDelete();
+ if (!(attacker == this || attacker == _rider || willKill || Core.Now < NextMountAbility)
+ && DoMountAbility(amount, from))
+ {
+ NextMountAbility = Core.Now + MountAbilityDelay;
}
+ }
- public override void OnDelete()
- {
- Rider = null;
+ public override bool OnBeforeDeath()
+ {
+ Rider = null;
+ return base.OnBeforeDeath();
+ }
- base.OnDelete();
- }
+ public override void OnAfterDelete()
+ {
+ InternalItem?.Delete();
+ InternalItem = null;
+
+ base.OnAfterDelete();
+ }
+
+ public override void OnDelete()
+ {
+ Rider = null;
- [AfterDeserialization(false)]
- private void AfterDeserialize()
+ base.OnDelete();
+ }
+
+ [AfterDeserialization(false)]
+ private void AfterDeserialize()
+ {
+ if (InternalItem == null)
{
- if (InternalItem == null)
- {
- Delete();
- }
+ Delete();
}
+ }
+
+ public virtual void OnDisallowedRider(Mobile m)
+ {
+ m.SendLocalizedMessage(1042317); // You may not ride at this time
+ }
- public virtual void OnDisallowedRider(Mobile m)
+ public override void OnDoubleClick(Mobile from)
+ {
+ if (IsDeadPet)
{
- m.SendMessage("You may not ride this creature.");
+ return;
}
- public override void OnDoubleClick(Mobile from)
+ if (from.IsBodyMod && !from.Body.IsHuman)
{
- if (IsDeadPet)
+ if (Core.AOS) // You cannot ride a mount in your current form.
{
- return;
+ PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1062061, from.NetState);
}
-
- if (from.IsBodyMod && !from.Body.IsHuman)
+ else
{
- if (Core.AOS) // You cannot ride a mount in your current form.
- {
- PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1062061, from.NetState);
- }
- else
- {
- from.SendLocalizedMessage(1061628); // You can't do that while polymorphed.
- }
-
- return;
+ from.SendLocalizedMessage(1061628); // You can't do that while polymorphed.
}
- if (!CheckMountAllowed(from))
- {
- return;
- }
+ return;
+ }
- if (from.Mounted)
- {
- from.SendLocalizedMessage(1005583); // Please dismount first.
- return;
- }
+ if (!CheckMountAllowed(from))
+ {
+ return;
+ }
- if (from.Female ? !AllowFemaleRider : !AllowMaleRider)
- {
- OnDisallowedRider(from);
- return;
- }
+ if (from.Mounted)
+ {
+ from.SendLocalizedMessage(1005583); // Please dismount first.
+ return;
+ }
- if (!DesignContext.Check(from))
- {
- return;
- }
+ if (from.Female ? !AllowFemaleRider : !AllowMaleRider)
+ {
+ OnDisallowedRider(from);
+ return;
+ }
- if (from.HasTrade)
- {
- from.SendLocalizedMessage(1042317, "", 0x41); // You may not ride at this time
- return;
- }
+ if (!DesignContext.Check(from))
+ {
+ return;
+ }
- if (from.InRange(this, 1))
- {
- var canAccess = from.AccessLevel >= AccessLevel.GameMaster
- || Controlled && ControlMaster == from
- || Summoned && SummonMaster == from;
+ if (from.HasTrade)
+ {
+ from.SendLocalizedMessage(1042317, "", 0x41); // You may not ride at this time
+ return;
+ }
- if (canAccess)
- {
- if (Poisoned)
- {
- PrivateOverheadMessage(
- MessageType.Regular,
- 0x3B2,
- 1049692,
- from.NetState
- ); // This mount is too ill to ride.
- }
- else
- {
- Rider = from;
- }
- }
- else if (!Controlled && !Summoned)
+ if (from.InRange(this, 1))
+ {
+ var canAccess = from.AccessLevel >= AccessLevel.GameMaster
+ || Controlled && ControlMaster == from
+ || Summoned && SummonMaster == from;
+
+ if (canAccess)
+ {
+ if (Poisoned)
{
- // That mount does not look broken! You would have to tame it to ride it.
- PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501263, from.NetState);
+ // This mount is too ill to ride.
+ PrivateOverheadMessage(MessageType.Regular,0x3B2,1049692,from.NetState);
}
else
{
- // This isn't your mount; it refuses to let you ride.
- PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501264, from.NetState);
+ Rider = from;
}
}
+ else if (!Controlled && !Summoned)
+ {
+ // That mount does not look broken! You would have to tame it to ride it.
+ PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501263, from.NetState);
+ }
else
{
- from.SendLocalizedMessage(500206); // That is too far away to ride.
+ // This isn't your mount; it refuses to let you ride.
+ PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501264, from.NetState);
}
}
-
- public static void Dismount(Mobile m)
+ else
{
- var mount = m.Mount;
-
- if (mount != null)
- {
- mount.Rider = null;
- }
+ from.SendLocalizedMessage(500206); // That is too far away to ride.
}
+ }
- // 1040024 You are still too dazed from being knocked off your mount to ride!
- // 1062910 You cannot mount while recovering from a bola throw.
- // 1070859 You cannot mount while recovering from a dismount special maneuver.
+ public static void Dismount(Mobile m)
+ {
+ var mount = m.Mount;
- public static bool CheckMountAllowed(Mobile mob)
+ if (mount != null)
{
- var result = true;
+ mount.Rider = null;
+ }
+ }
- if (mob is PlayerMobile mobile)
- {
- if (mobile.MountBlockReason != BlockMountType.None)
- {
- mobile.SendLocalizedMessage((int)mobile.MountBlockReason);
- result = false;
- }
+ public static bool CheckMountAllowed(Mobile mob)
+ {
+ var result = true;
- if (mobile.Race == Race.Gargoyle)
- {
- mobile.PrivateOverheadMessage(MessageType.Regular, mobile.SpeechHue, 1112281, mobile.NetState); // Gargoyles are unable to ride animals.
- result = false;
- }
+ if (mob is PlayerMobile mobile)
+ {
+ if (mobile.MountBlockReason != BlockMountType.None)
+ {
+ mobile.SendLocalizedMessage((int)mobile.MountBlockReason);
+ result = false;
}
- return result;
+ if (mobile.Race == Race.Gargoyle)
+ {
+ mobile.PrivateOverheadMessage(MessageType.Regular, mobile.SpeechHue, 1112281, mobile.NetState); // Gargoyles are unable to ride animals.
+ result = false;
+ }
}
- public virtual bool DoMountAbility(int damage, Mobile attacker) => false;
+ return result;
}
- [SerializationGenerator(0, false)]
- public partial class MountItem : Item, IMountItem
- {
- private BaseMount _mount;
+ public virtual bool DoMountAbility(int damage, Mobile attacker) => false;
+}
- public MountItem(BaseMount mount, int itemID) : base(itemID)
- {
- Layer = Layer.Mount;
- Movable = false;
+[SerializationGenerator(0, false)]
+public partial class MountItem : Item, IMountItem
+{
+ private BaseMount _mount;
- _mount = mount;
- }
+ public MountItem(BaseMount mount, int itemID) : base(itemID)
+ {
+ Layer = Layer.Mount;
+ Movable = false;
- public override double DefaultWeight => 0;
+ _mount = mount;
+ }
- [SerializableProperty(0, useField: nameof(_mount))]
- public IMount Mount => _mount;
+ public override double DefaultWeight => 0;
- public override void OnAfterDelete()
- {
- _mount?.Delete();
- _mount = null;
+ [SerializableProperty(0, useField: nameof(_mount))]
+ public IMount Mount => _mount;
- base.OnAfterDelete();
- }
+ public override void OnAfterDelete()
+ {
+ _mount?.Delete();
+ _mount = null;
- public override DeathMoveResult OnParentDeath(Mobile parent)
- {
- if (_mount != null)
- {
- _mount.Rider = null;
- }
+ base.OnAfterDelete();
+ }
- return DeathMoveResult.RemainEquipped;
+ public override DeathMoveResult OnParentDeath(Mobile parent)
+ {
+ if (_mount != null)
+ {
+ _mount.Rider = null;
}
- [AfterDeserialization(false)]
- private void AfterDeserialize()
+ return DeathMoveResult.RemainEquipped;
+ }
+
+ [AfterDeserialization(false)]
+ private void AfterDeserialize()
+ {
+ if (_mount == null)
{
- if (_mount == null)
- {
- Delete();
- }
+ Delete();
}
}
}
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs
index 84ba971bf8..ba9959d253 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs
@@ -50,6 +50,7 @@ public Beetle() : base( 0x317, 0x3EBC, AIType.AI_Melee)
AddItem(pack);
}
+ public override int StepsMax => 4480;
public override string CorpseName => "a giant beetle corpse";
public virtual double BoostedSpeed => 0.1;
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs
index 2ee8d6efcd..edc4b02eeb 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs
@@ -1,22 +1,31 @@
using ModernUO.Serialization;
using System;
+using System.Runtime.CompilerServices;
using Server.Engines.VeteranRewards;
using Server.Multis;
using Server.Spells;
namespace Server.Mobiles
{
- [SerializationGenerator(3, false)]
+ [SerializationGenerator(4, false)]
public partial class EtherealMount : Item, IMount, IMountItem, IRewardItem
{
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
public bool _isDonationItem;
-
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [SerializableFieldSaveFlag(0)]
+ public bool ShouldSerializeIsDonationItem() => _isDonationItem;
+
[SerializableField(1)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
public bool _isRewardItem;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [SerializableFieldSaveFlag(1)]
+ public bool ShouldSerializeIsRewardItem() => _isRewardItem;
+
[Constructible]
public EtherealMount(int itemID, int mountID) : base(itemID)
{
@@ -115,6 +124,29 @@ public Mobile Rider
}
}
+ [SerializableFieldSaveFlag(4)]
+ private bool ShouldSerializeRider() => _rider != null;
+
+ [CommandProperty(AccessLevel.GameMaster)]
+ [SerializableProperty(5)]
+ public int Steps
+ {
+ get => _steps;
+ set
+ {
+ _steps = Math.Clamp(value, 0, StepsMax);
+ this.MarkDirty();
+ }
+ }
+
+ [SerializableFieldSaveFlag(5)]
+ private bool ShouldSerializeSteps() => _steps != StepsMax;
+
+ public virtual int StepsMax => 3840; // Should be same as horse
+
+ public virtual int StepsGainedPerIdleTime => 1;
+ public virtual TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(10);
+
public void OnRiderDamaged(int amount, Mobile from, bool willKill)
{
}
@@ -215,10 +247,8 @@ private void Deserialize(IGenericReader reader, int version)
_mountedID = reader.ReadInt();
_regularID = reader.ReadInt();
_rider = reader.ReadEntity();
- if (_mountedID == 0x3EA2)
- {
- _mountedID = 0x3EAA;
- }
+
+ _steps = StepsMax;
}
[AfterDeserialization]
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/FrenziedOstard.cs b/Projects/UOContent/Mobiles/Animals/Mounts/FrenziedOstard.cs
index 3f9c66bc94..78a4fa0dcf 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/FrenziedOstard.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/FrenziedOstard.cs
@@ -42,6 +42,7 @@ public FrenziedOstard() : base(0xDA, 0x3EA4, AIType.AI_Melee)
MinTameSkill = 77.1;
}
+ public override int StepsMax => 5120;
public override string CorpseName => "an ostard corpse";
public override int Meat => 3;
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/HellSteed.cs b/Projects/UOContent/Mobiles/Animals/Mounts/HellSteed.cs
index 3a2f12a6a2..a67d6e3cc9 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/HellSteed.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/HellSteed.cs
@@ -13,6 +13,7 @@ public HellSteed() : base(793, 0x3EBB, AIType.AI_Animal, FightMode.Aggressor)
SetStats(this);
}
+ public override int StepsMax => 5120;
public override string CorpseName => "a hellsteed corpse";
public override Poison PoisonImmune => Poison.Lethal;
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Hiryu.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Hiryu.cs
index a443a4bb7e..f9c60eb464 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/Hiryu.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/Hiryu.cs
@@ -54,6 +54,7 @@ public Hiryu() : base(243, 0x3E94, AIType.AI_Melee)
}
}
+ public override int StepsMax => 4480;
public override string CorpseName => "a hiryu corpse";
public override double WeaponAbilityChance => 0.07; /* 1 in 15 chance of using per landed hit */
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Kirin.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Kirin.cs
index 69b705c865..a537cb8004 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/Kirin.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/Kirin.cs
@@ -48,6 +48,7 @@ public Kirin() : base(132, 0x3EAD, AIType.AI_Mage, FightMode.Evil)
MinTameSkill = 95.1;
}
+ public override int StepsMax => 4480;
public override string CorpseName => "a ki-rin corpse";
public override bool AllowFemaleRider => false;
public override bool AllowFemaleTamer => false;
@@ -75,12 +76,12 @@ public override bool DoMountAbility(int damage, Mobile attacker)
return false;
}
- // Range and map checked here instead of other base fuction because of abiliites that don't need to check this
+ // Range and map checked here instead of other base function because of abilities that don't need to check this
if (Rider.Hits - damage < 30 && Rider.Map == attacker.Map && Rider.InRange(attacker, 18))
{
attacker.BoltEffect(0);
// 35~100 damage, unresistable, by the Ki-rin.
- // Don't inform mount about this damage, Still unsure wether or not it's flagged as the mount doing damage or the player.
+ // Don't inform mount about this damage, Still unsure whether or not it's flagged as the mount doing damage or the player.
// If changed to player, without the extra bool it'd be an infinite loop
attacker.Damage(Utility.RandomMinMax(35, 100), this, false);
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/LesserHiryu.cs b/Projects/UOContent/Mobiles/Animals/Mounts/LesserHiryu.cs
index 8791836c7c..8bc8b90427 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/LesserHiryu.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/LesserHiryu.cs
@@ -49,6 +49,7 @@ public LesserHiryu() : base(243, 0x3E94, AIType.AI_Melee)
}
}
+ public override int StepsMax => 4480;
public override string CorpseName => "a hiryu corpse";
public override double WeaponAbilityChance => 0.07; /* 1 in 15 chance of using; 1 in 5 chance of success */
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Nightmare.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Nightmare.cs
index 9fb71709c9..479d44f112 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/Nightmare.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/Nightmare.cs
@@ -89,6 +89,7 @@ public Nightmare() : base(0x74, 0x3EA7, AIType.AI_Mage)
PackItem(new SulfurousAsh(Utility.RandomMinMax(3, 5)));
}
+ public override int StepsMax => 6400;
public override string CorpseName => "a nightmare corpse";
public override int Meat => 5;
public override int Hides => 10;
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/RidableLlama.cs b/Projects/UOContent/Mobiles/Animals/Mounts/RidableLlama.cs
index aa33972fdb..8fbbd9f4d0 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/RidableLlama.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/RidableLlama.cs
@@ -42,6 +42,7 @@ public RidableLlama() : base(0xDC, 0x3EA6, AIType.AI_Animal, FightMode.Aggressor
MinTameSkill = 29.1;
}
+ public override int StepsMax => 2560;
public override string CorpseName => "a llama corpse";
public override int Meat => 1;
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Ridgeback.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Ridgeback.cs
index 887005fdba..c88a336c2e 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/Ridgeback.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/Ridgeback.cs
@@ -41,6 +41,7 @@ public Ridgeback() : base(187, 0x3EBA, AIType.AI_Animal, FightMode.Aggressor)
MinTameSkill = 83.1;
}
+ public override int StepsMax => 4480;
public override string CorpseName => "a ridgeback corpse";
public override int Meat => 1;
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/SavageRidgeback.cs b/Projects/UOContent/Mobiles/Animals/Mounts/SavageRidgeback.cs
index 9fae286571..0e15f1a298 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/SavageRidgeback.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/SavageRidgeback.cs
@@ -41,6 +41,7 @@ public SavageRidgeback() : base(188, 0x3EB8, AIType.AI_Melee, FightMode.Aggresso
MinTameSkill = 83.1;
}
+ public override int StepsMax => 4480;
public override string CorpseName => "a savage ridgeback corpse";
public override int Meat => 1;
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/ScaledSwampDragon.cs b/Projects/UOContent/Mobiles/Animals/Mounts/ScaledSwampDragon.cs
index 3a2edebbd8..8bb2f221ba 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/ScaledSwampDragon.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/ScaledSwampDragon.cs
@@ -40,6 +40,7 @@ public ScaledSwampDragon() : base(0x31F, 0x3EBE, AIType.AI_Melee, FightMode.Aggr
MinTameSkill = 93.9;
}
+ public override int StepsMax => 4480;
public override string CorpseName => "a swamp dragon corpse";
public override bool AutoDispel => !Controlled;
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/SilverSteed.cs b/Projects/UOContent/Mobiles/Animals/Mounts/SilverSteed.cs
index 5d0e70f057..148dbe3cf7 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/SilverSteed.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/SilverSteed.cs
@@ -20,6 +20,7 @@ public SilverSteed() : base(0x75, 0x3EA8, AIType.AI_Animal, FightMode.Aggressor)
MinTameSkill = 103.1;
}
+ public override int StepsMax => 4480;
public override string CorpseName => "a silver steed corpse";
}
}
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/SkeletalMount.cs b/Projects/UOContent/Mobiles/Animals/Mounts/SkeletalMount.cs
index 3ef433a5f4..edc313bd09 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/SkeletalMount.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/SkeletalMount.cs
@@ -32,6 +32,7 @@ public SkeletalMount() : base(793, 0x3EBB, AIType.AI_Animal, FightMode.Aggressor
Karma = 0;
}
+ public override int StepsMax => 4480;
public override string CorpseName => "an undead horse corpse";
public override string DefaultName => "a skeletal steed";
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/SwampDragon.cs b/Projects/UOContent/Mobiles/Animals/Mounts/SwampDragon.cs
index ff3631345f..69bd9ce565 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/SwampDragon.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/SwampDragon.cs
@@ -110,6 +110,7 @@ public CraftResource BardingResource
[CommandProperty(AccessLevel.GameMaster)]
public int BardingMaxHP => _bardingExceptional ? 2500 : 1000;
+ public override int StepsMax => 4480;
public override bool ReacquireOnMovement => true;
public override bool AutoDispel => !Controlled;
public override FoodType FavoriteFood => FoodType.Meat;
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Unicorn.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Unicorn.cs
index bd8f64ba66..2a33b99bf6 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/Unicorn.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/Unicorn.cs
@@ -46,6 +46,7 @@ public Unicorn() : base(0x7A, 0x3EB4, AIType.AI_Mage, FightMode.Evil)
MinTameSkill = 95.1;
}
+ public override int StepsMax => 4480;
public override string CorpseName => "a unicorn corpse";
public override bool AllowMaleRider => false;
public override bool AllowMaleTamer => false;
@@ -89,12 +90,8 @@ public override bool DoMountAbility(int damage, Mobile attacker)
// TODO: Confirm if mount is the one flagged for curing it or the rider is
if (Rider.CurePoison(this))
{
- Rider.LocalOverheadMessage(
- MessageType.Regular,
- 0x3B2,
- true,
- "Your mount senses you are in danger and aids you with magic."
- );
+ // Your mount senses you are in danger and aids you with magic.
+ Rider.LocalOverheadMessage(MessageType.Regular,0x3B2,1080039);
Rider.FixedParticles(0x373A, 10, 15, 5012, EffectLayer.Waist);
Rider.PlaySound(0x1E0); // Cure spell effect.
Rider.PlaySound(0xA9); // Unicorn's whinny.
diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/War Horses/BaseWarHorse.cs b/Projects/UOContent/Mobiles/Animals/Mounts/War Horses/BaseWarHorse.cs
index b78273ed57..3f6a04a4d9 100644
--- a/Projects/UOContent/Mobiles/Animals/Mounts/War Horses/BaseWarHorse.cs
+++ b/Projects/UOContent/Mobiles/Animals/Mounts/War Horses/BaseWarHorse.cs
@@ -52,6 +52,7 @@ public BaseWarHorse(
MinTameSkill = 29.1;
}
+ public override int StepsMax => 6400;
public override string CorpseName => "a war horse corpse";
public override FoodType FavoriteFood => FoodType.FruitsAndVegies | FoodType.GrainsAndHay;
diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs
index ea2ed659fd..aa3b198aea 100644
--- a/Projects/UOContent/Mobiles/BaseCreature.cs
+++ b/Projects/UOContent/Mobiles/BaseCreature.cs
@@ -2369,6 +2369,8 @@ public override void OnAfterDelete()
UnsummonTimer.StopTimer(this);
+ StaminaSystem.RemoveEntry(this as IHasSteps);
+
base.OnAfterDelete();
}
@@ -4177,15 +4179,13 @@ public virtual bool CheckFeed(Mobile from, Item dropped)
{
if (!IsDeadPet && Controlled && (ControlMaster == from || IsPetFriend(from)))
{
- var f = dropped;
-
- if (CheckFoodPreference(f))
+ if (CheckFoodPreference(dropped))
{
- var amount = f.Amount;
+ var amount = dropped.Amount;
if (amount > 0)
{
- int stamGain = f switch
+ int stamGain = dropped switch
{
Gold => amount - 50,
_ => amount * 15 - 50
@@ -4194,6 +4194,8 @@ public virtual bool CheckFeed(Mobile from, Item dropped)
if (stamGain > 0)
{
Stam += stamGain;
+ // 64 food = 3,640 steps
+ StaminaSystem.RegenSteps(this as IHasSteps, stamGain * 4);
}
if (Core.SE)
diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs
index 972949c894..0df10c313c 100644
--- a/Projects/UOContent/Mobiles/PlayerMobile.cs
+++ b/Projects/UOContent/Mobiles/PlayerMobile.cs
@@ -90,12 +90,12 @@ public enum SolenFriendship
public enum BlockMountType
{
None = -1,
- Dazed = 1040024,
- BolaRecovery = 1062910,
- DismountRecovery = 1070859
+ Dazed = 1040024, // You are still too dazed from being knocked off your mount to ride!
+ BolaRecovery = 1062910, // You cannot mount while recovering from a bola throw.
+ DismountRecovery = 1070859 // You cannot mount while recovering from a dismount special maneuver.
}
- public class PlayerMobile : Mobile, IHonorTarget
+ public class PlayerMobile : Mobile, IHonorTarget, IHasSteps
{
private static bool m_NoRecursion;
@@ -214,6 +214,12 @@ public PlayerMobile(Serial s) : base(s)
VisibilityList = new List();
}
+ public int StepsMax => 16;
+
+ public int StepsGainedPerIdleTime => 1;
+
+ public TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(1);
+
[CommandProperty(AccessLevel.GameMaster)]
public DateTime AnkhNextUse { get; set; }
@@ -399,8 +405,6 @@ public RankDefinition GuildRank
[CommandProperty(AccessLevel.GameMaster)]
public int Profession { get; set; }
- public int StepsTaken { get; set; }
-
[CommandProperty(AccessLevel.GameMaster)]
public bool IsStealthing // IsStealthing should be moved to Server.Mobiles
{
diff --git a/Projects/UOContent/Mobiles/Special/ChaosGuard.cs b/Projects/UOContent/Mobiles/Special/ChaosGuard.cs
index 02efbc5319..8f91e9022d 100644
--- a/Projects/UOContent/Mobiles/Special/ChaosGuard.cs
+++ b/Projects/UOContent/Mobiles/Special/ChaosGuard.cs
@@ -4,7 +4,7 @@
namespace Server.Mobiles;
-[SerializationGenerator(0)]
+[SerializationGenerator(0, false)]
public partial class ChaosGuard : BaseShieldGuard
{
[Constructible]
diff --git a/Projects/UOContent/Mobiles/Special/Neira.cs b/Projects/UOContent/Mobiles/Special/Neira.cs
index cc8fa055c4..80d2a947c5 100644
--- a/Projects/UOContent/Mobiles/Special/Neira.cs
+++ b/Projects/UOContent/Mobiles/Special/Neira.cs
@@ -201,65 +201,6 @@ private void AfterDeserialization()
CheckSpeedBoost();
}
- private class VirtualMount : IMount
- {
- private readonly VirtualMountItem _item;
-
- public VirtualMount(VirtualMountItem item) => _item = item;
-
- Mobile IMount.Rider
- {
- get => _item.Rider;
- set { }
- }
-
- public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill)
- {
- }
- }
-
- [SerializationGenerator(0)]
- private partial class VirtualMountItem : Item, IMountItem
- {
- private VirtualMount _mount;
-
- [SerializableField(0, setter: "private")]
- private Mobile _rider;
-
- public VirtualMountItem(Mobile mob) : base(0x3EBB)
- {
- Layer = Layer.Mount;
-
- Movable = false;
-
- _rider = mob;
- _mount = new VirtualMount(this);
- }
-
- public IMount Mount => _mount;
-
- [AfterDeserialization(false)]
- private void AfterDeserialization()
- {
- if (_rider?.Deleted != false)
- {
- Delete();
- }
- else
- {
- _mount = new VirtualMount(this);
- }
- }
-
- public override DeathMoveResult OnParentDeath(Mobile parent)
- {
- _mount = null;
- Delete();
-
- return DeathMoveResult.RemainEquipped;
- }
- }
-
private class DelayTimer : Timer
{
private readonly Mobile m_Mobile;
diff --git a/Projects/UOContent/Mobiles/VirtualMountItem.cs b/Projects/UOContent/Mobiles/VirtualMountItem.cs
new file mode 100644
index 0000000000..bab4d98158
--- /dev/null
+++ b/Projects/UOContent/Mobiles/VirtualMountItem.cs
@@ -0,0 +1,48 @@
+using System;
+using ModernUO.Serialization;
+
+namespace Server.Mobiles;
+
+[SerializationGenerator(0, false)]
+public partial class VirtualMountItem : Item, IMountItem, IMount
+{
+ public VirtualMountItem(Mobile mob) : base(0x3EA0)
+ {
+ Layer = Layer.Mount;
+ _rider = mob;
+ }
+
+ public IMount Mount => this;
+
+ [SerializableProperty(0)]
+ public Mobile Rider
+ {
+ get => _rider;
+ set { }
+ }
+
+ [AfterDeserialization(false)]
+ private void AfterDeserialize()
+ {
+ if (_rider == null)
+ {
+ Delete();
+ }
+ }
+
+ public int Steps { get; set; }
+
+ public int StepsMax => 400;
+ public int StepsGainedPerIdleTime => 1;
+ public TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(10);
+
+ public void OnRiderDamaged(int amount, Mobile from, bool willKill)
+ {
+ }
+
+ public override DeathMoveResult OnParentDeath(Mobile parent)
+ {
+ Delete();
+ return DeathMoveResult.RemainEquipped;
+ }
+}
diff --git a/version.json b/version.json
index 0ab5664cca..0c162543dd 100644
--- a/version.json
+++ b/version.json
@@ -1,4 +1,4 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
- "version": "0.9.8"
+ "version": "0.9.9"
}