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,77 @@
// <copyright file="BuffHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using NUnit.Framework;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
/// <summary>
/// Tests for <see cref="BuffHandler"/>.
/// </summary>
[TestFixture]
public class BuffHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="BuffHandler.PerformBuffsAsync"/> returns true immediately
/// when no buff skills are configured.
/// </summary>
[Test]
public async ValueTask ReturnsTrueWhenNoBuffsConfiguredAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var config = new MuHelperSettings
{
BuffSkill0Id = 0,
BuffSkill1Id = 0,
BuffSkill2Id = 0,
};
var handler = new BuffHandler(player, config);
// Act
var result = await handler.PerformBuffsAsync().ConfigureAwait(false);
// Assert
Assert.That(result, Is.True);
}
/// <summary>
/// Tests that <see cref="BuffHandler.PerformBuffsAsync"/> returns true when config is null.
/// </summary>
[Test]
public async ValueTask ReturnsTrueWhenConfigNullAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var handler = new BuffHandler(player, null);
// Act
var result = await handler.PerformBuffsAsync().ConfigureAwait(false);
// Assert
Assert.That(result, Is.True);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,160 @@
// <copyright file="CombatHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using Moq;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
using MUnique.OpenMU.Pathfinding;
using MonsterDefinition = MUnique.OpenMU.Persistence.BasicModel.MonsterDefinition;
using MonsterAttribute = MUnique.OpenMU.Persistence.BasicModel.MonsterAttribute;
/// <summary>
/// Tests for <see cref="CombatHandler"/>.
/// </summary>
[TestFixture]
public class CombatHandlerTests
{
private IGameContext _gameContext = null!;
private readonly Point _origin = new(100, 100);
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="CombatHandler.PerformAttackAsync"/> moves closer to the target if out of range.
/// </summary>
[Test]
public async ValueTask PerformAttackAsync_MovesCloserToTargetAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.Position = this._origin;
var monster = await this.CreateMonsterAsync(new Point(105, 105)).ConfigureAwait(false);
await player.CurrentMap!.AddAsync(monster).ConfigureAwait(false);
var config = new MuHelperSettings { HuntingRange = 10 };
var movementHandler = new MovementHandler(player, config, this._origin);
var handler = new CombatHandler(player, config, movementHandler, this._origin);
// Act
await handler.PerformAttackAsync().ConfigureAwait(false);
// Assert
// Should have initiated a walk closer to the monster
Assert.That(player.IsWalking, Is.True);
}
/// <summary>
/// Tests that <see cref="CombatHandler.PerformDrainLifeRecoveryAsync"/> uses Drain Life when HP is low.
/// </summary>
[Test]
public async ValueTask PerformDrainLifeRecoveryAsync_UsesDrainLifeWhenLowHpAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.Position = this._origin;
player.Attributes![Stats.CurrentHealth] = 10; // Low HP
player.Attributes[Stats.MaximumHealth] = 100;
// Place monster slightly away from player so GetDirectionTo doesn't return Undefined
var monsterPosition = new Point((byte)(this._origin.X + 1), this._origin.Y);
var monster = await this.CreateMonsterAsync(monsterPosition).ConfigureAwait(false);
await player.CurrentMap!.AddAsync(monster).ConfigureAwait(false);
var config = new MuHelperSettings
{
UseDrainLife = true,
HealThresholdPercent = 50,
HuntingRange = 10
};
// Add Drain Life skill to player
var drainSkill = new TestSkill
{
Number = 214,
};
await player.SkillList!.AddLearnedSkillAsync(drainSkill).ConfigureAwait(false);
var movementHandler = new MovementHandler(player, config, this._origin);
var handler = new CombatHandler(player, config, movementHandler, this._origin);
// Act
await handler.PerformDrainLifeRecoveryAsync().ConfigureAwait(false);
// Assert
// We verify that rotation was updated, which happens during ExecuteAttackAsync
Assert.That(player.Rotation, Is.Not.EqualTo(default(Direction)));
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
private async ValueTask<Monster> CreateMonsterAsync(Point position)
{
var monsterDefinition = new MonsterDefinition
{
ObjectKind = NpcObjectKind.Monster,
};
monsterDefinition.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.MaximumHealth, Value = 1000 });
monsterDefinition.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.DefenseBase, Value = 100 });
var map = await this._gameContext.GetMapAsync(0).ConfigureAwait(false)!;
var spawnArea = new MonsterSpawnArea
{
MonsterDefinition = monsterDefinition,
GameMap = map!.Definition,
X1 = position.X,
Y1 = position.Y,
X2 = position.X,
Y2 = position.Y,
Quantity = 1,
};
var monster = new Monster(
spawnArea,
monsterDefinition,
map,
NullDropGenerator.Instance,
new Mock<INpcIntelligence>().Object,
this._gameContext.PlugInManager,
this._gameContext.PathFinderPool);
monster.Initialize();
monster.Attributes[Stats.CurrentHealth] = 100;
return monster;
}
private class TestSkill : Skill
{
public TestSkill()
{
this.Requirements = new List<AttributeRequirement>();
this.ConsumeRequirements = new List<AttributeRequirement>();
this.Range = 1;
this.SkillType = SkillType.DirectHit;
this.DamageType = DamageType.Curse;
}
}
}

