//
// 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;
}
}