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