From 40e84be35833cd225263f9133ca701472536a14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Sun, 26 Dec 2021 23:04:51 +0100 Subject: [PATCH] feat: add responders for character actions --- .../Extensions/SkillsExtensions.cs | 45 ++++++ .../Characters/CharacterInitResponder.cs | 138 ++++++++++++++++++ .../Characters/SkillResponder.cs | 91 ++++++++++++ .../Characters/WalkResponder.cs | 59 ++++++++ .../Entities/SkillUsedResponder.cs | 118 +++++++++++++++ .../Login/CListPacketResponder.cs | 52 +++++++ 6 files changed, 503 insertions(+) create mode 100644 Core/NosSmooth.Game/Extensions/SkillsExtensions.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Login/CListPacketResponder.cs diff --git a/Core/NosSmooth.Game/Extensions/SkillsExtensions.cs b/Core/NosSmooth.Game/Extensions/SkillsExtensions.cs new file mode 100644 index 0000000..b8d10c9 --- /dev/null +++ b/Core/NosSmooth.Game/Extensions/SkillsExtensions.cs @@ -0,0 +1,45 @@ +// +// SkillsExtensions.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Characters; +using Remora.Results; + +namespace NosSmooth.Game.Extensions; + +/// +/// Contains extension methods for . +/// +public static class SkillsExtensions +{ + /// + /// Tries to get the skill of the specified vnum. + /// + /// The skills of the player. + /// The vnum to search for. + /// The skill, if found. + public static Result TryGetSkill(this Skills skills, long skillVNum) + { + if (skills.PrimarySkill.SkillVNum == skillVNum) + { + return skills.PrimarySkill; + } + + if (skills.SecondarySkill.SkillVNum == skillVNum) + { + return skills.SecondarySkill; + } + + foreach (Skill skill in skills.OtherSkills) + { + if (skill.SkillVNum == skillVNum) + { + return skill; + } + } + + return new NotFoundError(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs new file mode 100644 index 0000000..64d5b16 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs @@ -0,0 +1,138 @@ +// +// CharacterInitResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosCore.Packets.ServerPackets.Player; +using NosSmooth.Core.Packets; +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Data.Info; +using NosSmooth.Game.Data.Social; +using NosSmooth.Game.Events.Characters; +using NosSmooth.Game.Events.Core; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Characters; + +/// +/// Responds to CInfoPacket by creating the character. +/// +public class CharacterInitResponder : IPacketResponder, IPacketResponder, IPacketResponder +{ + private readonly Game _game; + private readonly EventDispatcher _eventDispatcher; + + /// + /// Initializes a new instance of the class. + /// + /// The nostale game. + /// The event dispatcher. + public CharacterInitResponder(Game game, EventDispatcher eventDispatcher) + { + _game = game; + _eventDispatcher = eventDispatcher; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var oldCharacter = _game.Character; + var character = oldCharacter; + var packet = packetArgs.Packet; + await _game.EnsureCharacterCreatedAsync(false, ct).ConfigureAwait(false); + if (character is not null) + { + _game.Character = character = character with + { + Id = packet.CharacterId, + AuthorityType = packet.Authority, + Gender = packet.Gender, + HairStyle = packet.HairStyle, + HairColor = packet.HairColor, + Class = packet.Class, + Icon = packet.Icon, + Compliment = packet.Compliment, + Morph = (character.Morph ?? new Morph(packet.Morph, packet.MorphUpgrade)) with + { + VNum = packet.Morph, Upgrade = packet.MorphUpgrade + }, + ArenaWinner = packet.ArenaWinner, + Invisible = packet.Invisible, + Family = new Family(packet.FamilyId, packet.FamilyName, packet.FamilyLevel) + }; + } + + _game.SetSemaphore.Release(); + if (character is not null && character != oldCharacter) + { + return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct); + } + + return Result.FromSuccess(); + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + await _game.EnsureCharacterCreatedAsync(false, ct).ConfigureAwait(false); + var oldCharacter = _game.Character; + var character = oldCharacter; + + if (character is not null) + { + _game.Character = character = character with + { + SkillCp = packet.SkillCp, + Reputation = packet.Reputation, + Level = new Level(packet.Level, packet.LevelXp, packet.XpLoad), + JobLevel = new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad), + HeroLevel = new Level(packet.HeroLevel, packet.HeroXp, packet.HeroXpLoad) + }; + } + + _game.SetSemaphore.Release(); + if (character is not null && character != oldCharacter) + { + return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct); + } + + return Result.FromSuccess(); + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + var oldCharacter = _game.Character; + var character = oldCharacter; + + if (character is null || character.Id != packetArgs.Packet.VisualId) + { // Not the current character. + return Result.FromSuccess(); + } + + await _game.SetSemaphore.WaitAsync(ct); + character = character with + { + Morph = new Morph + ( + packet.Morph, + packet.MorphUpgrade, + packet.MorphDesign, + packet.MorphBonus, + packet.MorphSkin + ), + Size = packet.Size + }; + + _game.SetSemaphore.Release(); + if (oldCharacter != character) + { + return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct); + } + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs new file mode 100644 index 0000000..7c6e1ee --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs @@ -0,0 +1,91 @@ +// +// SkillResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosCore.Packets.ServerPackets.Battle; +using NosSmooth.Core.Packets; +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Events.Characters; +using NosSmooth.Game.Events.Core; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Characters; + +/// +/// Responds to SkiPacket to add skill to the character. +/// +public class SkillResponder : IPacketResponder +{ + private readonly Game _game; + private readonly EventDispatcher _eventDispatcher; + + /// + /// Initializes a new instance of the class. + /// + /// The nostale game. + /// The event dispatcher. + public SkillResponder(Game game, EventDispatcher eventDispatcher) + { + _game = game; + _eventDispatcher = eventDispatcher; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + + Skill primarySkill, secondarySkill; + + var character = await _game.EnsureCharacterCreatedAsync(false, ct); + + if (packet.PrimarySkill == character.Skills?.PrimarySkill.SkillVNum) + { + primarySkill = character.Skills.PrimarySkill; + } + else + { + primarySkill = new Skill(packet.PrimarySkill); + } + + if (packet.PrimarySkill == packet.SecondarySkill) + { + secondarySkill = primarySkill; + } + else if (packet.SecondarySkill == character.Skills?.SecondarySkill.SkillVNum) + { + secondarySkill = character.Skills.SecondarySkill; + } + else + { + secondarySkill = new Skill(packet.SecondarySkill); + } + + var skillsFromPacket = packet.SkiSubPackets?.Select(x => x.SkillVNum).ToList() ?? new List(); + var skillsFromCharacter = character.Skills is null ? new List() : character.Skills.OtherSkills.Select(x => x.SkillVNum).ToList(); + var newSkills = skillsFromPacket.Except(skillsFromCharacter); + var oldSkills = skillsFromCharacter.Except(skillsFromPacket); + + var otherSkillsFromCharacter = new List(character.Skills?.OtherSkills ?? new Skill[] { }); + otherSkillsFromCharacter.RemoveAll(x => oldSkills.Contains(x.SkillVNum)); + + foreach (var newSkill in newSkills) + { + otherSkillsFromCharacter.Add(new Skill(newSkill)); + } + + var skills = new Skills(primarySkill, secondarySkill, otherSkillsFromCharacter); + + _game.Character = character with + { + Skills = skills + }; + + _game.SetSemaphore.Release(); + await _eventDispatcher.DispatchEvent(new SkillsReceivedEvent(skills), ct); + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs new file mode 100644 index 0000000..b6bca57 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs @@ -0,0 +1,59 @@ +// +// WalkResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosCore.Packets.ClientPackets.Movement; +using NosSmooth.Core.Packets; +using NosSmooth.Game.Data.Info; +using NosSmooth.Game.Events.Core; +using NosSmooth.Game.Events.Entities; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Characters; + +/// +/// Responds to walk packet. +/// +public class WalkResponder : IPacketResponder +{ + private readonly Game _game; + private readonly EventDispatcher _eventDispatcher; + + /// + /// Initializes a new instance of the class. + /// + /// The nostale game. + /// The event dispatcher. + public WalkResponder(Game game, EventDispatcher eventDispatcher) + { + _game = game; + _eventDispatcher = eventDispatcher; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var character = _game.Character; + if (character is not null && character.Position is not null) + { + var oldPosition = new Position + { + X = character.Position.X, + Y = character.Position.Y + }; + + character.Position.X = packetArgs.Packet.XCoordinate; + character.Position.Y = packetArgs.Packet.YCoordinate; + + return await _eventDispatcher.DispatchEvent + ( + new MovedEvent(character, character.Id, oldPosition, character.Position), + ct + ); + } + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs new file mode 100644 index 0000000..7109333 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs @@ -0,0 +1,118 @@ +// +// SkillUsedResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosCore.Packets.ServerPackets.Battle; +using NosCore.Shared.Enumerations; +using NosSmooth.Core.Packets; +using NosSmooth.Game.Data.Info; +using NosSmooth.Game.Events.Characters; +using NosSmooth.Game.Events.Core; +using NosSmooth.Game.Events.Players; +using NosSmooth.Game.Extensions; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Entities; + +/// +/// Responds to skill used packet. +/// +public class SkillUsedResponder : IPacketResponder, IPacketResponder +{ + private readonly Game _game; + private readonly EventDispatcher _eventDispatcher; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + /// The event dispatcher. + public SkillUsedResponder(Game game, EventDispatcher eventDispatcher) + { + _game = game; + _eventDispatcher = eventDispatcher; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + var character = _game.Character; + + if (packet.VisualType != VisualType.Player) + { + return Result.FromSuccess(); + } + + if (character is not null && character.Id != packet.VisualId && character.Skills is not null) + { + var skillResult = character.Skills.TryGetSkill(packet.SkillVnum); + + if (skillResult.IsDefined(out var skillEntity)) + { + skillEntity.LastUseTime = DateTimeOffset.Now; + skillEntity.Cooldown = TimeSpan.FromSeconds(packet.SkillCooldown / 10.0); + skillEntity.IsOnCooldown = true; + } + + await _eventDispatcher.DispatchEvent( + new SkillUsedEvent + ( + character, + character.Id, + skillResult.IsSuccess ? skillEntity : null, + packet.SkillVnum, + packet.TargetId, + new Position { X = packet.PositionX, Y = packet.PositionY } + ), + ct); + } + else + { + // TODO: add entity from the map, if exists. + await _eventDispatcher.DispatchEvent( + new SkillUsedEvent + ( + null, + packet.VisualId, + null, + packet.SkillVnum, + packet.TargetId, + new Position + { + X = packet.PositionX, Y = packet.PositionY + } + ), + ct + ); + } + + return Result.FromSuccess(); + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + var character = _game.Character; + + if (character is not null && character.Skills is not null) + { + var skillResult = character.Skills.TryGetSkill(packet.SkillVnum); + + if (skillResult.IsDefined(out var skillEntity)) + { + skillEntity.IsOnCooldown = false; + await _eventDispatcher.DispatchEvent(new SkillReadyEvent(skillEntity, skillEntity.SkillVNum), ct); + } + } + else + { + await _eventDispatcher.DispatchEvent(new SkillReadyEvent(null, packet.SkillVnum), ct); + } + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Login/CListPacketResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Login/CListPacketResponder.cs new file mode 100644 index 0000000..e52db8f --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Login/CListPacketResponder.cs @@ -0,0 +1,52 @@ +// +// CListPacketResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosCore.Packets.ClientPackets.Warehouse; +using NosCore.Packets.ServerPackets.CharacterSelectionScreen; +using NosSmooth.Core.Packets; +using NosSmooth.Game.Events.Core; +using NosSmooth.Game.Events.Login; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Login; + +/// +/// Handles FStashEnd packet to remove game data. +/// +public class CListPacketResponder : IPacketResponder +{ + private readonly EventDispatcher _eventDispatcher; + private readonly Game _game; + + /// + /// Initializes a new instance of the class. + /// + /// The events dispatcher. + /// The nostale game. + public CListPacketResponder(EventDispatcher eventDispatcher, Game game) + { + _eventDispatcher = eventDispatcher; + _game = game; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + await _game.SetSemaphore.WaitAsync(ct); + bool logout = _game.Character is not null || _game.CurrentMap is not null; + _game.Character = null; + _game.CurrentMap = null; + _game.CurrentRaid = null; + _game.SetSemaphore.Release(); + + if (logout) + { + return await _eventDispatcher.DispatchEvent(new LogoutEvent(), ct); + } + + return Result.FromSuccess(); + } +} \ No newline at end of file -- 2.48.1