~ruther/NosSmooth

435cabf797e39038ab6eca097754ab84a47427ab — František Boháček 2 years ago e919f25
feat(game): add (basic) inventory parsing
D Core/NosSmooth.Game/Data/Inventory/Bag.cs => Core/NosSmooth.Game/Data/Inventory/Bag.cs +0 -12
@@ 1,12 0,0 @@
//
//  Bag.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.

namespace NosSmooth.Game.Data.Inventory;

/// <summary>
/// Represents one bag in the inventory of the player.
/// </summary>
public record Bag();
\ No newline at end of file

M Core/NosSmooth.Game/Data/Inventory/Inventory.cs => Core/NosSmooth.Game/Data/Inventory/Inventory.cs +65 -1
@@ 4,9 4,73 @@
//  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.Collections;
using System.Collections.Concurrent;
using NosSmooth.Data.Abstractions.Enums;

namespace NosSmooth.Game.Data.Inventory;

/// <summary>
/// Represents the whole inventory of the character.
/// </summary>
public record Inventory();
\ No newline at end of file
public class Inventory : IEnumerable<InventoryBag>
{
    /// <summary>
    /// Get default number of slots in the given bag type.
    /// </summary>
    /// <param name="bag">The bag.</param>
    /// <returns>Default number of slots.</returns>
    public static short GetDefaultSlots(BagType bag)
    {
        switch (bag)
        {
            case BagType.Miniland:
                return 5 * 10;
            case BagType.Main:
            case BagType.Etc:
            case BagType.Equipment:
                return 6 * 8;
            case BagType.Costume:
            case BagType.Specialist:
                return 4 * 5;
            default:
                return 0;
        }
    }

    private readonly ConcurrentDictionary<BagType, InventoryBag> _bags;

    /// <summary>
    /// Initializes a new instance of the <see cref="Inventory"/> class.
    /// </summary>
    public Inventory()
    {
        _bags = new ConcurrentDictionary<BagType, InventoryBag>();
    }

    /// <summary>
    /// Gets a bag from inventory.
    /// </summary>
    /// <param name="bag">The bag.</param>
    /// <returns>An inventory bag.</returns>
    public InventoryBag GetBag(BagType bag)
        => _bags.GetOrAdd(bag, _ => new InventoryBag(bag, GetDefaultSlots(bag)));

    /// <summary>
    /// Gets a bag from inventory.
    /// </summary>
    /// <param name="bag">An inventory bag.</param>
    public InventoryBag this[BagType bag] => GetBag(bag);

    /// <inheritdoc />
    public IEnumerator<InventoryBag> GetEnumerator()
    {
        return _bags.Values.GetEnumerator();
    }

    /// <inheritdoc/>
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Inventory/InventoryBag.cs => Core/NosSmooth.Game/Data/Inventory/InventoryBag.cs +104 -0
@@ 0,0 1,104 @@
//
//  InventoryBag.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.Collections;
using System.Collections.Concurrent;
using NosSmooth.Data.Abstractions.Enums;

namespace NosSmooth.Game.Data.Inventory;

/// <summary>
/// Represents one bag in the inventory of the player.
/// </summary>
public class InventoryBag : IEnumerable<InventorySlot>
{
    private ConcurrentDictionary<short, InventorySlot> _slots;

    /// <summary>
    /// Initializes a new instance of the <see cref="InventoryBag"/> class.
    /// </summary>
    /// <param name="bagType">The type of the bag.</param>
    /// <param name="slots">The number of slots.</param>
    public InventoryBag(BagType bagType, short slots)
    {
        Type = bagType;
        Slots = slots;
        _slots = new ConcurrentDictionary<short, InventorySlot>();
    }

    /// <summary>
    /// Gets the type of teh bag.
    /// </summary>
    public BagType Type { get; }

    /// <summary>
    /// Gets the number of slots.
    /// </summary>
    public short Slots { get; internal set; }

    /// <summary>
    /// Get contents of the given slot.
    /// </summary>
    /// <param name="slot">The slot to get contents of.</param>
    /// <returns>A slot.</returns>
    /// <exception cref="ArgumentOutOfRangeException">The slot is outside of the bounds of the bag.</exception>
    public InventorySlot GetSlot(short slot)
    {
        if (slot < 0 || slot >= Slots)
        {
            throw new ArgumentOutOfRangeException(nameof(slot), slot, "There is not that many slots in the bag.");
        }

        return _slots.GetValueOrDefault(slot, new InventorySlot(slot, 0, null));
    }