View File

@@ -0,0 +1,103 @@
// <copyright file="HealingHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using Moq;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
/// <summary>
/// Tests for <see cref="HealingHandler"/>.
/// </summary>
[TestFixture]
public class HealingHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="HealingHandler.PerformHealthRecoveryAsync"/> does nothing
/// when config is null.
/// </summary>
[Test]
public async ValueTask DoesNothingWhenConfigNullAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var handler = new HealingHandler(player, null);
// Act
await handler.PerformHealthRecoveryAsync().ConfigureAwait(false);
}
/// <summary>
/// Tests that <see cref="HealingHandler.PerformHealthRecoveryAsync"/> does not consume
/// a potion when the player's HP is above the threshold.
/// </summary>
[Test]
public async ValueTask DoesNotUsePotionAboveThresholdAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var maxHp = player.Attributes![Stats.MaximumHealth];
player.Attributes[Stats.CurrentHealth] = maxHp * 0.9f;
var potion = this.CreateHealthPotion();
await player.Inventory!.AddItemAsync((byte)(InventoryConstants.FirstEquippableItemSlotIndex + 12), potion).ConfigureAwait(false);
var config = new MuHelperSettings
{
UseHealPotion = true,
PotionThresholdPercent = 50,
};
var handler = new HealingHandler(player, config);
// Act
await handler.PerformHealthRecoveryAsync().ConfigureAwait(false);
// Assert
Assert.That(player.Inventory?.GetItem(potion.ItemSlot), Is.Not.Null);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
private MUnique.OpenMU.DataModel.Entities.Item CreateHealthPotion()
{
var definition = new Mock<MUnique.OpenMU.DataModel.Configuration.Items.ItemDefinition>();
definition.SetupAllProperties();
definition.Setup(d => d.BasePowerUpAttributes).Returns(new List<ItemBasePowerUpDefinition>());
definition.Setup(d => d.PossibleItemOptions).Returns(new List<ItemOptionDefinition>());
definition.Object.Number = ItemConstants.SmallHealingPotion.Number!.Value;
definition.Object.Group = ItemConstants.SmallHealingPotion.Group;
var item = new Mock<MUnique.OpenMU.DataModel.Entities.Item>();
item.SetupAllProperties();
item.Setup(i => i.Definition).Returns(definition.Object);
item.Setup(i => i.ItemOptions).Returns(new List<ItemOptionLink>());
item.Setup(i => i.ItemSetGroups).Returns(new List<ItemOfItemSet>());
item.Object.Durability = 1;
return item.Object;
}
}

