diff --git a/src/Dapr/FriendServer.Host/FriendServerController.cs b/src/Dapr/FriendServer.Host/FriendServerController.cs index 0196f6f1d..5e5dfd438 100644 --- a/src/Dapr/FriendServer.Host/FriendServerController.cs +++ b/src/Dapr/FriendServer.Host/FriendServerController.cs @@ -77,6 +77,17 @@ public async Task FriendResponseAsync([FromBody] FriendResponseArguments data) await this._friendServer.FriendResponseAsync(data.CharacterName, data.FriendName, data.Accepted).ConfigureAwait(false); } + /// + /// Determines whether two players are friends. + /// + /// The data. + /// True if the two players are friends; otherwise false. + [HttpPost(nameof(IFriendServer.IsFriendAsync))] + public async Task IsFriendAsync([FromBody] RequestArguments data) + { + return await this._friendServer.IsFriendAsync(data.Requester, data.Receiver).ConfigureAwait(false); + } + /// /// Sends a friend request to the friend, and adds a new friend view item to the players friend list. /// diff --git a/src/Dapr/ServerClients/FriendServer.cs b/src/Dapr/ServerClients/FriendServer.cs index cd7172eea..7a721d42c 100644 --- a/src/Dapr/ServerClients/FriendServer.cs +++ b/src/Dapr/ServerClients/FriendServer.cs @@ -82,6 +82,20 @@ public async ValueTask SetPlayerVisibilityStateAsync(byte serverId, Guid charact } } + /// + public async ValueTask IsFriendAsync(string characterName, string friendName) + { + try + { + return await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.IsFriendAsync), new RequestArguments(characterName, friendName)).ConfigureAwait(false); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Unexpected error when checking friendship."); + return false; + } + } + /// public async ValueTask FriendRequestAsync(string playerName, string friendName) { diff --git a/src/FriendServer/FriendServer.cs b/src/FriendServer/FriendServer.cs index 3a1e06633..7eaee2320 100644 --- a/src/FriendServer/FriendServer.cs +++ b/src/FriendServer/FriendServer.cs @@ -83,16 +83,32 @@ public async ValueTask FriendRequestAsync(string playerName, string friend return friendIsNew && saveSuccess; } + /// + public async ValueTask IsFriendAsync(string characterName, string friendName) + { + if (this.OnlineFriends.TryGetValue(characterName, out var player) + && this.OnlineFriends.TryGetValue(friendName, out var friend)) + { + return player.HasSubscriber(friend); + } + + using var context = this._persistenceContextProvider.CreateNewFriendServerContext(); + var friendEntry = await context.GetFriendByNamesAsync(characterName, friendName).ConfigureAwait(false); + return friendEntry?.Accepted == true; + } + /// public async ValueTask DeleteFriendAsync(string playerName, string friendName) { if (this.OnlineFriends.TryGetValue(playerName, out var player) && this.OnlineFriends.TryGetValue(friendName, out var friend)) { player.RemoveSubscriber(friend); + friend.RemoveSubscriber(player); } using var context = this._persistenceContextProvider.CreateNewFriendServerContext(); await context.DeleteAsync(playerName, friendName).ConfigureAwait(false); + await context.DeleteAsync(friendName, playerName).ConfigureAwait(false); await context.SaveChangesAsync().ConfigureAwait(false); } diff --git a/src/GameLogic/MuHelper/IMuHelperSettings.cs b/src/GameLogic/MuHelper/IMuHelperSettings.cs index 006df0bdc..dfd6fddd4 100644 --- a/src/GameLogic/MuHelper/IMuHelperSettings.cs +++ b/src/GameLogic/MuHelper/IMuHelperSettings.cs @@ -140,4 +140,16 @@ public interface IMuHelperSettings /// Gets a value indicating whether to repair items. bool RepairItem { get; } + + /// Gets a value indicating whether to automatically defend against nearby monsters attacking the character. + bool UseSelfDefense { get; } + + /// Gets a value indicating whether to automatically accept friend requests. + bool AutoAcceptFriend { get; } + + /// Gets a value indicating whether to automatically accept guild join requests. + bool AutoAcceptGuild { get; } + + /// Gets a value indicating whether to use basic attack as fallback when the configured skill cannot be used. + bool FallbackBasicAttack { get; } } diff --git a/src/GameLogic/MuHelper/PartyRequestHandler.cs b/src/GameLogic/MuHelper/PartyRequestHandler.cs new file mode 100644 index 000000000..ff190b024 --- /dev/null +++ b/src/GameLogic/MuHelper/PartyRequestHandler.cs @@ -0,0 +1,100 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.MuHelper; + +using MUnique.OpenMU.Interfaces; + +/// +/// Handles auto-accepting incoming party requests from friends and guild members +/// based on the player's MU Helper settings flags. +/// +public static class PartyRequestHandler +{ + /// + /// Automatically accepts a party request if the receiver has the relevant flag enabled. + /// + /// The player receiving the party request. + /// The player who sent the party request. + /// True if the criteria matched (auto-accept was attempted, regardless of success); false if no criteria matched. + public static async ValueTask TryAutoAcceptPartyRequestAsync(Player receiver, Player requester) + { + var settings = receiver.MuHelperSettings; + if (settings is null) + { + return false; + } + + if (settings.AutoAcceptGuild && AreGuildMembers(receiver, requester)) + { + await AcceptPartyRequestAsync(receiver, requester).ConfigureAwait(false); + return true; + } + + if (settings.AutoAcceptFriend && await AreFriendsAsync(receiver, requester).ConfigureAwait(false)) + { + await AcceptPartyRequestAsync(receiver, requester).ConfigureAwait(false); + return true; + } + + return false; + } + + private static bool AreGuildMembers(Player receiver, Player requester) + { + return receiver.GuildStatus?.GuildId != null + && receiver.GuildStatus.GuildId == requester.GuildStatus?.GuildId; + } + + private static async ValueTask AreFriendsAsync(Player receiver, Player requester) + { + if (receiver.SelectedCharacter is null || requester.SelectedCharacter is null) + { + return false; + } + + var friendServer = (receiver.GameContext as IGameServerContext)?.FriendServer; + if (friendServer is null) + { + return false; + } + + return await friendServer.IsFriendAsync(receiver.SelectedCharacter.Name, requester.SelectedCharacter.Name).ConfigureAwait(false); + } + + private static async ValueTask AcceptPartyRequestAsync(Player receiver, Player requester) + { + bool success = false; + try + { + if (receiver.Party != null) + { + if (requester.Party == null) + { + // Receiver is the offline party master; add the solo requester to the existing party. + success = await receiver.Party.AddAsync(requester).ConfigureAwait(false); + } + } + else if (requester.Party != null) + { + // Requester already has a party; add the offline receiver to it. + success = await requester.Party.AddAsync(receiver).ConfigureAwait(false); + } + else + { + // Neither side has a party; create a new one. + var party = receiver.GameContext.PartyManager.CreateParty(); + success = await party.AddAsync(requester).ConfigureAwait(false) + && await party.AddAsync(receiver).ConfigureAwait(false); + } + } + finally + { + await receiver.PlayerState.TryAdvanceToAsync(PlayerState.EnteredWorld).ConfigureAwait(false); + receiver.LastPartyRequester = null; + } + + return success; + } +} diff --git a/src/GameLogic/Offline/CombatHandler.cs b/src/GameLogic/Offline/CombatHandler.cs index 330a22b06..a2caeca5b 100644 --- a/src/GameLogic/Offline/CombatHandler.cs +++ b/src/GameLogic/Offline/CombatHandler.cs @@ -18,20 +18,20 @@ namespace MUnique.OpenMU.GameLogic.Offline; public sealed class CombatHandler { private const byte DefaultRange = 1; + private const byte BowRange = 6; private const int ComboFinisherDelayTicks = 3; private const int InterSkillDelayTicks = 1; private const int MinComboSkillCount = 3; - private static readonly TargetedSkillDefaultPlugin DefaultPlugin = new(); - private const short DrainLifeBaseSkillId = 214; private const short DrainLifeStrengthenerSkillId = 458; private const short DrainLifeMasterySkillId = 462; + private static readonly TargetedSkillDefaultPlugin DefaultPlugin = new(); + private readonly OfflinePlayer _player; private readonly IMuHelperSettings? _config; private readonly MovementHandler _movementHandler; - private readonly BuffHandler _buffHandler; private readonly Point _originPosition; private readonly ConditionalSkillSlot[] _conditionalSkillSlots; @@ -46,14 +46,12 @@ public sealed class CombatHandler /// The offline player. /// The MU helper settings. /// The movement handler. - /// The buff handler. /// The original position to hunt around. - public CombatHandler(OfflinePlayer player, IMuHelperSettings? config, MovementHandler movementHandler, BuffHandler buffHandler, Point originPosition) + public CombatHandler(OfflinePlayer player, IMuHelperSettings? config, MovementHandler movementHandler, Point originPosition) { this._player = player; this._config = config; this._movementHandler = movementHandler; - this._buffHandler = buffHandler; this._originPosition = originPosition; this._conditionalSkillSlots = config is null ? [] : [ @@ -163,14 +161,9 @@ public async ValueTask PerformDrainLifeRecoveryAsync() private async ValueTask ExecuteAttackAsync(IAttackable target) { var skill = this.SelectAttackSkill(); - if (skill == null) + if (skill == null && this._config?.FallbackBasicAttack != true) { - // Do not attack if there are buffs configured to handle buff-only classes. - var buffs = this._buffHandler.ConfiguredBuffIds; - if (buffs.Any(id => id > 0)) - { - return; - } + return; } await this.ExecuteAttackAsync(target, skill, false).ConfigureAwait(false); @@ -522,6 +515,12 @@ private byte GetEffectiveAttackRange() } } + if (this._player.Attributes is { } attributes + && (attributes[Stats.IsBowEquipped] > 0 || attributes[Stats.IsCrossBowEquipped] > 0)) + { + return BowRange; + } + return DefaultRange; } diff --git a/src/GameLogic/Offline/OfflinePlayerMuHelper.cs b/src/GameLogic/Offline/OfflinePlayerMuHelper.cs index 90ea25fe4..2804e3958 100644 --- a/src/GameLogic/Offline/OfflinePlayerMuHelper.cs +++ b/src/GameLogic/Offline/OfflinePlayerMuHelper.cs @@ -54,7 +54,7 @@ public OfflinePlayerMuHelper(OfflinePlayer player) this._healingHandler = new HealingHandler(player, config); this._itemPickupHandler = new ItemPickupHandler(player, config); this._movementHandler = new MovementHandler(player, config, originalPosition); - this._combatHandler = new CombatHandler(player, config, this._movementHandler, this._buffHandler, originalPosition); + this._combatHandler = new CombatHandler(player, config, this._movementHandler, originalPosition); this._repairHandler = new RepairHandler(player, config); this._zenHandler = new ZenConsumptionHandler(player); this._petHandler = new PetHandler(player, config); diff --git a/src/GameLogic/PlayerActions/Party/PartyRequestAction.cs b/src/GameLogic/PlayerActions/Party/PartyRequestAction.cs index df63f9246..c10bf92ef 100644 --- a/src/GameLogic/PlayerActions/Party/PartyRequestAction.cs +++ b/src/GameLogic/PlayerActions/Party/PartyRequestAction.cs @@ -1,9 +1,10 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlayerActions.Party; +using MUnique.OpenMU.GameLogic.MuHelper; using MUnique.OpenMU.GameLogic.Views.Party; /// @@ -27,6 +28,14 @@ public async ValueTask HandlePartyRequestAsync(Player player, Player toRequest) if (toRequest.Party != null || toRequest.LastPartyRequester != null) { + if (toRequest.Party != null && Equals(toRequest.Party.PartyMaster, toRequest)) + { + if (await PartyRequestHandler.TryAutoAcceptPartyRequestAsync(toRequest, player).ConfigureAwait(false)) + { + return; + } + } + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerIsAlreadyInParty), toRequest.Name).ConfigureAwait(false); return; } @@ -37,6 +46,17 @@ public async ValueTask HandlePartyRequestAsync(Player player, Player toRequest) return; } + if (toRequest is Offline.OfflinePlayer) + { + if (await PartyRequestHandler.TryAutoAcceptPartyRequestAsync(toRequest, player).ConfigureAwait(false)) + { + return; + } + + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.PlayerIsAlreadyInParty), toRequest.Name).ConfigureAwait(false); + return; + } + if (await toRequest.PlayerState.TryAdvanceToAsync(PlayerState.PartyRequest).ConfigureAwait(false)) { await this.SendPartyRequestAsync(toRequest, player).ConfigureAwait(false); @@ -52,6 +72,15 @@ private async ValueTask SendPartyRequestAsync(IPartyMember toRequest, IPartyMemb } toRequest.LastPartyRequester = requester; + + if (toRequest is Player receiver && requester is Player requesterPlayer) + { + if (await PartyRequestHandler.TryAutoAcceptPartyRequestAsync(receiver, requesterPlayer).ConfigureAwait(false)) + { + return; + } + } + await toRequest.InvokeViewPlugInAsync(p => p.ShowPartyRequestAsync(requester)).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/GameServer/MessageHandler/MuHelper/MuHelperSaveDataRequestHandlerPlugin.cs b/src/GameServer/MessageHandler/MuHelper/MuHelperSaveDataRequestHandlerPlugin.cs index 8a0b5d871..debf81ceb 100644 --- a/src/GameServer/MessageHandler/MuHelper/MuHelperSaveDataRequestHandlerPlugin.cs +++ b/src/GameServer/MessageHandler/MuHelper/MuHelperSaveDataRequestHandlerPlugin.cs @@ -8,6 +8,7 @@ namespace MUnique.OpenMU.GameServer.MessageHandler.MuHelper; using System.Runtime.InteropServices; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.PlayerActions.MuHelper; +using MUnique.OpenMU.GameServer.RemoteView.MuHelper; using MUnique.OpenMU.Network.Packets.ClientToServer; using MUnique.OpenMU.PlugIns; @@ -36,5 +37,9 @@ public async ValueTask HandlePacketAsync(Player player, Memory packet) var memory = memoryOwner.Memory[..dataSize]; message.HelperData.CopyTo(memory.Span); await this._updateMuBotConfigurationAction.SaveDataAsync(player, memory).ConfigureAwait(false); + if (player.SelectedCharacter?.MuHelperConfiguration is { } configuration) + { + player.MuHelperSettings = MuHelperSettingsSerializer.TryDeserialize(configuration); + } } } \ No newline at end of file diff --git a/src/GameServer/RemoteView/MuHelper/MuHelperSettings.cs b/src/GameServer/RemoteView/MuHelper/MuHelperSettings.cs index d5f4c0412..7cd0cc3cf 100644 --- a/src/GameServer/RemoteView/MuHelper/MuHelperSettings.cs +++ b/src/GameServer/RemoteView/MuHelper/MuHelperSettings.cs @@ -26,10 +26,10 @@ public sealed class MuHelperSettings : IMuHelperSettings /// Gets the timer interval (seconds) for ActivationSkill2 when Skill2Delay is set. public int DelayMinSkill2 { get; init; } - /// Gets a value indicating whether to use timer for the skill 1. + /// Gets a value indicating whether to use a timer for skill 1. public bool Skill1UseTimer { get; init; } - /// Gets a value indicating whether to use condition for the skill 1. + /// Gets a value indicating whether to use a condition for skill 1. public bool Skill1UseCondition { get; init; } /// Gets a value indicating whether to use the precondition for Skill1 (false = nearby, true = attacking). @@ -38,10 +38,10 @@ public sealed class MuHelperSettings : IMuHelperSettings /// Gets the mob count threshold for Skill1 condition: 0=2+, 1=3+, 2=4+, 3=5+. public int Skill1SubCondition { get; init; } - /// Gets a value indicating whether to use timer for the skill 2. + /// Gets a value indicating whether to use a timer for skill 2. public bool Skill2UseTimer { get; init; } - /// Gets a value indicating whether to use condition for the skill 2. + /// Gets a value indicating whether to use a condition for skill 2. public bool Skill2UseCondition { get; init; } /// Gets a value indicating whether to use the precondition for Skill2 (false = nearby, true = attacking). @@ -56,13 +56,13 @@ public sealed class MuHelperSettings : IMuHelperSettings /// Gets the hunting range nibble (0-15); multiply to get tile distance. public int HuntingRange { get; init; } - /// Gets the max seconds away from original position before regrouping. + /// Gets the max seconds away from the original position before regrouping. public int MaxSecondsAway { get; init; } /// Gets a value indicating whether to counter-attack enemies that attack from long range. public bool LongRangeCounterAttack { get; init; } - /// Gets a value indicating whether to return to original spawn position when away too long. + /// Gets a value indicating whether to return to the original spawn position when away too long. public bool ReturnToOriginalPosition { get; init; } /// Gets the Buff 0 skill id. @@ -74,10 +74,10 @@ public sealed class MuHelperSettings : IMuHelperSettings /// Gets the Buff 2 skill id. public int BuffSkill2Id { get; init; } - /// Gets a value indicating whether apply buffs based on duration (i.e. when the buff expires). + /// Gets a value indicating whether to apply buffs based on duration (i.e. when the buff expires). public bool BuffOnDuration { get; init; } - /// Gets a value indicating whether apply buff duration logic to party members too. + /// Gets a value indicating whether to apply buff duration logic to party members too. public bool BuffDurationForParty { get; init; } /// Gets the buff cast interval in seconds (0 = disabled). @@ -92,13 +92,13 @@ public sealed class MuHelperSettings : IMuHelperSettings /// Gets a value indicating whether to use drain life. public bool UseDrainLife { get; init; } - /// Gets a value indicating whether to use healing potion. + /// Gets a value indicating whether to use a healing potion. public bool UseHealPotion { get; init; } /// Gets the potion use threshold (% HP). public int PotionThresholdPercent { get; init; } - /// Gets a value indicating whether to support party. + /// Gets a value indicating whether to support a party. public bool SupportParty { get; init; } /// Gets a value indicating whether to auto heal party. @@ -110,31 +110,31 @@ public sealed class MuHelperSettings : IMuHelperSettings /// Gets a value indicating whether to use dark raven. public bool UseDarkRaven { get; init; } - /// Gets the dark raven mode 0 = cease, 1 = auto-attack, 2 = attack with owner. + /// Gets the dark raven mode 0 = cease, 1 = auto-attack, 2 = attack with an owner. public int DarkRavenMode { get; init; } - /// Gets the obtain range. + /// Gets the range to obtain. public int ObtainRange { get; init; } - /// Gets a value indicating whether pickup all items. + /// Gets a value indicating whether to pick up all items. public bool PickAllItems { get; init; } - /// Gets a value indicating whether pickup selected items. + /// Gets a value indicating whether to pick up selected items. public bool PickSelectItems { get; init; } - /// Gets a value indicating whether pickup jewels. + /// Gets a value indicating whether to pick up jewels. public bool PickJewel { get; init; } - /// Gets a value indicating whether pickup zen. + /// Gets a value indicating whether to pick up zen. public bool PickZen { get; init; } - /// Gets a value indicating whether pickup ancient items. + /// Gets a value indicating whether to pick up ancient items. public bool PickAncient { get; init; } - /// Gets a value indicating whether pickup excellent items. + /// Gets a value indicating whether to pick up excellent items. public bool PickExcellent { get; init; } - /// Gets a value indicating whether pickup extra items. + /// Gets a value indicating whether to pick up extra items. public bool PickExtraItems { get; init; } /// Gets the extra item names. Up to 12 item name substrings; pick any dropped item whose name contains one of these. @@ -143,6 +143,18 @@ public sealed class MuHelperSettings : IMuHelperSettings /// Gets a value indicating whether to repair items. public bool RepairItem { get; init; } + /// Gets a value indicating whether to automatically defend against nearby monsters attacking the character. + public bool UseSelfDefense { get; init; } + + /// Gets a value indicating whether to automatically accept requests from friends. + public bool AutoAcceptFriend { get; init; } + + /// Gets a value indicating whether to automatically accept requests from guild. + public bool AutoAcceptGuild { get; init; } + + /// Gets a value indicating whether to use basic attack as fallback when the configured skill cannot be used. + public bool FallbackBasicAttack { get; init; } + /// public override string ToString() { diff --git a/src/GameServer/RemoteView/MuHelper/MuHelperSettingsSerializer.cs b/src/GameServer/RemoteView/MuHelper/MuHelperSettingsSerializer.cs index d9aa291f8..50e90b39a 100644 --- a/src/GameServer/RemoteView/MuHelper/MuHelperSettingsSerializer.cs +++ b/src/GameServer/RemoteView/MuHelper/MuHelperSettingsSerializer.cs @@ -34,7 +34,8 @@ namespace MUnique.OpenMU.GameServer.RemoteView.MuHelper; /// 27 [Skill2Delay:1][Skill2Con:1][Skill2PreCon:1][Skill2SubCon:2] /// [RepairItem:1][PickAllNearItems:1][PickSelectedItems:1] /// 28 PetAttack (BYTE: 0=cease, 1=auto, 2=together) -/// 29-64 _UnusedPadding[36] +/// 29 [UseSelfDefense:1][AutoAcceptFriend:1][AutoAcceptGuild:1][FallbackBasicAttack:1][Unused:4] +/// 30-64 _UnusedPadding[35] /// 65-244 ExtraItems[12][15] (null-terminated ASCII item name filters) /// 245-256 (trailing padding, ignored) /// @@ -61,11 +62,17 @@ public static class MuHelperSettingsSerializer private const int Skill1FlagsOffset = 26; private const int Skill2FlagsOffset = 27; private const int PetAttackOffset = 28; + private const int HelperFlagsOffset = 29; private const int ExtraItemsOffset = 65; private const int ExtraItemsEndOffset = 245; private const int ExtraItemSlotCount = 12; private const int ExtraItemSlotLength = 15; + private const int UseSelfDefenseFlag = 1 << 0; + private const int AutoAcceptFriendFlag = 1 << 1; + private const int AutoAcceptGuildFlag = 1 << 2; + private const int FallbackBasicAttackFlag = 1 << 3; + private const int PickJewelFlag = 1 << 3; private const int PickSetItemFlag = 1 << 4; private const int PickExcellentFlag = 1 << 5; @@ -180,6 +187,12 @@ public static class MuHelperSettingsSerializer int petAttack = blob[PetAttackOffset]; + byte helperFlags = blob[HelperFlagsOffset]; + bool useSelfDefense = (helperFlags & UseSelfDefenseFlag) != 0; + bool autoAcceptFriend = (helperFlags & AutoAcceptFriendFlag) != 0; + bool autoAcceptGuild = (helperFlags & AutoAcceptGuildFlag) != 0; + bool fallbackBasicAttack = (helperFlags & FallbackBasicAttackFlag) != 0; + var extraNames = new List(); if (blob.Length >= ExtraItemsEndOffset) { @@ -245,6 +258,10 @@ public static class MuHelperSettingsSerializer PickExtraItems = extraItem, ExtraItemNames = extraNames, RepairItem = repairItem, + UseSelfDefense = useSelfDefense, + AutoAcceptFriend = autoAcceptFriend, + AutoAcceptGuild = autoAcceptGuild, + FallbackBasicAttack = fallbackBasicAttack, }; } diff --git a/src/Interfaces/IFriendServer.cs b/src/Interfaces/IFriendServer.cs index 61ed348ef..849a93cb3 100644 --- a/src/Interfaces/IFriendServer.cs +++ b/src/Interfaces/IFriendServer.cs @@ -68,6 +68,14 @@ public interface IFriendServer /// If set to true, the character is visible as online. Otherwise, it appears as offline for other players, but is still online ValueTask SetPlayerVisibilityStateAsync(byte serverId, Guid characterId, string characterName, bool isVisible); + /// + /// Determines whether two players are friends (accepted friend relationship). + /// + /// The character name of the first player. + /// The character name of the second player. + /// True if the two players are friends; otherwise false. + ValueTask IsFriendAsync(string characterName, string friendName); + /// /// Sends a friend request to the friend, and adds a new friend view item to the players friend list. /// diff --git a/src/Persistence/EntityFramework/FriendServerContext.cs b/src/Persistence/EntityFramework/FriendServerContext.cs index 8de0c8114..384fe8eee 100644 --- a/src/Persistence/EntityFramework/FriendServerContext.cs +++ b/src/Persistence/EntityFramework/FriendServerContext.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // diff --git a/src/Persistence/InMemory/FriendServerInMemoryContext.cs b/src/Persistence/InMemory/FriendServerInMemoryContext.cs index 2ffebe966..1b869d239 100644 --- a/src/Persistence/InMemory/FriendServerInMemoryContext.cs +++ b/src/Persistence/InMemory/FriendServerInMemoryContext.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // diff --git a/tests/MUnique.OpenMU.Tests/Offline/CombatHandlerTests.cs b/tests/MUnique.OpenMU.Tests/Offline/CombatHandlerTests.cs index 0ebe30098..1f5ef007a 100644 --- a/tests/MUnique.OpenMU.Tests/Offline/CombatHandlerTests.cs +++ b/tests/MUnique.OpenMU.Tests/Offline/CombatHandlerTests.cs @@ -51,9 +51,8 @@ public async ValueTask PerformAttackAsync_MovesCloserToTargetAsync() var config = new MuHelperSettings { HuntingRange = 10 }; var movementHandler = new MovementHandler(player, config, this._origin); - var buffHandler = new BuffHandler(player, config); - var handler = new CombatHandler(player, config, movementHandler, buffHandler, this._origin); + var handler = new CombatHandler(player, config, movementHandler, this._origin); // Act await handler.PerformAttackAsync().ConfigureAwait(false); @@ -95,9 +94,8 @@ public async ValueTask PerformDrainLifeRecoveryAsync_UsesDrainLifeWhenLowHpAsync await player.SkillList!.AddLearnedSkillAsync(drainSkill).ConfigureAwait(false); var movementHandler = new MovementHandler(player, config, this._origin); - var buffHandler = new BuffHandler(player, config); - var handler = new CombatHandler(player, config, movementHandler, buffHandler, this._origin); + var handler = new CombatHandler(player, config, movementHandler, this._origin); // Act await handler.PerformDrainLifeRecoveryAsync().ConfigureAwait(false); diff --git a/tests/MUnique.OpenMU.Tests/Party/PartyTest.cs b/tests/MUnique.OpenMU.Tests/Party/PartyTest.cs index 786351347..00c28b3aa 100644 --- a/tests/MUnique.OpenMU.Tests/Party/PartyTest.cs +++ b/tests/MUnique.OpenMU.Tests/Party/PartyTest.cs @@ -8,8 +8,12 @@ namespace MUnique.OpenMU.Tests; using Moq; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.MuHelper; using MUnique.OpenMU.GameLogic.PlayerActions.Party; using MUnique.OpenMU.GameLogic.Views.Party; +using MUnique.OpenMU.GameServer; +using MUnique.OpenMU.Interfaces; +using MUnique.OpenMU.Pathfinding; using MUnique.OpenMU.Persistence.InMemory; using MUnique.OpenMU.PlugIns; @@ -182,6 +186,194 @@ public async ValueTask PartyResponseAcceptExistingPartyAsync() Mock.Get(player.ViewPlugIns.GetPlugIn()!).Verify(v => v!.ShowPartyRequestAsync(requester), Times.Never); } + /// + /// Tests if a party request is auto-accepted when is true + /// and the requester is a friend. + /// + [Test] + public async ValueTask PartyRequestAutoAcceptByFriendAsync() + { + var friendServer = new Mock(); + friendServer.Setup(f => f.IsFriendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + var gameContext = PartyTest.CreateGameServerContext(friendServer.Object); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + player.SelectedCharacter!.Name = "Requester"; + toRequest.SelectedCharacter!.Name = "Receiver"; + player.Observers.Add(toRequest); + + var settingsMock = new Mock(); + settingsMock.Setup(s => s.AutoAcceptFriend).Returns(true); + toRequest.MuHelperSettings = settingsMock.Object; + + var handler = new PartyRequestAction(); + await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false); + + Assert.That(player.Party, Is.Not.Null); + Assert.That(toRequest.Party, Is.SameAs(player.Party)); + Mock.Get(toRequest.ViewPlugIns.GetPlugIn()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Never); + } + + /// + /// Tests if a party request still shows the dialog when + /// is true but the requester is not a friend. + /// + [Test] + public async ValueTask PartyRequestAutoAcceptByFriendNotFriendAsync() + { + var friendServer = new Mock(); + friendServer.Setup(f => f.IsFriendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false); + var gameContext = PartyTest.CreateGameServerContext(friendServer.Object); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + player.SelectedCharacter!.Name = "Requester"; + toRequest.SelectedCharacter!.Name = "Receiver"; + player.Observers.Add(toRequest); + + var settingsMock = new Mock(); + settingsMock.Setup(s => s.AutoAcceptFriend).Returns(true); + toRequest.MuHelperSettings = settingsMock.Object; + + var handler = new PartyRequestAction(); + await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false); + + Assert.That(player.Party, Is.Null); + Mock.Get(toRequest.ViewPlugIns.GetPlugIn()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Once); + } + + /// + /// Tests if a party request is auto-accepted when is true + /// and both players are members of the same guild. + /// + [Test] + public async ValueTask PartyRequestAutoAcceptByGuildAsync() + { + var gameContext = GameContextTestHelper.CreateGameContext(); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + player.Observers.Add(toRequest); + + var guildId = 1u; + player.GuildStatus = new GuildMemberStatus(guildId, GuildPosition.GuildMaster); + toRequest.GuildStatus = new GuildMemberStatus(guildId, GuildPosition.NormalMember); + + var settingsMock = new Mock(); + settingsMock.Setup(s => s.AutoAcceptGuild).Returns(true); + toRequest.MuHelperSettings = settingsMock.Object; + + var handler = new PartyRequestAction(); + await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false); + + Assert.That(player.Party, Is.Not.Null); + Assert.That(toRequest.Party, Is.SameAs(player.Party)); + Mock.Get(toRequest.ViewPlugIns.GetPlugIn()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Never); + } + + /// + /// Tests if a party request still shows the dialog when + /// is true but the players are in different guilds. + /// + [Test] + public async ValueTask PartyRequestAutoAcceptByGuildDifferentGuildAsync() + { + var gameContext = GameContextTestHelper.CreateGameContext(); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + player.Observers.Add(toRequest); + + player.GuildStatus = new GuildMemberStatus(1u, GuildPosition.GuildMaster); + toRequest.GuildStatus = new GuildMemberStatus(2u, GuildPosition.NormalMember); + + var settingsMock = new Mock(); + settingsMock.Setup(s => s.AutoAcceptGuild).Returns(true); + toRequest.MuHelperSettings = settingsMock.Object; + + var handler = new PartyRequestAction(); + await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false); + + Assert.That(player.Party, Is.Null); + Mock.Get(toRequest.ViewPlugIns.GetPlugIn()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Once); + } + + /// + /// Tests that a party request is not auto-accepted when no relevant flags are set. + /// + [Test] + public async ValueTask PartyRequestAutoAcceptNoFlagsAsync() + { + var friendServer = new Mock(); + var gameContext = PartyTest.CreateGameServerContext(friendServer.Object); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + player.SelectedCharacter!.Name = "Requester"; + toRequest.SelectedCharacter!.Name = "Receiver"; + player.Observers.Add(toRequest); + + var settingsMock = new Mock(); + toRequest.MuHelperSettings = settingsMock.Object; + + var handler = new PartyRequestAction(); + await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false); + + Assert.That(player.Party, Is.Null); + Mock.Get(toRequest.ViewPlugIns.GetPlugIn()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Once); + } + + /// + /// Tests that a party request is not auto-accepted when MuHelperSettings is null. + /// + [Test] + public async ValueTask PartyRequestAutoAcceptNoSettingsAsync() + { + var friendServer = new Mock(); + var gameContext = PartyTest.CreateGameServerContext(friendServer.Object); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + var toRequest = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + player.Observers.Add(toRequest); + + var handler = new PartyRequestAction(); + await handler.HandlePartyRequestAsync(player, toRequest).ConfigureAwait(false); + + Assert.That(player.Party, Is.Null); + Mock.Get(toRequest.ViewPlugIns.GetPlugIn()!).Verify(v => v!.ShowPartyRequestAsync(player), Times.Once); + } + + private static IGameServerContext CreateGameServerContext(IFriendServer friendServer) + { + var contextProvider = new InMemoryPersistenceContextProvider(); + var context = contextProvider.CreateNewContext(); + var gameConfiguration = context.CreateNew(); + gameConfiguration.MaximumPartySize = 5; + gameConfiguration.RecoveryInterval = int.MaxValue; + gameConfiguration.MaximumInventoryMoney = int.MaxValue; + var mapDef = context.CreateNew(); + mapDef.Number = 0; + mapDef.TerrainData = new byte[ushort.MaxValue + 3]; + gameConfiguration.Maps.Add(mapDef); + + var mapInitializer = new MapInitializer(gameConfiguration, new NullLogger(), NullDropGenerator.Instance, null); + + var gameServer = new GameServerContext( + new GameServerDefinition + { + GameConfiguration = gameConfiguration, + ServerConfiguration = new GameServerConfiguration(), + }, + new Mock().Object, + new Mock().Object, + new Mock().Object, + friendServer, + new InMemoryPersistenceContextProvider(), + mapInitializer, + new NullLoggerFactory(), + new PlugInManager(new List(), new NullLoggerFactory(), null, null), + NullDropGenerator.Instance, + new ConfigurationChangeMediator()); + mapInitializer.PlugInManager = gameServer.PlugInManager; + mapInitializer.PathFinderPool = gameServer.PathFinderPool; + return gameServer; + } + private async ValueTask CreatePartyMemberAsync() { var result = await PlayerTestHelper.CreatePlayerAsync(GameContextTestHelper.CreateGameContext()).ConfigureAwait(false);