baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
// <copyright file="AttackerSurrogate.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.Pet;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// A surrogate for an <see cref="Player"/> which exposes the attack attributes
/// of another <see cref="IAttributeSystem"/>.
/// </summary>
public class AttackerSurrogate : IAttacker, IWorldObserver, IPlayerSurrogate
{
/// <summary>
/// Initializes a new instance of the <see cref="AttackerSurrogate" /> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="attributeSystem">The attribute system.</param>
public AttackerSurrogate(Player owner, IAttributeSystem attributeSystem)
{
this.Owner = owner;
this.Attributes = attributeSystem;
}
/// <inheritdoc />
public Player Owner { get; }
/// <inheritdoc/>
/// <remarks>
/// Pets don't use "skills", so it doesn't make sense to pass the
/// combo state of the player here.
/// </remarks>
public ComboStateMachine? ComboState => null;
/// <inheritdoc />
public ushort Id => this.Owner.Id;
/// <inheritdoc />
public GameMap? CurrentMap => this.Owner.CurrentMap;
/// <inheritdoc />
public Point Position
{
get => this.Owner.Position;
set => this.Owner.Position = value;
}
/// <inheritdoc />
public IAttributeSystem Attributes { get; }
/// <inheritdoc />
public ILogger Logger => this.Owner.Logger;
/// <inheritdoc />
public ICustomPlugInContainer<IViewPlugIn> ViewPlugIns => this.Owner.ViewPlugIns;
}

View File

@@ -0,0 +1,20 @@
// <copyright file="IPetCommandManager.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.Pet;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
/// <summary>
/// Interface for a pet command manager.
/// </summary>
public interface IPetCommandManager : IDisposable
{
/// <summary>
/// Sets the behaviour of the pet.
/// </summary>
/// <param name="newBehaviour">The new behaviour.</param>
/// <param name="target">The target.</param>
ValueTask SetBehaviourAsync(PetBehaviour newBehaviour, IAttackable? target);
}

View File

@@ -0,0 +1,57 @@
// <copyright file="PetLevelHelper.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.Pet;
using System.Collections.Concurrent;
using MUnique.OpenMU.DataModel.Configuration.Items;
using org.mariuszgromada.math.mxparser;
/// <summary>
/// Helper class for pet related stuff.
/// </summary>
public static class PetLevelHelper
{
private static readonly ConcurrentDictionary<(string, byte), uint[]> ValueResultCache = new();
/// <summary>
/// Gets the experience of level.
/// </summary>
/// <param name="petDefinition">The pet definition.</param>
/// <param name="petLevel">The pet level.</param>
/// <param name="maximumLevel">The maximum level.</param>
/// <returns>The calculated experience of the specified pet level.</returns>
public static uint GetExperienceOfPetLevel(this ItemDefinition petDefinition, byte petLevel, byte maximumLevel)
{
if (petLevel <= 0 || petLevel > maximumLevel)
{
return uint.MaxValue;
}
var formula = petDefinition.PetExperienceFormula;
if (formula is null)
{
return uint.MaxValue;
}
if (ValueResultCache.TryGetValue((formula, maximumLevel), out var results))
{
return results[petLevel - 1];
}
results = new uint[maximumLevel];
var argument = new Argument("level", 0);
var expression = new Expression(formula);
expression.addArguments(argument);
for (int i = 0; i < maximumLevel; i++)
{
argument.setArgumentValue(i + 1);
results[i] = (uint)expression.calculate();
}
ValueResultCache.TryAdd((formula, maximumLevel), results);
return results[petLevel - 1];
}
}

View File

