luk3yx 5246bac802 Use base64 for book titles if it reduces the amount of space they take (#36)
This will only use base64 when needed (when the calculated length of the base64-ed data is less than the length that the engine would encode it in).

The title is encoded with base64 as well (if using base64 would save space), but the description is never base64-encoded.
2022-07-12 00:52:51 +03:00

533 lines
12 KiB
Lua

local S = default.S
local C = default.colors
local esc = minetest.formspec_escape
local function formspec_string(lpp, page, lines, string)
for i = ((lpp * page) - lpp) + 1, lpp * page do
if not lines[i] then break end
string = string .. lines[i] .. "\n"
end
return string
end
local function gold(s) return minetest.colorize("#ff0", s) end
local lpp = 14 -- Lines per book's page
local function book_on_use(itemstack, user)
local player_name = user:get_player_name()
local meta = itemstack:get_meta()
local title, text, owner = "", "", player_name
local page, page_max, lines, string = 1, 1, {}, ""
-- Backwards compatibility
local old_data = minetest.deserialize(itemstack:get_metadata())
if old_data then
meta:from_table({fields = old_data})
end
local data = meta:to_table().fields
if data.owner then
title = data.title_b64 and minetest.decode_base64(data.title_b64) or data.title or ""
text = data.text_b64 and minetest.decode_base64(data.text_b64) or data.text or ""
owner = data.owner
for str in (text .. "\n"):gmatch("([^\n]*)[\n]") do
lines[#lines + 1] = str
end
if data.page then
page = data.page
page_max = data.page_max
string = formspec_string(lpp, page, lines, string)
end
end
local item_name = itemstack:get_name()
local formspec = "size[9,8.75]" ..
default.gui_bg .. default.gui_bg_img ..
default.gui_close_btn() ..
"item_image[0,-0.1;1,1;" .. item_name .. "]"
if owner == player_name then
formspec = formspec ..
"label[0.9,0.1;" .. esc(S("Book")) .. "]" ..
"field[0.5,1.8;8.5,0;title;" .. esc(S("Title:")) .. ";" ..
esc(title) .. "]" ..
"textarea[0.5,2.25;8.6,6.75;text;" .. esc(S("Contents:")) .. ";" ..
esc(text) .. "]" ..
"button_exit[3,8.1;3,1;save;" .. esc(S("Save")) .. "]"
else
formspec = formspec ..
"label[0.9,0.1;" .. esc(S("Book")) .. ": " ..
"\"" .. esc(gold(title)) .. "\", " ..
esc(S("by @1", owner)) .. "]" ..
"textarea[0.5,0.9;8.5,8;;" .. esc(string ~= "" and string or text) .. ";]" ..
"image_button[0.1,8.2;0.75,0.75;formspec_prev.png;book_prev;;true;false;formspec_prev_pressed.png]" ..
"image_button[3,8.2;3,0.75;blank.png;;" ..
S("Page: @1 of @2", gold(page), gold(page_max)) .. ";false;false;]" ..
"image_button[8.1,8.2;0.75,0.75;formspec_next.png;book_next;;true;false;formspec_next_pressed.png]"
end
minetest.show_formspec(player_name, "default:book", formspec)
return itemstack
end
local max_text_size = 10000
local max_title_size = 50
local short_title_size = 30
local single_character_escapes = {[34] = true, [92] = true, [47] = true, [8] = true, [12] = true, [10] = true, [13] = true, [9] = true}
local function get_itemstack_meta_len(text)
local size = 0
for i = 1, #text do
local char = text:byte(i)
if single_character_escapes[char] then
size = size + 2
elseif char >= 32 and char <= 126 then
-- ASCII printable characters are added as-is (so long as they
-- don't need to be escaped)
size = size + 1
else
-- Minetest's ItemStack metadata encodes every other byte (not
-- character) as \u00<hex>
-- This isn't valid JSON
size = size + 6
end
end
return size
end
-- Like text:sub(1, max_size) but won't split a multi-byte character (which
-- causes the text to be shown as <invalid UTF-8 string> or something)
local function safely_trim_to_length(text, max_size)
if #text > max_size then
return utf8.remove(text:sub(1, max_size + 1))
end
return text
end
local function set_optionally_b64_field(data, field_name, text)
-- 4 is added to the base64 length for "_b64"
if get_itemstack_meta_len(text) > math.ceil(#text / 0.75) + 4 then
data[field_name] = nil
data[field_name .. "_b64"] = minetest.encode_base64(text)
else
data[field_name] = text
data[field_name .. "_b64"] = nil
end
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "default:book" then return end
local inv = player:get_inventory()
local stack = player:get_wielded_item()
if fields.save and fields.title and fields.text
and fields.title ~= "" and fields.text ~= "" then
local new_stack, data
if stack:get_name() ~= "default:book_written" then
local count = stack:get_count()
if count == 1 then
stack:set_name("default:book_written")
else
stack:set_count(count - 1)
new_stack = ItemStack("default:book_written")
end
else
data = stack:get_meta():to_table().fields
end
if data and data.owner and data.owner ~= player:get_player_name() then
return
end
if not data then data = {} end
data.owner = player:get_player_name()
-- Title
local title = safely_trim_to_length(fields.title, max_title_size)
set_optionally_b64_field(data, "title", title)
-- Description
local short_title = title
-- Don't bother triming the title if the trailing dots would make it longer
if #short_title > short_title_size + 3 then
short_title = safely_trim_to_length(short_title, short_title_size) .. "..."
end
data.description = S("\"@1\" by @2", short_title, data.owner)
-- Text
local text = safely_trim_to_length(fields.text, max_text_size)
text = text:gsub("\r\n", "\n"):gsub("\r", "\n")
set_optionally_b64_field(data, "text", text)
data.page = 1
data.page_max = math.ceil((#text:gsub("[^\n]", "") + 1) / lpp)
if new_stack then
new_stack:get_meta():from_table({fields = data})
if inv:room_for_item("main", new_stack) then
inv:add_item("main", new_stack)
else
minetest.add_item(player:get_pos(), new_stack)
end
else
stack:get_meta():from_table({fields = data})
end
elseif fields.book_next or fields.book_prev then
local data = stack:get_meta():to_table().fields
if not data or not data.page then
return
end
data.page = tonumber(data.page)
data.page_max = tonumber(data.page_max)
if fields.book_next then
data.page = data.page + 1
if data.page > data.page_max then
data.page = 1
end
else
data.page = data.page - 1
if data.page == 0 then
data.page = data.page_max
end
end
stack:get_meta():from_table({fields = data})
stack = book_on_use(stack, player)
end
-- Update stack (check current stack first)
local wield_item = player:get_wielded_item():get_name()
if wield_item == "default:book" or
wield_item == "default:book_written" then
player:set_wielded_item(stack)
end
end)
--
-- Craftitem registry
--
minetest.register_craftitem("default:blueberries", {
description = S("Blueberries"),
inventory_image = "default_blueberries.png",
groups = {food = 1, food_blueberries = 1, food_berry = 1},
on_use = minetest.item_eat(1)
})
minetest.register_craftitem("default:book", {
description = S("Book"),
inventory_image = "default_book.png",
groups = {book = 1, flammable = 3},
on_use = book_on_use
})
minetest.register_craftitem("default:book_written", {
description = S("Book with Text"),
inventory_image = "default_book_written.png",
groups = {book = 1, not_in_creative_inventory = 1, flammable = 3},
stack_max = 1,
on_use = book_on_use
})
minetest.register_craftitem("default:clay_brick", {
description = S("Clay Brick"),
inventory_image = "default_clay_brick.png"
})
minetest.register_craftitem("default:clay_lump", {
description = S("Clay Lump"),
inventory_image = "default_clay_lump.png"
})
minetest.register_craftitem("default:coal_lump", {
description = S("Coal Lump"),
inventory_image = "default_coal_lump.png",
groups = {coal = 1, flammable = 1}
})
minetest.register_craftitem("default:charcoal_lump", {
description = S("Charcoal Lump"),
inventory_image = "default_charcoal_lump.png",
groups = {coal = 1, flammable = 1}
})
minetest.register_craftitem("default:diamond", {
description = S("Diamond"),
inventory_image = "default_diamond.png"
})
minetest.register_craftitem("default:flint", {
description = S("Flint"),
inventory_image = "default_flint.png"
})
minetest.register_craftitem("default:gold_ingot", {
description = S("Gold Ingot"),
inventory_image = "default_gold_ingot.png"
})
minetest.register_craftitem("default:paper", {
description = S("Paper"),
inventory_image = "default_paper.png",
groups = {flammable = 3}
})
minetest.register_craftitem("default:steel_ingot", {
description = S("Steel Ingot"),
inventory_image = "default_steel_ingot.png"
})
minetest.register_craftitem("default:stick", {
description = S("Stick"),
inventory_image = "default_stick.png",
groups = {stick = 1, flammable = 2, wieldview = 2}
})
minetest.register_craftitem("default:emerald", {
description = C.emerald .. S("Emerald"),
inventory_image = "default_emerald.png"
})
minetest.register_craftitem("default:ruby", {
description = C.ruby .. S("Ruby"),
inventory_image = "default_ruby.png"
})
minetest.register_craftitem("default:gunpowder", {
description = S("Gunpowder"),
inventory_image = "default_gunpowder.png"
})
minetest.register_craftitem("default:bone", {
description = S("Bone"),
inventory_image = "default_bone.png",
groups = {wieldview = 2}
})
minetest.register_craftitem("default:glowstone_dust", {
description = S("Glowstone Dust"),
inventory_image = "default_glowstone_dust.png"
})
minetest.register_craftitem("default:sugar", {
description = S("Sugar"),
inventory_image = "default_sugar.png"
})
minetest.register_craftitem("default:snowball", {
description = S("Snowball"),
inventory_image = "default_snowball.png",
stack_max = 16,
groups = {flammable = 3},
on_place = function(itemstack, placer, pointed_thing)
if minetest.item_place_node(ItemStack("default:snow"), placer, pointed_thing) then
if not minetest.is_creative_enabled(placer:get_player_name()) then
itemstack:take_item()
end
end
return itemstack
end
})
--
-- Crafting recipes
--
minetest.register_craft({
output = "default:book",
recipe = {
{"default:paper"},
{"default:paper"},
{"default:paper"}
}
})
default.register_craft_metadata_copy("default:book", "default:book_written")
minetest.register_craft({
output = "default:clay_brick 4",
recipe = {
{"default:brick"}
}
})
minetest.register_craft({
output = "default:coal_lump 9",
recipe = {
{"default:coalblock"}
}
})
minetest.register_craft({
output = "default:diamond 9",
recipe = {
{"default:diamondblock"}
}
})
minetest.register_craft({
output = "default:gold_ingot 9",
recipe = {
{"default:goldblock"}
}
})
minetest.register_craft({
output = "default:paper",
recipe = {
{"default:sugarcane", "default:sugarcane", "default:sugarcane"}
}
})
minetest.register_craft({
output = "default:steel_ingot 9",
recipe = {
{"default:steelblock"}
}
})
minetest.register_craft({
output = "default:stick 4",
recipe = {
{"group:wood"},
{"group:wood"}
}
})
minetest.register_craft({
output = "default:snowball 9",
recipe = {
{"default:snowblock"}
}
})
minetest.register_craft({
output = "default:emerald 9",
recipe = {
{"default:emeraldblock"}
}
})
minetest.register_craft({
output = "default:ruby 9",
recipe = {
{"default:rubyblock"}
}
})
minetest.register_craft({
output = "default:glowstone_dust 4",
recipe = {
{"default:glowstone"}
}
})
minetest.register_craft({
type = "shapeless",
output = "default:gunpowder",
recipe = {
"default:sand",
"default:gravel"
}
})
minetest.register_craft({
output = "default:sugar 2",
recipe = {
{"default:sugarcane"}
}
})
--
-- Cooking recipes
--
minetest.register_craft({
type = "cooking",
output = "default:clay_brick",
recipe = "default:clay_lump"
})
minetest.register_craft({
type = "cooking",
output = "default:hardened_clay",
recipe = "default:clay"
})
minetest.register_craft({
type = "cooking",
output = "default:gold_ingot",
recipe = "default:stone_with_gold"
})
minetest.register_craft({
type = "cooking",
output = "default:steel_ingot",
recipe = "default:stone_with_iron"
})
minetest.register_craft({
type = "cooking",
output = "default:diamond",
recipe = "default:stone_with_diamond"
})
minetest.register_craft({
type = "cooking",
output = "default:charcoal_lump",
recipe = "group:tree"
})
minetest.register_craft({
type = "cooking",
output = "default:coal_lump",
recipe = "default:stone_with_coal"
})
--
-- Fuels
--
minetest.register_craft({
type = "fuel",
recipe = "default:book",
burntime = 3
})
minetest.register_craft({
type = "fuel",
recipe = "default:book_written",
burntime = 3
})
minetest.register_craft({
type = "fuel",
recipe = "default:coal_lump",
burntime = 60
})
minetest.register_craft({
type = "fuel",
recipe = "default:charcoal_lump",
burntime = 60
})
minetest.register_craft({
type = "fuel",
recipe = "default:paper",
burntime = 1
})
minetest.register_craft({
type = "fuel",
recipe = "group:stick",
burntime = 1
})