avatar
scuti
gave a whole new meaning to touching grass
https://scuti.neocities.org/

Minetest: Digilines Vending Machine

Last Updated: 2023 September 05 (2023 April 16)

A vending machine in Minetest constructed with the Digilines mod and programmed in Lua. This is what Digilines is: https://mesecons.net/digilines.html

In Minetest, I am a green skeleton that lives deep beneath the surface of the world. The existence I lead is not very solitary.

In the market row of spawn town, I have deployed a teleporter. It looks like a yellow phone booth, and people can use it to access my underground lair. One of the points of interests of my lair is a supermarket containing a vending machine.

“I don’t have any money,” the new players say as I invite visitors.

And there’s no problem in that. I progressed enough into the game, and my food production is mostly automated.

“How can this be economical?” one may ask.

Blueberries and wheat as far as minetest is concerned can be grown in low light areas. Grass, however, requires real sunlight. I can farm blueberries and wheat at a large scale, but the game doesn’t let me farm grass. So an easy way to get started on the server I play on is to rip up grass from random areas and sell it to the vending machine.

The Vending Machine

The vending machine is in Subterra Market and to the left of the point of arrival. You can find the teleporter to the vending machine at survival.juneland.fr:30000 (-961, 12.5, 1911).

How to Use

The vending machine works a lot like a real one; except, there is no item that represents a reptacle to slide in dollar bills. So the steps to use the vending machine are slightly different.

Each item listed for sale has a price of one default:grass

  1. Put grass into the Digiline Chest (the wooden chest pointed by the sign “DEPOSIT”).

  2. Interact with the digistuff:touchscreen and click the button “Deposit”

After the item finishes traveling in the tube, the screen above the touchscreen should display the item deposited and the quantity.

  1. Open the touchscreen again and place an order by entering a number in one of the fields.

  2. Hit the Enter (or Return) key on your keyboard.

If the price does not surpass the balance, the product should travel in the tube to arrive in the silver chest.

  1. Open the touchscreen again and click the button “Refund” to return any unspent credits.

Source Code

This machine uses:

