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 | 567 +++++++++--------- .../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, 983 insertions(+), 493 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 000000000..15bd9a959 --- /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 78c642438..9ce95e692 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 e93aac4be..37f3f4c87 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -4081,33 +4081,49 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro 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 partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro 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 3388af223..5e06a2422 100644 --- a/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholySteed.cs +++ b/Projects/UOContent/Engines/Ethics/Evil/Mobiles/UnholySteed.cs @@ -44,6 +44,7 @@ namespace Server.Mobiles { } + 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 1999d3f70..a5fb4c602 100644 --- a/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolySteed.cs +++ b/Projects/UOContent/Engines/Ethics/Hero/Mobiles/HolySteed.cs @@ -43,6 +43,7 @@ namespace Server.Mobiles { } + 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 896c79546..f6a19ccca 100644 --- a/Projects/UOContent/Engines/Factions/Mobiles/FactionWarHorse.cs +++ b/Projects/UOContent/Engines/Factions/Mobiles/FactionWarHorse.cs @@ -49,6 +49,7 @@ namespace Server.Factions { } + 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 598ddb773..9ce69ddee 100644 --- a/Projects/UOContent/Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs +++ b/Projects/UOContent/Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs @@ -468,61 +468,4 @@ namespace Server.Factions 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 3811c675f..f78faf3c7 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 b74f74b7e..9e0f96bc5 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 0f6dc9f58..d997a11f2 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 class PlayerMurderSystem EventSink.Disconnected += OnDisconnected; EventSink.Login += OnLogin; EventSink.PlayerDeleted += OnPlayerDeleted; - - _murdererTimer.Start(); } private static void OnPlayerDeleted(Mobile m) @@ -182,6 +178,11 @@ public static class PlayerMurderSystem { } + public static void Initialize() + { + new MurdererTimer().Start(); + } + protected override void OnTick() { if (_contextTerms.Count == 0) @@ -208,5 +209,10 @@ public static class PlayerMurderSystem } } } + + ~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 ebc7d57a3..12bde4fb3 100644 --- a/Projects/UOContent/Engines/Virtues/VirtueSystem.cs +++ b/Projects/UOContent/Engines/Virtues/VirtueSystem.cs @@ -2,6 +2,7 @@ using System; 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 class VirtueSystem FixVirtue(m, values); } } - - _virtueTimer.Start(); } private static void Serialize(IGenericWriter writer) @@ -346,6 +345,11 @@ public static class VirtueSystem { } + public static void Initialize() + { + new VirtueTimer().Start(); + } + protected override void OnTick() { if (_playerVirtues.Count == 0) @@ -371,5 +375,10 @@ public static class VirtueSystem _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 d5f63f9ab..b3fb60bd1 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 000000000..2b50a2479 --- /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 e033c6a7d..fec730a45 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 class StaminaSystem { 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; + } + } + + 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}"); + } } } - DeathStrike.AddStep(from); + 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 class StaminaSystem 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 aa0edf8e8..5181d7de3 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; + + InternalItem = new MountItem(this, itemID); + } + + public virtual TimeSpan MountAbilityDelay => TimeSpan.Zero; + + [SerializableField(0)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + public DateTime _nextMountAbility; + + [SerializableField(2)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + protected Item _internalItem; + + 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 + { + get => base.Hue; + set { - Body = bodyID; + base.Hue = value; - InternalItem = new MountItem(this, itemID); - } - - public virtual TimeSpan MountAbilityDelay => TimeSpan.Zero; - - [SerializedCommandProperty(AccessLevel.GameMaster)] - [SerializableField(0)] - public DateTime _nextMountAbility; - - [SerializedCommandProperty(AccessLevel.GameMaster)] - [SerializableField(2)] - protected Item _internalItem; - - public virtual bool AllowMaleRider => true; - public virtual bool AllowFemaleRider => true; - - [Hue] - [CommandProperty(AccessLevel.GameMaster)] - public override int Hue - { - get => base.Hue; - set + if (InternalItem != null) { - base.Hue = value; - - if (InternalItem != null) - { - InternalItem.Hue = value; - } + 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) - { - 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(); - } - } - } - - public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill) - { - if (_rider == null) + if (_rider == value) { return; } - var attacker = from ?? _rider.FindMostRecentDamager(true); - - if (!(attacker == this || attacker == _rider || willKill || Core.Now < NextMountAbility) - && DoMountAbility(amount, from)) + if (value == null) { - NextMountAbility = Core.Now + MountAbilityDelay; - } - } + var loc = _rider.Location; + var map = _rider.Map; - public override bool OnBeforeDeath() - { - Rider = null; - return base.OnBeforeDeath(); - } - - public override void OnAfterDelete() - { - InternalItem?.Delete(); - InternalItem = null; - - base.OnAfterDelete(); - } - - public override void OnDelete() - { - Rider = null; - - base.OnDelete(); - } - - [AfterDeserialization(false)] - private void AfterDeserialize() - { - if (InternalItem == null) - { - Delete(); - } - } - - public virtual void OnDisallowedRider(Mobile m) - { - m.SendMessage("You may not ride this creature."); - } - - public override void OnDoubleClick(Mobile from) - { - if (IsDeadPet) - { - return; - } - - if (from.IsBodyMod && !from.Body.IsHuman) - { - if (Core.AOS) // You cannot ride a mount in your current form. + if (map == null || map == Map.Internal) { - PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1062061, from.NetState); - } - else - { - from.SendLocalizedMessage(1061628); // You can't do that while polymorphed. + loc = _rider.LogoutLocation; + map = _rider.LogoutMap; } - return; - } + Direction = _rider.Direction; + Location = loc; + Map = map; - if (!CheckMountAllowed(from)) - { - return; - } - - if (from.Mounted) - { - from.SendLocalizedMessage(1005583); // Please dismount first. - return; - } - - if (from.Female ? !AllowFemaleRider : !AllowMaleRider) - { - OnDisallowedRider(from); - return; - } - - if (!DesignContext.Check(from)) - { - return; - } - - if (from.HasTrade) - { - from.SendLocalizedMessage(1042317, "", 0x41); // You may not ride at this time - return; - } - - if (from.InRange(this, 1)) - { - var canAccess = from.AccessLevel >= AccessLevel.GameMaster - || Controlled && ControlMaster == from - || Summoned && SummonMaster == from; - - 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) - { - // That mount does not look broken! You would have to tame it to ride it. - PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501263, from.NetState); - } - else - { - // This isn't your mount; it refuses to let you ride. - PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501264, from.NetState); - } + InternalItem?.Internalize(); } else { - from.SendLocalizedMessage(500206); // That is too far away to ride. - } - } - - public static void Dismount(Mobile m) - { - var mount = m.Mount; - - if (mount != null) - { - mount.Rider = null; - } - } - - // 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 bool CheckMountAllowed(Mobile mob) - { - var result = true; - - if (mob is PlayerMobile mobile) - { - if (mobile.MountBlockReason != BlockMountType.None) + if (_rider != null) { - mobile.SendLocalizedMessage((int)mobile.MountBlockReason); - result = false; + Dismount(_rider); } - if (mobile.Race == Race.Gargoyle) + Dismount(value); + + if (InternalItem != null) { - mobile.PrivateOverheadMessage(MessageType.Regular, mobile.SpeechHue, 1112281, mobile.NetState); // Gargoyles are unable to ride animals. - result = false; + value.AddItem(InternalItem); + } + + value.Direction = Direction; + + Internalize(); + + if (value.Target is Bola.BolaTarget) + { + Target.Cancel(value); } } - return result; - } + _rider = value; - public virtual bool DoMountAbility(int damage, Mobile attacker) => false; + if (value == null) + { + StaminaSystem.OnDismount(this); + } + + this.MarkDirty(); + } } - [SerializationGenerator(0, false)] - public partial class MountItem : Item, IMountItem + public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill) { - private BaseMount _mount; - - public MountItem(BaseMount mount, int itemID) : base(itemID) + if (_rider == null) { - Layer = Layer.Mount; - Movable = false; - - _mount = mount; + return; } - public override double DefaultWeight => 0; + var attacker = from ?? _rider.FindMostRecentDamager(true); - [SerializableProperty(0, useField: nameof(_mount))] - public IMount Mount => _mount; - - public override void OnAfterDelete() + if (!(attacker == this || attacker == _rider || willKill || Core.Now < NextMountAbility) + && DoMountAbility(amount, from)) { - _mount?.Delete(); - _mount = null; + NextMountAbility = Core.Now + MountAbilityDelay; + } + } - base.OnAfterDelete(); + public override bool OnBeforeDeath() + { + Rider = null; + return base.OnBeforeDeath(); + } + + public override void OnAfterDelete() + { + InternalItem?.Delete(); + InternalItem = null; + + base.OnAfterDelete(); + } + + public override void OnDelete() + { + Rider = null; + + base.OnDelete(); + } + + [AfterDeserialization(false)] + private void AfterDeserialize() + { + if (InternalItem == null) + { + Delete(); + } + } + + public virtual void OnDisallowedRider(Mobile m) + { + m.SendLocalizedMessage(1042317); // You may not ride at this time + } + + public override void OnDoubleClick(Mobile from) + { + if (IsDeadPet) + { + return; } - public override DeathMoveResult OnParentDeath(Mobile parent) + if (from.IsBodyMod && !from.Body.IsHuman) { - if (_mount != null) + if (Core.AOS) // You cannot ride a mount in your current form. { - _mount.Rider = null; + PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1062061, from.NetState); + } + else + { + from.SendLocalizedMessage(1061628); // You can't do that while polymorphed. } - return DeathMoveResult.RemainEquipped; + return; } - [AfterDeserialization(false)] - private void AfterDeserialize() + if (!CheckMountAllowed(from)) { - if (_mount == null) + return; + } + + if (from.Mounted) + { + from.SendLocalizedMessage(1005583); // Please dismount first. + return; + } + + if (from.Female ? !AllowFemaleRider : !AllowMaleRider) + { + OnDisallowedRider(from); + return; + } + + if (!DesignContext.Check(from)) + { + return; + } + + if (from.HasTrade) + { + from.SendLocalizedMessage(1042317, "", 0x41); // You may not ride at this time + return; + } + + if (from.InRange(this, 1)) + { + var canAccess = from.AccessLevel >= AccessLevel.GameMaster + || Controlled && ControlMaster == from + || Summoned && SummonMaster == from; + + if (canAccess) { - Delete(); + if (Poisoned) + { + // This mount is too ill to ride. + PrivateOverheadMessage(MessageType.Regular,0x3B2,1049692,from.NetState); + } + else + { + 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 + { + // This isn't your mount; it refuses to let you ride. + PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501264, from.NetState); + } + } + else + { + from.SendLocalizedMessage(500206); // That is too far away to ride. + } + } + + public static void Dismount(Mobile m) + { + var mount = m.Mount; + + if (mount != null) + { + mount.Rider = null; + } + } + + public static bool CheckMountAllowed(Mobile mob) + { + var result = true; + + if (mob is PlayerMobile mobile) + { + if (mobile.MountBlockReason != BlockMountType.None) + { + mobile.SendLocalizedMessage((int)mobile.MountBlockReason); + result = false; + } + + if (mobile.Race == Race.Gargoyle) + { + mobile.PrivateOverheadMessage(MessageType.Regular, mobile.SpeechHue, 1112281, mobile.NetState); // Gargoyles are unable to ride animals. + result = false; + } + } + + return result; + } + + public virtual bool DoMountAbility(int damage, Mobile attacker) => false; +} + +[SerializationGenerator(0, false)] +public partial class MountItem : Item, IMountItem +{ + private BaseMount _mount; + + public MountItem(BaseMount mount, int itemID) : base(itemID) + { + Layer = Layer.Mount; + Movable = false; + + _mount = mount; + } + + public override double DefaultWeight => 0; + + [SerializableProperty(0, useField: nameof(_mount))] + public IMount Mount => _mount; + + public override void OnAfterDelete() + { + _mount?.Delete(); + _mount = null; + + base.OnAfterDelete(); + } + + public override DeathMoveResult OnParentDeath(Mobile parent) + { + if (_mount != null) + { + _mount.Rider = null; + } + + return DeathMoveResult.RemainEquipped; + } + + [AfterDeserialization(false)] + private void AfterDeserialize() + { + if (_mount == null) + { + Delete(); } } } diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs index 84ba971bf..ba9959d25 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs @@ -50,6 +50,7 @@ namespace Server.Mobiles 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 2ee8d6efc..edc4b02ee 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 @@ namespace Server.Mobiles } } + [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 @@ namespace Server.Mobiles _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 3f9c66bc9..78a4fa0dc 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/FrenziedOstard.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/FrenziedOstard.cs @@ -42,6 +42,7 @@ namespace Server.Mobiles 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 3a2f12a6a..a67d6e3cc 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/HellSteed.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/HellSteed.cs @@ -13,6 +13,7 @@ namespace Server.Mobiles 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 a443a4bb7..f9c60eb46 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/Hiryu.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/Hiryu.cs @@ -54,6 +54,7 @@ namespace Server.Mobiles } } + 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 69b705c86..a537cb800 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/Kirin.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/Kirin.cs @@ -48,6 +48,7 @@ namespace Server.Mobiles 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 @@ namespace Server.Mobiles 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 8791836c7..8bc8b9042 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/LesserHiryu.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/LesserHiryu.cs @@ -49,6 +49,7 @@ namespace Server.Mobiles } } + 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 9fb71709c..479d44f11 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/Nightmare.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/Nightmare.cs @@ -89,6 +89,7 @@ namespace Server.Mobiles 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 aa33972fd..8fbbd9f4d 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/RidableLlama.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/RidableLlama.cs @@ -42,6 +42,7 @@ namespace Server.Mobiles 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 887005fdb..c88a336c2 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/Ridgeback.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/Ridgeback.cs @@ -41,6 +41,7 @@ namespace Server.Mobiles 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 9fae28657..0e15f1a29 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/SavageRidgeback.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/SavageRidgeback.cs @@ -41,6 +41,7 @@ namespace Server.Mobiles 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 3a2edebbd..8bb2f221b 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/ScaledSwampDragon.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/ScaledSwampDragon.cs @@ -40,6 +40,7 @@ namespace Server.Mobiles 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 5d0e70f05..148dbe3cf 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/SilverSteed.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/SilverSteed.cs @@ -20,6 +20,7 @@ namespace Server.Mobiles 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 3ef433a5f..edc313bd0 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/SkeletalMount.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/SkeletalMount.cs @@ -32,6 +32,7 @@ namespace Server.Mobiles 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 ff3631345..69bd9ce56 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/SwampDragon.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/SwampDragon.cs @@ -110,6 +110,7 @@ namespace Server.Mobiles [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 bd8f64ba6..2a33b99bf 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/Unicorn.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/Unicorn.cs @@ -46,6 +46,7 @@ namespace Server.Mobiles 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 @@ namespace Server.Mobiles // 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 b78273ed5..3f6a04a4d 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 @@ namespace Server.Mobiles 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 ea2ed659f..aa3b198ae 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -2369,6 +2369,8 @@ namespace Server.Mobiles UnsummonTimer.StopTimer(this); + StaminaSystem.RemoveEntry(this as IHasSteps); + base.OnAfterDelete(); } @@ -4177,15 +4179,13 @@ namespace Server.Mobiles { 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 @@ namespace Server.Mobiles 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 972949c89..0df10c313 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -90,12 +90,12 @@ namespace Server.Mobiles 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 @@ namespace Server.Mobiles 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 @@ namespace Server.Mobiles [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 02efbc531..8f91e9022 100644 --- a/Projects/UOContent/Mobiles/Special/ChaosGuard.cs +++ b/Projects/UOContent/Mobiles/Special/ChaosGuard.cs @@ -4,7 +4,7 @@ using Server.Items; 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 cc8fa055c..80d2a947c 100644 --- a/Projects/UOContent/Mobiles/Special/Neira.cs +++ b/Projects/UOContent/Mobiles/Special/Neira.cs @@ -201,65 +201,6 @@ public partial class Neira : BaseChampion 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 000000000..bab4d9815 --- /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 0ab5664cc..0c162543d 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" }