Creating Inventory Context Menus

Contents

We are going to look at the creation of inventory context menus today. These are the menus that come up when you right click an item in your inventory.

We are going to add a new option to the inventory.

In the course of this tutorial we will create a quite simple mod which allows the player to destroy items in his inventory. If you haven't looked at my beginner tutorials I suggest that you do so, because this is a fairly advanced topic and will also require some more knowledge about lua. Let's get started.

Choosing the correct event

As with all mods we need an event which executes our mod. For the inventory context we will use OnPreFillInventoryObjectContextMenu. This is fired when a new context menu is created (so when we right click in the inventory), but before all the menu points are created. It allows us to hook our own methods in, so that we can add our own context option to it. To use the event we have to call it like this:

UIDestroyItems = {};

Events.OnPreFillInventoryObjectContextMenu.Add(UIDestroyItems.createMenu);

The first line UIDestroyItems = {}; creates a new lua table in which all global functions and variables of our mod will be stored. It is not only a good coding practise to do so, but imperative to minimize conflicts between mods. If we would have a second mod which also uses a createMenu function Project Zomboid wouldn't know which of the two to pick. If we put it in a lua table we can reference the function with its unique name UIDestroyItems.createMenu.

This is exactly what we do in the Events.OnPreFillInventoryObjectContextMenu.Add(UIDestroyItems.createMenu); call. It might look a bit intimidating at first, but you can simply read it as "If the inventory event fires, call our function createMenu from UIDestroyItems". That's all you have to know and remember here.

Adding our first function

Now of course we have to add the actual UIDestroyItems.createMenu function. We do it like this:

-- Our global Table
UIDestroyItems = {};

---
-- This will create a new context menu entry.
-- @param _player - The player who clicked the inventory.
-- @param _context - The context menu to which we add our option.
-- @param _items - The items which have been clicked.
--
function UIDestroyItems.createMenu(_player, _context, _items)
    ...
end

-- Call our function when the event is fired
Events.OnPreFillInventoryObjectContextMenu.Add(UIDestroyItems.createMenu);

We have created an (empty) function called createMenu and stored it in our global table UIDestroyItems. Now you probably also have read the strange @param things above it. These are annotations and explain what the code does. Commenting is very important in programming. It will help you and others to understand your code, especially when you come back to it after a long period of time. We want to prevent reactions like this.

The annotations explain the parameters which the event passes to our function when it calls it. Simply imagine _player, _context and _items as a bunch of informations that our function gets to work with. This is a very nifty feature and we will put it to good use. Let's extend our function:

function UIDestroyItems.createMenu(_player, _context, _items)
    local player = getSpecificPlayer(_player); -- get player 1 or 2
    local clickedItems = _items;

    -- Will store the clicked stuff.
    local item;
    local stack;

    -- Iterate through all clicked items
    for _, entry in ipairs(clickedItems) do
        ...
    end
end

We have added a bunch of local variables to our function (In lua all variables should be declared as locals if possible to prevent polluting the global scoop). First of all you might wonder why we have put the parameters in extra local variables player and clickedItems. While this isn't necessary, this makes it easier to make changes later on because you can simply change the topmost variable definitions.

The variables item and stack will be used to store whatever is returned from the for loop and have so called nil values (no values at all).

Now lets take a look at the for loop. If you have never seen a for loop before I suggest that you read up on it here. The short and not very technical explanation would be that it repeats its statements until a certain point is reached. We will use it to look through the clicked items that have been passed on to our function via parameter.

Iterating over the clicked items

The following explanations will be hard to understand if you have never worked with lua or programming in general before.

for _, entry in ipairs(clickedItems) do
   ... doStuff ...
end

With each cyle of the foor loop the following will happen:

  • _ will be incremented.
  • entry will receive the value of the next entry of the clickedItems table
  • the loop will stop if the last entry in clickedItems has been reached
  • doStuff will be executed

So basically we will look through all entries in clickedItems and do something with them. Now here comes the difficult part: clickedItems can basically contain three types of values depending on what the player has clicked in the inventory.

  1. A single items when he clicked on the item list.
  2. A stack (table) of items if he right clicked on a stack of the same items.
  3. Multiple stacks of items if the player has selected more than one inventory item (by dragging the mouse).

