~ruther/NosSmooth

57e494db60d03920d8d1c257f29eb628955235cb — Rutherther 2 years ago db8b2f1
feat(game): add group processing
M Core/NosSmooth.Game/Data/Social/Group.cs => Core/NosSmooth.Game/Data/Social/Group.cs +42 -1
@@ 5,7 5,10 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Data.Maps;
using NosSmooth.Packets.Enums.Entities;
using OneOf;
using Remora.Results;

namespace NosSmooth.Game.Data.Social;



@@ 15,4 18,42 @@ namespace NosSmooth.Game.Data.Social;
/// <param name="Id">The id of the group.</param>
/// <param name="Size">The size of the group.</param>
/// <param name="Members">The members of the group. (excluding the character)</param>
public record Group(short? Id, byte? Size, IReadOnlyList<OneOf<Player, IPet>>? Members);
\ No newline at end of file
public record Group(short? Id, IReadOnlyList<GroupMember>? Members)
{
    /// <summary>
    /// Gets the living entities from the map associated with group members.
    /// </summary>
    /// <remarks>
    /// If a group member is not found on the map, null will be returned instead on its position.
    /// </remarks>
    /// <param name="game">The map to get map from.</param>
    /// <returns>The living entities representing group members.</returns>
    public IReadOnlyList<Player?> GetLivingEntities(Game game)
        => GetLivingEntities(game.CurrentMap);

    /// <summary>
    /// Gets the living entities from the map associated with group members.
    /// </summary>
    /// <remarks>
    /// If a group member is not found on the map, null will be returned instead on its position.
    /// </remarks>
    /// <param name="map">The map to get entities from.</param>
    /// <returns>The living entities representing group members.</returns>
    public IReadOnlyList<Player?> GetLivingEntities(Map? map)
        => GetLivingEntities(map?.Entities);

    /// <summary>
    /// Gets the living entities from the map associated with group members.
    /// </summary>
    /// <remarks>
    /// If a group member is not found on the map, null will be returned instead on its position.
    /// </remarks>
    /// <param name="entities">The entities to look at.</param>
    /// <returns>The living entities representing group members.</returns>
    public IReadOnlyList<Player?> GetLivingEntities(MapEntities? entities)
    {
        return (IReadOnlyList<Player?>?)Members?
            .Select(x => entities?.GetEntity<Player>(x.PlayerId))
            .ToList() ?? Array.Empty<Player?>();
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Social/GroupMember.cs => Core/NosSmooth.Game/Data/Social/GroupMember.cs +58 -0
@@ 0,0 1,58 @@
//
//  GroupMember.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.Info;
using NosSmooth.Packets.Enums.Players;

namespace NosSmooth.Game.Data.Social;

public record GroupMember(long PlayerId)
{
    /// <summary>
    /// Gets the level of the member.
    /// </summary>
    public byte Level { get; internal set; }

    /// <summary>
    /// Gets the hero level of the member.
    /// </summary>
    public byte? HeroLevel { get; internal set; }

    /// <summary>
    /// Gets the name of the member.
    /// </summary>
    public string? Name { get; internal set; }

    /// <summary>
    /// Gets the class of the member.
    /// </summary>
    public PlayerClass Class { get; internal set; }

    /// <summary>
    /// Gets the sex of the member.
    /// </summary>
    public SexType Sex { get; internal set; }

    /// <summary>
    /// Gets the morph vnum of the player.
    /// </summary>
    public long MorphVNum { get; internal set; }

    /// <summary>
    /// Gets the hp of the member.
    /// </summary>
    public Health? Hp { get; internal set; }

    /// <summary>
    /// Gets the mp of the member.
    /// </summary>
    public Health? Mp { get; internal set; }

    /// <summary>
    /// Gets the effects of the member.
    /// </summary>
    public IReadOnlyList<long>? EffectsVNums { get; internal set; }
}
\ No newline at end of file

A Core/NosSmooth.Game/Events/Groups/GroupInitializedEvent.cs => Core/NosSmooth.Game/Events/Groups/GroupInitializedEvent.cs +11 -0
@@ 0,0 1,11 @@
//
//  GroupInitializedEvent.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.Social;

namespace NosSmooth.Game.Events.Groups;

public record GroupInitializedEvent(Group Group) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Groups/GroupMemberStatEvent.cs => Core/NosSmooth.Game/Events/Groups/GroupMemberStatEvent.cs +11 -0
@@ 0,0 1,11 @@
//
//  GroupMemberStatEvent.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.Social;

namespace NosSmooth.Game.Events.Groups;

public record GroupMemberStatEvent(GroupMember Member) : IGameEvent;
\ No newline at end of file

M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +3 -1
@@ 14,6 14,7 @@ using NosSmooth.Game.PacketHandlers.Characters;
using NosSmooth.Game.PacketHandlers.Entities;
using NosSmooth.Game.PacketHandlers.Inventory;
using NosSmooth.Game.PacketHandlers.Map;
using NosSmooth.Game.PacketHandlers.Relations;
using NosSmooth.Game.PacketHandlers.Specialists;

namespace NosSmooth.Game.Extensions;


@@ 38,10 39,11 @@ public static class ServiceCollectionExtensions

        serviceCollection
            .AddPacketResponder<CharacterInitResponder>()
            .AddPacketResponder<SkillResponder>()
            .AddPacketResponder<PlayerSkillResponder>()
            .AddPacketResponder<WalkResponder>()
            .AddPacketResponder<SkillUsedResponder>()
            .AddPacketResponder<InventoryInitResponder>()
            .AddPacketResponder<GroupInitResponder>()
            .AddPacketResponder<AoeSkillUsedResponder>()
            .AddPacketResponder<AtResponder>()
            .AddPacketResponder<CMapResponder>()

M Core/NosSmooth.Game/Game.cs => Core/NosSmooth.Game/Game.cs +19 -19
@@ 96,10 96,10 @@ public class Game : IStatefulEntity
    /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the skills.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The updated skills.</returns>
    internal async Task<Skills> CreateOrUpdateSkillsAsync
    internal async Task<Skills?> CreateOrUpdateSkillsAsync
    (
        Func<Skills> create,
        Func<Skills, Skills> update,
        Func<Skills?> create,
        Func<Skills, Skills?> update,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )


@@ 124,10 124,10 @@ public class Game : IStatefulEntity
    /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the inventory.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The updated inventory.</returns>
    internal async Task<Inventory> CreateOrUpdateInventoryAsync
    internal async Task<Inventory?> CreateOrUpdateInventoryAsync
    (
        Func<Inventory> create,
        Func<Inventory, Inventory> update,
        Func<Inventory?> create,
        Func<Inventory, Inventory?> update,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )


@@ 152,10 152,10 @@ public class Game : IStatefulEntity
    /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the family.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The updated family.</returns>
    internal async Task<Family> CreateOrUpdateFamilyAsync
    internal async Task<Family?> CreateOrUpdateFamilyAsync
    (
        Func<Family> create,
        Func<Family, Family> update,
        Func<Family?> create,
        Func<Family, Family?> update,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )


@@ 196,10 196,10 @@ public class Game : IStatefulEntity
    /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the group.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The updated group.</returns>
    internal async Task<Group> CreateOrUpdateGroupAsync
    internal async Task<Group?> CreateOrUpdateGroupAsync
    (
        Func<Group> create,
        Func<Group, Group> update,
        Func<Group?> create,
        Func<Group, Group?> update,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )


@@ 224,10 224,10 @@ public class Game : IStatefulEntity
    /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the character.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The updated character.</returns>
    internal async Task<Character> CreateOrUpdateCharacterAsync
    internal async Task<Character?> CreateOrUpdateCharacterAsync
    (
        Func<Character> create,
        Func<Character, Character> update,
        Func<Character?> create,
        Func<Character, Character?> update,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )


@@ 322,13 322,13 @@ public class Game : IStatefulEntity
        }
    }

    private async Task<T> CreateOrUpdateAsync<T>
    private async Task<T?> CreateOrUpdateAsync<T>
    (
        GameSemaphoreType type,
        Func<T?> get,
        Action<T> set,
        Func<T> create,
        Func<T, T> update,
        Action<T?> set,
        Func<T?> create,
        Func<T, T?> update,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )

M Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs +17 -1
@@ 4,6 4,7 @@
//  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 System.Diagnostics;
using NosSmooth.Core.Packets;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Info;


@@ 80,7 81,7 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp

        await _game.CreateOrUpdateGroupAsync
        (
            () => new Group(packet.GroupId, null, null),
            () => new Group(packet.GroupId, null),
            g => g with
            {
                Id = packet.GroupId


@@ 107,6 108,11 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp
            ct: ct
        );

        if (character is null)
        {
            throw new UnreachableException();
        }

        if (character != oldCharacter)
        {
            return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(character), ct);


@@ 143,6 149,11 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp
            ct: ct
        );

        if (character is null)
        {
            throw new UnreachableException();
        }

        if (character != oldCharacter)
        {
            return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(character), ct);


@@ 182,6 193,11 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp
            ct: ct
        );

        if (character is null)
        {
            throw new UnreachableException();
        }

        if (oldCharacter != character)
        {
            return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(character), ct);

M Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs +6 -0
@@ 4,6 4,7 @@
//  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 System.Diagnostics;
using NosSmooth.Core.Packets;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Info;


@@ 55,6 56,11 @@ public class WalkResponder : IPacketResponder<WalkPacket>
            ct: ct
        );

        if (character is null)
        {
            throw new UnreachableException();
        }

        return await _eventDispatcher.DispatchEvent
        (
            new EntityMovedEvent(character, oldPosition, character.Position!.Value),

M Core/NosSmooth.Game/PacketHandlers/Inventory/InventoryInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Inventory/InventoryInitResponder.cs +11 -0
@@ 4,6 4,7 @@
//  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 System.Diagnostics;
using Microsoft.Extensions.Logging;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;


@@ 91,6 92,11 @@ public class InventoryInitResponder : IPacketResponder<InvPacket>, IPacketRespon
            ct: ct
        );

        if (inventory is null)
        {
            throw new UnreachableException();
        }

        if (packet.Bag == Packets.Enums.Inventory.BagType.Costume)
        {
            // last bag initialized. TODO solve race condition.


@@ 161,6 167,11 @@ public class InventoryInitResponder : IPacketResponder<InvPacket>, IPacketRespon
            ct: ct
        );

        if (inventory is null)
        {
            throw new UnreachableException();
        }

        return await _eventDispatcher.DispatchEvent
        (
            new InventorySlotUpdatedEvent

A Core/NosSmooth.Game/PacketHandlers/Relations/GroupInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Relations/GroupInitResponder.cs +132 -0
@@ 0,0 1,132 @@
//
//  GroupInitResponder.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 System.Diagnostics;
using System.Linq.Expressions;
using NosSmooth.Core.Packets;
using NosSmooth.Game.Data.Info;
using NosSmooth.Game.Data.Social;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Groups;
using NosSmooth.Packets.Enums.Entities;
using NosSmooth.Packets.Server.Groups;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Relations;

/// <summary>
/// A group initialization responder.
/// </summary>
public class GroupInitResponder : IPacketResponder<PinitPacket>, IPacketResponder<PstPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

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

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<PinitPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;

        Group BuildGroup(Group? group)
        {
            var members = packet.PinitSubPackets?
                .Where(x => x.PlayerSubPacket is not null)
                .OrderBy(x => x.PlayerSubPacket!.GroupPosition)
                .Select(e => CreateEntity(e, group?.Members))
                .ToList() ?? new List<GroupMember>();

            return new Group(null, members);
        }

        var group = await _game.CreateOrUpdateGroupAsync
        (
            () => BuildGroup(null),
            BuildGroup,
            ct: ct
        );

        if (group is null)
        {
            throw new UnreachableException();
        }

        return await _eventDispatcher.DispatchEvent(new GroupInitializedEvent(group), ct);
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<PstPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        if (packet.EntityType != EntityType.Player)
        {
            return Result.FromSuccess();
        }

        GroupMember? member = null;
        await _game.CreateOrUpdateGroupAsync
        (
            () => null,
            g =>
            {
                member = g.Members?.FirstOrDefault(x => x.PlayerId == packet.EntityId);

                if (member is not null)
                {
                    member.Hp = new Health { Amount = packet.Hp, Percentage = packet.HpPercentage };
                    member.Mp = new Health { Amount = packet.Mp, Percentage = packet.MpPercentage };
                    member.Class = packet.PlayerClass ?? member.Class;
                    member.Sex = packet.PlayerSex ?? member.Sex;
                    member.EffectsVNums = packet.Effects?.Select(x => x.CardId).ToList();
                    member.MorphVNum = packet.PlayerMorphVNum ?? member.MorphVNum;
                }

                return g;
            },
            ct: ct
        );

        if (member is not null)
        {
            await _eventDispatcher.DispatchEvent
            (
                new GroupMemberStatEvent(member),
                ct
            );
        }

        return Result.FromSuccess();
    }

    private GroupMember CreateEntity(PinitSubPacket packet, IReadOnlyList<GroupMember>? members)
    {
        var playerSubPacket = packet.PlayerSubPacket!;
        var originalMember = members?.FirstOrDefault(x => x.PlayerId == packet.EntityId);

        return new(packet.EntityId)
        {
            Level = playerSubPacket.Level,
            HeroLevel = playerSubPacket.HeroLevel,
            Name = playerSubPacket.Name?.Name,
            Class = playerSubPacket.Class,
            Sex = playerSubPacket.Sex,
            MorphVNum = playerSubPacket.MorphVNum,
            Hp = originalMember?.Hp,
            Mp = originalMember?.Mp,
            EffectsVNums = originalMember?.EffectsVNums
        };
    }
}
\ No newline at end of file

R Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs => Core/NosSmooth.Game/PacketHandlers/Skills/PlayerSkillResponder.cs +6 -6
@@ 1,5 1,5 @@
//
//  SkillResponder.cs
//  PlayerSkillResponder.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.


@@ 19,26 19,26 @@ namespace NosSmooth.Game.PacketHandlers.Characters;
/// <summary>
/// Responds to SkiPacket to add skill to the character.
/// </summary>
public class SkillResponder : IPacketResponder<SkiPacket>
public class PlayerSkillResponder : IPacketResponder<SkiPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;
    private readonly IInfoService _infoService;
    private readonly ILogger<SkillResponder> _logger;
    private readonly ILogger<PlayerSkillResponder> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="SkillResponder"/> class.
    /// Initializes a new instance of the <see cref="PlayerSkillResponder"/> class.
    /// </summary>
    /// <param name="game">The nostale game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    /// <param name="infoService">The info service.</param>
    /// <param name="logger">The logger.</param>
    public SkillResponder
    public PlayerSkillResponder
    (
        Game game,
        EventDispatcher eventDispatcher,
        IInfoService infoService,
        ILogger<SkillResponder> logger
        ILogger<PlayerSkillResponder> logger
    )
    {
        _game = game;

Do not follow this link