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