From 0f2870e94ade59a056417a6db15a525ba00721e8 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 16 Sep 2023 17:52:54 -0700 Subject: [PATCH] feat: Adds Stamina System to overhaul overweight. (#1465) ## Overhaul to Stamina (Overweight) System ### Added configurations ```json { "settings": { "stamina.enableMountStamina": "True", "stamina.cannotMoveWhenFatigued": "True", "stamina.stonesPerOverweightLoss": "25", "stamina.stonesOverweightAllowance": "4", "stamina.baseOverweightLoss": "5", "stamina.additionalLossWhenBelow": "0.1", "stamina.mountLastMoveStepsReset": "01:00:00:00", "stamina.enableMountStamina": "True", "stamina.useMountStaminaOnlyWhenOverloaded": "False", }, } ``` - `stamina.cannotMoveWhenFatigued` - By default, Pre-AOS expansions will outright block a player if they are fatigued. A player is fatigued when they run out of stamina by any mechanism. - `stamina.stonesPerOverweightLoss` - The amount of stamina lost for every X stones above overweight. Example, if a person is overweight 28 stones overweight, then there there is 1 additional stamina. 28 / 25 = 1 (no decimals, no rounding) - `stamina.stonesOverweightAllowance` - The number of stones allowed before overweight penalties take affect. This _does not_ substract from the overweight stones calculation. - `stamina.baseOverweightLoss` - The base amount of stamina loss for being overweight. While running, the final amount is multiplied by 2. If mount stamina is turned off, then the final amount is divided by 3 while mounted. ### Mount stamina Mounts have a new property `StepsMax` to determine the maximum steps they can take before being fatigued. To regain steps, the player _must stay on the mount and not move_ (per OSI). The gain rates are configurable. If a mount is dismounted, or the player logs off, the mount is considered inactive and mounts regain all of their steps after _24 hours_. ### Changes to player stamina Players now have a proper inactivity time reset for their steps. If a player is idle for 16 seconds (including logging off, or the server being offline), then the steps counter is rest. ### Developer Notes - `StepsTaken` - This field has been removed. It was not serialized and could not be used reliably. If a developer was using it, then the recommendation is to build a mechanism to track total steps another way. - `IHasStamina` - This new interface was added and currently `IMount` and `PlayerMobile` are valid types. Important: Entities that are sent to the StaminaSystem for tracking (mounts, players, or something else), must be an `ISerializable` to be serialized properly. If they are not, then the serialization system will record a null, and nothing will be deserialized upon world load. No errors will be given. --- Projects/Server/Mobiles/IHasSteps.cs | 12 + Projects/Server/Mobiles/IMount.cs | 4 +- Projects/Server/Mobiles/Mobile.cs | 122 +++-- .../Ethics/Evil/Mobiles/UnholySteed.cs | 1 + .../Engines/Ethics/Hero/Mobiles/HolySteed.cs | 1 + .../Factions/Mobiles/FactionWarHorse.cs | 1 + .../Mobiles/Guards/BaseFactionGuard.cs | 57 -- .../Mobiles/Guards/Types/FactionDragoon.cs | 1 + .../Mobiles/Guards/Types/FactionPaladin.cs | 1 + .../PlayerMurderSystem.cs | 14 +- .../UOContent/Engines/Virtues/VirtueSystem.cs | 17 +- ...n => Server.Mobiles.EtherealMount.v4.json} | 14 +- .../Server.Mobiles.VirtualMountItem.v0.json | 11 + Projects/UOContent/Misc/StaminaSystem.cs | 444 +++++++++++++++- .../Mobiles/Animals/Mounts/BaseMount.cs | 495 +++++++++--------- .../Mobiles/Animals/Mounts/Beetle.cs | 1 + .../Mobiles/Animals/Mounts/Ethereals.cs | 42 +- .../Mobiles/Animals/Mounts/FrenziedOstard.cs | 1 + .../Mobiles/Animals/Mounts/HellSteed.cs | 1 + .../UOContent/Mobiles/Animals/Mounts/Hiryu.cs | 1 + .../UOContent/Mobiles/Animals/Mounts/Kirin.cs | 5 +- .../Mobiles/Animals/Mounts/LesserHiryu.cs | 1 + .../Mobiles/Animals/Mounts/Nightmare.cs | 1 + .../Mobiles/Animals/Mounts/RidableLlama.cs | 1 + .../Mobiles/Animals/Mounts/Ridgeback.cs | 1 + .../Mobiles/Animals/Mounts/SavageRidgeback.cs | 1 + .../Animals/Mounts/ScaledSwampDragon.cs | 1 + .../Mobiles/Animals/Mounts/SilverSteed.cs | 1 + .../Mobiles/Animals/Mounts/SkeletalMount.cs | 1 + .../Mobiles/Animals/Mounts/SwampDragon.cs | 1 + .../Mobiles/Animals/Mounts/Unicorn.cs | 9 +- .../Animals/Mounts/War Horses/BaseWarHorse.cs | 1 + Projects/UOContent/Mobiles/BaseCreature.cs | 12 +- Projects/UOContent/Mobiles/PlayerMobile.cs | 16 +- .../UOContent/Mobiles/Special/ChaosGuard.cs | 2 +- Projects/UOContent/Mobiles/Special/Neira.cs | 59 --- .../UOContent/Mobiles/VirtualMountItem.cs | 48 ++ version.json | 2 +- 38 files changed, 947 insertions(+), 457 deletions(-) create mode 100644 Projects/Server/Mobiles/IHasSteps.cs rename Projects/UOContent/Migrations/{Server.Mobiles.EtherealMount.v3.json => Server.Mobiles.EtherealMount.v4.json} (74%) create mode 100644 Projects/UOContent/Migrations/Server.Mobiles.VirtualMountItem.v0.json create mode 100644 Projects/UOContent/Mobiles/VirtualMountItem.cs 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" }