From 2a2c2dc800f4b17c39bcd7367eabd06a540a33fd Mon Sep 17 00:00:00 2001 From: sven-n Date: Thu, 5 Dec 2024 21:35:26 +0100 Subject: [PATCH] Refactored stats update The IUpdateStatsPlugIn (view-plugin) implementations are responsible to inform the client about changes in certain stats like Health, Mana etc. Previously, only a fixed subset of the available stats were available for updates. Now we support to update all stats - the implementation can decide which stat it wants to send to the client. Because some messages include more than one stat, and they can change in short amount of time, I implemented a mechanism which sends the update after a short delay of 16 ms. This results in less network messages. Additionally, I removed the periodically sending of the messages, when nothing changed. --- src/AttributeSystem/AttributeSystem.cs | 38 ++++++- src/GameLogic/AttackableExtensions.cs | 16 --- .../Attributes/ItemAwareAttributeSystem.cs | 42 +++++++ src/GameLogic/Player.cs | 70 ++++-------- .../HealthPotionConsumeHandlerPlugIn.cs | 8 -- .../ManaPotionConsumeHandler.cs | 6 - .../Skills/DrainLifeSkillPlugIn.cs | 14 +-- .../Views/Character/IUpdateStatsPlugIn.cs | 41 +------ src/GameServer/GameServer.cs | 2 +- .../Character/UpdateStatsBasePlugIn.cs | 106 ++++++++++++++++++ .../Character/UpdateStatsExtendedPlugIn.cs | 68 +++++------ .../RemoteView/Character/UpdateStatsPlugIn.cs | 82 +++++++------- .../RemoteView/ViewPlugInContainer.cs | 2 + 13 files changed, 292 insertions(+), 203 deletions(-) create mode 100644 src/GameServer/RemoteView/Character/UpdateStatsBasePlugIn.cs diff --git a/src/AttributeSystem/AttributeSystem.cs b/src/AttributeSystem/AttributeSystem.cs index 432e1b768..7f707a6f9 100644 --- a/src/AttributeSystem/AttributeSystem.cs +++ b/src/AttributeSystem/AttributeSystem.cs @@ -4,10 +4,12 @@ namespace MUnique.OpenMU.AttributeSystem; +using System.Collections; + /// /// The attribute system which holds all attributes of a character. /// -public class AttributeSystem : IAttributeSystem +public class AttributeSystem : IAttributeSystem, IEnumerable { private readonly IDictionary _attributes = new Dictionary(); @@ -16,7 +18,7 @@ public class AttributeSystem : IAttributeSystem /// /// The stat attributes. These attributes are added just as-is and are not wrapped by a . /// The initial base attributes. These attributes contain the base values which will be wrapped by a , so additional elements can contribute to the attributes value. Instead of providing them here, you could also add them to the system by calling later. - /// The initial attribute relationships. Instead of providing them here, you could also add them to the system by calling later. + /// The initial attribute relationships. Instead of providing them here, you could also add them to the system by calling later. public AttributeSystem(IEnumerable statAttributes, IEnumerable baseAttributes, IEnumerable attributeRelationships) { foreach (var statAttribute in statAttributes) @@ -129,6 +131,7 @@ public void AddElement(IElement element, AttributeDefinition targetAttribute) { attribute = new ComposableAttribute(targetAttribute); this._attributes.Add(targetAttribute, attribute); + this.OnAttributeAdded(attribute); } if (attribute is IComposableAttribute composableAttribute) @@ -180,6 +183,18 @@ public override string ToString() return stringBuilder.ToString(); } + /// + public IEnumerator GetEnumerator() + { + return this._attributes.Values.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + /// /// Gets or creates the element with the specified attribute. /// @@ -193,11 +208,30 @@ public IElement GetOrCreateAttribute(AttributeDefinition attributeDefinition) var composableAttribute = new ComposableAttribute(attributeDefinition); element = composableAttribute; this._attributes.Add(attributeDefinition, composableAttribute); + this.OnAttributeAdded(composableAttribute); } return element; } + /// + /// Called when an attribute was added to the system after the initial construction. + /// + /// The attribute. + protected virtual void OnAttributeAdded(IAttribute attribute) + { + // can be overwritten. + } + + /// + /// Called when an attribute was removed from the system. + /// + /// The attribute. + protected virtual void OnAttributeRemoved(IAttribute attribute) + { + // can be overwritten. + } + /// /// Adds the attribute relationship. /// diff --git a/src/GameLogic/AttackableExtensions.cs b/src/GameLogic/AttackableExtensions.cs index 4de23e609..70226e434 100644 --- a/src/GameLogic/AttackableExtensions.cs +++ b/src/GameLogic/AttackableExtensions.cs @@ -174,8 +174,6 @@ public static async ValueTask ApplyRegenerationAsync(this IAttackable target, Pl skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill)); - var isHealthUpdated = false; - var isManaUpdated = false; var skill = skillEntry.Skill; foreach (var powerUpDefinition in skill.MagicEffectDef?.PowerUpDefinitions ?? Enumerable.Empty()) { @@ -187,8 +185,6 @@ public static async ValueTask ApplyRegenerationAsync(this IAttackable target, Pl target.Attributes[regeneration.CurrentAttribute] = Math.Min( target.Attributes[regeneration.CurrentAttribute] + value, target.Attributes[regeneration.MaximumAttribute]); - isHealthUpdated |= regeneration.CurrentAttribute == Stats.CurrentHealth || regeneration.CurrentAttribute == Stats.CurrentShield; - isManaUpdated |= regeneration.CurrentAttribute == Stats.CurrentMana || regeneration.CurrentAttribute == Stats.CurrentAbility; } else { @@ -196,18 +192,6 @@ public static async ValueTask ApplyRegenerationAsync(this IAttackable target, Pl $"Regeneration skill {skill.Name} is configured to regenerate a non-regeneration-able target attribute {powerUpDefinition.TargetAttribute}."); } } - - if (target is IWorldObserver observer) - { - var updatedStats = - (isHealthUpdated ? IUpdateStatsPlugIn.UpdatedStats.Health : IUpdateStatsPlugIn.UpdatedStats.Undefined) - | (isManaUpdated ? IUpdateStatsPlugIn.UpdatedStats.Mana : IUpdateStatsPlugIn.UpdatedStats.Undefined); - - if (updatedStats != IUpdateStatsPlugIn.UpdatedStats.Undefined) - { - await observer.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync(updatedStats)).ConfigureAwait(false); - } - } } /// diff --git a/src/GameLogic/Attributes/ItemAwareAttributeSystem.cs b/src/GameLogic/Attributes/ItemAwareAttributeSystem.cs index 9fbecbf81..3a72a8816 100644 --- a/src/GameLogic/Attributes/ItemAwareAttributeSystem.cs +++ b/src/GameLogic/Attributes/ItemAwareAttributeSystem.cs @@ -23,8 +23,17 @@ public ItemAwareAttributeSystem(Account account, Character character) character.CharacterClass.AttributeCombinations) { this.ItemPowerUps = new Dictionary>(); + foreach (var attribute in this) + { + this.OnAttributeAdded(attribute); + } } + /// + /// Occurs when an attribute value changed. + /// + public event EventHandler? AttributeValueChanged; + /// /// Gets the item power ups. /// @@ -38,6 +47,12 @@ public ItemAwareAttributeSystem(Account account, Character character) /// public void Dispose() { + this.AttributeValueChanged = null; + foreach (var attribute in this) + { + attribute.ValueChanged -= this.OnAttributeValueChanged; + } + foreach (var powerUpWrapper in this.ItemPowerUps.SelectMany(p => p.Value)) { powerUpWrapper.Dispose(); @@ -82,4 +97,31 @@ public override string ToString() return stringBuilder.ToString(); } + + /// + protected override void OnAttributeAdded(IAttribute attribute) + { + base.OnAttributeAdded(attribute); + attribute.ValueChanged += this.OnAttributeValueChanged; + } + + /// + protected override void OnAttributeRemoved(IAttribute attribute) + { + attribute.ValueChanged -= this.OnAttributeValueChanged; + base.OnAttributeRemoved(attribute); + } + + /// + /// Called when an attribute value changed. Forwards the event to . + /// + /// The sender. + /// The instance containing the event data. + private void OnAttributeValueChanged(object? sender, EventArgs e) + { + if (sender is IAttribute attribute) + { + this.AttributeValueChanged?.Invoke(this, attribute); + } + } } \ No newline at end of file diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index accfb1edf..52cfa5d5d 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -19,7 +19,6 @@ namespace MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.PlugIns; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.GameLogic.Views.Character; -using MUnique.OpenMU.GameLogic.Views.Duel; using MUnique.OpenMU.GameLogic.Views.Inventory; using MUnique.OpenMU.GameLogic.Views.MuHelper; using MUnique.OpenMU.GameLogic.Views.Pet; @@ -30,7 +29,6 @@ namespace MUnique.OpenMU.GameLogic; using MUnique.OpenMU.Persistence; using MUnique.OpenMU.PlugIns; using Nito.AsyncEx; -using static MUnique.OpenMU.GameLogic.Views.Character.IUpdateStatsPlugIn; /// /// The base implementation of a player. @@ -626,13 +624,11 @@ public bool IsAnySelfDefenseActive() if (Rand.NextRandomBool(this.Attributes[Stats.FullyRecoverHealthAfterHitChance])) { this.Attributes[Stats.CurrentHealth] = this.Attributes[Stats.MaximumHealth]; - await this.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync(UpdatedStats.Health)).ConfigureAwait(false); } if (Rand.NextRandomBool(this.Attributes[Stats.FullyRecoverManaAfterHitChance])) { this.Attributes[Stats.CurrentMana] = this.Attributes[Stats.MaximumMana]; - await this.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync(UpdatedStats.Mana)).ConfigureAwait(false); } await this.HitAsync(hitInfo, attacker, skill?.Skill).ConfigureAwait(false); @@ -777,8 +773,6 @@ public async ValueTask AfterKilledMonsterAsync() var additionalValue = (uint)((this.Attributes![recoverAfterMonsterKill.RegenerationMultiplier] * this.Attributes[recoverAfterMonsterKill.MaximumAttribute]) + this.Attributes[recoverAfterMonsterKill.AbsoluteAttribute]); this.Attributes[recoverAfterMonsterKill.CurrentAttribute] = (uint)Math.Min(this.Attributes[recoverAfterMonsterKill.MaximumAttribute], this.Attributes[recoverAfterMonsterKill.CurrentAttribute] + additionalValue); } - - await this.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync()).ConfigureAwait(false); } /// @@ -1276,8 +1270,6 @@ public async Task RegenerateAsync() attributes[r.MaximumAttribute]); } - await this.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync()).ConfigureAwait(false); - await this.RegenerateHeroStateAsync().ConfigureAwait(false); } catch (InvalidOperationException) @@ -1385,8 +1377,6 @@ public async ValueTask TryConsumeForSkillAsync(Skill skill) this.Attributes![requirement.Attribute] -= this.GetRequiredValue(requirement); } - await this.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync(UpdatedStats.Mana)).ConfigureAwait(false); - return true; } @@ -1688,6 +1678,7 @@ protected override async ValueTask DisposeAsyncCore() this._walker.Dispose(); await this.MagicEffectList.DisposeAsync().ConfigureAwait(false); this._respawnAfterDeathCts?.Dispose(); + (this._viewPlugIns as IDisposable)?.Dispose(); this.PlayerDisconnected = null; this.PlayerEnteredWorld = null; @@ -2138,12 +2129,8 @@ private async ValueTask OnPlayerEnteredWorldAsync() await this.InvokeViewPlugInAsync(p => p.ShowQuestStateAsync(null)).ConfigureAwait(false); // Legacy quest system await this.InvokeViewPlugInAsync(p => p.ShowActiveQuestsAsync()).ConfigureAwait(false); // New quest system - this.Attributes.GetOrCreateAttribute(Stats.MaximumMana).ValueChanged += this.OnMaximumManaOrAbilityChanged; - this.Attributes.GetOrCreateAttribute(Stats.MaximumAbility).ValueChanged += this.OnMaximumManaOrAbilityChanged; - this.Attributes.GetOrCreateAttribute(Stats.MaximumHealth).ValueChanged += this.OnMaximumHealthOrShieldChanged; - this.Attributes.GetOrCreateAttribute(Stats.MaximumShield).ValueChanged += this.OnMaximumHealthOrShieldChanged; + this.Attributes.AttributeValueChanged += this.OnAttributeValueChanged; this.Attributes.GetOrCreateAttribute(Stats.TransformationSkin).ValueChanged += this.OnTransformationSkinChanged; - this.Attributes.GetOrCreateAttribute(Stats.AttackSpeed).ValueChanged += this.OnAttackSpeedChanged; var ammoAttribute = this.Attributes.GetOrCreateAttribute(Stats.AmmunitionAmount); this.Attributes[Stats.AmmunitionAmount] = (float)(this.Inventory?.EquippedAmmunitionItem?.Durability ?? 0); @@ -2232,19 +2219,31 @@ private void SetReclaimableAttributesToMaximum() } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")] - private async void OnMaximumHealthOrShieldChanged(object? sender, EventArgs args) + private async void OnAttributeValueChanged(object? sender, IAttribute attribute) { try { - this.Attributes![Stats.CurrentHealth] = Math.Min(this.Attributes[Stats.CurrentHealth], this.Attributes[Stats.MaximumHealth]); - this.Attributes[Stats.CurrentShield] = Math.Min(this.Attributes[Stats.CurrentShield], this.Attributes[Stats.MaximumShield]); + _ = LimitCurrentAttribute(Stats.MaximumHealth, Stats.CurrentHealth) + || LimitCurrentAttribute(Stats.MaximumMana, Stats.CurrentMana) + || LimitCurrentAttribute(Stats.MaximumShield, Stats.CurrentShield) + || LimitCurrentAttribute(Stats.MaximumAbility, Stats.CurrentAbility); - await this.InvokeViewPlugInAsync(p => p.UpdateMaximumStatsAsync(UpdatedStats.Health)).ConfigureAwait(false); - await this.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync(UpdatedStats.Health)).ConfigureAwait(false); + await this.InvokeViewPlugInAsync(p => p.UpdateStatsAsync(attribute.Definition, attribute.Value)).ConfigureAwait(false); } catch (Exception ex) { - this.Logger.LogError(ex, nameof(this.OnMaximumHealthOrShieldChanged)); + this.Logger.LogError(ex, $"{nameof(this.OnAttributeValueChanged)} failed for attribute {attribute.Definition}."); + } + + bool LimitCurrentAttribute(AttributeDefinition maximumDefinition, AttributeDefinition currentDefinition) + { + if (attribute.Definition == maximumDefinition && attribute.Value < this.Attributes![currentDefinition]) + { + this.Attributes![currentDefinition] = attribute.Value; + return true; + } + + return false; } } @@ -2274,35 +2273,6 @@ private async void OnAmmunitionAmountChanged(object? sender, EventArgs args) } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")] - private async void OnMaximumManaOrAbilityChanged(object? sender, EventArgs args) - { - try - { - this.Attributes![Stats.CurrentMana] = Math.Min(this.Attributes[Stats.CurrentMana], this.Attributes[Stats.MaximumMana]); - this.Attributes[Stats.CurrentAbility] = Math.Min(this.Attributes[Stats.CurrentAbility], this.Attributes[Stats.MaximumAbility]); - await this.InvokeViewPlugInAsync(p => p.UpdateMaximumStatsAsync(UpdatedStats.Mana)).ConfigureAwait(false); - await this.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync(UpdatedStats.Mana)).ConfigureAwait(false); - } - catch (Exception ex) - { - this.Logger.LogError(ex, nameof(this.OnMaximumManaOrAbilityChanged)); - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")] - private async void OnAttackSpeedChanged(object? sender, EventArgs args) - { - try - { - await this.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync(UpdatedStats.Speed)).ConfigureAwait(false); - } - catch (Exception ex) - { - this.Logger.LogError(ex, nameof(this.OnMaximumManaOrAbilityChanged)); - } - } - private async ValueTask DecreaseItemDurabilityAfterHitAsync(HitInfo hitInfo) { var randomArmorItem = this.Inventory?.EquippedItems.Where(ItemExtensions.IsDefensiveItem).SelectRandom(); diff --git a/src/GameLogic/PlayerActions/ItemConsumeActions/HealthPotionConsumeHandlerPlugIn.cs b/src/GameLogic/PlayerActions/ItemConsumeActions/HealthPotionConsumeHandlerPlugIn.cs index dd230f5c9..3ee7ec41c 100644 --- a/src/GameLogic/PlayerActions/ItemConsumeActions/HealthPotionConsumeHandlerPlugIn.cs +++ b/src/GameLogic/PlayerActions/ItemConsumeActions/HealthPotionConsumeHandlerPlugIn.cs @@ -8,7 +8,6 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions; using MUnique.OpenMU.AttributeSystem; using MUnique.OpenMU.GameLogic.Attributes; -using MUnique.OpenMU.GameLogic.Views.Character; /// /// The consume handler for a potion that recovers health. @@ -20,11 +19,4 @@ public abstract class HealthPotionConsumeHandlerPlugIn : RecoverConsumeHandlerPl /// protected override AttributeDefinition CurrentAttribute => Stats.CurrentHealth; - - /// - protected override async ValueTask OnAfterRecoverAsync(Player player) - { - // maybe instead of calling UpdateCurrentHealth etc. provide a more general method where we pass this.CurrentAttribute. The view can then decide what to do with it. - await player.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync(IUpdateStatsPlugIn.UpdatedStats.Health)).ConfigureAwait(false); - } } \ No newline at end of file diff --git a/src/GameLogic/PlayerActions/ItemConsumeActions/ManaPotionConsumeHandler.cs b/src/GameLogic/PlayerActions/ItemConsumeActions/ManaPotionConsumeHandler.cs index 30efe2401..112f1e424 100644 --- a/src/GameLogic/PlayerActions/ItemConsumeActions/ManaPotionConsumeHandler.cs +++ b/src/GameLogic/PlayerActions/ItemConsumeActions/ManaPotionConsumeHandler.cs @@ -20,10 +20,4 @@ public abstract class ManaPotionConsumeHandler : RecoverConsumeHandlerPlugIn.Man /// protected override AttributeDefinition CurrentAttribute => Stats.CurrentMana; - - /// - protected override async ValueTask OnAfterRecoverAsync(Player player) - { - await player.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync(IUpdateStatsPlugIn.UpdatedStats.Mana)).ConfigureAwait(false); - } } \ No newline at end of file diff --git a/src/GameLogic/PlayerActions/Skills/DrainLifeSkillPlugIn.cs b/src/GameLogic/PlayerActions/Skills/DrainLifeSkillPlugIn.cs index b0e57570c..a3c823f08 100644 --- a/src/GameLogic/PlayerActions/Skills/DrainLifeSkillPlugIn.cs +++ b/src/GameLogic/PlayerActions/Skills/DrainLifeSkillPlugIn.cs @@ -24,15 +24,13 @@ public class DrainLifeSkillPlugIn : IAreaSkillPlugIn /// public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter, HitInfo? hitInfo) { - if (attacker is Player attackerPlayer && hitInfo != null && hitInfo.Value.HealthDamage > 0) + if (attacker is not Player attackerPlayer + || hitInfo is not { HealthDamage: > 0 } + || attackerPlayer.Attributes is not { } playerAttributes) { - var playerAttributes = attackerPlayer.Attributes; - - if (playerAttributes != null) - { - playerAttributes[Stats.CurrentHealth] = (uint)Math.Min(playerAttributes[Stats.MaximumHealth], playerAttributes[Stats.CurrentHealth] + hitInfo.Value.HealthDamage); - await attackerPlayer.InvokeViewPlugInAsync(p => p.UpdateCurrentStatsAsync(IUpdateStatsPlugIn.UpdatedStats.Health)).ConfigureAwait(false); - } + return; } + + playerAttributes[Stats.CurrentHealth] = (uint)Math.Min(playerAttributes[Stats.MaximumHealth], playerAttributes[Stats.CurrentHealth] + hitInfo.Value.HealthDamage); } } \ No newline at end of file diff --git a/src/GameLogic/Views/Character/IUpdateStatsPlugIn.cs b/src/GameLogic/Views/Character/IUpdateStatsPlugIn.cs index 6e6d81f42..e1a3c926c 100644 --- a/src/GameLogic/Views/Character/IUpdateStatsPlugIn.cs +++ b/src/GameLogic/Views/Character/IUpdateStatsPlugIn.cs @@ -12,42 +12,9 @@ namespace MUnique.OpenMU.GameLogic.Views.Character; public interface IUpdateStatsPlugIn : IViewPlugIn { /// - /// Updates the maximum stats. + /// Updates the attribute value. /// - /// The updated stats. - ValueTask UpdateMaximumStatsAsync(UpdatedStats updatedStats = UpdatedStats.Health | UpdatedStats.Mana | UpdatedStats.Speed); - - /// - /// Updates the current stats. - /// - /// The updated stats. - ValueTask UpdateCurrentStatsAsync(UpdatedStats updatedStats = UpdatedStats.Health | UpdatedStats.Mana | UpdatedStats.Speed); - - /// - /// The updated stat. - /// This might be replaced by the actual in the future. - /// - [Flags] - public enum UpdatedStats - { - /// - /// Undefined. - /// - Undefined, - - /// - /// The health or shield changed. - /// - Health = 0x01, - - /// - /// The mana or ability changed. - /// - Mana = 0x02, - - /// - /// The attack speed changed. - /// - Speed = 0x04, - } + /// The attribute. + /// The value. + ValueTask UpdateStatsAsync(AttributeDefinition attribute, float value); } \ No newline at end of file diff --git a/src/GameServer/GameServer.cs b/src/GameServer/GameServer.cs index deaae87d4..46876fad8 100644 --- a/src/GameServer/GameServer.cs +++ b/src/GameServer/GameServer.cs @@ -431,7 +431,7 @@ private async ValueTask OnPlayerDisconnectedAsync(Player remotePlayer) { await this.SaveSessionOfPlayerAsync(remotePlayer).ConfigureAwait(false); await this.SetOfflineAtLoginServerAsync(remotePlayer).ConfigureAwait(false); - remotePlayer.Dispose(); + await remotePlayer.DisposeAsync().ConfigureAwait(false); this.OnPropertyChanged(nameof(this.CurrentConnections)); } diff --git a/src/GameServer/RemoteView/Character/UpdateStatsBasePlugIn.cs b/src/GameServer/RemoteView/Character/UpdateStatsBasePlugIn.cs new file mode 100644 index 000000000..8354685d6 --- /dev/null +++ b/src/GameServer/RemoteView/Character/UpdateStatsBasePlugIn.cs @@ -0,0 +1,106 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.RemoteView.Character; + +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Threading; +using MUnique.OpenMU.AttributeSystem; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Views.Character; +using UpdateAction = Func; + +/// +/// The default implementation of the which is forwarding everything to the game client with specific data packets. +/// +public abstract class UpdateStatsBasePlugIn : Disposable, IUpdateStatsPlugIn +{ + private static readonly int SendDelayMs = 16; + + private static readonly ConcurrentDictionary< + FrozenDictionary, + FrozenDictionary> ActionIndexMappings = new(); + + private readonly RemotePlayer _player; + + private readonly FrozenDictionary _actionIndexMapping; + + private readonly FrozenDictionary _changeActions; + + private readonly AutoResetEvent[] _resetEvents; + + /// + /// Initializes a new instance of the class. + /// + /// The player. + /// The change actions. + protected UpdateStatsBasePlugIn(RemotePlayer player, FrozenDictionary changeActions) + { + this._player = player; + this._changeActions = changeActions; + this._actionIndexMapping = GetActionIndexMapping(changeActions); + this._resetEvents = new AutoResetEvent[changeActions.Count]; + for (int i = 0; i < this._resetEvents.Length; i++) + { + this._resetEvents[i] = new AutoResetEvent(true); + } + } + + /// + public async ValueTask UpdateStatsAsync(AttributeDefinition attribute, float value) + { + if (this._player.Attributes is null + || !(this._player.Connection?.Connected ?? false)) + { + return; + } + + if (this._changeActions.TryGetValue(attribute, out var action)) + { + _ = this.SendDelayedUpdateAsync(action, attribute); + } + } + + /// + protected override void Dispose(bool disposing) + { + foreach (var are in this._resetEvents) + { + are.Dispose(); + } + + base.Dispose(disposing); + } + + private async ValueTask SendDelayedUpdateAsync(UpdateAction action, AttributeDefinition attribute) + { + var autoResetEvent = this._resetEvents[this._actionIndexMapping[action]]; + if (!autoResetEvent.WaitOne(0)) + { + // we're sending an update already. + return; + } + + try + { + await Task.Delay(SendDelayMs).ConfigureAwait(false); + await action(this._player).ConfigureAwait(false); + } + finally + { + autoResetEvent.Set(); + } + } + + private static FrozenDictionary GetActionIndexMapping(FrozenDictionary changeActions) + { + return ActionIndexMappings.GetOrAdd(changeActions, CreateNewIndexDictionary); + + FrozenDictionary CreateNewIndexDictionary(FrozenDictionary dict) + { + return dict.Values.Distinct().Index().ToFrozenDictionary(tuple => tuple.Item, tuple => tuple.Index); + } + } +} \ No newline at end of file diff --git a/src/GameServer/RemoteView/Character/UpdateStatsExtendedPlugIn.cs b/src/GameServer/RemoteView/Character/UpdateStatsExtendedPlugIn.cs index 1e70249c4..acf506406 100644 --- a/src/GameServer/RemoteView/Character/UpdateStatsExtendedPlugIn.cs +++ b/src/GameServer/RemoteView/Character/UpdateStatsExtendedPlugIn.cs @@ -1,10 +1,12 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameServer.RemoteView.Character; +using System.Collections.Frozen; using System.Runtime.InteropServices; +using MUnique.OpenMU.AttributeSystem; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.Views.Character; using MUnique.OpenMU.Network.Packets.ServerToClient; @@ -17,47 +19,49 @@ namespace MUnique.OpenMU.GameServer.RemoteView.Character; [PlugIn(nameof(UpdateStatsExtendedPlugIn), "The extended implementation of the IUpdateStatsPlugIn which is forwarding everything to the game client with specific data packets.")] [Guid("E9A1CCBE-416F-41BA-8E74-74CBEB7042DD")] [MinimumClient(106, 3, ClientLanguage.Invariant)] -public class UpdateStatsExtendedPlugIn : IUpdateStatsPlugIn +public class UpdateStatsExtendedPlugIn : UpdateStatsBasePlugIn { - private readonly RemotePlayer _player; + private static readonly FrozenDictionary> AttributeChangeActions = new Dictionary> + { + { Stats.MaximumHealth, OnMaximumStatsChangedAsync }, + { Stats.MaximumShield, OnMaximumStatsChangedAsync }, + { Stats.MaximumMana, OnMaximumStatsChangedAsync }, + { Stats.MaximumAbility, OnMaximumStatsChangedAsync }, + + { Stats.CurrentHealth, OnCurrentStatsChangedAsync }, + { Stats.CurrentShield, OnCurrentStatsChangedAsync }, + { Stats.CurrentMana, OnCurrentStatsChangedAsync }, + { Stats.CurrentAbility, OnCurrentStatsChangedAsync }, + { Stats.AttackSpeed, OnCurrentStatsChangedAsync }, + { Stats.MagicSpeed, OnCurrentStatsChangedAsync }, + }.ToFrozenDictionary(); /// /// Initializes a new instance of the class. /// /// The player. - public UpdateStatsExtendedPlugIn(RemotePlayer player) => this._player = player; - - /// - public async ValueTask UpdateMaximumStatsAsync(IUpdateStatsPlugIn.UpdatedStats updatedStats = IUpdateStatsPlugIn.UpdatedStats.Undefined) + public UpdateStatsExtendedPlugIn(RemotePlayer player) + : base(player, AttributeChangeActions) { - if (this._player.Attributes is null - || !(this._player.Connection?.Connected ?? false)) - { - return; - } - - await this._player.Connection.SendMaximumStatsExtendedAsync( - (uint)this._player.Attributes[Stats.MaximumHealth], - (uint)this._player.Attributes[Stats.MaximumShield], - (uint)this._player.Attributes[Stats.MaximumMana], - (uint)this._player.Attributes[Stats.MaximumAbility]).ConfigureAwait(false); } - /// - public async ValueTask UpdateCurrentStatsAsync(IUpdateStatsPlugIn.UpdatedStats updatedStats = IUpdateStatsPlugIn.UpdatedStats.Undefined) + private static async ValueTask OnMaximumStatsChangedAsync(RemotePlayer player) { - if (this._player.Attributes is null - || !(this._player.Connection?.Connected ?? false)) - { - return; - } + await player.Connection.SendMaximumStatsExtendedAsync( + (uint)player.Attributes![Stats.MaximumHealth], + (uint)player.Attributes[Stats.MaximumShield], + (uint)player.Attributes[Stats.MaximumMana], + (uint)player.Attributes[Stats.MaximumAbility]).ConfigureAwait(false); + } - await this._player.Connection.SendCurrentStatsExtendedAsync( - (uint)this._player.Attributes[Stats.CurrentHealth], - (uint)this._player.Attributes[Stats.CurrentShield], - (uint)this._player.Attributes[Stats.CurrentMana], - (uint)this._player.Attributes[Stats.CurrentAbility], - (ushort)this._player.Attributes[Stats.AttackSpeed], - (ushort)this._player.Attributes[Stats.MagicSpeed]).ConfigureAwait(false); + private static async ValueTask OnCurrentStatsChangedAsync(RemotePlayer player) + { + await player.Connection.SendCurrentStatsExtendedAsync( + (uint)player.Attributes![Stats.CurrentHealth], + (uint)player.Attributes[Stats.CurrentShield], + (uint)player.Attributes[Stats.CurrentMana], + (uint)player.Attributes[Stats.CurrentAbility], + (ushort)player.Attributes[Stats.AttackSpeed], + (ushort)player.Attributes[Stats.MagicSpeed]).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/GameServer/RemoteView/Character/UpdateStatsPlugIn.cs b/src/GameServer/RemoteView/Character/UpdateStatsPlugIn.cs index 5f9cae629..954025cba 100644 --- a/src/GameServer/RemoteView/Character/UpdateStatsPlugIn.cs +++ b/src/GameServer/RemoteView/Character/UpdateStatsPlugIn.cs @@ -4,7 +4,9 @@ namespace MUnique.OpenMU.GameServer.RemoteView.Character; +using System.Collections.Frozen; using System.Runtime.InteropServices; +using MUnique.OpenMU.AttributeSystem; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.Views.Character; using MUnique.OpenMU.Network.Packets.ServerToClient; @@ -15,61 +17,55 @@ namespace MUnique.OpenMU.GameServer.RemoteView.Character; /// [PlugIn(nameof(UpdateStatsPlugIn), "The default implementation of the IUpdateStatsPlugIn which is forwarding everything to the game client with specific data packets.")] [Guid("2A8BFB0C-2AFF-4A52-B390-5A68D5C5F26A")] -public class UpdateStatsPlugIn : IUpdateStatsPlugIn +public class UpdateStatsPlugIn : UpdateStatsBasePlugIn { - private readonly RemotePlayer _player; + private static readonly FrozenDictionary> AttributeChangeActions = new Dictionary> + { + { Stats.CurrentHealth, OnCurrentHealthOrShieldChangedAsync }, + { Stats.CurrentShield, OnCurrentHealthOrShieldChangedAsync }, + { Stats.MaximumHealth, OnMaximumHealthOrShieldChangedAsync }, + { Stats.MaximumShield, OnMaximumHealthOrShieldChangedAsync }, + + { Stats.CurrentMana, OnCurrentManaOrAbilityChangedAsync }, + { Stats.CurrentAbility, OnCurrentManaOrAbilityChangedAsync }, + { Stats.MaximumMana, OnMaximumManaOrAbilityChangedAsync }, + { Stats.MaximumAbility, OnMaximumManaOrAbilityChangedAsync }, + }.ToFrozenDictionary(); /// /// Initializes a new instance of the class. /// /// The player. - public UpdateStatsPlugIn(RemotePlayer player) => this._player = player; - - /// - public async ValueTask UpdateMaximumStatsAsync(IUpdateStatsPlugIn.UpdatedStats updatedStats = IUpdateStatsPlugIn.UpdatedStats.Undefined | IUpdateStatsPlugIn.UpdatedStats.Health | IUpdateStatsPlugIn.UpdatedStats.Mana | IUpdateStatsPlugIn.UpdatedStats.Speed) + public UpdateStatsPlugIn(RemotePlayer player) + : base(player, AttributeChangeActions) { - if (this._player.Attributes is null - || !(this._player.Connection?.Connected ?? false)) - { - return; - } - - if (updatedStats.HasFlag(IUpdateStatsPlugIn.UpdatedStats.Health)) - { - await this._player.Connection.SendMaximumHealthAndShieldAsync( - (ushort)Math.Max(this._player.Attributes[Stats.MaximumHealth], 0f), - (ushort)Math.Max(this._player.Attributes[Stats.MaximumShield], 0f)).ConfigureAwait(false); - } + } - if (updatedStats.HasFlag(IUpdateStatsPlugIn.UpdatedStats.Mana)) - { - await this._player.Connection.SendMaximumManaAndAbilityAsync( - (ushort)Math.Max(this._player.Attributes[Stats.MaximumMana], 0f), - (ushort)Math.Max(this._player.Attributes[Stats.MaximumAbility], 0f)).ConfigureAwait(false); - } + private static async ValueTask OnMaximumHealthOrShieldChangedAsync(RemotePlayer player) + { + await player.Connection.SendMaximumHealthAndShieldAsync( + (ushort)Math.Max(player.Attributes![Stats.MaximumHealth], 0f), + (ushort)Math.Max(player.Attributes[Stats.MaximumShield], 0f)).ConfigureAwait(false); } - /// - public async ValueTask UpdateCurrentStatsAsync(IUpdateStatsPlugIn.UpdatedStats updatedStats = IUpdateStatsPlugIn.UpdatedStats.Undefined | IUpdateStatsPlugIn.UpdatedStats.Health | IUpdateStatsPlugIn.UpdatedStats.Mana | IUpdateStatsPlugIn.UpdatedStats.Speed) + private static async ValueTask OnMaximumManaOrAbilityChangedAsync(RemotePlayer player) { - if (this._player.Attributes is null - || !(this._player.Connection?.Connected ?? false)) - { - return; - } + await player.Connection.SendMaximumManaAndAbilityAsync( + (ushort)Math.Max(player.Attributes![Stats.MaximumMana], 0f), + (ushort)Math.Max(player.Attributes[Stats.MaximumAbility], 0f)).ConfigureAwait(false); + } - if (updatedStats.HasFlag(IUpdateStatsPlugIn.UpdatedStats.Health)) - { - await this._player.Connection.SendCurrentHealthAndShieldAsync( - (ushort)Math.Max(this._player.Attributes[Stats.CurrentHealth], 0f), - (ushort)Math.Max(this._player.Attributes[Stats.CurrentShield], 0f)).ConfigureAwait(false); - } + private static async ValueTask OnCurrentHealthOrShieldChangedAsync(RemotePlayer player) + { + await player.Connection.SendCurrentHealthAndShieldAsync( + (ushort)Math.Max(player.Attributes![Stats.CurrentHealth], 0f), + (ushort)Math.Max(player.Attributes[Stats.CurrentShield], 0f)).ConfigureAwait(false); + } - if (updatedStats.HasFlag(IUpdateStatsPlugIn.UpdatedStats.Mana)) - { - await this._player.Connection.SendCurrentManaAndAbilityAsync( - (ushort)Math.Max(this._player.Attributes[Stats.CurrentMana], 0f), - (ushort)Math.Max(this._player.Attributes[Stats.CurrentAbility], 0f)).ConfigureAwait(false); - } + private static async ValueTask OnCurrentManaOrAbilityChangedAsync(RemotePlayer player) + { + await player.Connection.SendCurrentManaAndAbilityAsync( + (ushort)Math.Max(player.Attributes![Stats.CurrentMana], 0f), + (ushort)Math.Max(player.Attributes[Stats.CurrentAbility], 0f)).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/GameServer/RemoteView/ViewPlugInContainer.cs b/src/GameServer/RemoteView/ViewPlugInContainer.cs index a684ae9e2..f55586d54 100644 --- a/src/GameServer/RemoteView/ViewPlugInContainer.cs +++ b/src/GameServer/RemoteView/ViewPlugInContainer.cs @@ -7,6 +7,7 @@ namespace MUnique.OpenMU.GameServer.RemoteView; using System.ComponentModel.Design; using System.Reflection; using Microsoft.Extensions.DependencyInjection; +using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.Network.PlugIns; using MUnique.OpenMU.PlugIns; @@ -58,6 +59,7 @@ public ViewPlugInContainer(RemotePlayer player, ClientVersion clientVersion, Plu public void Dispose() { this._serviceContainer.Dispose(); + this.KnownPlugIns.OfType().ForEach(d => d.Dispose()); } ///