-- simple LED marquee mod -- by Vanessa Dannenberg led_marquee = {} led_marquee.scheduled_messages = {} -- settings led_marquee.message_minimum_time = tonumber(minetest.settings:get("led_marquee_message_minimum_time")) or 0.5 led_marquee.message_schedule_dtime = tonumber(minetest.settings:get("led_marquee_message_schedule_dtime")) or 0.2 led_marquee.message_schedule_size = tonumber(minetest.settings:get("led_marquee_message_schedule_size")) or 10 led_marquee.relay_timer = 0 local S = minetest.get_translator(minetest.get_current_modname()) local FS = function(...) return minetest.formspec_escape(S(...)) end local color_to_char = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R" } local char_to_color = { ["0"] = 0, ["1"] = 1, ["2"] = 2, ["3"] = 3, ["4"] = 4, ["5"] = 5, ["6"] = 6, ["7"] = 7, ["8"] = 8, ["9"] = 9, ["A"] = 10, ["B"] = 11, ["C"] = 12, ["D"] = 13, ["E"] = 14, ["F"] = 15, ["G"] = 16, ["H"] = 17, ["I"] = 18, ["J"] = 19, ["K"] = 20, ["L"] = 21, ["M"] = 22, ["N"] = 23, ["O"] = 24, ["P"] = 25, ["Q"] = 26, ["R"] = 27, ["a"] = 10, ["b"] = 11, ["c"] = 12, ["d"] = 13, ["e"] = 14, ["f"] = 15, ["g"] = 16, ["h"] = 17, ["i"] = 18, ["j"] = 19, ["k"] = 20, ["l"] = 21, ["m"] = 22, ["n"] = 23, ["o"] = 24, ["p"] = 25, ["q"] = 26, ["r"] = 27 } -- the following functions based on the so-named ones in Jeija's digilines mod local reset_meta = function(pos) minetest.get_meta(pos):set_string("formspec", "formspec_version[4]".. "size[8,4]".. "button_exit[3,2.5;2,0.5;proceed;"..FS("Proceed").."]".. "field[1.75,1.5;4.5,0.5;channel;"..FS("Channel")..";${channel}]" ) end -- convert Lua's idea of a UTF-8 char to ISO-8859-1 local get_iso = function(c) local hb = string.byte(c,1) or 0 local lb = string.byte(c,2) or 0 local dec = lb+hb*256 local char = dec - 49664 if dec > 49855 then char = dec - 49856 end return char end local make_iso = function(s) local i = 1 local s2 = "" while i <= string.len(s) do if string.byte(s,i) > 159 then local ciso = get_iso(string.sub(s, i, i+1)) if ciso >= 0 and ciso < 256 then s2 = s2..string.char(ciso) else s2 = s2..string.char(127) end i = i + 2 else s2 = s2..string.sub(s, i, i) i = i + 1 end end return s2 end -- scrolling led_marquee.set_timer = function(pos, timeout) local timer = minetest.get_node_timer(pos) timer:stop() if not timeout or timeout < led_marquee.message_minimum_time or timeout > 5 then return false end if timeout > 0 then local meta = minetest.get_meta(pos) meta:set_int("timeout", timeout) timer:start(timeout) end end led_marquee.scroll_text = function(pos, elapsed, skip) skip = skip or 1 local meta = minetest.get_meta(pos) local msg = meta:get_string("last_msg") local channel = meta:get_string("channel") local index = meta:get_int("index") local color = meta:get_int("last_color") local colorchar = color_to_char[color+1] if not index or index < 1 then index = 1 end local len = string.len(msg) index = index + skip if index > len then index = 1 end -- search backward to find the most recent color code in the string local r = index while r > 0 and not string.match(string.sub(msg, r, r+1), "/[0-9A-Ra-r]") do r = r - 1 end if r == 0 then r = 1 end if string.match(string.sub(msg, r, r+1), "/[0-9A-Ra-r]") then colorchar = string.sub(msg, r+1, r+1) end -- search forward to find the next printable symbol after the current index local f = index while f < len do if string.match(string.sub(msg, f-1, f), "/[0-9A-Ra-r]") then f = f + 2 else break end end led_marquee.schedule_msg(pos, channel, "/"..colorchar..string.sub(msg, f)..string.rep(" ", skip + 1)) meta:set_int("index", f) if not elapsed or elapsed < 0.2 then return false end return true end -- the nodes: local fdir_to_right = { { 0, -1 }, { 0, -1 }, { 0, -1 }, { 0, 1 }, { 1, 0 }, { -1, 0 }, } local cbox = { type = "wallmounted", wall_top = { -8/16, 7/16, -8/16, 8/16, 8/16, 8/16 }, wall_bottom = { -8/16, -8/16, -8/16, 8/16, -7/16, 8/16 }, wall_side = { -8/16, -8/16, -8/16, -7/16, 8/16, 8/16 } } led_marquee.decode_color = function(msg) end minetest.register_globalstep(function(dtime) if dtime <= led_marquee.message_schedule_dtime and (#led_marquee.scheduled_messages) > 0 then led_marquee.display_msg( led_marquee.scheduled_messages[1].pos, led_marquee.scheduled_messages[1].channel, led_marquee.scheduled_messages[1].msg ) end table.remove(led_marquee.scheduled_messages, 1) end) led_marquee.schedule_msg = function(pos, channel, msg) local idx = #led_marquee.scheduled_messages led_marquee.scheduled_messages[idx+1] = { pos=pos, channel=channel, msg=msg } if idx >= led_marquee.message_schedule_size then table.remove(led_marquee.scheduled_messages, 1) end end led_marquee.display_msg = function(pos, channel, msg) msg = string.sub(msg, 1, 6144).." " if string.sub(msg,1,1) == string.char(255) then -- treat it as incoming UTF-8 msg = make_iso(string.sub(msg, 2, 6144)) end local master_fdir = minetest.get_node(pos).param2 % 8 local master_meta = minetest.get_meta(pos) local last_color = master_meta:get_int("last_color") local pos2 = table.copy(pos) if not last_color or last_color < 0 or last_color > 27 then last_color = 0 master_meta:set_int("last_color", 0) end local i = 1 local len = string.len(msg) local wrapped = nil while i <= len do local node = minetest.get_node(pos2) local fdir = node.param2 % 8 local meta = minetest.get_meta(pos2) local setchan = nil if meta then setchan = meta:get_string("channel") end local asc = string.byte(msg, i, i) if not string.match(node.name, "led_marquee:char_") then if not wrapped then pos2.x = pos.x pos2.y = pos2.y-1 pos2.z = pos.z wrapped = true else break end elseif string.match(node.name, "led_marquee:char_") and fdir ~= master_fdir or (setchan ~= nil and setchan ~= "" and setchan ~= channel) then break elseif asc == 10 then pos2.x = pos.x pos2.y = pos2.y-1 pos2.z = pos.z i = i + 1 wrapped = nil elseif asc == 29 then local c = string.byte(msg, i+1, i+1) or 0 local r = string.byte(msg, i+2, i+2) or 0 pos2.x = pos.x + (fdir_to_right[fdir+1][1])*c pos2.y = pos.y - r pos2.z = pos.z + (fdir_to_right[fdir+1][2])*c i = i + 3 wrapped = nil elseif asc == 30 then -- translate to slash for printing minetest.swap_node(pos2, { name = "led_marquee:char_47", param2 = master_fdir + (last_color*8)}) pos2.x = pos2.x + fdir_to_right[fdir+1][1] pos2.z = pos2.z + fdir_to_right[fdir+1][2] i = i + 1 elseif asc == 47 then -- slash local ccode = string.sub(msg, i+1, i+1) if ccode then if char_to_color[ccode] then last_color = char_to_color[ccode] i = i + 2 else minetest.swap_node(pos2, { name = "led_marquee:char_47", param2 = master_fdir + (last_color*8)}) pos2.x = pos2.x + fdir_to_right[fdir+1][1] pos2.z = pos2.z + fdir_to_right[fdir+1][2] i = i + 1 end end master_meta:set_int("last_color", last_color) wrapped = nil elseif asc > 30 and asc < 256 then minetest.swap_node(pos2, { name = "led_marquee:char_"..asc, param2 = master_fdir + (last_color*8)}) pos2.x = pos2.x + fdir_to_right[fdir+1][1] pos2.z = pos2.z + fdir_to_right[fdir+1][2] i = i + 1 wrapped = nil else i = i + 1 end end end local on_digiline_receive_string = function(pos, node, channel, msg) local meta = minetest.get_meta(pos) local setchan = meta:get_string("channel") local last_color = meta:get_int("last_color") if not last_color or last_color < 0 or last_color > 27 then last_color = 0 meta:set_int("last_color", 0) end local fdir = node.param2 % 8 if setchan ~= channel then return end if msg and msg ~= "" and type(msg) == "string" then if string.len(msg) > 1 then if msg == "clear" then led_marquee.set_timer(pos, 0) msg = string.rep(" ", 2048) meta:set_string("last_msg", msg) led_marquee.schedule_msg(pos, channel, msg) meta:set_int("index", 1) elseif msg == "allon" then led_marquee.set_timer(pos, 0) msg = string.rep(string.char(144), 2048) meta:set_string("last_msg", msg) led_marquee.schedule_msg(pos, channel, msg) meta:set_int("index", 1) elseif msg == "start_scroll" then local timeout = meta:get_int("timeout") led_marquee.set_timer(pos, timeout) elseif msg == "stop_scroll" then led_marquee.set_timer(pos, 0) return elseif string.sub(msg, 1, 12) == "scroll_speed" then local timeout = tonumber(string.sub(msg, 13)) or 0 led_marquee.set_timer(pos, math.max(timeout, led_marquee.message_minimum_time)) elseif string.sub(msg, 1, 11) == "scroll_step" then local skip = tonumber(string.sub(msg, 12)) led_marquee.scroll_text(pos, nil, skip) elseif msg == "get" then -- get the master panel's displayed char as ASCII numerical value digilines.receptor_send(pos, digilines.rules.default, channel, tonumber(string.match(minetest.get_node(pos).name,"led_marquee:char_(.+)"))) elseif msg == "getstr" then -- get the last stored message digilines.receptor_send(pos, digilines.rules.default, channel, meta:get_string("last_msg")) elseif msg == "getindex" then -- get the scroll index digilines.receptor_send(pos, digilines.rules.default, channel, meta:get_int("index")) else msg = string.gsub(msg, "//", string.char(30)) led_marquee.set_timer(pos, 0) local last_msg = meta:get_string("last_msg") meta:set_string("last_msg", msg) led_marquee.schedule_msg(pos, channel, msg) if last_msg ~= msg then meta:set_int("index", 1) end end else local asc = string.byte(msg) if asc > 30 and asc < 256 then minetest.swap_node(pos, { name = "led_marquee:char_"..asc, param2 = fdir + (last_color*8)}) meta:set_string("last_msg", tostring(msg)) meta:set_int("index", 1) end end elseif msg and type(msg) == "number" then meta:set_string("last_msg", tostring(msg)) led_marquee.schedule_msg(pos, channel, tostring(msg)) meta:set_int("index", 1) end end -- the nodes! for i = 31, 255 do local groups = { cracky = 2, not_in_creative_inventory = 1} local light = minetest.LIGHT_MAX-2 local description = S("LED marquee panel (@1)", i) local leds = "led_marquee_char_"..i..".png^[mask:led_marquee_leds_on.png" if i == 31 then leds ={ name = "led_marquee_char_31.png^[mask:led_marquee_leds_on_cursor.png", animation = {type = "vertical_frames", aspect_w = 32, aspect_h = 32, length = 0.75} } end local wimage if i == 32 then groups = {cracky = 2} light = nil description = S("LED marquee panel") wimage = "led_marquee_leds_off.png^(led_marquee_char_155.png^[multiply:red)" end minetest.register_node("led_marquee:char_"..i, { description = description, drawtype = "mesh", mesh = "led_marquee.obj", tiles = { { name = "led_marquee_base.png", color = "white" }, { name = "led_marquee_leds_off.png", color = "white" } }, overlay_tiles = { "", leds }, inventory_image = wimage, wield_image = wimage, palette="led_marquee_palette.png", use_texture_alpha = "blend", groups = groups, paramtype = "light", paramtype2 = "colorwallmounted", light_source = light, selection_box = cbox, node_box = cbox, on_construct = function(pos) reset_meta(pos) end, on_receive_fields = function(pos, formname, fields, sender) local name = sender:get_player_name() if minetest.is_protected(pos, name) and not minetest.check_player_privs(name, {protection_bypass=true}) then minetest.record_protection_violation(pos, name) return end if (fields.channel) then minetest.get_meta(pos):set_string("channel", fields.channel) end end, digiline = { receptor = {}, effector = { action = on_digiline_receive_string, }, }, drop = "led_marquee:char_32", on_timer = led_marquee.scroll_text }) end -- crafts minetest.register_craft({ output = "led_marquee:char_32 6", recipe = { { "default:glass", "default:glass", "default:glass" }, { "mesecons_lamp:lamp_off", "mesecons_lamp:lamp_off", "mesecons_lamp:lamp_off" }, { "group:wood", "mesecons_microcontroller:microcontroller0000", "group:wood" } }, }) minetest.log("action", "[led_marquee] loaded.")