View File

@@ -0,0 +1,134 @@
// <copyright file="ItemPickupHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using Moq;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
/// <summary>
/// Tests for <see cref="ItemPickupHandler"/>.
/// </summary>
[TestFixture]
public class ItemPickupHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that pickup does nothing when all pickup options are disabled.
/// </summary>
[Test]
public async ValueTask DoesNothingWhenAllDisabledAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var config = new MuHelperSettings
{
PickAllItems = false,
PickJewel = false,
PickAncient = false,
PickZen = false,
PickExcellent = false,
};
var handler = new ItemPickupHandler(player, config);
// Act
await handler.PickupItemsAsync().ConfigureAwait(false);
}
/// <summary>
/// Tests that pickup does nothing when config is null.
/// </summary>
[Test]
public async ValueTask DoesNothingWhenConfigNullAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var handler = new ItemPickupHandler(player, null);
// Act
await handler.PickupItemsAsync().ConfigureAwait(false);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
private Item CreateItem(byte group, short number)
{
var definition = new Mock<ItemDefinition>();
definition.SetupAllProperties();
definition.Object.Group = group;
definition.Object.Number = number;
definition.Object.Width = 1;
definition.Object.Height = 1;
var item = new Mock<Item>();
item.SetupAllProperties();
item.Setup(i => i.Definition).Returns(definition.Object);
item.Setup(i => i.ItemOptions).Returns(new List<ItemOptionLink>());
item.Setup(i => i.ItemSetGroups).Returns(new List<ItemOfItemSet>());
return item.Object;
}
/// <summary>
/// Tests that a jewel pickup option only picks up actual jewels.
/// </summary>
[Test]
public async ValueTask PickJewel_PicksUpJewelsOnlyAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
Assert.That(player.CurrentMap, Is.Not.Null);
var config = new MuHelperSettings
{
PickSelectItems = true,
PickJewel = true,
ObtainRange = 5,
};
var handler = new ItemPickupHandler(player, config);
var bless = this.CreateItem(14, 13);
var chaos = this.CreateItem(12, 15);
var potion = this.CreateItem(14, 0);
var eye = this.CreateItem(14, 17);
var blessDrop = new DroppedItem(bless, player.Position, player.CurrentMap!, player);
var chaosDrop = new DroppedItem(chaos, player.Position, player.CurrentMap!, player);
var potionDrop = new DroppedItem(potion, player.Position, player.CurrentMap!, player);
var eyeDrop = new DroppedItem(eye, player.Position, player.CurrentMap!, player);
await player.CurrentMap!.AddAsync(blessDrop).ConfigureAwait(false);
await player.CurrentMap.AddAsync(chaosDrop).ConfigureAwait(false);
await player.CurrentMap.AddAsync(potionDrop).ConfigureAwait(false);
await player.CurrentMap.AddAsync(eyeDrop).ConfigureAwait(false);
// Act
await handler.PickupItemsAsync().ConfigureAwait(false);
// Assert
Assert.That(player.CurrentMap.GetObject(blessDrop.Id), Is.Null, "Bless should be picked up");
Assert.That(player.CurrentMap.GetObject(chaosDrop.Id), Is.Null, "Chaos should be picked up");
Assert.That(player.CurrentMap.GetObject(potionDrop.Id), Is.Not.Null, "Potion should NOT be picked up");
Assert.That(player.CurrentMap.GetObject(eyeDrop.Id), Is.Not.Null, "Devil's Eye should NOT be picked up");
}
}

View File