This information is very important and you should make sure to understand it. When I have been working on my Unpack Bags mod it took me quite a long time to figure it out actually.

Now that we know what clickedItems could possibly contain, we have to ask ourselves what we want to do with it. For our mod we want it to give us the option to destroy items that have reached condition zero. For reasons of simplicity we will ignore multiple selected stacks. This leaves us with single items and item stacks.

This means that we can rule out mutiple selected stacks in our code. The "#" symbol will return the highest index of a table. We use it to test if clickedItems has multiple entries and end the function using the return call if that's the case.

for _, entry in ipairs(clickedItems) do
    -- stop function if player has selected multiple item stacks
    if #clickedItems > 1 then
        return;
    end
end

Now we know that - if there is an entry in clickedItems - it can only be a stack of items, or a single InventoryItem. We still need to distinguish between those to:

    -- Will store the clicked stuff.
    local item;
    local stack;

    -- Iterate through all clicked items
    for _, entry in ipairs(clickedItems) do
        -- stop function if player has selected multiple item stacks
        if #clickedItems > 1 then
            return;
        end

        -- test if we have a single item
        if instanceof(entry, "InventoryItem") then
            item = entry; -- store in local variable
            break;
        elseif type(entry) == "table" then
            stack = entry;
            break;
        end
    end

We use instanceof to test, if the entry in clickedItems is an instance of InventoryItem and store it in the local variable item which we declared above the loop. If it isn't, we jump to the next elseif test and check if it is of type "table", which would be the case if we have a stack of items. (FYI: the instanceof and type calls are standard lua functions). Your whole lua file should now look like this:

-- Our global Table
UIDestroyItems = {};

---
-- This will create a new context menu entry.
-- @param _player - The player who clicked the inventory.
-- @param _context - The context menu to which we add our option.
-- @param _items - The items which have been clicked.
--
function UIDestroyItems.createMenu(_player, _context, _items)
    local player = getSpecificPlayer(_player);
    local clickedItems = _items;

    -- Will store the clicked stuff.
    local item;
    local stack;

    -- Iterate through all clicked items
    for _, entry in ipairs(clickedItems) do
        -- stop function if player has selected multiple item stacks
        if #clickedItems > 1 then
            return;
        end

        -- test if we have a single item
        if instanceof(entry, "InventoryItem") then
            item = entry; -- store in local variable
            break;
        elseif type(entry) == "table" then
            stack = entry;
            break;
        end
    end
end

-- Call our function when the event is fired
Events.OnPreFillInventoryObjectContextMenu.Add(UIDestroyItems.createMenu);

Adding the context menu options

At this point of the code we have either a lua table full of clicked InventoryItems or a single InventoryItem stored and we can move on to create the actual menu entries. We'll create the entry for the single items first:

    -- Adds context menu entry for single items.
    if item then
        _context:addOption("Destroy Item", clickedItems,
UIDestroyItems.onDestroyItem, player, item);
    end

The if call in this statement will return true if we have any value stored in item (FYI: lua treats all values other than nil and false as true) and this will only be the case if the for loop stored a value in it.

The addOption function receives several parameters. "Destroy Item" will be the text which will be displayed in the menu. UIDestroyItems.onDestroyItem will be the function which is called once the player clicks on the menu entry and player and item are pretty self explanatory.

Now we do the same for the item stack. Actually this isn't much different.

    -- Adds context menu entries for multiple bags.
    if stack then
        -- We start to iterate at the second index to jump over the dummy
        -- item that is contained in the item-table.
        for i = 2, #stack.items do
            local item = stack.items[i];
            if instanceof(item, "InventoryItem") then
                _context:addOption("Destroy Item", clickedItems,
UIDestroyItems.onDestroyItem, player, item);
            end
        end
    end

We test if stack contains a value. Then we iterate over the table to get the single items. The tricky thing about this is, that the stacks in Project Zomboid always have a dummy item as their first entry so we need to ignore that by starting the for loop at the second index (i = 2). As before we test if the entry is an instance of InventoryItem and then we add the entry to the context menu.

Destroying the item

