Add MultiCraft builtin improvements

master
MoNTE48 2020-08-06 19:26:15 +02:00
parent c9c3087102
commit af540c389e
33 changed files with 1809 additions and 240 deletions

View File

@ -2,10 +2,10 @@
-- handled by the engine.
core.register_on_death(function()
core.display_chat_message("You died.")
core.display_chat_message(fgettext("You died."))
local formspec = "size[11,5.5]bgcolor[#320000b4;true]" ..
"label[4.85,1.35;" .. fgettext("You died") ..
"]button_exit[4,3;3,0.5;btn_respawn;".. fgettext("Respawn") .."]"
"label[5,2;" .. fgettext("You died.") ..
"]button_exit[3.5,3;4,0.5;btn_respawn;".. fgettext("Respawn") .."]"
core.show_formspec("bultin:death", formspec)
end)

View File

@ -3,7 +3,7 @@
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--the Free Software Foundation; either version 3.0 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,

View File

@ -16,7 +16,10 @@ local LIST_FORMSPEC_DESCRIPTION = [[
tablecolumns[color;tree;text;text]
table[0,0.5;12.8,4.8;list;%s;%i]
box[0,5.5;12.8,1.5;#000]
textarea[0.3,5.5;13.05,1.9;;;%s]
]] ..
-- textarea[0.3,5.5;13.05,1.9;;;%s] -- for compatibility with 0.4
[[
label[0.3,5.5;%s]
button_exit[5,7;3,1;quit;%s]
]]
@ -149,4 +152,3 @@ help_command.func = function(name, param)
return old_help_func(name, param)
end

View File

@ -280,7 +280,7 @@ end
--------------------------------------------------------------------------------
if INIT == "game" then
local dirs1 = {9, 18, 7, 12}
local dirs1 = {10, 19, 4, 13}
local dirs2 = {20, 23, 22, 21}
function core.rotate_and_place(itemstack, placer, pointed_thing,
@ -292,8 +292,7 @@ if INIT == "game" then
return
end
local undef = core.registered_nodes[unode.name]
local sneaking = placer and placer:get_player_control().sneak
if undef and undef.on_rightclick and not sneaking then
if undef and undef.on_rightclick then
return undef.on_rightclick(pointed_thing.under, unode, placer,
itemstack, pointed_thing)
end

View File

@ -115,6 +115,7 @@ function core.serialize(x)
function dump_val(x)
local tp = type(x)
if x == nil then return "nil"
elseif tp == "userdata" then return "nil"
elseif tp == "string" then return string.format("%q", x)
elseif tp == "boolean" then return x and "true" or "false"
elseif tp == "function" then

View File

@ -1,6 +1,8 @@
vector = {}
local floor, hypot = math.floor, math.hypot
local min, max, pi = math.min, math.max, math.pi
function vector.new(a, b, c)
if type(a) == "table" then
assert(a.x and a.y and a.z, "Invalid vector passed to vector.new()")
@ -19,7 +21,7 @@ function vector.equals(a, b)
end
function vector.length(v)
return math.hypot(v.x, math.hypot(v.y, v.z))
return hypot(v.x, hypot(v.y, v.z))
end
function vector.normalize(v)
@ -33,17 +35,17 @@ end
function vector.floor(v)
return {
x = math.floor(v.x),
y = math.floor(v.y),
z = math.floor(v.z)
x = floor(v.x),
y = floor(v.y),
z = floor(v.z)
}
end
function vector.round(v)
return {
x = math.floor(v.x + 0.5),
y = math.floor(v.y + 0.5),
z = math.floor(v.z + 0.5)
x = floor(v.x + 0.5),
y = floor(v.y + 0.5),
z = floor(v.z + 0.5)
}
end
@ -59,7 +61,7 @@ function vector.distance(a, b)
local x = a.x - b.x
local y = a.y - b.y
local z = a.z - b.z
return math.hypot(x, math.hypot(y, z))
return hypot(x, hypot(y, z))
end
function vector.direction(pos1, pos2)
@ -138,12 +140,12 @@ function vector.divide(a, b)
end
function vector.sort(a, b)
return {x = math.min(a.x, b.x), y = math.min(a.y, b.y), z = math.min(a.z, b.z)},
{x = math.max(a.x, b.x), y = math.max(a.y, b.y), z = math.max(a.z, b.z)}
return {x = min(a.x, b.x), y = min(a.y, b.y), z = min(a.z, b.z)},
{x = max(a.x, b.x), y = max(a.y, b.y), z = max(a.z, b.z)}
end
local function sin(x)
if x % math.pi == 0 then
if x % pi == 0 then
return 0
else
return math.sin(x)
@ -151,7 +153,7 @@ local function sin(x)
end
local function cos(x)
if x % math.pi == math.pi / 2 then
if x % pi == pi / 2 then
return 0
else
return math.cos(x)

View File

@ -138,10 +138,10 @@ core.register_on_prejoinplayer(function(name, ip)
local name_lower = name:lower()
for k in core.builtin_auth_handler.iterate() do
if k:lower() == name_lower then
return string.format("\nCannot create new player called '%s'. "..
"Another account called '%s' is already registered. "..
"Please check the spelling if it's your account "..
"or use a different nickname.", name, k)
return ("\nYou can not register as \"%s\"! "..
"Another player called \"%s\" is already registered. " ..
"Please check the spelling if it's your account " ..
"or use a different name."):format(name, k)
end
end
end)

View File

@ -105,6 +105,15 @@ local function parse_range_str(player_name, str)
return p1, p2
end
-- Chatcommand aliases (by @luk3yx)
local function register_chatcommand_alias(new, old)
local def = assert(core.registered_chatcommands[old])
assert(not core.registered_chatcommands[new])
core.registered_chatcommands[new] = def
end
core.register_chatcommand_alias = register_chatcommand_alias
--
-- Chat commands
--
@ -436,7 +445,6 @@ core.register_chatcommand("teleport", {
p.y = tonumber(p.y)
p.z = tonumber(p.z)
if p.x and p.y and p.z then
local lm = 31000
if p.x < -lm or p.x > lm or p.y < -lm or p.y > lm or p.z < -lm or p.z > lm then
return false, "Cannot teleport out of map bounds!"
@ -447,7 +455,7 @@ core.register_chatcommand("teleport", {
return false, "Can't teleport, you're attached to an object!"
end
teleportee:set_pos(p)
return true, "Teleporting to "..core.pos_to_string(p)
return true, "Teleporting to " .. core.pos_to_string(p, 1)
end
end
@ -469,7 +477,7 @@ core.register_chatcommand("teleport", {
p = find_free_position_near(p)
teleportee:set_pos(p)
return true, "Teleporting to " .. target_name
.. " at "..core.pos_to_string(p)
.. " at " .. core.pos_to_string(p, 1)
end
if not core.check_player_privs(name, {bring=true}) then
@ -491,7 +499,7 @@ core.register_chatcommand("teleport", {
end
teleportee:set_pos(p)
return true, "Teleporting " .. teleportee_name
.. " to " .. core.pos_to_string(p)
.. " to " .. core.pos_to_string(p, 1)
end
teleportee = nil
@ -514,13 +522,14 @@ core.register_chatcommand("teleport", {
teleportee:set_pos(p)
return true, "Teleporting " .. teleportee_name
.. " to " .. target_name
.. " at " .. core.pos_to_string(p)
.. " at " .. core.pos_to_string(p, 1)
end
return false, 'Invalid parameters ("' .. param
.. '") or player not found (see /help teleport)'
end,
})
register_chatcommand_alias("tp", "teleport")
core.register_chatcommand("set", {
params = "([-n] <name> <value>) | <name>",
@ -649,7 +658,7 @@ core.register_chatcommand("fixlight", {
core.register_chatcommand("mods", {
params = "",
description = "List mods installed on the server",
privs = {},
privs = {server = true},
func = function(name, param)
return true, table.concat(core.get_modnames(), ", ")
end,
@ -658,6 +667,18 @@ core.register_chatcommand("mods", {
local function handle_give_command(cmd, giver, receiver, stackstring)
core.log("action", giver .. " invoked " .. cmd
.. ', stackstring="' .. stackstring .. '"')
local ritems = core.registered_items
if not stackstring:match(":") and not ritems[stackstring] then
local modslist = core.get_modnames()
table.insert(modslist, 1, "default")
for _, modname in pairs(modslist) do
local namecheck = modname .. ":" .. stackstring:match("%S*")
if ritems[namecheck] then
stackstring = modname .. ":" .. stackstring
break
end
end
end
local itemstack = ItemStack(stackstring)
if itemstack:is_empty() then
return false, "Cannot give an empty item"
@ -926,6 +947,7 @@ core.register_chatcommand("time", {
return true, "Time of day changed."
end,
})
register_chatcommand_alias("settime", "time")
core.register_chatcommand("days", {
description = "Show day count since world creation",
@ -1030,8 +1052,7 @@ core.register_chatcommand("clearobjects", {
core.log("action", name .. " clears all objects ("
.. options.mode .. " mode).")
core.chat_send_all("Clearing all objects. This may take a long time."
.. " You may experience a timeout. (by "
.. name .. ")")
.. " You may experience a timeout.")
core.clear_objects(options)
core.log("action", "Object clearing done.")
core.chat_send_all("*** Cleared all objects.")
@ -1041,7 +1062,7 @@ core.register_chatcommand("clearobjects", {
core.register_chatcommand("msg", {
params = "<name> <message>",
description = "Send a direct message to a player",
description = "Send a private message to a player",
privs = {shout=true},
func = function(name, param)
local sendto, message = param:match("^(%S+)%s(.+)$")
@ -1052,13 +1073,15 @@ core.register_chatcommand("msg", {
return false, "The player " .. sendto
.. " is not online."
end
core.log("action", "DM from " .. name .. " to " .. sendto
core.log("action", "PM from " .. name .. " to " .. sendto
.. ": " .. message)
core.chat_send_player(sendto, "DM from " .. name .. ": "
.. message)
core.chat_send_player(sendto, minetest.colorize("green",
"PM from " .. name .. ": " .. message))
return true, "Message sent."
end,
})
register_chatcommand_alias("m", "msg")
register_chatcommand_alias("pm", "msg")
core.register_chatcommand("last-login", {
params = "[<name>]",
@ -1135,3 +1158,34 @@ core.register_chatcommand("kill", {
return handle_kill_command(name, param == "" and name or param)
end,
})
core.register_chatcommand("spawn", {
description = "Teleport to the spawn point",
func = function(name)
local player = core.get_player_by_name(name)
if not player then
return false
end
local spawnpoint = core.setting_get_pos("static_spawnpoint")
if spawnpoint then
player:set_pos(spawnpoint)
return true, "Teleporting to spawn..."
else
return false, "The spawn point is not set!"
end
end
})
core.register_chatcommand("setspawn", {
description = "Sets the spawn point to your current position",
privs = {server = true},
func = function(name)
local player = core.get_player_by_name(name)
if not player then
return false
end
local pos = core.pos_to_string(player:get_pos(), 1)
core.settings:set("static_spawnpoint", pos)
return true, "The spawn point are set to " .. pos
end
})

View File

@ -1,33 +1,36 @@
-- Minetest: builtin/item.lua
local random, pi = math.random, math.pi
local vnew, vround, vadd, vapply = vector.new, vector.round, vector.add, vector.apply
local builtin_shared = ...
local SCALE = 0.667
local facedir_to_euler = {
{y = 0, x = 0, z = 0},
{y = -math.pi/2, x = 0, z = 0},
{y = math.pi, x = 0, z = 0},
{y = math.pi/2, x = 0, z = 0},
{y = math.pi/2, x = -math.pi/2, z = math.pi/2},
{y = math.pi/2, x = math.pi, z = math.pi/2},
{y = math.pi/2, x = math.pi/2, z = math.pi/2},
{y = math.pi/2, x = 0, z = math.pi/2},
{y = -math.pi/2, x = math.pi/2, z = math.pi/2},
{y = -math.pi/2, x = 0, z = math.pi/2},
{y = -math.pi/2, x = -math.pi/2, z = math.pi/2},
{y = -math.pi/2, x = math.pi, z = math.pi/2},
{y = 0, x = 0, z = math.pi/2},
{y = 0, x = -math.pi/2, z = math.pi/2},
{y = 0, x = math.pi, z = math.pi/2},
{y = 0, x = math.pi/2, z = math.pi/2},
{y = math.pi, x = math.pi, z = math.pi/2},
{y = math.pi, x = math.pi/2, z = math.pi/2},
{y = math.pi, x = 0, z = math.pi/2},
{y = math.pi, x = -math.pi/2, z = math.pi/2},
{y = math.pi, x = math.pi, z = 0},
{y = -math.pi/2, x = math.pi, z = 0},
{y = 0, x = math.pi, z = 0},
{y = math.pi/2, x = math.pi, z = 0}
{y = -pi/2, x = 0, z = 0},
{y = pi, x = 0, z = 0},
{y = pi/2, x = 0, z = 0},
{y = pi/2, x = -pi/2, z = pi/2},
{y = pi/2, x = pi, z = pi/2},
{y = pi/2, x = pi/2, z = pi/2},
{y = pi/2, x = 0, z = pi/2},
{y = -pi/2, x = pi/2, z = pi/2},
{y = -pi/2, x = 0, z = pi/2},
{y = -pi/2, x = -pi/2, z = pi/2},
{y = -pi/2, x = pi, z = pi/2},
{y = 0, x = 0, z = pi/2},
{y = 0, x = -pi/2, z = pi/2},
{y = 0, x = pi, z = pi/2},
{y = 0, x = pi/2, z = pi/2},
{y = pi, x = pi, z = pi/2},
{y = pi, x = pi/2, z = pi/2},
{y = pi, x = 0, z = pi/2},
{y = pi, x = -pi/2, z = pi/2},
{y = pi, x = pi, z = 0},
{y = -pi/2, x = pi, z = 0},
{y = 0, x = pi, z = 0},
{y = pi/2, x = pi, z = 0}
}
local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81
@ -36,6 +39,8 @@ local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81
-- Falling stuff
--
local drop_attached_node
core.register_entity(":__builtin:falling_node", {
initial_properties = {
visual = "item",
@ -50,8 +55,10 @@ core.register_entity(":__builtin:falling_node", {
node = {},
meta = {},
floats = false,
timer = 0,
set_node = function(self, node, meta)
node.param2 = node.param2 or 0
self.node = node
meta = meta or {}
if type(meta.to_table) == "function" then
@ -143,7 +150,7 @@ core.register_entity(":__builtin:falling_node", {
-- Rotate entity
if def.drawtype == "torchlike" then
self.object:set_yaw(math.pi*0.25)
self.object:set_yaw(pi*0.25)
elseif (node.param2 ~= 0 and (def.wield_image == ""
or def.wield_image == nil))
or def.drawtype == "signlike"
@ -161,28 +168,28 @@ core.register_entity(":__builtin:falling_node", {
local rot = node.param2 % 8
local pitch, yaw, roll = 0, 0, 0
if rot == 1 then
pitch, yaw = math.pi, math.pi
pitch, yaw = pi, pi
elseif rot == 2 then
pitch, yaw = math.pi/2, math.pi/2
pitch, yaw = pi/2, pi/2
elseif rot == 3 then
pitch, yaw = math.pi/2, -math.pi/2
pitch, yaw = pi/2, -pi/2
elseif rot == 4 then
pitch, yaw = math.pi/2, math.pi
pitch, yaw = pi/2, pi
elseif rot == 5 then
pitch, yaw = math.pi/2, 0
pitch, yaw = pi/2, 0
end
if def.drawtype == "signlike" then
pitch = pitch - math.pi/2
pitch = pitch - pi/2
if rot == 0 then
yaw = yaw + math.pi/2
yaw = yaw + pi/2
elseif rot == 1 then
yaw = yaw - math.pi/2
yaw = yaw - pi/2
end
elseif def.drawtype == "mesh" or def.drawtype == "normal" then
if rot >= 0 and rot <= 1 then
roll = roll + math.pi
roll = roll + pi
else
yaw = yaw + math.pi
yaw = yaw + pi
end
end
self.object:set_rotation({x=pitch, y=yaw, z=roll})
@ -217,9 +224,9 @@ core.register_entity(":__builtin:falling_node", {
-- Add levels if dropped on same leveled node
if bcd and bcd.paramtype2 == "leveled" and
bcn.name == self.node.name then
local addlevel = self.node.level
if (addlevel or 0) <= 0 then
addlevel = bcd.leveled
local addlevel = self.node.level or 0
if addlevel <= 0 then
addlevel = bcd.leveled or 0
end
if core.add_node_level(bcp, addlevel) < addlevel then
return true
@ -230,7 +237,7 @@ core.register_entity(":__builtin:falling_node", {
end
-- Decide if we're replacing the node or placing on top
local np = vector.new(bcp)
local np = vnew(bcp)
if bcd and bcd.buildable_to and
(not self.floats or bcd.liquidtype == "none") then
core.remove_node(bcp)
@ -262,6 +269,12 @@ core.register_entity(":__builtin:falling_node", {
if self.meta then
core.get_meta(np):from_table(self.meta)
end
-- Drop node if falls into a protected area
if core.is_protected(np, "") then
self.object:remove()
drop_attached_node(np)
return true
end
if def.sounds and def.sounds.place then
core.sound_play(def.sounds.place, {pos = np}, true)
end
@ -276,7 +289,7 @@ core.register_entity(":__builtin:falling_node", {
if self.floats then
local pos = self.object:get_pos()
local bcp = vector.round({x = pos.x, y = pos.y - 0.7, z = pos.z})
local bcp = vround({x = pos.x, y = pos.y - 0.7, z = pos.z})
local bcn = core.get_node(bcp)
local bcd = core.registered_nodes[bcn.name]
@ -322,7 +335,7 @@ core.register_entity(":__builtin:falling_node", {
y = player_collision.old_velocity.y,
z = vel.z
})
self.object:set_pos(vector.add(self.object:get_pos(),
self.object:set_pos(vadd(self.object:get_pos(),
{x = 0, y = -0.5, z = 0}))
end
return
@ -335,7 +348,7 @@ core.register_entity(":__builtin:falling_node", {
local failure = false
local pos = self.object:get_pos()
local distance = vector.apply(vector.subtract(pos, bcp), math.abs)
local distance = vapply(vector.subtract(pos, bcp), math.abs)
if distance.x >= 1 or distance.z >= 1 then
-- We're colliding with some part of a node that's sticking out
-- Since we don't want to visually teleport, drop as item
@ -353,6 +366,14 @@ core.register_entity(":__builtin:falling_node", {
end
end
-- Drop node if does not fall within 5 seconds
self.timer = self.timer + dtime
if self.timer > 5 then
core.add_item(pos, self.node)
self.object:remove()
return
end
-- Try to actually place ourselves
if not failure then
failure = not self:try_place(bcp, bcn)
@ -396,7 +417,7 @@ function core.spawn_falling_node(pos)
return convert_to_falling_node(pos, node)
end
local function drop_attached_node(p)
function drop_attached_node(p)
local n = core.get_node(p)
local drops = core.get_node_drops(n, "")
local def = core.registered_items[n.name]
@ -418,9 +439,9 @@ local function drop_attached_node(p)
core.remove_node(p)
for _, item in pairs(drops) do
local pos = {
x = p.x + math.random()/2 - 0.25,
y = p.y + math.random()/2 - 0.25,
z = p.z + math.random()/2 - 0.25,
x = p.x + random()/2 - 0.25,
y = p.y + random()/2 - 0.25,
z = p.z + random()/2 - 0.25,
}
core.add_item(pos, item)
end
@ -439,7 +460,7 @@ function builtin_shared.check_attached_node(p, n)
else
d.y = -1
end
local p2 = vector.add(p, d)
local p2 = vadd(p, d)
local nn = core.get_node(p2).name
local def2 = core.registered_nodes[nn]
if def2 and not def2.walkable then
@ -486,6 +507,33 @@ function core.check_single_for_falling(p)
end
end
-- Attached, but not wallmounted. Yes, no one thought about it.
-- This is an alternative to function 'check_attached_node', so it seems too complicated.
local check_connected = {
{x = -1, y = 0, z = 0},
{x = 1, y = 0, z = 0},
{x = 0, y = 0, z = 1},
{x = 0, y = 0, z = -1}
}
for i = 1, 4 do
local pa = vadd(p, check_connected[i])
local nc = core.get_node(pa)
if core.get_item_group(nc.name, "attached_node2") ~= 0 then
for j = 1, 4 do
local ptwo = vadd(pa, check_connected[j])
local ntwo = core.get_node(ptwo)
local def = core.registered_nodes[ntwo.name]
if def and def.walkable then
return false
end
end
drop_attached_node(pa)
return true
end
end
return false
end
@ -509,7 +557,7 @@ local check_for_falling_neighbors = {
function core.check_for_falling(p)
-- Round p to prevent falling entities to get stuck.
p = vector.round(p)
p = vround(p)
-- We make a stack, and manually maintain size for performance.
-- Stored in the stack, we will maintain tables with pos, and
@ -526,7 +574,7 @@ function core.check_for_falling(p)
n = n + 1
s[n] = {p = p, v = v}
-- Select next node from neighbor list.
p = vector.add(p, check_for_falling_neighbors[v])
p = vadd(p, check_for_falling_neighbors[v])
-- Now we check out the node. If it is in need of an update,
-- it will let us know in the return value (true = updated).
if not core.check_single_for_falling(p) then

167
builtin/game/hud.lua Normal file
View File

@ -0,0 +1,167 @@
--[[
From Better HUD mod
Copyright (C) BlockMen (2013-2016)
Copyright (C) MultiCraft Development Team (2019-2020)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 3.0 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
]]
hud, hud_id = {}, {}
local items, sb_bg = {}, {}
local function throw_error(msg)
core.log("error", "HUD: " .. msg)
end
--
-- API
--
function hud.register(name, def)
if not name or not def then
throw_error("Not enough parameters given")
return false
end
if items[name] then
throw_error("A statbar with name " .. name .. " already exists")
return false
end
-- actually register
-- add background first since draworder is based on id
if def.hud_elem_type == "statbar" and def.background then
sb_bg[name] = table.copy(def)
sb_bg[name].text = def.background
if not def.autohide_bg and def.max then
sb_bg[name].number = def.max
end
end
-- add item itself
items[name] = def
return true
end
function hud.change_item(player, name, def)
if not player or not player:is_player() then
return false
end
if not name or not def then
throw_error("Not enough parameters given")
return false
end
local i_name = player:get_player_name() .. "_" .. name
local elem = hud_id[i_name]
local item = items[name]
if not elem then
-- throw_error("Given HUD element " .. dump(name) .. " does not exist")
return false
end
if def.number then
if item.hud_elem_type ~= "statbar" then
throw_error("Attempted to change an statbar HUD parameter for text HUD element " .. dump(name))
return false
end
if item.max and def.number > item.max then
def.number = item.max
end
player:hud_change(elem.id, "number", def.number)
-- hide background when set
if item.autohide_bg then
local bg = hud_id[i_name .. "_bg"]
if not bg then
throw_error("Given HUD element " .. dump(name) .. "_bg does not exist")
return false
end
local num = def.number ~= 0 and (item.max or item.number) or 0
player:hud_change(bg.id, "number", num)
end
elseif def.text then
player:hud_change(elem.id, "text", def.text)
elseif def.offset then
player:hud_change(elem.id, "offset", def.offset)
end
return true
end
function hud.remove_item(player, name)
if not player or not player:is_player() then
return false
end
if not name then
throw_error("Not enough parameters given")
return false
end
local i_name = player:get_player_name() .. "_" .. name
local elem = hud_id[i_name]
if not elem then
throw_error("Given HUD element " .. dump(name) .. " does not exist")
return false
end
player:hud_remove(elem.id)
return true
end
--
-- Add registered HUD items to joining players
--
-- Following code is placed here to keep HUD ids internal
local function add_hud_item(player, name, def)
if not player or not name or not def then
throw_error("Not enough parameters given")
return false
end
local i_name = player:get_player_name() .. "_" .. name
hud_id[i_name] = {}
hud_id[i_name].id = player:hud_add(def)
end
core.register_on_joinplayer(function(player)
-- add the backgrounds for statbars
for name, item in pairs(sb_bg) do
add_hud_item(player, name .. "_bg", item)
end
-- and finally the actual HUD items
for name, item in pairs(items) do
add_hud_item(player, name, item)
end
end)
core.register_on_leaveplayer(function(player)
local player_name = player:get_player_name()
for name, _ in pairs(sb_bg) do
sb_bg[player_name .. "_" .. name] = nil
end
for name, _ in pairs(items) do
hud_id[player_name .. "_" .. name] = nil
end
end)

376
builtin/game/hunger.lua Normal file
View File

@ -0,0 +1,376 @@
--[[
From Stamina mod
Copyright (C) BlockMen (2013-2015)
Copyright (C) Auke Kok <sofar@foo-projects.org> (2016)
Copyright (C) Minetest Mods Team (2016-2019)
Copyright (C) MultiCraft Development Team (2016-2020)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 3.0 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
]]
if not core.settings:get_bool("enable_damage")
or not core.settings:get_bool("enable_hunger") then
return
end
hunger = {}
local function get_setting(key, default)
local setting = core.settings:get("hunger." .. key)
return tonumber(setting) or default
end
hunger.settings = {
-- see settingtypes.txt for descriptions
tick = get_setting("tick", 600),
tick_min = get_setting("tick_min", 4),
health_tick = get_setting("health_tick", 4),
move_tick = get_setting("move_tick", 0.5),
poison_tick = get_setting("poison_tick", 1),
exhaust_dig = get_setting("exhaust_dig", 2),
exhaust_place = get_setting("exhaust_place", 1),
exhaust_move = get_setting("exhaust_move", 2),
exhaust_jump = get_setting("exhaust_jump", 4),
exhaust_craft = get_setting("exhaust_craft", 2),
exhaust_punch = get_setting("exhaust_punch", 5),
exhaust_lvl = get_setting("exhaust_lvl", 192),
heal = get_setting("heal", 1),
heal_lvl = get_setting("heal_lvl", 5),
starve = get_setting("starve", 1),
starve_lvl = get_setting("starve_lvl", 2),
level_max = get_setting("level_max", 21),
visual_max = get_setting("visual_max", 20)
}
local settings = hunger.settings
local min, max = math.min, math.max
local vlength = vector.length
local attribute = {
saturation = "hunger:level",
poisoned = "hunger:poisoned",
exhaustion = "hunger:exhaustion"
}
local function is_player(player)
return (
minetest.is_player(player) and
not player.is_fake_player)
end
local function get_int_attribute(player, key)
local level = player:get_attribute(key)
if level then
return tonumber(level)
else
return nil
end
end
--- SATURATION API ---
function hunger.get_saturation(player)
return get_int_attribute(player, attribute.saturation)
end
function hunger.set_saturation(player, level)
player:set_attribute(attribute.saturation, level)
hud.change_item(player, "hunger", {number = min(settings.visual_max, level)})
end
hunger.registered_on_update_saturations = {}
function hunger.register_on_update_saturation(fun)
local saturations = hunger.registered_on_update_saturations
saturations[#saturations+1] = fun
end
function hunger.update_saturation(player, level)
for _, callback in pairs(hunger.registered_on_update_saturations) do
local result = callback(player, level)
if result then
return result
end
end
local old = hunger.get_saturation(player)
if not old or old == level then -- To suppress HUD update
return
end
-- players without interact priv cannot eat
if old < settings.heal_lvl and not core.check_player_privs(player, {interact=true}) then
return
end
hunger.set_saturation(player, level)
end
function hunger.change_saturation(player, change)
if not is_player(player) or not change or change == 0 then
return false
end
local level = hunger.get_saturation(player) + change or 0
level = max(level, 0)
level = min(level, settings.level_max)
hunger.update_saturation(player, level)
return true
end
hunger.change = hunger.change_saturation -- for backwards compatablity
--- END SATURATION API ---
--- POISON API ---
function hunger.is_poisoned(player)
return player:get_attribute(attribute.poisoned) == "yes"
end
function hunger.set_poisoned(player, poisoned)
if poisoned then
hud.change_item(player, "hunger", {text = "hunger_poisen.png"})
player:set_attribute(attribute.poisoned, "yes")
else
hud.change_item(player, "hunger", {text = "hunger.png"})
player:set_attribute(attribute.poisoned, "no")
end
end
local function poison_tick(player, ticks, interval, elapsed)
if not hunger.is_poisoned(player) then
return
elseif elapsed > ticks then
hunger.set_poisoned(player, false)
else
local hp = player:get_hp() - 1
if hp > 0 then
player:set_hp(hp)
end
core.after(interval, poison_tick, player, ticks, interval, elapsed + 1)
end
end
hunger.registered_on_poisons = {}
function hunger.register_on_poison(fun)
local poison = hunger.registered_on_poisons
poison[#poison+1] = fun
end
function hunger.poison(player, ticks, interval)
for _, fun in pairs(hunger.registered_on_poisons) do
local rv = fun(player, ticks, interval)
if rv == true then
return
end
end
if not is_player(player) then
return
end
hunger.set_poisoned(player, true)
poison_tick(player, ticks, interval, 0)
end
--- END POISON API ---
--- EXHAUSTION API ---
hunger.exhaustion_reasons = {
craft = "craft",
dig = "dig",
heal = "heal",
jump = "jump",
move = "move",
place = "place",
punch = "punch"
}
function hunger.get_exhaustion(player)
return get_int_attribute(player, attribute.exhaustion)
end
function hunger.set_exhaustion(player, exhaustion)
player:set_attribute(attribute.exhaustion, exhaustion)
end
hunger.registered_on_exhaust_players = {}
function hunger.register_on_exhaust_player(fun)
local exhaust = hunger.registered_on_exhaust_players
exhaust[#exhaust+1] = fun
end
function hunger.exhaust_player(player, change, cause)
for _, callback in pairs(hunger.registered_on_exhaust_players) do
local result = callback(player, change, cause)
if result then
return result
end
end
if not is_player(player) then
return
end
local exhaustion = hunger.get_exhaustion(player) or 0
exhaustion = exhaustion + change
if exhaustion >= settings.exhaust_lvl then
exhaustion = exhaustion - settings.exhaust_lvl
hunger.change(player, -1)
end
hunger.set_exhaustion(player, exhaustion)
end
--- END EXHAUSTION API ---
-- Time based hunger functions
local connected_players = core.get_connected_players
local function move_tick()
for _, player in pairs(connected_players()) do
local controls = player:get_player_control()
local is_moving = controls.up or controls.down or controls.left or controls.right
local velocity = player:get_player_velocity()
velocity.y = 0
local horizontal_speed = vlength(velocity)
local has_velocity = horizontal_speed > 0.05
if controls.jump then
hunger.exhaust_player(player, settings.exhaust_jump, hunger.exhaustion_reasons.jump)
elseif is_moving and has_velocity then
hunger.exhaust_player(player, settings.exhaust_move, hunger.exhaustion_reasons.move)
end
end
end
local function hunger_tick()
-- lower saturation by 1 point after settings.tick seconds
for _, player in pairs(connected_players()) do
local saturation = hunger.get_saturation(player) or 0
if saturation > settings.tick_min then
hunger.update_saturation(player, saturation - 1)
end
end
end
local function health_tick()
-- heal or damage player, depending on saturation
for _, player in pairs(connected_players()) do
local air = player:get_breath() or 0
local hp = player:get_hp() or 0
local saturation = hunger.get_saturation(player) or 0
-- don't heal if dead, drowning, or poisoned
local should_heal = (
saturation >= settings.heal_lvl and
hp > 0 and
hp < 20 and
air > 0
and not hunger.is_poisoned(player)
)
-- or damage player by 1 hp if saturation is < 2 (of 21)
local is_starving = (
saturation < settings.starve_lvl and
hp > 0
)
if should_heal then
player:set_hp(hp + settings.heal)
elseif is_starving then
player:set_hp(hp - settings.starve)
hud.change_item(player, "hunger", {number = 0})
core.after(0.5, function()
hud.change_item(player, "hunger", {number = min(settings.visual_max, saturation)})
end)
end
end
end
local hunger_timer = 0
local health_timer = 0
local action_timer = 0
core.register_globalstep(function(dtime)
hunger_timer = hunger_timer + dtime
health_timer = health_timer + dtime
action_timer = action_timer + dtime
if action_timer > settings.move_tick then
action_timer = 0
move_tick()
end
if hunger_timer > settings.tick then
hunger_timer = 0
hunger_tick()
end
if health_timer > settings.health_tick then
health_timer = 0
health_tick()
end
end)
function hunger.item_eat(hp_change, user, poison)
if not poison then
hunger.change_saturation(user, hp_change)
hunger.set_exhaustion(user, 0)
else
hunger.change_saturation(user, hp_change)
hunger.poison(user, -poison, settings.poison_tick)
end
end
hud.register("hunger", {
hud_elem_type = "statbar",
position = {x = 0.5, y = 1},
alignment = {x = -1, y = -1},
offset = {x = 8, y = -94},
size = {x = 24, y = 24},
text = "hunger.png",
background = "hunger_gone.png",
number = settings.visual_max
})
core.register_on_joinplayer(function(player)
local level = hunger.get_saturation(player) or settings.level_max
-- reset poisoned
hunger.set_poisoned(player, false)
-- set saturation
player:set_attribute(attribute.saturation, level)
-- we must manually update the HUD
if level and (level < settings.visual_max) then
core.after(1, function()
hud.change_item(player, "hunger", {number = min(settings.visual_max, level)})
end)
end
end)
local exhaust = hunger.exhaust_player
core.register_on_placenode(function(_, _, player)
exhaust(player, settings.exhaust_place, hunger.exhaustion_reasons.place)
end)
core.register_on_dignode(function(_, _, player)
exhaust(player, settings.exhaust_dig, hunger.exhaustion_reasons.dig)
end)
core.register_on_craft(function(_, player)
exhaust(player, settings.exhaust_craft, hunger.exhaustion_reasons.craft)
end)
core.register_on_punchplayer(function(_, hitter)
exhaust(hitter, settings.exhaust_punch, hunger.exhaustion_reasons.punch)
end)
core.register_on_respawnplayer(function(player)
hunger.set_saturation(player, settings.level_max)
hunger.set_exhaustion(player, 0)
hunger.set_poisoned(player, false)
end)

View File

@ -1,4 +1,3 @@
local scriptpath = core.get_builtin_path()
local commonpath = scriptpath .. "common" .. DIR_DELIM
local gamepath = scriptpath .. "game".. DIR_DELIM
@ -32,8 +31,10 @@ assert(loadfile(gamepath .. "falling.lua"))(builtin_shared)
dofile(gamepath .. "features.lua")
dofile(gamepath .. "voxelarea.lua")
dofile(gamepath .. "forceloading.lua")
dofile(gamepath .. "hud.lua")
dofile(gamepath .. "statbars.lua")
dofile(gamepath .. "knockback.lua")
dofile(gamepath .. "hunger.lua")
dofile(gamepath .. "sscsm" .. DIR_DELIM .. "init.lua")
profiler = nil

View File

@ -2,11 +2,19 @@
local builtin_shared = ...
local abs, atan2, cos, floor, max, sin, random =
math.abs, math.atan2, math.cos, math.floor, math.max, math.sin, math.random
local vadd, vnew, vmultiply, vnormalize, vsubtract =
vector.add, vector.new, vector.multiply, vector.normalize, vector.subtract
local creative_mode = core.settings:get_bool("creative_mode")
local node_drop = core.settings:get_bool("node_drop", true)
local function copy_pointed_thing(pointed_thing)
return {
type = pointed_thing.type,
above = vector.new(pointed_thing.above),
under = vector.new(pointed_thing.under),
above = vnew(pointed_thing.above),
under = vnew(pointed_thing.under),
ref = pointed_thing.ref,
}
end
@ -39,11 +47,11 @@ end
function core.dir_to_facedir(dir, is6d)
--account for y if requested
if is6d and math.abs(dir.y) > math.abs(dir.x) and math.abs(dir.y) > math.abs(dir.z) then
if is6d and abs(dir.y) > abs(dir.x) and abs(dir.y) > abs(dir.z) then
--from above
if dir.y < 0 then
if math.abs(dir.x) > math.abs(dir.z) then
if abs(dir.x) > abs(dir.z) then
if dir.x < 0 then
return 19
else
@ -59,7 +67,7 @@ function core.dir_to_facedir(dir, is6d)
--from below
else
if math.abs(dir.x) > math.abs(dir.z) then
if abs(dir.x) > abs(dir.z) then
if dir.x < 0 then
return 15
else
@ -75,7 +83,7 @@ function core.dir_to_facedir(dir, is6d)
end
--otherwise, place horizontally
elseif math.abs(dir.x) > math.abs(dir.z) then
elseif abs(dir.x) > abs(dir.z) then
if dir.x < 0 then
return 3
else
@ -113,13 +121,13 @@ function core.facedir_to_dir(facedir)
end
function core.dir_to_wallmounted(dir)
if math.abs(dir.y) > math.max(math.abs(dir.x), math.abs(dir.z)) then
if abs(dir.y) > max(abs(dir.x), abs(dir.z)) then
if dir.y < 0 then
return 1
else
return 0
end
elseif math.abs(dir.x) > math.abs(dir.z) then
elseif abs(dir.x) > abs(dir.z) then
if dir.x < 0 then
return 3
else
@ -148,11 +156,11 @@ function core.wallmounted_to_dir(wallmounted)
end
function core.dir_to_yaw(dir)
return -math.atan2(dir.x, dir.z)
return -atan2(dir.x, dir.z)
end
function core.yaw_to_dir(yaw)
return {x = -math.sin(yaw), y = 0, z = math.cos(yaw)}
return {x = -sin(yaw), y = 0, z = cos(yaw)}
end
function core.is_colored_paramtype(ptype)
@ -165,9 +173,9 @@ function core.strip_param2_color(param2, paramtype2)
return nil
end
if paramtype2 == "colorfacedir" then
param2 = math.floor(param2 / 32) * 32
param2 = floor(param2 / 32) * 32
elseif paramtype2 == "colorwallmounted" then
param2 = math.floor(param2 / 8) * 8
param2 = floor(param2 / 8) * 8
end
-- paramtype2 == "color" requires no modification.
return param2
@ -210,7 +218,7 @@ function core.get_node_drops(node, toolname)
local good_rarity = true
local good_tool = true
if item.rarity ~= nil then
good_rarity = item.rarity < 1 or math.random(item.rarity) == 1
good_rarity = item.rarity < 1 or random(item.rarity) == 1
end
if item.tools ~= nil then
good_tool = false
@ -347,7 +355,7 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2,
color_divisor = 32
end
if color_divisor then
local color = math.floor(metatable.palette_index / color_divisor)
local color = floor(metatable.palette_index / color_divisor)
local other = newnode.param2 % color_divisor
newnode.param2 = color * color_divisor + other
end
@ -406,16 +414,6 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2,
return itemstack, place_to
end
-- deprecated, item_place does not call this
function core.item_place_object(itemstack, placer, pointed_thing)
local pos = core.get_pointed_thing_position(pointed_thing, true)
if pos ~= nil then
local item = itemstack:take_item()
core.add_item(pos, item)
end
return itemstack
end
function core.item_place(itemstack, placer, pointed_thing, param2)
-- Call on_rightclick if the pointed node defines it
if pointed_thing.type == "node" and placer and
@ -439,10 +437,103 @@ function core.item_secondary_use(itemstack, placer)
return itemstack
end
local function item_throw_step(entity, dtime)
entity.throw_timer = entity.throw_timer + dtime
if entity.throw_timer > 20 then
entity.object:remove()
return
end
if not entity.thrower then
return
end
local pos = entity.object:get_pos()
if not core.is_valid_pos(pos) then
entity.object:remove()
return
end
local hit_object
local dir = vnormalize(entity.object:get_velocity())
local pos2 = vadd(pos, vmultiply(dir, 3))
local _, node_pos = core.line_of_sight(pos, pos2)
if node_pos then
local def = core.get_node(node_pos)
if def then
pos = vsubtract(node_pos, vmultiply(dir, 1.5))
entity.object:move_to(pos)
else
node_pos = nil
end
end
local objs = core.get_objects_inside_radius(pos, 1.5)
for _, obj in pairs(objs) do
if obj:is_player() then
local name = obj:get_player_name()
if name ~= entity.thrower then
hit_object = obj
break
end
elseif obj:get_luaentity() ~= nil and
obj:get_luaentity().name ~= "__builtin:throwing_item" and
obj:get_luaentity().name ~= "__builtin:item" then
hit_object = obj
break
end
end
if hit_object or node_pos then
local player = core.get_player_by_name(entity.thrower)
entity.on_impact(player, pos, entity.throw_direction, hit_object)
entity.object:remove()
end
end
function core.item_throw(name, thrower, speed, accel, on_impact)
if not thrower or not thrower:is_player() then
return
end
local pos = thrower:get_pos()
if not core.is_valid_pos(pos) then
return
end
pos.y = pos.y + 1.5
local obj
local properties = {is_visible=true}
if core.registered_entities[name] then
obj = core.add_entity(pos, name)
elseif core.registered_items[name] then
obj = core.add_entity(pos, "__builtin:throwing_item")
properties.textures = {name}
else
return
end
if obj then
local ent = obj:get_luaentity()
if ent then
local s = speed or 19 -- default speed
local a = accel or -3 -- default acceleration
local dir = thrower:get_look_dir()
local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81
ent.thrower = thrower:get_player_name()
ent.throw_timer = 0
ent.throw_direction = dir
ent.on_step = item_throw_step
ent.on_impact = on_impact and on_impact or function() end
obj:set_properties(properties)
obj:set_velocity({x=dir.x * s, y=dir.y * s, z=dir.z * s})
obj:set_acceleration({x=dir.x * a, y=-gravity, z=dir.z * a})
return obj
else
obj:remove()
end
end
end
function core.item_drop(itemstack, dropper, pos)
local dropper_is_player = dropper and dropper:is_player()
local p = table.copy(pos)
local cnt = itemstack:get_count()
if not core.is_valid_pos(p) then
return
end
if dropper_is_player then
p.y = p.y + 1.2
end
@ -450,12 +541,19 @@ function core.item_drop(itemstack, dropper, pos)
local obj = core.add_item(p, item)
if obj then
if dropper_is_player then
local vel = dropper:get_player_velocity()
local dir = dropper:get_look_dir()
dir.x = dir.x * 2.9
dir.y = dir.y * 2.9 + 2
dir.z = dir.z * 2.9
dir.x = vel.x + dir.x * 4
dir.y = vel.y + dir.y * 4 + 2
dir.z = vel.z + dir.z * 4
obj:set_velocity(dir)
obj:get_luaentity().dropped_by = dropper:get_player_name()
else
obj:set_velocity({
x = random(-2, 2),
y = random(2, 4),
z = random(-2, 2)
})
end
return itemstack
end
@ -463,7 +561,8 @@ function core.item_drop(itemstack, dropper, pos)
-- environment failed
end
function core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing)
local enable_hunger = core.settings:get_bool("enable_damage") and core.settings:get_bool("enable_hunger")
function core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing, poison)
for _, callback in pairs(core.registered_on_item_eats) do
local result = callback(hp_change, replace_with_item, itemstack, user, pointed_thing)
if result then
@ -472,15 +571,47 @@ function core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed
end
local def = itemstack:get_definition()
if itemstack:take_item() ~= nil then
user:set_hp(user:get_hp() + hp_change)
if enable_hunger then
hunger.item_eat(hp_change, user, poison)
else
user:set_hp(user:get_hp() + hp_change)
end
local pos = user:get_pos()
if not core.is_valid_pos(pos) then
return itemstack
end
if def and def.sound and def.sound.eat then
core.sound_play(def.sound.eat, {
pos = user:get_pos(),
max_hear_distance = 16
}, true)
pos = pos,
max_hear_distance = 16})
else
core.sound_play("player_eat", {
pos = pos,
max_hear_distance = 10,
gain = 0.3})
end
local dir = user:get_look_dir()
local ppos = {x = pos.x, y = pos.y + 1.3, z = pos.z}
core.add_particlespawner({
amount = 20,
time = 0.1,
minpos = ppos,
maxpos = ppos,
minvel = {x = dir.x - 1, y = 2, z = dir.z - 1},
maxvel = {x = dir.x + 1, y = 2, z = dir.z + 1},
minacc = {x = 0, y = -5, z = 0},
maxacc = {x = 0, y = -9, z = 0},
minexptime = 1,
maxexptime = 1,
minsize = 1,
maxsize = 1,
vertical = false,
texture = def.inventory_image
})
if replace_with_item then
if itemstack:is_empty() then
itemstack:add_item(replace_with_item)
@ -490,8 +621,7 @@ function core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed
if inv and inv:room_for_item("main", {name=replace_with_item}) then
inv:add_item("main", replace_with_item)
else
local pos = user:get_pos()
pos.y = math.floor(pos.y + 0.5)
pos.y = pos.y + 0.5
core.add_item(pos, replace_with_item)
end
end
@ -500,10 +630,10 @@ function core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed
return itemstack
end
function core.item_eat(hp_change, replace_with_item)
function core.item_eat(hp_change, replace_with_item, poison)
return function(itemstack, user, pointed_thing) -- closure
if user then
return core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing)
return core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing, poison)
end
end
end
@ -512,7 +642,7 @@ function core.node_punch(pos, node, puncher, pointed_thing)
-- Run script hook
for _, callback in ipairs(core.registered_on_punchnodes) do
-- Copy pos and node because callback can modify them
local pos_copy = vector.new(pos)
local pos_copy = vnew(pos)
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
local pointed_thing_copy = pointed_thing and copy_pointed_thing(pointed_thing) or nil
callback(pos_copy, node_copy, puncher, pointed_thing_copy)
@ -523,7 +653,7 @@ function core.handle_node_drops(pos, drops, digger)
-- Add dropped items to object's inventory
local inv = digger and digger:get_inventory()
local give_item
if inv then
if (not node_drop or creative_mode) and inv then
give_item = function(item)
return inv:add_item("main", item)
end
@ -537,12 +667,7 @@ function core.handle_node_drops(pos, drops, digger)
for _, dropped_item in pairs(drops) do
local left = give_item(dropped_item)
if not left:is_empty() then
local p = {
x = pos.x + math.random()/2-0.25,
y = pos.y + math.random()/2-0.25,
z = pos.z + math.random()/2-0.25,
}
core.add_item(p, left)
core.item_drop(left, nil, pos)
end
end
end
@ -675,7 +800,7 @@ end
-- Item definition defaults
--
local default_stack_max = tonumber(minetest.settings:get("default_stack_max")) or 99
local default_stack_max = tonumber(minetest.settings:get("default_stack_max")) or 64
core.nodedef_default = {
-- Item properties

View File

@ -1,5 +1,7 @@
-- Minetest: builtin/item_entity.lua
local abs, min, floor, random, pi = math.abs, math.min, math.floor, math.random, math.pi
local vnormalize = vector.normalize
function core.spawn_item(pos, item)
-- Take item in any format
local stack = ItemStack(item)
@ -14,9 +16,53 @@ end
-- If item_entity_ttl is not set, enity will have default life time
-- Setting it to -1 disables the feature
local time_to_live = tonumber(core.settings:get("item_entity_ttl")) or 900
local time_to_live = tonumber(core.settings:get("item_entity_ttl")) or 600
local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81
local collection = core.settings:get_bool("item_collection", true)
local water_flow = core.settings:get_bool("item_water_flow", true)
local lava_destroy = core.settings:get_bool("item_lava_destroy", true)
-- Water flow functions, based on QwertyMine3 (WTFPL), and TenPlus1 (MIT) mods
local function quick_flow_logic(node, pos_testing, dir)
local node_testing = core.get_node_or_nil(pos_testing)
if not node_testing then return 0 end
local liquid = core.registered_nodes[node_testing.name] and core.registered_nodes[node_testing.name].liquidtype
if not liquid or liquid ~= "flowing" and liquid ~= "source" then
return 0
end
local sum = node.param2 - node_testing.param2
return (sum < -6 or (sum < 6 and sum > 0) or sum == 0) and dir or -dir
end
local function quick_flow(pos, node)
local x, z = 0, 0
x = x + quick_flow_logic(node, {x = pos.x - 1.01, y = pos.y, z = pos.z}, -1)
x = x + quick_flow_logic(node, {x = pos.x + 1.01, y = pos.y, z = pos.z}, 1)
z = z + quick_flow_logic(node, {x = pos.x, y = pos.y, z = pos.z - 1.01}, -1)
z = z + quick_flow_logic(node, {x = pos.x, y = pos.y, z = pos.z + 1.01}, 1)
return vnormalize({x = x, y = 0, z = z})
end
core.register_entity(":__builtin:throwing_item", {
physical = false,
visual = "wielditem",
collisionbox = {0, 0, 0, 0, 0, 0},
textures = {""},
visual_size = {x = 0.4, y = 0.4},
is_visible = false,
on_activate = function(self, staticdata)
if staticdata == "expired" then
self.object:remove()
end
end,
get_staticdata = function()
return "expired"
end
})
core.register_entity(":__builtin:item", {
initial_properties = {
@ -52,10 +98,10 @@ core.register_entity(":__builtin:item", {
local itemname = stack:is_known() and stack:get_name() or "unknown"
local max_count = stack:get_stack_max()
local count = math.min(stack:get_count(), max_count)
local count = min(stack:get_count(), max_count)
local size = 0.2 + 0.1 * (count / max_count) ^ (1 / 3)
local def = core.registered_nodes[itemname]
local glow = def and math.floor(def.light_source / 2 + 0.5)
local glow = def and floor(def.light_source / 2 + 0.5)
self.object:set_properties({
is_visible = true,
@ -63,9 +109,10 @@ core.register_entity(":__builtin:item", {
textures = {itemname},
visual_size = {x = size, y = size},
collisionbox = {-size, -size, -size, size, size, size},
automatic_rotate = math.pi * 0.5 * 0.2 / size,
automatic_rotate = pi * 0.5 * 0.15 / size,
wield_item = self.itemstring,
glow = glow,
infotext = core.registered_items[itemname].description
})
end,
@ -79,7 +126,7 @@ core.register_entity(":__builtin:item", {
end,
on_activate = function(self, staticdata, dtime_s)
if string.sub(staticdata, 1, string.len("return")) == "return" then
if staticdata:sub(1, 6) == "return" then
local data = core.deserialize(staticdata)
if data and type(data) == "table" then
self.itemstring = data.itemstring
@ -89,7 +136,7 @@ core.register_entity(":__builtin:item", {
else
self.itemstring = staticdata
end
self.object:set_armor_groups({immortal = 1})
self.object:set_armor_groups({immortal = 1, silent = 1})
self.object:set_velocity({x = 0, y = 2, z = 0})
self.object:set_acceleration({x = 0, y = -gravity, z = 0})
self:set_item()
@ -165,6 +212,7 @@ core.register_entity(":__builtin:item", {
y = pos.y + self.object:get_properties().collisionbox[2] - 0.05,
z = pos.z
})
local node_inside = core.get_node_or_nil(pos)
-- Delete in 'ignore' nodes
if node and node.name == "ignore" then
self.itemstring = ""
@ -264,16 +312,64 @@ core.register_entity(":__builtin:item", {
end
end
-- Slide on slippery nodes
local vel = self.object:get_velocity()
local def = node and core.registered_nodes[node.name]
local def_inside = node_inside and core.registered_nodes[node_inside.name]
-- local is_moving = (def and not def.walkable) or
-- vel.x ~= 0 or vel.y ~= 0 or vel.z ~= 0
-- local is_slippery = false
-- Destroy item when dropped into lava
if lava_destroy and def_inside
and def_inside.groups and def_inside.groups.lava then
core.sound_play("default_cool_lava", {
pos = pos, max_hear_distance = 10})
self.object:remove()
core.add_particlespawner({
amount = 3,
time = 0.1,
minpos = {x = pos.x - 0.1, y = pos.y + 0.1, z = pos.z - 0.1},
maxpos = {x = pos.x + 0.1, y = pos.y + 0.2, z = pos.z + 0.1},
minvel = {x = 0, y = 2.5, z = 0},
maxvel = {x = 0, y = 2.5, z = 0},
minacc = {x = -0.15, y = -0.02, z = -0.15},
maxacc = {x = 0.15, y = -0.01, z = 0.15},
minexptime = 4,
maxexptime = 6,
minsize = 2,
maxsize = 4,
texture = "item_smoke.png"
})
return
end
-- Moving items in the water flow (TenPlus1, MIT)
if water_flow and def_inside and def_inside.liquidtype == "flowing" then
local vec = quick_flow(pos, node_inside)
self.object:set_velocity({x = vec.x, y = vel.y, z = vec.z})
return
end
-- Move item inside node to free space (TenPlus1, MIT)
--[[if not self.stuck and def_inside and def_inside.walkable and
not def_inside.liquid and node_inside.name ~= "air" and
def_inside.drawtype == "normal" then
local npos = core.find_node_near(pos, 1, "air")
if npos then
self.object:move_to(npos)
else
self.stuck = true
end
end]]
-- Slide on slippery nodes
local keep_movement = false
if def then
local slippery = core.get_item_group(node.name, "slippery")
local vel = self.object:get_velocity()
if slippery ~= 0 and (math.abs(vel.x) > 0.1 or math.abs(vel.z) > 0.1) then
if slippery ~= 0 and (abs(vel.x) > 0.1 or abs(vel.z) > 0.1) then
-- Horizontal deceleration
local factor = math.min(4 / (slippery + 4) * dtime, 1)
local factor = min(4 / (slippery + 4) * dtime, 1)
self.object:set_velocity({
x = vel.x * (1 - factor),
y = 0,
@ -302,7 +398,7 @@ core.register_entity(":__builtin:item", {
if own_stack:get_free_space() == 0 then
return
end
local objects = core.get_objects_inside_radius(pos, 1.0)
local objects = core.get_objects_inside_radius(pos, 0.5)
for k, obj in pairs(objects) do
local entity = obj:get_luaentity()
if entity and entity.name == "__builtin:item" then
@ -329,3 +425,57 @@ core.register_entity(":__builtin:item", {
self.object:remove()
end,
})
-- Item Collection
if collection then
local function collect_items(player)
local ppos = player:get_pos()
ppos.y = ppos.y + 1.3
if not core.is_valid_pos(ppos) then
return
end
-- Detect
local objects = core.get_objects_inside_radius(ppos, 2)
for _, obj in pairs(objects) do
local entity = obj:get_luaentity()
if entity and entity.name == "__builtin:item" and
not entity.collectioner and
entity.age and entity.age > 0.5 then
local item = ItemStack(entity.itemstring)
local inv = player:get_inventory()
if item:get_name() ~= "" and
inv and inv:room_for_item("main", item) then
-- Magnet
obj:move_to(ppos)
entity.collectioner = true
-- Collect
if entity.collectioner == true then
core.after(0.05, function()
core.sound_play("item_drop_pickup", {
pos = ppos,
max_hear_distance = 10,
gain = 0.2,
pitch = random(60,100)/100
})
entity.itemstring = ""
obj:remove()
item = inv:add_item("main", item)
if not item:is_empty() then
core.item_drop(item, player, ppos)
end
end)
end
end
end
end
end
core.register_playerstep(function(dtime, playernames)
for _, name in pairs(playernames) do
local player = core.get_player_by_name(name)
if player and player:is_player() and player:get_hp() > 0 then
collect_items(player)
end
end
end, core.is_singleplayer()) -- Force step in singlplayer mode only
end

View File

@ -17,14 +17,15 @@ function core.calculate_knockback(player, hitter, time_from_last_punch, tool_cap
return res
end
local max, abs = math.max, math.abs
local function vector_absmax(v)
local max, abs = math.max, math.abs
return max(max(abs(v.x), abs(v.y)), abs(v.z))
end
core.register_on_punchplayer(function(player, hitter, time_from_last_punch, tool_capabilities, unused_dir, damage)
if player:get_hp() == 0 then
return -- RIP
if player:get_hp() == 0
or player:get_animation().range ~= "stand" then
return -- RIP or attached
end
-- Server::handleCommand_Interact() adds eye offset to one but not the other

View File

@ -42,13 +42,13 @@ end
function core.send_join_message(player_name)
if not core.is_singleplayer() then
core.chat_send_all("*** " .. player_name .. " joined the game.")
core.chat_send_all("=> " .. player_name .. " has joined the server")
end
end
function core.send_leave_message(player_name, timed_out)
local announcement = "*** " .. player_name .. " left the game."
local announcement = "<= " .. player_name .. " left the server"
if timed_out then
announcement = announcement .. " (timed out)"
end
@ -109,6 +109,22 @@ function core.get_player_radius_area(player_name, radius)
end
local mapgen_limit = tonumber(core.settings:get("mapgen_limit"))
function core.is_valid_pos(pos)
if not pos or type(pos) ~= "table" then
return false
end
for _, v in pairs({"x", "y", "z"}) do
if not pos[v] or pos[v] ~= pos[v] or
pos[v] < -mapgen_limit or pos[v] > mapgen_limit then
return false
end
end
return true
end
function core.hash_node_position(pos)
return (pos.z + 32768) * 65536 * 65536
+ (pos.y + 32768) * 65536
@ -157,6 +173,9 @@ function core.is_protected(pos, name)
return false
end
function core.is_protected_action()
return false
end
function core.record_protection_violation(pos, name)
for _, func in pairs(core.registered_on_protection_violation) do

View File

@ -28,6 +28,11 @@ function core.register_privilege(name, param)
core.registered_privileges[name] = def
end
local creative = false
if core.settings:get_bool("creative_mode") then
creative = true
end
core.register_privilege("interact", "Can interact with things and modify the world")
core.register_privilege("shout", "Can speak in chat")
core.register_privilege("basic_privs", "Can modify 'shout' and 'interact' privileges")
@ -35,7 +40,7 @@ core.register_privilege("privs", "Can modify privileges")
core.register_privilege("teleport", {
description = "Can teleport self",
give_to_singleplayer = false,
give_to_singleplayer = creative,
})
core.register_privilege("bring", {
description = "Can teleport other players",
@ -43,7 +48,7 @@ core.register_privilege("bring", {
})
core.register_privilege("settime", {
description = "Can set the time of day using /time",
give_to_singleplayer = false,
give_to_singleplayer = creative,
})
core.register_privilege("server", {
description = "Can do server maintenance stuff",
@ -75,11 +80,11 @@ core.register_privilege("password", {
})
core.register_privilege("fly", {
description = "Can use fly mode",
give_to_singleplayer = false,
give_to_singleplayer = creative,
})
core.register_privilege("fast", {
description = "Can use fast mode",
give_to_singleplayer = false,
give_to_singleplayer = creative,
})
core.register_privilege("noclip", {
description = "Can fly through solid nodes using noclip mode",

View File

@ -107,6 +107,9 @@ function core.register_entity(name, prototype)
prototype.mod_origin = core.get_current_modname() or "??"
end
-- Intllib
Sl = intllib.make_gettext_pair("locales")
function core.register_item(name, itemdef)
-- Check name
if name == nil then
@ -134,6 +137,9 @@ function core.register_item(name, itemdef)
core.log("warning", "Node 'light_source' value exceeds maximum," ..
" limiting to maximum: " ..name)
end
if itemdef.light_source == nil then
itemdef.light_source = 0
end
setmetatable(itemdef, {__index = core.nodedef_default})
core.registered_nodes[itemdef.name] = itemdef
elseif itemdef.type == "craft" then
@ -153,6 +159,12 @@ function core.register_item(name, itemdef)
itemdef.paramtype2 = "flowingliquid"
end
-- Intllib
if itemdef.description and itemdef.description ~= "" then
itemdef.description = Sl(itemdef.description:gsub("@", "\001"))
:gsub("\001", "@")
end
-- BEGIN Legacy stuff
if itemdef.cookresult_itemstring ~= nil and itemdef.cookresult_itemstring ~= "" then
core.register_craft({
@ -344,8 +356,8 @@ core.register_item(":unknown", {
core.register_node(":air", {
description = "Air",
inventory_image = "air.png",
wield_image = "air.png",
inventory_image = "blank.png",
wield_image = "blank.png",
drawtype = "airlike",
paramtype = "light",
sunlight_propagates = true,
@ -356,6 +368,7 @@ core.register_node(":air", {
floodable = true,
air_equivalent = true,
drop = "",
drowning = 0,
groups = {not_in_creative_inventory=1},
})
@ -372,6 +385,7 @@ core.register_node(":ignore", {
buildable_to = true, -- A way to remove accidentally placed ignores
air_equivalent = true,
drop = "",
drowning = 0,
groups = {not_in_creative_inventory=1},
on_place = function(itemstack, placer, pointed_thing)
core.chat_send_player(
@ -385,7 +399,11 @@ core.register_node(":ignore", {
-- The hand (bare definition)
core.register_item(":", {
type = "none",
wield_image = "wieldhand.png",
wield_image = "blank.png",
tool_capabilities = {
full_punch_interval = 0.5,
damage_groups = {fleshy = core.settings:get_bool("creative_mode") and 5 or 1}
},
groups = {not_in_creative_inventory=1},
})
@ -408,6 +426,15 @@ function core.override_item(name, redefinition)
end
function core.add_group(name, adding)
local addgroup = table.copy(core.registered_items[name].groups) or {}
for k, v in pairs(adding) do
addgroup[k] = v
end
core.override_item(name, {groups = addgroup})
end
core.callback_origins = {}
function core.run_callbacks(callbacks, mode, ...)
@ -613,6 +640,102 @@ core.registered_on_modchannel_message, core.register_on_modchannel_message = mak
core.registered_on_player_inventory_actions, core.register_on_player_inventory_action = make_registration()
core.registered_allow_player_inventory_actions, core.register_allow_player_inventory_action = make_registration()
--
-- Player step iteration
--
players_per_step = tonumber(core.settings:get("players_per_globalstep")) or 20
local player_iter
local player_iter_forced
local playerstep_iter
local playerstep_funcs = {}
local playerstep_funcs_forced = {}
local playernames = {}
local playernames_forced = {}
core.register_playerstep = function(func, force)
local funcs = force and playerstep_funcs_forced or playerstep_funcs
funcs[#funcs + 1] = func
end
local function table_iter(t)
local i = 0
local n = table.getn(t)
return function ()
i = i + 1
if i <= n then
return t[i]
end
end
end
function core.get_player_iter()
local names = {}
for player in table_iter(core.get_connected_players()) do
local name = player:get_player_name()
if name then
names[#names + 1] = name
end
end
return table_iter(names)
end
local function get_playerstep_func()
if playerstep_iter == nil then
playerstep_iter = table_iter(playerstep_funcs)
playernames = {}
for _ = 1, players_per_step do
if player_iter == nil then
player_iter = core.get_player_iter()
end
local name = player_iter()
if not name then
player_iter = nil
break
end
playernames[#playernames + 1] = name
end
end
local func = playerstep_iter()
playerstep_iter = func and playerstep_iter
return func or get_playerstep_func()
end
-- Run playerstep callbacks
core.register_globalstep(function(dtime)
-- Run forced callbacks
if #playerstep_funcs_forced ~= 0 then
playernames_forced = {}
for _ = 1, players_per_step do
if player_iter_forced == nil then
player_iter_forced = core.get_player_iter()
end
local name = player_iter_forced()
if not name then
player_iter_forced = nil
break
end
playernames_forced[#playernames_forced + 1] = name
end
for func in table_iter(playerstep_funcs_forced) do
if type(func) == "function" then
func(dtime, playernames_forced)
end
end
end
if #playerstep_funcs ~= 0 then
-- Run single step callbacks
local func = get_playerstep_func()
if type(func) == "function" then
func(dtime, playernames)
end
end
end)
--
-- Compatibility for on_mapgen_init()
--

View File

@ -1,41 +1,30 @@
-- cache setting
local enable_damage = core.settings:get_bool("enable_damage")
local disable_health = false
local health_bar_definition = {
hud_elem_type = "statbar",
position = {x = 0.5, y = 1},
text = "heart.png",
text2 = "heart_gone.png",
number = core.PLAYER_MAX_HP_DEFAULT,
item = core.PLAYER_MAX_HP_DEFAULT,
direction = 0,
size = {x = 24, y = 24},
offset = {x = (-10 * 24) - 25, y = -(48 + 24 + 16)},
position = {x = 0.5, y = 1},
alignment = {x = -1, y = -1},
offset = {x = -247, y = -94},
size = {x = 24, y = 24},
text = "heart.png",
background = "heart_gone.png",
number = 20
}
local breath_bar_definition = {
hud_elem_type = "statbar",
position = {x = 0.5, y = 1},
text = "bubble.png",
text2 = "bubble_gone.png",
number = core.PLAYER_MAX_BREATH_DEFAULT,
item = core.PLAYER_MAX_BREATH_DEFAULT * 2,
direction = 0,
size = {x = 24, y = 24},
offset = {x = 25, y= -(48 + 24 + 16)},
position = {x = 0.5, y = 1},
alignment = {x = -1, y = -1},
offset = {x = 8, y = -120},
size = {x = 24, y = 24},
text = "bubble.png",
number = 20
}
local hud_ids = {}
local function scaleToDefault(player, field)
-- Scale "hp" or "breath" to the default dimensions
local current = player["get_" .. field](player)
local nominal = core["PLAYER_MAX_" .. field:upper() .. "_DEFAULT"]
local max_display = math.max(nominal,
math.max(player:get_properties()[field .. "_max"], current))
return current / max_display * nominal
end
local function update_builtin_statbars(player)
local name = player:get_player_name()
@ -50,47 +39,24 @@ local function update_builtin_statbars(player)
-- our current flags are transmitted by sending them actively
player:hud_set_flags(flags)
end
local hud = hud_ids[name]
local hud_id = hud_ids[name]
local immortal = player:get_armor_groups().immortal == 1
if flags.healthbar and enable_damage and not immortal then
local number = scaleToDefault(player, "hp")
if hud.id_healthbar == nil then
local hud_def = table.copy(health_bar_definition)
hud_def.number = number
hud.id_healthbar = player:hud_add(hud_def)
else
player:hud_change(hud.id_healthbar, "number", number)
end
elseif hud.id_healthbar then
player:hud_remove(hud.id_healthbar)
hud.id_healthbar = nil
if flags.healthbar and not disable_health then
hud.change_item(player, "health", {number = player:get_hp()})
end
local show_breathbar = flags.breathbar and enable_damage and not immortal
local breath = player:get_breath()
local breath_max = player:get_properties().breath_max
if show_breathbar and breath <= breath_max then
local number = 2 * scaleToDefault(player, "breath")
if not hud.id_breathbar and breath < breath_max then
if flags.breathbar and player:get_breath() < 10 then
local number = player:get_breath() * 2
if hud_id.id_breathbar == nil then
local hud_def = table.copy(breath_bar_definition)
hud_def.number = number
hud.id_breathbar = player:hud_add(hud_def)
elseif hud.id_breathbar then
player:hud_change(hud.id_breathbar, "number", number)
end
hud_id.id_breathbar = player:hud_add(hud_def)
else
player:hud_change(hud_id.id_breathbar, "number", number)
end
if hud.id_breathbar and (not show_breathbar or breath == breath_max) then
minetest.after(1, function(player_name, breath_bar)
local player = minetest.get_player_by_name(player_name)
if player then
player:hud_remove(breath_bar)
end
end, name, hud.id_breathbar)
hud.id_breathbar = nil
elseif hud_id.id_breathbar then
player:hud_remove(hud_id.id_breathbar)
hud_id.id_breathbar = nil
end
end
@ -109,7 +75,7 @@ local function player_event_handler(player,eventname)
local name = player:get_player_name()
if name == "" or not hud_ids[name] then
if name == "" then
return
end
@ -138,26 +104,16 @@ local function player_event_handler(player,eventname)
end
function core.hud_replace_builtin(hud_name, definition)
if hud_name == "health" then
disable_health = true
return true
end
if type(definition) ~= "table" or
definition.hud_elem_type ~= "statbar" then
return false
end
if hud_name == "health" then
health_bar_definition = definition
for name, ids in pairs(hud_ids) do
local player = core.get_player_by_name(name)
if player and ids.id_healthbar then
player:hud_remove(ids.id_healthbar)
ids.id_healthbar = nil
update_builtin_statbars(player)
end
end
return true
end
if hud_name == "breath" then
breath_bar_definition = definition
@ -177,8 +133,13 @@ end
-- Append "update_builtin_statbars" as late as possible
-- This ensures that the HUD is hidden when the flags are updated in this callback
core.register_on_mods_loaded(function()
core.register_on_joinplayer(update_builtin_statbars)
end)
core.register_on_leaveplayer(cleanup_builtin_statbars)
core.register_playerevent(player_event_handler)
if enable_damage then
core.register_on_mods_loaded(function()
if not disable_health then
hud.register("health", health_bar_definition)
core.register_on_joinplayer(update_builtin_statbars)
end
end)
core.register_on_leaveplayer(cleanup_builtin_statbars)
core.register_playerevent(player_event_handler)
end

View File

@ -29,12 +29,14 @@ local gamepath = scriptdir .. "game" .. DIR_DELIM
local clientpath = scriptdir .. "client" .. DIR_DELIM
local commonpath = scriptdir .. "common" .. DIR_DELIM
local asyncpath = scriptdir .. "async" .. DIR_DELIM
local intlpath = scriptdir .. "intllib" .. DIR_DELIM
dofile(commonpath .. "strict.lua")
dofile(commonpath .. "serialize.lua")
dofile(commonpath .. "misc_helpers.lua")
if INIT == "game" then
dofile(intlpath .. "init.lua")
dofile(gamepath .. "init.lua")
assert(not core.get_http_api)
elseif INIT == "mainmenu" then

View File

@ -0,0 +1,27 @@
By Diego Martínez (kaeza).
Released under Unlicense.
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

225
builtin/intllib/gettext.lua Normal file
View File

@ -0,0 +1,225 @@
local strsub, strrep = string.sub, string.rep
local strmatch, strgsub = string.match, string.gsub
local function trim(str)
return strmatch(str, "^%s*(.-)%s*$")
end
local escapes = { n="\n", r="\r", t="\t" }
local function unescape(str)
return (strgsub(str, "(\\+)([nrt]?)", function(bs, c)
local bsl = #bs
local realbs = strrep("\\", bsl/2)
if bsl%2 == 1 then
c = escapes[c] or c
end
return realbs..c
end))
end
local function parse_po(str)
local state, msgid, msgid_plural, msgstrind
local texts = { }
local lineno = 0
local function perror(msg)
return error(msg.." at line "..lineno)
end
for _, line in ipairs(str:split("\n")) do repeat
lineno = lineno + 1
line = trim(line)
if line == "" or strmatch(line, "^#") then
state, msgid, msgid_plural = nil, nil, nil
break -- continue
end
local mid = strmatch(line, "^%s*msgid%s*\"(.*)\"%s*$")
if mid then
if state == "id" then
return perror("unexpected msgid")
end
state, msgid = "id", unescape(mid)
break -- continue
end
mid = strmatch(line, "^%s*msgid_plural%s*\"(.*)\"%s*$")
if mid then
if state ~= "id" then
return perror("unexpected msgid_plural")
end
state, msgid_plural = "idp", unescape(mid)
break -- continue
end
local ind, mstr = strmatch(line,
"^%s*msgstr([0-9%[%]]*)%s*\"(.*)\"%s*$")
if ind then
if not msgid then
return perror("missing msgid")
elseif ind == "" then
msgstrind = 0
elseif strmatch(ind, "%[[0-9]+%]") then
msgstrind = tonumber(strsub(ind, 2, -2))
else
return perror("malformed msgstr")
end
texts[msgid] = texts[msgid] or { }
if msgid_plural then
texts[msgid_plural] = texts[msgid]
end
texts[msgid][msgstrind] = unescape(mstr)
state = "str"
break -- continue
end
mstr = strmatch(line, "^%s*\"(.*)\"%s*$")
if mstr then
if state == "id" then
msgid = msgid..unescape(mstr)
break -- continue
elseif state == "idp" then
msgid_plural = msgid_plural..unescape(mstr)
break -- continue
elseif state == "str" then
local text = texts[msgid][msgstrind]
texts[msgid][msgstrind] = text..unescape(mstr)
break -- continue
end
end
return perror("malformed line")
-- luacheck: ignore
until true end -- end for
return texts
end
local M = { }
local function warn(msg)
core.log("warning", "[intllib] "..msg)
end
-- hax!
-- This function converts a C expression to an equivalent Lua expression.
-- It handles enough stuff to parse the `Plural-Forms` header correctly.
-- Note that it assumes the C expression is valid to begin with.
local function compile_plural_forms(str)
local plural = strmatch(str, "plural=([^;]+);?$")
local function replace_ternary(s)
local c, t, f = strmatch(s, "^(.-)%?(.-):(.*)")
if c then
return ("__if("
..replace_ternary(c)
..","..replace_ternary(t)
..","..replace_ternary(f)
..")")
end
return s
end
plural = replace_ternary(plural)
plural = strgsub(plural, "&&", " and ")
plural = strgsub(plural, "||", " or ")
plural = strgsub(plural, "!=", "~=")
plural = strgsub(plural, "!", " not ")
local f, err = loadstring([[
local function __if(c, t, f)
if c and c~=0 then return t else return f end
end
local function __f(n)
return (]]..plural..[[)
end
return (__f(...))
]])
if not f then return nil, err end
local env = { }
env._ENV, env._G = env, env
setfenv(f, env)
return function(n)
local v = f(n)
if type(v) == "boolean" then
-- Handle things like a plain `n != 1`
v = v and 1 or 0
end
return v
end
end
local function parse_headers(str)
local headers = { }
for _, line in ipairs(str:split("\n")) do
local k, v = strmatch(line, "^([^:]+):%s*(.*)")
if k then
headers[k] = v
end
end
return headers
end
local function load_catalog(filename)
local f, data, err
local function bail(msg)
warn(msg..(err and ": " or "")..(err or ""))
return nil
end
f, err = io.open(filename, "rb")
if not f then
return --bail("failed to open catalog")
end
data, err = f:read("*a")
f:close()
if not data then
return bail("failed to read catalog")
end
data, err = parse_po(data)
if not data then
return bail("failed to parse catalog")
end
err = nil
local hdrs = data[""]
if not (hdrs and hdrs[0]) then
return bail("catalog has no headers")
end
hdrs = parse_headers(hdrs[0])
local pf = hdrs["Plural-Forms"]
if not pf then
-- XXX: Is this right? Gettext assumes this if header not present.
pf = "nplurals=2; plural=n != 1"
end
data.plural_index, err = compile_plural_forms(pf)
if not data.plural_index then
return bail("failed to compile plural forms")
end
--warn("loaded: "..filename)
return data
end
function M.load_catalogs(path)
local langs = intllib.get_detected_languages()
local cats = { }
for _, lang in ipairs(langs) do
local cat = load_catalog(path.."/"..lang..".po")
if cat then
cats[#cats+1] = cat
end
end
return cats
end
return M

209
builtin/intllib/init.lua Normal file
View File

@ -0,0 +1,209 @@
intllib = {
getters = {},
strings = {},
}
local path = core.get_builtin_path() .. DIR_DELIM .. "intllib" .. DIR_DELIM
dofile(path .. "lib.lua")
local LANG = core.settings:get("language")
if not (LANG and (LANG ~= "")) then LANG = os.getenv("LANG") end
if not (LANG and (LANG ~= "")) then LANG = "en" end
local INS_CHAR = intllib.INSERTION_CHAR
local insertion_pattern = "("..INS_CHAR.."?)"..INS_CHAR.."(%(?)(%d+)(%)?)"
local function do_replacements(str, ...)
local args = {...}
-- Outer parens discard extra return values
return (str:gsub(insertion_pattern, function(escape, open, num, close)
if escape == "" then
local replacement = tostring(args[tonumber(num)])
if open == "" then
replacement = replacement..close
end
return replacement
else
return INS_CHAR..open..num..close
end
end))
end
local function make_getter(msgstrs)
return function(s, ...)
local str
if msgstrs then
str = msgstrs[s]
end
if not str or str == "" then
str = s
end
if select("#", ...) == 0 then
return str
end
return do_replacements(str, ...)
end
end
local function Getter(modname)
modname = modname or core.get_current_modname()
if not intllib.getters[modname] then
local msgstr = intllib.get_strings(modname)
intllib.getters[modname] = make_getter(msgstr)
end
return intllib.getters[modname]
end
function intllib.Getter(modname)
local info = debug and debug.getinfo and debug.getinfo(2)
local loc = info and info.short_src..":"..info.currentline
core.log("deprecated", "intllib.Getter is deprecated."
.." Please use intllib.make_gettext_pair instead."
..(info and " (called from "..loc..")" or ""))
return Getter(modname)
end
local strfind, strsub = string.find, string.sub
local langs
local function split(str, sep)
local pos, endp = 1, #str+1
return function()
if (not pos) or pos > endp then return end
local s, e = strfind(str, sep, pos, true)
local part = strsub(str, pos, s and s-1)
pos = e and e + 1
return part
end
end
function intllib.get_detected_languages()
if langs then return langs end
langs = { }
local function addlang(l)
local sep
langs[#langs+1] = l
sep = strfind(l, ".", 1, true)
if sep then
l = strsub(l, 1, sep-1)
langs[#langs+1] = l
end
sep = strfind(l, "_", 1, true)
if sep then
langs[#langs+1] = strsub(l, 1, sep-1)
end
end
local v
v = core.settings:get("language")
if v and v~="" then
addlang(v)
end
v = os.getenv("LANGUAGE")
if v then
for item in split(v, ":") do
langs[#langs+1] = item
end
end
v = os.getenv("LANG")
if v then
addlang(v)
end
langs[#langs+1] = "en"
return langs
end
local gettext = dofile(path .. "gettext.lua")
local function catgettext(catalogs, msgid)
for _, cat in ipairs(catalogs) do
local msgstr = cat and cat[msgid]
if msgstr and msgstr~="" then
local msg = msgstr[0]
return msg~="" and msg or nil
end
end
end
local function catngettext(catalogs, msgid, msgid_plural, n)
n = math.floor(n)
for _, cat in ipairs(catalogs) do
local msgstr = cat and cat[msgid]
if msgstr then
local index = cat.plural_index(n)
local msg = msgstr[index]
return msg~="" and msg or nil
end
end
return n==1 and msgid or msgid_plural
end
local gettext_getters = { }
function intllib.make_gettext_pair(modname)
modname = modname or core.get_current_modname()
if gettext_getters[modname] then
return unpack(gettext_getters[modname])
end
local modpath = core.get_modpath(modname) and core.get_modpath(modname)
local localedir = modpath and modpath.."/locale"
local catalogs = localedir and gettext.load_catalogs(localedir) or {}
local getter = Getter(modname)
local function gettext_func(msgid, ...)
local msgstr = (catgettext(catalogs, msgid)
or getter(msgid))
return do_replacements(msgstr, ...)
end
local function ngettext_func(msgid, msgid_plural, n, ...)
local msgstr = (catngettext(catalogs, msgid, msgid_plural, n)
or getter(msgid))
return do_replacements(msgstr, ...)
end
gettext_getters[modname] = { gettext_func, ngettext_func }
return gettext_func, ngettext_func
end
local function get_locales(code)
local ll, cc = code:match("^(..)_(..)")
if ll then
return { ll.."_"..cc, ll, ll~="en" and "en" or nil }
else
return { code, code~="en" and "en" or nil }
end
end
function intllib.get_strings(modname, langcode)
langcode = langcode or LANG
modname = modname or core.get_current_modname()
local msgstr = intllib.strings[modname]
if not msgstr then
local modpath = core.get_modpath(modname) and core.get_modpath(modname)
msgstr = { }
if modpath then
for _, l in ipairs(get_locales(langcode)) do
local t = intllib.load_strings(modpath.."/locale/"..modname.."."..l..".tr")
or intllib.load_strings(modpath.."/locale/"..l..".txt") or { }
for k, v in pairs(t) do
msgstr[k] = msgstr[k] or v
end
end
intllib.strings[modname] = msgstr
end
end
return msgstr
end

72
builtin/intllib/lib.lua Normal file
View File

@ -0,0 +1,72 @@
intllib = intllib or {}
local INS_CHAR = "@"
intllib.INSERTION_CHAR = INS_CHAR
local escapes = {
["\\"] = "\\",
["n"] = "\n",
["s"] = " ",
["t"] = "\t",
["r"] = "\r",
["f"] = "\f",
[INS_CHAR] = INS_CHAR..INS_CHAR,
}
local function unescape(str)
local parts = {}
local n = 1
local function add(s)
parts[n] = s
n = n + 1
end
local start = 1
while true do
local pos = str:find("[\\@]", start)
if pos then
add(str:sub(start, pos - 1))
else
add(str:sub(start))
break
end
local c = str:sub(pos + 1, pos + 1)
if escapes[c] then
add(escapes[c])
elseif str:sub(pos, pos) == "@" then
add("@" .. c)
else
add(c)
end
start = pos + 2
end
return table.concat(parts)
end
local function find_eq(s)
for slashes, pos in s:gmatch("([\\]*)=()") do
if (slashes:len() % 2) == 0 then
return pos - 1
end
end
end
function intllib.load_strings(filename)
local file, err = io.open(filename, "r")
if not file then
return nil, err
end
local strings = {}
for line in file:lines() do
line = line:trim()
if line ~= "" and line:sub(1, 1) ~= "#" then
local pos = find_eq(line)
if pos then
local msgid = unescape(line:sub(1, pos - 1):trim())
strings[msgid] = unescape(line:sub(pos + 1):trim())
end
end
end
file:close()
return strings
end

View File

@ -3,7 +3,7 @@
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--the Free Software Foundation; either version 3.0 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,

View File

@ -3,7 +3,7 @@
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--the Free Software Foundation; either version 3.0 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,

View File

@ -3,7 +3,7 @@
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--the Free Software Foundation; either version 3.0 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
@ -73,7 +73,7 @@ local Formatter = {
end
}
local widths = { 55, 9, 9, 9, 5, 5, 5 }
local widths = { 50, 8, 8, 8, 5, 5, 5 }
local txt_row_format = sprintf(" %%-%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds", unpack(widths))
local HR = {}

View File

@ -3,7 +3,7 @@
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--the Free Software Foundation; either version 3.0 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B