    /// <summary>
    /// Clears the bag.
    /// </summary>
    internal void Clear()
    {
        _slots.Clear();
    }

    /// <summary>
    /// Sets the given slot item.
    /// </summary>
    /// <param name="slot">The slot information to set.</param>
    internal void SetSlot(InventorySlot slot)
    {
        if (slot.Item is null)
        {
            _slots.Remove(slot.Slot, out _);
        }
        else
        {
            if (slot.Slot >= Slots)
            {
                Slots = (short)(slot.Slot + 1);
            }

            _slots[slot.Slot] = slot;
        }
    }

    /// <summary>
    /// Gets a slot by index.
    /// </summary>
    /// <param name="slot">The slot.</param>
    public InventorySlot this[short slot] => GetSlot(slot);

    /// <inheritdoc />
    public IEnumerator<InventorySlot> GetEnumerator()
    {
        return _slots.Values.GetEnumerator();
    }

    /// <inheritdoc/>
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
\ No newline at end of file

R Core/NosSmooth.Game/Data/Inventory/InventoryItem.cs => Core/NosSmooth.Game/Data/Inventory/InventorySlot.cs +4 -2
@@ 1,12 1,14 @@
//
//  InventoryItem.cs
//  InventorySlot.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.Items;

namespace NosSmooth.Game.Data.Inventory;

/// <summary>
/// Represents item in bag inventory of the character.
/// </summary>
public record InventoryItem();
\ No newline at end of file
public record InventorySlot(short Slot, short Amount, Item? Item);
\ No newline at end of file

M Core/NosSmooth.Game/Data/Items/Item.cs => Core/NosSmooth.Game/Data/Items/Item.cs +2 -2
@@ 11,6 11,6 @@ namespace NosSmooth.Game.Data.Items;
/// <summary>
/// A NosTale item.
/// </summary>
/// <param name="ItemVNum"></param>
/// <param name="Info"></param>
/// <param name="ItemVNum">The item's VNum.</param>
/// <param name="Info">The item's info.</param>
public record Item(int ItemVNum, IItemInfo? Info);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Items/SpItem.cs => Core/NosSmooth.Game/Data/Items/SpItem.cs +26 -0
@@ 0,0 1,26 @@
//
//  SpItem.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.Data.Abstractions.Infos;

namespace NosSmooth.Game.Data.Items;

/// <summary>
/// A special card NosTale item.
/// </summary>
/// <param name="ItemVNum">The item's VNum.</param>
/// <param name="Info">The item's info.</param>
/// <param name="Rare">Unknown TODO.</param>
/// <param name="Upgrade">The upgrade (+) of the sp.</param>
/// <param name="SpStone">The number of sp stones.</param>
public record SpItem
(
    int ItemVNum,
    IItemInfo? Info,
    sbyte? Rare,
    byte? Upgrade,
    byte? SpStone
) : Item(ItemVNum, Info);
\ No newline at end of file

M Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs => Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs +2 -1
@@ 15,4 15,5 @@ namespace NosSmooth.Game.Data.Items;
/// <param name="Info">The information about the item.</param>
/// <param name="Upgrade">The upgrade (0 - 10).</param>
/// <param name="Rare">The rare nubmer (0 - 8).</param>
public record UpgradeableItem(int ItemVNum, IItemInfo? Info, byte? Upgrade, sbyte? Rare) : Item(ItemVNum, Info);
\ No newline at end of file
/// <param name="RuneCount">The number of runes.</param>
public record UpgradeableItem(int ItemVNum, IItemInfo? Info, byte? Upgrade, sbyte? Rare, int? RuneCount) : Item(ItemVNum, Info);
\ No newline at end of file

A Core/NosSmooth.Game/Events/Inventory/InventoryBagInitializedEvent.cs => Core/NosSmooth.Game/Events/Inventory/InventoryBagInitializedEvent.cs +18 -0
@@ 0,0 1,18 @@
//
//  InventoryBagInitializedEvent.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.Inventory;

namespace NosSmooth.Game.Events.Inventory;

/// <summary>
/// An inventory bag was initialized.
/// </summary>
/// <param name="Bag">The bag that was initialized.</param>
public record InventoryBagInitializedEvent
(
    InventoryBag Bag
) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Inventory/InventoryInitializedEvent.cs => Core/NosSmooth.Game/Events/Inventory/InventoryInitializedEvent.cs +16 -0
@@ 0,0 1,16 @@
//
//  InventoryInitializedEvent.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.

namespace NosSmooth.Game.Events.Inventory;

/// <summary>
/// The whole inventory was initialized (every bag)
/// </summary>
/// <param name="Inventory">The game inventory.</param>
public record InventoryInitializedEvent
(
    Data.Inventory.Inventory Inventory
) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Inventory/InventorySlotUpdatedEvent.cs => Core/NosSmooth.Game/Events/Inventory/InventorySlotUpdatedEvent.cs +21 -0
@@ 0,0 1,21 @@
//
//  InventorySlotUpdatedEvent.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.Data.Abstractions.Enums;
using NosSmooth.Game.Data.Inventory;

namespace NosSmooth.Game.Events.Inventory;

/// <summary>
/// A solt inside of inventory bag was updated.
/// </summary>
/// <param name="Bag">The updated bag.</param>
/// <param name="Slot">The updated slot.</param>
public record InventorySlotUpdatedEvent
(
    InventoryBag Bag,
    InventorySlot Slot
) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Extensions/BagTypeExtensions.cs => Core/NosSmooth.Game/Extensions/BagTypeExtensions.cs +32 -0
@@ 0,0 1,32 @@
//
//  BagTypeExtensions.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 NosSmooth.Data.Abstractions.Enums;

namespace NosSmooth.Game.Extensions;

/// <summary>
/// Extension methods for <see cref="BagType"/>, <see cref="Packets.Enums.Inventory.BagType"/>.
/// </summary>
public static class BagTypeExtensions
{
    /// <summary>
    /// Convert packets bag type to data bag type.
    /// </summary>
    /// <param name="bagType">The data bag type.</param>
    /// <returns>The packets bag type.</returns>
    public static BagType Convert(this Packets.Enums.Inventory.BagType bagType)
        => (BagType)(int)bagType;