Last but not least we need the function which destroys the item. It will be pretty simple for this tutorial, but of course you could make it as complicated or simple as you want.

function UIDestroyItems.onDestroyItem(_items, _player, _item)
    _item:getContainer():Remove(_item);
    _player:Say("I love pz-mods.net")
end

Finally your whole script should look like this now:

-- Our global Table
UIDestroyItems = {};

---
-- This will create a new context menu entry.
-- @param _player - The player who clicked the inventory.
-- @param _context - The context menu to which we add our option.
-- @param _items - The items which have been clicked.
--
function UIDestroyItems.createMenu(_player, _context, _items)
    local player = getSpecificPlayer(_player);
    local clickedItems = _items;

    -- Will store the clicked stuff.
    local item;
    local stack;

    -- Iterate through all clicked items
    for _, entry in ipairs(clickedItems) do
        -- stop function if player has selected multiple item stacks
        if #clickedItems > 1 then
            return;
        end

        -- test if we have a single item
        if instanceof(entry, "InventoryItem") then
            item = entry; -- store in local variable
            break;
        elseif type(entry) == "table" then
            stack = entry;
            break;
        end
    end

    -- Adds context menu entry for single items.
    if item then
        _context:addOption("Destroy Item", clickedItems,
UIDestroyItems.onDestroyItem, player, item);
    end

    -- Adds context menu entries for multiple bags.
    if stack then
        -- We start to iterate at the second index to jump over the dummy
        -- item that is contained in the item-table.
        for i = 2, #stack.items do
            local item = stack.items[i];
            if instanceof(item, "InventoryItem") then
                _context:addOption("Destroy Item", clickedItems,
UIDestroyItems.onDestroyItem, player, item);
            end
        end
    end
end

---
-- Remove the clicked item from the inventory.
-- @param _items
-- @param _player
-- @param _item
--
function UIDestroyItems.onDestroyItem(_items, _player, _item)
    _item:getContainer():Remove(_item);
_player:Say("I love pz-mods.net");
end

-- Call our function when the event is fired
Events.OnPreFillInventoryObjectContextMenu.Add(UIDestroyItems.createMenu);

I hope this tutorial has helped you a little bit. As always, please keep in mind that while loving to mod I'm by no means a professional programmer and if you have any suggestions/improvements feel free to leave a comment on here or on the forum. Much love to Aricane for making such a great website and RobertJohnson for being so patient with me ;)

In the next tutorial we are going to take a look at Timed Actions and why they are awesome!

 

EDIT:
Eggplanticus posted a small optimization on the TIS forums. He suggested to move the test if we have one or multiple stacks in front of the for-loop to prevent redundant executions. The code would look like this then:

-- Our global Table
UIDestroyItems = {};

---
-- This will create a new context menu entry.
-- @param _player - The player who clicked the inventory.
-- @param _context - The context menu to which we add our option.
-- @param _items - The items which have been clicked.
--
function UIDestroyItems.createMenu(_player, _context, _items)
    local player = getSpecificPlayer(_player);
    local clickedItems = _items;

    -- Will store the clicked stuff.
    local item;
    local stack;

    -- stop function if player has selected multiple item stacks
    if #clickedItems > 1 then
        return;
    end
    
    -- Iterate through all clicked items
    for _, entry in ipairs(clickedItems) do

        -- test if we have a single item
        if instanceof(entry, "InventoryItem") then
            item = entry; -- store in local variable
            break;
        elseif type(entry) == "table" then
            stack = entry;
            break;
        end
    end

    -- Adds context menu entry for single items.
    if item then
        _context:addOption("Destroy Item", clickedItems,
            UIDestroyItems.onDestroyItem, player, item);
    end

    -- Adds context menu entries for multiple bags.
    if stack then
        -- We start to iterate at the second index to jump over the dummy
        -- item that is contained in the item-table.
        for i = 2, #stack.items do
            local item = stack.items[i];
            if instanceof(item, "InventoryItem") then
                _context:addOption("Destroy Item", clickedItems,
                    UIDestroyItems.onDestroyItem, player, item);
            end
        end
    end
end

