From ed71d901d13a7987ffcc5dcf7015f00188bf2391 Mon Sep 17 00:00:00 2001 From: nolt Date: Sun, 17 May 2026 20:58:22 +0200 Subject: [PATCH 1/2] fixed zen drop in party --- src/GameLogic/NPC/AttackableNpcBase.cs | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/GameLogic/NPC/AttackableNpcBase.cs b/src/GameLogic/NPC/AttackableNpcBase.cs index 3c5d186e4..b8423da73 100644 --- a/src/GameLogic/NPC/AttackableNpcBase.cs +++ b/src/GameLogic/NPC/AttackableNpcBase.cs @@ -373,12 +373,39 @@ private async ValueTask HandleMoneyDropAsync(uint amount, Player killer) return; } + var partySize = killer.Party?.PartyList + .OfType() + .Count(p => p.CurrentMap == killer.CurrentMap && !p.IsAtSafezone() && p.Attributes is { }) ?? 1; + if (partySize > 1) + { + amount /= (uint)partySize; + } + var droppedMoney = new DroppedMoney((uint)(amount * killer.Attributes![Stats.MoneyAmountRate]), this.Position, this.CurrentMap); await this.CurrentMap.AddAsync(droppedMoney).ConfigureAwait(false); } private async ValueTask DropItemAsync(int exp, Player killer) { + // When the killer is in a party, DistributeExperienceAfterKillAsync returns a + // total party experience that does NOT include game rate (ExperienceRate) or + // personal experience rate multipliers. Since the money drop amount is + // derived from this experience value, party money drops were dramatically + // lower than solo drops. We recalculate the experience for money purposes + // using the solo formula so money is consistent regardless of party state. + if (killer.Party is not null && killer.SelectedCharacter?.CharacterClass is { } characterClass) + { + var baseExp = this.CalculateBaseExperience(killer.Attributes![Stats.TotalLevel]); + var isMaster = characterClass.IsMasterClass + && (short)killer.Attributes[Stats.Level] == killer.GameContext.Configuration.MaximumLevel; + var expRateAttr = isMaster ? Stats.MasterExperienceRate : Stats.ExperienceRate; + var gameRate = isMaster ? killer.GameContext.MasterExperienceRate : killer.GameContext.ExperienceRate; + + var experience = baseExp * gameRate * (killer.Attributes[expRateAttr] + killer.Attributes[Stats.BonusExperienceRate]); + experience *= killer.CurrentMap?.Definition.ExpMultiplier ?? 1; + exp = Rand.NextInt((int)(experience * 0.8), (int)(experience * 1.2)); + } + var (generatedItems, droppedMoney) = await this._dropGenerator.GenerateItemDropsAsync(this.Definition, exp, killer).ConfigureAwait(false); if (droppedMoney > 0) { From 8f4ae4f4c14e6c937c1e4a9ef608402cc9f61637 Mon Sep 17 00:00:00 2001 From: nolt Date: Sun, 17 May 2026 21:28:49 +0200 Subject: [PATCH 2/2] additional fix to zen drop --- src/GameLogic/DroppedMoney.cs | 44 +++++++++++++---------- src/GameLogic/NPC/AttackableNpcBase.cs | 22 ++---------- src/GameLogic/Player.cs | 50 +++++++++++++++++++------- 3 files changed, 66 insertions(+), 50 deletions(-) diff --git a/src/GameLogic/DroppedMoney.cs b/src/GameLogic/DroppedMoney.cs index bbe2ec0e0..7285d6ad3 100644 --- a/src/GameLogic/DroppedMoney.cs +++ b/src/GameLogic/DroppedMoney.cs @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.GameLogic; using System.Diagnostics; using System.Threading; +using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.Pathfinding; using Nito.AsyncEx; @@ -67,8 +68,7 @@ public DroppedMoney(uint amount, Point position, GameMap map) public async ValueTask TryPickUpByAsync(Player player) { player.Logger.LogDebug("Player {0} tries to pick up {1}", player, this); - int amountToAdd = 0; - var clampMoneyOnPickup = player.GameContext?.Configuration?.ClampMoneyOnPickup ?? false; + using (await this._pickupLock.LockAsync()) { if (!this._availableToPick) @@ -77,12 +77,33 @@ public async ValueTask TryPickUpByAsync(Player player) return false; } + this._availableToPick = false; + } + + if (player.Party is { } party) + { + var partyMembers = party.PartyList + .OfType() + .Where(p => p.CurrentMap == player.CurrentMap && !p.IsAtSafezone() && p.Attributes is { }) + .ToList(); + + if (partyMembers.Count > 0) + { + var share = (int)(this.Amount / partyMembers.Count); + foreach (var member in partyMembers) + { + member.TryAddMoney((int)(share * member.Attributes![Stats.MoneyAmountRate])); + } + } + } + else + { + var clampMoneyOnPickup = player.GameContext?.Configuration?.ClampMoneyOnPickup ?? false; if (clampMoneyOnPickup) { - // Calculate how much can actually be added (clamp to max) var maxMoney = player.GameContext?.Configuration?.MaximumInventoryMoney ?? int.MaxValue; var currentMoney = player.Money; - amountToAdd = (int)Math.Min(this.Amount, (uint)Math.Max(0, maxMoney - currentMoney)); + var amountToAdd = (int)Math.Min(this.Amount, (uint)Math.Max(0, maxMoney - currentMoney)); if (amountToAdd <= 0) { @@ -90,7 +111,6 @@ public async ValueTask TryPickUpByAsync(Player player) return false; } - // Add the clamped amount if (!player.TryAddMoney(amountToAdd)) { player.Logger.LogDebug("Money could not be added to the inventory, Player {0}, Money {1}", player, this); @@ -99,26 +119,12 @@ public async ValueTask TryPickUpByAsync(Player player) } else { - // Original behavior: fail if it would exceed the maximum if (!player.TryAddMoney((int)this.Amount)) { player.Logger.LogDebug("Money could not be added to the inventory, Player {0}, Money {1}", player, this); return false; } - - amountToAdd = (int)this.Amount; } - - this._availableToPick = false; - } - - if (clampMoneyOnPickup && amountToAdd < this.Amount) - { - player.Logger.LogDebug("Money '{0}' was partially picked up by player '{1}' - added {2} out of {3} (player at max limit).", this, player, amountToAdd, this.Amount); - } - else - { - player.Logger.LogDebug("Money '{0}' was picked up by player '{1}' and added to his inventory.", this, player); } await this.DisposeAsync().ConfigureAwait(false); diff --git a/src/GameLogic/NPC/AttackableNpcBase.cs b/src/GameLogic/NPC/AttackableNpcBase.cs index b8423da73..9cc9a453a 100644 --- a/src/GameLogic/NPC/AttackableNpcBase.cs +++ b/src/GameLogic/NPC/AttackableNpcBase.cs @@ -373,15 +373,7 @@ private async ValueTask HandleMoneyDropAsync(uint amount, Player killer) return; } - var partySize = killer.Party?.PartyList - .OfType() - .Count(p => p.CurrentMap == killer.CurrentMap && !p.IsAtSafezone() && p.Attributes is { }) ?? 1; - if (partySize > 1) - { - amount /= (uint)partySize; - } - - var droppedMoney = new DroppedMoney((uint)(amount * killer.Attributes![Stats.MoneyAmountRate]), this.Position, this.CurrentMap); + var droppedMoney = new DroppedMoney((uint)(amount * (killer.Attributes?[Stats.MoneyAmountRate] ?? 1.0f)), this.Position, this.CurrentMap); await this.CurrentMap.AddAsync(droppedMoney).ConfigureAwait(false); } @@ -393,17 +385,9 @@ private async ValueTask DropItemAsync(int exp, Player killer) // derived from this experience value, party money drops were dramatically // lower than solo drops. We recalculate the experience for money purposes // using the solo formula so money is consistent regardless of party state. - if (killer.Party is not null && killer.SelectedCharacter?.CharacterClass is { } characterClass) + if (killer.Party is not null) { - var baseExp = this.CalculateBaseExperience(killer.Attributes![Stats.TotalLevel]); - var isMaster = characterClass.IsMasterClass - && (short)killer.Attributes[Stats.Level] == killer.GameContext.Configuration.MaximumLevel; - var expRateAttr = isMaster ? Stats.MasterExperienceRate : Stats.ExperienceRate; - var gameRate = isMaster ? killer.GameContext.MasterExperienceRate : killer.GameContext.ExperienceRate; - - var experience = baseExp * gameRate * (killer.Attributes[expRateAttr] + killer.Attributes[Stats.BonusExperienceRate]); - experience *= killer.CurrentMap?.Definition.ExpMultiplier ?? 1; - exp = Rand.NextInt((int)(experience * 0.8), (int)(experience * 1.2)); + exp = killer.CalculateExpAfterKill(this); } var (generatedItems, droppedMoney) = await this._dropGenerator.GenerateItemDropsAsync(this.Definition, exp, killer).ConfigureAwait(false); diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index ed06901e0..ddc86cfd5 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -1184,29 +1184,55 @@ public async ValueTask AddExpAfterKillAsync(IAttackable killedObject) return 0; } + var experience = this.CalculateExpAfterKill(killedObject); + if (experience == 0) + { + return 0; + } + var addMasterExperience = characterClass.IsMasterClass && (short)this.Attributes![Stats.Level] == this.GameContext.Configuration.MaximumLevel; - var expRateAttribute = addMasterExperience ? Stats.MasterExperienceRate : Stats.ExperienceRate; - var gameRate = addMasterExperience ? this.GameContext.MasterExperienceRate : this.GameContext.ExperienceRate; - - var experience = killedObject.CalculateBaseExperience(this.Attributes![Stats.TotalLevel]); - experience *= gameRate; - experience *= this.Attributes[expRateAttribute] + this.Attributes[Stats.BonusExperienceRate]; - experience *= this.CurrentMap?.Definition.ExpMultiplier ?? 1; - experience = Rand.NextInt((int)(experience * 0.8), (int)(experience * 1.2)); - if (addMasterExperience) { - await this.AddMasterExperienceAsync((int)experience, killedObject).ConfigureAwait(false); + await this.AddMasterExperienceAsync(experience, killedObject).ConfigureAwait(false); } else { - await this.AddExperienceAsync((int)experience, killedObject).ConfigureAwait(false); + await this.AddExperienceAsync(experience, killedObject).ConfigureAwait(false); } await this.AddPetExperienceAsync(experience).ConfigureAwait(false); - return (int)experience; + return experience; + } + + /// + /// Calculates the amount of experience gained after a kill, without applying it to the character. + /// + /// The killed monster. + /// The calculated experience amount. + public int CalculateExpAfterKill(IAttackable killedObject) + { + if (this.SelectedCharacter?.CharacterClass is not { } characterClass) + { + return 0; + } + + if (this.Attributes is not { } attributes) + { + return 0; + } + + var addMasterExperience = characterClass.IsMasterClass + && (short)attributes[Stats.Level] == this.GameContext.Configuration.MaximumLevel; + var expRateAttribute = addMasterExperience ? Stats.MasterExperienceRate : Stats.ExperienceRate; + var gameRate = addMasterExperience ? this.GameContext.MasterExperienceRate : this.GameContext.ExperienceRate; + + var experience = killedObject.CalculateBaseExperience(attributes[Stats.TotalLevel]); + experience *= gameRate; + experience *= attributes[expRateAttribute] + attributes[Stats.BonusExperienceRate]; + experience *= this.CurrentMap?.Definition.ExpMultiplier ?? 1; + return Rand.NextInt((int)(experience * 0.8), (int)(experience * 1.2)); } ///