From 880f9e8b965664314d0c5e42ae28be84a4628d8c Mon Sep 17 00:00:00 2001 From: Freeman Date: Fri, 18 Aug 2023 05:25:17 +0200 Subject: [PATCH] fancyshops --- fancyshop.lua | 1734 ++++++++++++++++++++++++++++++++ init.lua | 1 + mod.conf | 4 +- settingtypes.txt | 17 + textures/admin_depo.png | Bin 0 -> 303 bytes textures/admin_depo_front.png | Bin 0 -> 314 bytes textures/admin_vend.png | Bin 0 -> 257 bytes textures/admin_vend_front.png | Bin 0 -> 273 bytes textures/copier.png | Bin 0 -> 227 bytes textures/inactive.png | Bin 0 -> 890 bytes textures/player_depo.png | Bin 0 -> 565 bytes textures/player_depo_front.png | Bin 0 -> 553 bytes textures/player_vend.png | Bin 0 -> 552 bytes textures/player_vend_front.png | Bin 0 -> 526 bytes textures/upgrade_front.png | Bin 0 -> 556 bytes 15 files changed, 1754 insertions(+), 2 deletions(-) create mode 100644 fancyshop.lua create mode 100644 textures/admin_depo.png create mode 100644 textures/admin_depo_front.png create mode 100644 textures/admin_vend.png create mode 100644 textures/admin_vend_front.png create mode 100644 textures/copier.png create mode 100644 textures/inactive.png create mode 100644 textures/player_depo.png create mode 100644 textures/player_depo_front.png create mode 100644 textures/player_vend.png create mode 100644 textures/player_vend_front.png create mode 100644 textures/upgrade_front.png diff --git a/fancyshop.lua b/fancyshop.lua new file mode 100644 index 0000000..5a85de9 --- /dev/null +++ b/fancyshop.lua @@ -0,0 +1,1734 @@ + + +-- Copyright (C) 2021, 2023 Ale + +-- This file is part of Emeraldbank Minetest Mod. + +-- Emeraldbank is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. + +-- Emeraldbank is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. + +-- You should have received a copy of the GNU Affero General Public License +-- along with Emeraldbank. If not, see . + + +-- This code was forked from fancy_vend mod, adapted to MineClone2. + + +local S = core.get_translator(core.get_current_modname()) + +local settings = minetest.settings + +local display_node = (settings:get("fancy_vend.display_node") or "mcl_core:glass") +local max_logs = (tonumber(settings:get("fancy_vend.log_max")) or 40) +local autorotate_speed = (tonumber(settings:get("fancy_vend.autorotate_speed")) or 1) +local no_alerts = settings:get_bool("fancy_vend.no_alerts") + +local drop_vendor = "fancy_vend:player_vendor" + +-- Register a copy of the display node with no drops to make players separating the obsidian glass with something like a piston a non-issue. +local display_node_def = table.copy(minetest.registered_nodes[display_node]) +display_node_def.drop = "" +display_node_def.pointable = false +display_node_def.groups.not_in_creative_inventory = 1 +display_node_def.description = S("Fancy Vendor Display Node (you hacker you!)") +if pipeworks then + display_node_def.digiline = { + wire = { + rules = pipeworks.digilines_rules + } + } +end +minetest.register_node(":fancy_vend:display_node", display_node_def) + +-- Craftitem to display when vendor is inactive (Use just image for this???) +minetest.register_craftitem(":fancy_vend:inactive",{inventory_image = "inactive.png",}) + +minetest.register_privilege("admin_vendor", S("Enables the user to set regular vendors to admin vendors.")) +minetest.register_privilege("disable_vendor", S("Enables the user to set all vendors to inactive.")) + +local function bts(bool) + if bool == false then + return "false" + elseif bool == true then + return "true" + else + return bool + end +end + +local function stb(str) + if str == "false" then + return false + elseif str == "true" then + return true + else + return str + end +end + +local modstorage = minetest.get_mod_storage() + +if modstorage:get_string("all_inactive_force") == "" then + modstorage:set_string("all_inactive_force", "false") +end + +local all_inactive_force = stb(modstorage:get_string("all_inactive_force")) + +minetest.register_chatcommand("disable_all_vendors", { + description = S("Toggle vendor inactivity."), + privs = {disable_vendor=true}, + func = function(name, param) + if all_inactive_force then + all_inactive_force = false + modstorage:set_string("all_inactive_force", "false") + else + all_inactive_force = true + modstorage:set_string("all_inactive_force", "true") + end + end, +}) + +table.length = function(table) + local length + for i in pairs(table) do + length = length + 1 + end + return length +end + +local function send_message(pos, channel, msg) + if channel and channel ~= "" then + digilines.receptor_send(pos, digilines.rules.default, channel, msg) + end +end + +-- Awards +if minetest.get_modpath("awards") then + awards.register_achievement("fancy_vend:getting_fancy",{ + title = S("Getting Fancy"), + description = S("Craft a fancy vendor."), + trigger = { + type = "craft", + item = drop_vendor, + target = 1, + }, + icon = "player_vend_front.png^awards_level1.png", + }) + + awards.register_achievement("fancy_vend:wizard",{ + title = "You're a Wizard", + description = S("Craft a copy tool."), + trigger = { + type = "craft", + item = "fancy_vend:copy_tool", + target = 1, + }, + icon = "copier.png", + }) + + awards.register_achievement("fancy_vend:trader",{ + title = "Trader", + description = "Configure a depositor.", + icon = "player_depo_front.png", + }) + awards.register_achievement("fancy_vend:seller",{ + title = "Seller", + description = "Configure a vendor.", + icon = "player_vend_front.png^awards_level2.png", + }) + awards.register_achievement("fancy_vend:shop_keeper",{ + title = "Shop Keeper", + description = "Configure 10 vendors or depositors.", + icon = "player_vend_front.png^awards_level3.png", + }) + awards.register_achievement("fancy_vend:merchant",{ + title = "Merchant", + description = "Configure 25 vendors or depositors.", + icon = "player_vend_front.png^awards_level4.png", + }) + awards.register_achievement("fancy_vend:super_merchant",{ + title = "Super Merchant", + description = "Configure 100 vendors or depositors. How do you even have this much stuff to sell?", + icon = "player_vend_front.png^awards_level5.png", + }) + awards.register_achievement("fancy_vend:god_merchant",{ + title = "God Merchant", + description = "Configure 9001 vendors or depositors. Ok wot.", + icon = "player_vend_front.png^awards_level6.png", + secret = true, -- Oi. Cheater. + }) +end + +local tmp = {} + +minetest.register_entity(":fancy_vend:display_item",{ + hp_max = 1, + visual = "wielditem", + visual_size = {x = 0.33, y = 0.33}, + collisionbox = {0, 0, 0, 0, 0, 0}, + physical = false, + textures = {"air"}, + on_activate = function(self, staticdata) + if tmp.nodename ~= nil and tmp.texture ~= nil then + self.nodename = tmp.nodename + tmp.nodename = nil + self.texture = tmp.texture + tmp.texture = nil + else + if staticdata ~= nil and staticdata ~= "" then + local data = staticdata:split(';') + if data and data[1] and data[2] then + self.nodename = data[1] + self.texture = data[2] + end + end + end + if self.texture ~= nil then + self.object:set_properties({textures = {self.texture}}) + end + self.object:set_properties({automatic_rotate = autorotate_speed}) + end, + get_staticdata = function(self) + if self.nodename ~= nil and self.texture ~= nil then + return self.nodename .. ';' .. self.texture + end + return "" + end, +}) + +local function remove_item(pos) + local objs = nil + objs = minetest.get_objects_inside_radius(pos, .5) + if objs then + for _, obj in ipairs(objs) do + if obj and obj:get_luaentity() and obj:get_luaentity().name == "fancy_vend:display_item" then + obj:remove() + end + end + end +end + +local function update_item(pos, node) + pos.y = pos.y + 1 + remove_item(pos) + if minetest.get_node(pos).name ~= "fancy_vend:display_node" then + minetest.log("warning", "[Emeraldbank]: Placing display item inside "..minetest.get_node(pos).name.." at "..minetest.pos_to_string(pos).." is not permitted, aborting") + return + end + pos.y = pos.y - 1 + local meta = minetest.get_meta(pos) + if meta:get_string("item") ~= "" then + pos.y = pos.y + (12 / 16 + 0.11) + tmp.nodename = node.name + tmp.texture = ItemStack(meta:get_string("item")):get_name() + local e = minetest.add_entity(pos,"fancy_vend:display_item") + pos.y = pos.y - (12 / 16 + 0.11) + end +end + +-- LBM to refresh entities after clearobjects +minetest.register_lbm({ + label = "Refresh vendor display", + name = ":fancy_vend:display_refresh", + nodenames = {"fancy_vend:display_node"}, + run_at_every_load = true, + action = function(pos, node) + if not next(minetest.get_objects_inside_radius(pos, 0.5)) then + pos.y = pos.y - 1 + update_item(pos, node) + end + end +}) + +local function set_vendor_settings(pos, SettingsDef) + local meta = minetest.get_meta(pos) + meta:set_string("settings", minetest.serialize(SettingsDef)) +end + +local function reset_vendor_settings(pos) + local settings_default = { + input_item = "", -- Don't change this unless you plan on setting this up to add this item to the inventories + output_item = "", -- Don't change this unless you plan on setting this up to add this item to the inventories + input_item_qty = 1, + output_item_qty = 1, + admin_vendor = false, + depositor = false, + currency_eject = false, + accept_output_only = false, + split_incoming_stacks = false, + inactive_force = false, + accept_worn_input = true, + accept_worn_output = true, + digiline_channel = "", + co_sellers = "", + banned_buyers = "", + auto_sort = false, + } + set_vendor_settings(pos, settings_default) + return settings_default +end + +local function get_vendor_settings(pos) + local meta = minetest.get_meta(pos) + local settings = minetest.deserialize(meta:get_string("settings")) + if not settings then + return reset_vendor_settings(pos) + else + -- If settings added by newer versions of fancy_vend are nil then send defaults + if settings.auto_sort == nil then + settings.auto_sort = false + end + + -- Sanitatize number values (backwards compat) + settings.input_item_qty = (type(settings.input_item_qty) == "number" and math.abs(settings.input_item_qty) or 1) + settings.output_item_qty = (type(settings.output_item_qty) == "number" and math.abs(settings.output_item_qty) or 1) + return settings + end +end + +local function can_buy_from_vendor(pos, player) + local settings = get_vendor_settings(pos) + local banned_buyers = string.split((settings.banned_buyers or ""),",") + for i in pairs(banned_buyers) do + if banned_buyers[i] == player:get_player_name() then + return false + end + end + return true +end + +local function can_modify_vendor(pos, player) + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + local is_owner = false + if meta:get_string("owner") == player:get_player_name() or minetest.check_player_privs(player, {protection_bypass=true}) then + is_owner = true + end + return is_owner +end + +local function can_dig_vendor(pos, player) + local meta = minetest.get_meta(pos); + local inv = meta:get_inventory() + return inv:is_empty("main") and can_modify_vendor(pos, player) +end + +local function can_access_vendor_inv(player, pos) + local meta = minetest.get_meta(pos) + if minetest.check_player_privs(player, {protection_bypass=true}) or meta:get_string("owner") == player:get_player_name() then return true end + local settings = get_vendor_settings(pos) + local co_sellers = string.split(settings.co_sellers,",") + for i in pairs(co_sellers) do + if co_sellers[i] == player:get_player_name() then + return true + end + end + return false +end + +-- Inventory helpers: + +-- Function to sort inventory (Taken from technic_chests) +local function sort_inventory(inv) + local inlist = inv:get_list("main") + local typecnt = {} + local typekeys = {} + for _, st in ipairs(inlist) do + if not st:is_empty() then + local n = st:get_name() + local w = st:get_wear() + local m = st:get_metadata() + local k = string.format("%s %05d %s", n, w, m) + if not typecnt[k] then + typecnt[k] = { + name = n, + wear = w, + metadata = m, + stack_max = st:get_stack_max(), + count = 0, + } + table.insert(typekeys, k) + end + typecnt[k].count = typecnt[k].count + st:get_count() + end + end + table.sort(typekeys) + local outlist = {} + for _, k in ipairs(typekeys) do + local tc = typecnt[k] + while tc.count > 0 do + local c = math.min(tc.count, tc.stack_max) + table.insert(outlist, ItemStack({ + name = tc.name, + wear = tc.wear, + metadata = tc.metadata, + count = c, + })) + tc.count = tc.count - c + end + end + if #outlist > #inlist then return end + while #outlist < #inlist do + table.insert(outlist, ItemStack(nil)) + end + inv:set_list("main", outlist) +end + +local function free_slots(inv, listname, itemname, quantity) + local size = inv:get_size(listname) + local free = 0 + for i=1,size do + local stack = inv:get_stack(listname, i) + if stack:is_empty() or stack:get_free_space() > 0 then + if stack:is_empty() then + free = free + ItemStack(itemname):get_stack_max() + elseif stack:get_name() == itemname then + free = free + stack:get_free_space() + end + end + end + if free < quantity then + return false + else + return true + end +end + +local function inv_insert(inv, listname, itemstack, quantity, from_table, pos, input_eject) + local stackmax = itemstack:get_stack_max() + local name = itemstack:get_name() + local stacks = {} + local remaining_quantity = quantity + + -- Add the full stacks to the list + while remaining_quantity > stackmax do + table.insert(stacks, {name = name, count = stackmax}) + remaining_quantity = remaining_quantity - stackmax + end + -- Add the remaining stack to the list + table.insert(stacks, {name = name, count = remaining_quantity}) + + -- If tool add wears ignores if from_table = nil (eg, due to vendor beig admin vendor) + if minetest.registered_tools[name] and from_table then + for i in pairs(stacks) do + local from_item_table = from_table[i].item:to_table() + stacks[i].wear = from_item_table.wear + end + end + + -- if has metadata add metadata + if from_table then + for i in pairs(stacks) do + local from_item_table = from_table[i].item:to_table() + if from_item_table.name == name then + if from_item_table.metadata then + stacks[i].metadata = from_item_table.metadata -- Apparently some mods *cough* digtron *cough* do use deprecated metadata strings + end + if from_item_table.meta then + stacks[i].meta = from_item_table.meta -- Most mods use metadata tables which is the correct method but ok + end + end + end + end + + -- Add to inventory or eject to pipeworks/hoppers (whichever is applicable) + local output_tube_connected = false + local output_hopper_connected = false + if input_eject and pos then + local pos_under = vector.new(pos) + pos_under.y = pos_under.y - 1 + local node_under = minetest.get_node(pos_under) + if minetest.get_item_group(node_under.name, "tubedevice") > 0 then + output_tube_connected = true + end + if node_under.name == "hopper:hopper" or node_under.name == "hopper:hopper_side" then + output_hopper_connected = true + end + end + for i in pairs(stacks) do + if output_tube_connected then + pipeworks.tube_inject_item(pos, pos, vector.new(0, -1, 0), stacks[i], minetest.get_meta(pos):get_string("owner")) + else + local leftovers = ItemStack(stacks[i]) + if output_hopper_connected then + local pos_under = {x = pos.x, y = pos.y-1, z = pos.z} + local hopper_inv = minetest.get_meta(pos_under):get_inventory() + leftovers = hopper_inv:add_item("main", leftovers) + end + if not leftovers:is_empty() then + inv:add_item(listname, leftovers) + end + end + end +end + +local function inv_remove(inv, listname, remove_table, itemstring, quantity) + local count = 0 + for i in pairs(remove_table) do + count = count + remove_table[i].item:get_count() + inv:set_stack(listname, remove_table[i].id, nil) + end + -- Add back items if too many were taken + if count > quantity then + inv:add_item(listname, ItemStack({name = itemstring, count = count - quantity})) + end +end + +local function inv_contains_items(inv, listname, itemstring, quantity, ignore_wear) + local minimum = quantity + local get_items = {} + local count = 0 + + for i=1,inv:get_size(listname) do + local stack = inv:get_stack(listname, i) + if stack:get_name() == itemstring then + if ignore_wear or (not minetest.registered_tools[itemstring] or stack:get_wear() == 0) then + count = count + stack:get_count() + table.insert(get_items, {id=i, item=stack}) + if count >= minimum then + return true, get_items + end + end + end + end + return false +end + +local function get_vendor_status(pos) + local settings = get_vendor_settings(pos) + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + if all_inactive_force then + return false, "all_inactive_force" + elseif settings.input_item == "" or settings.output_item == "" then + return false, "unconfigured" + elseif settings.inactive_force then + return false, "inactive_force" + elseif not minetest.check_player_privs(meta:get_string("owner"), {admin_vendor=true}) and settings.admin_vendor == true then + return false, "no_privs" + elseif not inv_contains_items(inv, "main", settings.output_item, settings.output_item_qty, settings.accept_worn_output) and not settings.admin_vendor then + return false, "no_output" + elseif not free_slots(inv, "main", settings.input_item, settings.input_item_qty) and not settings.admin_vendor then + return false, "no_room" + else + return true + end +end + +local function make_inactive_string(errorcode) + local status_str = "" + if errorcode == "unconfigured" then + status_str = status_str.." (unconfigured)" + elseif errorcode == "inactive_force" then + status_str = status_str.." (forced)" + elseif errorcode == "no_output" then + status_str = status_str.." (out of stock)" + elseif errorcode == "no_room" then + status_str = status_str.." (no room)" + elseif errorcode == "no_privs" then + status_str = status_str.." (seller has insufficient privilages)" + elseif errorcode == "all_inactive_force" then + status_str = status_str.." (all vendors disabled temporarily by admin)" + end + return status_str +end + +-- Various email and tell mod support + +-- This function takes the position of a vendor and alerts the owner if it has just been emptied +local email_loaded, tell_loaded, mail_loaded = minetest.get_modpath("email"), minetest.get_modpath("tell"), minetest.get_modpath("mail") +local function alert_owner_if_empty(pos) + if no_alerts then return end + + local meta = minetest.get_meta(pos) + local settings = get_vendor_settings(pos) + local owner = meta:get_string("owner") + local alerted = stb(meta:get_string("alerted") or "false") -- check + local status, errorcode = get_vendor_status(pos) + + -- Message to send + local stock_msg = "Your vendor trading "..settings.input_item_qty.." "..minetest.registered_items[settings.input_item].description.." for "..settings.output_item_qty.." "..minetest.registered_items[settings.output_item].description.." at position "..minetest.pos_to_string(pos, 0).." has just run out of stock." + + if not alerted and not status and errorcode == "no_output" then + -- Rubenwardy's Email Mod: https://github.com/rubenwardy/email + if mail_loaded then + local inbox = {} + + -- load messages + if not mail.apiversion then + -- cheapie's mail mod https://cheapiesystems.com/git/mail/ + if not mail.messages[owner] then mail.messages[owner] = {} end + inbox = mail.messages[owner] + + elseif mail.apiversion >= 1.1 then + -- webmail fork https://github.com/thomasrudin-mt/mail (per player storage) + inbox = mail.getMessages(owner) + + end + + -- Instead of filling their inbox with mail, get the last message sent by "Fancy Vend" and append to the message + -- If there is no last message, then create a new one + + local message + for i, msg in pairs(inbox) do + if msg.sender == "Fancy Vend" then -- Put a space in the name to avoid impersonation + message = msg + end + end + + if message then + -- Set the message as unread + message.unread = true + + -- Append to the end + message.body = message.body..stock_msg.."\n" + else + mail.send("Fancy Vend", owner, "You have unstocked vendors!", stock_msg.."\n") + end + + -- save messages + if not mail.apiversion then + -- cheapie's mail mod https://cheapiesystems.com/git/mail/ + mail.save() + + elseif mail.apiversion >= 1.1 then + -- webmail fork https://github.com/thomasrudin-mt/mail + mail.setMessages(owner, inbox) + + end + + meta:set_string("alerted", "true") + + return + + elseif email_loaded then + email.send_mail("Fancy Vend", owner, stock_msg) + + meta:set_string("alerted", "true") + + return + + elseif tell_loaded then + -- octacians tell mod https://github.com/octacian/tell + tell.add(owner, "Fancy Vend", stock_msg) + + meta:set_string("alerted", "true") + + return + end + end +end + +local function run_inv_checks(pos, player, lots) + local settings = get_vendor_settings(pos) + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + local player_inv = player:get_inventory() + + local ct = {} + + -- Get input and output quantities after multiplying by lot count + local output_qty = settings.output_item_qty * lots + local input_qty = settings.input_item_qty * lots + + -- Perform inventory checks + ct.player_has, ct.player_item_table = inv_contains_items(player_inv, "main", settings.input_item, input_qty, settings.accept_worn_input) + ct.vendor_has, ct.vendor_item_table = inv_contains_items(inv, "main", settings.output_item, output_qty, settings.accept_worn_output) + ct.player_fits = free_slots(player_inv, "main", settings.output_item, output_qty) + ct.vendor_fits = free_slots(inv, "main", settings.input_item, input_qty) + + if ct.player_has and ct.vendor_has and ct.player_fits and ct.vendor_fits then + ct.overall = true + else + ct.overall = false + end + + return ct +end + +local function get_max_lots(pos, player) + local max = 0 + + while run_inv_checks(pos, player, max).overall do + max = max + 1 + end + + return max +end + +local function make_purchase(pos, player, lots) + if not can_buy_from_vendor(pos, player) then + return false, "You cannot purchase from this vendor" + end + + local settings = get_vendor_settings(pos) + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + local player_inv = player:get_inventory() + local status, errorcode = get_vendor_status(pos) + + -- Double check settings, vendors which were incorrectly set up before this bug fix won't matter anymore + settings.input_item_qty = math.abs(settings.input_item_qty) + settings.output_item_qty = math.abs(settings.output_item_qty) + + if status then + -- Get input and output quantities after multiplying by lot count + local output_qty = settings.output_item_qty * lots + local input_qty = settings.input_item_qty * lots + + -- Perform inventory checks + local ct = run_inv_checks(pos, player, lots) + + if ct.player_has then + if ct.player_fits then + if settings.admin_vendor then + minetest.log("action", player:get_player_name().." trades "..settings.input_item_qty.." "..settings.input_item.." for "..settings.output_item_qty.." "..settings.output_item.." using vendor at "..minetest.pos_to_string(pos)) + + inv_remove(player_inv, "main", ct.player_item_table, settings.input_item, input_qty) + inv_insert(player_inv, "main", ItemStack(settings.output_item), output_qty, nil) + + if minetest.get_modpath("digilines") then + send_message(pos, settings.digiline_channel, msg) + end + + return true, "Trade successful" + elseif ct.vendor_has then + if ct.vendor_fits then + minetest.log("action", player:get_player_name().." trades "..settings.input_item_qty.." "..settings.input_item.." for "..settings.output_item_qty.." "..settings.output_item.." using vendor at "..minetest.pos_to_string(pos)) + + inv_remove(inv, "main", ct.vendor_item_table, settings.output_item, output_qty) + inv_remove(player_inv, "main", ct.player_item_table, settings.input_item, input_qty) + inv_insert(player_inv, "main", ItemStack(settings.output_item), output_qty, ct.vendor_item_table) + inv_insert(inv, "main", ItemStack(settings.input_item), input_qty, ct.player_item_table, pos, (minetest.get_modpath("pipeworks") and settings.currency_eject)) + + -- Run mail mod checks + alert_owner_if_empty(pos) + + return true, "Trade successful" + else + return false, "Vendor has insufficient space" + end + else + return false, "Vendor has insufficient resources" + end + else + return false, "You have insufficient space" + end + else + return false, "You have insufficient funds" + end + else + return false, "Vendor is inactive"..make_inactive_string(errorcode) + end +end + + +local function get_vendor_buyer_fs(pos, player, lots) + local base = "size[9,9]".. + "label[0,0;"..S("Owner wants:").."]".. + "label[0,1.25;"..S("for:").."]".. + "button[0,2.7;2,1;buy;"..S("Buy").."]".. + "label[2.8,2.9;"..S("lots.").."]".. + "button[0,3.6;2,1;lot_fill;"..S("Fill lots to max.").."]".. + + "list[current_player;main;0,5.05;9,3;9]".. + mcl_formspec.get_itemslot_bg(0,5.05,9,3).. + "list[current_player;main;0,8.19;9,1;]".. + mcl_formspec.get_itemslot_bg(0,8.19,9,1).. + "listring[current_player;main]".. + + "field_close_on_enter[lot_count;false]" + + -- Add dynamic elements + local settings = get_vendor_settings(pos) + local meta = minetest.get_meta(pos) + local status, errorcode = get_vendor_status(pos) + + local itemstuff = + "item_image_button[0,0.4;1,1;"..settings.input_item..";ignore;]".. + "label[0.9,0.6;"..settings.input_item_qty.." "..minetest.registered_items[settings.input_item].description.."]".. + "item_image_button[0,1.7;1,1;"..settings.output_item..";ignore;]".. + "label[0.9,1.9;"..settings.output_item_qty.." "..minetest.registered_items[settings.output_item].description.."]" + + local status_str + if status then + status_str = "active" + else + status_str = "inactive"..make_inactive_string(errorcode) + end + local status_fs = + "label[4,0.4;Vendor status: "..status_str.."]".. + "label[4,0.8;Message: "..meta:get_string("message").."]".. + "label[4,0;Vendor owned by: "..meta:get_string("owner").."]" + + local setting_specific = "" + if not settings.accept_worn_input then + setting_specific = setting_specific.."label[4,1.6;"..S("Vendor will not accept worn tools.").."]" + end + if not settings.accept_worn_output then + setting_specific = setting_specific.."label[4,1.2;"..S("Vendor will not sell worn tools.").."]" + end + + local fields = "field[2.2,3.2;1,0.6;lot_count;;"..(lots or 1).."]" + + local fs = base..itemstuff..status_fs..setting_specific..fields + return fs +end + +local function get_vendor_settings_fs(pos) + local base = "size[9,10]".. + "label[2.8,0.5;"..S("Input item").."]".. + "label[6.8,0.5;"..S("Output item").."]".. + "image[0,1.3;1,1;debug_btn.png]".. + "item_image_button[0,2.3;1,1;mcl_books:book;button_log;]".. + "item_image_button[0,3.3;1,1;mcl_core:gold_ingot;button_buy;]".. + + "list[current_player;main;0,5;9,3;9]".. + mcl_formspec.get_itemslot_bg(0,5,9,3).. + "list[current_player;main;0,8.24;9,1;]".. + mcl_formspec.get_itemslot_bg(0,8.24,9,1).. + "listring[current_player;main]".. + + "button_exit[0,9.2;1,1;btn_exit;"..S("Done").."]" + + -- Add dynamic elements + local pos_str = pos.x..","..pos.y..","..pos.z + local settings = get_vendor_settings(pos) + + if settings.admin_vendor then + base = base.."item_image[0,0.3;1,1;mcl_chests:chest]" + else + base = base.."item_image_button[0,0.3;1,1;mcl_chests:chest;button_inv;]" + end + + local inv = + "list[nodemeta:"..pos_str..";wanted_item;1,0.3;1,1;]".. + mcl_formspec.get_itemslot_bg(1,0.3,1,1).. + "list[nodemeta:"..pos_str..";given_item;5,0.3;1,1;]".. + mcl_formspec.get_itemslot_bg(5,0.3,1,1).. + "listring[nodemeta:"..pos_str..";wanted_item]".. + "listring[nodemeta:"..pos_str..";given_item]" + + local fields = + "field[2.2,0.8;1,0.6;input_item_qty;;"..settings.input_item_qty.."]".. + "field[6.2,0.8;1,0.6;output_item_qty;;"..settings.output_item_qty.."]".. + "field[1.3,4.1;2.66,1;co_sellers;Co-Sellers:;"..settings.co_sellers.."]".. + "field[3.86,4.1;2.66,1;banned_buyers;Banned Buyers:;"..settings.banned_buyers.."]".. + "field_close_on_enter[input_item_qty;false]".. + "field_close_on_enter[output_item_qty;false]".. + "field_close_on_enter[co_sellers;false]".. + "field_close_on_enter[banned_buyers;false]" + + local checkboxes = + "checkbox[1,2.2;inactive_force;"..S("Force vendor into an inactive state.")..";"..bts(settings.inactive_force).."]".. + "checkbox[1,2.6;depositor;"..S("Set this vendor to a Depositor.")..";"..bts(settings.depositor).."]".. + "checkbox[1,3.0;accept_worn_output;"..S("Sell worn tools.")..";"..bts(settings.accept_worn_output).."]".. + "checkbox[5,3.0;accept_worn_input;"..S("Buy worn tools.")..";"..bts(settings.accept_worn_input).."]".. + "checkbox[5,2.6;auto_sort;"..S("Automatically sort inventory.")..";"..bts(settings.auto_sort).."]" + + -- Admin vendor checkbox only if owner is admin + local meta = minetest.get_meta(pos) + if minetest.check_player_privs(meta:get_string("owner"), {admin_vendor=true}) or settings.admin_vendor then + checkboxes = checkboxes.. + "checkbox[5,2.2;admin_vendor;"..S("Set vendor to an admin vendor.")..";"..bts(settings.admin_vendor).."]" + end + + + -- Optional dependancy specific elements + if minetest.get_modpath("pipeworks") or minetest.get_modpath("hopper") then + checkboxes = checkboxes.. + "checkbox[1,1.7;currency_eject;"..S("Eject incoming currency.")..";"..bts(settings.currency_eject).."]" + if minetest.get_modpath("pipeworks") then + checkboxes = checkboxes.. + "checkbox[5,1.3;accept_output_only;"..S("Accept for-sale item only.")..";"..bts(settings.accept_output_only).."]".. + "checkbox[1,1.3;split_incoming_stacks;"..S("Split incoming stacks.")..";"..bts(settings.split_incoming_stacks).."]" + end + end + + if minetest.get_modpath("digilines") then + fields = fields.. + "field[6.41,4.1;2.66,1;digiline_channel;"..S("Digiline Channel:")..";"..settings.digiline_channel.."]".. + "field_close_on_enter[digiline_channel;false]" + end + + local fs = base..inv..fields..checkboxes + return fs +end + +local function get_vendor_default_fs(pos, player) + local base = "size[16,11]".. + "item_image[0,0.3;1,1;mcl_chests:chest]".. + + "list[current_player;main;4,6.6;9,3;9]".. + mcl_formspec.get_itemslot_bg(4,6.6,9,3).. + "list[current_player;main;4,9.75;9,1;]".. + mcl_formspec.get_itemslot_bg(4,9.75,9,1).. + "listring[current_player;main]".. + + "button[1,6.85;3,1;inv_tovendor;"..S("All To Vendor").."]".. + "button[13,6.85;3,1;inv_fromvendor;"..S("All From Vendor").."]".. + "button[1,8.08;3,1;inv_output_tovendor;"..S("Output To Vendor").."]".. + "button[13,8.08;3,1;inv_input_fromvendor;"..S("Input From Vendor").."]".. + "button[1,9.31;3,1;sort;"..S("Sort Inventory").."]".. + "button_exit[0,10;1,1;btn_exit;"..S("Done").."]" + + -- Add dynamic elements + local pos_str = pos.x..","..pos.y..","..pos.z + local inv_lists = + "list[nodemeta:"..pos_str..";main;1,0.3;15,6;]".. + mcl_formspec.get_itemslot_bg(1,0.3,15,6).. + "listring[nodemeta:"..pos_str..";main]" + + local settings_btn = "" + if can_modify_vendor(pos, player) then + settings_btn = + "image_button[0,1.3;1,1;debug_btn.png;button_settings;]".. + "item_image_button[0,2.3;1,1;mcl_books:book;button_log;]".. + "item_image_button[0,3.3;1,1;mcl_core:gold_ingot;button_buy;]" + else + settings_btn = + "image[0,1.3;1,1;debug_btn.png]".. + "item_image[0,2.3;1,1;mcl_books:book]".. + "item_image[0,3.3;1,1;mcl_core:gold_ingot;button_buy;]" + end + + local fs = base..inv_lists..settings_btn + return fs +end + + +local function get_vendor_log_fs(pos) + local base = "size[9,9]".. + "image_button[0,1.3;1,1;debug_btn.png;button_settings;]".. + "item_image[0,2.3;1,1;mcl_books:book]".. + "item_image_button[0,3.3;1,1;mcl_core:gold_ingot;button_buy;]".. + "button_exit[0,8;1,1;btn_exit;"..S("Done").."]" + + -- Add dynamic elements + local meta = minetest.get_meta(pos) + local logs = minetest.deserialize(meta:get_string("log")) + + local settings = get_vendor_settings(pos) + if settings.admin_vendor then + base = base.."item_image[0,0.3;1,1;mcl_chests:chest]" + else + base = base.."item_image_button[0,0.3;1,1;mcl_chests:chest;button_inv;]" + end + + if not logs then logs = {S("Error loading logs"),} end + local logs_tl = + "textlist[1,0.5;7.8,8.6;logs;"..table.concat(logs, ",").."]".. + "label[1,0;Showing (up to "..max_logs..") recent log entries:]" + + local fs = base..logs_tl + return fs +end + +local function show_buyer_formspec(player, pos) + minetest.show_formspec(player:get_player_name(), "fancy_vend:buyer;"..minetest.pos_to_string(pos), get_vendor_buyer_fs(pos, player, nil)) +end + +local function show_vendor_formspec(player, pos) + local settings = get_vendor_settings(pos) + if can_access_vendor_inv(player, pos) then + local status, errorcode = get_vendor_status(pos) + if ((not status and errorcode == "unconfigured") and can_modify_vendor(pos, player)) or settings.admin_vendor then + minetest.show_formspec(player:get_player_name(), "fancy_vend:settings;"..minetest.pos_to_string(pos), get_vendor_settings_fs(pos)) + else + minetest.show_formspec(player:get_player_name(), "fancy_vend:default;"..minetest.pos_to_string(pos), get_vendor_default_fs(pos, player)) + end + else + show_buyer_formspec(player, pos) + end +end + +local function swap_vendor(pos, vendor_type) + local node = minetest.get_node(pos) + node.name = vendor_type + minetest.swap_node(pos, node) +end + +local function get_correct_vendor(settings) + if settings.admin_vendor then + if settings.depositor then + return "fancy_vend:admin_depo" + else + return "fancy_vend:admin_vendor" + end + else + if settings.depositor then + return "fancy_vend:player_depo" + else + return "fancy_vend:player_vendor" + end + end +end + +local function is_vendor(name) + local vendor_names = { + "fancy_vend:player_vendor", + "fancy_vend:player_depo", + "fancy_vend:admin_vendor", + "fancy_vend:admin_depo", + } + for i,n in ipairs(vendor_names) do + if name == n then + return true + end + end + return false +end + + +local function refresh_vendor(pos) + local node = minetest.get_node(pos) + if node.name:split(":")[1] ~= "fancy_vend" then + return false, "not a vendor" + end + + local settings = get_vendor_settings(pos) + local meta = minetest.get_meta(pos) + local status, errorcode = get_vendor_status(pos) + local correct_vendor = get_correct_vendor(settings) + + if status or errorcode ~= "no_output" then + meta:set_string("alerted", "false") + end + + if status then + meta:set_string("infotext", (settings.admin_vendor and "Admin" or "Player").." Vendor trading "..settings.input_item_qty.." "..minetest.registered_items[settings.input_item].description.." for "..settings.output_item_qty.." "..minetest.registered_items[settings.output_item].description.." (owned by " .. meta:get_string("owner") .. ")") + + if meta:get_string("configured") == "" then + meta:set_string("configured", "true") + -- if minetest.get_modpath("awards") then + -- local name = meta:get_string("owner") + -- local data = awards.player(name) + + -- -- Ensure fancy_vend_configure table is in data + -- if not data.fancy_vend_configure then + -- data.fancy_vend_configure = {} + -- end + + -- awards.increment_item_counter(data, "fancy_vend_configure", correct_vendor) + + -- total_item_count = 0 + + -- for k, v in pairs(data.fancy_vend_configure) do + -- total_item_count = total_item_count + v + -- end + + -- if awards.get_item_count(data, "fancy_vend_configure", "fancy_vend:player_vendor") >= 1 then + -- awards.unlock(name, "fancy_vend:seller") + -- end + -- if awards.get_item_count(data, "fancy_vend_configure", "fancy_vend:player_depo") >= 1 then + -- awards.unlock(name, "fancy_vend:trader") + -- end + -- if total_item_count >= 10 then + -- awards.unlock(name, "fancy_vend:shop_keeper") + -- end + -- if total_item_count >= 25 then + -- awards.unlock(name, "fancy_vend:merchant") + -- end + -- if total_item_count >= 100 then + -- awards.unlock(name, "fancy_vend:super_merchant") + -- end + -- if total_item_count >= 9001 then + -- awards.unlock(name, "fancy_vend:god_merchant") + -- end + -- end + end + + if settings.depositor then + if meta:get_string("item") ~= settings.input_item then + meta:set_string("item", settings.input_item) + update_item(pos, node) + end + else + if meta:get_string("item") ~= settings.output_item then + meta:set_string("item", settings.output_item) + update_item(pos, node) + end + end + else + meta:set_string("infotext", "Inactive "..(settings.admin_vendor and "Admin" or "Player").." Vendor"..make_inactive_string(errorcode).." (owned by " .. meta:get_string("owner") .. ")") + if meta:get_string("item") ~= "fancy_vend:inactive" then + meta:set_string("item", "fancy_vend:inactive") + update_item(pos, node) + end + + if not alerted and not status and errorcode == "no_room" then + minetest.chat_send_player(meta:get_string("owner"), "[Fancy_Vend]: Error with vendor at "..minetest.pos_to_string(pos, 0)..": does not have room for payment.") + meta:set_string("alerted", "true") + end + end + + if correct_vendor ~= node.name then + swap_vendor(pos, correct_vendor) + end +end + +local function move_inv(frominv, toinv, filter) + for i, v in ipairs(frominv:get_list("main") or {}) do + if v:get_name() == filter or not filter then + if toinv:room_for_item("main", v) then + local leftover = toinv:add_item("main", v) + + frominv:remove_item("main", v) + + if leftover + and not leftover:is_empty() then + frominv:add_item("main", v) + end + end + end + end +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + local name = formname:split(":")[1] + if name ~= "fancy_vend" then return end + local formtype = formname:split(":")[2] + formtype = formtype:split(";")[1] + local pos = minetest.string_to_pos(formname:split(";")[2]) + if not pos then return end + + local node = minetest.get_node(pos) + if not is_vendor(node.name) then return end + + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + local player_inv = player:get_inventory() + local settings = get_vendor_settings(pos) + + -- Handle settings changes + if can_modify_vendor(pos, player) then + for i in pairs(fields) do + if stb(fields[i]) ~= settings[i] then + settings[i] = stb(fields[i]) + end + end + + -- Check number-only fields contain only numbers + if not tonumber(settings.input_item_qty) then + settings.input_item_qty = 1 + else + settings.input_item_qty = math.floor(math.abs(tonumber(settings.input_item_qty))) + end + if not tonumber(settings.output_item_qty) then + settings.output_item_qty = 1 + else + settings.output_item_qty = math.floor(math.abs(tonumber(settings.output_item_qty))) + end + + -- Check item quantities aren't too high (which could lead to additional processing for no reason), if so, set it to the maximum the player inventory can handle + if ItemStack(settings.output_item):get_stack_max() * player_inv:get_size("main") < settings.output_item_qty then + settings.output_item_qty = ItemStack(settings.output_item):get_stack_max() * player_inv:get_size("main") + end + + if ItemStack(settings.input_item):get_stack_max() * player_inv:get_size("main") < settings.input_item_qty then + settings.input_item_qty = ItemStack(settings.input_item):get_stack_max() * player_inv:get_size("main") + end + + -- Admin vendor priv check + if not minetest.check_player_privs(meta:get_string("owner"), {admin_vendor=true}) and fields.admin_vendor == "true" then + settings.admin_vendor = false + end + + set_vendor_settings(pos, settings) + refresh_vendor(pos) + end + + if fields.quit then + if can_access_vendor_inv(player, pos) and settings.auto_sort then + sort_inventory(inv) + end + return true + end + + if fields.sort and can_access_vendor_inv(player, pos) then + sort_inventory(inv) + end + + if fields.buy then + local lots = math.floor(tonumber(fields.lot_count) or 1) + -- prevent negative numbers + lots = math.max(lots, 1) + local success, message = make_purchase(pos, player, lots) + if success then + -- Add to vendor logs + local logs = minetest.deserialize(meta:get_string("log")) + for i in pairs(logs) do + if i >= max_logs then + table.remove(logs, 1) + end + end + table.insert(logs, "Player "..player:get_player_name().." purchased "..lots.." lots from this vendor.") + meta:set_string("log", minetest.serialize(logs)) + + -- Send digiline message if applicable + if minetest.get_modpath("digilines") then + local msg = { + buyer = player:get_player_name(), + lots = lots, + settings = settings, + } + send_message(pos, settings.digiline_channel, msg) + end + end + -- Set message and refresh vendor + if message then + meta:set_string("message", message) + end + refresh_vendor(pos) + elseif fields.lot_fill then + minetest.show_formspec(player:get_player_name(), "fancy_vend:buyer;"..minetest.pos_to_string(pos), get_vendor_buyer_fs(pos, player, get_max_lots(pos, player))) + return true + end + + if can_access_vendor_inv(player, pos) then + if fields.inv_tovendor then + minetest.log("action", player:get_player_name().." moves inventory contents to vendor at "..minetest.pos_to_string(pos)) + move_inv(player_inv, inv, nil) + refresh_vendor(pos) + elseif fields.inv_output_tovendor then + minetest.log("action", player:get_player_name().." moves output items in inventory to vendor at "..minetest.pos_to_string(pos)) + move_inv(player_inv, inv, settings.output_item) + refresh_vendor(pos) + elseif fields.inv_fromvendor then + minetest.log("action", player:get_player_name().." moves inventory contents from vendor at "..minetest.pos_to_string(pos)) + move_inv(inv, player_inv, nil) + refresh_vendor(pos) + elseif fields.inv_input_fromvendor then + minetest.log("action", player:get_player_name().." moves input items from vendor at "..minetest.pos_to_string(pos)) + move_inv(inv, player_inv, settings.input_item) + refresh_vendor(pos) + end + end + + -- Handle page changes + if fields.button_log then + minetest.show_formspec(player:get_player_name(), "fancy_vend:log;"..minetest.pos_to_string(pos), get_vendor_log_fs(pos)) + return + elseif fields.button_settings then + minetest.show_formspec(player:get_player_name(), "fancy_vend:settings;"..minetest.pos_to_string(pos), get_vendor_settings_fs(pos)) + return + elseif fields.button_inv then + minetest.show_formspec(player:get_player_name(), "fancy_vend:default;"..minetest.pos_to_string(pos), get_vendor_default_fs(pos, player)) + return + elseif fields.button_buy then + minetest.show_formspec(player:get_player_name(), "fancy_vend:buyer;"..minetest.pos_to_string(pos), get_vendor_buyer_fs(pos, player, (tonumber(fields.lot_count) or 1))) + return + end + + -- Update formspec + if formtype == "log" then + minetest.show_formspec(player:get_player_name(), "fancy_vend:log;"..minetest.pos_to_string(pos), get_vendor_log_fs(pos, player)) + elseif formtype == "settings" then + minetest.show_formspec(player:get_player_name(), "fancy_vend:settings;"..minetest.pos_to_string(pos), get_vendor_settings_fs(pos, player)) + elseif formtype == "default" then + minetest.show_formspec(player:get_player_name(), "fancy_vend:default;"..minetest.pos_to_string(pos), get_vendor_default_fs(pos, player)) + elseif formtype == "buyer" then + minetest.show_formspec(player:get_player_name(), "fancy_vend:buyer;"..minetest.pos_to_string(pos), get_vendor_buyer_fs(pos, player, (tonumber(fields.lot_count) or 1))) + end +end) + +local vendor_template = { + description = S("Vending Machine"), + legacy_facedir_simple = true, + paramtype2 = "facedir", + groups = {choppy=2, oddly_breakable_by_hand=2, tubedevice=1, tubedevice_receiver=1, axey=1, handy=1}, + _mcl_blast_resistance = 5, + _mcl_hardness = 0.8, + selection_box = { + type = "fixed", + fixed = {-0.5, -0.5, -0.5, 0.5, 1.5, 0.5}, + }, + is_ground_content = false, + light_source = 8, + sounds = mcl_sounds.node_sound_wood_defaults(), + drop = drop_vendor, + on_construct = function(pos) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Unconfigured Player Vendor") + meta:set_string("message", "Vendor initialized") + meta:set_string("owner", "") + local inv = meta:get_inventory() + inv:set_size("main", 15*6) + inv:set_size("wanted_item", 1*1) + inv:set_size("given_item", 1*1) + reset_vendor_settings(pos) + meta:set_string("log", "") + end, + can_dig = can_dig_vendor, + on_place = function(itemstack, placer, pointed_thing) + if pointed_thing.type ~= "node" then return end + local pointed_node_pos = minetest.get_pointed_thing_position(pointed_thing, false) + local pointed_node = minetest.get_node(pointed_node_pos) + if minetest.registered_nodes[pointed_node.name].buildable_to then + pointed_thing.above = pointed_node_pos + end + -- Set variables for access later (for various checks, etc.) + local name = placer:get_player_name() + local above_node_pos = table.copy(pointed_thing.above) + above_node_pos.y = above_node_pos.y + 1 + local above_node = minetest.get_node(above_node_pos).name + + -- If node above is air or the display node, and it is not protected, attempt to place the vendor. If vendor sucessfully places, place display node above, otherwise alert the user + if (minetest.registered_nodes[above_node].buildable_to or above_node == "fancy_vend:display_node") and not minetest.is_protected(above_node_pos, name) then + local itemstack, success = minetest.item_place(itemstack, placer, pointed_thing, nil) + if above_node ~= "fancy_vend:display_node" and success then + minetest.set_node(above_node_pos, minetest.registered_nodes["fancy_vend:display_node"]) + end + -- Set owner + local meta = minetest.get_meta(pointed_thing.above) + meta:set_string("owner", placer:get_player_name() or "") + + -- Set default meta + meta:set_string("log", minetest.serialize({"Vendor placed by "..placer:get_player_name(),})) + reset_vendor_settings(pointed_thing.above) + refresh_vendor(pointed_thing.above) + else + minetest.chat_send_player(name, "Vendors require 2 nodes of space.") + end + + if minetest.get_modpath("pipeworks") then + pipeworks.after_place(pointed_thing.above) + end + + return itemstack + end, + on_dig = function(pos, node, digger) + -- Set variables for access later (for various checks, etc.) + local name = digger:get_player_name() + local above_node_pos = table.copy(pos) + above_node_pos.y = above_node_pos.y + 1 + + -- abandon if player shouldn't be able to dig node + local can_dig = can_dig_vendor(pos, digger) + if not can_dig then return end + + -- Try remove display node, if the whole node is able to be removed by the player, remove the display node and continue to remove vendor, if it doesn't exist and vendor can be dug continue to remove vendor. + local success + if minetest.get_node(above_node_pos).name == "fancy_vend:display_node" then + if not minetest.is_protected(above_node_pos, name) and not minetest.is_protected(pos, name) then + minetest.remove_node(above_node_pos) + remove_item(above_node_pos) + success = true + else + success = false + end + else + if not minetest.is_protected(pos, name) then + success = true + else + success = false + end + end + + -- If failed to remove display node, don't remove vendor. since protection for whole vendor was checked at display removal, protection need not be re-checked + if success then + minetest.remove_node(pos) + --minetest.handle_node_drops(pos, {drop_vendor}, digger) + minetest.add_item(pos, drop_vendor) + if minetest.get_modpath("pipeworks") then + pipeworks.after_dig(pos) + end + end + end, + tube = { + input_inventory = "main", + connect_sides = {left = 1, right = 1, back = 1, bottom = 1}, + insert_object = function(pos, node, stack, direction) + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + local remaining = inv:add_item("main", stack) + refresh_vendor(pos) + return remaining + end, + can_insert = function(pos, node, stack, direction) + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + local settings = get_vendor_settings(pos) + if settings.split_stacks then + stack = stack:peek_item(1) + end + if settings.accept_output_only then + if stack:get_name() ~= settings.output_item then + return false + end + end + return inv:room_for_item("main", stack) + end, + }, + allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player) + if (not can_access_vendor_inv(player, pos)) or to_list == "wanted_item" or to_list == "given_item" then + return 0 + end + return count + end, + allow_metadata_inventory_put = function(pos, listname, index, stack, player) + if not can_access_vendor_inv(player, pos) then + return 0 + end + if listname == "wanted_item" or listname == "given_item" then + local inv = minetest.get_meta(pos):get_inventory() + inv:set_stack(listname, index, ItemStack(stack:get_name())) + local settings = get_vendor_settings(pos) + if listname == "wanted_item" then + settings.input_item = stack:get_name() + elseif listname == "given_item" then + settings.output_item = stack:get_name() + end + set_vendor_settings(pos, settings) + return 0 + end + return stack:get_count() + end, + allow_metadata_inventory_take = function(pos, listname, index, stack, player) + if not can_access_vendor_inv(player, pos) then + return 0 + end + if listname == "wanted_item" or listname == "given_item" then + local inv = minetest.get_meta(pos):get_inventory() + local fake_stack = inv:get_stack(listname, index) + fake_stack:take_item(stack:get_count()) + inv:set_stack(listname, index, fake_stack) + local settings = get_vendor_settings(pos) + if listname == "wanted_item" then + settings.input_item = "" + elseif listname == "given_item" then + settings.output_item = "" + end + set_vendor_settings(pos, settings) + return 0 + end + return stack:get_count() + end, + on_rightclick = function(pos, node, clicker) + node = minetest.get_node(pos) + if node.name == "fancy_vend:display_node" then + pos.y = pos.y - 1 + end + show_vendor_formspec(clicker, pos) + end, + on_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player) + minetest.log("action", player:get_player_name().." moves stuff in vendor at "..minetest.pos_to_string(pos)) + refresh_vendor(pos) + end, + on_metadata_inventory_put = function(pos, listname, index, stack, player) + minetest.log("action", player:get_player_name().." moves "..stack:get_name().." to vendor at "..minetest.pos_to_string(pos)) + refresh_vendor(pos) + end, + on_metadata_inventory_take = function(pos, listname, index, stack, player) + minetest.log("action", player:get_player_name().." takes "..stack:get_name().." from vendor at "..minetest.pos_to_string(pos)) + refresh_vendor(pos) + end, + on_blast = function() + -- TNT immunity + end, +} + +if pipeworks then + vendor_template.digiline = { + receptor = {}, + effector = { + action = function() end + }, + wire = { + rules = pipeworks.digilines_rules + }, + } +end + +local player_vendor = table.copy(vendor_template) +player_vendor.tiles = { + "player_vend.png", "player_vend.png", + "player_vend.png^mcl_core_emerald.png", "player_vend.png^mcl_core_emerald.png", + "player_vend.png^mcl_core_emerald.png", "player_vend_front.png", + } + +local player_depo = table.copy(vendor_template) +player_depo.tiles = { + "player_depo.png", "player_depo.png", + "player_depo.png^mcl_core_emerald.png", "player_depo.png^mcl_core_emerald.png", + "player_depo.png^mcl_core_emerald.png", "player_depo_front.png", + } +player_depo.groups.not_in_creative_inventory = 1 + +local admin_vendor = table.copy(vendor_template) +admin_vendor.tiles = { + "admin_vend.png", "admin_vend.png", + "admin_vend.png^mcl_core_emerald.png", "admin_vend.png^mcl_core_emerald.png", + "admin_vend.png^mcl_core_emerald.png", "admin_vend_front.png", + } +admin_vendor.groups.not_in_creative_inventory = 1 + +local admin_depo = table.copy(vendor_template) +admin_depo.tiles = { + "admin_depo.png", "admin_depo.png", + "admin_depo.png^mcl_core_emerald.png", "admin_depo.png^mcl_core_emerald.png", + "admin_depo.png^mcl_core_emerald.png", "admin_depo_front.png", + } +admin_depo.groups.not_in_creative_inventory = 1 + +minetest.register_node(":fancy_vend:player_vendor", player_vendor) +minetest.register_node(":fancy_vend:player_depo", player_depo) +minetest.register_node(":fancy_vend:admin_vendor", admin_vendor) +minetest.register_node(":fancy_vend:admin_depo", admin_depo) + +minetest.register_craft({ + output = "fancy_vend:player_vendor", + recipe = { + { "mcl_core:gold_ingot",display_node, "mcl_core:gold_ingot"}, + { "mcl_core:diamond", "mesecons:redstone", "mcl_core:diamond"}, + { "mcl_core:gold_ingot","mcl_chests:chest","mcl_core:gold_ingot"}, + } +}) + +-- Hopper support +if minetest.get_modpath("hopper") then + hopper:add_container({ + {"side", "fancy_vend:player_vendor", "main"} + }) + + hopper:add_container({ + {"side", "fancy_vend:player_depo", "main"} + }) +end + + +--------------- +-- Copy Tool -- +--------------- + +local function get_vendor_pos_and_settings(pointed_thing) + if pointed_thing.type ~= "node" then return false end + local pos = minetest.get_pointed_thing_position(pointed_thing, false) + local node = minetest.get_node(pos) + if node.name == "fancy_vend:display_node" then + pos.y = pos.y - 1 + node = minetest.get_node(pos) + end + if not is_vendor(node.name) then return false end + + local settings = get_vendor_settings(pos) + + return pos, settings +end + +minetest.register_tool(":fancy_vend:copy_tool",{ + inventory_image = "copier.png", + description = S("Geminio Wand (For copying vendor settings, right click to save settings, left click to set settings.)"), + stack_max = 1, + on_place = function(itemstack, placer, pointed_thing) + local pos, settings = get_vendor_pos_and_settings(pointed_thing) + if not pos then return end + + local meta = itemstack:get_meta() + meta:set_string("settings", minetest.serialize(settings)) + + minetest.chat_send_player(placer:get_player_name(), "Settings saved.") + + return itemstack + end, + on_use = function(itemstack, user, pointed_thing) + local pos, current_settings = get_vendor_pos_and_settings(pointed_thing) + if not pos then return end + + local meta = itemstack:get_meta() + local node_meta = minetest.get_meta(pos) + local new_settings = minetest.deserialize(meta:get_string("settings")) + if not new_settings then + minetest.chat_send_player(user:get_player_name(), "No settings to set with. Right-click first on the vendor you want to copy settings from.") + return + end + + if can_modify_vendor(pos, user) then + -- Admin vendor priv check + if not minetest.check_player_privs(node_meta:get_string("owner"), {admin_vendor=true}) and new_settings.admin_vendor == true then + settings.admin_vendor = false + end + + new_settings.input_item = current_settings.input_item + new_settings.input_item_qty = current_settings.input_item_qty + new_settings.output_item = current_settings.output_item + new_settings.output_item_qty = current_settings.output_item_qty + + -- Admin vendor priv check + if not minetest.check_player_privs(node_meta:get_string("owner"), {admin_vendor=true}) and new_settings.admin_vendor then + new_settings.admin_vendor = current_settings.admin_vendor + end + + set_vendor_settings(pos, new_settings) + refresh_vendor(pos) + minetest.chat_send_player(user:get_player_name(), "Settings set.") + else + minetest.chat_send_player(user:get_player_name(), "You cannot modify this vendor.") + end + end, +}) + +minetest.register_craft({ + output = "fancy_vend:copy_tool", + recipe = { + {"mcl_core:stick","", "" }, + {"", "mcl_core:obsidian","" }, + {"", "", "mcl_core:diamond"}, + } +}) + +minetest.register_craft({ + output = "fancy_vend:copy_tool", + recipe = { + {"", "", "mcl_core:stick"}, + {"", "mcl_core:obsidian","" }, + {"mcl_core:diamond","", "" }, + } +}) + + +--------------------------- +-- Vendor Upgrade System -- +--------------------------- + +local old_vendor_mods = string.split((minetest.setting_get("fancy_vend_old_vendor_mods") or ""), ",") +local old_vendor_mods_table = {} + +for i in pairs(old_vendor_mods) do + old_vendor_mods_table[old_vendor_mods[i]] = true +end + +local base_upgrade_template = { + description = S("Shop Upgrade (Try and place to upgrade)"), + legacy_facedir_simple = true, + paramtype2 = "facedir", + groups = {choppy=2, oddly_breakable_by_hand=2, not_in_creative_inventory=1, axey=1, handy=1}, + _mcl_blast_resistance = 5, + _mcl_hardness = 0.8, + is_ground_content = false, + light_source = 8, + sounds = mcl_sounds.node_sound_wood_defaults(), + drop = drop_vendor, + tiles = { + "player_vend.png", "player_vend.png", + "player_vend.png^mcl_core_emerald.png", "player_vend.png^mcl_core_emerald.png", + "player_vend.png^mcl_core_emerald.png", "upgrade_front.png", + }, + on_place = function(itemstack, placer, pointed_thing) + return ItemStack(drop_vendor.." "..itemstack:get_count()) + end, + allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player) + local meta = minetest.get_meta(pos) + if player:get_player_name() ~= meta:get_string("owner") then return 0 end + return count + end, + allow_metadata_inventory_put = function(pos, listname, index, stack, player) + local meta = minetest.get_meta(pos) + if player:get_player_name() ~= meta:get_string("owner") then return 0 end + return stack:get_count() + end, + allow_metadata_inventory_take = function(pos, listname, index, stack, player) + local meta = minetest.get_meta(pos) + if player:get_player_name() ~= meta:get_string("owner") then return 0 end + return stack:get_count() + end, +} + +local clear_craft_vendors = {} + +if old_vendor_mods_table["emeraldbank"] then + emeraldbank.shop_drop = drop_vendor + emeraldbank.shop_empty_drop = drop_vendor +else + emeraldbank.shop_drop = "emeraldbank:shop" + emeraldbank.shop_empty_drop = "emeraldbank:shop" +end + +if old_vendor_mods_table["currency"] then + local currency_template = table.copy(base_upgrade_template) + + currency_template.can_dig = function(pos, player) + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + return inv:is_empty("stock") and inv:is_empty("customers_gave") and inv:is_empty("owner_wants") and inv:is_empty("owner_gives") and (meta:get_string("owner") == player:get_player_name() or minetest.check_player_privs(player:get_player_name(), {protection_bypass=true})) + end + currency_template.on_rightclick = function(pos, node, clicker, itemstack) + local meta = minetest.get_meta(pos) + local list_name = "nodemeta:"..pos.x..','..pos.y..','..pos.z + if clicker:get_player_name() == meta:get_string("owner") then + minetest.show_formspec(clicker:get_player_name(),"fancy_vend:currency_shop_formspec", + "size[8,9.5]".. + "label[0,0;" .. "Customers gave:" .. "]".. + "list["..list_name..";customers_gave;0,0.5;3,2;]".. + "label[0,2.5;" .. "Your stock:" .. "]".. + "list["..list_name..";stock;0,3;3,2;]".. + "label[5,0;" .. "You want:" .. "]".. + "list["..list_name..";owner_wants;5,0.5;3,2;]".. + "label[5,2.5;" .. "In exchange, you give:" .. "]".. + "list["..list_name..";owner_gives;5,3;3,2;]".. + "list[current_player;main;0,5.5;8,4;]" + ) + end + end + + minetest.register_node(":currency:shop", currency_template) + + table.insert(clear_craft_vendors, "currency:shop") +end + +if old_vendor_mods_table["easyvend"] then + local nodes = {"easyvend:vendor", "easyvend:vendor_on", "easyvend:depositor", "easyvend:depositor_on"} + for i in pairs(nodes) do + minetest.register_node(":"..nodes[i], base_upgrade_template) + table.insert(clear_craft_vendors, nodes[i]) + end +end + +if old_vendor_mods_table["vendor"] then + local nodes = {"vendor:vendor", "vendor:depositor"} + for i in pairs(nodes) do + minetest.register_node(":"..nodes[i], base_upgrade_template) + table.insert(clear_craft_vendors, nodes[i]) + end +end + +if old_vendor_mods_table["money"] then + local money_template = table.copy(base_upgrade_template) + money_template.can_dig = function(pos, player) + local meta = minetest.get_meta(pos) + local inv = meta:get_inventory() + return inv:is_empty("main") and (meta:get_string("owner") == player:get_player_name() or minetest.check_player_privs(player:get_player_name(), {protection_bypass=true})) + end + money_template.on_rightclick = function(pos, node, clicker, itemstack) + local meta = minetest.get_meta(pos) + local list_name = "nodemeta:"..pos.x..','..pos.y..','..pos.z + if clicker:get_player_name() == meta:get_string("owner") then + minetest.show_formspec(clicker:get_player_name(),"fancy_vend:money_shop_formspec", + "size[8,10;]".. + "list["..listname..";main;0,0;8,4;]".. + "list[current_player;main;0,6;8,4;]" + ) + end + end + local nodes = {"money:barter_shop", "money:shop", "money:admin_shop", "money:admin_barter_shop"} + for i in pairs(nodes) do + minetest.register_node(":"..nodes[i], money_template) + table.insert(clear_craft_vendors, nodes[i]) + end +end + +for i_n in pairs(clear_craft_vendors) do + local currency_crafts = minetest.get_all_craft_recipes(i_n) + if currency_crafts then + for i in pairs(currency_crafts) do + minetest.clear_craft(currency_crafts[i]) + end + end +end diff --git a/init.lua b/init.lua index bc31b77..cc40a69 100644 --- a/init.lua +++ b/init.lua @@ -30,5 +30,6 @@ if income_enabled then end dofile(modpath .. "/bank.lua") +dofile(modpath .. "/fancyshop.lua") dofile(modpath .. "/shop.lua") dofile(modpath .. "/commands.lua") diff --git a/mod.conf b/mod.conf index c189595..5fb1069 100644 --- a/mod.conf +++ b/mod.conf @@ -1,5 +1,5 @@ name = emeraldbank depends = mcl_core, mcl_sounds, mcl_inventory, mcl_formspec, mcl_title -optional_depends = irc, yl_matterbridge +optional_depends = irc, yl_matterbridge, pipeworks, digilines, awards, hopper, tell, email, mail description = Keep your Emeralds in a bank! And trade with shops. -supported_games = mineclone2, mineclone5 \ No newline at end of file +supported_games = mineclone2 diff --git a/settingtypes.txt b/settingtypes.txt index b5b0634..a6cf228 100644 --- a/settingtypes.txt +++ b/settingtypes.txt @@ -1,4 +1,6 @@ +[Bank and Shops] + # If true bank can be crafted emeraldbank.bank_craft (bank craft) bool true @@ -13,3 +15,18 @@ emeraldbank.income_count (number of emeralds) int 1 # Length of time (in seconds) between checking if a user should get income emeraldbank.income_period (time between income) int 1800 + + +[Fancy Shops] + +# The node to copy as the display node +fancy_vend.display_node (Display Node) string mcl_core:glass + +# Maximum number of mods to store per vender +fancy_vend.log_max (Log Max) int 40 + +# Speed of display item auto rotation +fancy_vend.autorotate_speed (Autorotate Speed) int 1 + +# Whether to not send mail alerts +fancy_vend.no_alerts (Don't Send Mail Alerts) bool false diff --git a/textures/admin_depo.png b/textures/admin_depo.png new file mode 100644 index 0000000000000000000000000000000000000000..63ca2d503fce203c9467663d3971672af2f15e52 GIT binary patch literal 303 zcmV+~0nq-5P)T2kDZ literal 0 HcmV?d00001 diff --git a/textures/admin_depo_front.png b/textures/admin_depo_front.png new file mode 100644 index 0000000000000000000000000000000000000000..e0936eeb76c288318dfabd690cd507220d32531f GIT binary patch literal 314 zcmV-A0mc4_P)pi7`exa%Zj6aod}cS< z{qz6qmvb&=J+a7Vw%!k7jw`#TZl-p*k~bN~PV M07*qoM6N<$f)DV4(EtDd literal 0 HcmV?d00001 diff --git a/textures/admin_vend.png b/textures/admin_vend.png new file mode 100644 index 0000000000000000000000000000000000000000..56d9a01f42692a37693db770ae147b44d5ee0328 GIT binary patch literal 257 zcmV+c0sj7pP)3|C+^^P0nF(-7JguiY$%8gHUO6kh`}9a zpa!5j7iKfE?JyT02k6vGkH~WY#RkAI(FPzp9XT<<4Zx-V+0R6~0GmPh(gew2fF2td zULZ9kf(?k?e&)abmJ=vAYyXA+AaM{4N-Q8;ee5a*28jazdH=aKL?jdS00000NkvXX Hu0mjf_{(Aw literal 0 HcmV?d00001 diff --git a/textures/admin_vend_front.png b/textures/admin_vend_front.png new file mode 100644 index 0000000000000000000000000000000000000000..4c6b4e5c2ea4e89aa9e95b4eb2842d8f4c22b4a6 GIT binary patch literal 273 zcmV+s0q*{ZP)eZHllNZ!&rove4oS{N#?>e8;C2Bv8?hRI&jm22>sa`K zF|wf`cE_1JlzCz5rAH70kWD5O@5liPHvrjG5TAj80V@VY8*=!NZ2*QBh&2Ed@5qS> zB@AFVdi$CG{##C<;H>=@z~UepuM3b3LXR@Mu|RCJA*Tnp7YI2SlvqHx`q)(r3=#(b XsbJfH$mv$;00000NkvXXu0mjfCNOPy literal 0 HcmV?d00001 diff --git a/textures/copier.png b/textures/copier.png new file mode 100644 index 0000000000000000000000000000000000000000..e87719eadfdf498af5864b64a4a95cfef581698e GIT binary patch literal 227 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4&HECyrsn$JLrv%n*= zn1O*?7=#%aX3dcR3bL1Y`ns||V&Y?z;Fk1}p92(9$P9@nan8@pP0cF-av2z$i&7Iy zQd1Pt@{1IFGxJhIQj1Gk>Kv?qQZAk@jv*GOlM@y&u*@*}qaM9^mx%JuS#P&$T(Eid z=HdGsS?>G)nV6WSWKIh=R=LZ-$jJD~^z>@Uf9XH|&#zg2I7i+50OPZRN}&Z-;x!<< NJYD@<);T3K0RSXALt+2` literal 0 HcmV?d00001 diff --git a/textures/inactive.png b/textures/inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..299586280ae3399b6f2f29ad0ce7e5a6abe2dfdb GIT binary patch literal 890 zcmV-=1BLvFP)_Hg5XhmNXx0kVp z%7!naZZ9&@&b@UyS6H>0w$p80)}*V|bitHHo8Bb%Msw4qY3@1Wj1Bg(hr`E#@5ebG z{6Aonf{{#0F@)Y9Bjn~(B+|ECtzyO){CoI)DiR5eVLTo~=#-I6ws6N`ON@{YZw3Oj zpAH_hzLlhzcr^OXR;gs;kk@!L`qm9WxF6rWyYZdhzcot8Y3|6LYu7$_MO8n0N9D;7cWc(CAd1#)-p1edN}ZLY1XoKpMx-fi=G2^0!|9S*3OCaM|+kkjGt!rJuo z9;c$bDk%!a)hh66m*pU%^ze>QNcD2~;st;tfws0rb5EZ9>>V5&B45qVmyA^EkJZV^ zZl}i+;E-%?g4EiI+jj0GveStXhfXJGnns_dQV)Dbj-1~=I-0;XQKbDNBftB?;md#C zy*pFLSJ6aWzdMNOmGg@uRh$B%#9KQi)*O%yE*0AO!z-R>D0n%^iE zC-RGn79#{;V*^+^4X9iOuq@EBS!>fY@AsTL_Z#o$?r&Vz^`@oa;k}~U9rkr~*;qag z*0W~-f&i>u2QW?W`u$jJZa%a$JltQ`bvt)xyILg=PM+*vzkT~tH5A(K^>{GWb>Ls8 zq6mC`FbuF{8MI2p$~=1XYg;68Vc+%Zv*b`R*}Fb5@p)%=_kbb@n7>L}T43EYX?A(J z!R&VAkR-%PC2+_xZjz+-)!EseQX(-&1~Zwj{lVY?ug{0+^XCAH0tM5wR?_Kv6~p+V z^75s_CP_P-vW#f42%_JQtVW|FKQ|ZP8UCSMDmAtl0~82wNVDnmwC~ul%i%=gOW)C> zA3c5ict$f#%G%qZPAQaiy+IlKo@d(?!}$ET+r4$KtIPVvZl8`vqi_Ew+fDIkbl(j@ zn2qn+WsNJ!Rt)1aMlvbGKU?LBA#`>Wdi5Xe~cl-p^Lk}0J8|G0vriY Qm;e9(07*qoM6N<$g2i99-2eap literal 0 HcmV?d00001 diff --git a/textures/player_depo.png b/textures/player_depo.png new file mode 100644 index 0000000000000000000000000000000000000000..0950b4b88b1e1c04ae8603a5153d0dd3d6b8d287 GIT binary patch literal 565 zcmV-50?Pe~P)q z-AmJP6vhwcOFAM9oydkQS|c6IFtri5g3VZrma;@$*@YrYDiFM|h%UOQZiEOVh!BY( zh!bF>Q?EBzIi;P?1TrvZBOew)wu#xYx2!WM=j9lC$k_9g?6F>%V9grlLS}R1|L}X(D70g))LNN~Ci}KphMc8N!1A&f16Q!{SH__-3OFOQO@A zZqFXTo(>UF15id%Gx1+TG}-HIV-7;=Xf6AI&|=6SU=mFMf7ft_55`z?x?gB3|R?C31B$R4xODnCKGu~$5@k;`Cm7Y literal 0 HcmV?d00001 diff --git a/textures/player_depo_front.png b/textures/player_depo_front.png new file mode 100644 index 0000000000000000000000000000000000000000..c63aacb3ec1a2af501c4688520408a374d44868c GIT binary patch literal 553 zcmV+^0@nSBP)J)^TLZD=&C=Uf9AB$IenuGR~HZ8mves4_c_lwKRMnPegDd_U-R|Hf#~5oKeK*# zQ)npPB^U8}4eT z`PodLuHQOi29iuE!C0_5(QPXPRsu4LDVNAA|C$TdU}|+T<|Y#0<6xGt%E=%`3sFiN zc}yB_eL#?gT)NVV00p#~66u@~z{(&oECg*lTM%IQ_S}F0-z*oF!47vC(@+E{$&|;Q z`NwSbm`kZVYUTbs7EO0~+n9sUI$Fy)AheivL%<}O0@xF6J{Tj-=}L+NRS+|ABfr5J r@bl3|R9Xp031B$B)}5X6|4RA=u=Bx(nmZ>j00000NkvXXu0mjfDe?i_ literal 0 HcmV?d00001 diff --git a/textures/player_vend.png b/textures/player_vend.png new file mode 100644 index 0000000000000000000000000000000000000000..e59cc21475756439b29e2372064452849e5af25a GIT binary patch literal 552 zcmV+@0@wYCP)JIDtpB`J zSvvZq&k95v%S7vnMC(f|trfRKL}MYlC(c=WrRdh`VboR%Y6(HCj6@6DHI^B|E zc;Aq&W3N;7LsO>C#q)HbyGRp#A;V%%vG&yl1jsz}LiAJnLv!)Vo3Me)U*AS_WxQHf zC!&UpsTvc*$@uPx%KX8(4#ystOgO;F$V)K}Mq}a`uC+;sca)1(=7=Wt&#eRAN}#jC z2D_fxeREhe{zSC5+Me;7IP3FANdX+RmfB0f3s7HZML2T~0H+l9+hJ0Gs=7CS?#kDh zwp>#{F7p&uSeR_iHY);a#}`(hA#5#@g<4QdKn$olki->rqNVUofIkNdhDFyxvLQw* zofuAOqZeR6u!db%X;#D#a?fzicp{zi0t^xv+yw_`!68~6ciJ*n-9Is{Up{b6DRFZJ zv0G2C|(q$0)GcW#Asy7Xox1tS4Rw6^e^%y(R%%-W;*;cErIuQK!KEOr5AfN#yvN zY3clqhdkM$6BIzna03YiV;)j5uD>X^CTrJE;I~MWNj)iyhT6_M3>qRm9y6 znZs|?an=^42%sQnmY2R>0K7U$f}ElNfKm*1>mo&fsVZ+??kM58)&em=T*gzB!2(l$ zjW|KDd-W&@d?qOoUDElk3^f|ieG=f$%CHa<(XW7QuntN-#7Zp1TZ(1z?>x~jiC)6U2x}N;v1$B?sa@DTzp*aJ@fwJ~I{4?;i{itt zKPs!=VNxSOrfaE4BO-LnJ)`@*85^`b9Y za||9ertA2%=@H#-;dH;!dqiLj3+s(TW@~=SBgw5_nK% zgQ<^GNusxGob~p#Bmo!NN<@3AY=FmupFSHn0=MS@Do~2K?{<;|xJq3Z^s+NwC);yR zD0uv3!<+lGCEKhJEPnfK2^xz=Q=aoR#H~rT=@XZsHT(<3T*;$?Q4xBTY zfHFzy2|`)6h0H-ip=e9OR9eg4!OZ~ehJZ=*1VxwZU{ofiD<~GIf|!Y`OoBb&^Z7Oo uuk2{o^41AFTi;dASs4}(-~4d~oF4%^kTbR1Ynzz>0000Z@e literal 0 HcmV?d00001