---
-- Remove the clicked item from the inventory.
-- @param _items
-- @param _player
-- @param _item
--
function UIDestroyItems.onDestroyItem(_items, _player, _item)
    _item:getContainer():Remove(_item);
    _player:Say("I love pz-mods.net");
end

-- Call our function when the event is fired
Events.OnPreFillInventoryObjectContextMenu.Add(UIDestroyItems.createMenu);
"I 
 _player:Say("I 
-- Our global Table
UIDestroyItems = {};

---
-- This will create a new context menu entry.
-- @param _player - The player who clicked the inventory.
-- @param _context - The context menu to which we add our option.
-- @param _items - The items which have been clicked.
--
function UIDestroyItems.createMenu(_player, _context, _items)
    local player = getSpecificPlayer(_player);
    local clickedItems = _items;

    -- Will store the clicked stuff.
    local item;
    local stack;

    -- stop function if player has selected multiple item stacks
    if #clickedItems > 1 then
        return;
    end
    
    -- Iterate through all clicked items
    for _, entry in ipairs(clickedItems) do

        -- test if we have a single item
        if instanceof(entry, "InventoryItem") then
            item = entry; -- store in local variable
            break;
        elseif type(entry) == "table" then
            stack = entry;
            break;
        end
    end

    -- Adds context menu entry for single items.
    if item then
        _context:addOption("Destroy Item", clickedItems,
            UIDestroyItems.onDestroyItem, player, item);
    end

    -- Adds context menu entries for multiple bags.
    if stack then
        -- We start to iterate at the second index to jump over the dummy
        -- item that is contained in the item-table.
        for i = 2, #stack.items do
            local item = stack.items[i];
            if instanceof(item, "InventoryItem") then
                _context:addOption("Destroy Item", clickedItems,
                    UIDestroyItems.onDestroyItem, player, item);
            end
        end
    end
end

---
-- Remove the clicked item from the inventory.
-- @param _items
-- @param _player
-- @param _item
--
function UIDestroyItems.onDestroyItem(_items, _player, _item)
    _item:getContainer():Remove(_item);
    _player:Say("I love pz-mods.net");
end

-- Call our function when the event is fired
Events.OnPreFillInventoryObjectContextMenu.Add(UIDestroyItems.createMenu);

Comments

  • msarabi

    Hi, I want to add an item that survivor just can equip it on his back. something like "RequiresEquippedBothHands" that be equppable just in both hands.
    I tried "RequiresEquippedBack" but it didn't work. is there any way to do thi

    Dec 7. 16
  • msarabi

    Hi, I want to add an item that survivor just can equip it on his back. something like "RequiresEquippedBothHands" that be equppable just in both hands.
    I tried "RequiresEquippedBack" but it didn't work. is there any way to do this?
    I Appreciate any help. Thanks :)

    Dec 7. 16
  • lyravega

    On "1.3 - Iterating over the clicked items", the for loop is described in a completely wrong way. First of all, "_" is a special character in LUA, and secondly, there are two types of "for" in LUA. The type in question here does not increment "_".

    It is a table for loop, and the first variable is the name of the key, and the second variable is the name of the value. When used in conjunction with "ipairs", "pairs" or "next", one can navigate through a table in specific orders.

    "ipairs" function expects a table with an array structure; which means, key names are ordered numbers. It iterates over the table in order, and has its own internal counter. "_" in the example does not get incremented; as said, "_" is a special character in LUA, which simply means a throwaway variable that won't be used or needed.

    I'm all in for knowledge. But if it is reflecting the truth. A piece fitting to a puzzle does not mean it is the piece for that puzzle.

    Jan 18. 15
    • RoboMat

      Thanks for your input, but it's not really true:

      First of all '_' is NOT a "special" character - It is a normal variable. You can print it to the console and it will return the current index in the table (so "1, 2, 3, ..."). It's simply a Lua-convention to use the underscore for variables which aren't used. It still gets incremented and it still is a normal variable like all the others.

      Also ipairs doesn't "expect" a sequence. You can also pass a mixed table, but it will just use the values which are stored in the "array structure".

      Anyway, I possibly could've explained that better in my tutorial, but then again it is a modding tutorial and not an in-depth explanation of Lua ;)

      Jun 25. 15
You need to be logged in to write comments