--[[ ======================================================================= SmartLine Modules Mod by Micu (c) 2018, 2019 Crops Watcher Advanced optical device to assist in crop farming automation. It scans rectangular area of selected radius for crops (wheat, tomatoes etc) and signals if field is ready for harvest. Device recognizes all registered farming nodes. Crop Watcher sees plants at its level and down to 2 levels below node (0, -1, -2), with exception of nodes directly under its box. Configuration options are accessible via panel: ID numbers to send messages to, scan radius (1-16) and minimum number of crops for area to be considered a valid crop field (0 to maximum field area). When Tubelib ID numbers are entered, scan can be initiated by sending "on" message to Watcher (by Timer, Button etc) - if field is ready to harvest, device immediately responses with "on" command sent to specified IDs (for example Tubelib Harvester). No messages are sent for other states - Crops Watcher never sends "off" commands to not interfere with machinery automation. Event-based node - it does not use node timer. Placement: place in the center of the field, up to 2 nodes above ground level. Status: - "error" - there are no crops in the area or they fall below defined minimum - "growing" - there are enough crops in the area, some of them are still growing - "ready" - there are enough crops in the area and all are ready for harvest Events (optional): - "on" - sent when device received "on" message, scanned area and result is "ready" Supported SaferLua functions: - $get_status(...) - $get_crops_status(...) Punch node to see current status and crop numbers. License: LGPLv2.1+ ======================================================================= ]]-- --[[ --------------- Local variables --------------- ]]-- -- range constants local RADIUS_MIN = 1 local RADIUS_MAX = 16 local LEVEL_TOP = 0 local LEVEL_BOTTOM = -2 -- states local CW_ERROR = 1 local CW_GROWING = 2 local CW_READY = 3 local cwstate = { [CW_ERROR] = "error", [CW_GROWING] = "growing", [CW_READY] = "ready" } -- tables with farming crop nodes local cropnames = {} local isgrowncrop = {} for _, c in pairs(farming.registered_plants) do for i = 1, c.steps do cropnames[#cropnames + 1] = c.crop .. "_" .. i end isgrowncrop[c.crop .. "_" .. c.steps] = true end --[[ ------- Helpers ------- ]]-- -- get status for crops area for watcher at pos -- return 1: CW_ERROR for not enough growable crops, -- CW_GROWING for crops in growing state, -- CW_READY if all crops are ready for harvest -- return 2: total number of crops in area -- return 3: grown crops in area local function get_cropswatcher_area_state(pos) local meta = minetest.get_meta(pos) local radius = meta:get_int("radius") or 8 local cropsmin = meta:get_int("crops_min") or 0 local _, crops = minetest.find_nodes_in_area( vector.add(pos, { x = -radius, y = LEVEL_BOTTOM, z = -radius }), vector.add(pos, { x = radius, y = LEVEL_TOP, z = radius }), cropnames) local _, subcrops = minetest.find_nodes_in_area( vector.add(pos, { x = 0, y = LEVEL_BOTTOM, z = 0 }), vector.add(pos, { x = 0, y = LEVEL_TOP, z = 0 }), cropnames) local crnum = 0 local grown = 0 for c, n in pairs(crops) do crnum = crnum + n if isgrowncrop[c] then grown = grown + n end end -- do not count nodes at the same (x, z) coordinates for c, n in pairs(subcrops) do crnum = crnum - n if isgrowncrop[c] then grown = grown - n end end if (cropsmin == 0 and crnum == 0) or (cropsmin > 0 and crnum < cropsmin) then return CW_ERROR, crnum, grown elseif grown == crnum then return CW_READY, crnum, grown end return CW_GROWING, crnum, grown end -- calculate max number of crops in area: -- note: (2*r+1)^2-1 = 4*r*(r+1) local function get_max_crops(radius) return 4 * radius * (radius + 1) end --[[ -------- Formspec -------- ]]-- -- configuration formspec local function formspec(meta, opt_radius, opt_cropsmin) local number = meta:get_string("own_num") local radius = opt_radius or meta:get_int("radius") or 8 local cropsmin = opt_cropsmin or meta:get_int("crops_min") or 0 local cropsmax = get_max_crops(radius) cropsmin = math.min(cropsmin, cropsmax) local radiuslist = "" for i = RADIUS_MIN, RADIUS_MAX do radiuslist = radiuslist .. i if i < RADIUS_MAX then radiuslist = radiuslist .. "," end end return "size[8.4,3.6]" .. "label[3,0;" .. minetest.colorize("#FFFF00", "Crops Watcher ") .. minetest.colorize("#00FFFF", number) .. "]" .. "label[0,1;Enter destination number(s) (optional)]" .. "field[4,1.1;4.5,1;numbers;;${numbers}]" .. "field_close_on_enter[numbers;true]" .. "label[0,2;Select radius]" .. "dropdown[1.5,1.85;0.75;radius;" .. radiuslist .. ";" .. radius .. "]" .. "label[2.8,2;Minimal number of crops in area (0 - " .. cropsmax .. ")]" .. "field[7.2,2.1;1.3,1;cropsmin;;" .. cropsmin .. "]" .. "field_close_on_enter[cropsmin;true]" .. "button_exit[5.2,2.8;1.5,1;ok;OK]" .. "button_exit[6.8,2.8;1.5,1;cancel;Cancel]" end --[[ ----------------- Node registration ----------------- ]]-- minetest.register_node("slmodules:cropswatcher", { description = "SmartLine Crops Watcher", tiles = { -- up, down, right, left, back, front "slmodules_default_plate.png", "slmodules_default_plate.png", "slmodules_cropswatcher_side.png", "slmodules_cropswatcher_side.png", "slmodules_cropswatcher_side.png", "slmodules_cropswatcher_side.png", }, after_place_node = function(pos, placer, itemstack, pointed_thing) local meta = minetest.get_meta(pos) local number = tubelib.add_node(pos, "slmodules:cropswatcher") meta:set_string("own_num", number) meta:set_string("infotext", "Crops Watcher " .. number) meta:set_string("owner", placer:get_player_name()) meta:set_string("numbers", "") meta:set_int("radius", 8) meta:set_int("crops_min", 0) meta:set_string("formspec", formspec(meta)) end, can_dig = function(pos, player) if minetest.is_protected(pos, player:get_player_name()) then return false end return true end, after_dig_node = function(pos, oldnode, oldmetadata, digger) tubelib.remove_node(pos) end, on_punch = function(pos, node, puncher, pointed_thing) local meta = minetest.get_meta(pos) local player_name = puncher:get_player_name() if meta:get_string("owner") ~= player_name then return false end local sn, cn, gn = get_cropswatcher_area_state(pos) local msgclr = { ["error"] = "#FFBFBF", ["growing"] = "#FFFFBF", ["ready"] = "#BFFFBF" } minetest.chat_send_player(player_name, minetest.colorize("#FFFF00", "[CropsWatcher:" .. meta:get_string("own_num") .. "]") .. " Status is " .. minetest.colorize(msgclr[cwstate[sn]], "\"" .. cwstate[sn] .. "\"") .. ", total crops: " .. cn .. ", fully grown: " .. gn) return true end, on_receive_fields = function(pos, formname, fields, sender) if minetest.is_protected(pos, sender:get_player_name()) then return end local meta = minetest.get_meta(pos) if fields.ok or fields.key_enter_field == "numbers" or fields.key_enter_field == "cropsmin" then local number = meta:get_string("own_num") local numbers = fields.numbers and fields.numbers:trim() or "" local cropsmin = tonumber(fields.cropsmin and fields.cropsmin:trim() or "0") or 0 local radius = tonumber(fields.radius) cropsmin = math.max(math.min(cropsmin, get_max_crops(radius)), 0) meta:set_string("numbers", numbers) meta:set_int("radius", radius) meta:set_int("crops_min", cropsmin) meta:set_string("infotext", "Crops Watcher " .. number .. (numbers ~= "" and " (connected with: " .. numbers .. ")" or "")) meta:set_string("formspec", formspec(meta)) elseif fields.radius and not fields.quit then local radius = tonumber(fields.radius) meta:set_string("formspec", formspec(meta, radius, math.max(math.min(fields.cropsmin, get_max_crops(radius)), 0))) elseif fields.cancel or fields.quit then meta:set_string("formspec", formspec(meta)) end end, on_rotate = screwdriver.disallow, paramtype = "light", sunlight_propagates = true, paramtype2 = "facedir", groups = { choppy = 2, cracky = 2, crumbly = 2 }, is_ground_content = false, sounds = default.node_sound_metal_defaults(), }) tubelib.register_node("slmodules:cropswatcher", {}, { on_recv_message = function(pos, topic, payload) if topic == "on" then local meta = minetest.get_meta(pos) local owner = meta:get_string("owner") local number = meta:get_string("own_num") local numbers = meta:get_string("numbers") or "" if numbers ~= "" and get_cropswatcher_area_state(pos) == CW_READY then -- send message: topic = "on", payload = "NUMBER" tubelib.send_message(numbers, owner, owner, "on", number) end return true elseif topic == "state" then return cwstate[get_cropswatcher_area_state(pos)] elseif topic == "crops" then local sn, cn, gn = get_cropswatcher_area_state(pos) return { cwstate[sn], cn, gn } else return "unsupported" end end, }) --[[ -------- Crafting -------- ]]-- minetest.register_craft({ output = "slmodules:cropswatcher", type = "shaped", recipe = { { "default:steelblock", "default:copper_ingot", "default:steelblock" }, { "default:glass", "default:diamond", "default:mese_crystal" }, { "group:wood", "tubelib:wlanchip", "group:wood" }, }, }) --[[ -------- SaferLua -------- ]]-- sl_controller.register_action("get_crops_status", { cmnd = function(self, num) num = tostring(num or "") return unpack(tubelib.send_request(num, "crops", nil) or { "", "", "" }) end, help = " $get_crops_status(num)\n" .. " Read Crops Watcher device state.\n" .. " Function returns 3 values:\n" .. ' 1: "error", "growing" or "ready"\n' .. " 2: total number of crops\n" .. " 3: number of fully grown crops\n" .. ' example: state, total, grown = $get_crops_status("1234")\n' })