Update documentation
[minetest_easyvend.git] / easyvend.lua
blob4ec34a548777ea79646aa00b80fd3a9ed90543ab
1 -- TODO: Improve mod compability
2 local slots_max = 31
4 local traversable_node_types = {
5 ["easyvend:vendor"] = true,
6 ["easyvend:depositor"] = true,
7 ["easyvend:vendor_on"] = true,
8 ["easyvend:depositor_on"] = true,
10 local registered_chests = {}
11 local cost_stack_max = minetest.registered_items[easyvend.currency].stack_max
12 local maxcost = cost_stack_max * slots_max
14 local joketimer_start = 3
16 -- Allow for other mods to register custom chests
17 easyvend.register_chest = function(node_name, inv_list, meta_owner)
18 registered_chests[node_name] = { inv_list = inv_list, meta_owner = meta_owner }
19 traversable_node_types[node_name] = true
20 end
22 -- Partly a wrapper around contains_item, but does special treatment if the item
23 -- is a tool. Basically checks whether the items exist in the supplied inventory
24 -- list. If check_wear is true, only counts items without wear.
25 easyvend.check_and_get_items = function(inventory, listname, itemtable, check_wear)
26 local itemstring = itemtable.name
27 local minimum = itemtable.count
28 if check_wear == nil then check_wear = false end
29 local get_items = {}
30 -- Tool workaround
31 if minetest.registered_tools[itemstring] ~= nil then
32 local count = 0
33 for i=1,inventory:get_size(listname) do
34 local stack = inventory:get_stack(listname, i)
35 if stack:get_name() == itemstring then
36 if not check_wear or stack:get_wear() == 0 then
37 count = count + 1
38 table.insert(get_items, {id=i, item=stack})
39 if count >= minimum then
40 return true, get_items
41 end
42 end
43 end
44 end
45 return false
46 else
47 -- Normal Minetest check
48 return inventory:contains_item(listname, ItemStack(itemtable))
49 end
50 end
53 if minetest.get_modpath("default") ~= nil then
54 easyvend.register_chest("default:chest_locked", "main", "owner")
55 end
57 easyvend.free_slots= function(inv, listname)
58 local size = inv:get_size(listname)
59 local free = 0
60 for i=1,size do
61 local stack = inv:get_stack(listname, i)
62 if stack:is_empty() then
63 free = free + 1
64 end
65 end
66 return free
67 end
69 easyvend.buysell = function(nodename)
70 local buysell = nil
71 if ( nodename == "easyvend:depositor" or nodename == "easyvend:depositor_on" ) then
72 buysell = "buy"
73 elseif ( nodename == "easyvend:vendor" or nodename == "easyvend:vendor_on" ) then
74 buysell = "sell"
75 end
76 return buysell
77 end
79 easyvend.is_active = function(nodename)
80 if ( nodename == "easyvend:depositor_on" or nodename == "easyvend:vendor_on" ) then
81 return true
82 elseif ( nodename == "easyvend:depositor" or nodename == "easyvend:vendor" ) then
83 return false
84 else
85 return nil
86 end
87 end
89 easyvend.set_formspec = function(pos, player)
90 local meta = minetest.get_meta(pos)
91 local node = minetest.get_node(pos)
93 local description = minetest.registered_nodes[node.name].description;
94 local number = meta:get_int("number")
95 local cost = meta:get_int("cost")
96 local itemname = meta:get_string("itemname")
97 local bg = ""
98 local configmode = meta:get_int("configmode") == 1
99 if minetest.get_modpath("default") then
100 bg = default.gui_bg .. default.gui_bg_img .. default.gui_slots
103 local numbertext, costtext, buysellbuttontext
104 local itemcounttooltip = "Item count (append “s” to multiply with maximum stack size)"
105 local buysell = easyvend.buysell(node.name)
106 if buysell == "sell" then
107 numbertext = "Offered item"
108 costtext = "Price"
109 buysellbuttontext = "Buy"
110 elseif buysell == "buy" then
111 numbertext = "Requested item"
112 costtext = "Payment"
113 buysellbuttontext = "Sell"
114 else
115 return
117 local status = meta:get_string("status")
118 if status == "" then status = "Unknown." end
119 local message = meta:get_string("message")
120 if message == "" then message = "No message." end
121 local status_image
122 if node.name == "easyvend:vendor_on" or node.name == "easyvend:depositor_on" then
123 status_image = "easyvend_status_on.png"
124 else
125 status_image = "easyvend_status_off.png"
128 -- TODO: Expose number of items in stock
130 local formspec = "size[8,7.3;]"
131 .. bg
132 .."label[3,-0.2;" .. minetest.formspec_escape(description) .. "]"
134 .."image[7.5,0.2;0.5,1;" .. status_image .. "]"
135 .."textarea[2.8,0.2;5.1,2;;Status: " .. minetest.formspec_escape(status) .. ";]"
136 .."textarea[2.8,1.3;5.6,2;;Message: " .. minetest.formspec_escape(message) .. ";]"
138 .."label[0,-0.15;"..numbertext.."]"
139 .."label[0,1.2;"..costtext.."]"
140 .."list[current_player;main;0,3.5;8,4;]"
141 if minetest.get_modpath("doc") and minetest.get_modpath("doc_items") then
142 if (doc.VERSION.MAJOR >= 1) or (doc.VERSION.MAJOR == 0 and doc.VERSION.MINOR >= 8) then
143 formspec = formspec .. "image_button[7.25,2;0.75,0.75;doc_button_icon_lores.png;doc;]" ..
144 "tooltip[doc;Help]"
148 if configmode then
149 local wear = "false"
150 if meta:get_int("wear") == 1 then wear = "true" end
151 formspec = formspec
152 .."item_image_button[0,1.65;1,1;"..easyvend.currency..";easyvend.currency_image;]"
153 .."list[current_name;item;0,0.35;1,1;]"
154 .."listring[current_player;main]"
155 .."listring[current_name;item]"
156 .."field[1.3,0.65;1.5,1;number;;" .. number .. "]"
157 .."tooltip[number;"..itemcounttooltip.."]"
158 .."field[1.3,1.95;1.5,1;cost;;" .. cost .. "]"
159 .."tooltip[cost;"..itemcounttooltip.."]"
160 .."button[6,2.8;2,0.5;save;Confirm]"
161 .."tooltip[save;Confirm configuration and activate machine (only for owner)]"
162 local weartext, weartooltip
163 if buysell == "buy" then
164 weartext = "Buy worn tools"
165 weartooltip = "If disabled, only tools in perfect condition will be bought from sellers (only settable by owner)"
166 else
167 weartext = "Sell worn tools"
168 weartooltip = "If disabled, only tools in perfect condition will be sold (only settable by owner)"
170 if minetest.registered_tools[itemname] ~= nil then
171 formspec = formspec .."checkbox[2,2.4;wear;"..minetest.formspec_escape(weartext)..";"..wear.."]"
172 .."tooltip[wear;"..minetest.formspec_escape(weartooltip).."]"
174 else
175 formspec = formspec
176 .."item_image_button[0,1.65;1,1;"..easyvend.currency..";easyvend.currency_image;]"
177 .."item_image_button[0,0.35;1,1;"..itemname..";item_image;]"
178 .."label[1,1.85;×" .. cost .. "]"
179 .."label[1,0.55;×" .. number .. "]"
180 .."button[6,2.8;2,0.5;config;Configure]"
181 if buysell == "sell" then
182 formspec = formspec .. "tooltip[config;Configure offered items and price (only for owner)]"
183 else
184 formspec = formspec .. "tooltip[config;Configure requested items and payment (only for owner)]"
186 formspec = formspec .."button[0,2.8;2,0.5;buysell;"..buysellbuttontext.."]"
187 if minetest.registered_tools[itemname] ~= nil then
188 local weartext
189 if meta:get_int("wear") == 0 then
190 if buysell == "buy" then
191 weartext = "Only intact tools are bought."
192 else
193 weartext = "Only intact tools are sold."
195 else
196 if buysell == "sell" then
197 weartext = "Warning: Might sell worn tools."
198 else
199 weartext = "Worn tools are bought, too."
202 if weartext ~= nil then
203 formspec = formspec .."textarea[2.3,2.6;3,1;;"..minetest.formspec_escape(weartext)..";]"
208 meta:set_string("formspec", formspec)
211 easyvend.machine_disable = function(pos, node, playername)
212 if node.name == "easyvend:vendor_on" then
213 easyvend.sound_disable(pos)
214 minetest.swap_node(pos, {name="easyvend:vendor", param2 = node.param2})
215 return true
216 elseif node.name == "easyvend:depositor_on" then
217 easyvend.sound_disable(pos)
218 minetest.swap_node(pos, {name="easyvend:depositor", param2 = node.param2})
219 return true
220 else
221 if playername ~= nil then
222 easyvend.sound_error(playername)
224 return false
228 easyvend.machine_enable = function(pos, node)
229 if node.name == "easyvend:vendor" then
230 easyvend.sound_setup(pos)
231 minetest.swap_node(pos, {name="easyvend:vendor_on", param2 = node.param2})
232 return true
233 elseif node.name == "easyvend:depositor" then
234 easyvend.sound_setup(pos)
235 minetest.swap_node(pos, {name="easyvend:depositor_on", param2 = node.param2})
236 return true
237 else
238 return false
242 easyvend.machine_check = function(pos, node)
243 local active = true
244 local status = "Ready."
246 local meta = minetest.get_meta(pos)
248 local machine_owner = meta:get_string("owner")
249 local number = meta:get_int("number")
250 local cost = meta:get_int("cost")
251 local itemname = meta:get_string("itemname")
252 local check_wear = meta:get_int("wear") == 0
253 local inv = meta:get_inventory()
254 local itemstack = inv:get_stack("item",1)
255 local buysell = easyvend.buysell(node.name)
257 local chest_pos_remove, chest_error_remove, chest_pos_add, chest_error_add
258 if buysell == "sell" then
259 chest_pos_remove, chest_error_remove = easyvend.find_connected_chest(machine_owner, pos, itemname, check_wear, number, true)
260 chest_pos_add, chest_error_add = easyvend.find_connected_chest(machine_owner, pos, easyvend.currency, check_wear, cost, false)
261 else
262 chest_pos_remove, chest_error_remove = easyvend.find_connected_chest(machine_owner, pos, easyvend.currency, check_wear, cost, true)
263 chest_pos_add, chest_error_add = easyvend.find_connected_chest(machine_owner, pos, itemname, check_wear, number, false)
265 if chest_pos_remove and chest_pos_add then
266 local rchest, rchestdef, rchest_meta, rchest_inv
267 rchest = minetest.get_node(chest_pos_remove)
268 rchestdef = registered_chests[rchest.name]
269 rchest_meta = minetest.get_meta(chest_pos_remove)
270 rchest_inv = rchest_meta:get_inventory()
272 local checkstack, checkitem
273 if buysell == "buy" then
274 checkitem = easyvend.currency
275 else
276 checkitem = itemname
278 local stock = 0
279 -- Count stock
280 -- FIXME: Ignore tools with bad wear level
281 for i=1,rchest_inv:get_size(rchestdef.inv_list) do
282 checkstack = rchest_inv:get_stack(rchestdef.inv_list, i)
283 if checkstack:get_name() == checkitem then
284 stock = stock + checkstack:get_count()
287 meta:set_int("stock", stock)
289 if not itemstack:is_empty() then
290 local number_stack_max = itemstack:get_stack_max()
291 local maxnumber = number_stack_max * slots_max
292 if not(number >= 1 and number <= maxnumber and cost >= 1 and cost <= maxcost) then
293 active = false
294 if buysell == "sell" then
295 status = "Invalid item count or price."
296 else
297 status = "Invalid item count or payment."
300 else
301 active = false
302 status = "Awaiting configuration by owner."
304 else
305 active = false
306 meta:set_int("stock", 0)
307 if chest_error_remove == "no_chest" and chest_error_add == "no_chest" then
308 status = "No storage; machine needs to be connected with a locked chest."
309 elseif chest_error_remove == "not_owned" or chest_error_add == "not_owned" then
310 status = "Storage can’t be accessed because it is owned by a different person!"
311 elseif chest_error_remove == "no_stock" then
312 if buysell == "sell" then
313 status = "The vending machine has insufficient materials!"
314 else
315 status = "The depositing machine is out of money!"
317 elseif chest_error_add == "no_space" then
318 status = "No room in the machine’s storage!"
319 else
320 status = "Unknown error!"
323 if meta:get_int("configmode") == 1 then
324 active = false
325 status = "Awaiting configuration by owner."
328 if itemname == easyvend.currency and number == cost and active then
329 local jt = meta:get_int("joketimer")
330 if jt > 0 then
331 jt = jt - 1
333 if jt == 0 then
334 if buysell == "sell" then
335 meta:set_string("message", "Item bought.")
336 else
337 meta:set_string("message", "Item sold.")
339 jt = -1
341 meta:set_int("joketimer", jt)
343 meta:set_string("status", status)
345 meta:set_string("infotext", easyvend.make_infotext(node.name, machine_owner, cost, number, itemname))
346 itemname=itemstack:get_name()
347 meta:set_string("itemname", itemname)
349 if minetest.get_modpath("awards") and buysell == "sell" then
350 if minetest.get_player_by_name(machine_owner) then
351 local earnings = meta:get_int("earnings")
352 if earnings >= 1 then
353 awards.unlock(machine_owner, "easyvend_seller")
355 if earnings >= easyvend.powerseller then
356 awards.unlock(machine_owner, "easyvend_powerseller")
361 local change
362 if node.name == "easyvend:vendor" or node.name == "easyvend:depositor" then
363 if active then change = easyvend.machine_enable(pos, node) end
364 elseif node.name == "easyvend:vendor_on" or node.name == "easyvend:depositor_on" then
365 if not active then change = easyvend.machine_disable(pos, node) end
367 easyvend.set_formspec(pos)
368 return change
371 easyvend.on_receive_fields_config = function(pos, formname, fields, sender)
372 local node = minetest.get_node(pos)
373 local meta = minetest.get_meta(pos)
374 local inv_self = meta:get_inventory()
375 local itemstack = inv_self:get_stack("item",1)
376 local buysell = easyvend.buysell(node.name)
378 if fields.config then
379 meta:set_int("configmode", 1)
380 local was_active = easyvend.is_active(node.name)
381 if was_active then
382 meta:set_string("message", "Configuration mode activated; machine disabled.")
383 else
384 meta:set_string("message", "Configuration mode activated.")
386 easyvend.machine_check(pos, node)
387 return
390 if not fields.save then
391 return
394 local number = fields.number
395 local cost = fields.cost
397 --[[ Convenience function:
398 When appending “s” or “S” to the number, it is multiplied
399 by the maximum stack size. ]]
400 local number_stack_max = itemstack:get_stack_max()
401 local ss = string.sub(number, #number, #number)
402 if ss == "s" or ss == "S" then
403 local n = tonumber(string.sub(number, 1, #number-1))
404 if string.len(number) == 1 then n = 1 end
405 if n ~= nil then
406 number = n * number_stack_max
409 ss = string.sub(cost, #cost, #cost)
410 if ss == "s" or ss == "S" then
411 local n = tonumber(string.sub(cost, 1, #cost-1))
412 if string.len(cost) == 1 then n = 1 end
413 if n ~= nil then
414 cost = n * cost_stack_max
417 number = tonumber(number)
418 cost = tonumber(cost)
420 local itemname=""
422 local oldnumber = meta:get_int("number")
423 local oldcost = meta:get_int("cost")
424 local maxnumber = number_stack_max * slots_max
426 if ( itemstack == nil or itemstack:is_empty() ) then
427 meta:set_string("status", "Awaiting configuration by owner.")
428 meta:set_string("message", "No item specified.")
429 easyvend.sound_error(sender:get_player_name())
430 easyvend.set_formspec(pos, sender)
431 return
432 elseif ( not itemstack:is_known() ) then
433 meta:set_string("status", "Awaiting configuration by owner.")
434 meta:set_string("message", "Unknown item specified.")
435 easyvend.sound_error(sender:get_player_name())
436 easyvend.set_formspec(pos, sender)
437 return
438 elseif ( number == nil or number < 1 or number > maxnumber ) then
439 if maxnumber > 1 then
440 meta:set_string("message", string.format("Invalid item count; must be between 1 and %d!", maxnumber))
441 else
442 meta:set_string("message", "Invalid item count; must be exactly 1!")
444 meta:set_int("number", oldnumber)
445 easyvend.sound_error(sender:get_player_name())
446 easyvend.set_formspec(pos, sender)
447 return
448 elseif ( cost == nil or cost < 1 or cost > maxcost ) then
449 if maxcost > 1 then
450 meta:set_string("message", string.format("Invalid cost; must be between 1 and %d!", maxcost))
451 else
452 meta:set_string("message", "Invalid cost; must be exactly 1!")
454 meta:set_int("cost", oldcost)
455 easyvend.sound_error(sender:get_player_name())
456 easyvend.set_formspec(pos, sender)
457 return
459 meta:set_int("number", number)
460 meta:set_int("cost", cost)
461 itemname=itemstack:get_name()
462 meta:set_string("itemname", itemname)
463 meta:set_int("configmode", 0)
465 if itemname == easyvend.currency and number == cost and cost <= cost_stack_max then
466 meta:set_string("message", "Configuration successful. I am feeling funny.")
467 meta:set_int("joketimer", joketimer_start)
468 meta:set_int("joke_id", easyvend.assign_joke(buysell))
469 else
470 meta:set_string("message", "Configuration successful.")
473 local change = easyvend.machine_check(pos, node)
475 if not change then
476 if (node.name == "easyvend:vendor_on" or node.name == "easyvend:depositor_on") then
477 easyvend.sound_setup(pos)
478 else
479 easyvend.sound_disable(pos)
484 easyvend.make_infotext = function(nodename, owner, cost, number, itemstring)
485 local d = ""
486 if itemstring == nil or itemstring == "" or number == 0 or cost == 0 then
487 if easyvend.buysell(nodename) == "sell" then
488 d = string.format("Inactive vending machine (owned by %s)", owner)
489 else
490 d = string.format("Inactive depositing machine (owned by %s)", owner)
492 return d
494 local iname
495 if minetest.registered_items[itemstring] then
496 iname = minetest.registered_items[itemstring].description
497 else
498 iname = string.format("Unknown Item (%s)", itemstring)
500 if iname == nil then iname = itemstring end
501 local printitem, printcost
502 if number == 1 then
503 printitem = iname
504 else
505 printitem = string.format("%d×%s", number, iname)
507 if cost == 1 then
508 printcost = easyvend.currency_desc
509 else
510 printcost = string.format("%d×%s", cost, easyvend.currency_desc)
512 if nodename == "easyvend:vendor_on" then
513 d = string.format("Vending machine (owned by %s)\nSelling: %s\nPrice: %s", owner, printitem, printcost)
514 elseif nodename == "easyvend:vendor" then
515 d = string.format("Inactive vending machine (owned by %s)\nSelling: %s\nPrice: %s", owner, printitem, printcost)
516 elseif nodename == "easyvend:depositor_on" then
517 d = string.format("Depositing machine (owned by %s)\nBuying: %s\nPayment: %s", owner, printitem, printcost)
518 elseif nodename == "easyvend:depositor" then
519 d = string.format("Inactive depositing machine (owned by %s)\nBuying: %s\nPayment: %s", owner, printitem, printcost)
521 return d
524 if minetest.get_modpath("awards") then
525 awards.register_achievement("easyvend_seller",{
526 title = "First Sale",
527 description = "Sell something with a vending machine.",
528 icon = "easyvend_vendor_front_on.png^awards_level1.png",
530 local desc_powerseller
531 if easyvend.currency == "default:gold_ingot" then
532 desc_powerseller = string.format("Earn %d gold ingots by selling goods with a single vending machine.", easyvend.powerseller)
533 else
534 desc_powerseller = string.format("Earn %d currency items by selling goods with a single vending machine.", easyvend.powerseller)
536 awards.register_achievement("easyvend_powerseller",{
537 title = "Power Seller",
538 description = desc_powerseller,
539 icon = "easyvend_vendor_front_on.png^awards_level2.png",
543 easyvend.check_earnings = function(buyername, nodemeta)
544 local owner = nodemeta:get_string("owner")
545 if buyername ~= owner then
546 local cost = nodemeta:get_int("cost")
547 local itemname = nodemeta:get_string("itemname")
548 -- First sell
549 if minetest.get_modpath("awards") and minetest.get_player_by_name(owner) ~= nil then
550 awards.unlock(owner, "easyvend_seller")
552 if itemname ~= easyvend.currency then
553 local newearnings = nodemeta:get_int("earnings") + cost
554 if newearnings >= easyvend.powerseller and minetest.get_modpath("awards") then
555 if minetest.get_player_by_name(owner) ~= nil then
556 awards.unlock(owner, "easyvend_powerseller")
559 nodemeta:set_int("earnings", newearnings)
564 easyvend.on_receive_fields_buysell = function(pos, formname, fields, sender)
565 local sendername = sender:get_player_name()
566 local meta = minetest.get_meta(pos)
568 if not fields.buysell then
569 return
572 local node = minetest.get_node(pos)
573 local number = meta:get_int("number")
574 local cost = meta:get_int("cost")
575 local itemname=meta:get_string("itemname")
576 local item=meta:get_inventory():get_stack("item", 1)
577 local check_wear = meta:get_int("wear") == 0 and minetest.registered_tools[itemname] ~= nil
578 local machine_owner = meta:get_string("owner")
580 local buysell = easyvend.buysell(node.name)
582 local number_stack_max = item:get_stack_max()
583 local maxnumber = number_stack_max * slots_max
585 if ( number == nil or number < 1 or number > maxnumber ) or
586 ( cost == nil or cost < 1 or cost > maxcost ) or
587 ( itemname == nil or itemname=="") then
588 meta:set_string("status", "Invalid item count or price!")
589 easyvend.machine_disable(pos, node, sendername)
590 return
593 local chest_pos_remove, chest_error_remove, chest_pos_add, chest_error_add
594 if buysell == "sell" then
595 chest_pos_remove, chest_error_remove = easyvend.find_connected_chest(machine_owner, pos, itemname, check_wear, number, true)
596 chest_pos_add, chest_error_add = easyvend.find_connected_chest(machine_owner, pos, easyvend.currency, check_wear, cost, false)
597 else
598 chest_pos_remove, chest_error_remove = easyvend.find_connected_chest(machine_owner, pos, easyvend.currency, check_wear, cost, true)
599 chest_pos_add, chest_error_add = easyvend.find_connected_chest(machine_owner, pos, itemname, check_wear, number, false)
602 if chest_pos_remove ~= nil and chest_pos_add ~= nil and sender and sender:is_player() then
603 local rchest = minetest.get_node(chest_pos_remove)
604 local rchestdef = registered_chests[rchest.name]
605 local rchest_meta = minetest.get_meta(chest_pos_remove)
606 local rchest_inv = rchest_meta:get_inventory()
607 local achest = minetest.get_node(chest_pos_add)
608 local achestdef = registered_chests[achest.name]
609 local achest_meta = minetest.get_meta(chest_pos_add)
610 local achest_inv = achest_meta:get_inventory()
612 local player_inv = sender:get_inventory()
614 local stack = {name=itemname, count=number, wear=0, metadata=""}
615 local price = {name=easyvend.currency, count=cost, wear=0, metadata=""}
616 local chest_has, player_has, chest_free, player_free, chest_out, player_out
617 local msg = ""
618 if buysell == "sell" then
619 chest_has, chest_out = easyvend.check_and_get_items(rchest_inv, rchestdef.inv_list, stack, check_wear)
620 player_has, player_out = easyvend.check_and_get_items(player_inv, "main", price, check_wear)
621 chest_free = achest_inv:room_for_item(achestdef.inv_list, price)
622 player_free = player_inv:room_for_item("main", stack)
623 if chest_has and player_has and chest_free and player_free then
624 if cost <= cost_stack_max and number <= number_stack_max then
625 easyvend.machine_enable(pos, node)
626 player_inv:remove_item("main", price)
627 if check_wear then
628 rchest_inv:set_stack(rchestdef.inv_list, chest_out[1].id, "")
629 player_inv:add_item("main", chest_out[1].item)
630 else
631 stack = rchest_inv:remove_item(rchestdef.inv_list, stack)
632 player_inv:add_item("main", stack)
634 achest_inv:add_item(achestdef.inv_list, price)
635 if itemname == easyvend.currency and number == cost and cost <= cost_stack_max then
636 meta:set_string("message", easyvend.get_joke(buysell, meta:get_int("joke_id")))
637 meta:set_int("joketimer", joketimer_start)
638 else
639 meta:set_string("message", "Item bought.")
641 easyvend.check_earnings(sendername, meta)
642 easyvend.sound_vend(pos)
643 easyvend.machine_check(pos, node)
644 else
645 -- Large item counts (multiple stacks)
646 local coststacks = math.modf(cost / cost_stack_max)
647 local costremainder = math.fmod(cost, cost_stack_max)
648 local numberstacks = math.modf(number / number_stack_max)
649 local numberremainder = math.fmod(number, number_stack_max)
650 local numberfree = numberstacks
651 local costfree = coststacks
652 if numberremainder > 0 then numberfree = numberfree + 1 end
653 if costremainder > 0 then costfree = costfree + 1 end
654 if not player_free and easyvend.free_slots(player_inv, "main") < numberfree then
655 if numberfree > 1 then
656 msg = string.format("No room in your inventory (%d empty slots required)!", numberfree)
657 else
658 msg = "No room in your inventory!"
660 meta:set_string("message", msg)
661 elseif not chest_free and easyvend.free_slots(achest_inv, achestdef.inv_list) < costfree then
662 meta:set_string("status", "No room in the machine’s storage!")
663 easyvend.machine_disable(pos, node, sendername)
664 else
665 -- Remember items for transfer
666 local cheststacks = {}
667 easyvend.machine_enable(pos, node)
668 for i=1, coststacks do
669 price.count = cost_stack_max
670 player_inv:remove_item("main", price)
672 if costremainder > 0 then
673 price.count = costremainder
674 player_inv:remove_item("main", price)
676 if check_wear then
677 for o=1,#chest_out do
678 rchest_inv:set_stack(rchestdef.inv_list, chest_out[o].id, "")
680 else
681 for i=1, numberstacks do
682 stack.count = number_stack_max
683 table.insert(cheststacks, rchest_inv:remove_item(rchestdef.inv_list, stack))
686 if numberremainder > 0 then
687 stack.count = numberremainder
688 table.insert(cheststacks, rchest_inv:remove_item(rchestdef.inv_list, stack))
690 for i=1, coststacks do
691 price.count = cost_stack_max
692 achest_inv:add_item(achestdef.inv_list, price)
694 if costremainder > 0 then
695 price.count = costremainder
696 achest_inv:add_item(achestdef.inv_list, price)
698 if check_wear then
699 for o=1,#chest_out do
700 player_inv:add_item("main", chest_out[o].item)
702 else
703 for i=1,#cheststacks do
704 player_inv:add_item("main", cheststacks[i])
707 meta:set_string("message", "Item bought.")
708 easyvend.check_earnings(sendername, meta)
709 easyvend.sound_vend(pos)
710 easyvend.machine_check(pos, node)
713 elseif chest_has and player_has then
714 if not player_free then
715 msg = "No room in your inventory!"
716 meta:set_string("message", msg)
717 easyvend.sound_error(sendername)
718 elseif not chest_free then
719 msg = "No room in the machine’s storage!"
720 meta:set_string("status", msg)
721 easyvend.machine_disable(pos, node, sendername)
723 else
724 if not chest_has then
725 msg = "The vending machine has insufficient materials!"
726 meta:set_string("status", msg)
727 easyvend.machine_disable(pos, node, sendername)
728 elseif not player_has then
729 msg = "You can’t afford this item!"
730 meta:set_string("message", msg)
731 easyvend.sound_error(sendername)
734 else
735 chest_has, chest_out = easyvend.check_and_get_items(rchest_inv, rchestdef.inv_list, price, check_wear)
736 player_has, player_out = easyvend.check_and_get_items(player_inv, "main", stack, check_wear)
737 chest_free = achest_inv:room_for_item(achestdef.inv_list, stack)
738 player_free = player_inv:room_for_item("main", price)
739 if chest_has and player_has and chest_free and player_free then
740 if cost <= cost_stack_max and number <= number_stack_max then
741 easyvend.machine_enable(pos, node)
742 if check_wear then
743 player_inv:set_stack("main", player_out[1].id, "")
744 achest_inv:add_item(achestdef.inv_list, player_out[1].item)
745 else
746 stack = player_inv:remove_item("main", stack)
747 achest_inv:add_item(achestdef.inv_list, stack)
749 rchest_inv:remove_item(rchestdef.inv_list, price)
750 player_inv:add_item("main", price)
751 meta:set_string("status", "Ready.")
752 if itemname == easyvend.currency and number == cost and cost <= cost_stack_max then
753 meta:set_string("message", easyvend.get_joke(buysell, meta:get_int("joke_id")))
754 meta:set_int("joketimer", joketimer_start)
755 else
756 meta:set_string("message", "Item sold.")
758 easyvend.sound_deposit(pos)
759 easyvend.machine_check(pos, node)
760 else
761 -- Large item counts (multiple stacks)
762 local coststacks = math.modf(cost / cost_stack_max)
763 local costremainder = math.fmod(cost, cost_stack_max)
764 local numberstacks = math.modf(number / number_stack_max)
765 local numberremainder = math.fmod(number, number_stack_max)
766 local numberfree = numberstacks
767 local costfree = coststacks
768 if numberremainder > 0 then numberfree = numberfree + 1 end
769 if costremainder > 0 then costfree = costfree + 1 end
770 if not player_free and easyvend.free_slots(player_inv, "main") < costfree then
771 if costfree > 1 then
772 msg = string.format("No room in your inventory (%d empty slots required)!", costfree)
773 else
774 msg = "No room in your inventory!"
776 meta:set_string("message", msg)
777 easyvend.sound_error(sendername)
778 elseif not chest_free and easyvend.free_slots(achest_inv, achestdef.inv_list) < numberfree then
779 meta:set_string("status", "No room in the machine’s storage!")
780 easyvend.machine_disable(pos, node, sendername)
781 else
782 easyvend.machine_enable(pos, node)
783 -- Remember removed items for transfer
784 local playerstacks = {}
785 for i=1, coststacks do
786 price.count = cost_stack_max
787 rchest_inv:remove_item(rchestdef.inv_list, price)
789 if costremainder > 0 then
790 price.count = costremainder
791 rchest_inv:remove_item(rchestdef.inv_list, price)
793 if check_wear then
794 for o=1,#player_out do
795 player_inv:set_stack("main", player_out[o].id, "")
797 else
798 for i=1, numberstacks do
799 stack.count = number_stack_max
800 table.insert(playerstacks, player_inv:remove_item("main", stack))
803 if numberremainder > 0 then
804 stack.count = numberremainder
805 table.insert(playerstacks, player_inv:remove_item("main", stack))
807 for i=1, coststacks do
808 price.count = cost_stack_max
809 player_inv:add_item("main", price)
811 if costremainder > 0 then
812 price.count = costremainder
813 player_inv:add_item("main", price)
815 if check_wear then
816 for o=1,#player_out do
817 achest_inv:add_item(achestdef.inv_list, player_out[o].item)
819 else
820 for i=1,#playerstacks do
821 achest_inv:add_item(achestdef.inv_list, playerstacks[i])
824 meta:set_string("message", "Item sold.")
825 easyvend.sound_deposit(pos)
826 easyvend.machine_check(pos, node)
829 elseif chest_has and player_has then
830 if not player_free then
831 msg = "No room in your inventory!"
832 meta:set_string("message", msg)
833 easyvend.sound_error(sendername)
834 elseif not chest_free then
835 msg = "No room in the machine’s storage!"
836 meta:set_string("status", msg)
837 easyvend.machine_disable(pos, node, sendername)
839 else
840 if not player_has then
841 msg = "You have insufficient materials!"
842 meta:set_string("message", msg)
843 easyvend.sound_error(sendername)
844 elseif not chest_has then
845 msg = "The depositing machine is out of money!"
846 meta:set_string("status", msg)
847 easyvend.machine_disable(pos, node, sendername)
851 else
852 local status
853 meta:set_int("stock", 0)
854 if chest_error_remove == "no_chest" and chest_error_add == "no_chest" then
855 status = "No storage; machine needs to be connected with a locked chest."
856 elseif chest_error_remove == "not_owned" or chest_error_add == "not_owned" then
857 status = "Storage can’t be accessed because it is owned by a different person!"
858 elseif chest_error_remove == "no_stock" then
859 if buysell == "sell" then
860 status = "The vending machine has insufficient materials!"
861 else
862 status = "The depositing machine is out of money!"
864 elseif chest_error_add == "no_space" then
865 status = "No room in the machine’s storage!"
866 else
867 status = "Unknown error!"
869 meta:set_string("status", status)
870 easyvend.sound_error(sendername)
873 easyvend.set_formspec(pos, sender)
877 easyvend.after_place_node = function(pos, placer)
878 local node = minetest.get_node(pos)
879 local meta = minetest.get_meta(pos)
880 local inv = meta:get_inventory()
881 local player_name = placer:get_player_name()
882 inv:set_size("item", 1)
883 inv:set_size("gold", 1)
885 inv:set_stack( "gold", 1, easyvend.currency )
887 local d = ""
888 if node.name == "easyvend:vendor" then
889 d = string.format("Inactive vending machine (owned by %s)", player_name)
890 meta:set_int("wear", 1)
891 -- Total number of currency items earned for the machine's life time (excluding currency-currency trading)
892 meta:set_int("earnings", 0)
893 elseif node.name == "easyvend:depositor" then
894 d = string.format("Inactive depositing machine (owned by %s)", player_name)
895 meta:set_int("wear", 0)
897 meta:set_string("infotext", d)
898 meta:set_string("status", "Awaiting configuration by owner.")
899 meta:set_string("message", "Welcome! Please prepare the machine.")
900 meta:set_int("number", 1)
901 meta:set_int("cost", 1)
902 meta:set_int("stock", -1)
903 meta:set_int("configmode", 1)
904 meta:set_int("joketimer", -1)
905 meta:set_int("joke_id", 1)
906 meta:set_string("itemname", "")
908 meta:set_string("owner", player_name or "")
910 easyvend.set_formspec(pos, placer)
913 easyvend.can_dig = function(pos, player)
914 local meta = minetest.get_meta(pos)
915 local name = player:get_player_name()
916 local owner = meta:get_string("owner")
917 -- Owner can always dig shop
918 if owner == name then
919 return true
921 local chest_pos = easyvend.find_connected_chest(owner, pos)
922 local chest, meta_chest
923 if chest_pos then
924 chest = minetest.get_node(chest_pos)
925 meta_chest = minetest.get_meta(chest_pos)
926 else
927 return true --if no chest, enyone can dig this shop
929 if registered_chests[chest.name] then
930 if player and player:is_player() then
931 local owner_chest = meta_chest:get_string(registered_chests[chest.name].meta_owner)
932 if name == owner_chest then
933 return true --chest owner can also dig shop
936 return false
937 else
938 return true --if no chest, enyone can dig this shop
942 easyvend.on_receive_fields = function(pos, formname, fields, sender)
943 local meta = minetest.get_meta(pos)
944 local node = minetest.get_node(pos)
945 local owner = meta:get_string("owner")
946 local sendername = sender:get_player_name()
948 if fields.doc then
949 if minetest.get_modpath("doc") and minetest.get_modpath("doc_items") then
950 if easyvend.buysell(node.name) == "buy" then
951 doc.show_entry(sendername, "nodes", "easyvend:depositor", true)
952 else
953 doc.show_entry(sendername, "nodes", "easyvend:vendor", true)
956 elseif fields.config or fields.save or fields.usermode then
957 if sender:get_player_name() == owner then
958 easyvend.on_receive_fields_config(pos, formname, fields, sender)
959 else
960 meta:set_string("message", "Only the owner may change the configuration.")
961 easyvend.sound_error(sendername)
962 easyvend.set_formspec(pos, sender)
963 return
965 elseif fields.wear ~= nil then
966 if sender:get_player_name() == owner then
967 if fields.wear == "true" then
968 if easyvend.buysell(node.name) == "buy" then
969 meta:set_string("message", "Used tools are now accepted.")
970 else
971 meta:set_string("message", "Used tools are now for sale.")
973 meta:set_int("wear", 1)
974 elseif fields.wear == "false" then
975 if easyvend.buysell(node.name) == "buy" then
976 meta:set_string("message", "Used tools are now rejected.")
977 else
978 meta:set_string("message", "Used tools won’t be sold anymore.")
980 meta:set_int("wear", 0)
982 easyvend.set_formspec(pos, sender)
983 return
984 else
985 meta:set_string("message", "Only the owner may change the configuration.")
986 easyvend.sound_error(sendername)
987 easyvend.set_formspec(pos, sender)
988 return
990 elseif fields.buysell then
991 easyvend.on_receive_fields_buysell(pos, formname, fields, sender)
995 -- Jokes: Appear when machine exchanges currency for currency at equal rate
997 -- Vendor
998 local jokes_vendor = {
999 "Thank you. You have made a vending machine very happy.",
1000 "Humans have a strange sense of humor.",
1001 "Let’s get this over with …",
1002 "Item “bought”.",
1003 "Tit for tat.",
1004 "Do you realize what you’ve just bought?",
1006 -- Depositor
1007 local jokes_depositor = {
1008 "Thank you, the money started to smell inside.",
1009 "Money doesn’t grow on trees, you know?",
1010 "Sanity sold.",
1011 "Well, that was an awkward exchange.",
1012 "Are you having fun?",
1013 "Is this really trading?",
1016 easyvend.assign_joke = function(buysell)
1017 local jokes
1018 if buysell == "sell" then
1019 jokes = jokes_vendor
1020 elseif buysell == "buy" then
1021 jokes = jokes_depositor
1023 local r = math.random(1,#jokes)
1024 return r
1027 easyvend.get_joke = function(buysell, id)
1028 local joke
1029 if buysell == nil or id == nil then
1030 -- Fallback message (should never happen)
1031 return "Items exchanged."
1033 if buysell == "sell" then
1034 joke = jokes_vendor[id]
1035 if joke == nil then joke = jokes_vendor[1] end
1036 elseif buysell == "buy" then
1037 joke = jokes_depositor[id]
1038 if joke == nil then joke = jokes_depositor[1] end
1040 return joke
1043 easyvend.sound_error = function(playername)
1044 minetest.sound_play("easyvend_error", {to_player = playername, gain = 0.25})
1047 easyvend.sound_setup = function(pos)
1048 minetest.sound_play("easyvend_activate", {pos = pos, gain = 0.5, max_hear_distance = 12,})
1051 easyvend.sound_disable = function(pos)
1052 minetest.sound_play("easyvend_disable", {pos = pos, gain = 0.9, max_hear_distance = 12,})
1055 easyvend.sound_vend = function(pos)
1056 minetest.sound_play("easyvend_vend", {pos = pos, gain = 0.4, max_hear_distance = 5,})
1059 easyvend.sound_deposit = function(pos)
1060 minetest.sound_play("easyvend_deposit", {pos = pos, gain = 0.4, max_hear_distance = 5,})
1063 --[[ Tower building ]]
1065 easyvend.is_traversable = function(pos)
1066 local node = minetest.get_node_or_nil(pos)
1067 if (node == nil) then
1068 return false
1070 return traversable_node_types[node.name] == true
1073 easyvend.neighboring_nodes = function(pos)
1074 local check = {
1075 {x=pos.x, y=pos.y-1, z=pos.z},
1076 {x=pos.x, y=pos.y+1, z=pos.z},
1078 local trav = {}
1079 for i=1,#check do
1080 if easyvend.is_traversable(check[i]) then
1081 table.insert(trav, check[i])
1084 return trav
1087 easyvend.find_connected_chest = function(owner, pos, nodename, check_wear, amount, removing)
1088 local nodes = easyvend.neighboring_nodes(pos)
1090 if (#nodes < 1 or #nodes > 2) then
1091 return nil, "no_chest"
1094 -- Find the stack direction
1095 local first = nil
1096 local second = nil
1097 for i=1,#nodes do
1098 if ( first == nil ) then
1099 first = nodes[i]
1100 else
1101 second = nodes[i]
1105 local chest_pos, chest_internal
1107 if (first ~= nil and second ~= nil) then
1108 local dy = (first.y - second.y)/2
1109 chest_pos, chest_internal = easyvend.find_chest(owner, pos, dy, nodename, check_wear, amount, removing)
1110 if ( chest_pos == nil ) then
1111 chest_pos, chest_internal = easyvend.find_chest(owner, pos, -dy, nodename, check_wear, amount, removing, chest_internal)
1113 else
1114 local dy = first.y - pos.y
1115 chest_pos, chest_internal = easyvend.find_chest(owner, pos, dy, nodename, check_wear, amount, removing)
1118 if chest_internal.chests == 0 then
1119 return nil, "no_chest"
1120 elseif chest_internal.chests == chest_internal.other_chests then
1121 return nil, "not_owned"
1122 elseif removing and chest_internal.stock < 1 then
1123 return nil, "no_stock"
1124 elseif not removing and chest_internal.space < 1 then
1125 return nil, "no_space"
1126 elseif chest_pos ~= nil then
1127 return chest_pos
1128 else
1129 return nil, "unknown"
1133 easyvend.find_chest = function(owner, pos, dy, itemname, check_wear, amount, removing, internal)
1134 pos = {x=pos.x, y=pos.y + dy, z=pos.z}
1136 if internal == nil then
1137 internal = {}
1138 internal.chests = 0
1139 internal.other_chests = 0
1140 internal.stock = 0
1141 internal.space = 0
1144 local node = minetest.get_node_or_nil(pos)
1145 if ( node == nil ) then
1146 return nil, internal
1148 local chestdef = registered_chests[node.name]
1149 if (chestdef ~= nil) then
1150 internal.chests = internal.chests + 1
1151 local meta = minetest.get_meta(pos)
1152 if (owner ~= meta:get_string(chestdef.meta_owner)) then
1153 internal.other_chests = internal.other_chests + 1
1154 return nil, internal
1156 local inv = meta:get_inventory()
1157 if (inv ~= nil) then
1158 if (itemname ~= nil and minetest.registered_items[itemname] and amount ~= nil and removing ~= nil and check_wear ~= nil) then
1159 local chest_has, chest_free
1160 local stack = {name=itemname, count=amount, wear=0, metadata=""}
1161 local stack_max = minetest.registered_items[itemname].stack_max
1163 local stacks = math.modf(amount / stack_max)
1164 local stacksremainder = math.fmod(amount, stack_max)
1165 local free = stacks
1166 if stacksremainder > 0 then free = free + 1 end
1168 chest_has = easyvend.check_and_get_items(inv, chestdef.inv_list, stack, check_wear)
1169 if chest_has then
1170 internal.stock = internal.stock + 1
1172 chest_free = inv:room_for_item(chestdef.inv_list, stack) and easyvend.free_slots(inv, chestdef.inv_list) >= free
1173 if chest_free then
1174 internal.space = internal.space + 1
1177 if (removing and internal.stock == 0) or (not removing and internal.space == 0) then
1178 return easyvend.find_chest(owner, pos, dy, itemname, check_wear, amount, removing, internal)
1179 else
1180 return pos, internal
1182 else
1183 return nil, internal
1185 else
1186 return nil, internal
1188 elseif (node.name ~= "easyvend:vendor" and node.name~="easyvend:depositor" and node.name~="easyvend:vendor_on" and node.name~="easyvend:depositor_on") then
1189 return nil, internal
1192 return easyvend.find_chest(owner, pos, dy, itemname, check_wear, amount, removing, internal)
1195 -- Pseudo-inventory handling
1196 easyvend.allow_metadata_inventory_put = function(pos, listname, index, stack, player)
1197 if listname=="item" then
1198 local meta = minetest.get_meta(pos);
1199 local owner = meta:get_string("owner")
1200 local name = player:get_player_name()
1201 if name == owner then
1202 local inv = meta:get_inventory()
1203 if stack==nil then
1204 inv:set_stack( "item", 1, nil )
1205 else
1206 inv:set_stack( "item", 1, stack:get_name() )
1207 meta:set_string("itemname", stack:get_name())
1208 easyvend.set_formspec(pos, player)
1212 return 0
1215 easyvend.allow_metadata_inventory_take = function(pos, listname, index, stack, player)
1216 return 0
1219 easyvend.allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
1220 return 0
1223 minetest.register_abm({
1224 nodenames = {"easyvend:vendor", "easyvend:vendor_on", "easyvend:depositor", "easyvend:depositor_on"},
1225 interval = 5,
1226 chance = 1,
1227 catch_up = false,
1228 action = function(pos, node, active_object_count, active_object_count_wider)
1229 easyvend.machine_check(pos, node)
1233 -- Legacy support for vendor mod:
1234 -- Transform the world and items to use the easyvend nodes/items
1236 -- For safety reasons, only do this when player requested so
1237 if minetest.setting_getbool("easyvend_convert_vendor") == true then
1238 -- Replace vendor nodes
1239 minetest.register_lbm({
1240 name = "easyvend:replace_vendor",
1241 nodenames = { "vendor:vendor", "vendor:depositor" },
1242 run_at_every_load = true,
1243 action = function(pos, node)
1244 -- Replace node
1245 local newnodename
1246 if node.name == "vendor:vendor" then
1247 newnodename = "easyvend:vendor"
1248 elseif node.name == "vendor:depositor" then
1249 newnodename = "easyvend:depositor"
1251 -- Remove axis rotation; only allow 4 facedirs
1252 local p2 = math.fmod(node.param2, 4)
1253 minetest.swap_node(pos, { name = newnodename, param2 = p2 })
1255 -- Initialize metadata
1256 local meta = minetest.get_meta(pos)
1257 if node.name == "vendor:vendor" then
1258 meta:set_int("earnings", 0)
1260 meta:set_int("stock", -1)
1261 meta:set_int("joketimer", -1)
1262 meta:set_int("joke_id", 1)
1263 local inv = meta:get_inventory()
1264 inv:set_size("item", 1)
1265 inv:set_size("gold", 1)
1266 inv:set_stack("gold", 1, easyvend.currency)
1268 -- In vendor, all machines accepted worn tools
1269 meta:set_int("wear", 1)
1271 -- Set item
1272 local itemname = meta:get_string("itemname")
1273 if itemname == "" or itemname == nil then
1274 itemname = meta:get_string("itemtype")
1276 if itemname ~= "" and itemname ~= nil then
1277 inv:set_stack("item", 1, itemname)
1278 meta:set_string("itemname", itemname)
1281 -- Check for valid item, item count and price
1282 local configmode = 1
1283 if itemname ~= "" and itemname ~= nil then
1284 local itemstack = inv:get_stack("item", 1)
1285 local number_stack_max = itemstack:get_stack_max()
1286 local maxnumber = number_stack_max * slots_max
1287 local cost = meta:get_int("cost")
1288 local number = meta:get_int("number")
1289 if number >= 1 and number <= maxnumber and cost >= 1 and cost <= maxcost then
1290 -- Everything's OK, get out of config mode!
1291 configmode = 0
1295 -- Final initialization stuff
1296 meta:set_int("configmode", configmode)
1298 local owner = meta:get_string("owner")
1299 if easyvend.buysell(newnodename) == "sell" then
1300 meta:set_string("infotext", string.format("Vending machine (owned by %s)", owner))
1301 else
1302 meta:set_string("infotext", string.format("Depositing machine (owned by %s)", owner))
1306 meta:set_string("status", "Initializing …")
1307 meta:set_string("message", "Upgrade successful.")
1308 easyvend.machine_check(pos, node)
1309 end,