@@ -0,0 +1,61 @@
// <copyright file="MovementHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
using MUnique.OpenMU.Pathfinding;
/// <summary>
/// Tests for <see cref="MovementHandler"/>.
/// </summary>
[TestFixture]
public class MovementHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="MovementHandler.RegroupAsync"/> does nothing when within range.
/// </summary>
[Test]
public async ValueTask RegroupAsync_DoesNothingWhenWithinRangeAsync()
{
// Arrange
var origin = new Point(100, 100);
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.Position = new Point(100, 101); // Within RegroupDistanceThreshold (1)
var config = new MuHelperSettings
{
ReturnToOriginalPosition = true,
HuntingRange = 5,
};
var handler = new MovementHandler(player, config, origin);
// Act
var result = await handler.RegroupAsync().ConfigureAwait(false);
// Assert
Assert.That(result, Is.True);
Assert.That(player.IsWalking, Is.False);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,146 @@
// <copyright file="OfflinePlayerManagerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.Persistence;
/// <summary>
/// Tests for <see cref="OfflinePlayerManager"/>.
/// </summary>
[TestFixture]
public class OfflinePlayerManagerTests
{
private const string TestUserLoginName = "test";
private const string TestCharacterName = "testChar";
private IGameContext _gameContext = null!;
private IPersistenceContextProvider _contextProvider = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
this._contextProvider = this._gameContext.PersistenceContextProvider;
}
/// <summary>
/// Tests that <see cref="OfflinePlayerManager.StartAsync"/> returns true on success.
/// </summary>
[Test]
public async ValueTask StartAsync_WithValidPlayer_ReturnsTrueAsync()
{
// Arrange
var manager = new OfflinePlayerManager();
var realPlayer = await this.CreatePlayerWithPersistedAccountAsync().ConfigureAwait(false);
realPlayer.TryAddMoney(1_000_000);
realPlayer.Attributes![Stats.Level] = 100;
// Act
var result = await manager.StartAsync(realPlayer, TestUserLoginName).ConfigureAwait(false);
// Assert
Assert.That(result, Is.True);
Assert.That(manager.IsActive(TestUserLoginName), Is.True);
}
/// <summary>
/// Tests that <see cref="OfflinePlayerManager.StartAsync"/> fails if the player has insufficient Zen.
/// </summary>
[Test]
public async ValueTask StartAsync_WithInsufficientZen_ReturnsFalseAsync()
{
// Arrange
var manager = new OfflinePlayerManager();
var realPlayer = await this.CreatePlayerWithPersistedAccountAsync().ConfigureAwait(false);
realPlayer.Money = 0; // No money
realPlayer.Attributes![Stats.Level] = 100;
// Act
var result = await manager.StartAsync(realPlayer, TestUserLoginName).ConfigureAwait(false);
// Assert
Assert.That(result, Is.False);
Assert.That(manager.IsActive(TestUserLoginName), Is.False);
}
/// <summary>
/// Tests that <see cref="OfflinePlayerManager.StopAsync"/> successfully stops a session.
/// </summary>
[Test]
public async ValueTask StopAsync_WhenSessionActive_StopsSuccessfullyAsync()
{
// Arrange
var manager = new OfflinePlayerManager();
var realPlayer = await this.CreatePlayerWithPersistedAccountAsync().ConfigureAwait(false);
realPlayer.TryAddMoney(1_000_000);
realPlayer.Attributes![Stats.Level] = 100;
var started = await manager.StartAsync(realPlayer, TestUserLoginName).ConfigureAwait(false);
Assert.That(started, Is.True);
Assert.That(manager.IsActive(TestUserLoginName), Is.True);
// Act
await manager.StopAsync(TestUserLoginName).ConfigureAwait(false);
// Assert
Assert.That(manager.IsActive(TestUserLoginName), Is.False);
}
/// <summary>
/// Creates a player whose account is stored in the in-memory repository,
/// so that <see cref="OfflinePlayer.InitializeAsync"/> can find it.
/// </summary>
private async ValueTask<Player> CreatePlayerWithPersistedAccountAsync()
{
var config = this._gameContext.Configuration;
// Create persisted account + character in the in-memory repository.
using (var ctx = this._contextProvider.CreateNewPlayerContext(config))
{
var account = ctx.CreateNew<MUnique.OpenMU.DataModel.Entities.Account>();
account.LoginName = TestUserLoginName;
var character = ctx.CreateNew<MUnique.OpenMU.DataModel.Entities.Character>();
character.Name = TestCharacterName;
if (config.CharacterClasses.FirstOrDefault() is { } existingClass)
{
character.CharacterClass = existingClass;
}
else
{
// Build a minimal CharacterClass so OnPlayerEnteredWorldAsync does not throw.
var characterClass = ctx.CreateNew<MUnique.OpenMU.DataModel.Configuration.CharacterClass>();
characterClass.HomeMap = config.Maps.FirstOrDefault();
character.CharacterClass = characterClass;
}
account.Characters.Add(character);
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
var player = await PlayerTestHelper.CreatePlayerAsync(this._gameContext).ConfigureAwait(false);
player.Account!.LoginName = TestUserLoginName;
// Ensure the mock character name matches the persisted one.
var mockCharacter = player.SelectedCharacter!;
mockCharacter.Name = TestCharacterName;
mockCharacter.CharacterClass ??= player.Account.UnlockedCharacterClasses.FirstOrDefault();
if (mockCharacter.CharacterClass is null && config.CharacterClasses.FirstOrDefault() is { } cc)
{
mockCharacter.CharacterClass = cc;
}
return player;
}
}

View File

@@ -0,0 +1,64 @@
// <copyright file="OfflinePlayerMuHelperTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Offline;
/// <summary>
/// Tests for <see cref="OfflinePlayerMuHelper"/>.
/// </summary>
[TestFixture]
public class OfflinePlayerMuHelperTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that the intelligence can be created and started.
/// </summary>
[Test]
public async ValueTask StartsWithoutExceptionAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
// Act
var intelligence = new OfflinePlayerMuHelper(player);
intelligence.Start();
intelligence?.Dispose();
}
/// <summary>
/// Tests that disposing the intelligence twice does not throw.
/// </summary>
[Test]
public async ValueTask DisposeTwiceDoesNotThrowAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var intelligence = new OfflinePlayerMuHelper(player);
intelligence.Start();
// Act & Assert
intelligence.Dispose();
intelligence.Dispose();
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,63 @@
// <copyright file="OfflinePlayerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Offline;
/// <summary>
/// Tests for <see cref="OfflinePlayer"/>.
/// </summary>
[TestFixture]
public class OfflinePlayerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that the offline player is created and started successfully.
/// </summary>
[Test]
public async ValueTask InitializesSuccessfullyAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
// Assert
Assert.That(player.PlayerState.CurrentState, Is.EqualTo(PlayerState.EnteredWorld));
Assert.That(player.SelectedCharacter, Is.Not.Null);
}
/// <summary>
/// Tests that <see cref="OfflinePlayer.StopAsync"/> cleans up resources.
/// </summary>
[Test]
public async ValueTask StopAsync_CleansUpAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
// Act
await player.StopAsync().ConfigureAwait(false);
// Assert
Assert.That(player.PlayerState.CurrentState, Is.EqualTo(PlayerState.Finished));
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,117 @@
// <copyright file="PetHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using Moq;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
using MUnique.OpenMU.GameLogic.Pet;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using Item = MUnique.OpenMU.Persistence.BasicModel.Item;
using ItemDefinition = MUnique.OpenMU.Persistence.BasicModel.ItemDefinition;
using ItemSlotType = MUnique.OpenMU.Persistence.BasicModel.ItemSlotType;
/// <summary>
/// Tests for <see cref="PetHandler"/>.
/// </summary>
[TestFixture]
public class PetHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="PetHandler.InitializeAsync"/> sets the correct Dark Raven behavior.
/// </summary>
[Test]
[TestCase(1, PetBehaviour.AttackRandom)]
[TestCase(2, PetBehaviour.AttackWithOwner)]
public async ValueTask InitializeAsync_SetsDarkRavenBehaviorAsync(int mode, PetBehaviour expectedBehaviour)
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var petCommandManagerMock = new Mock<IPetCommandManager>();
var config = new MuHelperSettings
{
UseDarkRaven = true,
DarkRavenMode = (byte)mode,
};
var handler = new PetHandler(player, config, petCommandManagerMock.Object);
// Act
await handler.InitializeAsync().ConfigureAwait(false);
// Assert
petCommandManagerMock.Verify(m => m.SetBehaviourAsync(expectedBehaviour, null), Times.Once);
}
/// <summary>
/// Tests that <see cref="PetHandler.CheckPetDurabilityAsync"/> sets pet behavior to Idle when durability is 0.
/// </summary>
[Test]
public async ValueTask CheckPetDurabilityAsync_SetsIdleWhenDurabilityIsZeroAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var petCommandManagerMock = new Mock<IPetCommandManager>();
var petItem = new Item
{
Definition = new ItemDefinition
{
ItemSlot = new ItemSlotType(),
Width = 1,
Height = 1,
},
Durability = 0,
};
await player.Inventory!.AddItemAsync(InventoryConstants.PetSlot, petItem).ConfigureAwait(false);
var handler = new PetHandler(player, new MuHelperSettings(), petCommandManagerMock.Object);
// Act
await handler.CheckPetDurabilityAsync().ConfigureAwait(false);
// Assert
petCommandManagerMock.Verify(m => m.SetBehaviourAsync(PetBehaviour.Idle, null), Times.Once);
}
/// <summary>
/// Tests that <see cref="PetHandler.StopAsync"/> sets pet behavior to Idle.
/// </summary>
[Test]
public async ValueTask StopAsync_SetsIdleAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var petCommandManagerMock = new Mock<IPetCommandManager>();
var handler = new PetHandler(player, new MuHelperSettings(), petCommandManagerMock.Object);
// Act
await handler.StopAsync().ConfigureAwait(false);
// Assert
petCommandManagerMock.Verify(m => m.SetBehaviourAsync(PetBehaviour.Idle, null), Times.Once);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,221 @@
// <copyright file="RepairHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
using Item = MUnique.OpenMU.Persistence.BasicModel.Item;
using ItemDefinition = MUnique.OpenMU.Persistence.BasicModel.ItemDefinition;
using ItemSlotType = MUnique.OpenMU.Persistence.BasicModel.ItemSlotType;
/// <summary>
/// Tests for <see cref="RepairHandler"/>.
/// </summary>
[TestFixture]
public class RepairHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Items at or below the 50% durability threshold should be repaired when the player
/// has enough Zen. Uses 10% (well below threshold) to confirm the happy path.
/// </summary>
[Test]
public async ValueTask RepairsItemWhenSufficientZenAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 10);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(100));
}
/// <summary>
/// Auto-repair does nothing when disabled in the configuration, regardless of durability.
/// </summary>
[Test]
public async ValueTask DoesNothingWhenDisabledAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 10);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = false };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(10));
}
/// <summary>
/// Auto-repair does not repair when the player has insufficient Zen.
/// </summary>
[Test]
public async ValueTask DoesNotRepairWhenInsufficientZenAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 10);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(10));
}
/// <summary>
/// Fully durable items are skipped and no Zen is spent.
/// </summary>
[Test]
public async ValueTask SkipsFullyDurableItemsAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 100);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
var initialMoney = 1_000_000;
player.TryAddMoney(initialMoney);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(player.Money, Is.EqualTo(initialMoney));
}
/// <summary>
/// An item at exactly 50% durability (the threshold boundary) must be repaired.
/// Mirrors the client check: iHealth &lt;= DEFAULT_DURABILITY_THRESHOLD (50).
/// </summary>
[Test]
public async ValueTask RepairsItemAtExactlyFiftyPercentThresholdAsync()
{
// Arrange — 50 / 100 = 50% (ceiling-integer: (50*100 + 99) / 100 = 50)
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 50);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(100));
}
/// <summary>
/// An item at 51% durability is above the threshold and must NOT be repaired,
/// so no Zen is spent. Mirrors the client check: iHealth &lt;= 50.
/// </summary>
[Test]
public async ValueTask SkipsItemAboveFiftyPercentThresholdAsync()
{
// Arrange — 51 / 100 = 51% (ceiling-integer: (51*100 + 99) / 100 = 51)
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 51);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
var initialMoney = 1_000_000;
player.TryAddMoney(initialMoney);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert — durability unchanged, no money spent
Assert.That(item.Durability, Is.EqualTo(51));
Assert.That(player.Money, Is.EqualTo(initialMoney));
}
/// <summary>
/// Verifies that the ceiling-integer formula handles non-round max-durability values
/// correctly: 13 / 25 = 52% (above threshold → skip), but 12 / 25 = 48% (≤ 50% → repair).
/// </summary>
[TestCase(13, false, TestName = "NonRoundMax_52pct_Skipped")]
[TestCase(12, true, TestName = "NonRoundMax_48pct_Repaired")]
public async ValueTask ThresholdWithNonRoundMaxDurabilityAsync(byte currentDurability, bool expectRepair)
{
// Arrange — max = 25; ceiling(d*100/25) = ceiling(d*4)
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 25, currentDurability: currentDurability);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
if (expectRepair)
{
Assert.That(item.Durability, Is.EqualTo(25));
}
else
{
Assert.That(item.Durability, Is.EqualTo(currentDurability));
}
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
private Item CreateDamagedItem(byte maxDurability, byte currentDurability)
{
return new Item
{
Definition = new ItemDefinition
{
Durability = maxDurability,
ItemSlot = new ItemSlotType(),
Width = 1,
Height = 1,
Value = 1000,
},
Durability = currentDurability,
};
}
}

