Fix doc_items optional dependency
[minetest_easyvend.git] / easyvend.lua
blob7091558d9ce3891574b3673991fae07a4a7f4dc5
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 TODO: Expose this in user documentation ]]
401 local number_stack_max = itemstack:get_stack_max()
402 local ss = string.sub(number, #number, #number)
403 if ss == "s" or ss == "S" then
404 local n = tonumber(string.sub(number, 1, #number-1))
405 if string.len(number) == 1 then n = 1 end
406 if n ~= nil then
407 number = n * number_stack_max
410 ss = string.sub(cost, #cost, #cost)
411 if ss == "s" or ss == "S" then
412 local n = tonumber(string.sub(cost, 1, #cost-1))
413 if string.len(cost) == 1 then n = 1 end
414 if n ~= nil then
415 cost = n * cost_stack_max
418 number = tonumber(number)
419 cost = tonumber(cost)
421 local itemname=""
423 local oldnumber = meta:get_int("number")
424 local oldcost = meta:get_int("cost")
425 local maxnumber = number_stack_max * slots_max
427 if ( itemstack == nil or itemstack:is_empty() ) then
428 meta:set_string("status", "Awaiting configuration by owner.")
429 meta:set_string("message", "No item specified.")
430 easyvend.sound_error(sender:get_player_name())
431 easyvend.set_formspec(pos, sender)
432 return
433 elseif ( number == nil or number < 1 or number > maxnumber ) then
434 if maxnumber > 1 then
435 meta:set_string("message", string.format("Invalid item count; must be between 1 and %d!", maxnumber))
436 else
437 meta:set_string("message", "Invalid item count; must be exactly 1!")
439 meta:set_int("number", oldnumber)
440 easyvend.sound_error(sender:get_player_name())
441 easyvend.set_formspec(pos, sender)
442 return
443 elseif ( cost == nil or cost < 1 or cost > maxcost ) then
444 if maxcost > 1 then
445 meta:set_string("message", string.format("Invalid cost; must be between 1 and %d!", maxcost))
446 else
447 meta:set_string("message", "Invalid cost; must be exactly 1!")
449 meta:set_int("cost", oldcost)
450 easyvend.sound_error(sender:get_player_name())
451 easyvend.set_formspec(pos, sender)
452 return
454 meta:set_int("number", number)
455 meta:set_int("cost", cost)
456 itemname=itemstack:get_name()
457 meta:set_string("itemname", itemname)
458 meta:set_int("configmode", 0)
460 if itemname == easyvend.currency and number == cost and cost <= cost_stack_max then
461 meta:set_string("message", "Configuration successful. I am feeling funny.")
462 meta:set_int("joketimer", joketimer_start)
463 meta:set_int("joke_id", easyvend.assign_joke(buysell))
464 else
465 meta:set_string("message", "Configuration successful.")
468 local change = easyvend.machine_check(pos, node)
470 if not change then
471 if (node.name == "easyvend:vendor_on" or node.name == "easyvend:depositor_on") then
472 easyvend.sound_setup(pos)
473 else
474 easyvend.sound_disable(pos)
479 easyvend.make_infotext = function(nodename, owner, cost, number, itemstring)
480 local d = ""
481 if itemstring == nil or itemstring == "" or number == 0 or cost == 0 then
482 if easyvend.buysell(nodename) == "sell" then
483 d = string.format("Inactive vending machine (owned by %s)", owner)
484 else
485 d = string.format("Inactive depositing machine (owned by %s)", owner)
487 return d
489 local iname = minetest.registered_items[itemstring].description
490 if iname == nil then iname = itemstring end
491 local printitem, printcost
492 if number == 1 then
493 printitem = iname
494 else
495 printitem = string.format("%d×%s", number, iname)
497 if cost == 1 then
498 printcost = easyvend.currency_desc
499 else
500 printcost = string.format("%d×%s", cost, easyvend.currency_desc)
502 if nodename == "easyvend:vendor_on" then
503 d = string.format("Vending machine (owned by %s)\nSelling: %s\nPrice: %s", owner, printitem, printcost)
504 elseif nodename == "easyvend:vendor" then
505 d = string.format("Inactive vending machine (owned by %s)\nSelling: %s\nPrice: %s", owner, printitem, printcost)
506 elseif nodename == "easyvend:depositor_on" then
507 d = string.format("Depositing machine (owned by %s)\nBuying: %s\nPayment: %s", owner, printitem, printcost)
508 elseif nodename == "easyvend:depositor" then
509 d = string.format("Inactive depositing machine (owned by %s)\nBuying: %s\nPayment: %s", owner, printitem, printcost)
511 return d
514 if minetest.get_modpath("awards") then
515 awards.register_achievement("easyvend_seller",{
516 title = "First Sale",
517 description = "Sell something with a vending machine.",
518 icon = "easyvend_vendor_front_on.png^awards_level1.png",
520 local desc_powerseller
521 if easyvend.currency == "default:gold_ingot" then
522 desc_powerseller = string.format("Earn %d gold ingots by selling goods with a single vending machine.", easyvend.powerseller)
523 else
524 desc_powerseller = string.format("Earn %d currency items by selling goods with a single vending machine.", easyvend.powerseller)
526 awards.register_achievement("easyvend_powerseller",{
527 title = "Power Seller",
528 description = desc_powerseller,
529 icon = "easyvend_vendor_front_on.png^awards_level2.png",
533 easyvend.check_earnings = function(buyername, nodemeta)
534 local owner = nodemeta:get_string("owner")
535 if buyername ~= owner then
536 local cost = nodemeta:get_int("cost")
537 local itemname = nodemeta:get_string("itemname")
538 -- First sell
539 if minetest.get_modpath("awards") and minetest.get_player_by_name(owner) ~= nil then
540 awards.unlock(owner, "easyvend_seller")
542 if itemname ~= easyvend.currency then
543 local newearnings = nodemeta:get_int("earnings") + cost
544 if newearnings >= easyvend.powerseller and minetest.get_modpath("awards") then
545 if minetest.get_player_by_name(owner) ~= nil then
546 awards.unlock(owner, "easyvend_powerseller")
549 nodemeta:set_int("earnings", newearnings)
554 easyvend.on_receive_fields_buysell = function(pos, formname, fields, sender)
555 local sendername = sender:get_player_name()
556 local meta = minetest.get_meta(pos)
558 if not fields.buysell then
559 return
562 local node = minetest.get_node(pos)
563 local number = meta:get_int("number")
564 local cost = meta:get_int("cost")
565 local itemname=meta:get_string("itemname")
566 local item=meta:get_inventory():get_stack("item", 1)
567 local check_wear = meta:get_int("wear") == 0 and minetest.registered_tools[itemname] ~= nil
568 local machine_owner = meta:get_string("owner")
570 local buysell = easyvend.buysell(node.name)
572 local number_stack_max = item:get_stack_max()
573 local maxnumber = number_stack_max * slots_max
575 if ( number == nil or number < 1 or number > maxnumber ) or
576 ( cost == nil or cost < 1 or cost > maxcost ) or
577 ( itemname == nil or itemname=="") then
578 meta:set_string("status", "Invalid item count or price!")
579 easyvend.machine_disable(pos, node, sendername)
580 return
583 local chest_pos_remove, chest_error_remove, chest_pos_add, chest_error_add
584 if buysell == "sell" then
585 chest_pos_remove, chest_error_remove = easyvend.find_connected_chest(machine_owner, pos, itemname, check_wear, number, true)
586 chest_pos_add, chest_error_add = easyvend.find_connected_chest(machine_owner, pos, easyvend.currency, check_wear, cost, false)
587 else
588 chest_pos_remove, chest_error_remove = easyvend.find_connected_chest(machine_owner, pos, easyvend.currency, check_wear, cost, true)
589 chest_pos_add, chest_error_add = easyvend.find_connected_chest(machine_owner, pos, itemname, check_wear, number, false)
592 if chest_pos_remove ~= nil and chest_pos_add ~= nil and sender and sender:is_player() then
593 local rchest = minetest.get_node(chest_pos_remove)
594 local rchestdef = registered_chests[rchest.name]
595 local rchest_meta = minetest.get_meta(chest_pos_remove)
596 local rchest_inv = rchest_meta:get_inventory()
597 local achest = minetest.get_node(chest_pos_add)
598 local achestdef = registered_chests[achest.name]
599 local achest_meta = minetest.get_meta(chest_pos_add)
600 local achest_inv = achest_meta:get_inventory()
602 local player_inv = sender:get_inventory()
604 local stack = {name=itemname, count=number, wear=0, metadata=""}
605 local price = {name=easyvend.currency, count=cost, wear=0, metadata=""}
606 local chest_has, player_has, chest_free, player_free, chest_out, player_out
607 local msg = ""
608 if buysell == "sell" then
609 chest_has, chest_out = easyvend.check_and_get_items(rchest_inv, rchestdef.inv_list, stack, check_wear)
610 player_has, player_out = easyvend.check_and_get_items(player_inv, "main", price, check_wear)
611 chest_free = achest_inv:room_for_item(achestdef.inv_list, price)
612 player_free = player_inv:room_for_item("main", stack)
613 if chest_has and player_has and chest_free and player_free then
614 if cost <= cost_stack_max and number <= number_stack_max then
615 easyvend.machine_enable(pos, node)
616 player_inv:remove_item("main", price)
617 if check_wear then
618 rchest_inv:set_stack(rchestdef.inv_list, chest_out[1].id, "")
619 player_inv:add_item("main", chest_out[1].item)
620 else
621 stack = rchest_inv:remove_item(rchestdef.inv_list, stack)
622 player_inv:add_item("main", stack)
624 achest_inv:add_item(achestdef.inv_list, price)
625 if itemname == easyvend.currency and number == cost and cost <= cost_stack_max then
626 meta:set_string("message", easyvend.get_joke(buysell, meta:get_int("joke_id")))
627 meta:set_int("joketimer", joketimer_start)
628 else
629 meta:set_string("message", "Item bought.")
631 easyvend.check_earnings(sendername, meta)
632 easyvend.sound_vend(pos)
633 easyvend.machine_check(pos, node)
634 else
635 -- Large item counts (multiple stacks)
636 local coststacks = math.modf(cost / cost_stack_max)
637 local costremainder = math.fmod(cost, cost_stack_max)
638 local numberstacks = math.modf(number / number_stack_max)
639 local numberremainder = math.fmod(number, number_stack_max)
640 local numberfree = numberstacks
641 local costfree = coststacks
642 if numberremainder > 0 then numberfree = numberfree + 1 end
643 if costremainder > 0 then costfree = costfree + 1 end
644 if not player_free and easyvend.free_slots(player_inv, "main") < numberfree then
645 if numberfree > 1 then
646 msg = string.format("No room in your inventory (%d empty slots required)!", numberfree)
647 else
648 msg = "No room in your inventory!"
650 meta:set_string("message", msg)
651 elseif not chest_free and easyvend.free_slots(achest_inv, achestdef.inv_list) < costfree then
652 meta:set_string("status", "No room in the machine’s storage!")
653 easyvend.machine_disable(pos, node, sendername)
654 else
655 -- Remember items for transfer
656 local cheststacks = {}
657 easyvend.machine_enable(pos, node)
658 for i=1, coststacks do
659 price.count = cost_stack_max
660 player_inv:remove_item("main", price)
662 if costremainder > 0 then
663 price.count = costremainder
664 player_inv:remove_item("main", price)
666 if check_wear then
667 for o=1,#chest_out do
668 rchest_inv:set_stack(rchestdef.inv_list, chest_out[o].id, "")
670 else
671 for i=1, numberstacks do
672 stack.count = number_stack_max
673 table.insert(cheststacks, rchest_inv:remove_item(rchestdef.inv_list, stack))
676 if numberremainder > 0 then
677 stack.count = numberremainder
678 table.insert(cheststacks, rchest_inv:remove_item(rchestdef.inv_list, stack))
680 for i=1, coststacks do
681 price.count = cost_stack_max
682 achest_inv:add_item(achestdef.inv_list, price)
684 if costremainder > 0 then
685 price.count = costremainder
686 achest_inv:add_item(achestdef.inv_list, price)
688 if check_wear then
689 for o=1,#chest_out do
690 player_inv:add_item("main", chest_out[o].item)
692 else
693 for i=1,#cheststacks do
694 player_inv:add_item("main", cheststacks[i])
697 meta:set_string("message", "Item bought.")
698 easyvend.check_earnings(sendername, meta)
699 easyvend.sound_vend(pos)
700 easyvend.machine_check(pos, node)
703 elseif chest_has and player_has then
704 if not player_free then
705 msg = "No room in your inventory!"
706 meta:set_string("message", msg)
707 easyvend.sound_error(sendername)
708 elseif not chest_free then
709 msg = "No room in the machine’s storage!"
710 meta:set_string("status", msg)
711 easyvend.machine_disable(pos, node, sendername)
713 else
714 if not chest_has then
715 msg = "The vending machine has insufficient materials!"
716 meta:set_string("status", msg)
717 easyvend.machine_disable(pos, node, sendername)
718 elseif not player_has then
719 msg = "You can’t afford this item!"
720 meta:set_string("message", msg)
721 easyvend.sound_error(sendername)
724 else
725 chest_has, chest_out = easyvend.check_and_get_items(rchest_inv, rchestdef.inv_list, price, check_wear)
726 player_has, player_out = easyvend.check_and_get_items(player_inv, "main", stack, check_wear)
727 chest_free = achest_inv:room_for_item(achestdef.inv_list, stack)
728 player_free = player_inv:room_for_item("main", price)
729 if chest_has and player_has and chest_free and player_free then
730 if cost <= cost_stack_max and number <= number_stack_max then
731 easyvend.machine_enable(pos, node)
732 if check_wear then
733 player_inv:set_stack("main", player_out[1].id, "")
734 achest_inv:add_item(achestdef.inv_list, player_out[1].item)
735 else
736 stack = player_inv:remove_item("main", stack)
737 achest_inv:add_item(achestdef.inv_list, stack)
739 rchest_inv:remove_item(rchestdef.inv_list, price)
740 player_inv:add_item("main", price)
741 meta:set_string("status", "Ready.")
742 if itemname == easyvend.currency and number == cost and cost <= cost_stack_max then
743 meta:set_string("message", easyvend.get_joke(buysell, meta:get_int("joke_id")))
744 meta:set_int("joketimer", joketimer_start)
745 else
746 meta:set_string("message", "Item sold.")
748 easyvend.sound_deposit(pos)
749 easyvend.machine_check(pos, node)
750 else
751 -- Large item counts (multiple stacks)
752 local coststacks = math.modf(cost / cost_stack_max)
753 local costremainder = math.fmod(cost, cost_stack_max)
754 local numberstacks = math.modf(number / number_stack_max)
755 local numberremainder = math.fmod(number, number_stack_max)
756 local numberfree = numberstacks
757 local costfree = coststacks
758 if numberremainder > 0 then numberfree = numberfree + 1 end
759 if costremainder > 0 then costfree = costfree + 1 end
760 if not player_free and easyvend.free_slots(player_inv, "main") < costfree then
761 if costfree > 1 then
762 msg = string.format("No room in your inventory (%d empty slots required)!", costfree)
763 else
764 msg = "No room in your inventory!"
766 meta:set_string("message", msg)
767 easyvend.sound_error(sendername)
768 elseif not chest_free and easyvend.free_slots(achest_inv, achestdef.inv_list) < numberfree then
769 meta:set_string("status", "No room in the machine’s storage!")
770 easyvend.machine_disable(pos, node, sendername)
771 else
772 easyvend.machine_enable(pos, node)
773 -- Remember removed items for transfer
774 local playerstacks = {}
775 for i=1, coststacks do
776 price.count = cost_stack_max
777 rchest_inv:remove_item(rchestdef.inv_list, price)
779 if costremainder > 0 then
780 price.count = costremainder
781 rchest_inv:remove_item(rchestdef.inv_list, price)
783 if check_wear then
784 for o=1,#player_out do
785 player_inv:set_stack("main", player_out[o].id, "")
787 else
788 for i=1, numberstacks do
789 stack.count = number_stack_max
790 table.insert(playerstacks, player_inv:remove_item("main", stack))
793 if numberremainder > 0 then
794 stack.count = numberremainder
795 table.insert(playerstacks, player_inv:remove_item("main", stack))
797 for i=1, coststacks do
798 price.count = cost_stack_max
799 player_inv:add_item("main", price)
801 if costremainder > 0 then
802 price.count = costremainder
803 player_inv:add_item("main", price)
805 if check_wear then
806 for o=1,#player_out do
807 achest_inv:add_item(achestdef.inv_list, player_out[o].item)
809 else
810 for i=1,#playerstacks do
811 achest_inv:add_item(achestdef.inv_list, playerstacks[i])
814 meta:set_string("message", "Item sold.")
815 easyvend.sound_deposit(pos)
816 easyvend.machine_check(pos, node)
819 elseif chest_has and player_has then
820 if not player_free then
821 msg = "No room in your inventory!"
822 meta:set_string("message", msg)
823 easyvend.sound_error(sendername)
824 elseif not chest_free then
825 msg = "No room in the machine’s storage!"
826 meta:set_string("status", msg)
827 easyvend.machine_disable(pos, node, sendername)
829 else
830 if not player_has then
831 msg = "You have insufficient materials!"
832 meta:set_string("message", msg)
833 easyvend.sound_error(sendername)
834 elseif not chest_has then
835 msg = "The depositing machine is out of money!"
836 meta:set_string("status", msg)
837 easyvend.machine_disable(pos, node, sendername)
841 else
842 local status
843 meta:set_int("stock", 0)
844 if chest_error_remove == "no_chest" and chest_error_add == "no_chest" then
845 status = "No storage; machine needs to be connected with a locked chest."
846 elseif chest_error_remove == "not_owned" or chest_error_add == "not_owned" then
847 status = "Storage can’t be accessed because it is owned by a different person!"
848 elseif chest_error_remove == "no_stock" then
849 if buysell == "sell" then
850 status = "The vending machine has insufficient materials!"
851 else
852 status = "The depositing machine is out of money!"
854 elseif chest_error_add == "no_space" then
855 status = "No room in the machine’s storage!"
856 else
857 status = "Unknown error!"
859 meta:set_string("status", status)
860 easyvend.sound_error(sendername)
863 easyvend.set_formspec(pos, sender)
867 easyvend.after_place_node = function(pos, placer)
868 local node = minetest.get_node(pos)
869 local meta = minetest.get_meta(pos)
870 local inv = meta:get_inventory()
871 local player_name = placer:get_player_name()
872 inv:set_size("item", 1)
873 inv:set_size("gold", 1)
875 inv:set_stack( "gold", 1, easyvend.currency )
877 local d = ""
878 if node.name == "easyvend:vendor" then
879 d = string.format("Inactive vending machine (owned by %s)", player_name)
880 meta:set_int("wear", 1)
881 -- Total number of currency items earned for the machine's life time (excluding currency-currency trading)
882 meta:set_int("earnings", 0)
883 elseif node.name == "easyvend:depositor" then
884 d = string.format("Inactive depositing machine (owned by %s)", player_name)
885 meta:set_int("wear", 0)
887 meta:set_string("infotext", d)
888 meta:set_string("status", "Awaiting configuration by owner.")
889 meta:set_string("message", "Welcome! Please prepare the machine.")
890 meta:set_int("number", 1)
891 meta:set_int("cost", 1)
892 meta:set_int("stock", -1)
893 meta:set_int("configmode", 1)
894 meta:set_int("joketimer", -1)
895 meta:set_int("joke_id", 1)
896 meta:set_string("itemname", "")
898 meta:set_string("owner", player_name or "")
900 easyvend.set_formspec(pos, placer)
903 easyvend.can_dig = function(pos, player)
904 local meta = minetest.get_meta(pos)
905 local name = player:get_player_name()
906 local owner = meta:get_string("owner")
907 -- Owner can always dig shop
908 if owner == name then
909 return true
911 local chest_pos = easyvend.find_connected_chest(owner, pos)
912 local chest, meta_chest
913 if chest_pos then
914 chest = minetest.get_node(chest_pos)
915 meta_chest = minetest.get_meta(chest_pos)
916 else
917 return true --if no chest, enyone can dig this shop
919 if registered_chests[chest.name] then
920 if player and player:is_player() then
921 local owner_chest = meta_chest:get_string(registered_chests[chest.name].meta_owner)
922 if name == owner_chest then
923 return true --chest owner can also dig shop
926 return false
927 else
928 return true --if no chest, enyone can dig this shop
932 easyvend.on_receive_fields = function(pos, formname, fields, sender)
933 local meta = minetest.get_meta(pos)
934 local node = minetest.get_node(pos)
935 local owner = meta:get_string("owner")
936 local sendername = sender:get_player_name()
938 if fields.doc then
939 if minetest.get_modpath("doc") and minetest.get_modpath("doc_items") then
940 if easyvend.buysell(node.name) == "buy" then
941 doc.show_entry(sendername, "nodes", "easyvend:depositor", true)
942 else
943 doc.show_entry(sendername, "nodes", "easyvend:vendor", true)
946 elseif fields.config or fields.save or fields.usermode then
947 if sender:get_player_name() == owner then
948 easyvend.on_receive_fields_config(pos, formname, fields, sender)
949 else
950 meta:set_string("message", "Only the owner may change the configuration.")
951 easyvend.sound_error(sendername)
952 easyvend.set_formspec(pos, sender)
953 return
955 elseif fields.wear ~= nil then
956 if sender:get_player_name() == owner then
957 if fields.wear == "true" then
958 if easyvend.buysell(node.name) == "buy" then
959 meta:set_string("message", "Used tools are now accepted.")
960 else
961 meta:set_string("message", "Used tools are now for sale.")
963 meta:set_int("wear", 1)
964 elseif fields.wear == "false" then
965 if easyvend.buysell(node.name) == "buy" then
966 meta:set_string("message", "Used tools are now rejected.")
967 else
968 meta:set_string("message", "Used tools won’t be sold anymore.")
970 meta:set_int("wear", 0)
972 easyvend.set_formspec(pos, sender)
973 return
974 else
975 meta:set_string("message", "Only the owner may change the configuration.")
976 easyvend.sound_error(sendername)
977 easyvend.set_formspec(pos, sender)
978 return
980 elseif fields.buysell then
981 easyvend.on_receive_fields_buysell(pos, formname, fields, sender)
985 -- Jokes: Appear when machine exchanges currency for currency at equal rate
987 -- Vendor
988 local jokes_vendor = {
989 "Thank you. You have made a vending machine very happy.",
990 "Humans have a strange sense of humor.",
991 "Let’s get this over with …",
992 "Item “bought”.",
993 "Tit for tat.",
994 "Do you realize what you’ve just bought?",
996 -- Depositor
997 local jokes_depositor = {
998 "Thank you, the money started to smell inside.",
999 "Money doesn’t grow on trees, you know?",
1000 "Sanity sold.",
1001 "Well, that was an awkward exchange.",
1002 "Are you having fun?",
1003 "Is this really trading?",
1006 easyvend.assign_joke = function(buysell)
1007 local jokes
1008 if buysell == "sell" then
1009 jokes = jokes_vendor
1010 elseif buysell == "buy" then
1011 jokes = jokes_depositor
1013 local r = math.random(1,#jokes)
1014 return r
1017 easyvend.get_joke = function(buysell, id)
1018 local joke
1019 if buysell == nil or id == nil then
1020 -- Fallback message (should never happen)
1021 return "Items exchanged."
1023 if buysell == "sell" then
1024 joke = jokes_vendor[id]
1025 if joke == nil then joke = jokes_vendor[1] end
1026 elseif buysell == "buy" then
1027 joke = jokes_depositor[id]
1028 if joke == nil then joke = jokes_depositor[1] end
1030 return joke
1033 easyvend.sound_error = function(playername)
1034 minetest.sound_play("easyvend_error", {to_player = playername, gain = 0.25})
1037 easyvend.sound_setup = function(pos)
1038 minetest.sound_play("easyvend_activate", {pos = pos, gain = 0.5, max_hear_distance = 12,})
1041 easyvend.sound_disable = function(pos)
1042 minetest.sound_play("easyvend_disable", {pos = pos, gain = 0.9, max_hear_distance = 12,})
1045 easyvend.sound_vend = function(pos)
1046 minetest.sound_play("easyvend_vend", {pos = pos, gain = 0.4, max_hear_distance = 5,})
1049 easyvend.sound_deposit = function(pos)
1050 minetest.sound_play("easyvend_deposit", {pos = pos, gain = 0.4, max_hear_distance = 5,})
1053 --[[ Tower building ]]
1055 easyvend.is_traversable = function(pos)
1056 local node = minetest.get_node_or_nil(pos)
1057 if (node == nil) then
1058 return false
1060 return traversable_node_types[node.name] == true
1063 easyvend.neighboring_nodes = function(pos)
1064 local check = {
1065 {x=pos.x, y=pos.y-1, z=pos.z},
1066 {x=pos.x, y=pos.y+1, z=pos.z},
1068 local trav = {}
1069 for i=1,#check do
1070 if easyvend.is_traversable(check[i]) then
1071 table.insert(trav, check[i])
1074 return trav
1077 easyvend.find_connected_chest = function(owner, pos, nodename, check_wear, amount, removing)
1078 local nodes = easyvend.neighboring_nodes(pos)
1080 if (#nodes < 1 or #nodes > 2) then
1081 return nil, "no_chest"
1084 -- Find the stack direction
1085 local first = nil
1086 local second = nil
1087 for i=1,#nodes do
1088 if ( first == nil ) then
1089 first = nodes[i]
1090 else
1091 second = nodes[i]
1095 local chest_pos, chest_internal
1097 if (first ~= nil and second ~= nil) then
1098 local dy = (first.y - second.y)/2
1099 chest_pos, chest_internal = easyvend.find_chest(owner, pos, dy, nodename, check_wear, amount, removing)
1100 if ( chest_pos == nil ) then
1101 chest_pos, chest_internal = easyvend.find_chest(owner, pos, -dy, nodename, check_wear, amount, removing, chest_internal)
1103 else
1104 local dy = first.y - pos.y
1105 chest_pos, chest_internal = easyvend.find_chest(owner, pos, dy, nodename, check_wear, amount, removing)
1108 if chest_internal.chests == 0 then
1109 return nil, "no_chest"
1110 elseif chest_internal.chests == chest_internal.other_chests then
1111 return nil, "not_owned"
1112 elseif removing and chest_internal.stock < 1 then
1113 return nil, "no_stock"
1114 elseif not removing and chest_internal.space < 1 then
1115 return nil, "no_space"
1116 elseif chest_pos ~= nil then
1117 return chest_pos
1118 else
1119 return nil, "unknown"
1123 easyvend.find_chest = function(owner, pos, dy, itemname, check_wear, amount, removing, internal)
1124 pos = {x=pos.x, y=pos.y + dy, z=pos.z}
1126 if internal == nil then
1127 internal = {}
1128 internal.chests = 0
1129 internal.other_chests = 0
1130 internal.stock = 0
1131 internal.space = 0
1134 local node = minetest.get_node_or_nil(pos)
1135 if ( node == nil ) then
1136 return nil, internal
1138 local chestdef = registered_chests[node.name]
1139 if (chestdef ~= nil) then
1140 internal.chests = internal.chests + 1
1141 local meta = minetest.get_meta(pos)
1142 if (owner ~= meta:get_string(chestdef.meta_owner)) then
1143 internal.other_chests = internal.other_chests + 1
1144 return nil, internal
1146 local inv = meta:get_inventory()
1147 if (inv ~= nil) then
1148 if (itemname ~= nil and amount ~= nil and removing ~= nil and check_wear ~= nil) then
1149 local chest_has, chest_free
1150 local stack = {name=itemname, count=amount, wear=0, metadata=""}
1151 local stack_max = minetest.registered_items[itemname].stack_max
1153 local stacks = math.modf(amount / stack_max)
1154 local stacksremainder = math.fmod(amount, stack_max)
1155 local free = stacks
1156 if stacksremainder > 0 then free = free + 1 end
1158 chest_has = easyvend.check_and_get_items(inv, chestdef.inv_list, stack, check_wear)
1159 if chest_has then
1160 internal.stock = internal.stock + 1
1162 chest_free = inv:room_for_item(chestdef.inv_list, stack) and easyvend.free_slots(inv, chestdef.inv_list) >= free
1163 if chest_free then
1164 internal.space = internal.space + 1
1167 if (removing and internal.stock == 0) or (not removing and internal.space == 0) then
1168 return easyvend.find_chest(owner, pos, dy, itemname, check_wear, amount, removing, internal)
1169 else
1170 return pos, internal
1174 elseif (node.name ~= "easyvend:vendor" and node.name~="easyvend:depositor" and node.name~="easyvend:vendor_on" and node.name~="easyvend:depositor_on") then
1175 return nil, internal
1178 return easyvend.find_chest(owner, pos, dy, itemname, check_wear, amount, removing, internal)
1181 -- Pseudo-inventory handling
1182 easyvend.allow_metadata_inventory_put = function(pos, listname, index, stack, player)
1183 if listname=="item" then
1184 local meta = minetest.get_meta(pos);
1185 local owner = meta:get_string("owner")
1186 local name = player:get_player_name()
1187 if name == owner then
1188 local inv = meta:get_inventory()
1189 if stack==nil then
1190 inv:set_stack( "item", 1, nil )
1191 else
1192 inv:set_stack( "item", 1, stack:get_name() )
1193 meta:set_string("itemname", stack:get_name())
1194 easyvend.set_formspec(pos, player)
1198 return 0
1201 easyvend.allow_metadata_inventory_take = function(pos, listname, index, stack, player)
1202 return 0
1205 easyvend.allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
1206 return 0
1209 minetest.register_abm({
1210 nodenames = {"easyvend:vendor", "easyvend:vendor_on", "easyvend:depositor", "easyvend:depositor_on"},
1211 interval = 5,
1212 chance = 1,
1213 catch_up = false,
1214 action = function(pos, node, active_object_count, active_object_count_wider)
1215 easyvend.machine_check(pos, node)
1219 -- Legacy support for vendor mod:
1220 -- Transform the world and items to use the easyvend nodes/items
1222 -- For safety reasons, only do this when player requested so
1223 if minetest.setting_getbool("easyvend_convert_vendor") == true then
1224 -- Replace vendor nodes
1225 minetest.register_lbm({
1226 name = "easyvend:replace_vendor",
1227 nodenames = { "vendor:vendor", "vendor:depositor" },
1228 run_at_every_load = true,
1229 action = function(pos, node)
1230 -- Replace node
1231 local newnodename
1232 if node.name == "vendor:vendor" then
1233 newnodename = "easyvend:vendor"
1234 elseif node.name == "vendor:depositor" then
1235 newnodename = "easyvend:depositor"
1237 -- Remove axis rotation; only allow 4 facedirs
1238 local p2 = math.fmod(node.param2, 4)
1239 minetest.swap_node(pos, { name = newnodename, param2 = p2 })
1241 -- Initialize metadata
1242 local meta = minetest.get_meta(pos)
1243 if node.name == "vendor:vendor" then
1244 meta:set_int("earnings", 0)
1246 meta:set_int("stock", -1)
1247 meta:set_int("joketimer", -1)
1248 meta:set_int("joke_id", 1)
1249 local inv = meta:get_inventory()
1250 inv:set_size("item", 1)
1251 inv:set_size("gold", 1)
1252 inv:set_stack("gold", 1, easyvend.currency)
1254 -- In vendor, all machines accepted worn tools
1255 meta:set_int("wear", 1)
1257 -- Set item
1258 local itemname = meta:get_string("itemname")
1259 if itemname == "" or itemname == nil then
1260 itemname = meta:get_string("itemtype")
1262 if itemname ~= "" and itemname ~= nil then
1263 inv:set_stack("item", 1, itemname)
1264 meta:set_string("itemname", itemname)
1267 -- Check for valid item, item count and price
1268 local configmode = 1
1269 if itemname ~= "" and itemname ~= nil then
1270 local itemstack = inv:get_stack("item", 1)
1271 local number_stack_max = itemstack:get_stack_max()
1272 local maxnumber = number_stack_max * slots_max
1273 local cost = meta:get_int("cost")
1274 local number = meta:get_int("number")
1275 if number >= 1 and number <= maxnumber and cost >= 1 and cost <= maxcost then
1276 -- Everything's OK, get out of config mode!
1277 configmode = 0
1281 -- Final initialization stuff
1282 meta:set_int("configmode", configmode)
1284 local owner = meta:get_string("owner")
1285 if easyvend.buysell(newnodename) == "sell" then
1286 meta:set_string("infotext", string.format("Vending machine (owned by %s)", owner))
1287 else
1288 meta:set_string("infotext", string.format("Depositing machine (owned by %s)", owner))
1292 meta:set_string("status", "Initializing …")
1293 meta:set_string("message", "Upgrade successful.")
1294 easyvend.machine_check(pos, node)
1295 end,