~ruther/NosSmooth

40e84be35833cd225263f9133ca701472536a14e — František Boháček 3 years ago baf9dc5
feat: add responders for character actions
A Core/NosSmooth.Game/Extensions/SkillsExtensions.cs => Core/NosSmooth.Game/Extensions/SkillsExtensions.cs +45 -0
@@ 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;

/// <summary>
/// Contains extension methods for <see cref="Skills"/>.
/// </summary>
public static class SkillsExtensions
{
    /// <summary>
    /// Tries to get the skill of the specified vnum.
    /// </summary>
    /// <param name="skills">The skills of the player.</param>
    /// <param name="skillVNum">The vnum to search for.</param>
    /// <returns>The skill, if found.</returns>
    public static Result<Skill> 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

A Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs +138 -0
@@ 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;

/// <summary>
/// Responds to CInfoPacket by creating the character.
/// </summary>
public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResponder<LevPacket>, IPacketResponder<CModePacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

    /// <summary>
    /// Initializes a new instance of the <see cref="CharacterInitResponder"/> class.
    /// </summary>
    /// <param name="game">The nostale game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    public CharacterInitResponder(Game game, EventDispatcher eventDispatcher)
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<CInfoPacket> 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();
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<LevPacket> 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();
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<CModePacket> 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

A Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs +91 -0
@@ 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;

/// <summary>
/// Responds to SkiPacket to add skill to the character.
/// </summary>
public class SkillResponder : IPacketResponder<SkiPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

    /// <summary>
    /// Initializes a new instance of the <see cref="SkillResponder"/> class.
    /// </summary>
    /// <param name="game">The nostale game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    public SkillResponder(Game game, EventDispatcher eventDispatcher)
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<SkiPacket> 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<long>();
        var skillsFromCharacter = character.Skills is null ? new List<long>() : character.Skills.OtherSkills.Select(x => x.SkillVNum).ToList();
        var newSkills = skillsFromPacket.Except(skillsFromCharacter);
        var oldSkills = skillsFromCharacter.Except(skillsFromPacket);

        var otherSkillsFromCharacter = new List<Skill>(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

A Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs +59 -0
@@ 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;

/// <summary>
/// Responds to walk packet.
/// </summary>
public class WalkResponder : IPacketResponder<WalkPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

    /// <summary>
    /// Initializes a new instance of the <see cref="WalkResponder"/> class.
    /// </summary>
    /// <param name="game">The nostale game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    public WalkResponder(Game game, EventDispatcher eventDispatcher)
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<WalkPacket> 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

A Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs +118 -0
@@ 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;

/// <summary>
/// Responds to skill used packet.
/// </summary>
public class SkillUsedResponder : IPacketResponder<SuPacket>, IPacketResponder<SrPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

    /// <summary>
    /// Initializes a new instance of the <see cref="SkillUsedResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    public SkillUsedResponder(Game game, EventDispatcher eventDispatcher)
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<SuPacket> 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();
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<SrPacket> 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

A Core/NosSmooth.Game/PacketHandlers/Login/CListPacketResponder.cs => Core/NosSmooth.Game/PacketHandlers/Login/CListPacketResponder.cs +52 -0
@@ 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;

/// <summary>
/// Handles FStashEnd packet to remove game data.
/// </summary>
public class CListPacketResponder : IPacketResponder<ClistPacket>
{
    private readonly EventDispatcher _eventDispatcher;
    private readonly Game _game;

    /// <summary>
    /// Initializes a new instance of the <see cref="CListPacketResponder"/> class.
    /// </summary>
    /// <param name="eventDispatcher">The events dispatcher.</param>
    /// <param name="game">The nostale game.</param>
    public CListPacketResponder(EventDispatcher eventDispatcher, Game game)
    {
        _eventDispatcher = eventDispatcher;
        _game = game;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<ClistPacket> 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

Do not follow this link