A simple LED marquee mod, based on my nixie tubes mod
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

478 lines
13KB

  1. -- simple LED marquee mod
  2. -- by Vanessa Dannenberg
  3. led_marquee = {}
  4. led_marquee.scheduled_messages = {}
  5. led_marquee.message_minimum_time = tonumber(minetest.settings:get("led_marquee_message_minimum_time")) or 0.5
  6. led_marquee.message_schedule_dtime = tonumber(minetest.settings:get("led_marquee_message_schedule_dtime")) or 0.2
  7. led_marquee.message_schedule_size = tonumber(minetest.settings:get("led_marquee_message_schedule_size")) or 10
  8. led_marquee.relay_timer = 0
  9. local S
  10. if minetest.get_modpath("intllib") then
  11. S = intllib.make_gettext_pair()
  12. else
  13. S = function(s) return s end
  14. end
  15. local color_to_char = {
  16. "0",
  17. "1",
  18. "2",
  19. "3",
  20. "4",
  21. "5",
  22. "6",
  23. "7",
  24. "8",
  25. "9",
  26. "A",
  27. "B",
  28. "C",
  29. "D",
  30. "E",
  31. "F",
  32. "G",
  33. "H",
  34. "I",
  35. "J",
  36. "K",
  37. "L",
  38. "M",
  39. "N",
  40. "O",
  41. "P",
  42. "Q",
  43. "R"
  44. }
  45. local char_to_color = {
  46. ["0"] = 0,
  47. ["1"] = 1,
  48. ["2"] = 2,
  49. ["3"] = 3,
  50. ["4"] = 4,
  51. ["5"] = 5,
  52. ["6"] = 6,
  53. ["7"] = 7,
  54. ["8"] = 8,
  55. ["9"] = 9,
  56. ["A"] = 10,
  57. ["B"] = 11,
  58. ["C"] = 12,
  59. ["D"] = 13,
  60. ["E"] = 14,
  61. ["F"] = 15,
  62. ["G"] = 16,
  63. ["H"] = 17,
  64. ["I"] = 18,
  65. ["J"] = 19,
  66. ["K"] = 20,
  67. ["L"] = 21,
  68. ["M"] = 22,
  69. ["N"] = 23,
  70. ["O"] = 24,
  71. ["P"] = 25,
  72. ["Q"] = 26,
  73. ["R"] = 27,
  74. ["a"] = 10,
  75. ["b"] = 11,
  76. ["c"] = 12,
  77. ["d"] = 13,
  78. ["e"] = 14,
  79. ["f"] = 15,
  80. ["g"] = 16,
  81. ["h"] = 17,
  82. ["i"] = 18,
  83. ["j"] = 19,
  84. ["k"] = 20,
  85. ["l"] = 21,
  86. ["m"] = 22,
  87. ["n"] = 23,
  88. ["o"] = 24,
  89. ["p"] = 25,
  90. ["q"] = 26,
  91. ["r"] = 27
  92. }
  93. -- the following functions based on the so-named ones in Jeija's digilines mod
  94. local reset_meta = function(pos)
  95. minetest.get_meta(pos):set_string("formspec", "field[channel;Channel;${channel}]")
  96. end
  97. local on_digiline_receive_std = function(pos, node, channel, msg)
  98. local meta = minetest.get_meta(pos)
  99. local setchan = meta:get_string("channel")
  100. if setchan ~= channel then return end
  101. local num = tonumber(msg)
  102. if msg == "colon" or msg == "period" or msg == "off" or (num and (num >= 0 and num <= 9)) then
  103. minetest.swap_node(pos, { name = "led_marquee:marquee_"..msg, param2 = node.param2})
  104. end
  105. end
  106. -- convert Lua's idea of a UTF-8 char to ISO-8859-1
  107. -- first char is non-break space, 0xA0
  108. local iso_chars=" ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"
  109. local get_iso = function(c)
  110. local hb = string.byte(c,1) or 0
  111. local lb = string.byte(c,2) or 0
  112. local dec = lb+hb*256
  113. local char = dec - 49664
  114. if dec > 49855 then char = dec - 49856 end
  115. return char
  116. end
  117. local make_iso = function(s)
  118. local i = 1
  119. local s2 = ""
  120. while i <= string.len(s) do
  121. if string.byte(s,i) > 159 then
  122. local ciso = get_iso(string.sub(s, i, i+1))
  123. if ciso >= 0 and ciso < 256 then
  124. s2 = s2..string.char(ciso)
  125. else
  126. s2 = s2..string.char(127)
  127. end
  128. i = i + 2
  129. else
  130. s2 = s2..string.sub(s, i, i)
  131. i = i + 1
  132. end
  133. end
  134. return s2
  135. end
  136. -- scrolling
  137. led_marquee.set_timer = function(pos, timeout)
  138. local timer = minetest.get_node_timer(pos)
  139. timer:stop()
  140. if not timeout or timeout < led_marquee.message_minimum_time or timeout > 5 then return false end
  141. if timeout > 0 then
  142. local meta = minetest.get_meta(pos)
  143. meta:set_int("timeout", timeout)
  144. timer:start(timeout)
  145. end
  146. end
  147. led_marquee.scroll_text = function(pos, elapsed, skip)
  148. skip = skip or 1
  149. local meta = minetest.get_meta(pos)
  150. local msg = meta:get_string("last_msg")
  151. local channel = meta:get_string("channel")
  152. local index = meta:get_int("index")
  153. local color = meta:get_int("last_color")
  154. local colorchar = color_to_char[color+1]
  155. if not index or index < 1 then index = 1 end
  156. local len = string.len(msg)
  157. index = index + skip
  158. if index > len then index = 1 end
  159. -- search backward to find the most recent color code in the string
  160. local r = index
  161. while r > 0 and not string.match(string.sub(msg, r, r+1), "/[0-9A-Ra-r]") do
  162. r = r - 1
  163. end
  164. if r == 0 then r = 1 end
  165. if string.match(string.sub(msg, r, r+1), "/[0-9A-Ra-r]") then
  166. colorchar = string.sub(msg, r+1, r+1)
  167. end
  168. -- search forward to find the next printable symbol after the current index
  169. local f = index
  170. while f < len do
  171. if string.match(string.sub(msg, f-1, f), "/[0-9A-Ra-r]") then
  172. f = f + 2
  173. else
  174. break
  175. end
  176. end
  177. led_marquee.schedule_msg(pos, channel, "/"..colorchar..string.sub(msg, f)..string.rep(" ", skip + 1))
  178. meta:set_int("index", f)
  179. if not elapsed or elapsed < 0.2 then return false end
  180. return true
  181. end
  182. -- the nodes:
  183. local fdir_to_right = {
  184. { 0, -1 },
  185. { 0, -1 },
  186. { 0, -1 },
  187. { 0, 1 },
  188. { 1, 0 },
  189. { -1, 0 },
  190. }
  191. local cbox = {
  192. type = "wallmounted",
  193. wall_top = { -8/16, 7/16, -8/16, 8/16, 8/16, 8/16 },
  194. wall_bottom = { -8/16, -8/16, -8/16, 8/16, -7/16, 8/16 },
  195. wall_side = { -8/16, -8/16, -8/16, -7/16, 8/16, 8/16 }
  196. }
  197. led_marquee.decode_color = function(msg)
  198. end
  199. minetest.register_globalstep(function(dtime)
  200. if dtime <= led_marquee.message_schedule_dtime
  201. and (#led_marquee.scheduled_messages) > 0 then
  202. led_marquee.display_msg(
  203. led_marquee.scheduled_messages[1].pos,
  204. led_marquee.scheduled_messages[1].channel,
  205. led_marquee.scheduled_messages[1].msg
  206. )
  207. end
  208. table.remove(led_marquee.scheduled_messages, 1)
  209. end)
  210. led_marquee.schedule_msg = function(pos, channel, msg)
  211. local idx = #led_marquee.scheduled_messages
  212. led_marquee.scheduled_messages[idx+1] = { pos=pos, channel=channel, msg=msg }
  213. if idx >= led_marquee.message_schedule_size then
  214. table.remove(led_marquee.scheduled_messages, 1)
  215. end
  216. end
  217. led_marquee.display_msg = function(pos, channel, msg)
  218. msg = string.sub(msg, 1, 6144).." "
  219. if string.sub(msg,1,1) == string.char(255) then -- treat it as incoming UTF-8
  220. msg = make_iso(string.sub(msg, 2, 6144))
  221. end
  222. local master_fdir = minetest.get_node(pos).param2 % 8
  223. local master_meta = minetest.get_meta(pos)
  224. local last_color = master_meta:get_int("last_color")
  225. local pos2 = table.copy(pos)
  226. if not last_color or last_color < 0 or last_color > 27 then
  227. last_color = 0
  228. master_meta:set_int("last_color", 0)
  229. end
  230. local i = 1
  231. local len = string.len(msg)
  232. local wrapped = nil
  233. while i <= len do
  234. local node = minetest.get_node(pos2)
  235. local fdir = node.param2 % 8
  236. local meta = minetest.get_meta(pos2)
  237. local setchan = nil
  238. if meta then setchan = meta:get_string("channel") end
  239. local asc = string.byte(msg, i, i)
  240. if not string.match(node.name, "led_marquee:char_") then
  241. if not wrapped then
  242. pos2.x = pos.x
  243. pos2.y = pos2.y-1
  244. pos2.z = pos.z
  245. wrapped = true
  246. else
  247. break
  248. end
  249. elseif string.match(node.name, "led_marquee:char_")
  250. and fdir ~= master_fdir or (setchan ~= nil and setchan ~= "" and setchan ~= channel) then
  251. break
  252. elseif asc == 10 then
  253. pos2.x = pos.x
  254. pos2.y = pos2.y-1
  255. pos2.z = pos.z
  256. i = i + 1
  257. wrapped = nil
  258. elseif asc == 29 then
  259. local c = string.byte(msg, i+1, i+1) or 0
  260. local r = string.byte(msg, i+2, i+2) or 0
  261. pos2.x = pos.x + (fdir_to_right[fdir+1][1])*c
  262. pos2.y = pos.y - r
  263. pos2.z = pos.z + (fdir_to_right[fdir+1][2])*c
  264. i = i + 3
  265. wrapped = nil
  266. elseif asc == 30 then -- translate to slash for printing
  267. minetest.swap_node(pos2, { name = "led_marquee:char_47", param2 = master_fdir + (last_color*8)})
  268. pos2.x = pos2.x + fdir_to_right[fdir+1][1]
  269. pos2.z = pos2.z + fdir_to_right[fdir+1][2]
  270. i = i + 1
  271. elseif asc == 47 then -- slash
  272. local ccode = string.sub(msg, i+1, i+1)
  273. if ccode then
  274. if char_to_color[ccode] then
  275. last_color = char_to_color[ccode]
  276. i = i + 2
  277. else
  278. minetest.swap_node(pos2, { name = "led_marquee:char_47", param2 = master_fdir + (last_color*8)})
  279. pos2.x = pos2.x + fdir_to_right[fdir+1][1]
  280. pos2.z = pos2.z + fdir_to_right[fdir+1][2]
  281. i = i + 1
  282. end
  283. end
  284. master_meta:set_int("last_color", last_color)
  285. wrapped = nil
  286. elseif asc > 30 and asc < 256 then
  287. minetest.swap_node(pos2, { name = "led_marquee:char_"..asc, param2 = master_fdir + (last_color*8)})
  288. pos2.x = pos2.x + fdir_to_right[fdir+1][1]
  289. pos2.z = pos2.z + fdir_to_right[fdir+1][2]
  290. i = i + 1
  291. wrapped = nil
  292. else
  293. i = i + 1
  294. end
  295. end
  296. end
  297. local on_digiline_receive_string = function(pos, node, channel, msg)
  298. local meta = minetest.get_meta(pos)
  299. local setchan = meta:get_string("channel")
  300. local last_color = meta:get_int("last_color")
  301. if not last_color or last_color < 0 or last_color > 27 then
  302. last_color = 0
  303. meta:set_int("last_color", 0)
  304. end
  305. local fdir = node.param2 % 8
  306. if setchan ~= channel then return end
  307. if msg and msg ~= "" and type(msg) == "string" then
  308. if string.len(msg) > 1 then
  309. if msg == "clear" then
  310. led_marquee.set_timer(pos, 0)
  311. msg = string.rep(" ", 2048)
  312. meta:set_string("last_msg", msg)
  313. led_marquee.schedule_msg(pos, channel, msg)
  314. meta:set_int("index", 1)
  315. elseif msg == "allon" then
  316. led_marquee.set_timer(pos, 0)
  317. msg = string.rep(string.char(144), 2048)
  318. meta:set_string("last_msg", msg)
  319. led_marquee.schedule_msg(pos, channel, msg)
  320. meta:set_int("index", 1)
  321. elseif msg == "start_scroll" then
  322. local timeout = meta:get_int("timeout")
  323. led_marquee.set_timer(pos, timeout)
  324. elseif msg == "stop_scroll" then
  325. led_marquee.set_timer(pos, 0)
  326. return
  327. elseif string.sub(msg, 1, 12) == "scroll_speed" then
  328. local timeout = tonumber(string.sub(msg, 13))
  329. led_marquee.set_timer(pos, math.max(timeout, led_marquee.message_minimum_time))
  330. elseif string.sub(msg, 1, 11) == "scroll_step" then
  331. local skip = tonumber(string.sub(msg, 12))
  332. led_marquee.scroll_text(pos, nil, skip)
  333. elseif msg == "get" then -- get the master panel's displayed char as ASCII numerical value
  334. digilines.receptor_send(pos, digiline.rules.default, channel, tonumber(string.match(minetest.get_node(pos).name,"led_marquee:char_(.+)"))) -- wonderfully horrible string manipulaiton
  335. elseif msg == "getstr" then -- get the last stored message
  336. digilines.receptor_send(pos, digiline.rules.default, channel, meta:get_string("last_msg"))
  337. elseif msg == "getindex" then -- get the scroll index
  338. digilines.receptor_send(pos, digiline.rules.default, channel, meta:get_int("index"))
  339. else
  340. msg = string.gsub(msg, "//", string.char(30))
  341. led_marquee.set_timer(pos, 0)
  342. local last_msg = meta:get_string("last_msg")
  343. meta:set_string("last_msg", msg)
  344. led_marquee.schedule_msg(pos, channel, msg)
  345. if last_msg ~= msg then
  346. meta:set_int("index", 1)
  347. end
  348. end
  349. else
  350. local asc = string.byte(msg)
  351. if asc > 29 and asc < 256 then
  352. minetest.swap_node(pos, { name = "led_marquee:char_"..asc, param2 = fdir + (last_color*8)})
  353. meta:set_string("last_msg", tostring(msg))
  354. meta:set_int("index", 1)
  355. end
  356. end
  357. elseif msg and type(msg) == "number" then
  358. meta:set_string("last_msg", tostring(msg))
  359. led_marquee.schedule_msg(pos, channel, tostring(msg))
  360. meta:set_int("index", 1)
  361. end
  362. end
  363. -- the nodes!
  364. for i = 31, 255 do
  365. local groups = { cracky = 2, not_in_creative_inventory = 1}
  366. local light = LIGHT_MAX-2
  367. local description = S("LED marquee panel ("..i..")")
  368. local leds = "led_marquee_char_"..i..".png^[mask:led_marquee_leds_on.png"
  369. if i == 31 then
  370. leds ={
  371. name = "led_marquee_char_31.png^[mask:led_marquee_leds_on_cursor.png",
  372. animation = {type = "vertical_frames", aspect_w = 32, aspect_h = 32, length = 0.75}
  373. }
  374. end
  375. local wimage
  376. if i == 32 then
  377. groups = {cracky = 2}
  378. light = nil
  379. description = S("LED marquee panel")
  380. wimage = "led_marquee_leds_off.png^(led_marquee_char_155.png^[multiply:red)"
  381. end
  382. minetest.register_node("led_marquee:char_"..i, {
  383. description = description,
  384. drawtype = "mesh",
  385. mesh = "led_marquee.obj",
  386. tiles = {
  387. { name = "led_marquee_base.png", color = "white" },
  388. { name = "led_marquee_leds_off.png", color = "white" }
  389. },
  390. overlay_tiles = { "", leds },
  391. inventory_image = wimage,
  392. wield_image = wimage,
  393. palette="led_marquee_palette.png",
  394. use_texture_alpha = true,
  395. groups = groups,
  396. paramtype = "light",
  397. paramtype2 = "colorwallmounted",
  398. light_source = light,
  399. selection_box = cbox,
  400. node_box = cbox,
  401. on_construct = function(pos)
  402. reset_meta(pos)
  403. end,
  404. on_receive_fields = function(pos, formname, fields, sender)
  405. local name = sender:get_player_name()
  406. if minetest.is_protected(pos, name) and not minetest.check_player_privs(name, {protection_bypass=true}) then
  407. minetest.record_protection_violation(pos, name)
  408. return
  409. end
  410. if (fields.channel) then
  411. minetest.get_meta(pos):set_string("channel", fields.channel)
  412. end
  413. end,
  414. digiline = {
  415. receptor = {},
  416. effector = {
  417. action = on_digiline_receive_string,
  418. },
  419. },
  420. drop = "led_marquee:char_32",
  421. on_timer = led_marquee.scroll_text
  422. })
  423. end
  424. -- crafts
  425. minetest.register_craft({
  426. output = "led_marquee:char_32 6",
  427. recipe = {
  428. { "default:glass", "default:glass", "default:glass" },
  429. { "mesecons_lamp:lamp_off", "mesecons_lamp:lamp_off", "mesecons_lamp:lamp_off" },
  430. { "group:wood", "mesecons_microcontroller:microcontroller0000", "group:wood" }
  431. },
  432. })
  433. minetest.log("action", "[led_marquee] loaded.")