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" }