VoxeLibre/mods/ITEMS/mcl_signs/api.lua
2025-02-25 21:54:18 +07:00

563 lines
15 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

local S, charmap, utf8 = ...
local function table_merge(t, ...)
local t2 = table.copy(t)
return table.update(t2, ...)
end
local SIGN_WIDTH = 115
local LINE_LENGTH = 15
local NUMBER_OF_LINES = 4
local LINE_HEIGHT = 14
local CHAR_WIDTH = 5
local SIGN_GLOW_INTENSITY = 14
local NEWLINE = {
[0x000A] = true,
[0x000B] = true,
[0x000C] = true,
-- U+000D (CR) is dropped on U-string conversion
[0x0085] = true,
[0x2028] = true,
[0x2029] = true,
}
local WHITESPACE = {
[0x0009] = true,
[0x0020] = true,
-- U+00A0 is a whitespace, but a non-breaking one
[0x1680] = true,
[0x2000] = true,
[0x2001] = true,
[0x2002] = true,
[0x2003] = true,
[0x2004] = true,
[0x2005] = true,
[0x2006] = true,
-- U+2007 is a whitespace, but a non-breaking one
[0x2008] = true,
[0x2009] = true,
[0x200A] = true,
-- U+202F is a whitespace, but a non-breaking one
[0x205F] = true,
[0x3000] = true,
}
local HYPHEN = {
[0x002D] = true,
[0x00AD] = true,
[0x058A] = true,
[0x05BE] = true,
[0x1806] = true,
[0x2010] = true,
-- U+2011 is a hyphen, but a non-breaking one
[0x2E17] = true,
[0x2E5D] = true,
[0x30FB] = true,
[0xFE63] = true,
[0xFF0D] = true,
[0xFF65] = true,
}
local CR_CODEPOINT = utf8.codepoint("\r") -- ignored
local WRAP_CODEPOINT = utf8.codepoint("") -- default, ellipsis for "truncate"
local DEFAULT_COLOR = "#000000"
local DYE_TO_COLOR = {
["white"] = "#d0d6d7",
["grey"] = "#818177",
["dark_grey"] = "#383c40",
["black"] = "#080a10",
["violet"] = "#6821a0",
["blue"] = "#2e3094",
["lightblue"] = "#258ec9",
["cyan"] = "#167b8c",
["dark_green"] = "#4b5e25",
["green"] = "#60ac19",
["yellow"] = "#f1b216",
["brown"] = "#633d20",
["orange"] = "#e26501",
["red"] = "#912222",
["magenta"] = "#ab31a2",
["pink"] = "#d56791",
}
local F = core.formspec_escape
-- Template definition
local sign_tpl = {
_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 @1 lines of text with up to @2 characters for each line; anything beyond these limits is lost. Not all characters are supported. The text can be changed after it's written by rightclicking the sign. Can be colored and made to glow. Use bone meal to remove color and glow.", NUMBER_OF_LINES, LINE_LENGTH),
use_texture_alpha = "opaque",
sunlight_propagates = true,
walkable = false,
is_ground_content = false,
paramtype2 = "degrotate",
drawtype = "mesh",
mesh = "mcl_signs_sign.obj",
paramtype = "light",
selection_box = {
type = "fixed",
fixed = {-0.2, -0.5, -0.2, 0.2, 0.5, 0.2}
},
groups = {axey = 1, handy = 2, sign = 1, supported_node = 1, not_in_creative_inventory = 1},
stack_max = 16,
sounds = mcl_sounds.node_sound_wood_defaults(),
node_placement_prediction = "",
_mcl_sign_type = "standing"
}
-- Signs data / meta
local function normalize_rotation(rot)
return math.floor(0.5 + rot / 15) * 15
end
local function get_signdata(pos)
local node = core.get_node(pos)
local def = core.registered_nodes[node.name]
if not def or core.get_item_group(node.name, "sign") < 1 then return end
local meta = core.get_meta(pos)
local text = core.deserialize(meta:get_string("utext"), true) or {}
local color = meta:get_string("color")
if color == "" then
color = DEFAULT_COLOR
end
local glow = core.is_yes(meta:get_string("glow"))
local yaw, spos
local typ = "standing"
if def.paramtype2 == "wallmounted" then
typ = "wall"
local dir = core.wallmounted_to_dir(node.param2)
spos = vector.add(vector.offset(pos, 0, -0.25, 0), dir * 0.41)
yaw = core.dir_to_yaw(dir)
else
yaw = math.rad(((node.param2 * 1.5) + 1) % 360)
local dir = core.yaw_to_dir(yaw)
spos = vector.add(vector.offset(pos, 0, 0.08, 0), dir * -0.05)
end
return {
text = text,
color = color,
yaw = yaw,
node = node,
typ = typ,
glow = glow,
text_pos = spos,
}
end
local function set_signmeta(pos, tbl)
local meta = core.get_meta(pos)
if tbl.text then meta:set_string("utext", core.serialize(tbl.text)) end
if tbl.color then meta:set_string("color", tbl.color) end
if tbl.glow then meta:set_string("glow", tbl.glow) end
end
-- Text processing
local function string_to_ustring(str, max_characters)
-- limit saved text to 256 characters by default
-- (4 lines x 15 chars = 60 so this should be more than is ever needed)
max_characters = max_characters or 256
local ustr = {}
-- pcall wrapping to protect against invalid UTF-8
local iter = utf8.codes(str)
while true do
local success, i, code = pcall(iter)
if not success or not i or i >= max_characters
or code == CR_CODEPOINT then
break
end
table.insert(ustr, code)
end
return ustr
end
mcl_signs.string_to_ustring = string_to_ustring
local function ustring_to_string(ustr)
local str = ""
for _, code in ipairs(ustr) do
str = str .. utf8.char(code)
end
return str
end
mcl_signs.ustring_to_string = ustring_to_string
-- TODO: make shared code as table.slice()?
local function subseq(ustr, s, e)
local line = {}
for i = s, e do
line[#line+1] = ustr[i]
end
return line
end
local ustring_to_line_array
local wrap_mode = core.settings:get("mcl_signs_wrap_mode") or "word_wrap"
if wrap_mode == "word_break" then
function ustring_to_line_array(ustr)
local lines = {}
local line = {}
for _, code in ipairs(ustr) do
if #lines >= NUMBER_OF_LINES then break end
if NEWLINE[code]
or WHITESPACE[code] and #line >= (LINE_LENGTH - 1) then
table.insert(lines, line)
line = {}
elseif #line >= LINE_LENGTH then
table.insert(line, WRAP_CODEPOINT)
table.insert(lines, line)
line = {code}
else
table.insert(line, code)
end
end
if #line > 0 and #lines < NUMBER_OF_LINES then table.insert(lines, line) end
return lines
end
elseif wrap_mode == "word_wrap" then
function ustring_to_line_array(ustr)
local lines = {}
local start, stop = 1, 1
for cursor, code in ipairs(ustr) do
if #lines >= NUMBER_OF_LINES then break end
if WHITESPACE[code] or HYPHEN[code] then
stop = cursor
elseif NEWLINE[code] then
table.insert(lines, subseq(ustr, start, cursor - 1))
start, stop = cursor + 1, cursor + 1
elseif cursor - start + 1 >= LINE_LENGTH then
if stop <= start then -- forced break, no space in word
local line = subseq(ustr, start, cursor)
table.insert(line, WRAP_CODEPOINT)
table.insert(lines, line)
start, stop = cursor + 1, cursor + 1
else
table.insert(lines, subseq(ustr, start, stop + (HYPHEN[ustr[stop]] and 0 or -1)))
start, stop = stop + 1, stop + 1
end
end
end
if #lines < NUMBER_OF_LINES and start < #ustr then
table.insert(lines, subseq(ustr, start, #ustr))
end
return lines
end
elseif wrap_mode == "truncate" then
WRAP_CODEPOINT = utf8.codepoint("")
function ustring_to_line_array(ustr)
local lines = {}
local line = {}
for _, code in ipairs(ustr) do
if #lines >= NUMBER_OF_LINES then break end
if NEWLINE[code] then
table.insert(lines, line)
line = {}
elseif #line == LINE_LENGTH then
table.insert(line, WRAP_CODEPOINT)
elseif #line < LINE_LENGTH then
table.insert(line, code)
end
end
if #line > 0 and #lines < NUMBER_OF_LINES then table.insert(lines, line) end
return lines
end
end
mcl_signs.ustring_to_line_array = ustring_to_line_array
local function generate_line(ustr, ypos)
local parsed = {}
local width = 0
local printed_char_width = CHAR_WIDTH + 1
for _, code in ipairs(ustr) do
local file = "_rc"
if charmap[code] then file = charmap[code] end
width = width + printed_char_width
table.insert(parsed, file)
end
width = width - 1
local texture = ""
local xpos = math.floor((SIGN_WIDTH - width) / 2) -- center with X offset
for _, file in ipairs(parsed) do
texture = texture .. ":" .. xpos .. "," .. ypos .. "=" .. file.. ".png"
xpos = xpos + printed_char_width
end
return texture
end
mcl_signs.generate_line = generate_line
local function generate_texture(data)
local lines = ustring_to_line_array(data.text)
local texture = "[combine:" .. SIGN_WIDTH .. "x" .. SIGN_WIDTH
local ypos = 0
local letter_color = data.color or DEFAULT_COLOR
for _, line in ipairs(lines) do
texture = texture .. generate_line(line, ypos)
ypos = ypos + LINE_HEIGHT
end
texture = "(" .. texture .. "^[multiply:" .. letter_color .. ")"
return texture
end
mcl_signs.generate_texture = generate_texture
-- Text entity handling
local function get_text_entity(pos, force_remove)
local objects = core.get_objects_inside_radius(pos, 0.5)
local text_entity
local i = 0
for _, v in pairs(objects) do
local ent = v:get_luaentity()
if ent and ent.name == "mcl_signs:text" then
i = i + 1
if i > 1 or force_remove == true then
v:remove()
else
text_entity = v
end
end
end
return text_entity
end
mcl_signs.get_text_entity = get_text_entity
-- Update the sign text entity (create if doesn't exist)
local function update_sign(pos)
local data = get_signdata(pos)
local text_entity = get_text_entity(pos)
if text_entity and not data then
text_entity:remove()
return false
elseif not data then
return false
elseif not text_entity then
text_entity = core.add_entity(data.text_pos, "mcl_signs:text")
if not text_entity or not text_entity:get_pos() then return end
end
text_entity:set_properties({
textures = {generate_texture(data)},
glow = data.glow and SIGN_GLOW_INTENSITY or 0,
})
text_entity:set_yaw(data.yaw)
text_entity:set_armor_groups({immortal = 1})
return true
end
mcl_signs.update_sign = update_sign
core.register_lbm({
name = "mcl_signs:restore_entities",
nodenames = {"group:sign"},
label = "Restore sign text",
run_at_every_load = true,
action = update_sign,
})
-- Text entity definition
core.register_entity("mcl_signs:text", {
initial_properties = {
pointable = false,
visual = "upright_sprite",
physical = false,
collide_with_objects = false,
},
on_activate = function(self)
local pos = self.object:get_pos()
update_sign(pos)
local props = self.object:get_properties()
local t = props and props.textures
if type(t) ~= "table" or #t == 0 then self.object:remove() end
end,
})
-- Formspec
local function show_formspec(player, pos, guest)
if not pos then return end
local meta = core.get_meta(pos)
local old_text = ustring_to_string(core.deserialize(meta:get_string("utext"), true) or {})
local fs
if guest then
fs = {
"size[6,2.3]textarea[0.25,0.25;6,1.5;;",
F(S("Sign text:")), ";", F(old_text), "]",
"button_exit[0,1.7;6,1;submit;", F(S("Close")), "]"
}
else
fs = {
"size[6,3]textarea[0.25,0.25;6,1.5;text;",
F(S("Enter sign text:")), ";", F(old_text), "]",
"label[0,1.5;",
F(S("Maximum line length: @1", LINE_LENGTH)), "\n",
F(S("Maximum lines: @1", NUMBER_OF_LINES)),
"]",
"button_exit[0,2.4;6,1;submit;", F(S("Done")), "]"
}
end
core.show_formspec(player:get_player_name(), "mcl_signs:set_text_"..pos.x.."_"..pos.y.."_"..pos.z, table.concat(fs))
end
mcl_signs.show_formspec = show_formspec
core.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 = vector.new(tonumber(x), tonumber(y), tonumber(z))
if not fields or not fields.text then return end
if not mcl_util.check_position_protection(pos, player) then
local utext = string_to_ustring(fields.text)
set_signmeta(pos, {text = utext})
update_sign(pos)
end
end
end)
-- Node definition callbacks
function sign_tpl.on_place(itemstack, placer, pointed_thing)
local under = pointed_thing.under
do -- ensure the node we attach to can actually be attached to
local node = core.get_node(under)
local def = core.registered_nodes[node.name]
if def and def.buildable_to then return itemstack end
end
local new_stack = mcl_util.call_on_rightclick(itemstack, placer, pointed_thing)
if new_stack then
return new_stack
end
local above = pointed_thing.above
local dir = vector.subtract(under, above)
local wdir = core.dir_to_wallmounted(dir)
local itemstring = itemstack:get_name()
local def = itemstack:get_definition()
local pos
local placestack = ItemStack(itemstack)
if wdir < 1 then
-- no placement on ceilings allowed yet
return itemstack
elseif wdir == 1 then
placestack:set_name("mcl_signs:standing_sign_"..def._mcl_sign_wood)
-- param2 value is degrees / 1.5
local rot = normalize_rotation(placer:get_look_horizontal() * 180 / math.pi / 1.5)
itemstack, pos = core.item_place_node(placestack, placer, pointed_thing, rot)
else
placestack:set_name("mcl_signs:wall_sign_"..def._mcl_sign_wood)
itemstack, pos = core.item_place_node(placestack, placer, pointed_thing, wdir)
end
show_formspec(placer, pos)
-- restore canonical name as core.item_place_node might have changed it
itemstack:set_name(itemstring)
return itemstack
end
function sign_tpl.on_rightclick(pos, _, clicker, itemstack)
if core.is_protected(pos, clicker:get_player_name()) then
show_formspec(clicker, pos, true)
return itemstack
end
local iname = itemstack:get_name()
if iname == "mcl_mobitems:glow_ink_sac" then
local data = get_signdata(pos)
if data then
if data.color == "#000000" then
data.color = "#7e7e7e" -- black doesn't glow in the dark
end
set_signmeta(pos, {glow = "true", color = data.color})
update_sign(pos)
if not core.is_creative_enabled(clicker:get_player_name()) then
itemstack:take_item()
end
end
elseif iname == "mcl_bone_meal:bone_meal" then
set_signmeta(pos, {
glow = "false",
color = DEFAULT_COLOR,
})
update_sign(pos)
if not core.is_creative_enabled(clicker:get_player_name()) then
itemstack:take_item()
end
elseif iname:sub(1, 8) == "mcl_dye:" then
local dye = iname:sub(9)
set_signmeta(pos, {color = DYE_TO_COLOR[dye]})
update_sign(pos)
if not core.is_creative_enabled(clicker:get_player_name()) then
itemstack:take_item()
end
else
show_formspec(clicker, pos)
end
return itemstack
end
function sign_tpl.on_destruct(pos)
get_text_entity(pos, true)
end
-- TODO: reactivate when a good dyes API is finished
--function sign_tpl._on_dye_place(pos, color)
-- set_signmeta(pos, {
-- color = mcl_dyes.colors[color].rgb
-- })
-- mcl_signs.update_sign(pos)
--end
-- Wall sign definition
local sign_wall = table_merge(sign_tpl, {
mesh = "mcl_signs_signonwallmount.obj",
paramtype2 = "wallmounted",
selection_box = {
type = "wallmounted",
wall_side = {-0.5, -7/28, -0.5, -23/56, 7/28, 0.5}
},
groups = {axey = 1, handy = 2, sign = 1, supported_node_wallmounted = 1, deco_block = 1},
_mcl_sign_type = "wall",
})
local function colored_texture(texture, color)
return texture.."^[multiply:"..color
end
function mcl_signs.register_sign(name, color, def)
local newfields = {
tiles = {colored_texture("mcl_signs_sign_greyscale.png", color)},
inventory_image = colored_texture("mcl_signs_default_sign_greyscale.png", color),
wield_image = colored_texture("mcl_signs_default_sign_greyscale.png", color),
drop = "mcl_signs:wall_sign_"..name,
_mcl_sign_wood = name,
}
def = def or {}
core.register_node(":mcl_signs:standing_sign_"..name, table_merge(sign_tpl, newfields, def))
core.register_node(":mcl_signs:wall_sign_"..name, table_merge(sign_wall, newfields, def))
end