local S = minetest.get_translator("mcl_signs") local F = minetest.formspec_escape -- Load the characters map (characters.txt) --[[ File format of characters.txt: It's an UTF-8 encoded text file that contains metadata for all supported characters. It contains a sequence of info blocks, one for each character. Each info block is made out of 3 lines: Line 1: The literal UTF-8 encoded character Line 2: Name of the texture file for this character minus the “.png” suffix; found in the “textures/” sub-directory Line 3: Currently ignored. Previously this was for the character width in pixels After line 3, another info block may follow. This repeats until the end of the file. All character files must be 5 or 6 pixels wide (5 pixels are preferred) ]] local chars_file = io.open(minetest.get_modpath("mcl_signs").."/characters.txt", "r") -- FIXME: Support more characters (many characters are missing). Currently ASCII and Latin-1 Supplement are supported. local charmap = {} if not chars_file then minetest.log("error", "[mcl_signs] : character map file not found") else while true do local char = chars_file:read("*l") if char == nil then break end local img = chars_file:read("*l") chars_file:read("*l") charmap[char] = img end end -- CONSTANTS local SIGN_WIDTH = 115 local LINE_LENGTH = 15 local NUMBER_OF_LINES = 4 local LINE_HEIGHT = 14 local CHAR_WIDTH = 5 -- Helper functions local function round(num, idp) local mult = 10^(idp or 0) return math.floor(num * mult + 0.5) / mult end local string_to_array = function(str) local tab = {} for i=1,string.len(str) do table.insert(tab, string.sub(str, i,i)) end return tab end local string_to_line_array = function(str) local tab = {} local current = 1 local linechar = 1 tab[1] = "" for _,char in ipairs(string_to_array(str)) do -- New line if char == "\n" then current = current + 1 tab[current] = "" linechar = 1 else tab[current] = tab[current]..char linechar = linechar + 1 end end return tab end local create_lines = function(text) local line_num = 1 local tab = {} for _, line in ipairs(string_to_line_array(text)) do if line_num > NUMBER_OF_LINES then break end table.insert(tab, line) line_num = line_num + 1 end return tab end local generate_line = function(s, ypos) local i = 1 local parsed = {} local width = 0 local chars = 0 local printed_char_width = CHAR_WIDTH + 1 while chars < LINE_LENGTH and i <= #s do local file = nil -- Get and render character if charmap[s:sub(i, i)] ~= nil then file = charmap[s:sub(i, i)] i = i + 1 elseif i < #s and charmap[s:sub(i, i + 1)] ~= nil then file = charmap[s:sub(i, i + 1)] i = i + 2 else -- No character image found. -- Use replacement character: file = "_rc" i = i + 1 minetest.log("verbose", "[mcl_signs] Unknown symbol in '"..s.."' at "..i) end if file ~= nil then width = width + printed_char_width table.insert(parsed, file) chars = chars + 1 end end width = width - 1 local texture = "" local xpos = math.floor((SIGN_WIDTH - width) / 2) for i = 1, #parsed do texture = texture..":"..xpos..","..ypos.."="..parsed[i]..".png" xpos = xpos + printed_char_width end return texture end local generate_texture = function(lines, signnodename) local texture = "[combine:"..SIGN_WIDTH.."x"..SIGN_WIDTH local ypos if signnodename == "mcl_signs:wall_sign" then ypos = 30 else ypos = 0 end for i = 1, #lines do texture = texture..generate_line(lines[i], ypos) ypos = ypos + LINE_HEIGHT end return texture end local n = 23/56 - 1/128 local signtext_info_wall = { {delta = {x = 0, y = 0, z = n}, yaw = 0}, {delta = {x = n, y = 0, z = 0}, yaw = math.pi / -2}, {delta = {x = 0, y = 0, z = -n}, yaw = math.pi}, {delta = {x = -n, y = 0, z = 0}, yaw = math.pi / 2}, } local signtext_info_standing = {} local m = -1/16 + 1/64 for rot=0, 15 do local yaw = math.pi*2 - (((math.pi*2) / 16) * rot) local delta = vector.multiply(minetest.yaw_to_dir(yaw), m) -- Offset because sign is a bit above node boundaries delta.y = delta.y + 2/28 table.insert(signtext_info_standing, { delta = delta, yaw = yaw }) end local function get_rotation_level(facedir, nodename) local rl = facedir * 4 if nodename == "mcl_signs:standing_sign22_5" then rl = rl + 1 elseif nodename == "mcl_signs:standing_sign45" then rl = rl + 2 elseif nodename == "mcl_signs:standing_sign67_5" then rl = rl + 3 end return rl end local function get_wall_signtext_info(param2, nodename) local dir = minetest.wallmounted_to_dir(param2) if dir.x > 0 then return 2 elseif dir.z > 0 then return 1 elseif dir.x < 0 then return 4 else return 3 end end local sign_groups = {handy=1,axey=1, flammable=1, deco_block=1, material_wood=1, attached_node=1, dig_by_piston=1} local destruct_sign = function(pos) local objects = minetest.get_objects_inside_radius(pos, 0.5) for _, v in ipairs(objects) do local ent = v:get_luaentity() if ent and ent.name == "mcl_signs:text" then v:remove() end end local players = minetest.get_connected_players() for p=1, #players do minetest.close_formspec(players[p]:get_player_name(), "mcl_signs:set_text_"..pos.x.."_"..pos.y.."_"..pos.z) end end local update_sign = function(pos, fields, sender, force_remove) local meta = minetest.get_meta(pos) if not meta then return end local text = meta:get_string("text") if fields and (text == "" and fields.text) then meta:set_string("text", fields.text) text = fields.text end if text == nil then text = "" end local sign_info local n = minetest.get_node(pos) local nn = n.name if nn == "mcl_signs:standing_sign" or nn == "mcl_signs:standing_sign22_5" or nn == "mcl_signs:standing_sign45" or nn == "mcl_signs:standing_sign67_5" then sign_info = signtext_info_standing[get_rotation_level(n.param2, nn) + 1] elseif nn == "mcl_signs:wall_sign" then sign_info = signtext_info_wall[get_wall_signtext_info(n.param2)] end if sign_info == nil then minetest.log("error", "[mcl_signs] Missing sign_info!") return end local objects = minetest.get_objects_inside_radius(pos, 0.5) local text_entity for _, v in ipairs(objects) do local ent = v:get_luaentity() if ent and ent.name == "mcl_signs:text" then if force_remove then v:remove() else text_entity = v break end end end if not text_entity then text_entity = minetest.add_entity({ x = pos.x + sign_info.delta.x, y = pos.y + sign_info.delta.y, z = pos.z + sign_info.delta.z}, "mcl_signs:text") end text_entity:get_luaentity()._signnodename = nn text_entity:set_properties({textures={generate_texture(create_lines(text), nn)}}) text_entity:set_yaw(sign_info.yaw) end local show_formspec = function(player, pos) minetest.show_formspec( player:get_player_name(), "mcl_signs:set_text_"..pos.x.."_"..pos.y.."_"..pos.z, "size[6,3]textarea[0.25,0.25;6,1.5;text;"..F(S("Enter sign text:"))..";]label[0,1.5;"..F(S("Maximum line length: 15")).."\n"..F(S("Maximum lines: 4")).."]button_exit[0,2.5;6,1;submit;"..F(S("Done")).."]" ) end minetest.register_on_player_receive_fields(function(player, formname, fields) if formname:find("mcl_signs:set_text_") == 1 then local x, y, z = formname:match("mcl_signs:set_text_(.-)_(.-)_(.*)") local pos = {x=tonumber(x), y=tonumber(y), z=tonumber(z)} if not pos or not pos.x or not pos.y or not pos.z then return end update_sign(pos, fields, player) end end) local node_sounds if minetest.get_modpath("mcl_sounds") then node_sounds = mcl_sounds.node_sound_wood_defaults() end minetest.register_node("mcl_signs:wall_sign", { description = S("Sign"), _tt_help = S("Can be written"), _doc_items_longdesc = S("Signs can be written and come in two variants: Wall sign and sign on a sign post. Signs can be placed on the top and the sides of other blocks, but not below them."), _doc_items_usagehelp = S("After placing the sign, you can write something on it. You have 4 lines of text with up to 15 characters for each line; anything beyond these limits is lost. Not all characters are supported. The text can not be changed once it has been written; you have to break and place the sign again."), inventory_image = "default_sign.png", walkable = false, is_ground_content = false, wield_image = "default_sign.png", node_placement_prediction = "", paramtype = "light", sunlight_propagates = true, paramtype2 = "wallmounted", drawtype = "mesh", mesh = "mcl_signs_signonwallmount.obj", selection_box = {type = "wallmounted", wall_side = {-0.5, -7/28, -0.5, -23/56, 7/28, 0.5}}, tiles = {"mcl_signs_sign.png"}, groups = sign_groups, stack_max = 16, sounds = node_sounds, on_place = function(itemstack, placer, pointed_thing) local above = pointed_thing.above local under = pointed_thing.under -- Use pointed node's on_rightclick function first, if present local node_under = minetest.get_node(under) if placer and not placer:get_player_control().sneak then if minetest.registered_nodes[node_under.name] and minetest.registered_nodes[node_under.name].on_rightclick then return minetest.registered_nodes[node_under.name].on_rightclick(under, node_under, placer, itemstack) or itemstack end end local dir = vector.subtract(under, above) -- Only build when it's legal local abovenodedef = minetest.registered_nodes[minetest.get_node(above).name] if not abovenodedef or abovenodedef.buildable_to == false then return itemstack end local wdir = minetest.dir_to_wallmounted(dir) local placer_pos = placer:get_pos() local fdir = minetest.dir_to_facedir(dir) local sign_info local nodeitem = ItemStack(itemstack) -- Ceiling if wdir == 0 then --how would you add sign to ceiling? return itemstack -- Floor elseif wdir == 1 then -- Standing sign -- Determine the sign rotation based on player's yaw local yaw = math.pi*2 - placer:get_look_horizontal() -- Select one of 16 possible rotations (0-15) local rotation_level = round((yaw / (math.pi*2)) * 16) if rotation_level > 15 then rotation_level = 0 elseif rotation_level < 0 then rotation_level = 15 end -- The actual rotation is a combination of predefined mesh and facedir (see node definition) if rotation_level % 4 == 0 then nodeitem:set_name("mcl_signs:standing_sign") elseif rotation_level % 4 == 1 then nodeitem:set_name("mcl_signs:standing_sign22_5") elseif rotation_level % 4 == 2 then nodeitem:set_name("mcl_signs:standing_sign45") elseif rotation_level % 4 == 3 then nodeitem:set_name("mcl_signs:standing_sign67_5") end fdir = math.floor(rotation_level / 4) -- Place the node! local _, success = minetest.item_place_node(nodeitem, placer, pointed_thing, fdir) if not success then return itemstack end if not minetest.settings:get_bool("creative_mode") then itemstack:take_item() end sign_info = signtext_info_standing[rotation_level + 1] -- Side else -- Wall sign local _, success = minetest.item_place_node(itemstack, placer, pointed_thing, wdir) if not success then return itemstack end sign_info = signtext_info_wall[fdir + 1] end -- Determine spawn position of entity local place_pos if minetest.registered_nodes[node_under.name].buildable_to then place_pos = under else place_pos = above end local text_entity = minetest.add_entity({ x = place_pos.x + sign_info.delta.x, y = place_pos.y + sign_info.delta.y, z = place_pos.z + sign_info.delta.z}, "mcl_signs:text") text_entity:set_yaw(sign_info.yaw) text_entity:get_luaentity()._signnodename = nodeitem:get_name() minetest.sound_play({name="default_place_node_hard", gain=1.0}, {pos = place_pos}, true) show_formspec(placer, place_pos) return itemstack end, on_destruct = destruct_sign, on_punch = function(pos, node, puncher) update_sign(pos) end, on_rotate = function(pos, node, user, mode) if mode == screwdriver.ROTATE_FACE then local r = screwdriver.rotate.wallmounted(pos, node, mode) node.param2 = r minetest.swap_node(pos, node) update_sign(pos, nil, nil, true) return true else return false end end, _mcl_hardness = 1, _mcl_blast_resistance = 1, }) -- Standing sign nodes. -- 4 rotations at 0°, 22.5°, 45° and 67.5°. -- These are 4 out of 16 possible rotations. -- With facedir the remaining 12 rotations are constructed. -- 0° local ssign = { paramtype = "light", sunlight_propagates = true, walkable = false, is_ground_content = false, paramtype2 = "facedir", drawtype = "mesh", mesh = "mcl_signs_sign.obj", selection_box = {type = "fixed", fixed = {-0.2, -0.5, -0.2, 0.2, 0.5, 0.2}}, tiles = {"mcl_signs_sign.png"}, groups = sign_groups, drop = "mcl_signs:wall_sign", stack_max = 16, sounds = node_sounds, on_destruct = destruct_sign, on_punch = function(pos, node, puncher) update_sign(pos) end, on_rotate = function(pos, node, user, mode) if mode == screwdriver.ROTATE_FACE then node.name = "mcl_signs:standing_sign22_5" minetest.swap_node(pos, node) elseif mode == screwdriver.ROTATE_AXIS then return false end update_sign(pos, nil, nil, true) return true end, _mcl_hardness = 1, _mcl_blast_resistance = 1, } minetest.register_node("mcl_signs:standing_sign", ssign) -- 22.5° local ssign22_5 = table.copy(ssign) ssign22_5.mesh = "mcl_signs_sign22.5.obj" ssign22_5.on_rotate = function(pos, node, user, mode) if mode == screwdriver.ROTATE_FACE then node.name = "mcl_signs:standing_sign45" minetest.swap_node(pos, node) elseif mode == screwdriver.ROTATE_AXIS then return false end update_sign(pos, nil, nil, true) return true end minetest.register_node("mcl_signs:standing_sign22_5", ssign22_5) -- 45° local ssign45 = table.copy(ssign) ssign45.mesh = "mcl_signs_sign45.obj" ssign45.on_rotate = function(pos, node, user, mode) if mode == screwdriver.ROTATE_FACE then node.name = "mcl_signs:standing_sign67_5" minetest.swap_node(pos, node) elseif mode == screwdriver.ROTATE_AXIS then return false end update_sign(pos, nil, nil, true) return true end minetest.register_node("mcl_signs:standing_sign45", ssign45) -- 67.5° local ssign67_5 = table.copy(ssign) ssign67_5.mesh = "mcl_signs_sign67.5.obj" ssign67_5.on_rotate = function(pos, node, user, mode) if mode == screwdriver.ROTATE_FACE then node.name = "mcl_signs:standing_sign" node.param2 = (node.param2 + 1) % 4 minetest.swap_node(pos, node) elseif mode == screwdriver.ROTATE_AXIS then return false end update_sign(pos, nil, nil, true) return true end minetest.register_node("mcl_signs:standing_sign67_5", ssign67_5) -- FIXME: Prevent entity destruction by /clearobjects minetest.register_entity("mcl_signs:text", { pointable = false, visual = "upright_sprite", textures = {}, physical = false, collide_with_objects = false, _signnodename = nil, -- node name of sign node to which the text belongs on_activate = function(self, staticdata) if staticdata ~= nil and staticdata ~= "" then local des = minetest.deserialize(staticdata) if des then self._signnodename = des._signnodename end end local meta = minetest.get_meta(self.object:get_pos()) local text = meta:get_string("text") self.object:set_properties({ textures={generate_texture(create_lines(text), self._signnodename)}, }) self.object:set_armor_groups({ immortal = 1 }) end, get_staticdata = function(self) local out = { _signnodename = self._signnodename } return minetest.serialize(out) end, }) minetest.register_craft({ type = "fuel", recipe = "mcl_signs:wall_sign", burntime = 10, }) if minetest.get_modpath("mcl_core") then minetest.register_craft({ output = 'mcl_signs:wall_sign 3', recipe = { {'group:wood', 'group:wood', 'group:wood'}, {'group:wood', 'group:wood', 'group:wood'}, {'', 'mcl_core:stick', ''}, } }) end if minetest.get_modpath("doc") then doc.add_entry_alias("nodes", "mcl_signs:wall_sign", "nodes", "mcl_signs:standing_sign") doc.add_entry_alias("nodes", "mcl_signs:wall_sign", "nodes", "mcl_signs:standing_sign22_5") doc.add_entry_alias("nodes", "mcl_signs:wall_sign", "nodes", "mcl_signs:standing_sign45") doc.add_entry_alias("nodes", "mcl_signs:wall_sign", "nodes", "mcl_signs:standing_sign67_5") end minetest.register_alias("signs:sign_wall", "mcl_signs:wall_sign") minetest.register_alias("signs:sign_yard", "mcl_signs:standing_sign") minetest.register_lbm({ name = "mcl_signs:respawn_entities", label = "Respawn sign text entities", run_at_every_load = true, nodenames = { "mcl_signs:wall_sign", "mcl_signs:standing_sign", "mcl_signs:standing_sign22_5", "mcl_signs:standing_sign45", "mcl_signs:standing_sign67_5" }, action = function(pos, node) update_sign(pos) end, }) if minetest.settings:get_bool("log_mods") then minetest.log("action", "[mcl_signs] loaded") end