@@ -0,0 +1,98 @@
// <copyright file="RavenAttributeSystem.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.Pet;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Attributes;
/// <summary>
/// An <see cref="IAttributeSystem"/> which provides the attack attributes of the raven.
/// </summary>
public class RavenAttributeSystem : IAttributeSystem
{
private static readonly Dictionary<AttributeDefinition, Func<Player, float>> StatMapping =
new()
{
{
Stats.MinimumPhysBaseDmg, player =>
{
var minDamage = player.Attributes?[Stats.RavenMinimumDamage] ?? 0;
minDamage += minDamage * (player.Attributes?[Stats.RavenAttackDamageIncrease] ?? 1);
return minDamage;
}
},
{
Stats.MaximumPhysBaseDmg, player =>
{
var maxDamage = player.Attributes?[Stats.RavenMaximumDamage] ?? 0;
maxDamage += maxDamage * (player.Attributes?[Stats.RavenAttackDamageIncrease] ?? 1);
return maxDamage;
}
},
{ Stats.RavenBonusDamage, player => player.Attributes?[Stats.RavenBonusDamage] ?? 0 },
{ Stats.AttackSpeed, player => player.Attributes?[Stats.RavenAttackSpeed] ?? 0 },
{ Stats.AttackRatePvm, player => player.Attributes?[Stats.RavenAttackRate] ?? 0 },
{ Stats.AttackRatePvp, player => player.Attributes?[Stats.AttackRatePvp] ?? 0 },
{ Stats.CriticalDamageChance, player => player.Attributes?[Stats.RavenCriticalDamageChance] ?? 0 },
{ Stats.ExcellentDamageChance, player => player.Attributes?[Stats.RavenExcDamageChance] ?? 0 },
{ Stats.DefenseIgnoreChance, player => player.Attributes?[Stats.DefenseIgnoreChance] ?? 0 },
{ Stats.ShieldBypassChance, player => player.Attributes?[Stats.ShieldBypassChance] ?? 0 },
{ Stats.ShieldDecreaseRateIncrease, player => player.Attributes?[Stats.ShieldDecreaseRateIncrease] ?? 0 },
{ Stats.AttackDamageIncrease, player => 1.0f },
};
private readonly Player _owner;
/// <summary>
/// Initializes a new instance of the <see cref="RavenAttributeSystem"/> class.
/// </summary>
/// <param name="owner">The owner of the pet.</param>
public RavenAttributeSystem(Player owner)
{
this._owner = owner;
}
/// <inheritdoc />
public float this[AttributeDefinition key]
{
get => this.GetValueOfAttribute(key);
set => throw new NotImplementedException();
}
/// <inheritdoc />
public float GetValueOfAttribute(AttributeDefinition attributeDefinition)
{
if (StatMapping.TryGetValue(attributeDefinition, out var mappingFunction))
{
return mappingFunction(this._owner);
}
return 0;
}
/// <inheritdoc />
public void AddElement(IElement element, AttributeDefinition targetAttribute)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public void RemoveElement(IElement element, AttributeDefinition targetAttribute)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public void AddAttributeRelationship(AttributeRelationship relationship, IAttributeSystem sourceAttributeHolder, AggregateType aggregateType)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public IElement GetOrCreateAttribute(AttributeDefinition attributeDefinition)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,300 @@
// <copyright file="RavenCommandManager.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.Pet;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using MUnique.OpenMU.GameLogic.Views.Pet;
using Nito.AsyncEx;
/// <summary>
/// Implementation of the <see cref="IPetCommandManager"/> for the dark raven pet.
/// </summary>
public class RavenCommandManager : Disposable, IPetCommandManager
{
private const int AttackRange = 7;
private const int MaxRangeHits = 3;
private readonly Player _owner;
private readonly Item _pet;
private readonly IAttacker _petAttackerSurrogate;
private readonly List<IAttackable> _targetBuffer = new(MaxRangeHits);
private readonly AsyncLock _rangeAttackLock = new();
private CancellationTokenSource? _attackCts;
private PetBehaviour _currentBehaviour;
/// <summary>
/// Initializes a new instance of the <see cref="RavenCommandManager" /> class.
/// </summary>
/// <param name="owner">The owner of the pet.</param>
/// <param name="pet">The pet.</param>
public RavenCommandManager(Player owner, Item pet)
{
this._owner = owner;
this._pet = pet;
this._petAttackerSurrogate = new AttackerSurrogate(owner, new RavenAttributeSystem(owner));
}
private TimeSpan AttackDelay => TimeSpan.FromMilliseconds(Math.Max(100, 1500 - (this._petAttackerSurrogate.Attributes[Stats.AttackSpeed] * 10)));
/// <summary>
/// Sets the behaviour.
/// </summary>
/// <param name="newBehaviour">The new behaviour.</param>
/// <param name="target">The target.</param>
public async ValueTask SetBehaviourAsync(PetBehaviour newBehaviour, IAttackable? target)
{
await (this._attackCts?.CancelAsync() ?? Task.CompletedTask).ConfigureAwait(false);
this._attackCts?.Dispose();
this._attackCts = null;
if (this._pet.Durability == 0.0)
{
this._currentBehaviour = PetBehaviour.Idle;
}
this._currentBehaviour = newBehaviour;
await this._owner.InvokeViewPlugInAsync<IPetBehaviourChangedViewPlugIn>(p => p.PetBehaviourChangedAsync(this._pet, this._currentBehaviour, target)).ConfigureAwait(false);
if (newBehaviour == PetBehaviour.Idle)
{
return;
}
this._attackCts = new();
switch (newBehaviour)
{
case PetBehaviour.AttackRandom:
_ = this.AttackRandomAsync(this._attackCts.Token);
break;
case PetBehaviour.AttackTarget when target is { }:
_ = this.AttackTargetUntilDeathAsync(target, this._attackCts.Token);
break;
case PetBehaviour.AttackWithOwner:
_ = this.AttackSameAsOwnerAsync(this._attackCts.Token);
break;
default:
throw new ArgumentOutOfRangeException(nameof(newBehaviour), $"Unknown behavior: {newBehaviour}");
}
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._attackCts?.Cancel();
this._attackCts?.Dispose();
this._attackCts = null;
}
private async ValueTask AttackAsync(IAttackable target, CancellationToken cancellationToken)
{
if (this._owner.Logger.IsEnabled(LogLevel.Debug))
{
this._owner.Logger.LogDebug($"Pet attacks target: {target}");
}
if (this._owner.IsAtSafezone())
{
return;
}
var attackType = Rand.NextRandomBool(0.3) ? PetAttackType.RangeAttack : PetAttackType.SingleTarget;
await this._owner.ForEachWorldObserverAsync<IPetAttackViewPlugIn>(
p => p.ShowPetAttackAnimationAsync(this._owner, this._pet, target, attackType),
true)
.ConfigureAwait(false);
if (attackType == PetAttackType.SingleTarget)
{
await target.AttackByAsync(this._petAttackerSurrogate, null, false).ConfigureAwait(false);
}
else
{
_ = this.RangeAttackAsync(target, cancellationToken);
}
}
/// <summary>
/// Does a range attack on the main target and other targets in the same area, with up to 3 hits.
/// </summary>
/// <param name="mainTarget">The main target which was selected to be attacked.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private async ValueTask RangeAttackAsync(IAttackable mainTarget, CancellationToken cancellationToken)
{
try
{
using var l = await this._rangeAttackLock.LockAsync(cancellationToken);
var delay = this.AttackDelay / 4;
this._targetBuffer.Clear();
this._targetBuffer.Add(mainTarget);
var targets = mainTarget
.CurrentMap!
.GetAttackablesInRange(mainTarget.Position, 3)
.OfType<AttackableNpcBase>()
.Where(t => t != mainTarget)
.Where(this.IsValidTarget)
.Take(2);
this._targetBuffer.AddRange(targets);
for (int i = 0; i < 3; i++)
{
var target = this._targetBuffer[i % this._targetBuffer.Count];
await this._owner.ForEachWorldObserverAsync<IPetAttackViewPlugIn>(
p => p.ShowPetAttackAnimationAsync(this._owner, this._pet, target, PetAttackType.RangeAttack),
true)
.ConfigureAwait(false);
await target.AttackByAsync(this._petAttackerSurrogate, null, false).ConfigureAwait(false);
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
// We can ignore that.
}
catch (Exception ex)
{
this._owner.Logger.LogError(ex, "Unexpected error in range attack of the pet.");
}
}
private async Task AttackTargetUntilDeathAsync(IAttackable target, CancellationToken cancellationToken)
{
this._owner.Logger.LogDebug($"Starting to attack {target} with the pet, until it's dead.");
try
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
if (!target.IsAlive)
{
break;
}
if (!this._owner.IsAtSafezone() && this.IsValidTarget(target))
{
await this.AttackAsync(target, cancellationToken).ConfigureAwait(false);
}
await Task.Delay(this.AttackDelay, cancellationToken).ConfigureAwait(false);
}
this._currentBehaviour = PetBehaviour.Idle;
await this._owner.InvokeViewPlugInAsync<IPetBehaviourChangedViewPlugIn>(p => p.PetBehaviourChangedAsync(this._pet, this._currentBehaviour, null)).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
this._owner.Logger.LogDebug("Cancelled attack with the pet.");
}
catch (Exception ex)
{
this._owner.Logger.LogError(ex, "Unexpected error in attack loop of the pet.");
}
this._owner.Logger.LogDebug("Ending attack with the pet.");
}
private async Task AttackRandomAsync(CancellationToken cancellationToken)
{
this._owner.Logger.LogDebug("Starting random attack with the pet.");
try
{
IAttackable? currentTarget = null;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
if (!this._owner.IsAtSafezone())
{
if (!this.IsValidTarget(currentTarget))
{
var attackablesInRange = this._owner.CurrentMap?
.GetAttackablesInRange(this._owner.Position, AttackRange)
.OfType<AttackableNpcBase>()
.Where(this.IsValidTarget);
currentTarget = attackablesInRange?.SelectRandom();
if (this._owner.Logger.IsEnabled(LogLevel.Debug))
{
this._owner.Logger.LogDebug($"Pet selected target: {currentTarget}");
}
}
if (currentTarget is { IsAlive: true })
{
await this.AttackAsync(currentTarget, cancellationToken).ConfigureAwait(false);
}
}
await Task.Delay(this.AttackDelay, cancellationToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
this._owner.Logger.LogDebug("Cancelled random attack with the pet.");
}
catch (Exception ex)
{
this._owner.Logger.LogError(ex, "Unexpected error in random attack loop of the pet.");
}
this._owner.Logger.LogDebug("Ending random attack with the pet.");
}
private async Task AttackSameAsOwnerAsync(CancellationToken cancellationToken)
{
this._owner.Logger.LogDebug("Starting attack with the pet.");
try
{
IAttackable? currentTarget = null;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
if (this._owner.LastAttackedTarget.TryGetTarget(out var lastTarget)
&& lastTarget != currentTarget)
{
currentTarget = lastTarget;
if (this._owner.Logger.IsEnabled(LogLevel.Debug))
{
this._owner.Logger.LogDebug($"Pet selected last target of player: {currentTarget}");
}
}
if (this.IsValidTarget(currentTarget))
{
await this.AttackAsync(currentTarget, cancellationToken).ConfigureAwait(false);
}
await Task.Delay(this.AttackDelay, cancellationToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
this._owner.Logger.LogDebug("Cancelled attack with the pet.");
}
catch (Exception ex)
{
this._owner.Logger.LogError(ex, "Unexpected error in attack loop of the pet.");
}
this._owner.Logger.LogDebug("Ending attack with the pet.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsValidTarget([NotNullWhen(true)] IAttackable? target)
{
return target is not null
&& target is not Monster { Definition.ObjectKind: NpcObjectKind.Guard }
&& target.IsActive()
&& target.IsInRange(this._owner.Position, AttackRange);
}
}

View File

@@ -0,0 +1,44 @@
// <copyright file="UpdatePetCommandManagerOnItemMovePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.Pet;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Updates the pet command manager on the player object after a pet has been equipped or unequipped.
/// </summary>
[Guid("E25CBAE0-CFD2-4BAF-9178-4FBD90F26E4B")]
[PlugIn]
[Display(Name = nameof(PlugInResources.UpdatePetCommandManagerOnItemMovePlugIn_Name), Description = nameof(PlugInResources.UpdatePetCommandManagerOnItemMovePlugIn_Description), ResourceType = typeof(PlugInResources))]
public class UpdatePetCommandManagerOnItemMovePlugIn : IItemMovedPlugIn
{
/// <inheritdoc />
public async ValueTask ItemMovedAsync(Player player, Item item)
{
if (player.Inventory is null)
{
return;
}
var isAttackPet = item.IsTrainablePet() && !item.IsDefensiveItem();
if (!isAttackPet)
{
return;
}
var itemAdded = player.Inventory.EquippedItems.Contains(item);
if (itemAdded && player.PetCommandManager is { } petCommandManager)
{
await petCommandManager.SetBehaviourAsync(PetBehaviour.Idle, null).ConfigureAwait(false);
}
else
{
player.RemovePetCommandManager();
}
}
}