    /// <summary>
    /// Convert data bag type to packets bag type.
    /// </summary>
    /// <param name="bagType">The packets bag type.</param>
    /// <returns>The data bag type.</returns>
    public static Packets.Enums.Inventory.BagType Convert(this BagType bagType)
        => (Packets.Enums.Inventory.BagType)(int)bagType;
}
\ No newline at end of file

M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +3 -0
@@ 9,8 9,10 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using NosSmooth.Core.Extensions;
using NosSmooth.Game.Apis;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Inventory;
using NosSmooth.Game.PacketHandlers.Characters;
using NosSmooth.Game.PacketHandlers.Entities;
using NosSmooth.Game.PacketHandlers.Inventory;
using NosSmooth.Game.PacketHandlers.Map;
using NosSmooth.Game.PacketHandlers.Specialists;



@@ 39,6 41,7 @@ public static class ServiceCollectionExtensions
            .AddPacketResponder<SkillResponder>()
            .AddPacketResponder<WalkResponder>()
            .AddPacketResponder<SkillUsedResponder>()
            .AddPacketResponder<InventoryInitResponder>()
            .AddPacketResponder<AoeSkillUsedResponder>()
            .AddPacketResponder<AtResponder>()
            .AddPacketResponder<CMapResponder>()

M Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs => Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs +2 -1
@@ 89,7 89,8 @@ public static class EquipmentHelpers
            itemVNum.Value,
            itemInfo.IsSuccess ? itemInfo.Entity : null,
            upgradeRareSubPacket?.Upgrade,
            upgradeRareSubPacket?.Rare
            upgradeRareSubPacket?.Rare,
            null
        );
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/PacketHandlers/Inventory/InventoryInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Inventory/InventoryInitResponder.cs +221 -0
@@ 0,0 1,221 @@
//
//  InventoryInitResponder.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 Microsoft.Extensions.Logging;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;
using NosSmooth.Data.Abstractions;
using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Game.Data.Inventory;
using NosSmooth.Game.Data.Items;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Inventory;
using NosSmooth.Game.Extensions;
using NosSmooth.Packets.Server.Inventory;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Inventory;

/// <summary>
/// Initialize an inventory.
/// </summary>
public class InventoryInitResponder : IPacketResponder<InvPacket>, IPacketResponder<ExtsPacket>,
    IPacketResponder<IvnPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;
    private readonly IInfoService _infoService;
    private readonly ILogger<InventoryInitResponder> _logger;

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

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

