Jump to content
Moopler
Sign in to follow this  
Razz

Guide [Guide] Inventory internal workings [C#]

Recommended Posts

A while ago for an exploit I reverse engineered the inventory update packets and internal mechanisms to work with my clientless bot so that I could sell obtained items to an NPC. This 'guide' will focus mainly on the incoming packets, rather than the inventory updating/swapping/dropping requests.

1. Packet

In v114.1 the header of this packet is 0x0027. It's structured like this:

Type    Name    Length
uint16  header  2
byte    N/A     1
byte    N/A     1
byte    N/A     1
byte    control 1
[...]

Depending on the value of the control byte the rest of the packet is constructed. There are four main actions that are being done through this packet:

  1. Add item into inventory slot [0]
  2. Update inventory slot item [1]
  3. Swap inventory slots [2]
  4. Remove inventory slot item [3]

So the packet is working like this:

[Header(2)] [N/A(1)] [N/A(1)] [N/A(1)] [control(1)] [control specific data] [tab specific data]

Please keep in mind all index values are not 0-based, you'd have to substract one everytime when working with 0-based index collections.

1.1 Add item into inventory slot

1.1.1 Equip

The specific data for this control byte is structured like this:

Type    Name    Length
[...]
byte    tab     1
uint16  slot    2
byte    N/A     1
uint    itemid  4
[...]

For adding equip items to our own inventory this is all we need, unless we want to know the stats of the equip, in which case you would have to reverse that yourself. The only thing we need to add use/etc,setup and cash items to our inventory is getting the quantity as these items are stackable.

1.1.2 Use, Etc, Setup and Cash

The specific data for the use, etc, setup and cash tab is structured like this:

Type    Name        Length
[...]
byte    nullByte    1
uint64  crc?        8
int32   seed        4
uint16  quantity    2
string  nullString  Variable (I have only encountered a null string which has a size of 2)
uint16  nullShort   2
[...]

The nullByte, nullString and nullShort probably have a purpose in special situations, but I haven't seen them being relevant (yet).

1.2 Update inventory slot item

This specific data for this control byte is structured like this:

Type    Name        Length
byte    tab         1
uint16  slot        2
uint16  quantity    2

Please do note that the quantity field is the new quantity which should replace the old one. So no adding or substracting.

1.3 Swap inventory slots

This specific data for this control byte is structured like this:

Type    Name        Length
byte    tab         1
uint16  sourceSlot  2
uint16  destSlot    2

1.4 Remove inventory slot item

This specific data for this control byte is structured like this:

Type    Name        Length
byte    tab         1
uint16  slot        2

2. C# Interpretation

2.1 InventoryItem

public class InventoryItem
    {
        public ushort Quantity { get; set; }

        public uint Id { get; set; }

        public InventoryItem(uint id, ushort quantity = 1)
        {
            Id = id;
            Quantity = quantity;
        }
    }

2.2 InventoryTab

public class InventoryTab
    {
        public InventoryItem[] Slots { get; set; }

        public int Count { get; set; }

        public InventoryTab()
        {
            Slots = new InventoryItem[96];
        }

        public void SetSlot(ushort slot, InventoryItem item)
        {
            Slots[slot] = item;
            Count++;
        }

        public void ResetSlot(ushort slot)
        {
            Slots[slot] = null;
            Count--;
        }

        public void UpdateSlot(ushort slot, ushort quantity)
        {
            if (Slots[slot] == null)
                return;

            Slots[slot].Quantity = quantity;
        }

        public void SwapSlot(ushort source, ushort dest)
        {
            if(Slots[dest] != null)
            {
                InventoryItem temp = Slots[dest];
                Slots[dest] = Slots[source];
                Slots[source] = temp;
            }
            else
            {
                Slots[dest] = Slots[source];
                Slots[source] = null;
            }
        }
    }

2.3 Inventory

I use a dictionary of tabpages to store the different tabpages, but one variable for each tabpage would work as well.

public Dictionary<byte, InventoryTab> Inventory { get; set; }

2.4 Packet handlers

To parse the packets according to chapter one of this guide I use the following handlers:

public static class Inventory
    {
        public static void OnInventoryUpdateReceived(this GameContext c, PacketReader reader)
        {
            if (reader.Length == 5) //3 data + 2 header bytes
                return;

            reader.Advance(3); //The first three bytes are irrelevant for now

            switch (reader.ReadByte())
            {
                case 0:
                    c.AddInventoryItem(reader);
                    break;
                case 1:
                    c.UpdateInventoryItem(reader);
                    break;
                case 2:
                    c.SwapInventoryItem(reader);
                    break;
                case 3:
                    c.RemoveInventoryItem(reader);
                    break;
            }
        }

        public static void AddInventoryItem(this GameContext c, PacketReader reader)
        {
            byte tab = reader.ReadByte();
            ushort slot = reader.ReadUShort();
            slot--; //Correct to 0-based index
            byte unknown = reader.ReadByte();
            uint itemId = reader.ReadUInt();

            if (tab == 1) //Equip (non-stackable)
            {
                c.Player.Inventory[tab].SetSlot(slot, new Data.InventoryItem(itemId));
            }
            else //Other tabs(stackable)
            {
                byte nullByte = reader.ReadByte();
                ulong crc = reader.ReadULong(); //not sure
                int seed = reader.ReadInt(); //not sure
                ushort quantity = reader.ReadUShort();
                string s = reader.ReadDynamicString(); //usually empty
                ushort nullShort = reader.ReadUShort();

                c.Player.Inventory[tab].SetSlot(slot, new Data.InventoryItem(itemId, quantity));
            }
        }

        public static void UpdateInventoryItem(this GameContext c, PacketReader reader)
        {
            byte tab = reader.ReadByte();
            ushort slot = reader.ReadByte();
            slot--; //Correct to 0-based index
            ushort quantity = reader.ReadUShort();

            c.Player.Inventory[tab].UpdateSlot(slot, quantity);
        }

        public static void SwapInventoryItem(this GameContext c, PacketReader reader)
        {
            byte tab = reader.ReadByte();
            ushort sourceSlot = reader.ReadUShort();
            sourceSlot--;
            ushort destSlot = reader.ReadUShort();
            destSlot--;

            c.Player.Inventory[tab].SwapSlot(sourceSlot, destSlot);
        }

        public static void RemoveInventoryItem(this GameContext c, PacketReader reader)
        {
            byte tab = reader.ReadByte();
            ushort slot = reader.ReadByte();
            slot--;

            c.Player.Inventory[tab].ResetSlot(slot);
        }
    }

 2.5 Remarks

  • InventoryTabs and InventoryItems are not threadsafe
  • There's no protection from accessing non-existant slots or tabs
  • The meaning of all fields is based on my own interpretation and is not backed by any understanding of MapleStory's client
Edited by Razz

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  
×