View File

@@ -0,0 +1,97 @@
// <copyright file="ZenConsumptionHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
/// <summary>
/// Tests for <see cref="ZenConsumptionHandler"/>.
/// </summary>
[TestFixture]
public class ZenConsumptionHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that Zen is deducted when the pay interval has passed.
/// </summary>
[Test]
public async ValueTask DeductsZenWhenIntervalPassedAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.TryAddMoney(1_000_000);
player.Attributes![Stats.Level] = 100;
var handler = new ZenConsumptionHandler(player);
player.StartTimestamp = DateTime.UtcNow.AddMinutes(-2);
// Act
await handler.DeductZenAsync().ConfigureAwait(false);
// Assert
Assert.That(player.Money, Is.LessThan(1_000_000));
}
/// <summary>
/// Tests that Zen is not deducted when the pay interval has not passed.
/// </summary>
[Test]
public async ValueTask DoesNotDeductZenWhenIntervalNotPassedAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var handler = new ZenConsumptionHandler(player);
// Act
await handler.DeductZenAsync().ConfigureAwait(false);
// Assert
Assert.That(player.Money, Is.EqualTo(1_000_000));
}
/// <summary>
/// Tests that <see cref="ZenConsumptionHandler.DeductZenAsync"/> returns false
/// when the player has insufficient Zen.
/// </summary>
[Test]
public async ValueTask ReturnsFalseWhenInsufficientZenAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
player.TryAddMoney(100);
player.Attributes![Stats.Level] = 100;
var handler = new ZenConsumptionHandler(player);
player.StartTimestamp = DateTime.UtcNow.AddMinutes(-2);
// Act
var result = await handler.DeductZenAsync().ConfigureAwait(false);
// Assert
Assert.That(result, Is.False);
}
private async ValueTask<OfflinePlayer> CreateOfflinePlayerAsync()
{
var player = await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
player.Attributes![Stats.MasterLevel] = 0;
return player;
}
}