        var slots = new List<InventorySlot>();

        foreach (var subPacket in packet.InvSubPackets)
        {
            slots.Add(await CreateSlot(subPacket, ct));
        }

        void AddItems(Data.Inventory.Inventory inv)
        {
            var converted = packet.Bag.Convert();
            var bag = inv.GetBag(converted);
            bag.Clear();

            foreach (var slot in slots)
            {
                bag.SetSlot(slot);
            }
        }

        var inventory = await _game.CreateOrUpdateInventoryAsync
        (
            () =>
            {
                var inv = new Data.Inventory.Inventory();
                AddItems(inv);
                return inv;
            },
            inv =>
            {
                AddItems(inv);
                return inv;
            },
            ct: ct
        );

        if (packet.Bag == Packets.Enums.Inventory.BagType.Costume)
        {
            // last bag initialized. TODO solve race condition.
            await _eventDispatcher.DispatchEvent
            (
                new InventoryInitializedEvent(inventory),
                ct
            );
        }

        return await _eventDispatcher.DispatchEvent
        (
            new InventoryBagInitializedEvent(inventory.GetBag(packet.Bag.Convert())),
            ct
        );
    }

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

        void SetSlots(Data.Inventory.Inventory inv)
        {
            inv.GetBag(BagType.Main).Slots = packet.MainSlots;
            inv.GetBag(BagType.Equipment).Slots = packet.EquipmentSlots;
            inv.GetBag(BagType.Etc).Slots = packet.EtcSlots;
        }

        await _game.CreateOrUpdateInventoryAsync
        (
            () =>
            {
                var inv = new Data.Inventory.Inventory();
                SetSlots(inv);
                return inv;
            },
            inv =>
            {
                SetSlots(inv);
                return inv;
            },
            ct: ct
        );

        return Result.FromSuccess();
    }

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

        var inventory = await _game.CreateOrUpdateInventoryAsync
        (
            () =>
            {
                var inv = new Data.Inventory.Inventory();
                inv.GetBag(packet.Bag.Convert()).SetSlot(slot);
                return inv;
            },
            inv =>
            {
                inv.GetBag(packet.Bag.Convert()).SetSlot(slot);
                return inv;
            },
            ct: ct
        );

        return await _eventDispatcher.DispatchEvent
        (
            new InventorySlotUpdatedEvent
            (
                inventory.GetBag(packet.Bag.Convert()),
                slot
            ),
            ct
        );
    }

    private async Task<InventorySlot> CreateSlot(InvSubPacket packet, CancellationToken ct)
    {
        if (packet.VNum is null)
        {
            return new InventorySlot(packet.Slot, 0, null);
        }

        var itemInfoResult = await _infoService.GetItemInfoAsync(packet.VNum.Value, ct);

        if (!itemInfoResult.IsDefined(out var itemInfo))
        {
            _logger.LogWarning
                ("Could not obtain an item info for vnum {vnum}: {error}", packet.VNum, itemInfoResult.ToFullString());
        }

        // TODO: figure out other stuff from equipment inventory such as fairies
        if (itemInfo?.Type is ItemType.Weapon or ItemType.Armor)
        {
            var item = new UpgradeableItem
            (
                packet.VNum.Value,
                itemInfo,
                (byte?)packet.UpgradeOrDesign,
                (sbyte)packet.RareOrAmount,
                packet.RuneCount
            );
            return new InventorySlot(packet.Slot, 1, item);
        }
        else if (itemInfo?.Type is ItemType.Specialist)
        {
            var item = new SpItem
            (
                packet.VNum.Value,
                itemInfo,
                (sbyte?)packet.RareOrAmount,
                (byte?)packet.UpgradeOrDesign,
                packet.SpStoneUpgrade
            );
            return new InventorySlot(packet.Slot, 1, item);
        }
        else
        {
            var item = new Item(packet.VNum.Value, itemInfo);
            return new InventorySlot(packet.Slot, packet.RareOrAmount, item);
        }
    }
}
\ No newline at end of file

Do not follow this link