From 435cabf797e39038ab6eca097754ab84a47427ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Thu, 5 Jan 2023 21:32:40 +0100 Subject: [PATCH] feat(game): add (basic) inventory parsing --- Core/NosSmooth.Game/Data/Inventory/Bag.cs | 12 - .../Data/Inventory/Inventory.cs | 66 +++++- .../Data/Inventory/InventoryBag.cs | 104 +++++++++ .../{InventoryItem.cs => InventorySlot.cs} | 6 +- Core/NosSmooth.Game/Data/Items/Item.cs | 4 +- Core/NosSmooth.Game/Data/Items/SpItem.cs | 26 +++ .../Data/Items/UpgradeableItem.cs | 3 +- .../Inventory/InventoryBagInitializedEvent.cs | 18 ++ .../Inventory/InventoryInitializedEvent.cs | 16 ++ .../Inventory/InventorySlotUpdatedEvent.cs | 21 ++ .../Extensions/BagTypeExtensions.cs | 32 +++ .../Extensions/ServiceCollectionExtensions.cs | 3 + .../Helpers/EquipmentHelpers.cs | 3 +- .../Inventory/InventoryInitResponder.cs | 221 ++++++++++++++++++ 14 files changed, 516 insertions(+), 19 deletions(-) delete mode 100644 Core/NosSmooth.Game/Data/Inventory/Bag.cs create mode 100644 Core/NosSmooth.Game/Data/Inventory/InventoryBag.cs rename Core/NosSmooth.Game/Data/Inventory/{InventoryItem.cs => InventorySlot.cs} (71%) create mode 100644 Core/NosSmooth.Game/Data/Items/SpItem.cs create mode 100644 Core/NosSmooth.Game/Events/Inventory/InventoryBagInitializedEvent.cs create mode 100644 Core/NosSmooth.Game/Events/Inventory/InventoryInitializedEvent.cs create mode 100644 Core/NosSmooth.Game/Events/Inventory/InventorySlotUpdatedEvent.cs create mode 100644 Core/NosSmooth.Game/Extensions/BagTypeExtensions.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Inventory/InventoryInitResponder.cs diff --git a/Core/NosSmooth.Game/Data/Inventory/Bag.cs b/Core/NosSmooth.Game/Data/Inventory/Bag.cs deleted file mode 100644 index 5ea5754c84dbc1f5e0a664a1a348d100bd756199..0000000000000000000000000000000000000000 --- a/Core/NosSmooth.Game/Data/Inventory/Bag.cs +++ /dev/null @@ -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; - -/// -/// Represents one bag in the inventory of the player. -/// -public record Bag(); \ No newline at end of file diff --git a/Core/NosSmooth.Game/Data/Inventory/Inventory.cs b/Core/NosSmooth.Game/Data/Inventory/Inventory.cs index 38294655489fb8c47b615755f0b476c909858e4d..28ecc387b76b3ff4483352ff5221fb85a0b497c9 100644 --- a/Core/NosSmooth.Game/Data/Inventory/Inventory.cs +++ b/Core/NosSmooth.Game/Data/Inventory/Inventory.cs @@ -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; /// /// Represents the whole inventory of the character. /// -public record Inventory(); \ No newline at end of file +public class Inventory : IEnumerable +{ + /// + /// Get default number of slots in the given bag type. + /// + /// The bag. + /// Default number of slots. + 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 _bags; + + /// + /// Initializes a new instance of the class. + /// + public Inventory() + { + _bags = new ConcurrentDictionary(); + } + + /// + /// Gets a bag from inventory. + /// + /// The bag. + /// An inventory bag. + public InventoryBag GetBag(BagType bag) + => _bags.GetOrAdd(bag, _ => new InventoryBag(bag, GetDefaultSlots(bag))); + + /// + /// Gets a bag from inventory. + /// + /// An inventory bag. + public InventoryBag this[BagType bag] => GetBag(bag); + + /// + public IEnumerator GetEnumerator() + { + return _bags.Values.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/Data/Inventory/InventoryBag.cs b/Core/NosSmooth.Game/Data/Inventory/InventoryBag.cs new file mode 100644 index 0000000000000000000000000000000000000000..070f2e1a568e9cbd2daef563156d91ce36e67bc4 --- /dev/null +++ b/Core/NosSmooth.Game/Data/Inventory/InventoryBag.cs @@ -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; + +/// +/// Represents one bag in the inventory of the player. +/// +public class InventoryBag : IEnumerable +{ + private ConcurrentDictionary _slots; + + /// + /// Initializes a new instance of the class. + /// + /// The type of the bag. + /// The number of slots. + public InventoryBag(BagType bagType, short slots) + { + Type = bagType; + Slots = slots; + _slots = new ConcurrentDictionary(); + } + + /// + /// Gets the type of teh bag. + /// + public BagType Type { get; } + + /// + /// Gets the number of slots. + /// + public short Slots { get; internal set; } + + /// + /// Get contents of the given slot. + /// + /// The slot to get contents of. + /// A slot. + /// The slot is outside of the bounds of the bag. + 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)); + } + + /// + /// Clears the bag. + /// + internal void Clear() + { + _slots.Clear(); + } + + /// + /// Sets the given slot item. + /// + /// The slot information to set. + 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; + } + } + + /// + /// Gets a slot by index. + /// + /// The slot. + public InventorySlot this[short slot] => GetSlot(slot); + + /// + public IEnumerator GetEnumerator() + { + return _slots.Values.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/Data/Inventory/InventoryItem.cs b/Core/NosSmooth.Game/Data/Inventory/InventorySlot.cs similarity index 71% rename from Core/NosSmooth.Game/Data/Inventory/InventoryItem.cs rename to Core/NosSmooth.Game/Data/Inventory/InventorySlot.cs index f31010d60859b04183b1e77f718654b5dfacc516..c79745c6f9b6b7acab12e41bc47be46ad199be42 100644 --- a/Core/NosSmooth.Game/Data/Inventory/InventoryItem.cs +++ b/Core/NosSmooth.Game/Data/Inventory/InventorySlot.cs @@ -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; /// /// Represents item in bag inventory of the character. /// -public record InventoryItem(); \ No newline at end of file +public record InventorySlot(short Slot, short Amount, Item? Item); \ No newline at end of file diff --git a/Core/NosSmooth.Game/Data/Items/Item.cs b/Core/NosSmooth.Game/Data/Items/Item.cs index 581f85ae12ba2e237adb4e95652fd16a85b3feeb..172203699e34b2c5aa63c67208f0b478a19441d6 100644 --- a/Core/NosSmooth.Game/Data/Items/Item.cs +++ b/Core/NosSmooth.Game/Data/Items/Item.cs @@ -11,6 +11,6 @@ namespace NosSmooth.Game.Data.Items; /// /// A NosTale item. /// -/// -/// +/// The item's VNum. +/// The item's info. public record Item(int ItemVNum, IItemInfo? Info); \ No newline at end of file diff --git a/Core/NosSmooth.Game/Data/Items/SpItem.cs b/Core/NosSmooth.Game/Data/Items/SpItem.cs new file mode 100644 index 0000000000000000000000000000000000000000..97d22da652e91b83f82a7c816a56d6c915f12c41 --- /dev/null +++ b/Core/NosSmooth.Game/Data/Items/SpItem.cs @@ -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; + +/// +/// A special card NosTale item. +/// +/// The item's VNum. +/// The item's info. +/// Unknown TODO. +/// The upgrade (+) of the sp. +/// The number of sp stones. +public record SpItem +( + int ItemVNum, + IItemInfo? Info, + sbyte? Rare, + byte? Upgrade, + byte? SpStone +) : Item(ItemVNum, Info); \ No newline at end of file diff --git a/Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs b/Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs index e8e16de371a1903da854e913571085a6eaed9ad5..d8c52e55e316ee72688059731b48c5121a19d117 100644 --- a/Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs +++ b/Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs @@ -15,4 +15,5 @@ namespace NosSmooth.Game.Data.Items; /// The information about the item. /// The upgrade (0 - 10). /// The rare nubmer (0 - 8). -public record UpgradeableItem(int ItemVNum, IItemInfo? Info, byte? Upgrade, sbyte? Rare) : Item(ItemVNum, Info); \ No newline at end of file +/// The number of runes. +public record UpgradeableItem(int ItemVNum, IItemInfo? Info, byte? Upgrade, sbyte? Rare, int? RuneCount) : Item(ItemVNum, Info); \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Inventory/InventoryBagInitializedEvent.cs b/Core/NosSmooth.Game/Events/Inventory/InventoryBagInitializedEvent.cs new file mode 100644 index 0000000000000000000000000000000000000000..246e61f1223a2a463f6257b92223ec3ac78a0dcb --- /dev/null +++ b/Core/NosSmooth.Game/Events/Inventory/InventoryBagInitializedEvent.cs @@ -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; + +/// +/// An inventory bag was initialized. +/// +/// The bag that was initialized. +public record InventoryBagInitializedEvent +( + InventoryBag Bag +) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Inventory/InventoryInitializedEvent.cs b/Core/NosSmooth.Game/Events/Inventory/InventoryInitializedEvent.cs new file mode 100644 index 0000000000000000000000000000000000000000..5120d218f451f183366808e5f10174782e9870ff --- /dev/null +++ b/Core/NosSmooth.Game/Events/Inventory/InventoryInitializedEvent.cs @@ -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; + +/// +/// The whole inventory was initialized (every bag) +/// +/// The game inventory. +public record InventoryInitializedEvent +( + Data.Inventory.Inventory Inventory +) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Inventory/InventorySlotUpdatedEvent.cs b/Core/NosSmooth.Game/Events/Inventory/InventorySlotUpdatedEvent.cs new file mode 100644 index 0000000000000000000000000000000000000000..8399325937b2a7e9c6e949983568c2e30010bf13 --- /dev/null +++ b/Core/NosSmooth.Game/Events/Inventory/InventorySlotUpdatedEvent.cs @@ -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; + +/// +/// A solt inside of inventory bag was updated. +/// +/// The updated bag. +/// The updated slot. +public record InventorySlotUpdatedEvent +( + InventoryBag Bag, + InventorySlot Slot +) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Extensions/BagTypeExtensions.cs b/Core/NosSmooth.Game/Extensions/BagTypeExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..b4ad904ece13a2b1547da9949499d2d2ab91bcb9 --- /dev/null +++ b/Core/NosSmooth.Game/Extensions/BagTypeExtensions.cs @@ -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; + +/// +/// Extension methods for , . +/// +public static class BagTypeExtensions +{ + /// + /// Convert packets bag type to data bag type. + /// + /// The data bag type. + /// The packets bag type. + public static BagType Convert(this Packets.Enums.Inventory.BagType bagType) + => (BagType)(int)bagType; + + /// + /// Convert data bag type to packets bag type. + /// + /// The packets bag type. + /// The data bag type. + public static Packets.Enums.Inventory.BagType Convert(this BagType bagType) + => (Packets.Enums.Inventory.BagType)(int)bagType; +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs b/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs index 42ce1e9795efda2bc4d16bb6dc997ebb7489e058..7b5cdcb781edd6438b17171e45c1af7c8b1910ee 100644 --- a/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +++ b/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs @@ -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() .AddPacketResponder() .AddPacketResponder() + .AddPacketResponder() .AddPacketResponder() .AddPacketResponder() .AddPacketResponder() diff --git a/Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs b/Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs index 4d6a7af74c99cfa64fb1f6ca09a7807329505ceb..4b590977d626169e293e53aabea166359f959102 100644 --- a/Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs +++ b/Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs @@ -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 diff --git a/Core/NosSmooth.Game/PacketHandlers/Inventory/InventoryInitResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Inventory/InventoryInitResponder.cs new file mode 100644 index 0000000000000000000000000000000000000000..8b9005e28893452d2f7f124fe3162b343f16b577 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Inventory/InventoryInitResponder.cs @@ -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; + +/// +/// Initialize an inventory. +/// +public class InventoryInitResponder : IPacketResponder, IPacketResponder, + IPacketResponder +{ + private readonly Game _game; + private readonly EventDispatcher _eventDispatcher; + private readonly IInfoService _infoService; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + /// The event dispatcher. + /// The info service. + /// The logger. + public InventoryInitResponder + ( + Game game, + EventDispatcher eventDispatcher, + IInfoService infoService, + ILogger logger + ) + { + _game = game; + _eventDispatcher = eventDispatcher; + _infoService = infoService; + _logger = logger; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + + var slots = new List(); + + 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 + ); + } + + /// + public async Task Respond(PacketEventArgs 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(); + } + + /// + public async Task Respond(PacketEventArgs 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 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