See device names starting at line #199


    --{{GLOBAL}}--
    STORE_CREDIT = "default:grass_1"

    -- reports the contents of a list
    -- channel intended for LCDs
    local function showlist(channel, list, empty_msg)
        empty_msg = empty_msg or "Empty List."
        local size = table.maxn(list)
        if size <= 0 then
            digiline_send(channel, empty_msg)
            return
        end
        local longstr= ""
        for _, v in ipairs(list) do
            longstr = longstr .. v.name .. ", ".. v.count .."; \n"
        end
        digiline_send(channel, longstr)
    end

    local function is_in_list(string, list)
        for i, v in ipairs(list) do
            if v.name == string then
                return i;
            end
        end
        return -1
    end

    local function additem(list, stack)
        local index = is_in_list(stack.name, list)
    -- if the item is not in the list or the list is empty
        if (index < 0 or table.maxn(list) <= 0) and stack.count > 0 then
            table.insert(list, stack)
            return
        else
    -- if the item is in the list and it must be updated
            list[index].count = list[index].count + stack.count
        end
        if list[index].count <= 0 then
            table.remove(list, index)
        end
    end

    -- use for both payment chest and stock chest
    -- payment chest responds to uput or utake
    -- stock chest responds to tput or ttake
    local function detect(msg, list)
        if msg  == nil then 
            return 
        end
        if msg.stack == nil then 
            return 
        end
        local n = msg.stack.name
        local c = msg.stack.count
        if msg.action == "uput" then
        -- player has put an item stack in the digichest
            additem(list, {name=n, count= c})
        elseif msg.action == "utake" then
        -- player has taken an item
            additem(list, {name=n, count= -c})
        elseif msg.action  == "tput" then
        -- a tube has put an item
            additem(list, {name=n, count= c})
        elseif msg.action == "ttake" then
        -- a tube has taken an item
            additem(list, {name=n, count= -c})
        end
    end

    -- only take specified item as currency
    -- to either load funds or refund
    local function process_payment(dfi, list, currency)
        local size = table.maxn(list)
        if size <= 0 then
            return false
        end
        for i, v in ipairs(list) do
            if v.name == currency then
                digiline_send(dfi, v)
                return true
            end
        end
    end

    local function process_sale(dfi, order, itemname)
        if itemname == nil then
            return "Programming error: var 'itemname' is nil"
        end
        if tonumber(order[itemname]) == nil then 
            return "Error: invalid input: not a number."
        end
        local quantity = math.floor(order[itemname])
        if quantity <= 0 then
            return "Error: invalid quantity (".. quantity ..")"
        end
        for i, v in ipairs(mem.stock) do
            if v.name == itemname and quantity > v.count then
                return "Order quantity exceeds stock!"
            end
        end
        local cost = 1 * quantity
        if table.maxn(mem.paid) <= 0 then
        -- note: tried to check mem.paid == {} - doesn't work
            return "Error: no funds loaded."
        end
        if mem.paid[1].count < cost then
            return "Error: Insufficient funds."
        end
        -- sale is good to go
        digiline_send(dfi, {name=itemname, count=quantity})
        additem(mem.paid, {name=mem.paid[1].name, count=-cost})
        return "Sale confirmed"
    end

    -- dfi_receive = customer payment
    -- dfi_send = send to customer (refunds, wares)
    local function touch_response(msg, dfi_receive, dfi_send)
    -- ignore the event when a player closes menu
        local lcd_status = "lcd3"
        local credit = STORE_CREDIT
        if msg["key_enter_field"] == nil and msg["quit"] ~= nil then 
            return "Hello! Use the touchscreen above to begin."
        end
        if msg["pay"] ~= nil then
            if process_payment(dfi_receive, mem.queue, credit) then
                return "Sent payment."
            else
                return "No payment items detected."
            end
        elseif msg["refund"] ~= nil then
            if process_payment(dfi_send, mem.paid, credit) then
                return "Sent refund."
            else
                return "No payment to return."
            end
        end
        for i, itemstack in ipairs(mem.stock) do
            if msg["key_enter_field"] == itemstack.name then
                return process_sale(dfi_send, msg, itemstack.name)
            end
        end
    end

    local function spawnfield(channel, name, label, default, y)
        local height = 1
        local width = 8
        local n = {}
        n.command = "addfield"
        n["X"] = 1
        n["Y"] = 1 + y
        n["W"] = width
        n["H"] = height
        n["name"] = name
        n["label"] = label
        n["default"] = default
        digiline_send(channel, n)
    end

    local function touch_init(channel, wares)
        local reset = {}
        reset.command = "clear"
        digiline_send(channel, reset)

        local btn_pay = {}
        btn_pay.command = "addbutton"
        btn_pay["X"] = 1
        btn_pay["Y"] = 0
        btn_pay["W"] = 4
        btn_pay["H"] = 1
        btn_pay["name"] = "pay"
        btn_pay["label"] = "Deposit"
        digiline_send(channel, btn_pay)

        local btn_refund = {}
        btn_refund.command = "addbutton"
        btn_refund["X"] = 5
        btn_refund["Y"] = 0
        btn_refund["W"] = 4
        btn_refund["H"] = 1
        btn_refund["name"] = "refund"
        btn_refund["label"] = "Refund"
        digiline_send(channel, btn_refund)

        local guide = {}
        guide.command = "addlabel"
        guide["X"] = 2.5
        guide["Y"] = 1
        guide.label = "Press ENTER (or RETURN) key to place order."
        digiline_send(channel, guide)

        for i, v in ipairs(wares) do
            local label = v.name .. " (stock: " .. v.count .. ")"
            spawnfield(channel, v.name, label, "0", i)
        end
    end

    local function main()
        local ts    = "ts"      -- touchscreen, order form
        local pay   = "input"   -- player pays w/ digichest
        local dfi1  = "accept"  -- DFI sends payment from 'pay'
        local store = "store"   -- digichest holds stock & payment
        local dfi2  = "deploy"  -- DFI sends wares from stock
        local card  = "swipe"   -- experimental card reader
        local lcd_queue = "lcd"
        local lcd_paid  = "lcd2"
        if event.type == "program" then
            mem.queue = {} -- items placed in chest
            mem.paid = {}  -- items paid by customers
            mem.stock = {} -- items stocked by owner
            touch_init(ts, mem.stock)
            digiline_send("lcd", "Initialized!")
            digiline_send("lcd2", "Initialized!")
            digiline_send("lcd3", "Initialized!")
        end
        if event.type == "digiline" and event.channel == ts then
            local status = touch_response(event.msg, dfi1, dfi2)
            touch_init(ts, mem.stock)
            digiline_send("lcd3", status)
            showlist(lcd_paid, mem.paid, "No funds loaded.")
        end
        if event.type == "digiline" and event.channel == pay then
            detect(event.msg, mem.queue)
            showlist(lcd_queue, mem.queue, "Place payment in the chest below.")
        end
        if event.type == "digiline" and event.channel == store then
            if event.msg.action == "uput" 
            or event.msg.action == "utake" then
            -- store stock directly accessed by player 
            -- (as opposed to vending)
                detect(event.msg, mem.stock)
            elseif event.msg.action == "tput" then
            -- vending, payment confirmation
            -- update mem.paid to show current store credits
                detect(event.msg, mem.paid)
                showlist(lcd_paid, mem.paid, "No funds loaded.")
            elseif event.msg.action == "ttake" then
            -- vending, update stock after selling
                if event.msg.stack.name == STORE_CREDIT then
                    detect(event.msg, mem.paid)
                else
                    detect(event.msg, mem.stock)
                end
                showlist(lcd_paid, mem.paid, "No funds loaded.")
            -- process_sale already deducts store creditss
            -- note: upon refunds, the code will attempt to deduct
            -- currency from the stock list
            -- however additems() does allow negative quantities to be indexed.
            end
            touch_init(ts, mem.stock)
        end
    end

    return main()

Top