// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic; using System.Diagnostics; using System.Threading; using MUnique.OpenMU.Pathfinding; using Nito.AsyncEx; /// /// Money which got dropped on the ground of a map. /// public sealed class DroppedMoney : AsyncDisposable, ILocateable { /// /// Gets the pickup lock. Used to synchronize pick up requests from the players. /// private readonly AsyncLock _pickupLock; private readonly IReadOnlyList _shares; private Timer? _removeTimer; private bool _availableToPick = true; /// /// Initializes a new instance of the class. /// /// The amount. /// The position where the item was dropped on the map. /// The map. /// /// The part of the money which is reserved for each player, matching the experience they gained from the kill. /// When it's empty - for example for money from an item box - the money is split equally instead. /// public DroppedMoney(uint amount, Point position, GameMap map, IReadOnlyList? shares = null) { this.Amount = amount; this._shares = shares ?? []; this._pickupLock = new(); this.Position = position; this.CurrentMap = map; this._removeTimer = new Timer(this.OnTimerTimeout, null, (int)map.ItemDropDuration.TotalMilliseconds, Timeout.Infinite); } /// /// Gets the money item. /// public uint Amount { get; } /// public Point Position { get; set; } /// /// Gets or sets the identifier. /// public ushort Id { get; set; } /// public GameMap CurrentMap { get; } /// /// Tries to pick the money by the specified player. /// /// The player. /// /// The success. /// /// /// Can be overwritten, for example for quest items which dropped only for a specific player. /// public async ValueTask TryPickUpByAsync(Player player) { player.Logger.LogDebug("Player {0} tries to pick up {1}", player, this); using (await this._pickupLock.LockAsync()) { if (!this._availableToPick) { player.Logger.LogDebug("Picked up by another player in the mean time, Player {0}, Money {1}", player, this); return false; } this._availableToPick = false; } if (!this.TryGiveMoneyTo(player)) { // Nobody got the money, so the drop is released again. Keeping it claimed would leave it // lying on the map, unpickable for everyone until it expires - and then lost. using (await this._pickupLock.LockAsync()) { this._availableToPick = true; } return false; } await this.DisposeAsync().ConfigureAwait(false); return true; } /// public override string ToString() { return $"Money: {this.Amount} at {this.CurrentMap.Definition.Name} ({this.Position})"; } /// protected override async ValueTask DisposeAsyncCore() { if (this._removeTimer is { } timer) { try { this._removeTimer = null; await timer.DisposeAsync().ConfigureAwait(false); await this.CurrentMap.RemoveAsync(this).ConfigureAwait(false); } catch (Exception e) { Debug.Fail(e.Message, e.StackTrace); } } await base.DisposeAsyncCore().ConfigureAwait(false); } /// /// Tries to hand the money over to the player, or to its party. Returns false if it could not be /// given to anyone, e.g. because the receiver is already at the maximum inventory money. /// /// The player which picks the money up. /// True, if at least one player received money; Otherwise, false. private bool TryGiveMoneyTo(Player player) { if (player.Party is not { } party) { if (!MoneyDistribution.TryPay(player, this.Amount)) { player.Logger.LogDebug("Money could not be added to the inventory, Player {0}, Money {1}", player, this); return false; } return true; } // Money has no owner - it can always be picked up, by strangers too. The recorded shares // only apply when the party which picks it up is the one which earned it; then the money // follows the experience. For anyone else there is no experience to follow, so it is split // equally between the picking party, just like money without any shares (e.g. an item box). var earnedByThisParty = this._shares.Any(share => party.IsEligibleForMoney(share.Player, player)); var shares = earnedByThisParty ? this._shares : MoneyDistribution.CreateEqualShares(this.Amount, party.PartyList.OfType().ToList()); var received = MoneyDistribution.TryPayShares(shares, member => party.IsEligibleForMoney(member, player)); if (!received) { player.Logger.LogDebug("No party member could take the money, Player {0}, Money {1}", player, this); } return received; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")] private async void OnTimerTimeout(object? state) { try { await this.DisposeAsync().ConfigureAwait(false); } catch (Exception ex) { Debug.Fail(ex.Message, ex.StackTrace); } } }