code:
# Fancy Vend
A full-featured, fully-integrated vendor mod for Minetest.

There are many vendor mods for Minetest, but most have too few options, lack support for automation mods, or are too tedious to set up and maintain. Fancy vendors are entirely self-contained nodes which provide light, trade, display and store items. Fancy vendors are pipeworks, digilines and awards compatible, enabling a variety of automation-based features.

<!-- For forums: -->
<!-- [**Download**](https://github.com/ChimneySwift/fancy_vend/archive/master.zip)
[**GitHub**](https://github.com/ChimneySwift/fancy_vend) -->

**Code license:** [MIT](https://opensource.org/licenses/MIT)

**Textures license:** [MIT](https://opensource.org/licenses/MIT)

**Dependencies:** default

**Optional Dependencies:** [pipeworks](https://github.com/minetest-mods/pipeworks), [digilines](https://github.com/minetest-mods/digilines), [awards](https://github.com/minetest-mods/awards), [email](https://github.com/rubenwardy/email), [tell](https://github.com/octacian/tell), [mail](https://cheapiesystems.com/git/mail/)

**Contributors:** Many thanks to LadyK for the textures, patience and ideas.

**Note:** While this mod is still a WIP and you should use it with caution, it has been tested for some time on several worlds with no problems.

## Crafting and basic configuration
A fancy vendor can be crafted as follows:

![Crafting a fancy vendor](https://github.com/ChimneySwift/fancy_vend/blob/master/screenshots/Crafting%20a%20fancy%20vendor.PNG?raw=true "Crafting a fancy vendor")

While the recipe is more expensive than most vendors, it more than makes up for this in the vendor's inventory size and additional features.

Each fancy vendor needs 2 nodes of room. If the area you wish to place the vendor doesn't have enough space, due to either a node or a protected area, the node will not be placed. If the vendor has sufficient room, it will place a vendor node in addition to a display node on top. The display node's only purpose is aesthetics, to "contain" the rotating display object.

Configuring a vendor is as simple as picking up and placing the input and output items into their respective slots as if they were regular inventories (items in these inventories are "ghosts", so don't actually take anything from the player's inventory) and setting the quantities for each item, as shown below:

![Setting up a vendor](https://github.com/ChimneySwift/fancy_vend/blob/master/screenshots/Setting%20up%20a%20vendor.PNG?raw=true "Setting up a vendor")

Stocking a vendor is as simple as placing the output item into the vendor's inventory (perhaps using one of the handy inventory movement buttons) as demonstrated below:

![Stocking a vendor](https://github.com/ChimneySwift/fancy_vend/blob/master/screenshots/Stocking%20a%20vendor.PNG?raw=true "Stocking a vendor")

After stocking a vendor, you may wish to press the "sort" button, which will combine unfilled stacks and sort the stacks in alphabetical order.

If the vendor is intended to be used to purchase items, you may wish to set the vendor to a depositor in the vendor settings, this will display the input item in the display case and change the vendor's appearance to make it clear to potential sellers the vendor's purpose. The previous example vendor set to a depositor can be seen below:

![A player depositor](https://github.com/ChimneySwift/fancy_vend/blob/master/screenshots/A%20player%20depositor.PNG?raw=true "A player depositor")

## Buying items
Regular players who can't access a fancy vendor's inventory or configuration will be displayed a purchase screen when they access a vendor. This screen displays vendor information and status, as well as buying options. Unlike many vendor mods, fancy vendors give buyers to buy multiple "lots" at once. Doing this can save a lot of clicking, as the number of input and output items traded is multiplied by this number.

There is also a button labeled "Fill lots to max" which will pre-load the lots field with the maximum number of lots that the player can purchase from the vendor (which will be either the maximum the player can afford, or the maximum the shop can sell, whichever is smaller).

An example image of the buyer formspec can be seen below:

![Buying from a fancy vendor](https://github.com/ChimneySwift/fancy_vend/blob/master/screenshots/Buying%20from%20a%20fancy%20vendor.PNG?raw=true "Buying from a fancy vendor")

## Advanced settings
Fancy vendors have many additional options which enable sellers to greatly customize vendors.

![Fancy Vendor settings](https://github.com/ChimneySwift/fancy_vend/blob/master/screenshots/Fancy%20Vendor%20settings.PNG?raw=true "Fancy Vendor settings")

**Banned Buyers:**
This field is a list of players (separated by commas) who won't be able to purchase from the vendor. This setting might be useful if you wish to stop players buying out your vendor's stock and reselling it for a higher margin.

**Co-Sellers:**
This field is a list of players (separated by commas) who will be able to access the vendor's inventory. **Please note: Anyone added to the list has full access to the vendor's inventory, however cannot modify the vendor settings nor dig the vendor.**

**Buy/sell worn tools:**
These options allow for the seller to stop the vendor from purchasing or selling worn tools. A message will display on the buyer formspec if one of these options is disabled.

**Inactive force:**
While the seller is reconfiguring a vendor, or in the process of creating a shop, the owner might wish for the vendor to be inactive so players cannot purchase from it, in which case they can enable this option.

**Autosort:**
If this option is enabled, the vendor's inventory will automatically sort when the vendor formspec is closed by someone who can access the inventory.

**Logs:**
The logs formspec displays the 40 most-recent transactions. While only people who can modify the vendor are allowed to access this formspec, these logs are kept in metadata which could be easily read client-side.

**Geminio Wand:**
This tool can be used to easily copy vendor settings from one vendor to another. Simply right click a vendor with it to copy the settings to the tool, and left click any other vendor to set that vendor's settings. **The Geminio Wand will not copy input and output items nor quantities.** You must be able to modify the settings of the vendor to do this. You can craft one with the following recipe:

![Crafting a Geminio Wand](https://github.com/ChimneySwift/fancy_vend/blob/master/screenshots/Crafting%20a%20Geminio%20Wand.PNG?raw=true "Crafting a Geminio Wand")

## Automation
Fancy Vendors are digilines and pipeworks compatible, enabling the creation of highly automated shops.

### Pipeworks
Fancy vendors connect to pipeworks devices from the bottom, rear and sides. When the optional pipeworks dependancy is satisfied, a number of pipeworks-specific options will also appear in the settings menu:

![Pipeworks Settings](https://github.com/ChimneySwift/fancy_vend/blob/master/screenshots/Pipeworks%20Settings.PNG?raw=true "Pipeworks Settings")

**Split incoming stacks:**
This option enabled incoming stacks from pipeworks tubes to be split if only some of the stack can be accepted.

**Eject incoming currency:**
When this option is enabled, the input items will be sent out of the **bottom** of the fancy vendor instead of being added to the vendor's internal inventory.

**Accept output only:**
When this option is enabled, incoming items which aren't the output item will be rejected and not added to the vendor's inventory.

### Hoppers
Fancy vendors are hopper compatible. They have been tested to work with [TenPlus1's hopper mod](https://github.com/tenplus1/hopper) however should work with all hopper mods and forks. Hoppers can filter into vendors from the sides, however cannot be drained from the bottom as hoppers are protected and Fancy Vendors are locked. However, the shop owner is given the `Eject incoming currency` option which operates on hoppers similarly to how it does on pipeworks, by manually adding incoming currency to the inventory of a hopper placed below the vendor (or into the vendor's inventory if the hopper is full). Due to API limitations, `Accept output only` is not hopper-compatible and `Split incoming stacks` is not necessary.

### Digilines
Fancy vendors are digilines compatible. When the digilines dependency is satisfied, an option to set the digiline channel will appear in settings. If this channel is set, every purchase will result in the following table being sent over that channel:

```lua
local msg = {
    buyer = player:get_player_name(), -- Purchaser's playername
    lots = lots, -- Number of lots purchased
    settings = settings, -- The settings table
}
```

The default settings table can be seen below:

```lua
local settings_default = {
    input_item = "",
    output_item = "",
    input_item_qty = 1,
    output_item_qty = 1,
    admin_vendor = false,
    depositor = false,
    currency_eject = false,
    accept_output_only = false,
    split_incoming_stacks = false,
    inactive_force = false,
    accept_worn_input = true,
    accept_worn_output = true,
    digiline_channel = "",
    co_sellers = "",
    banned_buyers = "",
}
```

Fancy Vendors and Fancy Vendor Display Nodes conduct digiline signals also.

This feature could theoretically make rewards-based shops feasable.

## Out Of Stock Mail
This mod integrates with several mail mods in order to alert shop owners when their shops lose stock. If multiple mods are in use, only one will be used to send the message. The order of which is:

- Email By Rubenwardy
- Mail By Cheapie
- Tell By Octacian

### Mail
When a vendor empties, the mod sends mail telling them that their vendor is out of stock. In the event another mod empties, the previous message will be edited so as to avoid flooding their inbox if they have many vendors.

### Tell & Email
As there is no tidy way of sending very long messages with these mods, messages will simply be sent whenever a vendor goes out of stock.

## Upgrading vendors

**NOTE:** If you wish to simply run this mod in parallel with existing vendor mods, simply load this mod (no configuration required)

Many servers already have vendor mods in place. Fancy Vend makes it easier for servers to upgrade with it's unique upgrading system. Fancy Vend will replace vendors from the supported vendor mods (money, vendor, easyvend and currency) with upgrade nodes. Upgrade nodes allow for sellers to empty the old node's inventory (if applicable), dig the upgrade node (which will drop a regular vendor) and set up a new shop. if a user tried to place an upgrade node, the stack is turned into a stack of regular vendors.

Since upgrade functionality can be achieved without the old mod loaded, the server owner must add the old vendor mod's name to `minetest.conf` as follows:

`fancy_vend_old_vendor_mods = money`

If there are multiple vendor mods the the owner wishes to enable this functionality on, they can add several mods to the list, separating each mod name with commas as follows:

`fancy_vend_old_vendor_mods = money,vendor,easyvend`

If the owner wishes to keep the mod loaded, but still replace vendor nodes, the will need to add the mod to both `minetest.conf` and fancy_vend's `depends.txt`.

Vendor upgrade:

![Vendor upgrade](https://github.com/ChimneySwift/fancy_vend/blob/master/screenshots/Vendor%20upgrade.PNG?raw=true "Vendor upgrade")

## Administration
Fancy vendors also include a variety of tools for server administrators.

**Admin vendors:**
Admin vendors perform the same role as their regular counterparts, however they do so with no stock or inventory requirements, simply creating and destroying items as players purchase.

Since incorrectly configured admin vendors could be easily used to duplicate items, the `admin_vendor` privilege is required for the option to switch a vendor to one appears. If the user is ever revoked this privilege, all admin vendors they own will be forced into an inactive state until the privilege is re-granted or the vendor is set to a player vendor.

**Modifying vendors:**
If a user has the `protection_bypass` privilege, they will be able to access the full extent of the vendor as if they were the owner, including inventory and settings.

**Server and client load:**
Fancy vend uses only one infrequent abm to refresh vendor objects in the event of a clearobjects. No other vendor updating is done unless a player purchases from the vendor or adjusts settings.

Note: Some clients using mobile and/or older devices, including PCs, may experience additional lag if many fancy vendors are used in a small space, however not to an extent greater than the majority of shops where players use itemframes or pedestals to achieve the same functionality as the fancy vendor's display.

**Settings:**
The following minetest.conf settings can be configured to further modify the appearence of Fancy Vendors:

`fancy_vend_display_node` - Change the display node to something other than `default:obsidian_glass` (ensure mod this node belongs to is in depends.txt)

`fancy_vend_log_max` - Change the maximum number of logs stored in a vendor's metadata

`fancy_vend_autorotate_speed` - Change the speed at which the display object rotates

**Temporarily disabling vendors:**
Any player with the `disable_vendor` privilege can run `/disable_all_vendors` to force all vendors into an inactive state. Please use this if you find a dangerous bug and are waiting for it to be fixed.


code:
--  /$$$$$$$$                                           /$$    /$$                          /$$
-- | $$_____/                                          | $$   | $$                         | $$
-- | $$    /$$$$$$  /$$$$$$$   /$$$$$$$ /$$   /$$      | $$   | $$ /$$$$$$  /$$$$$$$   /$$$$$$$
-- | $$$$$|____  $$| $$__  $$ /$$_____/| $$  | $$      |  $$ / $$//$$__  $$| $$__  $$ /$$__  $$
-- | $$__/ /$$$$$$$| $$  \ $$| $$      | $$  | $$       \  $$ $$/| $$$$$$$$| $$  \ $$| $$  | $$
-- | $$   /$$__  $$| $$  | $$| $$      | $$  | $$        \  $$$/ | $$_____/| $$  | $$| $$  | $$
-- | $$  |  $$$$$$$| $$  | $$|  $$$$$$$|  $$$$$$$         \  $/  |  $$$$$$$| $$  | $$|  $$$$$$$
-- |__/   \_______/|__/  |__/ \_______/ \____  $$          \_/    \_______/|__/  |__/ \_______/
--                                      /$$  | $$
--                                     |  $$$$$$/
--                                      \______/
--
-- A full-featured, fully-integrated vendor mod for Minetest

local settings = minetest.settings

local display_node = (settings:get("fancy_vend.display_node") or "default:obsidian_glass")
local max_logs = (tonumber(settings:get("fancy_vend.log_max")) or 40)
local autorotate_speed = (tonumber(settings:get("fancy_vend.autorotate_speed")) or 1)
local no_alerts = settings:get_bool("fancy_vend.no_alerts")

local drop_vendor = "fancy_vend:player_vendor"

-- Register a copy of the display node with no drops to make players separating the obsidian glass with something like a piston a non-issue.
local display_node_def = table.copy(minetest.registered_nodes[display_node])
display_node_def.drop = ""
display_node_def.pointable = false
display_node_def.groups.not_in_creative_inventory = 1
display_node_def.description = "Fancy Vendor Display Node (you hacker you!)"
if minetest.get_modpath("pipeworks") then
    display_node_def.digiline = {
        wire = {
            rules = pipeworks.digilines_rules
        }
    }
end
minetest.register_node("fancy_vend:display_node", display_node_def)

-- Craftitem to display when vendor is inactive (Use just image for this???)
minetest.register_craftitem("fancy_vend:inactive",{inventory_image = "inactive.png",})

minetest.register_privilege("admin_vendor", "Enables the user to set regular vendors to admin vendors.")
minetest.register_privilege("disable_vendor", "Enables the user to set all vendors to inactive.")

local function bts(bool)
    if bool == false then
        return "false"
    elseif bool == true then
        return "true"
    else
        return bool
    end
end

local function stb(str)
    if str == "false" then
        return false
    elseif str == "true" then
        return true
    else
        return str
    end
end

local modstorage = minetest.get_mod_storage()

if modstorage:get_string("all_inactive_force") == "" then
    modstorage:set_string("all_inactive_force", "false")
end

local all_inactive_force = stb(modstorage:get_string("all_inactive_force"))

minetest.register_chatcommand("disable_all_vendors", {
    description = "Toggle vendor inactivity.",
    privs = {disable_vendor=true},
    func = function(name, param)
        if all_inactive_force then
            all_inactive_force = false
            modstorage:set_string("all_inactive_force", "false")
        else
            all_inactive_force = true
            modstorage:set_string("all_inactive_force", "true")
        end
    end,
})

table.length = function(table)
    local length
    for i in pairs(table) do
        length = length + 1
    end
    return length
end

local function send_message(pos, channel, msg)
    if channel and channel ~= "" then
        digilines.receptor_send(pos, digilines.rules.default, channel, msg)
    end
end

-- Awards
if minetest.get_modpath("awards") then
    awards.register_award("fancy_vend:getting_fancy",{
        title = "Getting Fancy",
        description = "Craft a fancy vendor.",
        trigger = {
            type = "craft",
            item = drop_vendor,
            target = 1,
        },
        icon = "player_vend_front.png^awards_level1.png",
    })

    awards.register_award("fancy_vend:wizard",{
        title = "You're a Wizard",
        description = "Craft a copy tool.",
        trigger = {
            type = "craft",
            item = "fancy_vend:copy_tool",
            target = 1,
        },
        icon = "copier.png",
    })

    awards.register_award("fancy_vend:trader",{
        title = "Trader",
        description = "Configure a depositor.",
        icon = "player_depo_front.png",
    })
    awards.register_award("fancy_vend:seller",{
        title = "Seller",
        description = "Configure a vendor.",
        icon = "player_vend_front.png^awards_level2.png",
    })
    awards.register_award("fancy_vend:shop_keeper",{
        title = "Shop Keeper",
        description = "Configure 10 vendors or depositors.",
        icon = "player_vend_front.png^awards_level3.png",
    })
    awards.register_award("fancy_vend:merchant",{
        title = "Merchant",
        description = "Configure 25 vendors or depositors.",
        icon = "player_vend_front.png^awards_level4.png",
    })
    awards.register_award("fancy_vend:super_merchant",{
        title = "Super Merchant",
        description = "Configure 100 vendors or depositors. How do you even have this much stuff to sell?",
        icon = "player_vend_front.png^awards_level5.png",
    })
    awards.register_award("fancy_vend:god_merchant",{
        title = "God Merchant",
        description = "Configure 9001 vendors or depositors. Ok wot.",
        icon = "player_vend_front.png^awards_level6.png",
        secret = true, -- Oi. Cheater.
    })
end

local tmp = {}

minetest.register_entity("fancy_vend:display_item",{
    hp_max = 1,
    visual = "wielditem",
    visual_size = {x = 0.33, y = 0.33},
    collisionbox = {0, 0, 0, 0, 0, 0},
    physical = false,
    textures = {"air"},
    on_activate = function(self, staticdata)
        if tmp.nodename ~= nil and tmp.texture ~= nil then
            self.nodename = tmp.nodename
            tmp.nodename = nil
            self.texture = tmp.texture
            tmp.texture = nil
        else
            if staticdata ~= nil and staticdata ~= "" then
                local data = staticdata:split(';')
                if data and data[1] and data[2] then
                    self.nodename = data[1]
                    self.texture = data[2]
                end
            end
        end
        if self.texture ~= nil then
            self.object:set_properties({textures = {self.texture}})
        end
        self.object:set_properties({automatic_rotate = autorotate_speed})
    end,
    get_staticdata = function(self)
        if self.nodename ~= nil and self.texture ~= nil then
            return self.nodename .. ';' .. self.texture
        end
        return ""
    end,
})

local function remove_item(pos)
    local objs = nil
    objs = minetest.get_objects_inside_radius(pos, .5)
    if objs then
        for _, obj in ipairs(objs) do
            if obj and obj:get_luaentity() and obj:get_luaentity().name == "fancy_vend:display_item" then
                obj:remove()
            end
        end
    end
end

local function update_item(pos, node)
    pos.y = pos.y + 1
    remove_item(pos)
    if minetest.get_node(pos).name ~= "fancy_vend:display_node" then
        minetest.log("warning", "[fancy_vend]: Placing display item inside "..minetest.get_node(pos).name.." at "..minetest.pos_to_string(pos).." is not permitted, aborting")
        return
    end
    pos.y = pos.y - 1
    local meta = minetest.get_meta(pos)
    if meta:get_string("item") ~= "" then
        pos.y = pos.y + (12 / 16 + 0.11)
        tmp.nodename = node.name
        tmp.texture = ItemStack(meta:get_string("item")):get_name()
        local e = minetest.add_entity(pos,"fancy_vend:display_item")
        pos.y = pos.y - (12 / 16 + 0.11)
    end
end

-- LBM to refresh entities after clearobjects
minetest.register_lbm({
    label = "Refresh vendor display",
    name = "fancy_vend:display_refresh",
    nodenames = {"fancy_vend:display_node"},
    run_at_every_load = true,
    action = function(pos, node)
        if not next(minetest.get_objects_inside_radius(pos, 0.5)) then
            pos.y = pos.y - 1
            update_item(pos, node)
        end
    end
})

local function set_vendor_settings(pos, SettingsDef)
    local meta = minetest.get_meta(pos)
    meta:set_string("settings", minetest.serialize(SettingsDef))
end

local function reset_vendor_settings(pos)
    local settings_default = {
        input_item = "", -- Don't change this unless you plan on setting this up to add this item to the inventories
        output_item = "", -- Don't change this unless you plan on setting this up to add this item to the inventories
        input_item_qty = 1,
        output_item_qty = 1,
        admin_vendor = false,
        depositor = false,
        currency_eject = false,
        accept_output_only = false,
        split_incoming_stacks = false,
        inactive_force = false,
        accept_worn_input = true,
        accept_worn_output = true,
        digiline_channel = "",
        co_sellers = "",
        banned_buyers = "",
        auto_sort = false,
    }
    set_vendor_settings(pos, settings_default)
    return settings_default
end

local function get_vendor_settings(pos)
    local meta = minetest.get_meta(pos)
    local settings = minetest.deserialize(meta:get_string("settings"))
    if not settings then
        return reset_vendor_settings(pos)
    else
        -- If settings added by newer versions of fancy_vend are nil then send defaults
        if settings.auto_sort == nil then
            settings.auto_sort = false
        end

        -- Sanitatize number values (backwards compat)
        settings.input_item_qty = (type(settings.input_item_qty) == "number" and math.abs(settings.input_item_qty) or 1)
        settings.output_item_qty = (type(settings.output_item_qty) == "number" and math.abs(settings.output_item_qty) or 1)
        return settings
    end
end

local function can_buy_from_vendor(pos, player)
    local settings = get_vendor_settings(pos)
    local banned_buyers = string.split((settings.banned_buyers or ""),",")
    for i in pairs(banned_buyers) do
        if banned_buyers[i] == player:get_player_name() then
            return false
        end
    end
    return true
end

local function can_modify_vendor(pos, player)
    local meta = minetest.get_meta(pos)
    local inv = meta:get_inventory()
    local is_owner = false
    if meta:get_string("owner") == player:get_player_name() or minetest.check_player_privs(player, {protection_bypass=true}) then
        is_owner = true
    end
    return is_owner
end

local function can_dig_vendor(pos, player)
    local meta = minetest.get_meta(pos);
    local inv = meta:get_inventory()
    return inv:is_empty("main") and can_modify_vendor(pos, player)
end

local function can_access_vendor_inv(player, pos)
    local meta = minetest.get_meta(pos)
    if minetest.check_player_privs(player, {protection_bypass=true}) or meta:get_string("owner") == player:get_player_name() then return true end
    local settings = get_vendor_settings(pos)
    local co_sellers = string.split(settings.co_sellers,",")
    for i in pairs(co_sellers) do
        if co_sellers[i] == player:get_player_name() then
            return true
        end
    end
    return false
end

-- Inventory helpers:

-- Function to sort inventory (Taken from technic_chests)
local function sort_inventory(inv)
    local inlist = inv:get_list("main")
    local typecnt = {}
    local typekeys = {}
    for _, st in ipairs(inlist) do
        if not st:is_empty() then
            local n = st:get_name()
            local w = st:get_wear()
            local m = st:get_metadata()
            local k = string.format("%s %05d %s", n, w, m)
            if not typecnt[k] then
                typecnt[k] = {
                    name = n,
                    wear = w,
                    metadata = m,
                    stack_max = st:get_stack_max(),
                    count = 0,
                }
                table.insert(typekeys, k)
            end
            typecnt[k].count = typecnt[k].count + st:get_count()
        end
    end
    table.sort(typekeys)
    local outlist = {}
    for _, k in ipairs(typekeys) do
        local tc = typecnt[k]
        while tc.count > 0 do
            local c = math.min(tc.count, tc.stack_max)
            table.insert(outlist, ItemStack({
                name = tc.name,
                wear = tc.wear,
                metadata = tc.metadata,
                count = c,
            }))
            tc.count = tc.count - c
        end
    end
    if #outlist > #inlist then return end
    while #outlist < #inlist do
        table.insert(outlist, ItemStack(nil))
    end
    inv:set_list("main", outlist)
end

local function free_slots(inv, listname, itemname, quantity)
    local size = inv:get_size(listname)
    local free = 0
    for i=1,size do
        local stack = inv:get_stack(listname, i)
        if stack:is_empty() or stack:get_free_space() > 0 then
            if stack:is_empty() then
                free = free + ItemStack(itemname):get_stack_max()
            elseif stack:get_name() == itemname then
                free = free + stack:get_free_space()
            end
        end
    end
    if free < quantity then
        return false
    else
        return true
    end
end

local function inv_insert(inv, listname, itemstack, quantity, from_table, pos, input_eject)
    local stackmax = itemstack:get_stack_max()
    local name = itemstack:get_name()
    local stacks = {}
    local remaining_quantity = quantity

    -- Add the full stacks to the list
    while remaining_quantity > stackmax do
        table.insert(stacks, {name = name, count = stackmax})
        remaining_quantity = remaining_quantity - stackmax
    end
    -- Add the remaining stack to the list
    table.insert(stacks, {name = name, count = remaining_quantity})

   -- If tool add wears ignores if from_table = nil (eg, due to vendor beig admin vendor)
    if minetest.registered_tools[name] and from_table then
        for i in pairs(stacks) do
            local from_item_table = from_table[i].item:to_table()
            stacks[i].wear = from_item_table.wear
        end
    end

    -- if has metadata add metadata
    if from_table then
        for i in pairs(stacks) do
            local from_item_table = from_table[i].item:to_table()
            if from_item_table.name == name then
                if from_item_table.metadata then
                    stacks[i].metadata = from_item_table.metadata -- Apparently some mods *cough* digtron *cough* do use deprecated metadata strings
                end
                if from_item_table.meta then
                    stacks[i].meta = from_item_table.meta -- Most mods use metadata tables which is the correct method but ok
                end
            end
        end
    end

    -- Add to inventory or eject to pipeworks/hoppers (whichever is applicable)
    local output_tube_connected = false
    local output_hopper_connected = false
    if input_eject and pos then
        local pos_under = vector.new(pos)
        pos_under.y = pos_under.y - 1
        local node_under = minetest.get_node(pos_under)
        if minetest.get_item_group(node_under.name, "tubedevice") > 0 then
            output_tube_connected = true
        end
        if node_under.name == "hopper:hopper" or node_under.name == "hopper:hopper_side" then
            output_hopper_connected = true
        end
    end
    for i in pairs(stacks) do
        if output_tube_connected then
            pipeworks.tube_inject_item(pos, pos, vector.new(0, -1, 0), stacks[i], minetest.get_meta(pos):get_string("owner"))
        else
            local leftovers = ItemStack(stacks[i])
            if output_hopper_connected then
                local pos_under = {x = pos.x, y = pos.y-1, z = pos.z}
                local hopper_inv = minetest.get_meta(pos_under):get_inventory()
                leftovers = hopper_inv:add_item("main", leftovers)
            end
            if not leftovers:is_empty() then
                inv:add_item(listname, leftovers)
            end
        end
    end
end

local function inv_remove(inv, listname, remove_table, itemstring, quantity)
    local count = 0
    for i in pairs(remove_table) do
        count = count + remove_table[i].item:get_count()
        inv:set_stack(listname, remove_table[i].id, nil)
    end
    -- Add back items if too many were taken
    if count > quantity then
        inv:add_item(listname, ItemStack({name = itemstring, count = count - quantity}))
    end
end

local function inv_contains_items(inv, listname, itemstring, quantity, ignore_wear)
    local minimum = quantity
    local get_items = {}
    local count = 0

    for i=1,inv:get_size(listname) do
        local stack = inv:get_stack(listname, i)
        if stack:get_name() == itemstring then
            if ignore_wear or (not minetest.registered_tools[itemstring] or stack:get_wear() == 0) then
                count = count + stack:get_count()
                table.insert(get_items, {id=i, item=stack})
                if count >= minimum then
                    return true, get_items
                end
            end
        end
    end
    return false
end

local function get_vendor_status(pos)
    local settings = get_vendor_settings(pos)
    local meta = minetest.get_meta(pos)
    local inv = meta:get_inventory()
    if all_inactive_force then
        return false, "all_inactive_force"
    elseif settings.input_item == "" or settings.output_item == "" then
        return false, "unconfigured"
    elseif settings.inactive_force then
        return false, "inactive_force"
    elseif not minetest.check_player_privs(meta:get_string("owner"), {admin_vendor=true}) and settings.admin_vendor == true then
        return false, "no_privs"
    elseif not inv_contains_items(inv, "main", settings.output_item, settings.output_item_qty, settings.accept_worn_output) and not settings.admin_vendor then
        return false, "no_output"
    elseif not free_slots(inv, "main", settings.input_item, settings.input_item_qty) and not settings.admin_vendor then
        return false, "no_room"
    else
        return true
    end
end

local function make_inactive_string(errorcode)
    local status_str = ""
    if errorcode == "unconfigured" then
        status_str = status_str.." (unconfigured)"
    elseif errorcode == "inactive_force" then
        status_str = status_str.." (forced)"
    elseif errorcode == "no_output" then
        status_str = status_str.." (out of stock)"
    elseif errorcode == "no_room" then
        status_str = status_str.." (no room)"
    elseif errorcode == "no_privs" then
        status_str = status_str.." (seller has insufficient privilages)"
    elseif errorcode == "all_inactive_force" then
        status_str = status_str.." (all vendors disabled temporarily by admin)"
    end
    return status_str
end

-- Various email and tell mod support

-- This function takes the position of a vendor and alerts the owner if it has just been emptied
local email_loaded, tell_loaded, mail_loaded = minetest.get_modpath("email"), minetest.get_modpath("tell"), minetest.get_modpath("mail")
local function alert_owner_if_empty(pos)
    if no_alerts then return end

    local meta = minetest.get_meta(pos)
    local settings = get_vendor_settings(pos)
    local owner = meta:get_string("owner")
    local alerted = stb(meta:get_string("alerted") or "false") -- check
    local status, errorcode = get_vendor_status(pos)

    -- Message to send
    local stock_msg = "Your vendor trading "..settings.input_item_qty.." "..minetest.registered_items[settings.input_item].description.." for "..settings.output_item_qty.." "..minetest.registered_items[settings.output_item].description.." at position "..minetest.pos_to_string(pos, 0).." has just run out of stock."

    if not alerted and not status and errorcode == "no_output" then
        -- Rubenwardy's Email Mod: https://github.com/rubenwardy/email
        if mail_loaded then
            local inbox = {}

            -- load messages
            if not mail.apiversion then
              -- cheapie's mail mod https://cheapiesystems.com/git/mail/
              if not mail.messages[owner] then mail.messages[owner] = {} end
              inbox = mail.messages[owner]

            elseif mail.apiversion >= 1.1 then
              -- webmail fork https://github.com/thomasrudin-mt/mail (per player storage)
              inbox = mail.getMessages(owner)

            end

            -- Instead of filling their inbox with mail, get the last message sent by "Fancy Vend" and append to the message
            -- If there is no last message, then create a new one

            local message
            for i, msg in pairs(inbox) do
                if msg.sender == "Fancy Vend" then -- Put a space in the name to avoid impersonation
                    message = msg
                end
            end

            if message then
                -- Set the message as unread
                message.unread = true

                -- Append to the end
                message.body = message.body..stock_msg.."\n"
            else
                mail.send("Fancy Vend", owner, "You have unstocked vendors!", stock_msg.."\n")
            end

            -- save messages
            if not mail.apiversion then
              -- cheapie's mail mod https://cheapiesystems.com/git/mail/
              mail.save()

            elseif mail.apiversion >= 1.1 then
              -- webmail fork https://github.com/thomasrudin-mt/mail
              mail.setMessages(owner, inbox)

            end

            meta:set_string("alerted", "true")

            return

        elseif email_loaded then
            email.send_mail("Fancy Vend", owner, stock_msg)

            meta:set_string("alerted", "true")

            return

        elseif tell_loaded then
            -- octacians tell mod https://github.com/octacian/tell
            tell.add(owner, "Fancy Vend", stock_msg)

            meta:set_string("alerted", "true")

            return
        end
    end
end

local function run_inv_checks(pos, player, lots)
    local settings = get_vendor_settings(pos)
    local meta = minetest.get_meta(pos)
    local inv = meta:get_inventory()
    local player_inv = player:get_inventory()

    local ct = {}

    -- Get input and output quantities after multiplying by lot count
    local output_qty = settings.output_item_qty * lots
    local input_qty = settings.input_item_qty * lots

    -- Perform inventory checks
    ct.player_has, ct.player_item_table = inv_contains_items(player_inv, "main", settings.input_item, input_qty, settings.accept_worn_input)
    ct.vendor_has, ct.vendor_item_table = inv_contains_items(inv, "main", settings.output_item, output_qty, settings.accept_worn_output)
    ct.player_fits = free_slots(player_inv, "main", settings.output_item, output_qty)
    ct.vendor_fits = free_slots(inv, "main", settings.input_item, input_qty)

    if ct.player_has and ct.vendor_has and ct.player_fits and ct.vendor_fits then
        ct.overall = true
    else
        ct.overall = false
    end

    return ct
end

local function get_max_lots(pos, player)
    local max = 0

    while run_inv_checks(pos, player, max).overall do
        max = max + 1
    end

    return max
end

local function make_purchase(pos, player, lots)
    if not can_buy_from_vendor(pos, player) then
        return false, "You cannot purchase from this vendor"
    end

    local settings = get_vendor_settings(pos)
    local meta = minetest.get_meta(pos)
    local inv = meta:get_inventory()
    local player_inv = player:get_inventory()
    local status, errorcode = get_vendor_status(pos)

    -- Double check settings, vendors which were incorrectly set up before this bug fix won't matter anymore
    settings.input_item_qty = math.abs(settings.input_item_qty)
    settings.output_item_qty = math.abs(settings.output_item_qty)

    if status then
        -- Get input and output quantities after multiplying by lot count
        local output_qty = settings.output_item_qty * lots
        local input_qty = settings.input_item_qty * lots

        -- Perform inventory checks
        local ct = run_inv_checks(pos, player, lots)

        if ct.player_has then
            if ct.player_fits then
                if settings.admin_vendor then
                    minetest.log("action", player:get_player_name().." trades "..settings.input_item_qty.." "..settings.input_item.." for "..settings.output_item_qty.." "..settings.output_item.." using vendor at "..minetest.pos_to_string(pos))

                    inv_remove(player_inv, "main", ct.player_item_table, settings.input_item, input_qty)
                    inv_insert(player_inv, "main", ItemStack(settings.output_item), output_qty, nil)

                    if minetest.get_modpath("digilines") then
                        send_message(pos, settings.digiline_channel, msg)
                    end

                    return true, "Trade successful"
                elseif ct.vendor_has then
                    if ct.vendor_fits then
                        minetest.log("action", player:get_player_name().." trades "..settings.input_item_qty.." "..settings.input_item.." for "..settings.output_item_qty.." "..settings.output_item.." using vendor at "..minetest.pos_to_string(pos))

                        inv_remove(inv, "main", ct.vendor_item_table, settings.output_item, output_qty)
                        inv_remove(player_inv, "main", ct.player_item_table, settings.input_item, input_qty)
                        inv_insert(player_inv, "main", ItemStack(settings.output_item), output_qty, ct.vendor_item_table)
                        inv_insert(inv, "main", ItemStack(settings.input_item), input_qty, ct.player_item_table, pos, (minetest.get_modpath("pipeworks") and settings.currency_eject))

                        -- Run mail mod checks
                        alert_owner_if_empty(pos)

                        return true, "Trade successful"
                    else
                        return false, "Vendor has insufficient space"
                    end
                else
                    return false, "Vendor has insufficient resources"
                end
            else
                return false, "You have insufficient space"
            end
        else
            return false, "You have insufficient funds"
        end
    else
        return false, "Vendor is inactive"..make_inactive_string(errorcode)
    end
end

local function get_vendor_buyer_fs(pos, player, lots)
    local base = "size[8,9]"..
        "label[0,0;Owner wants:]"..
        "label[0,1.25;for:]"..
        "button[0,2.7;2,1;buy;Buy]"..
        "label[2.8,2.9;lots.]"..
        "button[0,3.6;2,1;lot_fill;Fill lots to max.]"..
        "list[current_player;main;0,4.85;8,1;]"..
        "list[current_player;main;0,6.08;8,3;8]"..
        "listring[current_player;main]"..
        "field_close_on_enter[lot_count;false]"

    -- Add dynamic elements
    local settings = get_vendor_settings(pos)
    local meta = minetest.get_meta(pos)
    local status, errorcode = get_vendor_status(pos)

    local itemstuff =
    "item_image_button[0,0.4;1,1;"..settings.input_item..";ignore;]"..
    "label[0.9,0.6;"..settings.input_item_qty.." "..minetest.registered_items[settings.input_item].description.."]"..
    "item_image_button[0,1.7;1,1;"..settings.output_item..";ignore;]"..
    "label[0.9,1.9;"..settings.output_item_qty.." "..minetest.registered_items[settings.output_item].description.."]"

    local status_str
    if status then
        status_str = "active"
    else
        status_str = "inactive"..make_inactive_string(errorcode)
    end
    local status_fs =
    "label[4,0.4;Vendor status: "..status_str.."]"..
    "label[4,0.8;Message: "..meta:get_string("message").."]"..
    "label[4,0;Vendor owned by: "..meta:get_string("owner").."]"

    local setting_specific = ""
    if not settings.accept_worn_input then
        setting_specific = setting_specific.."label[4,1.6;Vendor will not accept worn tools.]"
    end
    if not settings.accept_worn_output then
        setting_specific = setting_specific.."label[4,1.2;Vendor will not sell worn tools.]"
    end

    local fields = "field[2.2,3.2;1,0.6;lot_count;;"..(lots or 1).."]"

    local fs = base..itemstuff..status_fs..setting_specific..fields
    return fs
end

local function get_vendor_settings_fs(pos)
    local base = "size[9,9]"..
        "label[2.8,0.5;Input item]"..
        "label[6.8,0.5;Output item]"..
        "image[0,1.3;1,1;debug_btn.png]"..
        "item_image_button[0,2.3;1,1;default:book;button_log;]"..
        "item_image_button[0,3.3;1,1;default:gold_ingot;button_buy;]"..
        "list[current_player;main;1,4.85;8,1;]"..
        "list[current_player;main;1,6.08;8,3;8]"..
        "listring[current_player;main]"..
        "button_exit[0,8;1,1;btn_exit;Done]"

    -- Add dynamic elements
    local pos_str = pos.x..","..pos.y..","..pos.z
    local settings = get_vendor_settings(pos)

    if settings.admin_vendor then
        base = base.."item_image[0,0.3;1,1;default:chest]"
    else
        base = base.."item_image_button[0,0.3;1,1;default:chest;button_inv;]"
    end

    local inv =
        "list[nodemeta:"..pos_str..";wanted_item;1,0.3;1,1;]"..
        "list[nodemeta:"..pos_str..";given_item;5,0.3;1,1;]"..
        "listring[nodemeta:"..pos_str..";wanted_item]"..
        "listring[nodemeta:"..pos_str..";given_item]"

    local fields =
        "field[2.2,0.8;1,0.6;input_item_qty;;"..settings.input_item_qty.."]"..
        "field[6.2,0.8;1,0.6;output_item_qty;;"..settings.output_item_qty.."]"..
        "field[1.3,4.1;2.66,1;co_sellers;Co-Sellers:;"..settings.co_sellers.."]"..
        "field[3.86,4.1;2.66,1;banned_buyers;Banned Buyers:;"..settings.banned_buyers.."]"..
        "field_close_on_enter[input_item_qty;false]"..
        "field_close_on_enter[output_item_qty;false]"..
        "field_close_on_enter[co_sellers;false]"..
        "field_close_on_enter[banned_buyers;false]"

    local checkboxes =
        "checkbox[1,2.2;inactive_force;Force vendor into an inactive state.;"..bts(settings.inactive_force).."]"..
        "checkbox[1,2.6;depositor;Set this vendor to a Depositor.;"..bts(settings.depositor).."]"..
        "checkbox[1,3.0;accept_worn_output;Sell worn tools.;"..bts(settings.accept_worn_output).."]"..
        "checkbox[5,3.0;accept_worn_input;Buy worn tools.;"..bts(settings.accept_worn_input).."]"..
        "checkbox[5,2.6;auto_sort;Automatically sort inventory.;"..bts(settings.auto_sort).."]"

    -- Admin vendor checkbox only if owner is admin
    local meta = minetest.get_meta(pos)
    if minetest.check_player_privs(meta:get_string("owner"), {admin_vendor=true}) or settings.admin_vendor then
        checkboxes = checkboxes..
            "checkbox[5,2.2;admin_vendor;Set vendor to an admin vendor.;"..bts(settings.admin_vendor).."]"
    end

    -- Optional dependancy specific elements
    if minetest.get_modpath("pipeworks") or minetest.get_modpath("hopper") then
        checkboxes = checkboxes..
            "checkbox[1,1.7;currency_eject;Eject incoming currency.;"..bts(settings.currency_eject).."]"
        if minetest.get_modpath("pipeworks") then
            checkboxes = checkboxes..
            "checkbox[5,1.3;accept_output_only;Accept for-sale item only.;"..bts(settings.accept_output_only).."]"..
            "checkbox[1,1.3;split_incoming_stacks;Split incoming stacks.;"..bts(settings.split_incoming_stacks).."]"
        end
    end

    if minetest.get_modpath("digilines") then
        fields = fields..
            "field[6.41,4.1;2.66,1;digiline_channel;Digiline Channel:;"..settings.digiline_channel.."]"..
            "field_close_on_enter[digiline_channel;false]"
    end

    local fs = base..inv..fields..checkboxes
    return fs
end

local function get_vendor_default_fs(pos, player)
    local base = "size[16,11]"..
        "item_image[0,0.3;1,1;default:chest]"..
        "list[current_player;main;4,6.85;8,1;]"..
        "list[current_player;main;4,8.08;8,3;8]"..
        "listring[current_player;main]"..
        "button[1,6.85;3,1;inv_tovendor;All To Vendor]"..
        "button[12,6.85;3,1;inv_fromvendor;All From Vendor]"..
        "button[1,8.08;3,1;inv_output_tovendor;Output To Vendor]"..
        "button[12,8.08;3,1;inv_input_fromvendor;Input From Vendor]"..
        "button[1,9.31;3,1;sort;Sort Inventory]"..
        "button_exit[0,10;1,1;btn_exit;Done]"

    -- Add dynamic elements
    local pos_str = pos.x..","..pos.y..","..pos.z
    local inv_lists =
        "list[nodemeta:"..pos_str..";main;1,0.3;15,6;]"..
        "listring[nodemeta:"..pos_str..";main]"

    local settings_btn = ""
    if can_modify_vendor(pos, player) then
        settings_btn =
        "image_button[0,1.3;1,1;debug_btn.png;button_settings;]"..
        "item_image_button[0,2.3;1,1;default:book;button_log;]"..
        "item_image_button[0,3.3;1,1;default:gold_ingot;button_buy;]"
    else
        settings_btn =
        "image[0,1.3;1,1;debug_btn.png]"..
        "item_image[0,2.3;1,1;default:book]"..
        "item_image[0,3.3;1,1;default:gold_ingot;button_buy;]"
    end

    local fs = base..inv_lists..settings_btn
    return fs
end

local function get_vendor_log_fs(pos)
    local base = "size[9,9]"..
        "image_button[0,1.3;1,1;debug_btn.png;button_settings;]"..
        "item_image[0,2.3;1,1;default:book]"..
        "item_image_button[0,3.3;1,1;default:gold_ingot;button_buy;]"..
        "button_exit[0,8;1,1;btn_exit;Done]"

    -- Add dynamic elements
    local meta = minetest.get_meta(pos)
    local logs = minetest.deserialize(meta:get_string("log"))

    local settings = get_vendor_settings(pos)
    if settings.admin_vendor then
        base = base.."item_image[0,0.3;1,1;default:chest]"
    else
        base = base.."item_image_button[0,0.3;1,1;default:chest;button_inv;]"
    end

    if not logs then logs = {"Error loading logs",} end
    local logs_tl =
        "textlist[1,0.5;7.8,8.6;logs;"..table.concat(logs, ",").."]"..
        "label[1,0;Showing (up to "..max_logs..") recent log entries:]"

    local fs = base..logs_tl
    return fs
end

local function show_buyer_formspec(player, pos)
    minetest.show_formspec(player:get_player_name(), "fancy_vend:buyer;"..minetest.pos_to_string(pos), get_vendor_buyer_fs(pos, player, nil))
end

local function show_vendor_formspec(player, pos)
    local settings = get_vendor_settings(pos)
    if can_access_vendor_inv(player, pos) then
        local status, errorcode = get_vendor_status(pos)
        if ((not status and errorcode == "unconfigured") and can_modify_vendor(pos, player)) or settings.admin_vendor then
            minetest.show_formspec(player:get_player_name(), "fancy_vend:settings;"..minetest.pos_to_string(pos), get_vendor_settings_fs(pos))
        else
            minetest.show_formspec(player:get_player_name(), "fancy_vend:default;"..minetest.pos_to_string(pos), get_vendor_default_fs(pos, player))
        end
    else
        show_buyer_formspec(player, pos)
    end
end

local function swap_vendor(pos, vendor_type)
    local node = minetest.get_node(pos)
    node.name = vendor_type
    minetest.swap_node(pos, node)
end

local function get_correct_vendor(settings)
    if settings.admin_vendor then
        if settings.depositor then
            return "fancy_vend:admin_depo"
        else
            return "fancy_vend:admin_vendor"
        end
    else
        if settings.depositor then
            return "fancy_vend:player_depo"
        else
            return "fancy_vend:player_vendor"
        end
    end
end

local function is_vendor(name)
    local vendor_names = {
        "fancy_vend:player_vendor",
        "fancy_vend:player_depo",
        "fancy_vend:admin_vendor",
        "fancy_vend:admin_depo",
    }
    for i,n in ipairs(vendor_names) do
        if name == n then
            return true
        end
    end
    return false
end

local function refresh_vendor(pos)
    local node = minetest.get_node(pos)
    if node.name:split(":")[1] ~= "fancy_vend" then
        return false, "not a vendor"
    end

    local settings = get_vendor_settings(pos)
    local meta = minetest.get_meta(pos)
    local status, errorcode = get_vendor_status(pos)
    local correct_vendor = get_correct_vendor(settings)

    if status or errorcode ~= "no_output" then
        meta:set_string("alerted", "false")
    end

    if status then
--        meta:set_string("infotext", (settings.admin_vendor and "Admin" or "Player").." Vendor trading "..settings.input_item_qty.." "..minetest.registered_items[settings.input_item].description.." for "..settings.output_item_qty.." "..minetest.registered_items[settings.output_item].description.." (owned by " .. meta:get_string("owner") .. ")")

        if meta:get_string("configured") == "" then
            meta:set_string("configured", "true")
            if minetest.get_modpath("awards") then
                local name = meta:get_string("owner")
                local data = awards.player(name)

                -- Ensure fancy_vend_configure table is in data
                if not data.fancy_vend_configure then
                    data.fancy_vend_configure = {}
                end

                awards.increment_item_counter(data, "fancy_vend_configure", correct_vendor)

                total_item_count = 0

                for k, v in pairs(data.fancy_vend_configure) do
                    total_item_count = total_item_count + v
                end

                if awards.get_item_count(data, "fancy_vend_configure", "fancy_vend:player_vendor") >= 1 then
                    awards.unlock(name, "fancy_vend:seller")
                end
                if awards.get_item_count(data, "fancy_vend_configure", "fancy_vend:player_depo") >= 1 then
                    awards.unlock(name, "fancy_vend:trader")
                end
                if total_item_count >= 10 then
                    awards.unlock(name, "fancy_vend:shop_keeper")
                end
                if total_item_count >= 25 then
                    awards.unlock(name, "fancy_vend:merchant")
                end
                if total_item_count >= 100 then
                    awards.unlock(name, "fancy_vend:super_merchant")
                end
                if total_item_count >= 9001 then
                    awards.unlock(name, "fancy_vend:god_merchant")
                end
            end
        end

        if settings.depositor then
            if meta:get_string("item") ~= settings.input_item then
                meta:set_string("item", settings.input_item)
                update_item(pos, node)
            end
        else
            if meta:get_string("item") ~= settings.output_item then
                meta:set_string("item", settings.output_item)
                update_item(pos, node)
            end
        end
    else
        meta:set_string("infotext", "Inactive "..(settings.admin_vendor and "Admin" or "Player").." Vendor"..make_inactive_string(errorcode).." (owned by " .. meta:get_string("owner") .. ")")
        if meta:get_string("item") ~= "fancy_vend:inactive" then
            meta:set_string("item", "fancy_vend:inactive")
            update_item(pos, node)
        end

        if not alerted and not status and errorcode == "no_room" then
            minetest.chat_send_player(meta:get_string("owner"), "[Fancy_Vend]: Error with vendor at "..minetest.pos_to_string(pos, 0)..": does not have room for payment.")
            meta:set_string("alerted", "true")
        end
    end

    if correct_vendor ~= node.name then
        swap_vendor(pos, correct_vendor)
    end
end

local function move_inv(frominv, toinv, filter)
    for i, v in ipairs(frominv:get_list("main") or {}) do
        if v:get_name() == filter or not filter then
            if toinv:room_for_item("main", v) then
                local leftover = toinv:add_item("main", v)

                frominv:remove_item("main", v)

                if leftover
                and not leftover:is_empty() then
                    frominv:add_item("main", v)
                end
            end
        end
    end
end

minetest.register_on_player_receive_fields(function(player, formname, fields)
    local name = formname:split(":")[1]
    if name ~= "fancy_vend" then return end
    local formtype = formname:split(":")[2]
    formtype = formtype:split(";")[1]
    local pos = minetest.string_to_pos(formname:split(";")[2])
    if not pos then return end

    local node = minetest.get_node(pos)
    if not is_vendor(node.name) then return end

    local meta = minetest.get_meta(pos)
    local inv = meta:get_inventory()
    local player_inv = player:get_inventory()
    local settings = get_vendor_settings(pos)

    -- Handle settings changes
    if can_modify_vendor(pos, player) then
        for i in pairs(fields) do
            if stb(fields[i]) ~= settings[i] then
                settings[i] = stb(fields[i])
            end
        end

        -- Check number-only fields contain only numbers
        if not tonumber(settings.input_item_qty) then
            settings.input_item_qty = 1
        else
            settings.input_item_qty = math.floor(math.abs(tonumber(settings.input_item_qty)))
        end
        if not tonumber(settings.output_item_qty) then
            settings.output_item_qty = 1
        else
            settings.output_item_qty = math.floor(math.abs(tonumber(settings.output_item_qty)))
        end

        -- Check item quantities aren't too high (which could lead to additional processing for no reason), if so, set it to the maximum the player inventory can handle
        if ItemStack(settings.output_item):get_stack_max() * player_inv:get_size("main") < settings.output_item_qty then
            settings.output_item_qty = ItemStack(settings.output_item):get_stack_max() * player_inv:get_size("main")
        end

        if ItemStack(settings.input_item):get_stack_max() * player_inv:get_size("main") < settings.input_item_qty then
            settings.input_item_qty = ItemStack(settings.input_item):get_stack_max() * player_inv:get_size("main")
        end

        -- Admin vendor priv check
        if not minetest.check_player_privs(meta:get_string("owner"), {admin_vendor=true}) and fields.admin_vendor == "true" then
            settings.admin_vendor = false
        end

        set_vendor_settings(pos, settings)
        refresh_vendor(pos)
    end

    if fields.quit then
        if can_access_vendor_inv(player, pos) and settings.auto_sort then
            sort_inventory(inv)
        end
        return true
    end

    if fields.sort and can_access_vendor_inv(player, pos) then
        sort_inventory(inv)
    end

    if fields.buy then
        local lots = math.floor(tonumber(fields.lot_count) or 1)
	-- prevent negative numbers
	lots = math.max(lots, 1)
        local success, message = make_purchase(pos, player, lots)
        if success then
            -- Add to vendor logs
            local logs = minetest.deserialize(meta:get_string("log"))
            for i in pairs(logs) do
                if i >= max_logs then
                    table.remove(logs, 1)
                end
            end
            table.insert(logs, "Player "..player:get_player_name().." purchased "..lots.." lots from this vendor.")
            meta:set_string("log", minetest.serialize(logs))

            -- Send digiline message if applicable
            if minetest.get_modpath("digilines") then
                local msg = {
                    buyer = player:get_player_name(),
                    lots = lots,
                    settings = settings,
                }
                send_message(pos, settings.digiline_channel, msg)
            end
        end
        -- Set message and refresh vendor
        if message then
            meta:set_string("message", message)
        end
        refresh_vendor(pos)
    elseif fields.lot_fill then
        minetest.show_formspec(player:get_player_name(), "fancy_vend:buyer;"..minetest.pos_to_string(pos), get_vendor_buyer_fs(pos, player, get_max_lots(pos, player)))
        return true
    end

    if can_access_vendor_inv(player, pos) then
        if fields.inv_tovendor then
            minetest.log("action", player:get_player_name().." moves inventory contents to vendor at "..minetest.pos_to_string(pos))
            move_inv(player_inv, inv, nil)
            refresh_vendor(pos)
        elseif fields.inv_output_tovendor then
            minetest.log("action", player:get_player_name().." moves output items in inventory to vendor at "..minetest.pos_to_string(pos))
            move_inv(player_inv, inv, settings.output_item)
            refresh_vendor(pos)
        elseif fields.inv_fromvendor then
            minetest.log("action", player:get_player_name().." moves inventory contents from vendor at "..minetest.pos_to_string(pos))
            move_inv(inv, player_inv, nil)
            refresh_vendor(pos)
        elseif fields.inv_input_fromvendor then
            minetest.log("action", player:get_player_name().." moves input items from vendor at "..minetest.pos_to_string(pos))
            move_inv(inv, player_inv, settings.input_item)
            refresh_vendor(pos)
        end
    end

    -- Handle page changes
    if fields.button_log then
        minetest.show_formspec(player:get_player_name(), "fancy_vend:log;"..minetest.pos_to_string(pos), get_vendor_log_fs(pos))
        return
    elseif fields.button_settings then
        minetest.show_formspec(player:get_player_name(), "fancy_vend:settings;"..minetest.pos_to_string(pos), get_vendor_settings_fs(pos))
        return
    elseif fields.button_inv then
        minetest.show_formspec(player:get_player_name(), "fancy_vend:default;"..minetest.pos_to_string(pos), get_vendor_default_fs(pos, player))
        return
    elseif fields.button_buy then
        minetest.show_formspec(player:get_player_name(), "fancy_vend:buyer;"..minetest.pos_to_string(pos), get_vendor_buyer_fs(pos, player, (tonumber(fields.lot_count) or 1)))
        return
    end

    -- Update formspec
    if formtype == "log" then
        minetest.show_formspec(player:get_player_name(), "fancy_vend:log;"..minetest.pos_to_string(pos), get_vendor_log_fs(pos, player))
    elseif formtype == "settings" then
        minetest.show_formspec(player:get_player_name(), "fancy_vend:settings;"..minetest.pos_to_string(pos), get_vendor_settings_fs(pos, player))
    elseif formtype == "default" then
        minetest.show_formspec(player:get_player_name(), "fancy_vend:default;"..minetest.pos_to_string(pos), get_vendor_default_fs(pos, player))
    elseif formtype == "buyer" then
        minetest.show_formspec(player:get_player_name(), "fancy_vend:buyer;"..minetest.pos_to_string(pos), get_vendor_buyer_fs(pos, player, (tonumber(fields.lot_count) or 1)))
    end
end)

local vendor_template = {
    description = "Vending Machine",
    legacy_facedir_simple = true,
    paramtype2 = "facedir",
    groups = {choppy=2, oddly_breakable_by_hand=2, tubedevice=1, tubedevice_receiver=1},
    selection_box = {
        type = "fixed",
        fixed = {-0.5, -0.5, -0.5, 0.5, 1.5, 0.5},
    },
    is_ground_content = false,
    light_source = 8,
    sounds = default.node_sound_wood_defaults(),
    drop = drop_vendor,
    on_construct = function(pos)
        local meta = minetest.get_meta(pos)
        meta:set_string("infotext", "Unconfigured Player Vendor")
        meta:set_string("message", "Vendor initialized")
        meta:set_string("owner", "")
        local inv = meta:get_inventory()
        inv:set_size("main", 15*6)
        inv:set_size("wanted_item", 1*1)
        inv:set_size("given_item", 1*1)
        reset_vendor_settings(pos)
        meta:set_string("log", "")
    end,
    can_dig = can_dig_vendor,
    on_place = function(itemstack, placer, pointed_thing)
        if pointed_thing.type ~= "node" then return end
        local pointed_node_pos = minetest.get_pointed_thing_position(pointed_thing, false)
        local pointed_node = minetest.get_node(pointed_node_pos)
        if minetest.registered_nodes[pointed_node.name].buildable_to then
            pointed_thing.above = pointed_node_pos
        end
        -- Set variables for access later (for various checks, etc.)
        local name = placer:get_player_name()
        local above_node_pos = table.copy(pointed_thing.above)
        above_node_pos.y = above_node_pos.y + 1
        local above_node = minetest.get_node(above_node_pos).name

        -- If node above is air or the display node, and it is not protected, attempt to place the vendor. If vendor sucessfully places, place display node above, otherwise alert the user
        if (minetest.registered_nodes[above_node].buildable_to or above_node == "fancy_vend:display_node") and not minetest.is_protected(above_node_pos, name) then
            local itemstack, success = minetest.item_place(itemstack, placer, pointed_thing, nil)
            if above_node ~= "fancy_vend:display_node" and success then
                minetest.set_node(above_node_pos, minetest.registered_nodes["fancy_vend:display_node"])
            end
            -- Set owner
            local meta = minetest.get_meta(pointed_thing.above)
            meta:set_string("owner", placer:get_player_name() or "")

            -- Set default meta
            meta:set_string("log", minetest.serialize({"Vendor placed by "..placer:get_player_name(),}))
            reset_vendor_settings(pointed_thing.above)
            refresh_vendor(pointed_thing.above)
        else
            minetest.chat_send_player(name, "Vendors require 2 nodes of space.")
        end

        if minetest.get_modpath("pipeworks") then
            pipeworks.after_place(pointed_thing.above)
        end

        return itemstack
    end,
    on_dig = function(pos, node, digger)
        -- Set variables for access later (for various checks, etc.)
        local name = digger:get_player_name()
        local above_node_pos = table.copy(pos)
        above_node_pos.y = above_node_pos.y + 1

        -- abandon if player shouldn't be able to dig node
        local can_dig = can_dig_vendor(pos, digger)
        if not can_dig then return end

        -- Try remove display node, if the whole node is able to be removed by the player, remove the display node and continue to remove vendor, if it doesn't exist and vendor can be dug continue to remove vendor.
        local success
        if minetest.get_node(above_node_pos).name == "fancy_vend:display_node" then
            if not minetest.is_protected(above_node_pos, name) and not minetest.is_protected(pos, name) then
                minetest.remove_node(above_node_pos)
                remove_item(above_node_pos)
                success = true
            else
                success = false
            end
        else
            if not minetest.is_protected(pos, name) then
                success = true
            else
                success = false
            end
        end

        -- If failed to remove display node, don't remove vendor. since protection for whole vendor was checked at display removal, protection need not be re-checked
        if success then
            minetest.remove_node(pos)
            minetest.handle_node_drops(pos, {drop_vendor}, digger)
            if minetest.get_modpath("pipeworks") then
                pipeworks.after_dig(pos)
            end
        end
    end,
    tube = {
        input_inventory = "main",
        connect_sides = {left = 1, right = 1, back = 1, bottom = 1},
        insert_object = function(pos, node, stack, direction)
            local meta = minetest.get_meta(pos)
            local inv = meta:get_inventory()
            local remaining = inv:add_item("main", stack)
            refresh_vendor(pos)
            return remaining
        end,
        can_insert = function(pos, node, stack, direction)
            local meta = minetest.get_meta(pos)
            local inv = meta:get_inventory()
            local settings = get_vendor_settings(pos)
            if settings.split_stacks then
                stack = stack:peek_item(1)
            end
            if settings.accept_output_only then
                if stack:get_name() ~= settings.output_item then
                    return false
                end
            end
            return inv:room_for_item("main", stack)
        end,
    },
    allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
        if (not can_access_vendor_inv(player, pos)) or to_list == "wanted_item" or to_list == "given_item" then
            return 0
        end
        return count
    end,
    allow_metadata_inventory_put = function(pos, listname, index, stack, player)
        if not can_access_vendor_inv(player, pos) then
            return 0
        end
        if listname == "wanted_item" or listname == "given_item" then
            local inv = minetest.get_meta(pos):get_inventory()
            inv:set_stack(listname, index, ItemStack(stack:get_name()))
            local settings = get_vendor_settings(pos)
            if listname == "wanted_item" then
                settings.input_item = stack:get_name()
            elseif listname == "given_item" then
                settings.output_item = stack:get_name()
            end
            set_vendor_settings(pos, settings)
            return 0
        end
        return stack:get_count()
    end,
    allow_metadata_inventory_take = function(pos, listname, index, stack, player)
        if not can_access_vendor_inv(player, pos) then
            return 0
        end
        if listname == "wanted_item" or listname == "given_item" then
            local inv = minetest.get_meta(pos):get_inventory()
            local fake_stack = inv:get_stack(listname, index)
            fake_stack:take_item(stack:get_count())
            inv:set_stack(listname, index, fake_stack)
            local settings = get_vendor_settings(pos)
            if listname == "wanted_item" then
                settings.input_item = ""
            elseif listname == "given_item" then
                settings.output_item = ""
            end
            set_vendor_settings(pos, settings)
            return 0
        end
        return stack:get_count()
    end,
    on_rightclick = function(pos, node, clicker)
        node = minetest.get_node(pos)
        if node.name == "fancy_vend:display_node" then
            pos.y = pos.y - 1
        end
        show_vendor_formspec(clicker, pos)
    end,
    on_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
        minetest.log("action", player:get_player_name().." moves stuff in vendor at "..minetest.pos_to_string(pos))
        refresh_vendor(pos)
    end,
    on_metadata_inventory_put = function(pos, listname, index, stack, player)
        minetest.log("action", player:get_player_name().." moves "..stack:get_name().." to vendor at "..minetest.pos_to_string(pos))
        refresh_vendor(pos)
    end,
    on_metadata_inventory_take = function(pos, listname, index, stack, player)
        minetest.log("action", player:get_player_name().." takes "..stack:get_name().." from vendor at "..minetest.pos_to_string(pos))
        refresh_vendor(pos)
    end,
    on_blast = function()
        -- TNT immunity
    end,
}

if minetest.get_modpath("pipeworks") then
    vendor_template.digiline = {
        receptor = {},
        effector = {
        action = function() end
        },
        wire = {
        rules = pipeworks.digilines_rules
        },
    }
end

local player_vendor = table.copy(vendor_template)
player_vendor.tiles = {
        "player_vend.png", "player_vend.png",
        "player_vend.png", "player_vend.png",
        "player_vend.png", "player_vend_front.png",
    }

local player_depo = table.copy(vendor_template)
player_depo.tiles = {
        "player_depo.png", "player_depo.png",
        "player_depo.png", "player_depo.png",
        "player_depo.png", "player_depo_front.png",
    }
player_depo.groups.not_in_creative_inventory = 1

local admin_vendor = table.copy(vendor_template)
admin_vendor.tiles = {
        "admin_vend.png", "admin_vend.png",
        "admin_vend.png", "admin_vend.png",
        "admin_vend.png", "admin_vend_front.png",
    }
admin_vendor.groups.not_in_creative_inventory = 1

local admin_depo = table.copy(vendor_template)
admin_depo.tiles = {
        "admin_depo.png", "admin_depo.png",
        "admin_depo.png", "admin_depo.png",
        "admin_depo.png", "admin_depo_front.png",
    }
admin_depo.groups.not_in_creative_inventory = 1

minetest.register_node("fancy_vend:player_vendor", player_vendor)
minetest.register_node("fancy_vend:player_depo", player_depo)
minetest.register_node("fancy_vend:admin_vendor", admin_vendor)
minetest.register_node("fancy_vend:admin_depo", admin_depo)

minetest.register_craft({
    output = "fancy_vend:player_vendor",
    recipe = {
        { "default:gold_ingot",display_node,          "default:gold_ingot"},
        { "default:diamond",   "default:mese_crystal",        "default:diamond"},
        { "default:gold_ingot","default:chest_locked","default:gold_ingot"},
    }
})

-- Hopper support
if minetest.get_modpath("hopper") then
    hopper:add_container({
        {"side", "fancy_vend:player_vendor", "main"}
    })

    hopper:add_container({
        {"side", "fancy_vend:player_depo", "main"}
    })
end

---------------
-- Copy Tool --
---------------

local function get_vendor_pos_and_settings(pointed_thing)
    if pointed_thing.type ~= "node" then return false end
    local pos = minetest.get_pointed_thing_position(pointed_thing, false)
    local node = minetest.get_node(pos)
    if node.name == "fancy_vend:display_node" then
        pos.y = pos.y - 1
        node = minetest.get_node(pos)
    end
    if not is_vendor(node.name) then return false end

    local settings = get_vendor_settings(pos)

    return pos, settings
end

minetest.register_tool("fancy_vend:copy_tool",{
    inventory_image = "copier.png",
    description = "Geminio Wand (For copying vendor settings, right click to save settings, left click to set settings.)",
    stack_max = 1,
    on_place = function(itemstack, placer, pointed_thing)
        local pos, settings = get_vendor_pos_and_settings(pointed_thing)
        if not pos then return end

        local meta = itemstack:get_meta()
        meta:set_string("settings", minetest.serialize(settings))

        minetest.chat_send_player(placer:get_player_name(), "Settings saved.")

        return itemstack
    end,
    on_use = function(itemstack, user, pointed_thing)
        local pos, current_settings = get_vendor_pos_and_settings(pointed_thing)
        if not pos then return end

        local meta = itemstack:get_meta()
        local node_meta = minetest.get_meta(pos)
        local new_settings = minetest.deserialize(meta:get_string("settings"))
        if not new_settings then
        	minetest.chat_send_player(user:get_player_name(), "No settings to set with. Right-click first on the vendor you want to copy settings from.")
        	return
        end

        if can_modify_vendor(pos, user) then
            -- Admin vendor priv check
            if not minetest.check_player_privs(node_meta:get_string("owner"), {admin_vendor=true}) and new_settings.admin_vendor == true then
                settings.admin_vendor = false
            end

            new_settings.input_item = current_settings.input_item
            new_settings.input_item_qty = current_settings.input_item_qty
            new_settings.output_item = current_settings.output_item
            new_settings.output_item_qty = current_settings.output_item_qty

            -- Admin vendor priv check
            if not minetest.check_player_privs(node_meta:get_string("owner"), {admin_vendor=true}) and new_settings.admin_vendor then
                new_settings.admin_vendor = current_settings.admin_vendor
            end

            set_vendor_settings(pos, new_settings)
            refresh_vendor(pos)
            minetest.chat_send_player(user:get_player_name(), "Settings set.")
        else
            minetest.chat_send_player(user:get_player_name(), "You cannot modify this vendor.")
        end
    end,
})

minetest.register_craft({
    output = "fancy_vend:copy_tool",
    recipe = {
        {"default:stick","",                      ""               },
        {"",             "default:obsidian_shard",""               },
        {"",             "",                      "default:diamond"},
    }
})

minetest.register_craft({
    output = "fancy_vend:copy_tool",
    recipe = {
        {"",               "",                      "default:stick"},
        {"",               "default:obsidian_shard",""             },
        {"default:diamond","",                      ""             },
    }
})

---------------------------
-- Vendor Upgrade System --
---------------------------

local old_vendor_mods = string.split((minetest.settings:get("fancy_vend_old_vendor_mods") or ""), ",")
local old_vendor_mods_table = {}

for i in pairs(old_vendor_mods) do
    old_vendor_mods_table[old_vendor_mods[i]] = true
end

local base_upgrade_template = {
    description = "Shop Upgrade (Try and place to upgrade)",
    legacy_facedir_simple = true,
    paramtype2 = "facedir",
    groups = {choppy=2, oddly_breakable_by_hand=2, not_in_creative_inventory=1},
    is_ground_content = false,
    light_source = 8,
    sounds = default.node_sound_wood_defaults(),
    drop = drop_vendor,
    tiles = {
        "player_vend.png", "player_vend.png",
        "player_vend.png", "player_vend.png",
        "player_vend.png", "upgrade_front.png",
    },
    on_place = function(itemstack, placer, pointed_thing)
        return ItemStack(drop_vendor.." "..itemstack:get_count())
    end,
    allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
        local meta = minetest.get_meta(pos)
        if player:get_player_name() ~= meta:get_string("owner") then return 0 end
        return count
    end,
    allow_metadata_inventory_put = function(pos, listname, index, stack, player)
        local meta = minetest.get_meta(pos)
        if player:get_player_name() ~= meta:get_string("owner") then return 0 end
        return stack:get_count()
    end,
    allow_metadata_inventory_take = function(pos, listname, index, stack, player)
        local meta = minetest.get_meta(pos)
        if player:get_player_name() ~= meta:get_string("owner") then return 0 end
        return stack:get_count()
    end,
}

local clear_craft_vendors = {}

if old_vendor_mods_table["currency"] then
    local currency_template = table.copy(base_upgrade_template)

    currency_template.can_dig = function(pos, player)
        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        return inv:is_empty("stock") and inv:is_empty("customers_gave") and inv:is_empty("owner_wants") and inv:is_empty("owner_gives") and (meta:get_string("owner") == player:get_player_name() or minetest.check_player_privs(player:get_player_name(), {protection_bypass=true}))
    end
    currency_template.on_rightclick = function(pos, node, clicker, itemstack)
        local meta = minetest.get_meta(pos)
        local list_name = "nodemeta:"..pos.x..','..pos.y..','..pos.z
        if clicker:get_player_name() == meta:get_string("owner") then
            minetest.show_formspec(clicker:get_player_name(),"fancy_vend:currency_shop_formspec",
                "size[8,9.5]"..
                "label[0,0;" .. "Customers gave:" .. "]"..
                "list["..list_name..";customers_gave;0,0.5;3,2;]"..
                "label[0,2.5;" .. "Your stock:" .. "]"..
                "list["..list_name..";stock;0,3;3,2;]"..
                "label[5,0;" .. "You want:" .. "]"..
                "list["..list_name..";owner_wants;5,0.5;3,2;]"..
                "label[5,2.5;" .. "In exchange, you give:" .. "]"..
                "list["..list_name..";owner_gives;5,3;3,2;]"..
                "list[current_player;main;0,5.5;8,4;]"
            )
        end
    end

    minetest.register_node(":currency:shop", currency_template)

    table.insert(clear_craft_vendors, "currency:shop")
end

if old_vendor_mods_table["easyvend"] then
    local nodes = {"easyvend:vendor", "easyvend:vendor_on", "easyvend:depositor", "easyvend:depositor_on"}
    for i in pairs(nodes) do
        minetest.register_node(":"..nodes[i], base_upgrade_template)
        table.insert(clear_craft_vendors, nodes[i])
    end
end

if old_vendor_mods_table["vendor"] then
    local nodes = {"vendor:vendor", "vendor:depositor"}
    for i in pairs(nodes) do
        minetest.register_node(":"..nodes[i], base_upgrade_template)
        table.insert(clear_craft_vendors, nodes[i])
    end
end

if old_vendor_mods_table["money"] then
    local money_template = table.copy(base_upgrade_template)
    money_template.can_dig = function(pos, player)
        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()
        return inv:is_empty("main") and (meta:get_string("owner") == player:get_player_name() or minetest.check_player_privs(player:get_player_name(), {protection_bypass=true}))
    end
    money_template.on_rightclick = function(pos, node, clicker, itemstack)
        local meta = minetest.get_meta(pos)
        local list_name = "nodemeta:"..pos.x..','..pos.y..','..pos.z
        if clicker:get_player_name() == meta:get_string("owner") then
            minetest.show_formspec(clicker:get_player_name(),"fancy_vend:money_shop_formspec",
                "size[8,10;]"..
                "list["..listname..";main;0,0;8,4;]"..
                "list[current_player;main;0,6;8,4;]"
            )
        end
    end
    local nodes = {"money:barter_shop", "money:shop", "money:admin_shop", "money:admin_barter_shop"}
    for i in pairs(nodes) do
        minetest.register_node(":"..nodes[i], money_template)
        table.insert(clear_craft_vendors, nodes[i])
    end
end

for i_n in pairs(clear_craft_vendors) do
    local currency_crafts = minetest.get_all_craft_recipes(i_n)
    if currency_crafts then
        for i in pairs(currency_crafts) do
            minetest.clear_craft(currency_crafts[i])
        end
    end
end

repo_name: korkut/MeseCraft

license: mit


code:
name = fancy_vend
description = A full-featured, fully-integrated vendor mod for Minetest
author = ChimneySwift
depends = default
optional_depends = pipeworks, digilines, awards, hopper, tell, email, mail


code:
[Basic]

# The node to copy as the display node
fancy_vend.display_node (Display Node) string default:obsidian_glass

# Maximum number of mods to store per vender
fancy_vend.log_max (Log Max) int 40

# Speed of display item auto rotation
fancy_vend.autorotate_speed (Autorotate Speed) int 1

# Whether to not send mail alerts
fancy_vend.no_alerts (Don't Send Mail Alerts) bool false


code:
# Farming Redo Mod
### by TenPlus1

https://forum.minetest.net/viewtopic.php?id=9019

Farming Redo is a simplified version of the built-in farming mod in minetest and comes with wheat, cotton, carrot, cucumber, potato and tomato to start out with which spawn throughout the map... new foods need only be planted on tilled soil so no seeds are required, original wheat and cotton will require seeds which are found inside normal and jungle grass...

This mod works by adding your new plant to the {growing=1} group and numbering the stages from _1 to as many stages as you like, but the underscore MUST be used only once in the node name to separate plant from stage number e.g.

"farming:cotton_1"      through to   "farming:cotton_8"
"farming:wheat_1"       through to   "farming:wheat_8"
"farming:cucumber_4"    through to   "farming:cucumber_4"

### Changelog:

- 1.45 - Dirt and Hoes are more in line with default by using dry/wet/base, added cactus juice, added pasta, spaghetti, cabbage, korean bibimbap, code tidy
options, onion soup added (thanks edcrypt), Added apple pie, added wild cotton to savanna
- 1.44 - Added 'farming_stage_length' in mod settings for speed of crop growth, also thanks to TheDarkTiger for translation updates
- 1.43 - Scythe works on use instead of right-click, added seed=1 groups to actual seeds and seed=2 group for plantable food items.
- 1.42 - Soil needs water to be present within 3 blocks horizontally and 1 below to make wet soil, Jack 'o Lanterns now check protection, add chocolate block.
- 1.41 - Each crop has it's own spawn rate (can be changed in farming.conf)
- 1.40 - Added Mithril Scythe to quick harvest and replant crops on right-click.  Added Hoe's for MoreOres with Toolrank support.
- 1.39 - Added Rice, Rye and Oats thanks to Ademants Grains mod.  Added Jaffa Cake and multigrain bread.
- 1.38 - Pumpkin grows into block, use chopping board to cut into 4x slices, same with melon block, 2x2 slices makes a block, cocoa pods are no longer walkable
- 1.37 - Added custom 'growth_check(pos, nodename) function for crop nodes to use (check cocoa.lua for example)
- 1.36 - Added Beetroot, Beetroot Soup (6x beetroot, 1x bowl), fix register_plant() issue, add new recipes
- 1.35 - Deprecated bronze/mese/diamond hoe's, added hoe bomb and deprecated hoe's as lucky block prizes
- 1.34 - Added scarecrow Base (5x sticks in a cross shape)
- 1.33 - Added cooking utensils (wooden bowl, saucepan, cooking pot, baking tray, skillet, cutting board, mortar & pestle, juicer, glass mixing bowl) for easier food crafts.
- 1.32 - Added Pea plant (textures by Andrey01) - also added Wooden Bowl and Pea Soup crafts
- 1.31 - Added Pineapple which can be found growing in savannah areas (place pineapple in crafting to obtain 5x rings to eat and a top for re-planting), also Salt which is made from cooking a bucket of water, added food groups so it's more compatible with Ruben's food mods.
- 1.30 - Added Garlic, Pepper and Onions thanks to Grizzly Adam for sharing textures
- 1.29 - Updating functions so requires Minetest 0.4.16 and above to run
- 1.28 - Added chili peppers and bowl of chili, optimized code and fixed a few bugs, added porridge
- 1.27 - Added meshoptions to api and wheat plants, added farming.rarity setting to spawn more/less crops on map, have separate cotton/string items (4x cotton = 1x wool, 2x cotton = 2x string)
- 1.26 - Added support for [toolranks] mod when using hoe's
- 1.25 - Added check for farming.conf setting file to disable specific crops globally (inside mod folder) or world specific (inside world folder)
- 1.24 - Added Hemp which can be crafted into fibre, paper, string, rope and oil.
- 1.23 - Huge code tweak and tidy done and added barley seeds to be found in dry grass, barley can make flour for bread also.
- 1.22 - Added grape bushes at high climates which can be cultivated into grape vines using trellis (9 sticks).
- 1.21 - Added auto-refill code for planting crops (thanks crabman77), also fixed a few bugs
- 1.20b - Tidied code, made api compatible with new 0.4.13 changes and changed to soil texture overlays
- 1.20 - NEW growing routine added that allows crops to grow while player is away doing other things (thanks prestidigitator)
- 1.14 - Added Green Beans from Crops mod (thanks sofar), little bushels in the wild but need to be grown using beanpoles crafted with 4 sticks (2 either side)
- 1.13 - Fixed seed double-placement glitch.  Mapgen now uses 0.4.12+ for plant generation
- 1.12 - Player cannot place seeds in protected area, also growing speeds changed to match defaults
- 1.11 - Added Straw Bale, streamlined growing abm a little, fixed melon rotation bug with screwdriver
- 1.10 - Added Blueberry Bush and Blueberry Muffins, also Pumpkin/Melon easier to pick up, added check for unloaded map
- 1.09 - Corn now uses single nodes instead of 1 ontop of the other, Ethanol recipe is more expensive (requires 5 corn) and some code cleanup.
- 1.08 - Added Farming Plus compatibility, plus can be removed and no more missing nodes
- 1.07 - Added Rhubarb and Rhubarb Pie
- 1.06 - register_hoe and register_plant added for compatibility with default farming mod, although any plants registered will use farming redo to grow
- 1.05 - Added Raspberry Bushels and Raspberry Smoothie
- 1.04 - Added Donuts... normal, chocolate and apple... and a few code cleanups and now compatible with jungletree's from MoreTrees mod
- 1.03 - Bug fixes and more compatibility as drop-in replacement for built-in farming mod
- 1.02 - Added farming.mod string to help other mods identify which farming mod is running, if it returns "redo" then you're using this one, "" empty is built-in mod
- 1.01 - Crafting coffee or ethanol returns empty bucket/bottle, also Cocoa spawns a little rarer
- 1.0 - Added Cocoa which randomly grows on jungle tree's, pods give cocoa beans which can be used to farm more pods on a jungle trunk or make Cookies which have been added (or other treats)
- 0.9 - Added Pumpkin, Jack 'O Lantern, Pumpkin Slice and Sugar (a huge thanks to painterly.net for allowing me to use their textures)
- 0.8 - Added Watermelon and Melon Slice
- 0.7 - Added Coffee, Coffee Beans, Drinking Cup, Cold and Hot Cup of Coffee
- 0.6 - Added Corn, Corn on the Cob... Also reworked Abm
- 0.5 - Added Carrot, Cucumber, Potato (and Baked Potato), Tomato
- 0.4 - Checks for Protection, also performance changes
- 0.3 - Added Diamond and Mese hoe
- 0.2 - Fixed check for wet soil
- 0.1 - Fixed growing bug
- 0.0 - Initial release

### Lucky Blocks: 39


code:
Farming API
-----------

The farming API allows you to easily register plants and hoes.

`farming.register_hoe(name, hoe definition)`
 * Register a new hoe, see [#hoe definition]

`farming.register_plant(name, Plant definition)`
 * Register a new growing plant, see [#Plant definition]

`farming.registered_plants[name] = definition`
 * Table of registered plants, indexed by plant name
    'crop' holds name of growing crop node minus _step-number at end
    'seed' has name of seed required to plant crop
    'minlight' min light level needed to grow
    'maxlight' max light level needed to grow
    'steps' number of steps crop has in growth cycle

### Hoe Definition

	{
		description = "My Hoe",                -- Description for tooltip
		inventory_image = "unknown_item.png",  -- Image to be used as wield- and inventory image
		max_uses = 30,                         -- Uses until destroyed
		material = "",                         -- Material for recipes
		recipe = {                             -- Craft recipe, if material isn't used
			{"air", "air", "air"},
			{"", "group:stick"},
			{"", "group:stick"},
		}
	}

### Plant definition

	{
		description = "My Plant",              -- Description of seed item
		inventory_image = "unknown_item.png",  -- Image to be used as seed's wield- and inventory image
		steps = 8,                             -- How many steps the plant has to grow, until it can be harvested
		-- ^ Always provide a plant texture for each step, format: modname_plantname_i.png (i = stepnumber)
		minlight = 13,                         -- Minimum light to grow
		maxlight = default.LIGHT_MAX           -- Maximum light to grow
	}

Note: Any crops registered with the above function will use the new growing routines, also if crops are manually added with the {growing=1} group they will also grow.

### Crop functions

If a mod registers nodes to be used as crops using the {growing=1} group then an additional function can be used for custom growth checks instead of the standard 'are we above wet soil'.

growth_check = function(pos, node_name)
	-- check surrounding for jungle tree
	if minetest.find_node_near(pos, 1, {"default:jungletree"}) then
		return false -- place next growth stage
	end
	return true -- condition not met, skip next growth stage until next check
end,

### Scythe items that will not drop

This is a function to add items to a list that scythes will not drop, e.g. farming:trellis or farming:beanpole.

farming.add_to_scythe_not_drops(item_name)


code:
local S = farming.intllib

--= Helpers

local eth = minetest.get_modpath("ethereal")
local alias = function(orig, new)
	minetest.register_alias(orig, new)
end

--= Overrides (add food_* group to apple and brown mushroom)

minetest.override_item("default:apple", {
	groups = {food_apple = 1, fleshy = 3, dig_immediate = 3, flammable = 2,
		leafdecay = 3, leafdecay_drop = 1}
})

if minetest.registered_nodes["flowers:mushroom_brown"] then
minetest.override_item("flowers:mushroom_brown", {
	light_source = 1,
	groups = {food_mushroom = 1, snappy = 3, attached_node = 1, flammable = 2}
})
end

--= Aliases

-- Banana
if eth then
	alias("farming_plus:banana_sapling", "ethereal:banana_tree_sapling")
	alias("farming_plus:banana_leaves", "ethereal:bananaleaves")
	alias("farming_plus:banana", "ethereal:banana")
else
	minetest.register_node(":ethereal:banana", {
		description = S("Banana"),
		drawtype = "torchlike",
		tiles = {"banana_single.png"},
		inventory_image = "banana_single.png",
		wield_image = "banana_single.png",
		paramtype = "light",
		sunlight_propagates = true,
		walkable = false,
		selection_box = {
			type = "fixed",
			fixed = {-0.2, -0.5, -0.2, 0.2, 0.2, 0.2}
		},
		groups = {food_banana = 1, fleshy = 3, dig_immediate = 3, flammable = 2},
		on_use = minetest.item_eat(2),
		sounds = default.node_sound_leaves_defaults()
	})

	minetest.register_node(":ethereal:bananaleaves", {
		description = S("Banana Leaves"),
		tiles = {"banana_leaf.png"},
		inventory_image = "banana_leaf.png",
		wield_image = "banana_leaf.png",
		paramtype = "light",
		waving = 1,
		groups = {snappy = 3, leafdecay = 3, leaves = 1, flammable = 2},
		sounds = default.node_sound_leaves_defaults()
	})

	alias("farming_plus:banana_sapling", "default:sapling")
	alias("farming_plus:banana_leaves", "ethereal:bananaleaves")
	alias("farming_plus:banana", "ethereal:banana")
end

-- Carrot
alias("farming_plus:carrot_seed", "farming:carrot")
alias("farming_plus:carrot_1", "farming:carrot_1")
alias("farming_plus:carrot_2", "farming:carrot_4")
alias("farming_plus:carrot_3", "farming:carrot_6")
alias("farming_plus:carrot", "farming:carrot_8")
alias("farming_plus:carrot_item", "farming:carrot")

-- Cocoa
alias("farming_plus:cocoa_sapling", "farming:cocoa_beans")
alias("farming_plus:cocoa_leaves", "default:leaves")
alias("farming_plus:cocoa", "default:apple")
alias("farming_plus:cocoa_bean", "farming:cocoa_beans")

-- Orange
alias("farming_plus:orange_1", "farming:tomato_1")
alias("farming_plus:orange_2", "farming:tomato_4")
alias("farming_plus:orange_3", "farming:tomato_6")

if eth then
	alias("farming_plus:orange_item", "ethereal:orange")
	alias("farming_plus:orange", "ethereal:orange")
	alias("farming_plus:orange_seed", "ethereal:orange_tree_sapling")
else
	minetest.register_node(":ethereal:orange", {
		description = S("Orange"),
		drawtype = "plantlike",
		tiles = {"farming_orange.png"},
		inventory_image = "farming_orange.png",
		wield_image = "farming_orange.png",
		paramtype = "light",
		sunlight_propagates = true,
		walkable = false,
		selection_box = {
			type = "fixed",
			fixed = {-0.2, -0.3, -0.2, 0.2, 0.2, 0.2}
		},
		groups = {food_orange = 1, fleshy = 3, dig_immediate = 3, flammable = 2},
		on_use = minetest.item_eat(4),
		sounds = default.node_sound_leaves_defaults()
	})

	alias("farming_plus:orange_item", "ethereal:orange")
	alias("farming_plus:orange", "ethereal:orange")
	alias("farming_plus:orange_seed", "default:sapling")
end

-- Potato
alias("farming_plus:potato_item", "farming:potato")
alias("farming_plus:potato_1", "farming:potato_1")
alias("farming_plus:potato_2", "farming:potato_2")
alias("farming_plus:potato", "farming:potato_3")
alias("farming_plus:potato_seed", "farming:potato")

-- Pumpkin
alias("farming:pumpkin_seed", "farming:pumpkin_slice")
alias("farming:pumpkin_face", "farming:jackolantern")
alias("farming:pumpkin_face_light", "farming:jackolantern_on")
alias("farming:big_pumpkin", "farming:jackolantern")
alias("farming:big_pumpkin_side", "air")
alias("farming:big_pumpkin_top", "air")
alias("farming:big_pumpkin_corner", "air")
alias("farming:scarecrow", "farming:jackolantern")
alias("farming:scarecrow_light", "farming:jackolantern_on")
alias("farming:pumpkin_flour", "farming:pumpkin_dough")

-- Rhubarb
alias("farming_plus:rhubarb_seed", "farming:rhubarb")
alias("farming_plus:rhubarb_1", "farming:rhubarb_1")
alias("farming_plus:rhubarb_2", "farming:rhubarb_2")
alias("farming_plus:rhubarb", "farming:rhubarb_3")
alias("farming_plus:rhubarb_item", "farming:rhubarb")

-- Strawberry
if eth then
	alias("farming_plus:strawberry_item", "ethereal:strawberry")
	alias("farming_plus:strawberry_seed", "ethereal:strawberry")
	alias("farming_plus:strawberry_1", "ethereal:strawberry_1")
	alias("farming_plus:strawberry_2", "ethereal:strawberry_3")
	alias("farming_plus:strawberry_3", "ethereal:strawberry_5")
	alias("farming_plus:strawberry", "ethereal:strawberry_7")
else
	minetest.register_craftitem(":ethereal:strawberry", {
		description = S("Strawberry"),
		inventory_image = "strawberry.png",
		wield_image = "strawberry.png",
		groups = {food_strawberry = 1, flammable = 2},
		on_use = minetest.item_eat(1)
	})

	alias("farming_plus:strawberry_item", "ethereal:strawberry")
	alias("farming_plus:strawberry_seed", "ethereal:strawberry")
	alias("farming_plus:strawberry_1", "farming:raspberry_1")
	alias("farming_plus:strawberry_2", "farming:raspberry_2")
	alias("farming_plus:strawberry_3", "farming:raspberry_3")
	alias("farming_plus:strawberry", "farming:raspberry_4")
end

-- Tomato
alias("farming_plus:tomato_seed", "farming:tomato")
alias("farming_plus:tomato_item", "farming:tomato")
alias("farming_plus:tomato_1", "farming:tomato_2")
alias("farming_plus:tomato_2", "farming:tomato_4")
alias("farming_plus:tomato_3", "farming:tomato_6")
alias("farming_plus:tomato", "farming:tomato_8")

-- Weed
alias("farming:weed", "default:grass_2")


code:
local S = farming.intllib

-- barley seeds
minetest.register_node("farming:seed_barley", {
	description = S("Barley Seed"),
	tiles = {"farming_barley_seed.png"},
	inventory_image = "farming_barley_seed.png",
	wield_image = "farming_barley_seed.png",
	drawtype = "signlike",
	groups = {seed = 1, snappy = 3, attached_node = 1},
	paramtype = "light",
	paramtype2 = "wallmounted",
	walkable = false,
	sunlight_propagates = true,
	selection_box = farming.select,
	on_place = function(itemstack, placer, pointed_thing)
		return farming.place_seed(itemstack, placer, pointed_thing, "farming:barley_1")
	end
})

-- harvested barley
minetest.register_craftitem("farming:barley", {
	description = S("Barley"),
	inventory_image = "farming_barley.png",
	groups = {food_barley = 1, flammable = 2}
})

-- flour
minetest.register_craft({
	type = "shapeless",
	output = "farming:flour",
	recipe = {
		"farming:barley", "farming:barley", "farming:barley",
		"farming:barley", "farming:mortar_pestle"
	},
	replacements = {{"group:food_mortar_pestle", "farming:mortar_pestle"}}
})

-- barley definition
local def = {
	drawtype = "plantlike",
	tiles = {"farming_barley_1.png"},
	paramtype = "light",
	paramtype2 = "meshoptions",
	place_param2 = 3,
	sunlight_propagates = true,
	walkable = false,
	buildable_to = true,
	drop = "",
	selection_box = farming.select,
	groups = {
		snappy = 3, flammable = 2, plant = 1, attached_node = 1,
		not_in_creative_inventory = 1, growing = 1
	},
	sounds = default.node_sound_leaves_defaults()
}

-- stage 1
minetest.register_node("farming:barley_1", table.copy(def))

-- stage 2
def.tiles = {"farming_barley_2.png"}
minetest.register_node("farming:barley_2", table.copy(def))

-- stage 3
def.tiles = {"farming_barley_3.png"}
minetest.register_node("farming:barley_3", table.copy(def))

-- stage 4
def.tiles = {"farming_barley_4.png"}
minetest.register_node("farming:barley_4", table.copy(def))

-- stage 5
def.tiles = {"farming_barley_5.png"}
def.drop = {
	items = {
		{items = {"farming:barley"}, rarity = 2},
		{items = {"farming:seed_barley"}, rarity = 2}
	}
}
minetest.register_node("farming:barley_5", table.copy(def))

-- stage 6
def.tiles = {"farming_barley_6.png"}
def.drop = {
	items = {
		{items = {"farming:barley"}, rarity = 2},
		{items = {"farming:seed_barley"}, rarity = 1}
	}
}
minetest.register_node("farming:barley_6", table.copy(def))

-- stage 7 (final)
def.tiles = {"farming_barley_7.png"}
def.groups.growing = nil
def.drop = {
	items = {
		{items = {"farming:barley"}, rarity = 1},
		{items = {"farming:barley"}, rarity = 3},
		{items = {"farming:seed_barley"}, rarity = 1},
		{items = {"farming:seed_barley"}, rarity = 3}
	}
}
minetest.register_node("farming:barley_7", table.copy(def))

-- add to registered_plants
farming.registered_plants["farming:barley"] = {
	crop = "farming:barley",
	seed = "farming:seed_barley",
	minlight = 13,
	maxlight = 15,
	steps = 7
}

-- Fuel
minetest.register_craft({
	type = "fuel",
	recipe = "farming:barley",
	burntime = 1
})