// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Attributes; /// /// Splits a money drop between players and hands it over to them. /// /// /// This is the single place where and /// are applied, so that /// every path - dropped on the ground, added directly, solo or in a party - treats them the same way. /// internal static class MoneyDistribution { /// /// Splits the money proportionally to the experience each player gained from the kill. /// /// The total amount of money to split. /// The experience gained per player. /// The part of the money which is reserved for each player. public static IReadOnlyList CreateShares(uint amount, IReadOnlyList experienceShares) { if (experienceShares.Count == 0) { return []; } if (experienceShares.Count == 1) { return [new MoneyShare(experienceShares[0].Player, amount)]; } var weights = new long[experienceShares.Count]; for (int i = 0; i < experienceShares.Count; i++) { weights[i] = experienceShares[i].Experience; } var parts = SplitByWeight(amount, weights); var shares = new MoneyShare[experienceShares.Count]; for (int i = 0; i < experienceShares.Count; i++) { shares[i] = new MoneyShare(experienceShares[i].Player, parts[i]); } return shares; } /// /// Splits the money equally, which is used when no per player experience is known - for example /// for the fixed money amount of an item box. /// /// The total amount of money to split. /// The players to split it between. /// The part of the money which is reserved for each player. public static IReadOnlyList CreateEqualShares(uint amount, IReadOnlyList players) { if (players.Count == 0) { return []; } var parts = SplitByWeight(amount, new long[players.Count]); var shares = new MoneyShare[players.Count]; for (int i = 0; i < players.Count; i++) { shares[i] = new MoneyShare(players[i], parts[i]); } return shares; } /// /// Hands the shares over to the players which are still eligible. The shares of players which /// are not eligible anymore are re-distributed between the remaining ones. /// /// The shares. /// Determines whether a player may still receive their share. /// True, if at least one player received money; Otherwise, false. public static bool TryPayShares(IReadOnlyList shares, Func isEligible) { var eligible = new List(shares.Count); uint forfeited = 0; foreach (var share in shares) { if (isEligible(share.Player)) { eligible.Add(share); } else { forfeited += share.Amount; } } if (eligible.Count == 0) { return false; } var extra = new uint[eligible.Count]; if (forfeited > 0) { var weights = new long[eligible.Count]; for (int i = 0; i < eligible.Count; i++) { weights[i] = eligible[i].Amount; } extra = SplitByWeight(forfeited, weights); } var received = false; for (int i = 0; i < eligible.Count; i++) { received |= TryPay(eligible[i].Player, eligible[i].Amount + extra[i]); } return received; } /// /// Adds the money to the inventory of the player, applying their /// and the configured pick up clamp. /// /// The player which should receive the money. /// The amount, before the money rate of the player is applied. /// True, if the player received money; Otherwise, false. public static bool TryPay(Player player, uint amount) { if (amount == 0) { return false; } // The rate is applied in double precision: a float multiplication would round money amounts // above the ~16.7M the float mantissa can represent exactly, before the cast to long. var scaled = (long)(amount * (double)(player.Attributes?[Stats.MoneyAmountRate] ?? 1.0f)); if (scaled <= 0) { return false; } var amountToAdd = (int)Math.Min(scaled, int.MaxValue); if (player.GameContext?.Configuration?.ClampMoneyOnPickup ?? false) { var maximumMoney = player.GameContext?.Configuration?.MaximumInventoryMoney ?? int.MaxValue; amountToAdd = (int)Math.Min(amountToAdd, Math.Max(0, maximumMoney - player.Money)); if (amountToAdd <= 0) { return false; } } return player.TryAddMoney(amountToAdd); } /// /// Splits an amount proportionally to the given weights. When all weights are zero, it's split equally. /// private static uint[] SplitByWeight(uint amount, IReadOnlyList weights) { var result = new uint[weights.Count]; if (weights.Count == 0 || amount == 0) { return result; } long totalWeight = 0; foreach (var weight in weights) { totalWeight += Math.Max(0, weight); } var remainders = new long[weights.Count]; uint distributed = 0; for (int i = 0; i < weights.Count; i++) { if (totalWeight > 0) { var numerator = (long)amount * Math.Max(0, weights[i]); result[i] = (uint)(numerator / totalWeight); remainders[i] = numerator % totalWeight; } else { result[i] = amount / (uint)weights.Count; remainders[i] = 1; } distributed += result[i]; } // The integer division loses up to one unit per share. Handing all of it to the same // share would systematically favour one player over many kills, so the units go to the // shares which were cut the most, one each (largest remainder method). for (var rest = amount - distributed; rest > 0; rest--) { var pick = -1; for (int i = 0; i < remainders.Length; i++) { if (remainders[i] > 0 && (pick < 0 || remainders[i] > remainders[pick])) { pick = i; } } if (pick < 0) { break; } result[pick]++; remainders[pick] = 0; } return result; } }