Add files via upload
This commit is contained in:
parent
02b07eb370
commit
6c92923e61
17
builtin/async/init.lua
Normal file
17
builtin/async/init.lua
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
core.log("info", "Initializing Asynchronous environment")
|
||||
|
||||
function core.job_processor(serialized_func, serialized_param)
|
||||
local func = loadstring(serialized_func)
|
||||
local param = core.deserialize(serialized_param)
|
||||
local retval = nil
|
||||
|
||||
if type(func) == "function" then
|
||||
retval = core.serialize(func(param))
|
||||
else
|
||||
core.log("error", "ASYNC WORKER: Unable to deserialize function")
|
||||
end
|
||||
|
||||
return retval or core.serialize(nil)
|
||||
end
|
||||
|
194
builtin/client/chatcommands.lua
Normal file
194
builtin/client/chatcommands.lua
Normal file
@ -0,0 +1,194 @@
|
||||
-- Minetest: builtin/client/chatcommands.lua
|
||||
|
||||
core.register_on_sending_chat_message(function(message)
|
||||
if message:sub(1,2) == ".." then
|
||||
return false
|
||||
end
|
||||
|
||||
local first_char = message:sub(1,1)
|
||||
if first_char == "/" or first_char == "." then
|
||||
core.display_chat_message(core.gettext("issued command: ") .. message)
|
||||
end
|
||||
|
||||
if first_char ~= "." then
|
||||
return false
|
||||
end
|
||||
|
||||
local cmd, param = string.match(message, "^%.([^ ]+) *(.*)")
|
||||
param = param or ""
|
||||
|
||||
if not cmd then
|
||||
core.display_chat_message(core.gettext("-!- Empty command"))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Run core.registered_on_chatcommand callbacks.
|
||||
if core.run_callbacks(core.registered_on_chatcommand, 5, cmd, param) then
|
||||
return true
|
||||
end
|
||||
|
||||
local cmd_def = core.registered_chatcommands[cmd]
|
||||
if cmd_def then
|
||||
core.set_last_run_mod(cmd_def.mod_origin)
|
||||
local _, result = cmd_def.func(param)
|
||||
if result then
|
||||
core.display_chat_message(result)
|
||||
end
|
||||
else
|
||||
core.display_chat_message(core.gettext("-!- Invalid command: ") .. cmd)
|
||||
end
|
||||
|
||||
return true
|
||||
end)
|
||||
|
||||
function core.run_server_chatcommand(cmd, param)
|
||||
core.send_chat_message("/" .. cmd .. " " .. param)
|
||||
end
|
||||
|
||||
core.register_chatcommand("say", {
|
||||
description = "Send raw text",
|
||||
func = function(text)
|
||||
core.send_chat_message(text)
|
||||
return true
|
||||
end,
|
||||
})
|
||||
|
||||
core.register_chatcommand("teleport", {
|
||||
params = "<X>,<Y>,<Z>",
|
||||
description = "Teleport to coordinates.",
|
||||
func = function(param)
|
||||
local success, pos = core.parse_pos(param)
|
||||
if success then
|
||||
core.localplayer:set_pos(pos)
|
||||
return true, "Teleporting to " .. core.pos_to_string(pos)
|
||||
end
|
||||
return false, pos
|
||||
end,
|
||||
})
|
||||
|
||||
core.register_chatcommand("teleportjump", {
|
||||
params = "<X>,<Y>,<Z>",
|
||||
description = "Teleport to relative coordinates.",
|
||||
func = function(param)
|
||||
local success, pos = core.parse_relative_pos(param)
|
||||
if success then
|
||||
core.localplayer:set_pos(pos)
|
||||
return true, "Teleporting to " .. core.pos_to_string(pos)
|
||||
end
|
||||
return false, pos
|
||||
end,
|
||||
})
|
||||
|
||||
core.register_chatcommand("wielded", {
|
||||
description = "Print itemstring of wieleded item",
|
||||
func = function()
|
||||
return true, core.localplayer:get_wielded_item():get_name()
|
||||
end
|
||||
})
|
||||
|
||||
core.register_chatcommand("disconnect", {
|
||||
description = "Exit to main menu",
|
||||
func = function(param)
|
||||
core.disconnect()
|
||||
end,
|
||||
})
|
||||
|
||||
core.register_chatcommand("players", {
|
||||
description = "List online players",
|
||||
func = function(param)
|
||||
return true, "Online players: " .. table.concat(core.get_player_names(), ", ")
|
||||
end
|
||||
})
|
||||
|
||||
core.register_chatcommand("kill", {
|
||||
description = "Kill yourself",
|
||||
func = function()
|
||||
core.send_damage(10000)
|
||||
end,
|
||||
})
|
||||
|
||||
core.register_chatcommand("set", {
|
||||
params = "([-n] <name> <value>) | <name>",
|
||||
description = "Set or read client configuration setting",
|
||||
func = function(param)
|
||||
local arg, setname, setvalue = string.match(param, "(-[n]) ([^ ]+) (.+)")
|
||||
if arg and arg == "-n" and setname and setvalue then
|
||||
core.settings:set(setname, setvalue)
|
||||
return true, setname .. " = " .. setvalue
|
||||
end
|
||||
|
||||
setname, setvalue = string.match(param, "([^ ]+) (.+)")
|
||||
if setname and setvalue then
|
||||
if not core.settings:get(setname) then
|
||||
return false, "Failed. Use '.set -n <name> <value>' to create a new setting."
|
||||
end
|
||||
core.settings:set(setname, setvalue)
|
||||
return true, setname .. " = " .. setvalue
|
||||
end
|
||||
|
||||
setname = string.match(param, "([^ ]+)")
|
||||
if setname then
|
||||
setvalue = core.settings:get(setname)
|
||||
if not setvalue then
|
||||
setvalue = "<not set>"
|
||||
end
|
||||
return true, setname .. " = " .. setvalue
|
||||
end
|
||||
|
||||
return false, "Invalid parameters (see .help set)."
|
||||
end,
|
||||
})
|
||||
|
||||
core.register_chatcommand("place", {
|
||||
params = "<X>,<Y>,<Z>",
|
||||
description = "Place wielded item",
|
||||
func = function(param)
|
||||
local success, pos = core.parse_relative_pos(param)
|
||||
if success then
|
||||
core.place_node(pos)
|
||||
return true, "Node placed at " .. core.pos_to_string(pos)
|
||||
end
|
||||
return false, pos
|
||||
end,
|
||||
})
|
||||
|
||||
core.register_chatcommand("dig", {
|
||||
params = "<X>,<Y>,<Z>",
|
||||
description = "Dig node",
|
||||
func = function(param)
|
||||
local success, pos = core.parse_relative_pos(param)
|
||||
if success then
|
||||
core.dig_node(pos)
|
||||
return true, "Node at " .. core.pos_to_string(pos) .. " dug"
|
||||
end
|
||||
return false, pos
|
||||
end,
|
||||
})
|
||||
|
||||
core.register_chatcommand("setyaw", {
|
||||
params = "<yaw>",
|
||||
description = "Set your yaw",
|
||||
func = function(param)
|
||||
local yaw = tonumber(param)
|
||||
if yaw then
|
||||
core.localplayer:set_yaw(yaw)
|
||||
return true
|
||||
else
|
||||
return false, "Invalid usage (See /help setyaw)"
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
core.register_chatcommand("setpitch", {
|
||||
params = "<pitch>",
|
||||
description = "Set your pitch",
|
||||
func = function(param)
|
||||
local pitch = tonumber(param)
|
||||
if pitch then
|
||||
core.localplayer:set_pitch(pitch)
|
||||
return true
|
||||
else
|
||||
return false, "Invalid usage (See /help setpitch)"
|
||||
end
|
||||
end
|
||||
})
|
70
builtin/client/cheats.lua
Normal file
70
builtin/client/cheats.lua
Normal file
@ -0,0 +1,70 @@
|
||||
core.cheats = {
|
||||
["Combat"] = {
|
||||
["AntiKnockback"] = "antiknockback",
|
||||
["FastHit"] = "spamclick",
|
||||
["AttachmentFloat"] = "float_above_parent",
|
||||
["ThroughWalls"] = "dont_point_nodes",
|
||||
["AutoHit"] = "autohit",
|
||||
},
|
||||
["Movement"] = {
|
||||
["Freecam"] = "freecam",
|
||||
["AutoForward"] = "continuous_forward",
|
||||
["PitchMove"] = "pitch_move",
|
||||
["AutoJump"] = "autojump",
|
||||
["Jesus"] = "jesus",
|
||||
["NoSlow"] = "no_slow",
|
||||
["AutoForwSprint"] = 'autofsprint',
|
||||
["Jetpack"] = 'jetpack',
|
||||
["SpeedOverride"] = "override_speed",
|
||||
["JumpOverride"] = "override_jump",
|
||||
["GravityOverride"] = "override_gravity",
|
||||
["AntiSlip"] = "antislip",
|
||||
["NoPosUpdate"] = "noposupdate",
|
||||
},
|
||||
["Render"] = {
|
||||
["Xray"] = "xray",
|
||||
["Fullbright"] = "fullbright",
|
||||
["HUDBypass"] = "hud_flags_bypass",
|
||||
["NoHurtCam"] = "no_hurt_cam",
|
||||
["BrightNight"] = "no_night",
|
||||
["Coords"] = "coords",
|
||||
["Clouds"] = "enable_clouds",
|
||||
["CheatHUD"] = "cheat_hud",
|
||||
["EntityESP"] = "enable_entity_esp",
|
||||
["EntityTracers"] = "enable_entity_tracers",
|
||||
["PlayerESP"] = "enable_player_esp",
|
||||
["PlayerTracers"] = "enable_player_tracers",
|
||||
["NodeESP"] = "enable_node_esp",
|
||||
["NodeTracers"] = "enable_node_tracers",
|
||||
},
|
||||
["World"] = {
|
||||
["FastDig"] = "fastdig",
|
||||
["FastPlace"] = "fastplace",
|
||||
["AutoDig"] = "autodig",
|
||||
["AutoPlace"] = "autoplace",
|
||||
["InstantBreak"] = "instant_break",
|
||||
},
|
||||
["Exploit"] = {
|
||||
["EntitySpeed"] = "entity_speed",
|
||||
["ParticleExploit"] = "log_particles",
|
||||
},
|
||||
["Chat"] = {
|
||||
["IgnoreStatus"] = "ignore_status_messages",
|
||||
["Deathmessages"] = "mark_deathmessages",
|
||||
},
|
||||
["Player"] = {
|
||||
["NoFallDamage"] = "prevent_natural_damage",
|
||||
["NoForceRotate"] = "no_force_rotate",
|
||||
["IncreasedRange"] = "increase_tool_range",
|
||||
["UnlimitedRange"] = "increase_tool_range_plus",
|
||||
["PointLiquids"] = "point_liquids",
|
||||
["PrivBypass"] = "priv_bypass",
|
||||
},
|
||||
["Chat"] = {},
|
||||
["Inventory"] = {}
|
||||
}
|
||||
|
||||
function core.register_cheat(cheatname, category, func)
|
||||
core.cheats[category] = core.cheats[category] or {}
|
||||
core.cheats[category][cheatname] = func
|
||||
end
|
13
builtin/client/init.lua
Normal file
13
builtin/client/init.lua
Normal file
@ -0,0 +1,13 @@
|
||||
-- Minetest: builtin/client/init.lua
|
||||
local scriptpath = core.get_builtin_path()
|
||||
local clientpath = scriptpath.."client"..DIR_DELIM
|
||||
local commonpath = scriptpath.."common"..DIR_DELIM
|
||||
|
||||
dofile(clientpath .. "register.lua")
|
||||
dofile(commonpath .. "after.lua")
|
||||
dofile(commonpath .. "chatcommands.lua")
|
||||
dofile(commonpath .. "vector.lua")
|
||||
dofile(clientpath .. "util.lua")
|
||||
dofile(clientpath .. "chatcommands.lua")
|
||||
dofile(clientpath .. "cheats.lua")
|
||||
dofile(clientpath .. "wasplib.lua")
|
113
builtin/client/register.lua
Normal file
113
builtin/client/register.lua
Normal file
@ -0,0 +1,113 @@
|
||||
|
||||
core.callback_origins = {}
|
||||
|
||||
local getinfo = debug.getinfo
|
||||
debug.getinfo = nil
|
||||
|
||||
--- Runs given callbacks.
|
||||
--
|
||||
-- Note: this function is also called from C++
|
||||
-- @tparam table callbacks a table with registered callbacks, like `core.registered_on_*`
|
||||
-- @tparam number mode a RunCallbacksMode, as defined in src/script/common/c_internal.h
|
||||
-- @param ... arguments for the callback
|
||||
-- @return depends on mode
|
||||
function core.run_callbacks(callbacks, mode, ...)
|
||||
assert(type(callbacks) == "table")
|
||||
local cb_len = #callbacks
|
||||
if cb_len == 0 then
|
||||
if mode == 2 or mode == 3 then
|
||||
return true
|
||||
elseif mode == 4 or mode == 5 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
local ret
|
||||
for i = 1, cb_len do
|
||||
local cb_ret = callbacks[i](...)
|
||||
|
||||
if mode == 0 and i == 1 or mode == 1 and i == cb_len then
|
||||
ret = cb_ret
|
||||
elseif mode == 2 then
|
||||
if not cb_ret or i == 1 then
|
||||
ret = cb_ret
|
||||
end
|
||||
elseif mode == 3 then
|
||||
if cb_ret then
|
||||
return cb_ret
|
||||
end
|
||||
ret = cb_ret
|
||||
elseif mode == 4 then
|
||||
if (cb_ret and not ret) or i == 1 then
|
||||
ret = cb_ret
|
||||
end
|
||||
elseif mode == 5 and cb_ret then
|
||||
return cb_ret
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function core.override_item(name, redefinition)
|
||||
if redefinition.name ~= nil then
|
||||
error("Attempt to redefine name of "..name.." to "..dump(redefinition.name), 2)
|
||||
end
|
||||
if redefinition.type ~= nil then
|
||||
error("Attempt to redefine type of "..name.." to "..dump(redefinition.type), 2)
|
||||
end
|
||||
local itemdef = core.get_item_def(name)
|
||||
if not itemdef then
|
||||
error("Attempt to override non-existent item "..name, 2)
|
||||
end
|
||||
local nodedef = core.get_node_def(name)
|
||||
table.combine(itemdef, nodedef)
|
||||
|
||||
for k, v in pairs(redefinition) do
|
||||
rawset(itemdef, k, v)
|
||||
end
|
||||
core.register_item_raw(itemdef)
|
||||
end
|
||||
|
||||
--
|
||||
-- Callback registration
|
||||
--
|
||||
|
||||
local function make_registration()
|
||||
local t = {}
|
||||
local registerfunc = function(func)
|
||||
t[#t + 1] = func
|
||||
core.callback_origins[func] = {
|
||||
mod = core.get_current_modname() or "??",
|
||||
name = getinfo(1, "n").name or "??"
|
||||
}
|
||||
--local origin = core.callback_origins[func]
|
||||
--print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func))
|
||||
end
|
||||
return t, registerfunc
|
||||
end
|
||||
|
||||
core.registered_globalsteps, core.register_globalstep = make_registration()
|
||||
core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration()
|
||||
core.registered_on_shutdown, core.register_on_shutdown = make_registration()
|
||||
core.registered_on_receiving_chat_message, core.register_on_receiving_chat_message = make_registration()
|
||||
core.registered_on_sending_chat_message, core.register_on_sending_chat_message = make_registration()
|
||||
core.registered_on_chatcommand, core.register_on_chatcommand = make_registration()
|
||||
core.registered_on_death, core.register_on_death = make_registration()
|
||||
core.registered_on_hp_modification, core.register_on_hp_modification = make_registration()
|
||||
core.registered_on_damage_taken, core.register_on_damage_taken = make_registration()
|
||||
core.registered_on_formspec_input, core.register_on_formspec_input = make_registration()
|
||||
core.registered_on_dignode, core.register_on_dignode = make_registration()
|
||||
core.registered_on_punchnode, core.register_on_punchnode = make_registration()
|
||||
core.registered_on_placenode, core.register_on_placenode = make_registration()
|
||||
core.registered_on_item_use, core.register_on_item_use = make_registration()
|
||||
core.registered_on_item_activate, core.register_on_item_activate = make_registration()
|
||||
core.registered_on_modchannel_message, core.register_on_modchannel_message = make_registration()
|
||||
core.registered_on_modchannel_signal, core.register_on_modchannel_signal = make_registration()
|
||||
core.registered_on_inventory_open, core.register_on_inventory_open = make_registration()
|
||||
core.registered_on_receiving_inventory_form, core.register_on_receiving_inventory_form = make_registration()
|
||||
core.registered_on_nodemeta_form_open, core.register_on_nodemeta_form_open = make_registration()
|
||||
core.registered_on_recieve_physics_override, core.register_on_recieve_physics_override = make_registration()
|
||||
core.registered_on_play_sound, core.register_on_play_sound = make_registration()
|
||||
core.registered_on_spawn_particle, core.register_on_spawn_particle = make_registration()
|
||||
core.registered_on_particle_spawner, core.register_on_particle_spawner = make_registration()
|
||||
core.registered_on_sending_inventory_fields, core.register_on_sending_inventory_fields = make_registration()
|
||||
core.registered_on_sending_nodemeta_fields, core.register_on_sending_nodemeta_fields = make_registration()
|
46
builtin/client/util.lua
Normal file
46
builtin/client/util.lua
Normal file
@ -0,0 +1,46 @@
|
||||
function core.parse_pos(param)
|
||||
local p = {}
|
||||
local playerpos = core.localplayer:get_pos()
|
||||
p.x, p.y, p.z = string.match(param, "^([~|%d.-]+)[, ] *([~|%d.-]+)[, ] *([~|%d.-]+)$")
|
||||
for k, v in pairs(p) do
|
||||
if p[k] == "~" then
|
||||
p[k] = playerpos[k]
|
||||
else
|
||||
p[k] = tonumber(v)
|
||||
end
|
||||
end
|
||||
if p.x and p.y and p.z then
|
||||
return true, vector.round(p)
|
||||
end
|
||||
return false, "Invalid position (" .. param .. ")"
|
||||
end
|
||||
|
||||
function core.parse_relative_pos(param)
|
||||
local success, pos = core.parse_pos(param:gsub("~", "0"))
|
||||
if success then pos = vector.round(vector.add(core.localplayer:get_pos(), pos)) end
|
||||
return success, pos
|
||||
end
|
||||
|
||||
function core.find_item(item, mini, maxi)
|
||||
for index, stack in ipairs(core.get_inventory("current_player").main) do
|
||||
if (not mini or index >= mini) and (not maxi or index <= maxi) and stack:get_name() == item then
|
||||
return index
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function core.get_pointed_thing()
|
||||
local pos = core.camera:get_pos()
|
||||
local pos2 = vector.add(pos, vector.multiply(core.camera:get_look_dir(), 7))
|
||||
local player = core.localplayer
|
||||
if not player then return end
|
||||
local item = player:get_wielded_item()
|
||||
if not item then return end
|
||||
local def = core.get_item_def(item:get_name())
|
||||
local ray = core.raycast(pos, pos2, true, core.settings:get_bool("point_liquids") or def and def.liquids_pointable)
|
||||
return ray and ray:next()
|
||||
end
|
||||
|
||||
function core.close_formspec(formname)
|
||||
return core.show_formspec(formname, "")
|
||||
end
|
873
builtin/client/wasplib.lua
Normal file
873
builtin/client/wasplib.lua
Normal file
@ -0,0 +1,873 @@
|
||||
|
||||
ws = {}
|
||||
ws.registered_globalhacks = {}
|
||||
ws.displayed_wps={}
|
||||
|
||||
ws.c = core
|
||||
|
||||
ws.range=4
|
||||
ws.target=nil
|
||||
ws.targetpos=nil
|
||||
|
||||
local nextact = {}
|
||||
local ghwason={}
|
||||
|
||||
local nodes_this_tick=0
|
||||
|
||||
function ws.s(name,value)
|
||||
if value == nil then
|
||||
return ws.c.settings:get(name)
|
||||
else
|
||||
ws.c.settings:set(name,value)
|
||||
return ws.c.settings:get(name)
|
||||
end
|
||||
end
|
||||
function ws.sb(name,value)
|
||||
if value == nil then
|
||||
return ws.c.settings:get_bool(name)
|
||||
else
|
||||
ws.c.settings:set_bool(name,value)
|
||||
return ws.c.settings:get_bool(name)
|
||||
end
|
||||
end
|
||||
|
||||
function ws.dcm(msg)
|
||||
return minetest.display_chat_message(msg)
|
||||
end
|
||||
function ws.set_bool_bulk(settings,value)
|
||||
if type(settings) ~= 'table' then return false end
|
||||
for k,v in pairs(settings) do
|
||||
minetest.settings:set_bool(v,value)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ws.shuffle(tbl)
|
||||
for i = #tbl, 2, -1 do
|
||||
local j = math.random(i)
|
||||
tbl[i], tbl[j] = tbl[j], tbl[i]
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
function ws.in_list(val, list)
|
||||
if type(list) ~= "table" then return false end
|
||||
for i, v in pairs(list) do
|
||||
if v == val then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ws.random_table_element(tbl)
|
||||
local ks = {}
|
||||
for k in pairs(tbl) do
|
||||
table.insert(ks, k)
|
||||
end
|
||||
return tbl[ks[math.random(#ks)]]
|
||||
end
|
||||
|
||||
function ws.center()
|
||||
--local lp=ws.dircoord(0,0,0)
|
||||
--minetest.localplayer:set_pos(lp)
|
||||
end
|
||||
|
||||
function ws.globalhacktemplate(setting,func,funcstart,funcstop,daughters,delay)
|
||||
funcstart = funcstart or function() end
|
||||
funcstop = funcstop or function() end
|
||||
delay = delay or 0.5
|
||||
return function()
|
||||
if not minetest.localplayer then return end
|
||||
if minetest.settings:get_bool(setting) then
|
||||
if tps_client and tps_client.ping and tps_client.ping > 1000 then return end
|
||||
nodes_this_tick = 0
|
||||
if nextact[setting] and nextact[setting] > os.clock() then return end
|
||||
nextact[setting] = os.clock() + delay
|
||||
if not ghwason[setting] then
|
||||
if not funcstart() then
|
||||
ws.set_bool_bulk(daughters,true)
|
||||
ghwason[setting] = true
|
||||
--ws.dcm(setting.. " activated")
|
||||
ws.center()
|
||||
minetest.settings:set('last-dir',ws.getdir())
|
||||
minetest.settings:set('last-y',ws.dircoord(0,0,0).y)
|
||||
else minetest.settings:set_bool(setting,false)
|
||||
end
|
||||
else
|
||||
func()
|
||||
end
|
||||
|
||||
elseif ghwason[setting] then
|
||||
ghwason[setting] = false
|
||||
ws.set_bool_bulk(daughters,false)
|
||||
funcstop()
|
||||
--ws.dcm(setting.. " deactivated")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ws.register_globalhack(func)
|
||||
table.insert(ws.registered_globalhacks,func)
|
||||
end
|
||||
|
||||
function ws.register_globalhacktemplate(name,category,setting,func,funcstart,funcstop,daughters)
|
||||
ws.register_globalhack(ws.globalhacktemplate(setting,func,funcstart,funcstop,daughters))
|
||||
minetest.register_cheat(name,category,setting)
|
||||
end
|
||||
|
||||
ws.rg=ws.register_globalhacktemplate
|
||||
|
||||
function ws.step_globalhacks(dtime)
|
||||
for i, v in ipairs(ws.registered_globalhacks) do
|
||||
v(dtime)
|
||||
end
|
||||
end
|
||||
|
||||
minetest.register_globalstep(function(dtime) ws.step_globalhacks(dtime) end)
|
||||
minetest.settings:set_bool('continuous_forward',false)
|
||||
|
||||
function ws.on_connect(func)
|
||||
if not minetest.localplayer then minetest.after(0,function() ws.on_connect(func) end) return end
|
||||
if func then func() end
|
||||
end
|
||||
|
||||
ws.on_connect(function()
|
||||
local ldir =minetest.settings:get('last-dir')
|
||||
if ldir then ws.setdir(ldir) end
|
||||
end)
|
||||
|
||||
|
||||
-- COORD MAGIC
|
||||
|
||||
function ws.is_same_pos(pos1,pos2)
|
||||
return vector.distance(vector.round(pos1),vector.round(pos2)) == 0
|
||||
end
|
||||
function ws.get_reachable_positions(range,under)
|
||||
under=under or false
|
||||
range=range or 4
|
||||
local rt={}
|
||||
local lp=vector.round(minetest.localplayer:get_pos())
|
||||
local ylim=range
|
||||
if under then ylim=-1 end
|
||||
for x = -range,range,1 do
|
||||
for y = -range,ylim,1 do
|
||||
for z = -range,range,1 do
|
||||
table.insert(rt,vector.round(vector.add(lp,vector.new(x,y,z))))
|
||||
end
|
||||
end
|
||||
end
|
||||
return rt
|
||||
end
|
||||
|
||||
function ws.do_area(radius,func,plane)
|
||||
for k,v in pairs(ws.get_reachable_positions(range)) do
|
||||
if not plane or v.y == minetest.localplayer:get_pos().y -1 then
|
||||
func(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ws.get_hud_by_texture(texture)
|
||||
local def
|
||||
local i = -1
|
||||
repeat
|
||||
i = i + 1
|
||||
def = minetest.localplayer:hud_get(i)
|
||||
until not def or def.text == texture
|
||||
if def then
|
||||
return def
|
||||
end
|
||||
def.number=0
|
||||
return def
|
||||
end
|
||||
|
||||
function ws.find_player(name)
|
||||
for k, v in ipairs(minetest.localplayer.get_nearby_objects(500)) do
|
||||
if v:get_name() == name then
|
||||
return v:get_pos(),v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ws.display_wp(pos,name)
|
||||
local ix = #ws.displayed_wps + 1
|
||||
ws.displayed_wps[ix] = minetest.localplayer:hud_add({
|
||||
hud_elem_type = 'waypoint',
|
||||
name = name,
|
||||
text = name,
|
||||
number = 0x00ff00,
|
||||
world_pos = pos
|
||||
})
|
||||
return ix
|
||||
end
|
||||
|
||||
function ws.clear_wp(ix)
|
||||
table.remove(ws.displayed_wps,ix)
|
||||
end
|
||||
|
||||
function ws.clear_wps()
|
||||
for k,v in ipairs(ws.displayed_wps) do
|
||||
minetest.localplayer:hud_remove(v)
|
||||
table.remove(ws.displayed_wps,k)
|
||||
end
|
||||
end
|
||||
|
||||
function ws.register_chatcommand_alias(old, ...)
|
||||
local def = assert(minetest.registered_chatcommands[old])
|
||||
def.name = nil
|
||||
for i = 1, select('#', ...) do
|
||||
minetest.register_chatcommand(select(i, ...), table.copy(def))
|
||||
end
|
||||
end
|
||||
|
||||
function ws.round2(num, numDecimalPlaces)
|
||||
return tonumber(string.format("%." .. (numDecimalPlaces or 0) .. "f", num))
|
||||
end
|
||||
|
||||
function ws.pos_to_string(pos)
|
||||
if type(pos) == 'table' then
|
||||
pos = minetest.pos_to_string(vector.round(pos))
|
||||
end
|
||||
if type(pos) == 'string' then
|
||||
return pos
|
||||
end
|
||||
return pos
|
||||
end
|
||||
|
||||
function ws.string_to_pos(pos)
|
||||
if type(pos) == 'string' then
|
||||
pos = minetest.string_to_pos(pos)
|
||||
end
|
||||
if type(pos) == 'table' then
|
||||
return vector.round(pos)
|
||||
end
|
||||
return pos
|
||||
end
|
||||
|
||||
|
||||
|
||||
--ITEMS
|
||||
function ws.find_item_in_table(items,rnd)
|
||||
if type(items) == 'string' then
|
||||
return minetest.find_item(items)
|
||||
end
|
||||
if type(items) ~= 'table' then return end
|
||||
if rnd then items=ws.shuffle(items) end
|
||||
for i, v in pairs(items) do
|
||||
local n = minetest.find_item(v)
|
||||
if n then
|
||||
return n
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ws.find_empty(inv)
|
||||
for i, v in ipairs(inv) do
|
||||
if v:is_empty() then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ws.find_named(inv, name)
|
||||
if not inv then return -1 end
|
||||
if not name then return end
|
||||
for i, v in ipairs(inv) do
|
||||
if v:get_name():find(name) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ws.itemnameformat(description)
|
||||
description = description:gsub(string.char(0x1b) .. "%(.@[^)]+%)", "")
|
||||
description = description:match("([^\n]*)")
|
||||
return description
|
||||
end
|
||||
|
||||
function ws.find_nametagged(list, name)
|
||||
for i, v in ipairs(list) do
|
||||
if ws.itemnameformat(v:get_description()) == name then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hotbar_slot=8
|
||||
function ws.to_hotbar(it,hslot)
|
||||
local tpos=nil
|
||||
local plinv = minetest.get_inventory("current_player")
|
||||
if hslot and hslot < 10 then
|
||||
tpos=hslot
|
||||
else
|
||||
for i, v in ipairs(plinv.main) do
|
||||
if i<10 and v:is_empty() then
|
||||
tpos = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if tpos == nil then tpos=hotbar_slot end
|
||||
local mv = InventoryAction("move")
|
||||
mv:from("current_player", "main", it)
|
||||
mv:to("current_player", "main", tpos)
|
||||
mv:apply()
|
||||
return tpos
|
||||
end
|
||||
|
||||
function ws.switch_to_item(itname,hslot)
|
||||
if not minetest.localplayer then return false end
|
||||
local plinv = minetest.get_inventory("current_player")
|
||||
for i, v in ipairs(plinv.main) do
|
||||
if i<10 and v:get_name() == itname then
|
||||
minetest.localplayer:set_wield_index(i)
|
||||
return true
|
||||
end
|
||||
end
|
||||
local pos = ws.find_named(plinv.main, itname)
|
||||
if pos then
|
||||
minetest.localplayer:set_wield_index(ws.to_hotbar(pos,hslot))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
function ws.in_inv(itname)
|
||||
if not minetest.localplayer then return false end
|
||||
local plinv = minetest.get_inventory("current_player")
|
||||
local pos = ws.find_named(plinv.main, itname)
|
||||
if pos then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function core.switch_to_item(item) return ws.switch_to_item(item) end
|
||||
|
||||
function ws.switch_inv_or_echest(name,max_count,hslot)
|
||||
if not minetest.localplayer then return false end
|
||||
local plinv = minetest.get_inventory("current_player")
|
||||
if ws.switch_to_item(name) then return true end
|
||||
|
||||
local epos = ws.find_named(plinv.enderchest, name)
|
||||
if epos then
|
||||
local tpos
|
||||
for i, v in ipairs(plinv.main) do
|
||||
if i < 9 and v:is_empty() then
|
||||
tpos = i
|
||||
break
|
||||
end
|
||||
end
|
||||
if not tpos then tpos=hotbar_slot end
|
||||
|
||||
if tpos then
|
||||
local mv = InventoryAction("move")
|
||||
mv:from("current_player", "enderchest", epos)
|
||||
mv:to("current_player", "main", tpos)
|
||||
if max_count then
|
||||
mv:set_count(max_count)
|
||||
end
|
||||
mv:apply()
|
||||
minetest.localplayer:set_wield_index(tpos)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function posround(n)
|
||||
return math.floor(n + 0.5)
|
||||
end
|
||||
|
||||
local function fmt(c)
|
||||
return tostring(posround(c.x))..","..tostring(posround(c.y))..","..tostring(posround(c.z))
|
||||
end
|
||||
|
||||
local function map_pos(value)
|
||||
if value.x then
|
||||
return value
|
||||
else
|
||||
return {x = value[1], y = value[2], z = value[3]}
|
||||
end
|
||||
end
|
||||
|
||||
function ws.invparse(location)
|
||||
if type(location) == "string" then
|
||||
if string.match(location, "^[-]?[0-9]+,[-]?[0-9]+,[-]?[0-9]+$") then
|
||||
return "nodemeta:" .. location
|
||||
else
|
||||
return location
|
||||
end
|
||||
elseif type(location) == "table" then
|
||||
return "nodemeta:" .. fmt(map_pos(location))
|
||||
end
|
||||
end
|
||||
|
||||
function ws.invpos(p)
|
||||
return "nodemeta:"..p.x..","..p.y..","..p.z
|
||||
end
|
||||
|
||||
|
||||
-- TOOLS
|
||||
|
||||
|
||||
local function check_tool(stack, node_groups, old_best_time)
|
||||
local toolcaps = stack:get_tool_capabilities()
|
||||
if not toolcaps then return end
|
||||
local best_time = old_best_time
|
||||
for group, groupdef in pairs(toolcaps.groupcaps) do
|
||||
local level = node_groups[group]
|
||||
if level then
|
||||
local this_time = groupdef.times[level]
|
||||
if this_time and this_time < best_time then
|
||||
best_time = this_time
|
||||
end
|
||||
end
|
||||
end
|
||||
return best_time < old_best_time, best_time
|
||||
end
|
||||
|
||||
local function find_best_tool(nodename, switch)
|
||||
local player = minetest.localplayer
|
||||
local inventory = minetest.get_inventory("current_player")
|
||||
local node_groups = minetest.get_node_def(nodename).groups
|
||||
local new_index = player:get_wield_index()
|
||||
local is_better, best_time = false, math.huge
|
||||
|
||||
is_better, best_time = check_tool(player:get_wielded_item(), node_groups, best_time)
|
||||
if inventory.hand then
|
||||
is_better, best_time = check_tool(inventory.hand[1], node_groups, best_time)
|
||||
end
|
||||
|
||||
for index, stack in ipairs(inventory.main) do
|
||||
is_better, best_time = check_tool(stack, node_groups, best_time)
|
||||
if is_better then
|
||||
new_index = index
|
||||
end
|
||||
end
|
||||
|
||||
return new_index,best_time
|
||||
end
|
||||
|
||||
function ws.get_digtime(nodename)
|
||||
local idx,tm=find_best_tool(nodename)
|
||||
return tm
|
||||
end
|
||||
|
||||
function ws.select_best_tool(pos)
|
||||
local nd=minetest.get_node_or_nil(pos)
|
||||
local nodename='air'
|
||||
if nd then nodename=nd.name end
|
||||
local t=find_best_tool(nodename)
|
||||
minetest.localplayer:set_wield_index(ws.to_hotbar(t,hotbar_slot))
|
||||
--minetest.localplayer:set_wield_index(find_best_tool(nodename))
|
||||
end
|
||||
|
||||
--- COORDS
|
||||
function ws.coord(x, y, z)
|
||||
return vector.new(x,y,z)
|
||||
end
|
||||
function ws.ordercoord(c)
|
||||
if c.x == nil then
|
||||
return {x = c[1], y = c[2], z = c[3]}
|
||||
else
|
||||
return c
|
||||
end
|
||||
end
|
||||
|
||||
-- x or {x,y,z} or {x=x,y=y,z=z}
|
||||
function ws.optcoord(x, y, z)
|
||||
if y and z then
|
||||
return ws.coord(x, y, z)
|
||||
else
|
||||
return ws.ordercoord(x)
|
||||
end
|
||||
end
|
||||
function ws.cadd(c1, c2)
|
||||
return vector.add(c1,c2)
|
||||
--return ws.coord(c1.x + c2.x, c1.y + c2.y, c1.z + c2.z)
|
||||
end
|
||||
|
||||
function ws.relcoord(x, y, z, rpos)
|
||||
local pos = rpos or minetest.localplayer:get_pos()
|
||||
pos.y=math.ceil(pos.y)
|
||||
--math.floor(pos.y) + 0.5
|
||||
return ws.cadd(pos, ws.optcoord(x, y, z))
|
||||
end
|
||||
|
||||
local function between(x, y, z) -- x is between y and z (inclusive)
|
||||
return y <= x and x <= z
|
||||
end
|
||||
|
||||
function ws.getdir(yaw) --
|
||||
local rot = yaw or minetest.localplayer:get_yaw() % 360
|
||||
if between(rot, 315, 360) or between(rot, 0, 45) then
|
||||
return "north"
|
||||
elseif between(rot, 135, 225) then
|
||||
return "south"
|
||||
elseif between(rot, 225, 315) then
|
||||
return "east"
|
||||
elseif between(rot, 45, 135) then
|
||||
return "west"
|
||||
end
|
||||
end
|
||||
|
||||
function ws.getaxis()
|
||||
local dir=ws.getdir()
|
||||
if dir == "north" or dir == "south" then return "z" end
|
||||
return "x"
|
||||
end
|
||||
function ws.setdir(dir) --
|
||||
if dir == "north" then
|
||||
minetest.localplayer:set_yaw(0)
|
||||
elseif dir == "south" then
|
||||
minetest.localplayer:set_yaw(180)
|
||||
elseif dir == "east" then
|
||||
minetest.localplayer:set_yaw(270)
|
||||
elseif dir == "west" then
|
||||
minetest.localplayer:set_yaw(90)
|
||||
end
|
||||
end
|
||||
|
||||
function ws.dircoord(f, y, r ,rpos, rdir)
|
||||
local dir= ws.getdir(rdir)
|
||||
local coord = ws.optcoord(f, y, r)
|
||||
local f = coord.x
|
||||
local y = coord.y
|
||||
local r = coord.z
|
||||
local lp= rpos or minetest.localplayer:get_pos()
|
||||
if dir == "north" then
|
||||
return ws.relcoord(r, y, f,rpos)
|
||||
elseif dir == "south" then
|
||||
return ws.relcoord(-r, y, -f,rpos)
|
||||
elseif dir == "east" then
|
||||
return ws.relcoord(f, y, -r,rpos)
|
||||
elseif dir== "west" then
|
||||
return ws.relcoord(-f, y, r,rpos)
|
||||
end
|
||||
return ws.relcoord(0, 0, 0,rpos)
|
||||
end
|
||||
|
||||
function ws.get_dimension(pos)
|
||||
if pos.y > -65 then return "overworld"
|
||||
elseif pos.y > -8000 then return "void"
|
||||
elseif pos.y > -27000 then return "end"
|
||||
elseif pos.y > -28930 then return "void"
|
||||
elseif pos.y > -31000 then return "nether"
|
||||
else return "void"
|
||||
end
|
||||
end
|
||||
|
||||
function ws.aim(tpos)
|
||||
local ppos=minetest.localplayer:get_pos()
|
||||
local dir=vector.direction(ppos,tpos)
|
||||
local yyaw=0;
|
||||
local pitch=0;
|
||||
if dir.x < 0 then
|
||||
yyaw = math.atan2(-dir.x, dir.z) + (math.pi * 2)
|
||||
else
|
||||
yyaw = math.atan2(-dir.x, dir.z)
|
||||
end
|
||||
yyaw = ws.round2(math.deg(yyaw),2)
|
||||
pitch = ws.round2(math.deg(math.asin(-dir.y) * 1),2);
|
||||
minetest.localplayer:set_yaw(yyaw)
|
||||
minetest.localplayer:set_pitch(pitch)
|
||||
end
|
||||
|
||||
function ws.gaim(tpos,v,g)
|
||||
local v = v or 40
|
||||
local g = g or 9.81
|
||||
local ppos=minetest.localplayer:get_pos()
|
||||
local dir=vector.direction(ppos,tpos)
|
||||
local yyaw=0;
|
||||
local pitch=0;
|
||||
if dir.x < 0 then
|
||||
yyaw = math.atan2(-dir.x, dir.z) + (math.pi * 2)
|
||||
else
|
||||
yyaw = math.atan2(-dir.x, dir.z)
|
||||
end
|
||||
yyaw = ws.round2(math.deg(yyaw),2)
|
||||
local y = dir.y
|
||||
dir.y = 0
|
||||
local x = vector.length(dir)
|
||||
pitch=math.atan(math.pow(v, 2) / (g * x) + math.sqrt(math.pow(v, 4)/(math.pow(g, 2) * math.pow(x, 2)) - 2 * math.pow(v, 2) * y/(g * math.pow(x, 2)) - 1))
|
||||
--pitch = ws.round2(math.deg(math.asin(-dir.y) * 1),2);
|
||||
minetest.localplayer:set_yaw(yyaw)
|
||||
minetest.localplayer:set_pitch(math.deg(pitch))
|
||||
end
|
||||
|
||||
function ws.buildable_to(pos)
|
||||
local node=minetest.get_node_or_nil(pos)
|
||||
if node then
|
||||
return minetest.get_node_def(node.name).buildable_to
|
||||
end
|
||||
end
|
||||
|
||||
function ws.tplace(p,n,stay)
|
||||
if not p then return end
|
||||
if n then ws.switch_to_item(n) end
|
||||
local opos=ws.dircoord(0,0,0)
|
||||
local tpos=vector.add(p,vector.new(0,1,0))
|
||||
minetest.localplayer:set_pos(tpos)
|
||||
ws.place(p,{n})
|
||||
if not stay then
|
||||
minetest.after(0.1,function()
|
||||
minetest.localplayer:set_pos(opos)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
minetest.register_chatcommand("tplace", {
|
||||
description = "tp-place",
|
||||
param = "Y",
|
||||
func = function(param)
|
||||
return ws.tplace(minetest.string_to_pos(param))
|
||||
end
|
||||
})
|
||||
|
||||
function ws.ytp(param)
|
||||
local y=tonumber(param)
|
||||
local lp=ws.dircoord(0,0,0)
|
||||
if lp.y < y + 50 then return false,"Can't TP up." end
|
||||
if y < -30912 then return false,"Don't TP into the void lol." end
|
||||
minetest.localplayer:set_pos(vector.new(lp.x,y,lp.z))
|
||||
end
|
||||
|
||||
local function tablearg(arg)
|
||||
local tb={}
|
||||
if type(arg) == 'string' then
|
||||
tb={arg}
|
||||
elseif type(arg) == 'table' then
|
||||
tb=arg
|
||||
elseif type(arg) == 'function' then
|
||||
tb=arg()
|
||||
end
|
||||
return tb
|
||||
end
|
||||
|
||||
function ws.isnode(pos,arg)--arg is either an itemstring, a table of itemstrings or a function returning an itemstring
|
||||
local nodename=tablearg(arg)
|
||||
local nd=minetest.get_node_or_nil(pos)
|
||||
if nd and nodename and ws.in_list(nd.name,nodename) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function ws.can_place_at(pos)
|
||||
local node = minetest.get_node_or_nil(pos)
|
||||
return (node and (node.name == "air" or node.name=="mcl_core:water_source" or node.name=="mcl_core:water_flowing" or node.name=="mcl_core:lava_source" or node.name=="mcl_core:lava_flowing" or minetest.get_node_def(node.name).buildable_to))
|
||||
end
|
||||
|
||||
-- should check if wield is placeable
|
||||
-- minetest.get_node(wielded:get_name()) ~= nil should probably work
|
||||
-- otherwise it equips armor and eats food
|
||||
function ws.can_place_wielded_at(pos)
|
||||
local wield_empty = minetest.localplayer:get_wielded_item():is_empty()
|
||||
return not wield_empty and ws.can_place_at(pos)
|
||||
end
|
||||
|
||||
|
||||
function ws.find_any_swap(items,hslot)
|
||||
hslot=hslot or 8
|
||||
for i, v in ipairs(items) do
|
||||
local n = minetest.find_item(v)
|
||||
if n then
|
||||
ws.switch_to_item(v,hslot)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
-- swaps to any of the items and places if need be
|
||||
-- returns true if placed and in inventory or already there, false otherwise
|
||||
|
||||
local lastact=0
|
||||
local lastplc=0
|
||||
local lastdig=0
|
||||
local actint=10
|
||||
function ws.place(pos,items,hslot, place)
|
||||
--if nodes_this_tick > 8 then return end
|
||||
--nodes_this_tick = nodes_this_tick + 1
|
||||
--if not inside_constraints(pos) then return end
|
||||
if not pos then return end
|
||||
if not ws.can_place_at(pos) then return end
|
||||
items=tablearg(items)
|
||||
|
||||
place = place or minetest.place_node
|
||||
|
||||
local node = minetest.get_node_or_nil(pos)
|
||||
if not node then return end
|
||||
-- already there
|
||||
if ws.isnode(pos,items) then
|
||||
return true
|
||||
else
|
||||
if ws.find_any_swap(items,hslot) then
|
||||
place(pos)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ws.place_if_able(pos)
|
||||
if not pos then return end
|
||||
if not inside_constraints(pos) then return end
|
||||
if ws.can_place_wielded_at(pos) then
|
||||
minetest.place_node(pos)
|
||||
end
|
||||
end
|
||||
|
||||
function ws.is_diggable(pos)
|
||||
if not pos then return false end
|
||||
local nd=minetest.get_node_or_nil(pos)
|
||||
if not nd then return false end
|
||||
local n = minetest.get_node_def(nd.name)
|
||||
if n and n.diggable then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
function ws.dig(pos,condition,autotool)
|
||||
--if not inside_constraints(pos) then return end
|
||||
if autotool == nil then autotool = true end
|
||||
if condition and not condition(pos) then return false end
|
||||
if not ws.is_diggable(pos) then return end
|
||||
local nd=minetest.get_node_or_nil(pos)
|
||||
if nd and minetest.get_node_def(nd.name).diggable then
|
||||
if autotool then ws.select_best_tool(pos) end
|
||||
minetest.dig_node(pos)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ws.chunk_loaded()
|
||||
local ign=minetest.find_nodes_near(ws.dircoord(0,0,0),10,{'ignore'},true)
|
||||
if #ign == 0 then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
function ws.get_near(nodes,range)
|
||||
range=range or 5
|
||||
local nds=minetest.find_nodes_near(ws.dircoord(0,0,0),rang,nodes,true)
|
||||
if #nds > 0 then return nds end
|
||||
return false
|
||||
end
|
||||
|
||||
function ws.is_laggy()
|
||||
if tps_client and tps_client.ping and tps_client.ping > 1000 then return true end
|
||||
end
|
||||
|
||||
|
||||
function ws.donodes(poss,func,condition)
|
||||
if ws.is_laggy() then return end
|
||||
local dn_i=0
|
||||
for k,v in pairs(poss) do
|
||||
if dn_i > 8 then return end
|
||||
--local nd=minetest.get_node_or_nil(v)
|
||||
if condition == nil or condition(v) then
|
||||
func(v)
|
||||
dn_i = dn_i + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ws.dignodes(poss,condition)
|
||||
local func=function(p) ws.dig(p) end
|
||||
ws.donodes(poss,func,condition)
|
||||
end
|
||||
|
||||
|
||||
function ws.replace(pos,arg)
|
||||
arg=tablearg(arg)
|
||||
local nd=minetest.get_node_or_nil(pos)
|
||||
if nd and not ws.in_list(nd.name,arg) and ws.buildable_to(pos) then
|
||||
local tm=ws.get_digtime(nd.name) or 0
|
||||
ws.dig(pos)
|
||||
minetest.after(tm + 0.1,function()
|
||||
ws.place(pos,arg)
|
||||
end)
|
||||
return tm
|
||||
else
|
||||
return ws.place(pos,arg)
|
||||
end
|
||||
end
|
||||
|
||||
function ws.playeron(p)
|
||||
local pls=minetest.get_player_names()
|
||||
for k,v in pairs(pls) do
|
||||
if v == p then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function ws.between(x, y, z) -- x is between y and z (inclusive)
|
||||
return y <= x and x <= z
|
||||
end
|
||||
|
||||
|
||||
local wall_pos1={x=-1255,y=6,z=792}
|
||||
local wall_pos2={x=-1452,y=80,z=981}
|
||||
local iwall_pos1={x=-1266,y=6,z=802}
|
||||
local iwall_pos2={x=-1442,y=80,z=971}
|
||||
|
||||
function ws.in_cube(tpos,wpos1,wpos2)
|
||||
local xmax=wpos2.x
|
||||
local xmin=wpos1.x
|
||||
|
||||
local ymax=wpos2.y
|
||||
local ymin=wpos1.y
|
||||
|
||||
local zmax=wpos2.z
|
||||
local zmin=wpos1.z
|
||||
if wpos1.x > wpos2.x then
|
||||
xmax=wpos1.x
|
||||
xmin=wpos2.x
|
||||
end
|
||||
if wpos1.y > wpos2.y then
|
||||
ymax=wpos1.y
|
||||
ymin=wpos2.y
|
||||
end
|
||||
if wpos1.z > wpos2.z then
|
||||
zmax=wpos1.z
|
||||
zmin=wpos2.z
|
||||
end
|
||||
if ws.between(tpos.x,xmin,xmax) and ws.between(tpos.y,ymin,ymax) and ws.between(tpos.z,zmin,zmax) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ws.in_wall(pos)
|
||||
if ws.in_cube(pos,wall_pos1,wall_pos2) and not in_cube(pos,iwall_pos1,iwall_pos2) then
|
||||
return true end
|
||||
return false
|
||||
end
|
||||
|
||||
function ws.inside_wall(pos)
|
||||
local p1=iwall_pos1
|
||||
local p2=iwall_pos2
|
||||
if ws.in_cube(pos,p1,p2) then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
function ws.find_closest_reachable_airpocket(pos)
|
||||
local lp=ws.dircoord(0,0,0)
|
||||
local nds=minetest.find_nodes_near(lp,5,{'air'})
|
||||
local odst=10
|
||||
local rt=lp
|
||||
for k,v in ipairs(nds) do
|
||||
local dst=vector.distance(pos,v)
|
||||
if dst < odst then odst=dst rt=v end
|
||||
end
|
||||
if odst==10 then return false end
|
||||
return vector.add(rt,vector.new(0,-1,0))
|
||||
end
|
||||
|
||||
|
||||
-- DEBUG
|
||||
local function printwieldedmeta()
|
||||
ws.dcm(dump(minetest.localplayer:get_wielded_item():get_meta():to_table()))
|
||||
end
|
||||
minetest.register_cheat('ItemMeta','Test',printwieldedmeta)
|
43
builtin/common/after.lua
Normal file
43
builtin/common/after.lua
Normal file
@ -0,0 +1,43 @@
|
||||
local jobs = {}
|
||||
local time = 0.0
|
||||
local time_next = math.huge
|
||||
|
||||
core.register_globalstep(function(dtime)
|
||||
time = time + dtime
|
||||
|
||||
if time < time_next then
|
||||
return
|
||||
end
|
||||
|
||||
time_next = math.huge
|
||||
|
||||
-- Iterate backwards so that we miss any new timers added by
|
||||
-- a timer callback.
|
||||
for i = #jobs, 1, -1 do
|
||||
local job = jobs[i]
|
||||
if time >= job.expire then
|
||||
core.set_last_run_mod(job.mod_origin)
|
||||
job.func(unpack(job.arg))
|
||||
local jobs_l = #jobs
|
||||
jobs[i] = jobs[jobs_l]
|
||||
jobs[jobs_l] = nil
|
||||
elseif job.expire < time_next then
|
||||
time_next = job.expire
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function core.after(after, func, ...)
|
||||
assert(tonumber(after) and type(func) == "function",
|
||||
"Invalid minetest.after invocation")
|
||||
local expire = time + after
|
||||
local new_job = {
|
||||
func = func,
|
||||
expire = expire,
|
||||
arg = {...},
|
||||
mod_origin = core.get_last_run_mod(),
|
||||
}
|
||||
jobs[#jobs + 1] = new_job
|
||||
time_next = math.min(time_next, expire)
|
||||
return { cancel = function() new_job.func = function() end end }
|
||||
end
|
200
builtin/common/chatcommands.lua
Normal file
200
builtin/common/chatcommands.lua
Normal file
@ -0,0 +1,200 @@
|
||||
-- Minetest: builtin/common/chatcommands.lua
|
||||
|
||||
core.registered_chatcommands = {}
|
||||
|
||||
function core.register_chatcommand(cmd, def)
|
||||
def = def or {}
|
||||
def.params = def.params or ""
|
||||
def.description = def.description or ""
|
||||
def.privs = def.privs or {}
|
||||
def.mod_origin = core.get_current_modname() or "??"
|
||||
core.registered_chatcommands[cmd] = def
|
||||
end
|
||||
|
||||
function core.unregister_chatcommand(name)
|
||||
if core.registered_chatcommands[name] then
|
||||
core.registered_chatcommands[name] = nil
|
||||
else
|
||||
core.log("warning", "Not unregistering chatcommand " ..name..
|
||||
" because it doesn't exist.")
|
||||
end
|
||||
end
|
||||
|
||||
function core.override_chatcommand(name, redefinition)
|
||||
local chatcommand = core.registered_chatcommands[name]
|
||||
assert(chatcommand, "Attempt to override non-existent chatcommand "..name)
|
||||
for k, v in pairs(redefinition) do
|
||||
rawset(chatcommand, k, v)
|
||||
end
|
||||
core.registered_chatcommands[name] = chatcommand
|
||||
end
|
||||
|
||||
if INIT == "client" then
|
||||
function core.register_list_command(command, desc, setting)
|
||||
local def = {}
|
||||
def.description = desc
|
||||
def.params = "del <item> | add <item> | list"
|
||||
function def.func(param)
|
||||
local list = (minetest.settings:get(setting) or ""):split(",")
|
||||
if param == "list" then
|
||||
return true, table.concat(list, ", ")
|
||||
else
|
||||
local sparam = param:split(" ")
|
||||
local cmd = sparam[1]
|
||||
local item = sparam[2]
|
||||
if cmd == "del" then
|
||||
if not item then
|
||||
return false, "Missing item."
|
||||
end
|
||||
local i = table.indexof(list, item)
|
||||
if i == -1 then
|
||||
return false, item .. " is not on the list."
|
||||
else
|
||||
table.remove(list, i)
|
||||
core.settings:set(setting, table.concat(list, ","))
|
||||
return true, "Removed " .. item .. " from the list."
|
||||
end
|
||||
elseif cmd == "add" then
|
||||
if not item then
|
||||
return false, "Missing item."
|
||||
end
|
||||
local i = table.indexof(list, item)
|
||||
if i ~= -1 then
|
||||
return false, item .. " is already on the list."
|
||||
else
|
||||
table.insert(list, item)
|
||||
core.settings:set(setting, table.concat(list, ","))
|
||||
return true, "Added " .. item .. " to the list."
|
||||
end
|
||||
end
|
||||
end
|
||||
return false, "Invalid usage. (See .help " .. command .. ")"
|
||||
end
|
||||
core.register_chatcommand(command, def)
|
||||
end
|
||||
end
|
||||
|
||||
local cmd_marker = "/"
|
||||
|
||||
local function gettext(...)
|
||||
return ...
|
||||
end
|
||||
|
||||
local function gettext_replace(text, replace)
|
||||
return text:gsub("$1", replace)
|
||||
end
|
||||
|
||||
|
||||
if INIT == "client" then
|
||||
cmd_marker = "."
|
||||
gettext = core.gettext
|
||||
gettext_replace = fgettext_ne
|
||||
end
|
||||
|
||||
local function do_help_cmd(name, param)
|
||||
local function format_help_line(cmd, def)
|
||||
local msg = core.colorize("#00ffff", cmd_marker .. cmd)
|
||||
if def.params and def.params ~= "" then
|
||||
msg = msg .. " " .. def.params
|
||||
end
|
||||
if def.description and def.description ~= "" then
|
||||
msg = msg .. ": " .. def.description
|
||||
end
|
||||
return msg
|
||||
end
|
||||
if param == "" then
|
||||
local cmds = {}
|
||||
for cmd, def in pairs(core.registered_chatcommands) do
|
||||
if INIT == "client" or core.check_player_privs(name, def.privs) then
|
||||
cmds[#cmds + 1] = cmd
|
||||
end
|
||||
end
|
||||
table.sort(cmds)
|
||||
return true, gettext("Available commands: ") .. table.concat(cmds, " ") .. "\n"
|
||||
.. gettext_replace("Use '$1help <cmd>' to get more information,"
|
||||
.. " or '$1help all' to list everything.", cmd_marker)
|
||||
elseif param == "all" then
|
||||
local cmds = {}
|
||||
for cmd, def in pairs(core.registered_chatcommands) do
|
||||
if INIT == "client" or core.check_player_privs(name, def.privs) then
|
||||
cmds[#cmds + 1] = format_help_line(cmd, def)
|
||||
end
|
||||
end
|
||||
table.sort(cmds)
|
||||
return true, gettext("Available commands:").."\n"..table.concat(cmds, "\n")
|
||||
elseif INIT == "game" and param == "privs" then
|
||||
local privs = {}
|
||||
for priv, def in pairs(core.registered_privileges) do
|
||||
privs[#privs + 1] = priv .. ": " .. def.description
|
||||
end
|
||||
table.sort(privs)
|
||||
return true, "Available privileges:\n"..table.concat(privs, "\n")
|
||||
else
|
||||
local cmd = param
|
||||
local def = core.registered_chatcommands[cmd]
|
||||
if not def then
|
||||
return false, gettext("Command not available: ")..cmd
|
||||
else
|
||||
return true, format_help_line(cmd, def)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if INIT == "client" then
|
||||
core.register_chatcommand("help", {
|
||||
params = gettext("[all | <cmd>]"),
|
||||
description = gettext("Get help for commands"),
|
||||
func = function(param)
|
||||
return do_help_cmd(nil, param)
|
||||
end,
|
||||
})
|
||||
|
||||
function core.register_list_command(command, desc, setting)
|
||||
local def = {}
|
||||
def.description = desc
|
||||
def.params = "del <item> | add <item> | list"
|
||||
function def.func(param)
|
||||
local list = (minetest.settings:get(setting) or ""):split(",")
|
||||
if param == "list" then
|
||||
return true, table.concat(list, ", ")
|
||||
else
|
||||
local sparam = param:split(" ")
|
||||
local cmd = sparam[1]
|
||||
local item = sparam[2]
|
||||
if cmd == "del" then
|
||||
if not item then
|
||||
return false, "Missing item."
|
||||
end
|
||||
local i = table.indexof(list, item)
|
||||
if i == -1 then
|
||||
return false, item .. " is not on the list."
|
||||
else
|
||||
table.remove(list, i)
|
||||
core.settings:set(setting, table.concat(list, ","))
|
||||
return true, "Removed " .. item .. " from the list."
|
||||
end
|
||||
elseif cmd == "add" then
|
||||
if not item then
|
||||
return false, "Missing item."
|
||||
end
|
||||
local i = table.indexof(list, item)
|
||||
if i ~= -1 then
|
||||
return false, item .. " is already on the list."
|
||||
else
|
||||
table.insert(list, item)
|
||||
core.settings:set(setting, table.concat(list, ","))
|
||||
return true, "Added " .. item .. " to the list."
|
||||
end
|
||||
end
|
||||
end
|
||||
return false, "Invalid usage. (See /help " .. command .. ")"
|
||||
end
|
||||
core.register_chatcommand(command, def)
|
||||
end
|
||||
else
|
||||
core.register_chatcommand("help", {
|
||||
params = "[all | privs | <cmd>]",
|
||||
description = "Get help for commands or list privileges",
|
||||
func = do_help_cmd,
|
||||
})
|
||||
end
|
319
builtin/common/filterlist.lua
Normal file
319
builtin/common/filterlist.lua
Normal file
@ -0,0 +1,319 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- TODO improve doc --
|
||||
-- TODO code cleanup --
|
||||
-- Generic implementation of a filter/sortable list --
|
||||
-- Usage: --
|
||||
-- Filterlist needs to be initialized on creation. To achieve this you need to --
|
||||
-- pass following functions: --
|
||||
-- raw_fct() (mandatory): --
|
||||
-- function returning a table containing the elements to be filtered --
|
||||
-- compare_fct(element1,element2) (mandatory): --
|
||||
-- function returning true/false if element1 is same element as element2 --
|
||||
-- uid_match_fct(element1,uid) (optional) --
|
||||
-- function telling if uid is attached to element1 --
|
||||
-- filter_fct(element,filtercriteria) (optional) --
|
||||
-- function returning true/false if filtercriteria met to element --
|
||||
-- fetch_param (optional) --
|
||||
-- parameter passed to raw_fct to aquire correct raw data --
|
||||
-- --
|
||||
--------------------------------------------------------------------------------
|
||||
filterlist = {}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.refresh(self)
|
||||
self.m_raw_list = self.m_raw_list_fct(self.m_fetch_param)
|
||||
filterlist.process(self)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.create(raw_fct,compare_fct,uid_match_fct,filter_fct,fetch_param)
|
||||
|
||||
assert((raw_fct ~= nil) and (type(raw_fct) == "function"))
|
||||
assert((compare_fct ~= nil) and (type(compare_fct) == "function"))
|
||||
|
||||
local self = {}
|
||||
|
||||
self.m_raw_list_fct = raw_fct
|
||||
self.m_compare_fct = compare_fct
|
||||
self.m_filter_fct = filter_fct
|
||||
self.m_uid_match_fct = uid_match_fct
|
||||
|
||||
self.m_filtercriteria = nil
|
||||
self.m_fetch_param = fetch_param
|
||||
|
||||
self.m_sortmode = "none"
|
||||
self.m_sort_list = {}
|
||||
|
||||
self.m_processed_list = nil
|
||||
self.m_raw_list = self.m_raw_list_fct(self.m_fetch_param)
|
||||
|
||||
self.add_sort_mechanism = filterlist.add_sort_mechanism
|
||||
self.set_filtercriteria = filterlist.set_filtercriteria
|
||||
self.get_filtercriteria = filterlist.get_filtercriteria
|
||||
self.set_sortmode = filterlist.set_sortmode
|
||||
self.get_list = filterlist.get_list
|
||||
self.get_raw_list = filterlist.get_raw_list
|
||||
self.get_raw_element = filterlist.get_raw_element
|
||||
self.get_raw_index = filterlist.get_raw_index
|
||||
self.get_current_index = filterlist.get_current_index
|
||||
self.size = filterlist.size
|
||||
self.uid_exists_raw = filterlist.uid_exists_raw
|
||||
self.raw_index_by_uid = filterlist.raw_index_by_uid
|
||||
self.refresh = filterlist.refresh
|
||||
|
||||
filterlist.process(self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.add_sort_mechanism(self,name,fct)
|
||||
self.m_sort_list[name] = fct
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.set_filtercriteria(self,criteria)
|
||||
if criteria == self.m_filtercriteria and
|
||||
type(criteria) ~= "table" then
|
||||
return
|
||||
end
|
||||
self.m_filtercriteria = criteria
|
||||
filterlist.process(self)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_filtercriteria(self)
|
||||
return self.m_filtercriteria
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--supported sort mode "alphabetic|none"
|
||||
function filterlist.set_sortmode(self,mode)
|
||||
if (mode == self.m_sortmode) then
|
||||
return
|
||||
end
|
||||
self.m_sortmode = mode
|
||||
filterlist.process(self)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_list(self)
|
||||
return self.m_processed_list
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_raw_list(self)
|
||||
return self.m_raw_list
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_raw_element(self,idx)
|
||||
if type(idx) ~= "number" then
|
||||
idx = tonumber(idx)
|
||||
end
|
||||
|
||||
if idx ~= nil and idx > 0 and idx <= #self.m_raw_list then
|
||||
return self.m_raw_list[idx]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_raw_index(self,listindex)
|
||||
assert(self.m_processed_list ~= nil)
|
||||
|
||||
if listindex ~= nil and listindex > 0 and
|
||||
listindex <= #self.m_processed_list then
|
||||
local entry = self.m_processed_list[listindex]
|
||||
|
||||
for i,v in ipairs(self.m_raw_list) do
|
||||
|
||||
if self.m_compare_fct(v,entry) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_current_index(self,listindex)
|
||||
assert(self.m_processed_list ~= nil)
|
||||
|
||||
if listindex ~= nil and listindex > 0 and
|
||||
listindex <= #self.m_raw_list then
|
||||
local entry = self.m_raw_list[listindex]
|
||||
|
||||
for i,v in ipairs(self.m_processed_list) do
|
||||
|
||||
if self.m_compare_fct(v,entry) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.process(self)
|
||||
assert(self.m_raw_list ~= nil)
|
||||
|
||||
if self.m_sortmode == "none" and
|
||||
self.m_filtercriteria == nil then
|
||||
self.m_processed_list = self.m_raw_list
|
||||
return
|
||||
end
|
||||
|
||||
self.m_processed_list = {}
|
||||
|
||||
for k,v in pairs(self.m_raw_list) do
|
||||
if self.m_filtercriteria == nil or
|
||||
self.m_filter_fct(v,self.m_filtercriteria) then
|
||||
self.m_processed_list[#self.m_processed_list + 1] = v
|
||||
end
|
||||
end
|
||||
|
||||
if self.m_sortmode == "none" then
|
||||
return
|
||||
end
|
||||
|
||||
if self.m_sort_list[self.m_sortmode] ~= nil and
|
||||
type(self.m_sort_list[self.m_sortmode]) == "function" then
|
||||
|
||||
self.m_sort_list[self.m_sortmode](self)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.size(self)
|
||||
if self.m_processed_list == nil then
|
||||
return 0
|
||||
end
|
||||
|
||||
return #self.m_processed_list
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.uid_exists_raw(self,uid)
|
||||
for i,v in ipairs(self.m_raw_list) do
|
||||
if self.m_uid_match_fct(v,uid) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.raw_index_by_uid(self, uid)
|
||||
local elementcount = 0
|
||||
local elementidx = 0
|
||||
for i,v in ipairs(self.m_raw_list) do
|
||||
if self.m_uid_match_fct(v,uid) then
|
||||
elementcount = elementcount +1
|
||||
elementidx = i
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- If there are more elements than one with same name uid can't decide which
|
||||
-- one is meant. self shouldn't be possible but just for sure.
|
||||
if elementcount > 1 then
|
||||
elementidx=0
|
||||
end
|
||||
|
||||
return elementidx
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- COMMON helper functions --
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function compare_worlds(world1,world2)
|
||||
if world1.path ~= world2.path then
|
||||
return false
|
||||
end
|
||||
|
||||
if world1.name ~= world2.name then
|
||||
return false
|
||||
end
|
||||
|
||||
if world1.gameid ~= world2.gameid then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function sort_worlds_alphabetic(self)
|
||||
|
||||
table.sort(self.m_processed_list, function(a, b)
|
||||
--fixes issue #857 (crash due to sorting nil in worldlist)
|
||||
if a == nil or b == nil then
|
||||
if a == nil and b ~= nil then return false end
|
||||
if b == nil and a ~= nil then return true end
|
||||
return false
|
||||
end
|
||||
if a.name:lower() == b.name:lower() then
|
||||
return a.name < b.name
|
||||
end
|
||||
return a.name:lower() < b.name:lower()
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function sort_mod_list(self)
|
||||
|
||||
table.sort(self.m_processed_list, function(a, b)
|
||||
-- Show game mods at bottom
|
||||
if a.type ~= b.type or a.loc ~= b.loc then
|
||||
if b.type == "game" then
|
||||
return a.loc ~= "game"
|
||||
end
|
||||
return b.loc == "game"
|
||||
end
|
||||
-- If in same or no modpack, sort by name
|
||||
if a.modpack == b.modpack then
|
||||
if a.name:lower() == b.name:lower() then
|
||||
return a.name < b.name
|
||||
end
|
||||
return a.name:lower() < b.name:lower()
|
||||
-- Else compare name to modpack name
|
||||
else
|
||||
-- Always show modpack pseudo-mod on top of modpack mod list
|
||||
if a.name == b.modpack then
|
||||
return true
|
||||
elseif b.name == a.modpack then
|
||||
return false
|
||||
end
|
||||
|
||||
local name_a = a.modpack or a.name
|
||||
local name_b = b.modpack or b.name
|
||||
if name_a:lower() == name_b:lower() then
|
||||
return name_a < name_b
|
||||
end
|
||||
return name_a:lower() < name_b:lower()
|
||||
end
|
||||
end)
|
||||
end
|
152
builtin/common/information_formspecs.lua
Normal file
152
builtin/common/information_formspecs.lua
Normal file
@ -0,0 +1,152 @@
|
||||
local COLOR_BLUE = "#7AF"
|
||||
local COLOR_GREEN = "#7F7"
|
||||
local COLOR_GRAY = "#BBB"
|
||||
|
||||
local LIST_FORMSPEC = [[
|
||||
size[13,6.5]
|
||||
label[0,-0.1;%s]
|
||||
tablecolumns[color;tree;text;text]
|
||||
table[0,0.5;12.8,5.5;list;%s;0]
|
||||
button_exit[5,6;3,1;quit;%s]
|
||||
]]
|
||||
|
||||
local LIST_FORMSPEC_DESCRIPTION = [[
|
||||
size[13,7.5]
|
||||
label[0,-0.1;%s]
|
||||
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]
|
||||
button_exit[5,7;3,1;quit;%s]
|
||||
]]
|
||||
|
||||
local formspec_escape = core.formspec_escape
|
||||
local check_player_privs = core.check_player_privs
|
||||
|
||||
|
||||
-- CHAT COMMANDS FORMSPEC
|
||||
|
||||
local mod_cmds = {}
|
||||
|
||||
local function load_mod_command_tree()
|
||||
mod_cmds = {}
|
||||
|
||||
for name, def in pairs(core.registered_chatcommands) do
|
||||
mod_cmds[def.mod_origin] = mod_cmds[def.mod_origin] or {}
|
||||
local cmds = mod_cmds[def.mod_origin]
|
||||
|
||||
-- Could be simplified, but avoid the priv checks whenever possible
|
||||
cmds[#cmds + 1] = { name, def }
|
||||
end
|
||||
local sorted_mod_cmds = {}
|
||||
for modname, cmds in pairs(mod_cmds) do
|
||||
table.sort(cmds, function(a, b) return a[1] < b[1] end)
|
||||
sorted_mod_cmds[#sorted_mod_cmds + 1] = { modname, cmds }
|
||||
end
|
||||
table.sort(sorted_mod_cmds, function(a, b) return a[1] < b[1] end)
|
||||
mod_cmds = sorted_mod_cmds
|
||||
end
|
||||
|
||||
core.after(0, load_mod_command_tree)
|
||||
|
||||
local function build_chatcommands_formspec(name, sel, copy)
|
||||
local rows = {}
|
||||
rows[1] = "#FFF,0,Command,Parameters"
|
||||
|
||||
local description = "For more information, click on any entry in the list.\n" ..
|
||||
"Double-click to copy the entry to the chat history."
|
||||
|
||||
for i, data in ipairs(mod_cmds) do
|
||||
rows[#rows + 1] = COLOR_BLUE .. ",0," .. formspec_escape(data[1]) .. ","
|
||||
for j, cmds in ipairs(data[2]) do
|
||||
local has_priv = check_player_privs(name, cmds[2].privs)
|
||||
rows[#rows + 1] = ("%s,1,%s,%s"):format(
|
||||
has_priv and COLOR_GREEN or COLOR_GRAY,
|
||||
cmds[1], formspec_escape(cmds[2].params))
|
||||
if sel == #rows then
|
||||
description = cmds[2].description
|
||||
if copy then
|
||||
core.chat_send_player(name, ("Command: %s %s"):format(
|
||||
core.colorize("#0FF", "/" .. cmds[1]), cmds[2].params))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return LIST_FORMSPEC_DESCRIPTION:format(
|
||||
"Available commands: (see also: /help <cmd>)",
|
||||
table.concat(rows, ","), sel or 0,
|
||||
description, "Close"
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
-- PRIVILEGES FORMSPEC
|
||||
|
||||
local function build_privs_formspec(name)
|
||||
local privs = {}
|
||||
for priv_name, def in pairs(core.registered_privileges) do
|
||||
privs[#privs + 1] = { priv_name, def }
|
||||
end
|
||||
table.sort(privs, function(a, b) return a[1] < b[1] end)
|
||||
|
||||
local rows = {}
|
||||
rows[1] = "#FFF,0,Privilege,Description"
|
||||
|
||||
local player_privs = core.get_player_privs(name)
|
||||
for i, data in ipairs(privs) do
|
||||
rows[#rows + 1] = ("%s,0,%s,%s"):format(
|
||||
player_privs[data[1]] and COLOR_GREEN or COLOR_GRAY,
|
||||
data[1], formspec_escape(data[2].description))
|
||||
end
|
||||
|
||||
return LIST_FORMSPEC:format(
|
||||
"Available privileges:",
|
||||
table.concat(rows, ","),
|
||||
"Close"
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
-- DETAILED CHAT COMMAND INFORMATION
|
||||
|
||||
core.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname ~= "__builtin:help_cmds" or fields.quit then
|
||||
return
|
||||
end
|
||||
|
||||
local event = minetest.explode_table_event(fields.list)
|
||||
if event.type ~= "INV" then
|
||||
local name = player:get_player_name()
|
||||
core.show_formspec(name, "__builtin:help_cmds",
|
||||
build_chatcommands_formspec(name, event.row, event.type == "DCL"))
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
local help_command = core.registered_chatcommands["help"]
|
||||
local old_help_func = help_command.func
|
||||
|
||||
help_command.func = function(name, param)
|
||||
local admin = core.settings:get("name")
|
||||
|
||||
-- If the admin ran help, put the output in the chat buffer as well to
|
||||
-- work with the server terminal
|
||||
if param == "privs" then
|
||||
core.show_formspec(name, "__builtin:help_privs",
|
||||
build_privs_formspec(name))
|
||||
if name ~= admin then
|
||||
return true
|
||||
end
|
||||
end
|
||||
if param == "" or param == "all" then
|
||||
core.show_formspec(name, "__builtin:help_cmds",
|
||||
build_chatcommands_formspec(name))
|
||||
if name ~= admin then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return old_help_func(name, param)
|
||||
end
|
||||
|
783
builtin/common/misc_helpers.lua
Normal file
783
builtin/common/misc_helpers.lua
Normal file
@ -0,0 +1,783 @@
|
||||
-- Minetest: builtin/misc_helpers.lua
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Localize functions to avoid table lookups (better performance).
|
||||
local string_sub, string_find = string.sub, string.find
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function basic_dump(o)
|
||||
local tp = type(o)
|
||||
if tp == "number" then
|
||||
return tostring(o)
|
||||
elseif tp == "string" then
|
||||
return string.format("%q", o)
|
||||
elseif tp == "boolean" then
|
||||
return tostring(o)
|
||||
elseif tp == "nil" then
|
||||
return "nil"
|
||||
-- Uncomment for full function dumping support.
|
||||
-- Not currently enabled because bytecode isn't very human-readable and
|
||||
-- dump's output is intended for humans.
|
||||
--elseif tp == "function" then
|
||||
-- return string.format("loadstring(%q)", string.dump(o))
|
||||
elseif tp == "userdata" then
|
||||
return tostring(o)
|
||||
else
|
||||
return string.format("<%s>", tp)
|
||||
end
|
||||
end
|
||||
|
||||
local keywords = {
|
||||
["and"] = true,
|
||||
["break"] = true,
|
||||
["do"] = true,
|
||||
["else"] = true,
|
||||
["elseif"] = true,
|
||||
["end"] = true,
|
||||
["false"] = true,
|
||||
["for"] = true,
|
||||
["function"] = true,
|
||||
["goto"] = true, -- Lua 5.2
|
||||
["if"] = true,
|
||||
["in"] = true,
|
||||
["local"] = true,
|
||||
["nil"] = true,
|
||||
["not"] = true,
|
||||
["or"] = true,
|
||||
["repeat"] = true,
|
||||
["return"] = true,
|
||||
["then"] = true,
|
||||
["true"] = true,
|
||||
["until"] = true,
|
||||
["while"] = true,
|
||||
}
|
||||
local function is_valid_identifier(str)
|
||||
if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Dumps values in a line-per-value format.
|
||||
-- For example, {test = {"Testing..."}} becomes:
|
||||
-- _["test"] = {}
|
||||
-- _["test"][1] = "Testing..."
|
||||
-- This handles tables as keys and circular references properly.
|
||||
-- It also handles multiple references well, writing the table only once.
|
||||
-- The dumped argument is internal-only.
|
||||
|
||||
function dump2(o, name, dumped)
|
||||
name = name or "_"
|
||||
-- "dumped" is used to keep track of serialized tables to handle
|
||||
-- multiple references and circular tables properly.
|
||||
-- It only contains tables as keys. The value is the name that
|
||||
-- the table has in the dump, eg:
|
||||
-- {x = {"y"}} -> dumped[{"y"}] = '_["x"]'
|
||||
dumped = dumped or {}
|
||||
if type(o) ~= "table" then
|
||||
return string.format("%s = %s\n", name, basic_dump(o))
|
||||
end
|
||||
if dumped[o] then
|
||||
return string.format("%s = %s\n", name, dumped[o])
|
||||
end
|
||||
dumped[o] = name
|
||||
-- This contains a list of strings to be concatenated later (because
|
||||
-- Lua is slow at individual concatenation).
|
||||
local t = {}
|
||||
for k, v in pairs(o) do
|
||||
local keyStr
|
||||
if type(k) == "table" then
|
||||
if dumped[k] then
|
||||
keyStr = dumped[k]
|
||||
else
|
||||
-- Key tables don't have a name, so use one of
|
||||
-- the form _G["table: 0xFFFFFFF"]
|
||||
keyStr = string.format("_G[%q]", tostring(k))
|
||||
-- Dump key table
|
||||
t[#t + 1] = dump2(k, keyStr, dumped)
|
||||
end
|
||||
else
|
||||
keyStr = basic_dump(k)
|
||||
end
|
||||
local vname = string.format("%s[%s]", name, keyStr)
|
||||
t[#t + 1] = dump2(v, vname, dumped)
|
||||
end
|
||||
return string.format("%s = {}\n%s", name, table.concat(t))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- This dumps values in a one-statement format.
|
||||
-- For example, {test = {"Testing..."}} becomes:
|
||||
-- [[{
|
||||
-- test = {
|
||||
-- "Testing..."
|
||||
-- }
|
||||
-- }]]
|
||||
-- This supports tables as keys, but not circular references.
|
||||
-- It performs poorly with multiple references as it writes out the full
|
||||
-- table each time.
|
||||
-- The indent field specifies a indentation string, it defaults to a tab.
|
||||
-- Use the empty string to disable indentation.
|
||||
-- The dumped and level arguments are internal-only.
|
||||
|
||||
function dump(o, indent, nested, level)
|
||||
local t = type(o)
|
||||
if not level and t == "userdata" then
|
||||
-- when userdata (e.g. player) is passed directly, print its metatable:
|
||||
return "userdata metatable: " .. dump(getmetatable(o))
|
||||
end
|
||||
if t ~= "table" then
|
||||
return basic_dump(o)
|
||||
end
|
||||
|
||||
-- Contains table -> true/nil of currently nested tables
|
||||
nested = nested or {}
|
||||
if nested[o] then
|
||||
return "<circular reference>"
|
||||
end
|
||||
nested[o] = true
|
||||
indent = indent or "\t"
|
||||
level = level or 1
|
||||
|
||||
local ret = {}
|
||||
local dumped_indexes = {}
|
||||
for i, v in ipairs(o) do
|
||||
ret[#ret + 1] = dump(v, indent, nested, level + 1)
|
||||
dumped_indexes[i] = true
|
||||
end
|
||||
for k, v in pairs(o) do
|
||||
if not dumped_indexes[k] then
|
||||
if type(k) ~= "string" or not is_valid_identifier(k) then
|
||||
k = "["..dump(k, indent, nested, level + 1).."]"
|
||||
end
|
||||
v = dump(v, indent, nested, level + 1)
|
||||
ret[#ret + 1] = k.." = "..v
|
||||
end
|
||||
end
|
||||
nested[o] = nil
|
||||
if indent ~= "" then
|
||||
local indent_str = "\n"..string.rep(indent, level)
|
||||
local end_indent_str = "\n"..string.rep(indent, level - 1)
|
||||
return string.format("{%s%s%s}",
|
||||
indent_str,
|
||||
table.concat(ret, ","..indent_str),
|
||||
end_indent_str)
|
||||
end
|
||||
return "{"..table.concat(ret, ", ").."}"
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function string.split(str, delim, include_empty, max_splits, sep_is_pattern)
|
||||
delim = delim or ","
|
||||
max_splits = max_splits or -2
|
||||
local items = {}
|
||||
local pos, len = 1, #str
|
||||
local plain = not sep_is_pattern
|
||||
max_splits = max_splits + 1
|
||||
repeat
|
||||
local np, npe = string_find(str, delim, pos, plain)
|
||||
np, npe = (np or (len+1)), (npe or (len+1))
|
||||
if (not np) or (max_splits == 1) then
|
||||
np = len + 1
|
||||
npe = np
|
||||
end
|
||||
local s = string_sub(str, pos, np - 1)
|
||||
if include_empty or (s ~= "") then
|
||||
max_splits = max_splits - 1
|
||||
items[#items + 1] = s
|
||||
end
|
||||
pos = npe + 1
|
||||
until (max_splits == 0) or (pos > (len + 1))
|
||||
return items
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function table.indexof(list, val)
|
||||
for i, v in ipairs(list) do
|
||||
if v == val then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return -1
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function string:trim()
|
||||
return (self:gsub("^%s*(.-)%s*$", "%1"))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function math.hypot(x, y)
|
||||
local t
|
||||
x = math.abs(x)
|
||||
y = math.abs(y)
|
||||
t = math.min(x, y)
|
||||
x = math.max(x, y)
|
||||
if x == 0 then return 0 end
|
||||
t = t / x
|
||||
return x * math.sqrt(1 + t * t)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function math.sign(x, tolerance)
|
||||
tolerance = tolerance or 0
|
||||
if x > tolerance then
|
||||
return 1
|
||||
elseif x < -tolerance then
|
||||
return -1
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function math.factorial(x)
|
||||
assert(x % 1 == 0 and x >= 0, "factorial expects a non-negative integer")
|
||||
if x >= 171 then
|
||||
-- 171! is greater than the biggest double, no need to calculate
|
||||
return math.huge
|
||||
end
|
||||
local v = 1
|
||||
for k = 2, x do
|
||||
v = v * k
|
||||
end
|
||||
return v
|
||||
end
|
||||
|
||||
function core.formspec_escape(text)
|
||||
if text ~= nil then
|
||||
text = string.gsub(text,"\\","\\\\")
|
||||
text = string.gsub(text,"%]","\\]")
|
||||
text = string.gsub(text,"%[","\\[")
|
||||
text = string.gsub(text,";","\\;")
|
||||
text = string.gsub(text,",","\\,")
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
|
||||
function core.wrap_text(text, max_length, as_table)
|
||||
local result = {}
|
||||
local line = {}
|
||||
if #text <= max_length then
|
||||
return as_table and {text} or text
|
||||
end
|
||||
|
||||
for word in text:gmatch('%S+') do
|
||||
local cur_length = #table.concat(line, ' ')
|
||||
if cur_length > 0 and cur_length + #word + 1 >= max_length then
|
||||
-- word wouldn't fit on current line, move to next line
|
||||
table.insert(result, table.concat(line, ' '))
|
||||
line = {}
|
||||
end
|
||||
table.insert(line, word)
|
||||
end
|
||||
|
||||
table.insert(result, table.concat(line, ' '))
|
||||
return as_table and result or table.concat(result, '\n')
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
if INIT == "game" then
|
||||
local dirs1 = {9, 18, 7, 12}
|
||||
local dirs2 = {20, 23, 22, 21}
|
||||
|
||||
function core.rotate_and_place(itemstack, placer, pointed_thing,
|
||||
infinitestacks, orient_flags, prevent_after_place)
|
||||
orient_flags = orient_flags or {}
|
||||
|
||||
local unode = core.get_node_or_nil(pointed_thing.under)
|
||||
if not unode 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
|
||||
return undef.on_rightclick(pointed_thing.under, unode, placer,
|
||||
itemstack, pointed_thing)
|
||||
end
|
||||
local fdir = placer and core.dir_to_facedir(placer:get_look_dir()) or 0
|
||||
|
||||
local above = pointed_thing.above
|
||||
local under = pointed_thing.under
|
||||
local iswall = (above.y == under.y)
|
||||
local isceiling = not iswall and (above.y < under.y)
|
||||
|
||||
if undef and undef.buildable_to then
|
||||
iswall = false
|
||||
end
|
||||
|
||||
if orient_flags.force_floor then
|
||||
iswall = false
|
||||
isceiling = false
|
||||
elseif orient_flags.force_ceiling then
|
||||
iswall = false
|
||||
isceiling = true
|
||||
elseif orient_flags.force_wall then
|
||||
iswall = true
|
||||
isceiling = false
|
||||
elseif orient_flags.invert_wall then
|
||||
iswall = not iswall
|
||||
end
|
||||
|
||||
local param2 = fdir
|
||||
if iswall then
|
||||
param2 = dirs1[fdir + 1]
|
||||
elseif isceiling then
|
||||
if orient_flags.force_facedir then
|
||||
param2 = 20
|
||||
else
|
||||
param2 = dirs2[fdir + 1]
|
||||
end
|
||||
else -- place right side up
|
||||
if orient_flags.force_facedir then
|
||||
param2 = 0
|
||||
end
|
||||
end
|
||||
|
||||
local old_itemstack = ItemStack(itemstack)
|
||||
local new_itemstack = core.item_place_node(itemstack, placer,
|
||||
pointed_thing, param2, prevent_after_place)
|
||||
return infinitestacks and old_itemstack or new_itemstack
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--Wrapper for rotate_and_place() to check for sneak and assume Creative mode
|
||||
--implies infinite stacks when performing a 6d rotation.
|
||||
--------------------------------------------------------------------------------
|
||||
core.rotate_node = function(itemstack, placer, pointed_thing)
|
||||
local name = placer and placer:get_player_name() or ""
|
||||
local invert_wall = placer and placer:get_player_control().sneak or false
|
||||
return core.rotate_and_place(itemstack, placer, pointed_thing,
|
||||
core.is_creative_enabled(name),
|
||||
{invert_wall = invert_wall}, true)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.explode_table_event(evt)
|
||||
if evt ~= nil then
|
||||
local parts = evt:split(":")
|
||||
if #parts == 3 then
|
||||
local t = parts[1]:trim()
|
||||
local r = tonumber(parts[2]:trim())
|
||||
local c = tonumber(parts[3]:trim())
|
||||
if type(r) == "number" and type(c) == "number"
|
||||
and t ~= "INV" then
|
||||
return {type=t, row=r, column=c}
|
||||
end
|
||||
end
|
||||
end
|
||||
return {type="INV", row=0, column=0}
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.explode_textlist_event(evt)
|
||||
if evt ~= nil then
|
||||
local parts = evt:split(":")
|
||||
if #parts == 2 then
|
||||
local t = parts[1]:trim()
|
||||
local r = tonumber(parts[2]:trim())
|
||||
if type(r) == "number" and t ~= "INV" then
|
||||
return {type=t, index=r}
|
||||
end
|
||||
end
|
||||
end
|
||||
return {type="INV", index=0}
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.explode_scrollbar_event(evt)
|
||||
local retval = core.explode_textlist_event(evt)
|
||||
|
||||
retval.value = retval.index
|
||||
retval.index = nil
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.rgba(r, g, b, a)
|
||||
return a and string.format("#%02X%02X%02X%02X", r, g, b, a) or
|
||||
string.format("#%02X%02X%02X", r, g, b)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.pos_to_string(pos, decimal_places)
|
||||
local x = pos.x
|
||||
local y = pos.y
|
||||
local z = pos.z
|
||||
if decimal_places ~= nil then
|
||||
x = string.format("%." .. decimal_places .. "f", x)
|
||||
y = string.format("%." .. decimal_places .. "f", y)
|
||||
z = string.format("%." .. decimal_places .. "f", z)
|
||||
end
|
||||
return "(" .. x .. "," .. y .. "," .. z .. ")"
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.string_to_pos(value)
|
||||
if value == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local p = {}
|
||||
p.x, p.y, p.z = string.match(value, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
|
||||
if p.x and p.y and p.z then
|
||||
p.x = tonumber(p.x)
|
||||
p.y = tonumber(p.y)
|
||||
p.z = tonumber(p.z)
|
||||
return p
|
||||
end
|
||||
p = {}
|
||||
p.x, p.y, p.z = string.match(value, "^%( *([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+) *%)$")
|
||||
if p.x and p.y and p.z then
|
||||
p.x = tonumber(p.x)
|
||||
p.y = tonumber(p.y)
|
||||
p.z = tonumber(p.z)
|
||||
return p
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.string_to_area(value)
|
||||
local p1, p2 = unpack(value:split(") ("))
|
||||
if p1 == nil or p2 == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
p1 = core.string_to_pos(p1 .. ")")
|
||||
p2 = core.string_to_pos("(" .. p2)
|
||||
if p1 == nil or p2 == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
return p1, p2
|
||||
end
|
||||
|
||||
local function test_string_to_area()
|
||||
local p1, p2 = core.string_to_area("(10.0, 5, -2) ( 30.2, 4, -12.53)")
|
||||
assert(p1.x == 10.0 and p1.y == 5 and p1.z == -2)
|
||||
assert(p2.x == 30.2 and p2.y == 4 and p2.z == -12.53)
|
||||
|
||||
p1, p2 = core.string_to_area("(10.0, 5, -2 30.2, 4, -12.53")
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(10.0, 5,) -2 fgdf2, 4, -12.53")
|
||||
assert(p1 == nil and p2 == nil)
|
||||
end
|
||||
|
||||
test_string_to_area()
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function table.copy(t, seen)
|
||||
local n = {}
|
||||
seen = seen or {}
|
||||
seen[t] = n
|
||||
for k, v in pairs(t) do
|
||||
n[(type(k) == "table" and (seen[k] or table.copy(k, seen))) or k] =
|
||||
(type(v) == "table" and (seen[v] or table.copy(v, seen))) or v
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
|
||||
function table.insert_all(t, other)
|
||||
for i=1, #other do
|
||||
t[#t + 1] = other[i]
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
function table.key_value_swap(t)
|
||||
local ti = {}
|
||||
for k,v in pairs(t) do
|
||||
ti[v] = k
|
||||
end
|
||||
return ti
|
||||
end
|
||||
|
||||
|
||||
function table.shuffle(t, from, to, random)
|
||||
from = from or 1
|
||||
to = to or #t
|
||||
random = random or math.random
|
||||
local n = to - from + 1
|
||||
while n > 1 do
|
||||
local r = from + n-1
|
||||
local l = from + random(0, n-1)
|
||||
t[l], t[r] = t[r], t[l]
|
||||
n = n-1
|
||||
end
|
||||
end
|
||||
|
||||
function table.combine(t, other)
|
||||
other = other or {}
|
||||
for k, v in pairs(other) do
|
||||
if type(v) == "table" and type(t[k]) == "table" then
|
||||
table.combine(t[k], v)
|
||||
else
|
||||
t[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- mainmenu only functions
|
||||
--------------------------------------------------------------------------------
|
||||
if INIT == "mainmenu" then
|
||||
function core.get_game(index)
|
||||
local games = core.get_games()
|
||||
|
||||
if index > 0 and index <= #games then
|
||||
return games[index]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
if INIT == "client" or INIT == "mainmenu" then
|
||||
function fgettext_ne(text, ...)
|
||||
text = core.gettext(text)
|
||||
local arg = {n=select('#', ...), ...}
|
||||
if arg.n >= 1 then
|
||||
-- Insert positional parameters ($1, $2, ...)
|
||||
local result = ''
|
||||
local pos = 1
|
||||
while pos <= text:len() do
|
||||
local newpos = text:find('[$]', pos)
|
||||
if newpos == nil then
|
||||
result = result .. text:sub(pos)
|
||||
pos = text:len() + 1
|
||||
else
|
||||
local paramindex =
|
||||
tonumber(text:sub(newpos+1, newpos+1))
|
||||
result = result .. text:sub(pos, newpos-1)
|
||||
.. tostring(arg[paramindex])
|
||||
pos = newpos + 2
|
||||
end
|
||||
end
|
||||
text = result
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
function fgettext(text, ...)
|
||||
return core.formspec_escape(fgettext_ne(text, ...))
|
||||
end
|
||||
end
|
||||
|
||||
local ESCAPE_CHAR = string.char(0x1b)
|
||||
|
||||
function core.get_color_escape_sequence(color)
|
||||
return ESCAPE_CHAR .. "(c@" .. color .. ")"
|
||||
end
|
||||
|
||||
function core.get_background_escape_sequence(color)
|
||||
return ESCAPE_CHAR .. "(b@" .. color .. ")"
|
||||
end
|
||||
|
||||
function core.colorize(color, message)
|
||||
local lines = tostring(message):split("\n", true)
|
||||
local color_code = core.get_color_escape_sequence(color)
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
lines[i] = color_code .. line
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n") .. core.get_color_escape_sequence("#ffffff")
|
||||
end
|
||||
|
||||
local function rgb_to_hex(rgb)
|
||||
local hexadecimal = '#'
|
||||
|
||||
for key, value in pairs(rgb) do
|
||||
local hex = ''
|
||||
|
||||
while(value > 0)do
|
||||
local index = math.fmod(value, 16) + 1
|
||||
value = math.floor(value / 16)
|
||||
hex = string.sub('0123456789ABCDEF', index, index) .. hex
|
||||
end
|
||||
|
||||
if(string.len(hex) == 0)then
|
||||
hex = '00'
|
||||
|
||||
elseif(string.len(hex) == 1)then
|
||||
hex = '0' .. hex
|
||||
end
|
||||
|
||||
hexadecimal = hexadecimal .. hex
|
||||
end
|
||||
|
||||
return hexadecimal
|
||||
end
|
||||
|
||||
local function color_from_hue(hue)
|
||||
local h = hue / 60
|
||||
local c = 255
|
||||
local x = (1 - math.abs(h%2 - 1)) * 255
|
||||
|
||||
local i = math.floor(h);
|
||||
if (i == 0) then
|
||||
return rgb_to_hex({c, x, 0})
|
||||
elseif (i == 1) then
|
||||
return rgb_to_hex({x, c, 0})
|
||||
elseif (i == 2) then
|
||||
return rgb_to_hex({0, c, x})
|
||||
elseif (i == 3) then
|
||||
return rgb_to_hex({0, x, c});
|
||||
elseif (i == 4) then
|
||||
return rgb_to_hex({x, 0, c});
|
||||
else
|
||||
return rgb_to_hex({c, 0, x});
|
||||
end
|
||||
end
|
||||
|
||||
function core.rainbow(input)
|
||||
local step = 360 / input:len()
|
||||
local hue = 0
|
||||
local output = ""
|
||||
for i = 1, input:len() do
|
||||
local char = input:sub(i,i)
|
||||
if char:match("%s") then
|
||||
output = output .. char
|
||||
else
|
||||
output = output .. core.get_color_escape_sequence(color_from_hue(hue)) .. char
|
||||
end
|
||||
hue = hue + step
|
||||
end
|
||||
return output
|
||||
end
|
||||
|
||||
function core.strip_foreground_colors(str)
|
||||
return (str:gsub(ESCAPE_CHAR .. "%(c@[^)]+%)", ""))
|
||||
end
|
||||
|
||||
function core.strip_background_colors(str)
|
||||
return (str:gsub(ESCAPE_CHAR .. "%(b@[^)]+%)", ""))
|
||||
end
|
||||
|
||||
function core.strip_colors(str)
|
||||
return (str:gsub(ESCAPE_CHAR .. "%([bc]@[^)]+%)", ""))
|
||||
end
|
||||
|
||||
function core.translate(textdomain, str, ...)
|
||||
local start_seq
|
||||
if textdomain == "" then
|
||||
start_seq = ESCAPE_CHAR .. "T"
|
||||
else
|
||||
start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. ")"
|
||||
end
|
||||
local arg = {n=select('#', ...), ...}
|
||||
local end_seq = ESCAPE_CHAR .. "E"
|
||||
local arg_index = 1
|
||||
local translated = str:gsub("@(.)", function(matched)
|
||||
local c = string.byte(matched)
|
||||
if string.byte("1") <= c and c <= string.byte("9") then
|
||||
local a = c - string.byte("0")
|
||||
if a ~= arg_index then
|
||||
error("Escape sequences in string given to core.translate " ..
|
||||
"are not in the correct order: got @" .. matched ..
|
||||
"but expected @" .. tostring(arg_index))
|
||||
end
|
||||
if a > arg.n then
|
||||
error("Not enough arguments provided to core.translate")
|
||||
end
|
||||
arg_index = arg_index + 1
|
||||
return ESCAPE_CHAR .. "F" .. arg[a] .. ESCAPE_CHAR .. "E"
|
||||
elseif matched == "n" then
|
||||
return "\n"
|
||||
else
|
||||
return matched
|
||||
end
|
||||
end)
|
||||
if arg_index < arg.n + 1 then
|
||||
error("Too many arguments provided to core.translate")
|
||||
end
|
||||
return start_seq .. translated .. end_seq
|
||||
end
|
||||
|
||||
function core.get_translator(textdomain)
|
||||
return function(str, ...) return core.translate(textdomain or "", str, ...) end
|
||||
end
|
||||
|
||||
function core.get_pointed_thing_position(pointed_thing, above)
|
||||
if pointed_thing.type == "node" then
|
||||
if above then
|
||||
-- The position where a node would be placed
|
||||
return pointed_thing.above
|
||||
end
|
||||
-- The position where a node would be dug
|
||||
return pointed_thing.under
|
||||
elseif pointed_thing.type == "object" then
|
||||
return pointed_thing.ref and pointed_thing.ref:get_pos()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Returns the exact coordinate of a pointed surface
|
||||
--------------------------------------------------------------------------------
|
||||
function core.pointed_thing_to_face_pos(placer, pointed_thing)
|
||||
-- Avoid crash in some situations when player is inside a node, causing
|
||||
-- 'above' to equal 'under'.
|
||||
if vector.equals(pointed_thing.above, pointed_thing.under) then
|
||||
return pointed_thing.under
|
||||
end
|
||||
|
||||
local eye_height = placer:get_properties().eye_height
|
||||
local eye_offset_first = placer:get_eye_offset()
|
||||
local node_pos = pointed_thing.under
|
||||
local camera_pos = placer:get_pos()
|
||||
local pos_off = vector.multiply(
|
||||
vector.subtract(pointed_thing.above, node_pos), 0.5)
|
||||
local look_dir = placer:get_look_dir()
|
||||
local offset, nc
|
||||
local oc = {}
|
||||
|
||||
for c, v in pairs(pos_off) do
|
||||
if nc or v == 0 then
|
||||
oc[#oc + 1] = c
|
||||
else
|
||||
offset = v
|
||||
nc = c
|
||||
end
|
||||
end
|
||||
|
||||
local fine_pos = {[nc] = node_pos[nc] + offset}
|
||||
camera_pos.y = camera_pos.y + eye_height + eye_offset_first.y / 10
|
||||
local f = (node_pos[nc] + offset - camera_pos[nc]) / look_dir[nc]
|
||||
|
||||
for i = 1, #oc do
|
||||
fine_pos[oc[i]] = camera_pos[oc[i]] + look_dir[oc[i]] * f
|
||||
end
|
||||
return fine_pos
|
||||
end
|
||||
|
||||
function core.string_to_privs(str, delim)
|
||||
assert(type(str) == "string")
|
||||
delim = delim or ','
|
||||
local privs = {}
|
||||
for _, priv in pairs(string.split(str, delim)) do
|
||||
privs[priv:trim()] = true
|
||||
end
|
||||
return privs
|
||||
end
|
||||
|
||||
function core.privs_to_string(privs, delim)
|
||||
assert(type(privs) == "table")
|
||||
delim = delim or ','
|
||||
local list = {}
|
||||
for priv, bool in pairs(privs) do
|
||||
if bool then
|
||||
list[#list + 1] = priv
|
||||
end
|
||||
end
|
||||
return table.concat(list, delim)
|
||||
end
|
205
builtin/common/serialize.lua
Normal file
205
builtin/common/serialize.lua
Normal file
@ -0,0 +1,205 @@
|
||||
--- Lua module to serialize values as Lua code.
|
||||
-- From: https://github.com/fab13n/metalua/blob/no-dll/src/lib/serialize.lua
|
||||
-- License: MIT
|
||||
-- @copyright 2006-2997 Fabien Fleutot <metalua@gmail.com>
|
||||
-- @author Fabien Fleutot <metalua@gmail.com>
|
||||
-- @author ShadowNinja <shadowninja@minetest.net>
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--- Serialize an object into a source code string. This string, when passed as
|
||||
-- an argument to deserialize(), returns an object structurally identical to
|
||||
-- the original one. The following are currently supported:
|
||||
-- * Booleans, numbers, strings, and nil.
|
||||
-- * Functions; uses interpreter-dependent (and sometimes platform-dependent) bytecode!
|
||||
-- * Tables; they can cantain multiple references and can be recursive, but metatables aren't saved.
|
||||
-- This works in two phases:
|
||||
-- 1. Recursively find and record multiple references and recursion.
|
||||
-- 2. Recursively dump the value into a string.
|
||||
-- @param x Value to serialize (nil is allowed).
|
||||
-- @return load()able string containing the value.
|
||||
function core.serialize(x)
|
||||
local local_index = 1 -- Top index of the "_" local table in the dump
|
||||
-- table->nil/1/2 set of tables seen.
|
||||
-- nil = not seen, 1 = seen once, 2 = seen multiple times.
|
||||
local seen = {}
|
||||
|
||||
-- nest_points are places where a table appears within itself, directly
|
||||
-- or not. For instance, all of these chunks create nest points in
|
||||
-- table x: "x = {}; x[x] = 1", "x = {}; x[1] = x",
|
||||
-- "x = {}; x[1] = {y = {x}}".
|
||||
-- To handle those, two tables are used by mark_nest_point:
|
||||
-- * nested - Transient set of tables being currently traversed.
|
||||
-- Used for detecting nested tables.
|
||||
-- * nest_points - parent->{key=value, ...} table cantaining the nested
|
||||
-- keys and values in the parent. They're all dumped after all the
|
||||
-- other table operations have been performed.
|
||||
--
|
||||
-- mark_nest_point(p, k, v) fills nest_points with information required
|
||||
-- to remember that key/value (k, v) creates a nest point in table
|
||||
-- parent. It also marks "parent" and the nested item(s) as occuring
|
||||
-- multiple times, since several references to it will be required in
|
||||
-- order to patch the nest points.
|
||||
local nest_points = {}
|
||||
local nested = {}
|
||||
local function mark_nest_point(parent, k, v)
|
||||
local nk, nv = nested[k], nested[v]
|
||||
local np = nest_points[parent]
|
||||
if not np then
|
||||
np = {}
|
||||
nest_points[parent] = np
|
||||
end
|
||||
np[k] = v
|
||||
seen[parent] = 2
|
||||
if nk then seen[k] = 2 end
|
||||
if nv then seen[v] = 2 end
|
||||
end
|
||||
|
||||
-- First phase, list the tables and functions which appear more than
|
||||
-- once in x.
|
||||
local function mark_multiple_occurences(x)
|
||||
local tp = type(x)
|
||||
if tp ~= "table" and tp ~= "function" then
|
||||
-- No identity (comparison is done by value, not by instance)
|
||||
return
|
||||
end
|
||||
if seen[x] == 1 then
|
||||
seen[x] = 2
|
||||
elseif seen[x] ~= 2 then
|
||||
seen[x] = 1
|
||||
end
|
||||
|
||||
if tp == "table" then
|
||||
nested[x] = true
|
||||
for k, v in pairs(x) do
|
||||
if nested[k] or nested[v] then
|
||||
mark_nest_point(x, k, v)
|
||||
else
|
||||
mark_multiple_occurences(k)
|
||||
mark_multiple_occurences(v)
|
||||
end
|
||||
end
|
||||
nested[x] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local dumped = {} -- object->varname set
|
||||
local local_defs = {} -- Dumped local definitions as source code lines
|
||||
|
||||
-- Mutually recursive local functions:
|
||||
local dump_val, dump_or_ref_val
|
||||
|
||||
-- If x occurs multiple times, dump the local variable rather than
|
||||
-- the value. If it's the first time it's dumped, also dump the
|
||||
-- content in local_defs.
|
||||
function dump_or_ref_val(x)
|
||||
if seen[x] ~= 2 then
|
||||
return dump_val(x)
|
||||
end
|
||||
local var = dumped[x]
|
||||
if var then -- Already referenced
|
||||
return var
|
||||
end
|
||||
-- First occurence, create and register reference
|
||||
local val = dump_val(x)
|
||||
local i = local_index
|
||||
local_index = local_index + 1
|
||||
var = "_["..i.."]"
|
||||
local_defs[#local_defs + 1] = var.." = "..val
|
||||
dumped[x] = var
|
||||
return var
|
||||
end
|
||||
|
||||
-- Second phase. Dump the object; subparts occuring multiple times
|
||||
-- are dumped in local variables which can be referenced multiple
|
||||
-- times. Care is taken to dump local vars in a sensible order.
|
||||
function dump_val(x)
|
||||
local tp = type(x)
|
||||
if x == nil 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
|
||||
return string.format("loadstring(%q)", string.dump(x))
|
||||
elseif tp == "number" then
|
||||
-- Serialize numbers reversibly with string.format
|
||||
return string.format("%.17g", x)
|
||||
elseif tp == "table" then
|
||||
local vals = {}
|
||||
local idx_dumped = {}
|
||||
local np = nest_points[x]
|
||||
for i, v in ipairs(x) do
|
||||
if not np or not np[i] then
|
||||
vals[#vals + 1] = dump_or_ref_val(v)
|
||||
end
|
||||
idx_dumped[i] = true
|
||||
end
|
||||
for k, v in pairs(x) do
|
||||
if (not np or not np[k]) and
|
||||
not idx_dumped[k] then
|
||||
vals[#vals + 1] = "["..dump_or_ref_val(k).."] = "
|
||||
..dump_or_ref_val(v)
|
||||
end
|
||||
end
|
||||
return "{"..table.concat(vals, ", ").."}"
|
||||
else
|
||||
error("Can't serialize data of type "..tp)
|
||||
end
|
||||
end
|
||||
|
||||
local function dump_nest_points()
|
||||
for parent, vals in pairs(nest_points) do
|
||||
for k, v in pairs(vals) do
|
||||
local_defs[#local_defs + 1] = dump_or_ref_val(parent)
|
||||
.."["..dump_or_ref_val(k).."] = "
|
||||
..dump_or_ref_val(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
mark_multiple_occurences(x)
|
||||
local top_level = dump_or_ref_val(x)
|
||||
dump_nest_points()
|
||||
|
||||
if next(local_defs) then
|
||||
return "local _ = {}\n"
|
||||
..table.concat(local_defs, "\n")
|
||||
.."\nreturn "..top_level
|
||||
else
|
||||
return "return "..top_level
|
||||
end
|
||||
end
|
||||
|
||||
-- Deserialization
|
||||
|
||||
local function safe_loadstring(...)
|
||||
local func, err = loadstring(...)
|
||||
if func then
|
||||
setfenv(func, {})
|
||||
return func
|
||||
end
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local function dummy_func() end
|
||||
|
||||
function core.deserialize(str, safe)
|
||||
if type(str) ~= "string" then
|
||||
return nil, "Cannot deserialize type '"..type(str)
|
||||
.."'. Argument must be a string."
|
||||
end
|
||||
if str:byte(1) == 0x1B then
|
||||
return nil, "Bytecode prohibited"
|
||||
end
|
||||
local f, err = loadstring(str)
|
||||
if not f then return nil, err end
|
||||
|
||||
-- The environment is recreated every time so deseralized code cannot
|
||||
-- pollute it with permanent references.
|
||||
setfenv(f, {loadstring = safe and dummy_func or safe_loadstring})
|
||||
|
||||
local good, data = pcall(f)
|
||||
if good then
|
||||
return data
|
||||
else
|
||||
return nil, data
|
||||
end
|
||||
end
|
57
builtin/common/strict.lua
Normal file
57
builtin/common/strict.lua
Normal file
@ -0,0 +1,57 @@
|
||||
|
||||
-- Always warn when creating a global variable, even outside of a function.
|
||||
-- This ignores mod namespaces (variables with the same name as the current mod).
|
||||
local WARN_INIT = false
|
||||
|
||||
local getinfo = debug.getinfo
|
||||
|
||||
function core.global_exists(name)
|
||||
if type(name) ~= "string" then
|
||||
error("core.global_exists: " .. tostring(name) .. " is not a string")
|
||||
end
|
||||
return rawget(_G, name) ~= nil
|
||||
end
|
||||
|
||||
|
||||
local meta = {}
|
||||
local declared = {}
|
||||
-- Key is source file, line, and variable name; seperated by NULs
|
||||
local warned = {}
|
||||
|
||||
function meta:__newindex(name, value)
|
||||
local info = getinfo(2, "Sl")
|
||||
local desc = ("%s:%d"):format(info.short_src, info.currentline)
|
||||
if not declared[name] then
|
||||
local warn_key = ("%s\0%d\0%s"):format(info.source,
|
||||
info.currentline, name)
|
||||
if not warned[warn_key] and info.what ~= "main" and
|
||||
info.what ~= "C" then
|
||||
core.log("warning", ("Assignment to undeclared "..
|
||||
"global %q inside a function at %s.")
|
||||
:format(name, desc))
|
||||
warned[warn_key] = true
|
||||
end
|
||||
declared[name] = true
|
||||
end
|
||||
-- Ignore mod namespaces
|
||||
if WARN_INIT and name ~= core.get_current_modname() then
|
||||
core.log("warning", ("Global variable %q created at %s.")
|
||||
:format(name, desc))
|
||||
end
|
||||
rawset(self, name, value)
|
||||
end
|
||||
|
||||
|
||||
function meta:__index(name)
|
||||
local info = getinfo(2, "Sl")
|
||||
local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name)
|
||||
if not declared[name] and not warned[warn_key] and info.what ~= "C" then
|
||||
core.log("warning", ("Undeclared global variable %q accessed at %s:%s")
|
||||
:format(name, info.short_src, info.currentline))
|
||||
warned[warn_key] = true
|
||||
end
|
||||
return rawget(self, name)
|
||||
end
|
||||
|
||||
setmetatable(_G, meta)
|
||||
|
73
builtin/common/tests/misc_helpers_spec.lua
Normal file
73
builtin/common/tests/misc_helpers_spec.lua
Normal file
@ -0,0 +1,73 @@
|
||||
_G.core = {}
|
||||
dofile("builtin/common/misc_helpers.lua")
|
||||
|
||||
describe("string", function()
|
||||
it("trim()", function()
|
||||
assert.equal("foo bar", string.trim("\n \t\tfoo bar\t "))
|
||||
end)
|
||||
|
||||
describe("split()", function()
|
||||
it("removes empty", function()
|
||||
assert.same({ "hello" }, string.split("hello"))
|
||||
assert.same({ "hello", "world" }, string.split("hello,world"))
|
||||
assert.same({ "hello", "world" }, string.split("hello,world,,,"))
|
||||
assert.same({ "hello", "world" }, string.split(",,,hello,world"))
|
||||
assert.same({ "hello", "world", "2" }, string.split("hello,,,world,2"))
|
||||
assert.same({ "hello ", " world" }, string.split("hello :| world", ":|"))
|
||||
end)
|
||||
|
||||
it("keeps empty", function()
|
||||
assert.same({ "hello" }, string.split("hello", ",", true))
|
||||
assert.same({ "hello", "world" }, string.split("hello,world", ",", true))
|
||||
assert.same({ "hello", "world", "" }, string.split("hello,world,", ",", true))
|
||||
assert.same({ "hello", "", "", "world", "2" }, string.split("hello,,,world,2", ",", true))
|
||||
assert.same({ "", "", "hello", "world", "2" }, string.split(",,hello,world,2", ",", true))
|
||||
assert.same({ "hello ", " world | :" }, string.split("hello :| world | :", ":|"))
|
||||
end)
|
||||
|
||||
it("max_splits", function()
|
||||
assert.same({ "one" }, string.split("one", ",", true, 2))
|
||||
assert.same({ "one,two,three,four" }, string.split("one,two,three,four", ",", true, 0))
|
||||
assert.same({ "one", "two", "three,four" }, string.split("one,two,three,four", ",", true, 2))
|
||||
assert.same({ "one", "", "two,three,four" }, string.split("one,,two,three,four", ",", true, 2))
|
||||
assert.same({ "one", "two", "three,four" }, string.split("one,,,,,,two,three,four", ",", false, 2))
|
||||
end)
|
||||
|
||||
it("pattern", function()
|
||||
assert.same({ "one", "two" }, string.split("one,two", ",", false, -1, true))
|
||||
assert.same({ "one", "two", "three" }, string.split("one2two3three", "%d", false, -1, true))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("privs", function()
|
||||
it("from string", function()
|
||||
assert.same({ a = true, b = true }, core.string_to_privs("a,b"))
|
||||
end)
|
||||
|
||||
it("to string", function()
|
||||
assert.equal("one", core.privs_to_string({ one=true }))
|
||||
|
||||
local ret = core.privs_to_string({ a=true, b=true })
|
||||
assert(ret == "a,b" or ret == "b,a")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("pos", function()
|
||||
it("from string", function()
|
||||
assert.same({ x = 10, y = 5.1, z = -2}, core.string_to_pos("10.0, 5.1, -2"))
|
||||
assert.same({ x = 10, y = 5.1, z = -2}, core.string_to_pos("( 10.0, 5.1, -2)"))
|
||||
assert.is_nil(core.string_to_pos("asd, 5, -2)"))
|
||||
end)
|
||||
|
||||
it("to string", function()
|
||||
assert.equal("(10.1,5.2,-2.3)", core.pos_to_string({ x = 10.1, y = 5.2, z = -2.3}))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("table", function()
|
||||
it("indexof()", function()
|
||||
assert.equal(1, table.indexof({"foo", "bar"}, "foo"))
|
||||
assert.equal(-1, table.indexof({"foo", "bar"}, "baz"))
|
||||
end)
|
||||
end)
|
56
builtin/common/tests/serialize_spec.lua
Normal file
56
builtin/common/tests/serialize_spec.lua
Normal file
@ -0,0 +1,56 @@
|
||||
_G.core = {}
|
||||
|
||||
_G.setfenv = require 'busted.compatibility'.setfenv
|
||||
|
||||
dofile("builtin/common/serialize.lua")
|
||||
|
||||
describe("serialize", function()
|
||||
it("works", function()
|
||||
local test_in = {cat={sound="nyan", speed=400}, dog={sound="woof"}}
|
||||
local test_out = core.deserialize(core.serialize(test_in))
|
||||
|
||||
assert.same(test_in, test_out)
|
||||
end)
|
||||
|
||||
it("handles characters", function()
|
||||
local test_in = {escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"}
|
||||
local test_out = core.deserialize(core.serialize(test_in))
|
||||
assert.same(test_in, test_out)
|
||||
end)
|
||||
|
||||
it("handles precise numbers", function()
|
||||
local test_in = 0.2695949158945771
|
||||
local test_out = core.deserialize(core.serialize(test_in))
|
||||
assert.same(test_in, test_out)
|
||||
end)
|
||||
|
||||
it("handles big integers", function()
|
||||
local test_in = 269594915894577
|
||||
local test_out = core.deserialize(core.serialize(test_in))
|
||||
assert.same(test_in, test_out)
|
||||
end)
|
||||
|
||||
it("handles recursive structures", function()
|
||||
local test_in = { hello = "world" }
|
||||
test_in.foo = test_in
|
||||
|
||||
local test_out = core.deserialize(core.serialize(test_in))
|
||||
assert.same(test_in, test_out)
|
||||
end)
|
||||
|
||||
it("strips functions in safe mode", function()
|
||||
local test_in = {
|
||||
func = function(a, b)
|
||||
error("test")
|
||||
end,
|
||||
foo = "bar"
|
||||
}
|
||||
|
||||
local str = core.serialize(test_in)
|
||||
assert.not_nil(str:find("loadstring"))
|
||||
|
||||
local test_out = core.deserialize(str, true)
|
||||
assert.is_nil(test_out.func)
|
||||
assert.equals(test_out.foo, "bar")
|
||||
end)
|
||||
end)
|
192
builtin/common/tests/vector_spec.lua
Normal file
192
builtin/common/tests/vector_spec.lua
Normal file
@ -0,0 +1,192 @@
|
||||
_G.vector = {}
|
||||
dofile("builtin/common/vector.lua")
|
||||
|
||||
describe("vector", function()
|
||||
describe("new()", function()
|
||||
it("constructs", function()
|
||||
assert.same({ x = 0, y = 0, z = 0 }, vector.new())
|
||||
assert.same({ x = 1, y = 2, z = 3 }, vector.new(1, 2, 3))
|
||||
assert.same({ x = 3, y = 2, z = 1 }, vector.new({ x = 3, y = 2, z = 1 }))
|
||||
|
||||
local input = vector.new({ x = 3, y = 2, z = 1 })
|
||||
local output = vector.new(input)
|
||||
assert.same(input, output)
|
||||
assert.are_not.equal(input, output)
|
||||
end)
|
||||
|
||||
it("throws on invalid input", function()
|
||||
assert.has.errors(function()
|
||||
vector.new({ x = 3 })
|
||||
end)
|
||||
|
||||
assert.has.errors(function()
|
||||
vector.new({ d = 3 })
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
it("equal()", function()
|
||||
local function assertE(a, b)
|
||||
assert.is_true(vector.equals(a, b))
|
||||
end
|
||||
local function assertNE(a, b)
|
||||
assert.is_false(vector.equals(a, b))
|
||||
end
|
||||
|
||||
assertE({x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0})
|
||||
assertE({x = -1, y = 0, z = 1}, {x = -1, y = 0, z = 1})
|
||||
local a = { x = 2, y = 4, z = -10 }
|
||||
assertE(a, a)
|
||||
assertNE({x = -1, y = 0, z = 1}, a)
|
||||
end)
|
||||
|
||||
it("add()", function()
|
||||
assert.same({ x = 2, y = 4, z = 6 }, vector.add(vector.new(1, 2, 3), { x = 1, y = 2, z = 3 }))
|
||||
end)
|
||||
|
||||
it("offset()", function()
|
||||
assert.same({ x = 41, y = 52, z = 63 }, vector.offset(vector.new(1, 2, 3), 40, 50, 60))
|
||||
end)
|
||||
|
||||
-- This function is needed because of floating point imprecision.
|
||||
local function almost_equal(a, b)
|
||||
if type(a) == "number" then
|
||||
return math.abs(a - b) < 0.00000000001
|
||||
end
|
||||
return vector.distance(a, b) < 0.000000000001
|
||||
end
|
||||
|
||||
describe("rotate_around_axis()", function()
|
||||
it("rotates", function()
|
||||
assert.True(almost_equal({x = -1, y = 0, z = 0},
|
||||
vector.rotate_around_axis({x = 1, y = 0, z = 0}, {x = 0, y = 1, z = 0}, math.pi)))
|
||||
assert.True(almost_equal({x = 0, y = 1, z = 0},
|
||||
vector.rotate_around_axis({x = 0, y = 0, z = 1}, {x = 1, y = 0, z = 0}, math.pi / 2)))
|
||||
assert.True(almost_equal({x = 4, y = 1, z = 1},
|
||||
vector.rotate_around_axis({x = 4, y = 1, z = 1}, {x = 4, y = 1, z = 1}, math.pi / 6)))
|
||||
end)
|
||||
it("keeps distance to axis", function()
|
||||
local rotate1 = {x = 1, y = 3, z = 1}
|
||||
local axis1 = {x = 1, y = 3, z = 2}
|
||||
local rotated1 = vector.rotate_around_axis(rotate1, axis1, math.pi / 13)
|
||||
assert.True(almost_equal(vector.distance(axis1, rotate1), vector.distance(axis1, rotated1)))
|
||||
local rotate2 = {x = 1, y = 1, z = 3}
|
||||
local axis2 = {x = 2, y = 6, z = 100}
|
||||
local rotated2 = vector.rotate_around_axis(rotate2, axis2, math.pi / 23)
|
||||
assert.True(almost_equal(vector.distance(axis2, rotate2), vector.distance(axis2, rotated2)))
|
||||
local rotate3 = {x = 1, y = -1, z = 3}
|
||||
local axis3 = {x = 2, y = 6, z = 100}
|
||||
local rotated3 = vector.rotate_around_axis(rotate3, axis3, math.pi / 2)
|
||||
assert.True(almost_equal(vector.distance(axis3, rotate3), vector.distance(axis3, rotated3)))
|
||||
end)
|
||||
it("rotates back", function()
|
||||
local rotate1 = {x = 1, y = 3, z = 1}
|
||||
local axis1 = {x = 1, y = 3, z = 2}
|
||||
local rotated1 = vector.rotate_around_axis(rotate1, axis1, math.pi / 13)
|
||||
rotated1 = vector.rotate_around_axis(rotated1, axis1, -math.pi / 13)
|
||||
assert.True(almost_equal(rotate1, rotated1))
|
||||
local rotate2 = {x = 1, y = 1, z = 3}
|
||||
local axis2 = {x = 2, y = 6, z = 100}
|
||||
local rotated2 = vector.rotate_around_axis(rotate2, axis2, math.pi / 23)
|
||||
rotated2 = vector.rotate_around_axis(rotated2, axis2, -math.pi / 23)
|
||||
assert.True(almost_equal(rotate2, rotated2))
|
||||
local rotate3 = {x = 1, y = -1, z = 3}
|
||||
local axis3 = {x = 2, y = 6, z = 100}
|
||||
local rotated3 = vector.rotate_around_axis(rotate3, axis3, math.pi / 2)
|
||||
rotated3 = vector.rotate_around_axis(rotated3, axis3, -math.pi / 2)
|
||||
assert.True(almost_equal(rotate3, rotated3))
|
||||
end)
|
||||
it("is right handed", function()
|
||||
local v_before1 = {x = 0, y = 1, z = -1}
|
||||
local v_after1 = vector.rotate_around_axis(v_before1, {x = 1, y = 0, z = 0}, math.pi / 4)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after1, v_before1)), {x = 1, y = 0, z = 0}))
|
||||
|
||||
local v_before2 = {x = 0, y = 3, z = 4}
|
||||
local v_after2 = vector.rotate_around_axis(v_before2, {x = 1, y = 0, z = 0}, 2 * math.pi / 5)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after2, v_before2)), {x = 1, y = 0, z = 0}))
|
||||
|
||||
local v_before3 = {x = 1, y = 0, z = -1}
|
||||
local v_after3 = vector.rotate_around_axis(v_before3, {x = 0, y = 1, z = 0}, math.pi / 4)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after3, v_before3)), {x = 0, y = 1, z = 0}))
|
||||
|
||||
local v_before4 = {x = 3, y = 0, z = 4}
|
||||
local v_after4 = vector.rotate_around_axis(v_before4, {x = 0, y = 1, z = 0}, 2 * math.pi / 5)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after4, v_before4)), {x = 0, y = 1, z = 0}))
|
||||
|
||||
local v_before5 = {x = 1, y = -1, z = 0}
|
||||
local v_after5 = vector.rotate_around_axis(v_before5, {x = 0, y = 0, z = 1}, math.pi / 4)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after5, v_before5)), {x = 0, y = 0, z = 1}))
|
||||
|
||||
local v_before6 = {x = 3, y = 4, z = 0}
|
||||
local v_after6 = vector.rotate_around_axis(v_before6, {x = 0, y = 0, z = 1}, 2 * math.pi / 5)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after6, v_before6)), {x = 0, y = 0, z = 1}))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("rotate()", function()
|
||||
it("rotates", function()
|
||||
assert.True(almost_equal({x = -1, y = 0, z = 0},
|
||||
vector.rotate({x = 1, y = 0, z = 0}, {x = 0, y = math.pi, z = 0})))
|
||||
assert.True(almost_equal({x = 0, y = -1, z = 0},
|
||||
vector.rotate({x = 1, y = 0, z = 0}, {x = 0, y = 0, z = math.pi / 2})))
|
||||
assert.True(almost_equal({x = 1, y = 0, z = 0},
|
||||
vector.rotate({x = 1, y = 0, z = 0}, {x = math.pi / 123, y = 0, z = 0})))
|
||||
end)
|
||||
it("is counterclockwise", function()
|
||||
local v_before1 = {x = 0, y = 1, z = -1}
|
||||
local v_after1 = vector.rotate(v_before1, {x = math.pi / 4, y = 0, z = 0})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after1, v_before1)), {x = 1, y = 0, z = 0}))
|
||||
|
||||
local v_before2 = {x = 0, y = 3, z = 4}
|
||||
local v_after2 = vector.rotate(v_before2, {x = 2 * math.pi / 5, y = 0, z = 0})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after2, v_before2)), {x = 1, y = 0, z = 0}))
|
||||
|
||||
local v_before3 = {x = 1, y = 0, z = -1}
|
||||
local v_after3 = vector.rotate(v_before3, {x = 0, y = math.pi / 4, z = 0})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after3, v_before3)), {x = 0, y = 1, z = 0}))
|
||||
|
||||
local v_before4 = {x = 3, y = 0, z = 4}
|
||||
local v_after4 = vector.rotate(v_before4, {x = 0, y = 2 * math.pi / 5, z = 0})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after4, v_before4)), {x = 0, y = 1, z = 0}))
|
||||
|
||||
local v_before5 = {x = 1, y = -1, z = 0}
|
||||
local v_after5 = vector.rotate(v_before5, {x = 0, y = 0, z = math.pi / 4})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after5, v_before5)), {x = 0, y = 0, z = 1}))
|
||||
|
||||
local v_before6 = {x = 3, y = 4, z = 0}
|
||||
local v_after6 = vector.rotate(v_before6, {x = 0, y = 0, z = 2 * math.pi / 5})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after6, v_before6)), {x = 0, y = 0, z = 1}))
|
||||
end)
|
||||
end)
|
||||
|
||||
it("dir_to_rotation()", function()
|
||||
-- Comparing rotations (pitch, yaw, roll) is hard because of certain ambiguities,
|
||||
-- e.g. (pi, 0, pi) looks exactly the same as (0, pi, 0)
|
||||
-- So instead we convert the rotation back to vectors and compare these.
|
||||
local function forward_at_rot(rot)
|
||||
return vector.rotate(vector.new(0, 0, 1), rot)
|
||||
end
|
||||
local function up_at_rot(rot)
|
||||
return vector.rotate(vector.new(0, 1, 0), rot)
|
||||
end
|
||||
local rot1 = vector.dir_to_rotation({x = 1, y = 0, z = 0}, {x = 0, y = 1, z = 0})
|
||||
assert.True(almost_equal({x = 1, y = 0, z = 0}, forward_at_rot(rot1)))
|
||||
assert.True(almost_equal({x = 0, y = 1, z = 0}, up_at_rot(rot1)))
|
||||
local rot2 = vector.dir_to_rotation({x = 1, y = 1, z = 0}, {x = 0, y = 0, z = 1})
|
||||
assert.True(almost_equal({x = 1/math.sqrt(2), y = 1/math.sqrt(2), z = 0}, forward_at_rot(rot2)))
|
||||
assert.True(almost_equal({x = 0, y = 0, z = 1}, up_at_rot(rot2)))
|
||||
for i = 1, 1000 do
|
||||
local rand_vec = vector.new(math.random(), math.random(), math.random())
|
||||
if vector.length(rand_vec) ~= 0 then
|
||||
local rot_1 = vector.dir_to_rotation(rand_vec)
|
||||
local rot_2 = {
|
||||
x = math.atan2(rand_vec.y, math.sqrt(rand_vec.z * rand_vec.z + rand_vec.x * rand_vec.x)),
|
||||
y = -math.atan2(rand_vec.x, rand_vec.z),
|
||||
z = 0
|
||||
}
|
||||
assert.True(almost_equal(rot_1, rot_2))
|
||||
end
|
||||
end
|
||||
|
||||
end)
|
||||
end)
|
242
builtin/common/vector.lua
Normal file
242
builtin/common/vector.lua
Normal file
@ -0,0 +1,242 @@
|
||||
|
||||
vector = {}
|
||||
|
||||
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()")
|
||||
return {x=a.x, y=a.y, z=a.z}
|
||||
elseif a then
|
||||
assert(b and c, "Invalid arguments for vector.new()")
|
||||
return {x=a, y=b, z=c}
|
||||
end
|
||||
return {x=0, y=0, z=0}
|
||||
end
|
||||
|
||||
function vector.equals(a, b)
|
||||
return a.x == b.x and
|
||||
a.y == b.y and
|
||||
a.z == b.z
|
||||
end
|
||||
|
||||
function vector.length(v)
|
||||
return math.hypot(v.x, math.hypot(v.y, v.z))
|
||||
end
|
||||
|
||||
function vector.normalize(v)
|
||||
local len = vector.length(v)
|
||||
if len == 0 then
|
||||
return {x=0, y=0, z=0}
|
||||
else
|
||||
return vector.divide(v, len)
|
||||
end
|
||||
end
|
||||
|
||||
function vector.floor(v)
|
||||
return {
|
||||
x = math.floor(v.x),
|
||||
y = math.floor(v.y),
|
||||
z = math.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)
|
||||
}
|
||||
end
|
||||
|
||||
function vector.apply(v, func)
|
||||
return {
|
||||
x = func(v.x),
|
||||
y = func(v.y),
|
||||
z = func(v.z)
|
||||
}
|
||||
end
|
||||
|
||||
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))
|
||||
end
|
||||
|
||||
function vector.direction(pos1, pos2)
|
||||
return vector.normalize({
|
||||
x = pos2.x - pos1.x,
|
||||
y = pos2.y - pos1.y,
|
||||
z = pos2.z - pos1.z
|
||||
})
|
||||
end
|
||||
|
||||
function vector.angle(a, b)
|
||||
local dotp = vector.dot(a, b)
|
||||
local cp = vector.cross(a, b)
|
||||
local crossplen = vector.length(cp)
|
||||
return math.atan2(crossplen, dotp)
|
||||
end
|
||||
|
||||
function vector.dot(a, b)
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z
|
||||
end
|
||||
|
||||
function vector.cross(a, b)
|
||||
return {
|
||||
x = a.y * b.z - a.z * b.y,
|
||||
y = a.z * b.x - a.x * b.z,
|
||||
z = a.x * b.y - a.y * b.x
|
||||
}
|
||||
end
|
||||
|
||||
function vector.add(a, b)
|
||||
if type(b) == "table" then
|
||||
return {x = a.x + b.x,
|
||||
y = a.y + b.y,
|
||||
z = a.z + b.z}
|
||||
else
|
||||
return {x = a.x + b,
|
||||
y = a.y + b,
|
||||
z = a.z + b}
|
||||
end
|
||||
end
|
||||
|
||||
function vector.subtract(a, b)
|
||||
if type(b) == "table" then
|
||||
return {x = a.x - b.x,
|
||||
y = a.y - b.y,
|
||||
z = a.z - b.z}
|
||||
else
|
||||
return {x = a.x - b,
|
||||
y = a.y - b,
|
||||
z = a.z - b}
|
||||
end
|
||||
end
|
||||
|
||||
function vector.multiply(a, b)
|
||||
if type(b) == "table" then
|
||||
return {x = a.x * b.x,
|
||||
y = a.y * b.y,
|
||||
z = a.z * b.z}
|
||||
else
|
||||
return {x = a.x * b,
|
||||
y = a.y * b,
|
||||
z = a.z * b}
|
||||
end
|
||||
end
|
||||
|
||||
function vector.divide(a, b)
|
||||
if type(b) == "table" then
|
||||
return {x = a.x / b.x,
|
||||
y = a.y / b.y,
|
||||
z = a.z / b.z}
|
||||
else
|
||||
return {x = a.x / b,
|
||||
y = a.y / b,
|
||||
z = a.z / b}
|
||||
end
|
||||
end
|
||||
|
||||
function vector.offset(v, x, y, z)
|
||||
return {x = v.x + x,
|
||||
y = v.y + y,
|
||||
z = v.z + z}
|
||||
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)}
|
||||
end
|
||||
|
||||
local function sin(x)
|
||||
if x % math.pi == 0 then
|
||||
return 0
|
||||
else
|
||||
return math.sin(x)
|
||||
end
|
||||
end
|
||||
|
||||
local function cos(x)
|
||||
if x % math.pi == math.pi / 2 then
|
||||
return 0
|
||||
else
|
||||
return math.cos(x)
|
||||
end
|
||||
end
|
||||
|
||||
function vector.rotate_around_axis(v, axis, angle)
|
||||
local cosangle = cos(angle)
|
||||
local sinangle = sin(angle)
|
||||
axis = vector.normalize(axis)
|
||||
-- https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula
|
||||
local dot_axis = vector.multiply(axis, vector.dot(axis, v))
|
||||
local cross = vector.cross(v, axis)
|
||||
return vector.new(
|
||||
cross.x * sinangle + (v.x - dot_axis.x) * cosangle + dot_axis.x,
|
||||
cross.y * sinangle + (v.y - dot_axis.y) * cosangle + dot_axis.y,
|
||||
cross.z * sinangle + (v.z - dot_axis.z) * cosangle + dot_axis.z
|
||||
)
|
||||
end
|
||||
|
||||
function vector.rotate(v, rot)
|
||||
local sinpitch = sin(-rot.x)
|
||||
local sinyaw = sin(-rot.y)
|
||||
local sinroll = sin(-rot.z)
|
||||
local cospitch = cos(rot.x)
|
||||
local cosyaw = cos(rot.y)
|
||||
local cosroll = math.cos(rot.z)
|
||||
-- Rotation matrix that applies yaw, pitch and roll
|
||||
local matrix = {
|
||||
{
|
||||
sinyaw * sinpitch * sinroll + cosyaw * cosroll,
|
||||
sinyaw * sinpitch * cosroll - cosyaw * sinroll,
|
||||
sinyaw * cospitch,
|
||||
},
|
||||
{
|
||||
cospitch * sinroll,
|
||||
cospitch * cosroll,
|
||||
-sinpitch,
|
||||
},
|
||||
{
|
||||
cosyaw * sinpitch * sinroll - sinyaw * cosroll,
|
||||
cosyaw * sinpitch * cosroll + sinyaw * sinroll,
|
||||
cosyaw * cospitch,
|
||||
},
|
||||
}
|
||||
-- Compute matrix multiplication: `matrix` * `v`
|
||||
return vector.new(
|
||||
matrix[1][1] * v.x + matrix[1][2] * v.y + matrix[1][3] * v.z,
|
||||
matrix[2][1] * v.x + matrix[2][2] * v.y + matrix[2][3] * v.z,
|
||||
matrix[3][1] * v.x + matrix[3][2] * v.y + matrix[3][3] * v.z
|
||||
)
|
||||
end
|
||||
|
||||
function vector.dir_to_rotation(forward, up)
|
||||
forward = vector.normalize(forward)
|
||||
local rot = {x = math.asin(forward.y), y = -math.atan2(forward.x, forward.z), z = 0}
|
||||
if not up then
|
||||
return rot
|
||||
end
|
||||
assert(vector.dot(forward, up) < 0.000001,
|
||||
"Invalid vectors passed to vector.dir_to_rotation().")
|
||||
up = vector.normalize(up)
|
||||
-- Calculate vector pointing up with roll = 0, just based on forward vector.
|
||||
local forwup = vector.rotate({x = 0, y = 1, z = 0}, rot)
|
||||
-- 'forwup' and 'up' are now in a plane with 'forward' as normal.
|
||||
-- The angle between them is the absolute of the roll value we're looking for.
|
||||
rot.z = vector.angle(forwup, up)
|
||||
|
||||
-- Since vector.angle never returns a negative value or a value greater
|
||||
-- than math.pi, rot.z has to be inverted sometimes.
|
||||
-- To determine wether this is the case, we rotate the up vector back around
|
||||
-- the forward vector and check if it worked out.
|
||||
local back = vector.rotate_around_axis(up, forward, -rot.z)
|
||||
|
||||
-- We don't use vector.equals for this because of floating point imprecision.
|
||||
if (back.x - forwup.x) * (back.x - forwup.x) +
|
||||
(back.y - forwup.y) * (back.y - forwup.y) +
|
||||
(back.z - forwup.z) * (back.z - forwup.z) > 0.0000001 then
|
||||
rot.z = -rot.z
|
||||
end
|
||||
return rot
|
||||
end
|
215
builtin/fstk/buttonbar.lua
Normal file
215
builtin/fstk/buttonbar.lua
Normal file
@ -0,0 +1,215 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
|
||||
local function buttonbar_formspec(self)
|
||||
|
||||
if self.hidden then
|
||||
return ""
|
||||
end
|
||||
|
||||
local formspec = string.format("box[%f,%f;%f,%f;%s]",
|
||||
self.pos.x,self.pos.y ,self.size.x,self.size.y,self.bgcolor)
|
||||
|
||||
for i=self.startbutton,#self.buttons,1 do
|
||||
local btn_name = self.buttons[i].name
|
||||
local btn_pos = {}
|
||||
|
||||
if self.orientation == "horizontal" then
|
||||
btn_pos.x = self.pos.x + --base pos
|
||||
(i - self.startbutton) * self.btn_size + --button offset
|
||||
self.btn_initial_offset
|
||||
else
|
||||
btn_pos.x = self.pos.x + (self.btn_size * 0.05)
|
||||
end
|
||||
|
||||
if self.orientation == "vertical" then
|
||||
btn_pos.y = self.pos.y + --base pos
|
||||
(i - self.startbutton) * self.btn_size + --button offset
|
||||
self.btn_initial_offset
|
||||
else
|
||||
btn_pos.y = self.pos.y + (self.btn_size * 0.05)
|
||||
end
|
||||
|
||||
if (self.orientation == "vertical" and
|
||||
(btn_pos.y + self.btn_size <= self.pos.y + self.size.y)) or
|
||||
(self.orientation == "horizontal" and
|
||||
(btn_pos.x + self.btn_size <= self.pos.x + self.size.x)) then
|
||||
|
||||
local borders="true"
|
||||
|
||||
if self.buttons[i].image ~= nil then
|
||||
borders="false"
|
||||
end
|
||||
|
||||
formspec = formspec ..
|
||||
string.format("image_button[%f,%f;%f,%f;%s;%s;%s;true;%s]tooltip[%s;%s]",
|
||||
btn_pos.x, btn_pos.y, self.btn_size, self.btn_size,
|
||||
self.buttons[i].image, btn_name, self.buttons[i].caption,
|
||||
borders, btn_name, self.buttons[i].tooltip)
|
||||
else
|
||||
--print("end of displayable buttons: orientation: " .. self.orientation)
|
||||
--print( "button_end: " .. (btn_pos.y + self.btn_size - (self.btn_size * 0.05)))
|
||||
--print( "bar_end: " .. (self.pos.x + self.size.x))
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if (self.have_move_buttons) then
|
||||
local btn_dec_pos = {}
|
||||
btn_dec_pos.x = self.pos.x + (self.btn_size * 0.05)
|
||||
btn_dec_pos.y = self.pos.y + (self.btn_size * 0.05)
|
||||
local btn_inc_pos = {}
|
||||
local btn_size = {}
|
||||
|
||||
if self.orientation == "horizontal" then
|
||||
btn_size.x = 0.5
|
||||
btn_size.y = self.btn_size
|
||||
btn_inc_pos.x = self.pos.x + self.size.x - 0.5
|
||||
btn_inc_pos.y = self.pos.y + (self.btn_size * 0.05)
|
||||
else
|
||||
btn_size.x = self.btn_size
|
||||
btn_size.y = 0.5
|
||||
btn_inc_pos.x = self.pos.x + (self.btn_size * 0.05)
|
||||
btn_inc_pos.y = self.pos.y + self.size.y - 0.5
|
||||
end
|
||||
|
||||
local text_dec = "<"
|
||||
local text_inc = ">"
|
||||
if self.orientation == "vertical" then
|
||||
text_dec = "^"
|
||||
text_inc = "v"
|
||||
end
|
||||
|
||||
formspec = formspec ..
|
||||
string.format("image_button[%f,%f;%f,%f;;btnbar_dec_%s;%s;true;true]",
|
||||
btn_dec_pos.x, btn_dec_pos.y, btn_size.x, btn_size.y,
|
||||
self.name, text_dec)
|
||||
|
||||
formspec = formspec ..
|
||||
string.format("image_button[%f,%f;%f,%f;;btnbar_inc_%s;%s;true;true]",
|
||||
btn_inc_pos.x, btn_inc_pos.y, btn_size.x, btn_size.y,
|
||||
self.name, text_inc)
|
||||
end
|
||||
|
||||
return formspec
|
||||
end
|
||||
|
||||
local function buttonbar_buttonhandler(self, fields)
|
||||
|
||||
if fields["btnbar_inc_" .. self.name] ~= nil and
|
||||
self.startbutton < #self.buttons then
|
||||
|
||||
self.startbutton = self.startbutton + 1
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["btnbar_dec_" .. self.name] ~= nil and self.startbutton > 1 then
|
||||
self.startbutton = self.startbutton - 1
|
||||
return true
|
||||
end
|
||||
|
||||
for i=1,#self.buttons,1 do
|
||||
if fields[self.buttons[i].name] ~= nil then
|
||||
return self.userbuttonhandler(fields)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local buttonbar_metatable = {
|
||||
handle_buttons = buttonbar_buttonhandler,
|
||||
handle_events = function(self, event) end,
|
||||
get_formspec = buttonbar_formspec,
|
||||
|
||||
hide = function(self) self.hidden = true end,
|
||||
show = function(self) self.hidden = false end,
|
||||
|
||||
delete = function(self) ui.delete(self) end,
|
||||
|
||||
add_button = function(self, name, caption, image, tooltip)
|
||||
if caption == nil then caption = "" end
|
||||
if image == nil then image = "" end
|
||||
if tooltip == nil then tooltip = "" end
|
||||
|
||||
self.buttons[#self.buttons + 1] = {
|
||||
name = name,
|
||||
caption = caption,
|
||||
image = image,
|
||||
tooltip = tooltip
|
||||
}
|
||||
if self.orientation == "horizontal" then
|
||||
if ( (self.btn_size * #self.buttons) + (self.btn_size * 0.05 *2)
|
||||
> self.size.x ) then
|
||||
|
||||
self.btn_initial_offset = self.btn_size * 0.05 + 0.5
|
||||
self.have_move_buttons = true
|
||||
end
|
||||
else
|
||||
if ((self.btn_size * #self.buttons) + (self.btn_size * 0.05 *2)
|
||||
> self.size.y ) then
|
||||
|
||||
self.btn_initial_offset = self.btn_size * 0.05 + 0.5
|
||||
self.have_move_buttons = true
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
set_bgparams = function(self, bgcolor)
|
||||
if (type(bgcolor) == "string") then
|
||||
self.bgcolor = bgcolor
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
buttonbar_metatable.__index = buttonbar_metatable
|
||||
|
||||
function buttonbar_create(name, cbf_buttonhandler, pos, orientation, size)
|
||||
assert(name ~= nil)
|
||||
assert(cbf_buttonhandler ~= nil)
|
||||
assert(orientation == "vertical" or orientation == "horizontal")
|
||||
assert(pos ~= nil and type(pos) == "table")
|
||||
assert(size ~= nil and type(size) == "table")
|
||||
|
||||
local self = {}
|
||||
self.name = name
|
||||
self.type = "addon"
|
||||
self.bgcolor = "#000000"
|
||||
self.pos = pos
|
||||
self.size = size
|
||||
self.orientation = orientation
|
||||
self.startbutton = 1
|
||||
self.have_move_buttons = false
|
||||
self.hidden = false
|
||||
|
||||
if self.orientation == "horizontal" then
|
||||
self.btn_size = self.size.y
|
||||
else
|
||||
self.btn_size = self.size.x
|
||||
end
|
||||
|
||||
if (self.btn_initial_offset == nil) then
|
||||
self.btn_initial_offset = self.btn_size * 0.05
|
||||
end
|
||||
|
||||
self.userbuttonhandler = cbf_buttonhandler
|
||||
self.buttons = {}
|
||||
|
||||
setmetatable(self,buttonbar_metatable)
|
||||
|
||||
ui.add(self)
|
||||
return self
|
||||
end
|
88
builtin/fstk/dialog.lua
Normal file
88
builtin/fstk/dialog.lua
Normal file
@ -0,0 +1,88 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
local function dialog_event_handler(self,event)
|
||||
if self.user_eventhandler == nil or
|
||||
self.user_eventhandler(event) == false then
|
||||
|
||||
--close dialog on esc
|
||||
if event == "MenuQuit" then
|
||||
self:delete()
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local dialog_metatable = {
|
||||
eventhandler = dialog_event_handler,
|
||||
get_formspec = function(self)
|
||||
if not self.hidden then return self.formspec(self.data) end
|
||||
end,
|
||||
handle_buttons = function(self,fields)
|
||||
if not self.hidden then return self.buttonhandler(self,fields) end
|
||||
end,
|
||||
handle_events = function(self,event)
|
||||
if not self.hidden then return self.eventhandler(self,event) end
|
||||
end,
|
||||
hide = function(self) self.hidden = true end,
|
||||
show = function(self) self.hidden = false end,
|
||||
delete = function(self)
|
||||
if self.parent ~= nil then
|
||||
self.parent:show()
|
||||
end
|
||||
ui.delete(self)
|
||||
end,
|
||||
set_parent = function(self,parent) self.parent = parent end
|
||||
}
|
||||
dialog_metatable.__index = dialog_metatable
|
||||
|
||||
function dialog_create(name,get_formspec,buttonhandler,eventhandler)
|
||||
local self = {}
|
||||
|
||||
self.name = name
|
||||
self.type = "toplevel"
|
||||
self.hidden = true
|
||||
self.data = {}
|
||||
|
||||
self.formspec = get_formspec
|
||||
self.buttonhandler = buttonhandler
|
||||
self.user_eventhandler = eventhandler
|
||||
|
||||
setmetatable(self,dialog_metatable)
|
||||
|
||||
ui.add(self)
|
||||
return self
|
||||
end
|
||||
|
||||
function messagebox(name, message)
|
||||
return dialog_create(name,
|
||||
function()
|
||||
return ([[
|
||||
formspec_version[3]
|
||||
size[8,3]
|
||||
textarea[0.375,0.375;7.25,1.2;;;%s]
|
||||
button[3,1.825;2,0.8;ok;%s]
|
||||
]]):format(message, fgettext("OK"))
|
||||
end,
|
||||
function(this, fields)
|
||||
if fields.ok then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
end,
|
||||
nil)
|
||||
end
|
273
builtin/fstk/tabview.lua
Normal file
273
builtin/fstk/tabview.lua
Normal file
@ -0,0 +1,273 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- A tabview implementation --
|
||||
-- Usage: --
|
||||
-- tabview.create: returns initialized tabview raw element --
|
||||
-- element.add(tab): add a tab declaration --
|
||||
-- element.handle_buttons() --
|
||||
-- element.handle_events() --
|
||||
-- element.getFormspec() returns formspec of tabview --
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function add_tab(self,tab)
|
||||
assert(tab.size == nil or (type(tab.size) == table and
|
||||
tab.size.x ~= nil and tab.size.y ~= nil))
|
||||
assert(tab.cbf_formspec ~= nil and type(tab.cbf_formspec) == "function")
|
||||
assert(tab.cbf_button_handler == nil or
|
||||
type(tab.cbf_button_handler) == "function")
|
||||
assert(tab.cbf_events == nil or type(tab.cbf_events) == "function")
|
||||
|
||||
local newtab = {
|
||||
name = tab.name,
|
||||
caption = tab.caption,
|
||||
button_handler = tab.cbf_button_handler,
|
||||
event_handler = tab.cbf_events,
|
||||
get_formspec = tab.cbf_formspec,
|
||||
tabsize = tab.tabsize,
|
||||
on_change = tab.on_change,
|
||||
tabdata = {},
|
||||
}
|
||||
|
||||
self.tablist[#self.tablist + 1] = newtab
|
||||
|
||||
if self.last_tab_index == #self.tablist then
|
||||
self.current_tab = tab.name
|
||||
if tab.on_activate ~= nil then
|
||||
tab.on_activate(nil,tab.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function get_formspec(self)
|
||||
local formspec = ""
|
||||
|
||||
if not self.hidden and (self.parent == nil or not self.parent.hidden) then
|
||||
|
||||
if self.parent == nil then
|
||||
local tsize = self.tablist[self.last_tab_index].tabsize or
|
||||
{width=self.width, height=self.height}
|
||||
formspec = formspec ..
|
||||
string.format("size[%f,%f,%s]",tsize.width,tsize.height,
|
||||
dump(self.fixed_size))
|
||||
end
|
||||
formspec = formspec .. self:tab_header()
|
||||
formspec = formspec ..
|
||||
self.tablist[self.last_tab_index].get_formspec(
|
||||
self,
|
||||
self.tablist[self.last_tab_index].name,
|
||||
self.tablist[self.last_tab_index].tabdata,
|
||||
self.tablist[self.last_tab_index].tabsize
|
||||
)
|
||||
end
|
||||
return formspec
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function handle_buttons(self,fields)
|
||||
|
||||
if self.hidden then
|
||||
return false
|
||||
end
|
||||
|
||||
if self:handle_tab_buttons(fields) then
|
||||
return true
|
||||
end
|
||||
|
||||
if self.glb_btn_handler ~= nil and
|
||||
self.glb_btn_handler(self,fields) then
|
||||
return true
|
||||
end
|
||||
|
||||
if self.tablist[self.last_tab_index].button_handler ~= nil then
|
||||
return
|
||||
self.tablist[self.last_tab_index].button_handler(
|
||||
self,
|
||||
fields,
|
||||
self.tablist[self.last_tab_index].name,
|
||||
self.tablist[self.last_tab_index].tabdata
|
||||
)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function handle_events(self,event)
|
||||
|
||||
if self.hidden then
|
||||
return false
|
||||
end
|
||||
|
||||
if self.glb_evt_handler ~= nil and
|
||||
self.glb_evt_handler(self,event) then
|
||||
return true
|
||||
end
|
||||
|
||||
if self.tablist[self.last_tab_index].evt_handler ~= nil then
|
||||
return
|
||||
self.tablist[self.last_tab_index].evt_handler(
|
||||
self,
|
||||
event,
|
||||
self.tablist[self.last_tab_index].name,
|
||||
self.tablist[self.last_tab_index].tabdata
|
||||
)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function tab_header(self)
|
||||
|
||||
local toadd = ""
|
||||
|
||||
for i=1,#self.tablist,1 do
|
||||
|
||||
if toadd ~= "" then
|
||||
toadd = toadd .. ","
|
||||
end
|
||||
|
||||
toadd = toadd .. self.tablist[i].caption
|
||||
end
|
||||
return string.format("tabheader[%f,%f;%s;%s;%i;true;false]",
|
||||
self.header_x, self.header_y, self.name, toadd, self.last_tab_index);
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function switch_to_tab(self, index)
|
||||
--first call on_change for tab to leave
|
||||
if self.tablist[self.last_tab_index].on_change ~= nil then
|
||||
self.tablist[self.last_tab_index].on_change("LEAVE",
|
||||
self.current_tab, self.tablist[index].name)
|
||||
end
|
||||
|
||||
--update tabview data
|
||||
self.last_tab_index = index
|
||||
local old_tab = self.current_tab
|
||||
self.current_tab = self.tablist[index].name
|
||||
|
||||
if (self.autosave_tab) then
|
||||
core.settings:set(self.name .. "_LAST",self.current_tab)
|
||||
end
|
||||
|
||||
-- call for tab to enter
|
||||
if self.tablist[index].on_change ~= nil then
|
||||
self.tablist[index].on_change("ENTER",
|
||||
old_tab,self.current_tab)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function handle_tab_buttons(self,fields)
|
||||
--save tab selection to config file
|
||||
if fields[self.name] then
|
||||
local index = tonumber(fields[self.name])
|
||||
switch_to_tab(self, index)
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function set_tab_by_name(self, name)
|
||||
for i=1,#self.tablist,1 do
|
||||
if self.tablist[i].name == name then
|
||||
switch_to_tab(self, i)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function hide_tabview(self)
|
||||
self.hidden=true
|
||||
|
||||
--call on_change as we're not gonna show self tab any longer
|
||||
if self.tablist[self.last_tab_index].on_change ~= nil then
|
||||
self.tablist[self.last_tab_index].on_change("LEAVE",
|
||||
self.current_tab, nil)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function show_tabview(self)
|
||||
self.hidden=false
|
||||
|
||||
-- call for tab to enter
|
||||
if self.tablist[self.last_tab_index].on_change ~= nil then
|
||||
self.tablist[self.last_tab_index].on_change("ENTER",
|
||||
nil,self.current_tab)
|
||||
end
|
||||
end
|
||||
|
||||
local tabview_metatable = {
|
||||
add = add_tab,
|
||||
handle_buttons = handle_buttons,
|
||||
handle_events = handle_events,
|
||||
get_formspec = get_formspec,
|
||||
show = show_tabview,
|
||||
hide = hide_tabview,
|
||||
delete = function(self) ui.delete(self) end,
|
||||
set_parent = function(self,parent) self.parent = parent end,
|
||||
set_autosave_tab =
|
||||
function(self,value) self.autosave_tab = value end,
|
||||
set_tab = set_tab_by_name,
|
||||
set_global_button_handler =
|
||||
function(self,handler) self.glb_btn_handler = handler end,
|
||||
set_global_event_handler =
|
||||
function(self,handler) self.glb_evt_handler = handler end,
|
||||
set_fixed_size =
|
||||
function(self,state) self.fixed_size = state end,
|
||||
tab_header = tab_header,
|
||||
handle_tab_buttons = handle_tab_buttons
|
||||
}
|
||||
|
||||
tabview_metatable.__index = tabview_metatable
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function tabview_create(name, size, tabheaderpos)
|
||||
local self = {}
|
||||
|
||||
self.name = name
|
||||
self.type = "toplevel"
|
||||
self.width = size.x
|
||||
self.height = size.y
|
||||
self.header_x = tabheaderpos.x
|
||||
self.header_y = tabheaderpos.y
|
||||
|
||||
setmetatable(self, tabview_metatable)
|
||||
|
||||
self.fixed_size = true
|
||||
self.hidden = true
|
||||
self.current_tab = nil
|
||||
self.last_tab_index = 1
|
||||
self.tablist = {}
|
||||
|
||||
self.autosave_tab = false
|
||||
|
||||
ui.add(self)
|
||||
return self
|
||||
end
|
197
builtin/fstk/ui.lua
Normal file
197
builtin/fstk/ui.lua
Normal file
@ -0,0 +1,197 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
ui = {}
|
||||
ui.childlist = {}
|
||||
ui.default = nil
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.add(child)
|
||||
--TODO check child
|
||||
ui.childlist[child.name] = child
|
||||
|
||||
return child.name
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.delete(child)
|
||||
|
||||
if ui.childlist[child.name] == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
ui.childlist[child.name] = nil
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.set_default(name)
|
||||
ui.default = name
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.find_by_name(name)
|
||||
return ui.childlist[name]
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
-- Internal functions not to be called from user
|
||||
--------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
function ui.update()
|
||||
local formspec = {}
|
||||
|
||||
-- handle errors
|
||||
if gamedata ~= nil and gamedata.reconnect_requested then
|
||||
local error_message = core.formspec_escape(
|
||||
gamedata.errormessage or "<none available>")
|
||||
formspec = {
|
||||
"size[14,8]",
|
||||
"real_coordinates[true]",
|
||||
"set_focus[btn_reconnect_yes;true]",
|
||||
"box[0.5,1.2;13,5;#000]",
|
||||
("textarea[0.5,1.2;13,5;;%s;%s]"):format(
|
||||
fgettext("The server has requested a reconnect:"), error_message),
|
||||
"button[2,6.6;4,1;btn_reconnect_yes;" .. fgettext("Reconnect") .. "]",
|
||||
"button[8,6.6;4,1;btn_reconnect_no;" .. fgettext("Main menu") .. "]"
|
||||
}
|
||||
elseif gamedata ~= nil and gamedata.errormessage ~= nil then
|
||||
local error_message = core.formspec_escape(gamedata.errormessage)
|
||||
|
||||
local error_title
|
||||
if string.find(gamedata.errormessage, "ModError") then
|
||||
error_title = fgettext("An error occurred in a Lua script:")
|
||||
else
|
||||
error_title = fgettext("An error occurred:")
|
||||
end
|
||||
formspec = {
|
||||
"size[14,8]",
|
||||
"real_coordinates[true]",
|
||||
"set_focus[btn_error_confirm;true]",
|
||||
"box[0.5,1.2;13,5;#000]",
|
||||
("textarea[0.5,1.2;13,5;;%s;%s]"):format(
|
||||
error_title, error_message),
|
||||
"button[5,6.6;4,1;btn_error_confirm;" .. fgettext("OK") .. "]"
|
||||
}
|
||||
else
|
||||
local active_toplevel_ui_elements = 0
|
||||
for key,value in pairs(ui.childlist) do
|
||||
if (value.type == "toplevel") then
|
||||
local retval = value:get_formspec()
|
||||
|
||||
if retval ~= nil and retval ~= "" then
|
||||
active_toplevel_ui_elements = active_toplevel_ui_elements + 1
|
||||
table.insert(formspec, retval)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- no need to show addons if there ain't a toplevel element
|
||||
if (active_toplevel_ui_elements > 0) then
|
||||
for key,value in pairs(ui.childlist) do
|
||||
if (value.type == "addon") then
|
||||
local retval = value:get_formspec()
|
||||
|
||||
if retval ~= nil and retval ~= "" then
|
||||
table.insert(formspec, retval)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (active_toplevel_ui_elements > 1) then
|
||||
core.log("warning", "more than one active ui "..
|
||||
"element, self most likely isn't intended")
|
||||
end
|
||||
|
||||
if (active_toplevel_ui_elements == 0) then
|
||||
core.log("warning", "no toplevel ui element "..
|
||||
"active; switching to default")
|
||||
ui.childlist[ui.default]:show()
|
||||
formspec = {ui.childlist[ui.default]:get_formspec()}
|
||||
end
|
||||
end
|
||||
core.update_formspec(table.concat(formspec))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.handle_buttons(fields)
|
||||
for key,value in pairs(ui.childlist) do
|
||||
|
||||
local retval = value:handle_buttons(fields)
|
||||
|
||||
if retval then
|
||||
ui.update()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.handle_events(event)
|
||||
|
||||
for key,value in pairs(ui.childlist) do
|
||||
|
||||
if value.handle_events ~= nil then
|
||||
local retval = value:handle_events(event)
|
||||
|
||||
if retval then
|
||||
return retval
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
-- initialize callbacks
|
||||
--------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
core.button_handler = function(fields)
|
||||
if fields["btn_reconnect_yes"] then
|
||||
gamedata.reconnect_requested = false
|
||||
gamedata.errormessage = nil
|
||||
gamedata.do_reconnect = true
|
||||
core.start()
|
||||
return
|
||||
elseif fields["btn_reconnect_no"] or fields["btn_error_confirm"] then
|
||||
gamedata.errormessage = nil
|
||||
gamedata.reconnect_requested = false
|
||||
ui.update()
|
||||
return
|
||||
end
|
||||
|
||||
if ui.handle_buttons(fields) then
|
||||
ui.update()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
core.event_handler = function(event)
|
||||
if ui.handle_events(event) then
|
||||
ui.update()
|
||||
return
|
||||
end
|
||||
|
||||
if event == "Refresh" then
|
||||
ui.update()
|
||||
return
|
||||
end
|
||||
end
|
184
builtin/game/auth.lua
Normal file
184
builtin/game/auth.lua
Normal file
@ -0,0 +1,184 @@
|
||||
-- Minetest: builtin/auth.lua
|
||||
|
||||
--
|
||||
-- Builtin authentication handler
|
||||
--
|
||||
|
||||
-- Make the auth object private, deny access to mods
|
||||
local core_auth = core.auth
|
||||
core.auth = nil
|
||||
|
||||
core.builtin_auth_handler = {
|
||||
get_auth = function(name)
|
||||
assert(type(name) == "string")
|
||||
local auth_entry = core_auth.read(name)
|
||||
-- If no such auth found, return nil
|
||||
if not auth_entry then
|
||||
return nil
|
||||
end
|
||||
-- Figure out what privileges the player should have.
|
||||
-- Take a copy of the privilege table
|
||||
local privileges = {}
|
||||
for priv, _ in pairs(auth_entry.privileges) do
|
||||
privileges[priv] = true
|
||||
end
|
||||
-- If singleplayer, give all privileges except those marked as give_to_singleplayer = false
|
||||
if core.is_singleplayer() then
|
||||
for priv, def in pairs(core.registered_privileges) do
|
||||
if def.give_to_singleplayer then
|
||||
privileges[priv] = true
|
||||
end
|
||||
end
|
||||
-- For the admin, give everything
|
||||
elseif name == core.settings:get("name") then
|
||||
for priv, def in pairs(core.registered_privileges) do
|
||||
if def.give_to_admin then
|
||||
privileges[priv] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
-- All done
|
||||
return {
|
||||
password = auth_entry.password,
|
||||
privileges = privileges,
|
||||
last_login = auth_entry.last_login,
|
||||
}
|
||||
end,
|
||||
create_auth = function(name, password)
|
||||
assert(type(name) == "string")
|
||||
assert(type(password) == "string")
|
||||
core.log('info', "Built-in authentication handler adding player '"..name.."'")
|
||||
return core_auth.create({
|
||||
name = name,
|
||||
password = password,
|
||||
privileges = core.string_to_privs(core.settings:get("default_privs")),
|
||||
last_login = -1, -- Defer login time calculation until record_login (called by on_joinplayer)
|
||||
})
|
||||
end,
|
||||
delete_auth = function(name)
|
||||
assert(type(name) == "string")
|
||||
local auth_entry = core_auth.read(name)
|
||||
if not auth_entry then
|
||||
return false
|
||||
end
|
||||
core.log('info', "Built-in authentication handler deleting player '"..name.."'")
|
||||
return core_auth.delete(name)
|
||||
end,
|
||||
set_password = function(name, password)
|
||||
assert(type(name) == "string")
|
||||
assert(type(password) == "string")
|
||||
local auth_entry = core_auth.read(name)
|
||||
if not auth_entry then
|
||||
core.builtin_auth_handler.create_auth(name, password)
|
||||
else
|
||||
core.log('info', "Built-in authentication handler setting password of player '"..name.."'")
|
||||
auth_entry.password = password
|
||||
core_auth.save(auth_entry)
|
||||
end
|
||||
return true
|
||||
end,
|
||||
set_privileges = function(name, privileges)
|
||||
assert(type(name) == "string")
|
||||
assert(type(privileges) == "table")
|
||||
local auth_entry = core_auth.read(name)
|
||||
if not auth_entry then
|
||||
auth_entry = core.builtin_auth_handler.create_auth(name,
|
||||
core.get_password_hash(name,
|
||||
core.settings:get("default_password")))
|
||||
end
|
||||
|
||||
-- Run grant callbacks
|
||||
for priv, _ in pairs(privileges) do
|
||||
if not auth_entry.privileges[priv] then
|
||||
core.run_priv_callbacks(name, priv, nil, "grant")
|
||||
end
|
||||
end
|
||||
|
||||
-- Run revoke callbacks
|
||||
for priv, _ in pairs(auth_entry.privileges) do
|
||||
if not privileges[priv] then
|
||||
core.run_priv_callbacks(name, priv, nil, "revoke")
|
||||
end
|
||||
end
|
||||
|
||||
auth_entry.privileges = privileges
|
||||
core_auth.save(auth_entry)
|
||||
core.notify_authentication_modified(name)
|
||||
end,
|
||||
reload = function()
|
||||
core_auth.reload()
|
||||
return true
|
||||
end,
|
||||
record_login = function(name)
|
||||
assert(type(name) == "string")
|
||||
local auth_entry = core_auth.read(name)
|
||||
assert(auth_entry)
|
||||
auth_entry.last_login = os.time()
|
||||
core_auth.save(auth_entry)
|
||||
end,
|
||||
iterate = function()
|
||||
local names = {}
|
||||
local nameslist = core_auth.list_names()
|
||||
for k,v in pairs(nameslist) do
|
||||
names[v] = true
|
||||
end
|
||||
return pairs(names)
|
||||
end,
|
||||
}
|
||||
|
||||
core.register_on_prejoinplayer(function(name, ip)
|
||||
if core.registered_auth_handler ~= nil then
|
||||
return -- Don't do anything if custom auth handler registered
|
||||
end
|
||||
local auth_entry = core_auth.read(name)
|
||||
if auth_entry ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
--
|
||||
-- Authentication API
|
||||
--
|
||||
|
||||
function core.register_authentication_handler(handler)
|
||||
if core.registered_auth_handler then
|
||||
error("Add-on authentication handler already registered by "..core.registered_auth_handler_modname)
|
||||
end
|
||||
core.registered_auth_handler = handler
|
||||
core.registered_auth_handler_modname = core.get_current_modname()
|
||||
handler.mod_origin = core.registered_auth_handler_modname
|
||||
end
|
||||
|
||||
function core.get_auth_handler()
|
||||
return core.registered_auth_handler or core.builtin_auth_handler
|
||||
end
|
||||
|
||||
local function auth_pass(name)
|
||||
return function(...)
|
||||
local auth_handler = core.get_auth_handler()
|
||||
if auth_handler[name] then
|
||||
return auth_handler[name](...)
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
core.set_player_password = auth_pass("set_password")
|
||||
core.set_player_privs = auth_pass("set_privileges")
|
||||
core.remove_player_auth = auth_pass("delete_auth")
|
||||
core.auth_reload = auth_pass("reload")
|
||||
|
||||
local record_login = auth_pass("record_login")
|
||||
core.register_on_joinplayer(function(player)
|
||||
record_login(player:get_player_name())
|
||||
end)
|
1151
builtin/game/chat.lua
Normal file
1151
builtin/game/chat.lua
Normal file
File diff suppressed because it is too large
Load Diff
31
builtin/game/constants.lua
Normal file
31
builtin/game/constants.lua
Normal file
@ -0,0 +1,31 @@
|
||||
-- Minetest: builtin/constants.lua
|
||||
|
||||
--
|
||||
-- Constants values for use with the Lua API
|
||||
--
|
||||
|
||||
-- mapnode.h
|
||||
-- Built-in Content IDs (for use with VoxelManip API)
|
||||
core.CONTENT_UNKNOWN = 125
|
||||
core.CONTENT_AIR = 126
|
||||
core.CONTENT_IGNORE = 127
|
||||
|
||||
-- emerge.h
|
||||
-- Block emerge status constants (for use with core.emerge_area)
|
||||
core.EMERGE_CANCELLED = 0
|
||||
core.EMERGE_ERRORED = 1
|
||||
core.EMERGE_FROM_MEMORY = 2
|
||||
core.EMERGE_FROM_DISK = 3
|
||||
core.EMERGE_GENERATED = 4
|
||||
|
||||
-- constants.h
|
||||
-- Size of mapblocks in nodes
|
||||
core.MAP_BLOCKSIZE = 16
|
||||
-- Default maximal HP of a player
|
||||
core.PLAYER_MAX_HP_DEFAULT = 20
|
||||
-- Default maximal breath of a player
|
||||
core.PLAYER_MAX_BREATH_DEFAULT = 10
|
||||
|
||||
-- light.h
|
||||
-- Maximum value for node 'light_source' parameter
|
||||
core.LIGHT_MAX = 14
|
88
builtin/game/deprecated.lua
Normal file
88
builtin/game/deprecated.lua
Normal file
@ -0,0 +1,88 @@
|
||||
-- Minetest: builtin/deprecated.lua
|
||||
|
||||
--
|
||||
-- Default material types
|
||||
--
|
||||
local function digprop_err()
|
||||
core.log("deprecated", "The core.digprop_* functions are obsolete and need to be replaced by item groups.")
|
||||
end
|
||||
|
||||
core.digprop_constanttime = digprop_err
|
||||
core.digprop_stonelike = digprop_err
|
||||
core.digprop_dirtlike = digprop_err
|
||||
core.digprop_gravellike = digprop_err
|
||||
core.digprop_woodlike = digprop_err
|
||||
core.digprop_leaveslike = digprop_err
|
||||
core.digprop_glasslike = digprop_err
|
||||
|
||||
function core.node_metadata_inventory_move_allow_all()
|
||||
core.log("deprecated", "core.node_metadata_inventory_move_allow_all is obsolete and does nothing.")
|
||||
end
|
||||
|
||||
function core.add_to_creative_inventory(itemstring)
|
||||
core.log("deprecated", "core.add_to_creative_inventory is obsolete and does nothing.")
|
||||
end
|
||||
|
||||
--
|
||||
-- EnvRef
|
||||
--
|
||||
core.env = {}
|
||||
local envref_deprecation_message_printed = false
|
||||
setmetatable(core.env, {
|
||||
__index = function(table, key)
|
||||
if not envref_deprecation_message_printed then
|
||||
core.log("deprecated", "core.env:[...] is deprecated and should be replaced with core.[...]")
|
||||
envref_deprecation_message_printed = true
|
||||
end
|
||||
local func = core[key]
|
||||
if type(func) == "function" then
|
||||
rawset(table, key, function(self, ...)
|
||||
return func(...)
|
||||
end)
|
||||
else
|
||||
rawset(table, key, nil)
|
||||
end
|
||||
return rawget(table, key)
|
||||
end
|
||||
})
|
||||
|
||||
function core.rollback_get_last_node_actor(pos, range, seconds)
|
||||
return core.rollback_get_node_actions(pos, range, seconds, 1)[1]
|
||||
end
|
||||
|
||||
--
|
||||
-- core.setting_*
|
||||
--
|
||||
|
||||
local settings = core.settings
|
||||
|
||||
local function setting_proxy(name)
|
||||
return function(...)
|
||||
core.log("deprecated", "WARNING: minetest.setting_* "..
|
||||
"functions are deprecated. "..
|
||||
"Use methods on the minetest.settings object.")
|
||||
return settings[name](settings, ...)
|
||||
end
|
||||
end
|
||||
|
||||
core.setting_set = setting_proxy("set")
|
||||
core.setting_get = setting_proxy("get")
|
||||
core.setting_setbool = setting_proxy("set_bool")
|
||||
core.setting_getbool = setting_proxy("get_bool")
|
||||
core.setting_save = setting_proxy("write")
|
||||
|
||||
--
|
||||
-- core.register_on_auth_fail
|
||||
--
|
||||
|
||||
function core.register_on_auth_fail(func)
|
||||
core.log("deprecated", "core.register_on_auth_fail " ..
|
||||
"is obsolete and should be replaced by " ..
|
||||
"core.register_on_authplayer instead.")
|
||||
|
||||
core.register_on_authplayer(function (player_name, ip, is_success)
|
||||
if not is_success then
|
||||
func(player_name, ip)
|
||||
end
|
||||
end)
|
||||
end
|
24
builtin/game/detached_inventory.lua
Normal file
24
builtin/game/detached_inventory.lua
Normal file
@ -0,0 +1,24 @@
|
||||
-- Minetest: builtin/detached_inventory.lua
|
||||
|
||||
core.detached_inventories = {}
|
||||
|
||||
function core.create_detached_inventory(name, callbacks, player_name)
|
||||
local stuff = {}
|
||||
stuff.name = name
|
||||
if callbacks then
|
||||
stuff.allow_move = callbacks.allow_move
|
||||
stuff.allow_put = callbacks.allow_put
|
||||
stuff.allow_take = callbacks.allow_take
|
||||
stuff.on_move = callbacks.on_move
|
||||
stuff.on_put = callbacks.on_put
|
||||
stuff.on_take = callbacks.on_take
|
||||
end
|
||||
stuff.mod_origin = core.get_current_modname() or "??"
|
||||
core.detached_inventories[name] = stuff
|
||||
return core.create_detached_inventory_raw(name, player_name)
|
||||
end
|
||||
|
||||
function core.remove_detached_inventory(name)
|
||||
core.detached_inventories[name] = nil
|
||||
return core.remove_detached_inventory_raw(name)
|
||||
end
|
585
builtin/game/falling.lua
Normal file
585
builtin/game/falling.lua
Normal file
@ -0,0 +1,585 @@
|
||||
-- Minetest: builtin/item.lua
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81
|
||||
|
||||
--
|
||||
-- Falling stuff
|
||||
--
|
||||
|
||||
core.register_entity(":__builtin:falling_node", {
|
||||
initial_properties = {
|
||||
visual = "item",
|
||||
visual_size = {x = SCALE, y = SCALE, z = SCALE},
|
||||
textures = {},
|
||||
physical = true,
|
||||
is_visible = false,
|
||||
collide_with_objects = true,
|
||||
collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
|
||||
},
|
||||
|
||||
node = {},
|
||||
meta = {},
|
||||
floats = false,
|
||||
|
||||
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
|
||||
meta = meta:to_table()
|
||||
end
|
||||
for _, list in pairs(meta.inventory or {}) do
|
||||
for i, stack in pairs(list) do
|
||||
if type(stack) == "userdata" then
|
||||
list[i] = stack:to_string()
|
||||
end
|
||||
end
|
||||
end
|
||||
local def = core.registered_nodes[node.name]
|
||||
if not def then
|
||||
-- Don't allow unknown nodes to fall
|
||||
core.log("info",
|
||||
"Unknown falling node removed at "..
|
||||
core.pos_to_string(self.object:get_pos()))
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
self.meta = meta
|
||||
|
||||
-- Cache whether we're supposed to float on water
|
||||
self.floats = core.get_item_group(node.name, "float") ~= 0
|
||||
|
||||
-- Set entity visuals
|
||||
if def.drawtype == "torchlike" or def.drawtype == "signlike" then
|
||||
local textures
|
||||
if def.tiles and def.tiles[1] then
|
||||
local tile = def.tiles[1]
|
||||
if def.drawtype == "torchlike" and def.paramtype2 ~= "wallmounted" then
|
||||
tile = def.tiles[2] or def.tiles[1]
|
||||
end
|
||||
if type(tile) == "table" then
|
||||
tile = tile.name
|
||||
end
|
||||
if def.drawtype == "torchlike" then
|
||||
textures = { "("..tile..")^[transformFX", tile }
|
||||
else
|
||||
textures = { tile, "("..tile..")^[transformFX" }
|
||||
end
|
||||
end
|
||||
local vsize
|
||||
if def.visual_scale then
|
||||
local s = def.visual_scale
|
||||
vsize = {x = s, y = s, z = s}
|
||||
end
|
||||
self.object:set_properties({
|
||||
is_visible = true,
|
||||
visual = "upright_sprite",
|
||||
visual_size = vsize,
|
||||
textures = textures,
|
||||
glow = def.light_source,
|
||||
})
|
||||
elseif def.drawtype ~= "airlike" then
|
||||
local itemstring = node.name
|
||||
if core.is_colored_paramtype(def.paramtype2) then
|
||||
itemstring = core.itemstring_with_palette(itemstring, node.param2)
|
||||
end
|
||||
-- FIXME: solution needed for paramtype2 == "leveled"
|
||||
local vsize
|
||||
if def.visual_scale then
|
||||
local s = def.visual_scale * SCALE
|
||||
vsize = {x = s, y = s, z = s}
|
||||
end
|
||||
self.object:set_properties({
|
||||
is_visible = true,
|
||||
wield_item = itemstring,
|
||||
visual_size = vsize,
|
||||
glow = def.light_source,
|
||||
})
|
||||
end
|
||||
|
||||
-- Set collision box (certain nodeboxes only for now)
|
||||
local nb_types = {fixed=true, leveled=true, connected=true}
|
||||
if def.drawtype == "nodebox" and def.node_box and
|
||||
nb_types[def.node_box.type] then
|
||||
local box = table.copy(def.node_box.fixed)
|
||||
if type(box[1]) == "table" then
|
||||
box = #box == 1 and box[1] or nil -- We can only use a single box
|
||||
end
|
||||
if box then
|
||||
if def.paramtype2 == "leveled" and (self.node.level or 0) > 0 then
|
||||
box[5] = -0.5 + self.node.level / 64
|
||||
end
|
||||
self.object:set_properties({
|
||||
collisionbox = box
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Rotate entity
|
||||
if def.drawtype == "torchlike" then
|
||||
if def.paramtype2 == "wallmounted" then
|
||||
self.object:set_yaw(math.pi*0.25)
|
||||
else
|
||||
self.object:set_yaw(-math.pi*0.25)
|
||||
end
|
||||
elseif (node.param2 ~= 0 and (def.wield_image == ""
|
||||
or def.wield_image == nil))
|
||||
or def.drawtype == "signlike"
|
||||
or def.drawtype == "mesh"
|
||||
or def.drawtype == "normal"
|
||||
or def.drawtype == "nodebox" then
|
||||
if (def.paramtype2 == "facedir" or def.paramtype2 == "colorfacedir") then
|
||||
local fdir = node.param2 % 32
|
||||
-- Get rotation from a precalculated lookup table
|
||||
local euler = facedir_to_euler[fdir + 1]
|
||||
if euler then
|
||||
self.object:set_rotation(euler)
|
||||
end
|
||||
elseif (def.paramtype2 == "wallmounted" or def.paramtype2 == "colorwallmounted") then
|
||||
local rot = node.param2 % 8
|
||||
local pitch, yaw, roll = 0, 0, 0
|
||||
if rot == 1 then
|
||||
pitch, yaw = math.pi, math.pi
|
||||
elseif rot == 2 then
|
||||
pitch, yaw = math.pi/2, math.pi/2
|
||||
elseif rot == 3 then
|
||||
pitch, yaw = math.pi/2, -math.pi/2
|
||||
elseif rot == 4 then
|
||||
pitch, yaw = math.pi/2, math.pi
|
||||
elseif rot == 5 then
|
||||
pitch, yaw = math.pi/2, 0
|
||||
end
|
||||
if def.drawtype == "signlike" then
|
||||
pitch = pitch - math.pi/2
|
||||
if rot == 0 then
|
||||
yaw = yaw + math.pi/2
|
||||
elseif rot == 1 then
|
||||
yaw = yaw - math.pi/2
|
||||
end
|
||||
elseif def.drawtype == "mesh" or def.drawtype == "normal" then
|
||||
if rot >= 0 and rot <= 1 then
|
||||
roll = roll + math.pi
|
||||
else
|
||||
yaw = yaw + math.pi
|
||||
end
|
||||
end
|
||||
self.object:set_rotation({x=pitch, y=yaw, z=roll})
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
get_staticdata = function(self)
|
||||
local ds = {
|
||||
node = self.node,
|
||||
meta = self.meta,
|
||||
}
|
||||
return core.serialize(ds)
|
||||
end,
|
||||
|
||||
on_activate = function(self, staticdata)
|
||||
self.object:set_armor_groups({immortal = 1})
|
||||
self.object:set_acceleration({x = 0, y = -gravity, z = 0})
|
||||
|
||||
local ds = core.deserialize(staticdata)
|
||||
if ds and ds.node then
|
||||
self:set_node(ds.node, ds.meta)
|
||||
elseif ds then
|
||||
self:set_node(ds)
|
||||
elseif staticdata ~= "" then
|
||||
self:set_node({name = staticdata})
|
||||
end
|
||||
end,
|
||||
|
||||
try_place = function(self, bcp, bcn)
|
||||
local bcd = core.registered_nodes[bcn.name]
|
||||
-- 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
|
||||
end
|
||||
if core.add_node_level(bcp, addlevel) < addlevel then
|
||||
return true
|
||||
elseif bcd.buildable_to then
|
||||
-- Node level has already reached max, don't place anything
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Decide if we're replacing the node or placing on top
|
||||
local np = vector.new(bcp)
|
||||
if bcd and bcd.buildable_to and
|
||||
(not self.floats or bcd.liquidtype == "none") then
|
||||
core.remove_node(bcp)
|
||||
else
|
||||
np.y = np.y + 1
|
||||
end
|
||||
|
||||
-- Check what's here
|
||||
local n2 = core.get_node(np)
|
||||
local nd = core.registered_nodes[n2.name]
|
||||
-- If it's not air or liquid, remove node and replace it with
|
||||
-- it's drops
|
||||
if n2.name ~= "air" and (not nd or nd.liquidtype == "none") then
|
||||
if nd and nd.buildable_to == false then
|
||||
nd.on_dig(np, n2, nil)
|
||||
-- If it's still there, it might be protected
|
||||
if core.get_node(np).name == n2.name then
|
||||
return false
|
||||
end
|
||||
else
|
||||
core.remove_node(np)
|
||||
end
|
||||
end
|
||||
|
||||
-- Create node
|
||||
local def = core.registered_nodes[self.node.name]
|
||||
if def then
|
||||
core.add_node(np, self.node)
|
||||
if self.meta then
|
||||
core.get_meta(np):from_table(self.meta)
|
||||
end
|
||||
if def.sounds and def.sounds.place then
|
||||
core.sound_play(def.sounds.place, {pos = np}, true)
|
||||
end
|
||||
end
|
||||
core.check_for_falling(np)
|
||||
return true
|
||||
end,
|
||||
|
||||
on_step = function(self, dtime, moveresult)
|
||||
-- Fallback code since collision detection can't tell us
|
||||
-- about liquids (which do not collide)
|
||||
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 bcn = core.get_node(bcp)
|
||||
|
||||
local bcd = core.registered_nodes[bcn.name]
|
||||
if bcd and bcd.liquidtype ~= "none" then
|
||||
if self:try_place(bcp, bcn) then
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
assert(moveresult)
|
||||
if not moveresult.collides then
|
||||
return -- Nothing to do :)
|
||||
end
|
||||
|
||||
local bcp, bcn
|
||||
local player_collision
|
||||
if moveresult.touching_ground then
|
||||
for _, info in ipairs(moveresult.collisions) do
|
||||
if info.type == "object" then
|
||||
if info.axis == "y" and info.object:is_player() then
|
||||
player_collision = info
|
||||
end
|
||||
elseif info.axis == "y" then
|
||||
bcp = info.node_pos
|
||||
bcn = core.get_node(bcp)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not bcp then
|
||||
-- We're colliding with something, but not the ground. Irrelevant to us.
|
||||
if player_collision then
|
||||
-- Continue falling through players by moving a little into
|
||||
-- their collision box
|
||||
-- TODO: this hack could be avoided in the future if objects
|
||||
-- could choose who to collide with
|
||||
local vel = self.object:get_velocity()
|
||||
self.object:set_velocity({
|
||||
x = vel.x,
|
||||
y = player_collision.old_velocity.y,
|
||||
z = vel.z
|
||||
})
|
||||
self.object:set_pos(vector.add(self.object:get_pos(),
|
||||
{x = 0, y = -0.5, z = 0}))
|
||||
end
|
||||
return
|
||||
elseif bcn.name == "ignore" then
|
||||
-- Delete on contact with ignore at world edges
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
|
||||
local failure = false
|
||||
|
||||
local pos = self.object:get_pos()
|
||||
local distance = vector.apply(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
|
||||
failure = true
|
||||
elseif distance.y >= 2 then
|
||||
-- Doors consist of a hidden top node and a bottom node that is
|
||||
-- the actual door. Despite the top node being solid, the moveresult
|
||||
-- almost always indicates collision with the bottom node.
|
||||
-- Compensate for this by checking the top node
|
||||
bcp.y = bcp.y + 1
|
||||
bcn = core.get_node(bcp)
|
||||
local def = core.registered_nodes[bcn.name]
|
||||
if not (def and def.walkable) then
|
||||
failure = true -- This is unexpected, fail
|
||||
end
|
||||
end
|
||||
|
||||
-- Try to actually place ourselves
|
||||
if not failure then
|
||||
failure = not self:try_place(bcp, bcn)
|
||||
end
|
||||
|
||||
if failure then
|
||||
local drops = core.get_node_drops(self.node, "")
|
||||
for _, item in pairs(drops) do
|
||||
core.add_item(pos, item)
|
||||
end
|
||||
end
|
||||
self.object:remove()
|
||||
end
|
||||
})
|
||||
|
||||
local function convert_to_falling_node(pos, node)
|
||||
local obj = core.add_entity(pos, "__builtin:falling_node")
|
||||
if not obj then
|
||||
return false
|
||||
end
|
||||
-- remember node level, the entities' set_node() uses this
|
||||
node.level = core.get_node_level(pos)
|
||||
local meta = core.get_meta(pos)
|
||||
local metatable = meta and meta:to_table() or {}
|
||||
|
||||
local def = core.registered_nodes[node.name]
|
||||
if def and def.sounds and def.sounds.fall then
|
||||
core.sound_play(def.sounds.fall, {pos = pos}, true)
|
||||
end
|
||||
|
||||
obj:get_luaentity():set_node(node, metatable)
|
||||
core.remove_node(pos)
|
||||
return true
|
||||
end
|
||||
|
||||
function core.spawn_falling_node(pos)
|
||||
local node = core.get_node(pos)
|
||||
if node.name == "air" or node.name == "ignore" then
|
||||
return false
|
||||
end
|
||||
return convert_to_falling_node(pos, node)
|
||||
end
|
||||
|
||||
local 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]
|
||||
if def and def.preserve_metadata then
|
||||
local oldmeta = core.get_meta(p):to_table().fields
|
||||
-- Copy pos and node because the callback can modify them.
|
||||
local pos_copy = {x=p.x, y=p.y, z=p.z}
|
||||
local node_copy = {name=n.name, param1=n.param1, param2=n.param2}
|
||||
local drop_stacks = {}
|
||||
for k, v in pairs(drops) do
|
||||
drop_stacks[k] = ItemStack(v)
|
||||
end
|
||||
drops = drop_stacks
|
||||
def.preserve_metadata(pos_copy, node_copy, oldmeta, drops)
|
||||
end
|
||||
if def and def.sounds and def.sounds.fall then
|
||||
core.sound_play(def.sounds.fall, {pos = p}, true)
|
||||
end
|
||||
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,
|
||||
}
|
||||
core.add_item(pos, item)
|
||||
end
|
||||
end
|
||||
|
||||
function builtin_shared.check_attached_node(p, n)
|
||||
local def = core.registered_nodes[n.name]
|
||||
local d = {x = 0, y = 0, z = 0}
|
||||
if def.paramtype2 == "wallmounted" or
|
||||
def.paramtype2 == "colorwallmounted" then
|
||||
-- The fallback vector here is in case 'wallmounted to dir' is nil due
|
||||
-- to voxelmanip placing a wallmounted node without resetting a
|
||||
-- pre-existing param2 value that is out-of-range for wallmounted.
|
||||
-- The fallback vector corresponds to param2 = 0.
|
||||
d = core.wallmounted_to_dir(n.param2) or {x = 0, y = 1, z = 0}
|
||||
else
|
||||
d.y = -1
|
||||
end
|
||||
local p2 = vector.add(p, d)
|
||||
local nn = core.get_node(p2).name
|
||||
local def2 = core.registered_nodes[nn]
|
||||
if def2 and not def2.walkable then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--
|
||||
-- Some common functions
|
||||
--
|
||||
|
||||
function core.check_single_for_falling(p)
|
||||
local n = core.get_node(p)
|
||||
if core.get_item_group(n.name, "falling_node") ~= 0 then
|
||||
local p_bottom = {x = p.x, y = p.y - 1, z = p.z}
|
||||
-- Only spawn falling node if node below is loaded
|
||||
local n_bottom = core.get_node_or_nil(p_bottom)
|
||||
local d_bottom = n_bottom and core.registered_nodes[n_bottom.name]
|
||||
if d_bottom then
|
||||
local same = n.name == n_bottom.name
|
||||
-- Let leveled nodes fall if it can merge with the bottom node
|
||||
if same and d_bottom.paramtype2 == "leveled" and
|
||||
core.get_node_level(p_bottom) <
|
||||
core.get_node_max_level(p_bottom) then
|
||||
convert_to_falling_node(p, n)
|
||||
return true
|
||||
end
|
||||
-- Otherwise only if the bottom node is considered "fall through"
|
||||
if not same and
|
||||
(not d_bottom.walkable or d_bottom.buildable_to) and
|
||||
(core.get_item_group(n.name, "float") == 0 or
|
||||
d_bottom.liquidtype == "none") then
|
||||
convert_to_falling_node(p, n)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if core.get_item_group(n.name, "attached_node") ~= 0 then
|
||||
if not builtin_shared.check_attached_node(p, n) then
|
||||
drop_attached_node(p)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- This table is specifically ordered.
|
||||
-- We don't walk diagonals, only our direct neighbors, and self.
|
||||
-- Down first as likely case, but always before self. The same with sides.
|
||||
-- Up must come last, so that things above self will also fall all at once.
|
||||
local check_for_falling_neighbors = {
|
||||
{x = -1, y = -1, z = 0},
|
||||
{x = 1, y = -1, z = 0},
|
||||
{x = 0, y = -1, z = -1},
|
||||
{x = 0, y = -1, z = 1},
|
||||
{x = 0, y = -1, z = 0},
|
||||
{x = -1, y = 0, z = 0},
|
||||
{x = 1, y = 0, z = 0},
|
||||
{x = 0, y = 0, z = 1},
|
||||
{x = 0, y = 0, z = -1},
|
||||
{x = 0, y = 0, z = 0},
|
||||
{x = 0, y = 1, z = 0},
|
||||
}
|
||||
|
||||
function core.check_for_falling(p)
|
||||
-- Round p to prevent falling entities to get stuck.
|
||||
p = vector.round(p)
|
||||
|
||||
-- We make a stack, and manually maintain size for performance.
|
||||
-- Stored in the stack, we will maintain tables with pos, and
|
||||
-- last neighbor visited. This way, when we get back to each
|
||||
-- node, we know which directions we have already walked, and
|
||||
-- which direction is the next to walk.
|
||||
local s = {}
|
||||
local n = 0
|
||||
-- The neighbor order we will visit from our table.
|
||||
local v = 1
|
||||
|
||||
while true do
|
||||
-- Push current pos onto the stack.
|
||||
n = n + 1
|
||||
s[n] = {p = p, v = v}
|
||||
-- Select next node from neighbor list.
|
||||
p = vector.add(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
|
||||
-- If we don't need to "recurse" (walk) to it then pop
|
||||
-- our previous pos off the stack and continue from there,
|
||||
-- with the v value we were at when we last were at that
|
||||
-- node
|
||||
repeat
|
||||
local pop = s[n]
|
||||
p = pop.p
|
||||
v = pop.v
|
||||
s[n] = nil
|
||||
n = n - 1
|
||||
-- If there's nothing left on the stack, and no
|
||||
-- more sides to walk to, we're done and can exit
|
||||
if n == 0 and v == 11 then
|
||||
return
|
||||
end
|
||||
until v < 11
|
||||
-- The next round walk the next neighbor in list.
|
||||
v = v + 1
|
||||
else
|
||||
-- If we did need to walk the neighbor, then
|
||||
-- start walking it from the walk order start (1),
|
||||
-- and not the order we just pushed up the stack.
|
||||
v = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Global callbacks
|
||||
--
|
||||
|
||||
local function on_placenode(p, node)
|
||||
core.check_for_falling(p)
|
||||
end
|
||||
core.register_on_placenode(on_placenode)
|
||||
|
||||
local function on_dignode(p, node)
|
||||
core.check_for_falling(p)
|
||||
end
|
||||
core.register_on_dignode(on_dignode)
|
||||
|
||||
local function on_punchnode(p, node)
|
||||
core.check_for_falling(p)
|
||||
end
|
||||
core.register_on_punchnode(on_punchnode)
|
40
builtin/game/features.lua
Normal file
40
builtin/game/features.lua
Normal file
@ -0,0 +1,40 @@
|
||||
-- Minetest: builtin/features.lua
|
||||
|
||||
core.features = {
|
||||
glasslike_framed = true,
|
||||
nodebox_as_selectionbox = true,
|
||||
get_all_craft_recipes_works = true,
|
||||
use_texture_alpha = true,
|
||||
no_legacy_abms = true,
|
||||
texture_names_parens = true,
|
||||
area_store_custom_ids = true,
|
||||
add_entity_with_staticdata = true,
|
||||
no_chat_message_prediction = true,
|
||||
object_use_texture_alpha = true,
|
||||
object_independent_selectionbox = true,
|
||||
httpfetch_binary_data = true,
|
||||
formspec_version_element = true,
|
||||
area_store_persistent_ids = true,
|
||||
pathfinder_works = true,
|
||||
object_step_has_moveresult = true,
|
||||
direct_velocity_on_players = true,
|
||||
}
|
||||
|
||||
function core.has_feature(arg)
|
||||
if type(arg) == "table" then
|
||||
local missing_features = {}
|
||||
local result = true
|
||||
for ftr in pairs(arg) do
|
||||
if not core.features[ftr] then
|
||||
missing_features[ftr] = true
|
||||
result = false
|
||||
end
|
||||
end
|
||||
return result, missing_features
|
||||
elseif type(arg) == "string" then
|
||||
if not core.features[arg] then
|
||||
return false, {[arg]=true}
|
||||
end
|
||||
return true, {}
|
||||
end
|
||||
end
|
131
builtin/game/forceloading.lua
Normal file
131
builtin/game/forceloading.lua
Normal file
@ -0,0 +1,131 @@
|
||||
-- Prevent anyone else accessing those functions
|
||||
local forceload_block = core.forceload_block
|
||||
local forceload_free_block = core.forceload_free_block
|
||||
core.forceload_block = nil
|
||||
core.forceload_free_block = nil
|
||||
|
||||
local blocks_forceloaded
|
||||
local blocks_temploaded = {}
|
||||
local total_forceloaded = 0
|
||||
|
||||
-- true, if the forceloaded blocks got changed (flag for persistence on-disk)
|
||||
local forceload_blocks_changed = false
|
||||
|
||||
local BLOCKSIZE = core.MAP_BLOCKSIZE
|
||||
local function get_blockpos(pos)
|
||||
return {
|
||||
x = math.floor(pos.x/BLOCKSIZE),
|
||||
y = math.floor(pos.y/BLOCKSIZE),
|
||||
z = math.floor(pos.z/BLOCKSIZE)}
|
||||
end
|
||||
|
||||
-- When we create/free a forceload, it's either transient or persistent. We want
|
||||
-- to add to/remove from the table that corresponds to the type of forceload, but
|
||||
-- we also need the other table because whether we forceload a block depends on
|
||||
-- both tables.
|
||||
-- This function returns the "primary" table we are adding to/removing from, and
|
||||
-- the other table.
|
||||
local function get_relevant_tables(transient)
|
||||
if transient then
|
||||
return blocks_temploaded, blocks_forceloaded
|
||||
else
|
||||
return blocks_forceloaded, blocks_temploaded
|
||||
end
|
||||
end
|
||||
|
||||
function core.forceload_block(pos, transient)
|
||||
-- set changed flag
|
||||
forceload_blocks_changed = true
|
||||
|
||||
local blockpos = get_blockpos(pos)
|
||||
local hash = core.hash_node_position(blockpos)
|
||||
local relevant_table, other_table = get_relevant_tables(transient)
|
||||
if relevant_table[hash] ~= nil then
|
||||
relevant_table[hash] = relevant_table[hash] + 1
|
||||
return true
|
||||
elseif other_table[hash] ~= nil then
|
||||
relevant_table[hash] = 1
|
||||
else
|
||||
if total_forceloaded >= (tonumber(core.settings:get("max_forceloaded_blocks")) or 16) then
|
||||
return false
|
||||
end
|
||||
total_forceloaded = total_forceloaded+1
|
||||
relevant_table[hash] = 1
|
||||
forceload_block(blockpos)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function core.forceload_free_block(pos, transient)
|
||||
-- set changed flag
|
||||
forceload_blocks_changed = true
|
||||
|
||||
local blockpos = get_blockpos(pos)
|
||||
local hash = core.hash_node_position(blockpos)
|
||||
local relevant_table, other_table = get_relevant_tables(transient)
|
||||
if relevant_table[hash] == nil then return end
|
||||
if relevant_table[hash] > 1 then
|
||||
relevant_table[hash] = relevant_table[hash] - 1
|
||||
elseif other_table[hash] ~= nil then
|
||||
relevant_table[hash] = nil
|
||||
else
|
||||
total_forceloaded = total_forceloaded-1
|
||||
relevant_table[hash] = nil
|
||||
forceload_free_block(blockpos)
|
||||
end
|
||||
end
|
||||
|
||||
-- Keep the forceloaded areas after restart
|
||||
local wpath = core.get_worldpath()
|
||||
local function read_file(filename)
|
||||
local f = io.open(filename, "r")
|
||||
if f==nil then return {} end
|
||||
local t = f:read("*all")
|
||||
f:close()
|
||||
if t=="" or t==nil then return {} end
|
||||
return core.deserialize(t) or {}
|
||||
end
|
||||
|
||||
local function write_file(filename, table)
|
||||
local f = io.open(filename, "w")
|
||||
f:write(core.serialize(table))
|
||||
f:close()
|
||||
end
|
||||
|
||||
blocks_forceloaded = read_file(wpath.."/force_loaded.txt")
|
||||
for _, __ in pairs(blocks_forceloaded) do
|
||||
total_forceloaded = total_forceloaded + 1
|
||||
end
|
||||
|
||||
core.after(5, function()
|
||||
for hash, _ in pairs(blocks_forceloaded) do
|
||||
local blockpos = core.get_position_from_hash(hash)
|
||||
forceload_block(blockpos)
|
||||
end
|
||||
end)
|
||||
|
||||
-- persists the currently forceloaded blocks to disk
|
||||
local function persist_forceloaded_blocks()
|
||||
write_file(wpath.."/force_loaded.txt", blocks_forceloaded)
|
||||
end
|
||||
|
||||
-- periodical forceload persistence
|
||||
local function periodically_persist_forceloaded_blocks()
|
||||
|
||||
-- only persist if the blocks actually changed
|
||||
if forceload_blocks_changed then
|
||||
persist_forceloaded_blocks()
|
||||
|
||||
-- reset changed flag
|
||||
forceload_blocks_changed = false
|
||||
end
|
||||
|
||||
-- recheck after some time
|
||||
core.after(10, periodically_persist_forceloaded_blocks)
|
||||
end
|
||||
|
||||
-- persist periodically
|
||||
core.after(5, periodically_persist_forceloaded_blocks)
|
||||
|
||||
-- persist on shutdown
|
||||
core.register_on_shutdown(persist_forceloaded_blocks)
|
38
builtin/game/init.lua
Normal file
38
builtin/game/init.lua
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
local scriptpath = core.get_builtin_path()
|
||||
local commonpath = scriptpath .. "common" .. DIR_DELIM
|
||||
local gamepath = scriptpath .. "game".. DIR_DELIM
|
||||
|
||||
-- Shared between builtin files, but
|
||||
-- not exposed to outer context
|
||||
local builtin_shared = {}
|
||||
|
||||
dofile(commonpath .. "vector.lua")
|
||||
|
||||
dofile(gamepath .. "constants.lua")
|
||||
assert(loadfile(gamepath .. "item.lua"))(builtin_shared)
|
||||
dofile(gamepath .. "register.lua")
|
||||
|
||||
if core.settings:get_bool("profiler.load") then
|
||||
profiler = dofile(scriptpath .. "profiler" .. DIR_DELIM .. "init.lua")
|
||||
end
|
||||
|
||||
dofile(commonpath .. "after.lua")
|
||||
dofile(gamepath .. "item_entity.lua")
|
||||
dofile(gamepath .. "deprecated.lua")
|
||||
dofile(gamepath .. "misc.lua")
|
||||
dofile(gamepath .. "privileges.lua")
|
||||
dofile(gamepath .. "auth.lua")
|
||||
dofile(commonpath .. "chatcommands.lua")
|
||||
dofile(gamepath .. "chat.lua")
|
||||
dofile(commonpath .. "information_formspecs.lua")
|
||||
dofile(gamepath .. "static_spawn.lua")
|
||||
dofile(gamepath .. "detached_inventory.lua")
|
||||
assert(loadfile(gamepath .. "falling.lua"))(builtin_shared)
|
||||
dofile(gamepath .. "features.lua")
|
||||
dofile(gamepath .. "voxelarea.lua")
|
||||
dofile(gamepath .. "forceloading.lua")
|
||||
dofile(gamepath .. "statbars.lua")
|
||||
dofile(gamepath .. "knockback.lua")
|
||||
|
||||
profiler = nil
|
788
builtin/game/item.lua
Normal file
788
builtin/game/item.lua
Normal file
@ -0,0 +1,788 @@
|
||||
-- Minetest: builtin/item.lua
|
||||
|
||||
local builtin_shared = ...
|
||||
|
||||
local function copy_pointed_thing(pointed_thing)
|
||||
return {
|
||||
type = pointed_thing.type,
|
||||
above = vector.new(pointed_thing.above),
|
||||
under = vector.new(pointed_thing.under),
|
||||
ref = pointed_thing.ref,
|
||||
}
|
||||
end
|
||||
|
||||
--
|
||||
-- Item definition helpers
|
||||
--
|
||||
|
||||
function core.inventorycube(img1, img2, img3)
|
||||
img2 = img2 or img1
|
||||
img3 = img3 or img1
|
||||
return "[inventorycube"
|
||||
.. "{" .. img1:gsub("%^", "&")
|
||||
.. "{" .. img2:gsub("%^", "&")
|
||||
.. "{" .. img3:gsub("%^", "&")
|
||||
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
|
||||
|
||||
--from above
|
||||
if dir.y < 0 then
|
||||
if math.abs(dir.x) > math.abs(dir.z) then
|
||||
if dir.x < 0 then
|
||||
return 19
|
||||
else
|
||||
return 13
|
||||
end
|
||||
else
|
||||
if dir.z < 0 then
|
||||
return 10
|
||||
else
|
||||
return 4
|
||||
end
|
||||
end
|
||||
|
||||
--from below
|
||||
else
|
||||
if math.abs(dir.x) > math.abs(dir.z) then
|
||||
if dir.x < 0 then
|
||||
return 15
|
||||
else
|
||||
return 17
|
||||
end
|
||||
else
|
||||
if dir.z < 0 then
|
||||
return 6
|
||||
else
|
||||
return 8
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--otherwise, place horizontally
|
||||
elseif math.abs(dir.x) > math.abs(dir.z) then
|
||||
if dir.x < 0 then
|
||||
return 3
|
||||
else
|
||||
return 1
|
||||
end
|
||||
else
|
||||
if dir.z < 0 then
|
||||
return 2
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Table of possible dirs
|
||||
local facedir_to_dir = {
|
||||
{x= 0, y=0, z= 1},
|
||||
{x= 1, y=0, z= 0},
|
||||
{x= 0, y=0, z=-1},
|
||||
{x=-1, y=0, z= 0},
|
||||
{x= 0, y=-1, z= 0},
|
||||
{x= 0, y=1, z= 0},
|
||||
}
|
||||
-- Mapping from facedir value to index in facedir_to_dir.
|
||||
local facedir_to_dir_map = {
|
||||
[0]=1, 2, 3, 4,
|
||||
5, 2, 6, 4,
|
||||
6, 2, 5, 4,
|
||||
1, 5, 3, 6,
|
||||
1, 6, 3, 5,
|
||||
1, 4, 3, 2,
|
||||
}
|
||||
function core.facedir_to_dir(facedir)
|
||||
return facedir_to_dir[facedir_to_dir_map[facedir % 32]]
|
||||
end
|
||||
|
||||
function core.dir_to_wallmounted(dir)
|
||||
if math.abs(dir.y) > math.max(math.abs(dir.x), math.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
|
||||
if dir.x < 0 then
|
||||
return 3
|
||||
else
|
||||
return 2
|
||||
end
|
||||
else
|
||||
if dir.z < 0 then
|
||||
return 5
|
||||
else
|
||||
return 4
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- table of dirs in wallmounted order
|
||||
local wallmounted_to_dir = {
|
||||
[0] = {x = 0, y = 1, z = 0},
|
||||
{x = 0, y = -1, z = 0},
|
||||
{x = 1, y = 0, z = 0},
|
||||
{x = -1, y = 0, z = 0},
|
||||
{x = 0, y = 0, z = 1},
|
||||
{x = 0, y = 0, z = -1},
|
||||
}
|
||||
function core.wallmounted_to_dir(wallmounted)
|
||||
return wallmounted_to_dir[wallmounted % 8]
|
||||
end
|
||||
|
||||
function core.dir_to_yaw(dir)
|
||||
return -math.atan2(dir.x, dir.z)
|
||||
end
|
||||
|
||||
function core.yaw_to_dir(yaw)
|
||||
return {x = -math.sin(yaw), y = 0, z = math.cos(yaw)}
|
||||
end
|
||||
|
||||
function core.is_colored_paramtype(ptype)
|
||||
return (ptype == "color") or (ptype == "colorfacedir") or
|
||||
(ptype == "colorwallmounted")
|
||||
end
|
||||
|
||||
function core.strip_param2_color(param2, paramtype2)
|
||||
if not core.is_colored_paramtype(paramtype2) then
|
||||
return nil
|
||||
end
|
||||
if paramtype2 == "colorfacedir" then
|
||||
param2 = math.floor(param2 / 32) * 32
|
||||
elseif paramtype2 == "colorwallmounted" then
|
||||
param2 = math.floor(param2 / 8) * 8
|
||||
end
|
||||
-- paramtype2 == "color" requires no modification.
|
||||
return param2
|
||||
end
|
||||
|
||||
function core.get_node_drops(node, toolname)
|
||||
-- Compatibility, if node is string
|
||||
local nodename = node
|
||||
local param2 = 0
|
||||
-- New format, if node is table
|
||||
if (type(node) == "table") then
|
||||
nodename = node.name
|
||||
param2 = node.param2
|
||||
end
|
||||
local def = core.registered_nodes[nodename]
|
||||
local drop = def and def.drop
|
||||
local ptype = def and def.paramtype2
|
||||
-- get color, if there is color (otherwise nil)
|
||||
local palette_index = core.strip_param2_color(param2, ptype)
|
||||
if drop == nil then
|
||||
-- default drop
|
||||
if palette_index then
|
||||
local stack = ItemStack(nodename)
|
||||
stack:get_meta():set_int("palette_index", palette_index)
|
||||
return {stack:to_string()}
|
||||
end
|
||||
return {nodename}
|
||||
elseif type(drop) == "string" then
|
||||
-- itemstring drop
|
||||
return drop ~= "" and {drop} or {}
|
||||
elseif drop.items == nil then
|
||||
-- drop = {} to disable default drop
|
||||
return {}
|
||||
end
|
||||
|
||||
-- Extended drop table
|
||||
local got_items = {}
|
||||
local got_count = 0
|
||||
for _, item in ipairs(drop.items) do
|
||||
local good_rarity = true
|
||||
local good_tool = true
|
||||
if item.rarity ~= nil then
|
||||
good_rarity = item.rarity < 1 or math.random(item.rarity) == 1
|
||||
end
|
||||
if item.tools ~= nil then
|
||||
good_tool = false
|
||||
end
|
||||
if item.tools ~= nil and toolname then
|
||||
for _, tool in ipairs(item.tools) do
|
||||
if tool:sub(1, 1) == '~' then
|
||||
good_tool = toolname:find(tool:sub(2)) ~= nil
|
||||
else
|
||||
good_tool = toolname == tool
|
||||
end
|
||||
if good_tool then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if good_rarity and good_tool then
|
||||
got_count = got_count + 1
|
||||
for _, add_item in ipairs(item.items) do
|
||||
-- add color, if necessary
|
||||
if item.inherit_color and palette_index then
|
||||
local stack = ItemStack(add_item)
|
||||
stack:get_meta():set_int("palette_index", palette_index)
|
||||
add_item = stack:to_string()
|
||||
end
|
||||
got_items[#got_items+1] = add_item
|
||||
end
|
||||
if drop.max_items ~= nil and got_count == drop.max_items then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return got_items
|
||||
end
|
||||
|
||||
local function user_name(user)
|
||||
return user and user:get_player_name() or ""
|
||||
end
|
||||
|
||||
-- Returns a logging function. For empty names, does not log.
|
||||
local function make_log(name)
|
||||
return name ~= "" and core.log or function() end
|
||||
end
|
||||
|
||||
function core.item_place_node(itemstack, placer, pointed_thing, param2,
|
||||
prevent_after_place)
|
||||
local def = itemstack:get_definition()
|
||||
if def.type ~= "node" or pointed_thing.type ~= "node" then
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
local under = pointed_thing.under
|
||||
local oldnode_under = core.get_node_or_nil(under)
|
||||
local above = pointed_thing.above
|
||||
local oldnode_above = core.get_node_or_nil(above)
|
||||
local playername = user_name(placer)
|
||||
local log = make_log(playername)
|
||||
|
||||
if not oldnode_under or not oldnode_above then
|
||||
log("info", playername .. " tried to place"
|
||||
.. " node in unloaded position " .. core.pos_to_string(above))
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
local olddef_under = core.registered_nodes[oldnode_under.name]
|
||||
olddef_under = olddef_under or core.nodedef_default
|
||||
local olddef_above = core.registered_nodes[oldnode_above.name]
|
||||
olddef_above = olddef_above or core.nodedef_default
|
||||
|
||||
if not olddef_above.buildable_to and not olddef_under.buildable_to then
|
||||
log("info", playername .. " tried to place"
|
||||
.. " node in invalid position " .. core.pos_to_string(above)
|
||||
.. ", replacing " .. oldnode_above.name)
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
-- Place above pointed node
|
||||
local place_to = {x = above.x, y = above.y, z = above.z}
|
||||
|
||||
-- If node under is buildable_to, place into it instead (eg. snow)
|
||||
if olddef_under.buildable_to then
|
||||
log("info", "node under is buildable to")
|
||||
place_to = {x = under.x, y = under.y, z = under.z}
|
||||
end
|
||||
|
||||
if core.is_protected(place_to, playername) then
|
||||
log("action", playername
|
||||
.. " tried to place " .. def.name
|
||||
.. " at protected position "
|
||||
.. core.pos_to_string(place_to))
|
||||
core.record_protection_violation(place_to, playername)
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
local oldnode = core.get_node(place_to)
|
||||
local newnode = {name = def.name, param1 = 0, param2 = param2 or 0}
|
||||
|
||||
-- Calculate direction for wall mounted stuff like torches and signs
|
||||
if def.place_param2 ~= nil then
|
||||
newnode.param2 = def.place_param2
|
||||
elseif (def.paramtype2 == "wallmounted" or
|
||||
def.paramtype2 == "colorwallmounted") and not param2 then
|
||||
local dir = {
|
||||
x = under.x - above.x,
|
||||
y = under.y - above.y,
|
||||
z = under.z - above.z
|
||||
}
|
||||
newnode.param2 = core.dir_to_wallmounted(dir)
|
||||
-- Calculate the direction for furnaces and chests and stuff
|
||||
elseif (def.paramtype2 == "facedir" or
|
||||
def.paramtype2 == "colorfacedir") and not param2 then
|
||||
local placer_pos = placer and placer:get_pos()
|
||||
if placer_pos then
|
||||
local dir = {
|
||||
x = above.x - placer_pos.x,
|
||||
y = above.y - placer_pos.y,
|
||||
z = above.z - placer_pos.z
|
||||
}
|
||||
newnode.param2 = core.dir_to_facedir(dir)
|
||||
log("info", "facedir: " .. newnode.param2)
|
||||
end
|
||||
end
|
||||
|
||||
local metatable = itemstack:get_meta():to_table().fields
|
||||
|
||||
-- Transfer color information
|
||||
if metatable.palette_index and not def.place_param2 then
|
||||
local color_divisor = nil
|
||||
if def.paramtype2 == "color" then
|
||||
color_divisor = 1
|
||||
elseif def.paramtype2 == "colorwallmounted" then
|
||||
color_divisor = 8
|
||||
elseif def.paramtype2 == "colorfacedir" then
|
||||
color_divisor = 32
|
||||
end
|
||||
if color_divisor then
|
||||
local color = math.floor(metatable.palette_index / color_divisor)
|
||||
local other = newnode.param2 % color_divisor
|
||||
newnode.param2 = color * color_divisor + other
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if the node is attached and if it can be placed there
|
||||
if core.get_item_group(def.name, "attached_node") ~= 0 and
|
||||
not builtin_shared.check_attached_node(place_to, newnode) then
|
||||
log("action", "attached node " .. def.name ..
|
||||
" can not be placed at " .. core.pos_to_string(place_to))
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
log("action", playername .. " places node "
|
||||
.. def.name .. " at " .. core.pos_to_string(place_to))
|
||||
|
||||
-- Add node and update
|
||||
core.add_node(place_to, newnode)
|
||||
|
||||
-- Play sound if it was done by a player
|
||||
if playername ~= "" and def.sounds and def.sounds.place then
|
||||
core.sound_play(def.sounds.place, {
|
||||
pos = place_to,
|
||||
exclude_player = playername,
|
||||
}, true)
|
||||
end
|
||||
|
||||
local take_item = true
|
||||
|
||||
-- Run callback
|
||||
if def.after_place_node and not prevent_after_place then
|
||||
-- Deepcopy place_to and pointed_thing because callback can modify it
|
||||
local place_to_copy = {x=place_to.x, y=place_to.y, z=place_to.z}
|
||||
local pointed_thing_copy = copy_pointed_thing(pointed_thing)
|
||||
if def.after_place_node(place_to_copy, placer, itemstack,
|
||||
pointed_thing_copy) then
|
||||
take_item = false
|
||||
end
|
||||
end
|
||||
|
||||
-- Run script hook
|
||||
for _, callback in ipairs(core.registered_on_placenodes) do
|
||||
-- Deepcopy pos, node and pointed_thing because callback can modify them
|
||||
local place_to_copy = {x=place_to.x, y=place_to.y, z=place_to.z}
|
||||
local newnode_copy = {name=newnode.name, param1=newnode.param1, param2=newnode.param2}
|
||||
local oldnode_copy = {name=oldnode.name, param1=oldnode.param1, param2=oldnode.param2}
|
||||
local pointed_thing_copy = copy_pointed_thing(pointed_thing)
|
||||
if callback(place_to_copy, newnode_copy, placer, oldnode_copy, itemstack, pointed_thing_copy) then
|
||||
take_item = false
|
||||
end
|
||||
end
|
||||
|
||||
if take_item then
|
||||
itemstack:take_item()
|
||||
end
|
||||
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
|
||||
not placer:get_player_control().sneak then
|
||||
local n = core.get_node(pointed_thing.under)
|
||||
local nn = n.name
|
||||
if core.registered_nodes[nn] and core.registered_nodes[nn].on_rightclick then
|
||||
return core.registered_nodes[nn].on_rightclick(pointed_thing.under, n,
|
||||
placer, itemstack, pointed_thing) or itemstack, nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Place if node, otherwise do nothing
|
||||
if itemstack:get_definition().type == "node" then
|
||||
return core.item_place_node(itemstack, placer, pointed_thing, param2)
|
||||
end
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
function core.item_secondary_use(itemstack, placer)
|
||||
return itemstack
|
||||
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 dropper_is_player then
|
||||
p.y = p.y + 1.2
|
||||
end
|
||||
local item = itemstack:take_item(cnt)
|
||||
local obj = core.add_item(p, item)
|
||||
if obj then
|
||||
if dropper_is_player then
|
||||
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
|
||||
obj:set_velocity(dir)
|
||||
obj:get_luaentity().dropped_by = dropper:get_player_name()
|
||||
end
|
||||
return itemstack
|
||||
end
|
||||
-- If we reach this, adding the object to the
|
||||
-- environment failed
|
||||
end
|
||||
|
||||
function core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing)
|
||||
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
|
||||
return result
|
||||
end
|
||||
end
|
||||
local def = itemstack:get_definition()
|
||||
if itemstack:take_item() ~= nil then
|
||||
user:set_hp(user:get_hp() + hp_change)
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
if replace_with_item then
|
||||
if itemstack:is_empty() then
|
||||
itemstack:add_item(replace_with_item)
|
||||
else
|
||||
local inv = user:get_inventory()
|
||||
-- Check if inv is null, since non-players don't have one
|
||||
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)
|
||||
core.add_item(pos, replace_with_item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return itemstack
|
||||
end
|
||||
|
||||
function core.item_eat(hp_change, replace_with_item)
|
||||
return function(itemstack, user, pointed_thing) -- closure
|
||||
if user then
|
||||
return core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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 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)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
give_item = function(item)
|
||||
return inv:add_item("main", item)
|
||||
end
|
||||
else
|
||||
give_item = function(item)
|
||||
-- itemstring to ItemStack for left:is_empty()
|
||||
return ItemStack(item)
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function core.node_dig(pos, node, digger)
|
||||
local diggername = user_name(digger)
|
||||
local log = make_log(diggername)
|
||||
local def = core.registered_nodes[node.name]
|
||||
-- Copy pos because the callback could modify it
|
||||
if def and (not def.diggable or
|
||||
(def.can_dig and not def.can_dig(vector.new(pos), digger))) then
|
||||
log("info", diggername .. " tried to dig "
|
||||
.. node.name .. " which is not diggable "
|
||||
.. core.pos_to_string(pos))
|
||||
return
|
||||
end
|
||||
|
||||
if core.is_protected(pos, diggername) then
|
||||
log("action", diggername
|
||||
.. " tried to dig " .. node.name
|
||||
.. " at protected position "
|
||||
.. core.pos_to_string(pos))
|
||||
core.record_protection_violation(pos, diggername)
|
||||
return
|
||||
end
|
||||
|
||||
log('action', diggername .. " digs "
|
||||
.. node.name .. " at " .. core.pos_to_string(pos))
|
||||
|
||||
local wielded = digger and digger:get_wielded_item()
|
||||
local drops = core.get_node_drops(node, wielded and wielded:get_name())
|
||||
|
||||
if wielded then
|
||||
local wdef = wielded:get_definition()
|
||||
local tp = wielded:get_tool_capabilities()
|
||||
local dp = core.get_dig_params(def and def.groups, tp)
|
||||
if wdef and wdef.after_use then
|
||||
wielded = wdef.after_use(wielded, digger, node, dp) or wielded
|
||||
else
|
||||
-- Wear out tool
|
||||
if not core.is_creative_enabled(diggername) then
|
||||
wielded:add_wear(dp.wear)
|
||||
if wielded:get_count() == 0 and wdef.sound and wdef.sound.breaks then
|
||||
core.sound_play(wdef.sound.breaks, {
|
||||
pos = pos,
|
||||
gain = 0.5
|
||||
}, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
digger:set_wielded_item(wielded)
|
||||
end
|
||||
|
||||
-- Check to see if metadata should be preserved.
|
||||
if def and def.preserve_metadata then
|
||||
local oldmeta = core.get_meta(pos):to_table().fields
|
||||
-- Copy pos and node because the callback can modify them.
|
||||
local pos_copy = {x=pos.x, y=pos.y, z=pos.z}
|
||||
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
|
||||
local drop_stacks = {}
|
||||
for k, v in pairs(drops) do
|
||||
drop_stacks[k] = ItemStack(v)
|
||||
end
|
||||
drops = drop_stacks
|
||||
def.preserve_metadata(pos_copy, node_copy, oldmeta, drops)
|
||||
end
|
||||
|
||||
-- Handle drops
|
||||
core.handle_node_drops(pos, drops, digger)
|
||||
|
||||
local oldmetadata = nil
|
||||
if def and def.after_dig_node then
|
||||
oldmetadata = core.get_meta(pos):to_table()
|
||||
end
|
||||
|
||||
-- Remove node and update
|
||||
core.remove_node(pos)
|
||||
|
||||
-- Play sound if it was done by a player
|
||||
if diggername ~= "" and def and def.sounds and def.sounds.dug then
|
||||
core.sound_play(def.sounds.dug, {
|
||||
pos = pos,
|
||||
exclude_player = diggername,
|
||||
}, true)
|
||||
end
|
||||
|
||||
-- Run callback
|
||||
if def and def.after_dig_node then
|
||||
-- Copy pos and node because callback can modify them
|
||||
local pos_copy = {x=pos.x, y=pos.y, z=pos.z}
|
||||
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
|
||||
def.after_dig_node(pos_copy, node_copy, oldmetadata, digger)
|
||||
end
|
||||
|
||||
-- Run script hook
|
||||
for _, callback in ipairs(core.registered_on_dignodes) do
|
||||
local origin = core.callback_origins[callback]
|
||||
if origin then
|
||||
core.set_last_run_mod(origin.mod)
|
||||
end
|
||||
|
||||
-- Copy pos and node because callback can modify them
|
||||
local pos_copy = {x=pos.x, y=pos.y, z=pos.z}
|
||||
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
|
||||
callback(pos_copy, node_copy, digger)
|
||||
end
|
||||
end
|
||||
|
||||
function core.itemstring_with_palette(item, palette_index)
|
||||
local stack = ItemStack(item) -- convert to ItemStack
|
||||
stack:get_meta():set_int("palette_index", palette_index)
|
||||
return stack:to_string()
|
||||
end
|
||||
|
||||
function core.itemstring_with_color(item, colorstring)
|
||||
local stack = ItemStack(item) -- convert to ItemStack
|
||||
stack:get_meta():set_string("color", colorstring)
|
||||
return stack:to_string()
|
||||
end
|
||||
|
||||
-- This is used to allow mods to redefine core.item_place and so on
|
||||
-- NOTE: This is not the preferred way. Preferred way is to provide enough
|
||||
-- callbacks to not require redefining global functions. -celeron55
|
||||
local function redef_wrapper(table, name)
|
||||
return function(...)
|
||||
return table[name](...)
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Item definition defaults
|
||||
--
|
||||
|
||||
local default_stack_max = tonumber(minetest.settings:get("default_stack_max")) or 99
|
||||
|
||||
core.nodedef_default = {
|
||||
-- Item properties
|
||||
type="node",
|
||||
-- name intentionally not defined here
|
||||
description = "",
|
||||
groups = {},
|
||||
inventory_image = "",
|
||||
wield_image = "",
|
||||
wield_scale = {x=1,y=1,z=1},
|
||||
stack_max = default_stack_max,
|
||||
usable = false,
|
||||
liquids_pointable = false,
|
||||
tool_capabilities = nil,
|
||||
node_placement_prediction = nil,
|
||||
|
||||
-- Interaction callbacks
|
||||
on_place = redef_wrapper(core, 'item_place'), -- core.item_place
|
||||
on_drop = redef_wrapper(core, 'item_drop'), -- core.item_drop
|
||||
on_use = nil,
|
||||
can_dig = nil,
|
||||
|
||||
on_punch = redef_wrapper(core, 'node_punch'), -- core.node_punch
|
||||
on_rightclick = nil,
|
||||
on_dig = redef_wrapper(core, 'node_dig'), -- core.node_dig
|
||||
|
||||
on_receive_fields = nil,
|
||||
|
||||
on_metadata_inventory_move = core.node_metadata_inventory_move_allow_all,
|
||||
on_metadata_inventory_offer = core.node_metadata_inventory_offer_allow_all,
|
||||
on_metadata_inventory_take = core.node_metadata_inventory_take_allow_all,
|
||||
|
||||
-- Node properties
|
||||
drawtype = "normal",
|
||||
visual_scale = 1.0,
|
||||
-- Don't define these because otherwise the old tile_images and
|
||||
-- special_materials wouldn't be read
|
||||
--tiles ={""},
|
||||
--special_tiles = {
|
||||
-- {name="", backface_culling=true},
|
||||
-- {name="", backface_culling=true},
|
||||
--},
|
||||
alpha = 255,
|
||||
post_effect_color = {a=0, r=0, g=0, b=0},
|
||||
paramtype = "none",
|
||||
paramtype2 = "none",
|
||||
is_ground_content = true,
|
||||
sunlight_propagates = false,
|
||||
walkable = true,
|
||||
pointable = true,
|
||||
diggable = true,
|
||||
climbable = false,
|
||||
buildable_to = false,
|
||||
floodable = false,
|
||||
liquidtype = "none",
|
||||
liquid_alternative_flowing = "",
|
||||
liquid_alternative_source = "",
|
||||
liquid_viscosity = 0,
|
||||
drowning = 0,
|
||||
light_source = 0,
|
||||
damage_per_second = 0,
|
||||
selection_box = {type="regular"},
|
||||
legacy_facedir_simple = false,
|
||||
legacy_wallmounted = false,
|
||||
}
|
||||
|
||||
core.craftitemdef_default = {
|
||||
type="craft",
|
||||
-- name intentionally not defined here
|
||||
description = "",
|
||||
groups = {},
|
||||
inventory_image = "",
|
||||
wield_image = "",
|
||||
wield_scale = {x=1,y=1,z=1},
|
||||
stack_max = default_stack_max,
|
||||
liquids_pointable = false,
|
||||
tool_capabilities = nil,
|
||||
|
||||
-- Interaction callbacks
|
||||
on_place = redef_wrapper(core, 'item_place'), -- core.item_place
|
||||
on_drop = redef_wrapper(core, 'item_drop'), -- core.item_drop
|
||||
on_secondary_use = redef_wrapper(core, 'item_secondary_use'),
|
||||
on_use = nil,
|
||||
}
|
||||
|
||||
core.tooldef_default = {
|
||||
type="tool",
|
||||
-- name intentionally not defined here
|
||||
description = "",
|
||||
groups = {},
|
||||
inventory_image = "",
|
||||
wield_image = "",
|
||||
wield_scale = {x=1,y=1,z=1},
|
||||
stack_max = 1,
|
||||
liquids_pointable = false,
|
||||
tool_capabilities = nil,
|
||||
|
||||
-- Interaction callbacks
|
||||
on_place = redef_wrapper(core, 'item_place'), -- core.item_place
|
||||
on_secondary_use = redef_wrapper(core, 'item_secondary_use'),
|
||||
on_drop = redef_wrapper(core, 'item_drop'), -- core.item_drop
|
||||
on_use = nil,
|
||||
}
|
||||
|
||||
core.noneitemdef_default = { -- This is used for the hand and unknown items
|
||||
type="none",
|
||||
-- name intentionally not defined here
|
||||
description = "",
|
||||
groups = {},
|
||||
inventory_image = "",
|
||||
wield_image = "",
|
||||
wield_scale = {x=1,y=1,z=1},
|
||||
stack_max = default_stack_max,
|
||||
liquids_pointable = false,
|
||||
tool_capabilities = nil,
|
||||
|
||||
-- Interaction callbacks
|
||||
on_place = redef_wrapper(core, 'item_place'),
|
||||
on_secondary_use = redef_wrapper(core, 'item_secondary_use'),
|
||||
on_drop = nil,
|
||||
on_use = nil,
|
||||
}
|
328
builtin/game/item_entity.lua
Normal file
328
builtin/game/item_entity.lua
Normal file
@ -0,0 +1,328 @@
|
||||
-- Minetest: builtin/item_entity.lua
|
||||
|
||||
function core.spawn_item(pos, item)
|
||||
-- Take item in any format
|
||||
local stack = ItemStack(item)
|
||||
local obj = core.add_entity(pos, "__builtin:item")
|
||||
-- Don't use obj if it couldn't be added to the map.
|
||||
if obj then
|
||||
obj:get_luaentity():set_item(stack:to_string())
|
||||
end
|
||||
return obj
|
||||
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 gravity = tonumber(core.settings:get("movement_gravity")) or 9.81
|
||||
|
||||
|
||||
core.register_entity(":__builtin:item", {
|
||||
initial_properties = {
|
||||
hp_max = 1,
|
||||
physical = true,
|
||||
collide_with_objects = false,
|
||||
collisionbox = {-0.3, -0.3, -0.3, 0.3, 0.3, 0.3},
|
||||
visual = "wielditem",
|
||||
visual_size = {x = 0.4, y = 0.4},
|
||||
textures = {""},
|
||||
is_visible = false,
|
||||
},
|
||||
|
||||
itemstring = "",
|
||||
moving_state = true,
|
||||
physical_state = true,
|
||||
-- Item expiry
|
||||
age = 0,
|
||||
-- Pushing item out of solid nodes
|
||||
force_out = nil,
|
||||
force_out_start = nil,
|
||||
|
||||
set_item = function(self, item)
|
||||
local stack = ItemStack(item or self.itemstring)
|
||||
self.itemstring = stack:to_string()
|
||||
if self.itemstring == "" then
|
||||
-- item not yet known
|
||||
return
|
||||
end
|
||||
|
||||
-- Backwards compatibility: old clients use the texture
|
||||
-- to get the type of the 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 size = 0.2 + 0.1 * (count / max_count) ^ (1 / 3)
|
||||
local def = core.registered_items[itemname]
|
||||
local glow = def and def.light_source and
|
||||
math.floor(def.light_source / 2 + 0.5)
|
||||
|
||||
self.object:set_properties({
|
||||
is_visible = true,
|
||||
visual = "wielditem",
|
||||
textures = {itemname},
|
||||
visual_size = {x = size, y = size},
|
||||
collisionbox = {-size, -size, -size, size, size, size},
|
||||
automatic_rotate = math.pi * 0.5 * 0.2 / size,
|
||||
wield_item = self.itemstring,
|
||||
glow = glow,
|
||||
})
|
||||
|
||||
end,
|
||||
|
||||
get_staticdata = function(self)
|
||||
return core.serialize({
|
||||
itemstring = self.itemstring,
|
||||
age = self.age,
|
||||
dropped_by = self.dropped_by
|
||||
})
|
||||
end,
|
||||
|
||||
on_activate = function(self, staticdata, dtime_s)
|
||||
if string.sub(staticdata, 1, string.len("return")) == "return" then
|
||||
local data = core.deserialize(staticdata)
|
||||
if data and type(data) == "table" then
|
||||
self.itemstring = data.itemstring
|
||||
self.age = (data.age or 0) + dtime_s
|
||||
self.dropped_by = data.dropped_by
|
||||
end
|
||||
else
|
||||
self.itemstring = staticdata
|
||||
end
|
||||
self.object:set_armor_groups({immortal = 1})
|
||||
self.object:set_velocity({x = 0, y = 2, z = 0})
|
||||
self.object:set_acceleration({x = 0, y = -gravity, z = 0})
|
||||
self:set_item()
|
||||
end,
|
||||
|
||||
try_merge_with = function(self, own_stack, object, entity)
|
||||
if self.age == entity.age then
|
||||
-- Can not merge with itself
|
||||
return false
|
||||
end
|
||||
|
||||
local stack = ItemStack(entity.itemstring)
|
||||
local name = stack:get_name()
|
||||
if own_stack:get_name() ~= name or
|
||||
own_stack:get_meta() ~= stack:get_meta() or
|
||||
own_stack:get_wear() ~= stack:get_wear() or
|
||||
own_stack:get_free_space() == 0 then
|
||||
-- Can not merge different or full stack
|
||||
return false
|
||||
end
|
||||
|
||||
local count = own_stack:get_count()
|
||||
local total_count = stack:get_count() + count
|
||||
local max_count = stack:get_stack_max()
|
||||
|
||||
if total_count > max_count then
|
||||
return false
|
||||
end
|
||||
-- Merge the remote stack into this one
|
||||
|
||||
local pos = object:get_pos()
|
||||
pos.y = pos.y + ((total_count - count) / max_count) * 0.15
|
||||
self.object:move_to(pos)
|
||||
|
||||
self.age = 0 -- Handle as new entity
|
||||
own_stack:set_count(total_count)
|
||||
self:set_item(own_stack)
|
||||
|
||||
entity.itemstring = ""
|
||||
object:remove()
|
||||
return true
|
||||
end,
|
||||
|
||||
enable_physics = function(self)
|
||||
if not self.physical_state then
|
||||
self.physical_state = true
|
||||
self.object:set_properties({physical = true})
|
||||
self.object:set_velocity({x=0, y=0, z=0})
|
||||
self.object:set_acceleration({x=0, y=-gravity, z=0})
|
||||
end
|
||||
end,
|
||||
|
||||
disable_physics = function(self)
|
||||
if self.physical_state then
|
||||
self.physical_state = false
|
||||
self.object:set_properties({physical = false})
|
||||
self.object:set_velocity({x=0, y=0, z=0})
|
||||
self.object:set_acceleration({x=0, y=0, z=0})
|
||||
end
|
||||
end,
|
||||
|
||||
on_step = function(self, dtime, moveresult)
|
||||
self.age = self.age + dtime
|
||||
if time_to_live > 0 and self.age > time_to_live then
|
||||
self.itemstring = ""
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
|
||||
local pos = self.object:get_pos()
|
||||
local node = core.get_node_or_nil({
|
||||
x = pos.x,
|
||||
y = pos.y + self.object:get_properties().collisionbox[2] - 0.05,
|
||||
z = pos.z
|
||||
})
|
||||
-- Delete in 'ignore' nodes
|
||||
if node and node.name == "ignore" then
|
||||
self.itemstring = ""
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
|
||||
if self.force_out then
|
||||
-- This code runs after the entity got a push from the is_stuck code.
|
||||
-- It makes sure the entity is entirely outside the solid node
|
||||
local c = self.object:get_properties().collisionbox
|
||||
local s = self.force_out_start
|
||||
local f = self.force_out
|
||||
local ok = (f.x > 0 and pos.x + c[1] > s.x + 0.5) or
|
||||
(f.y > 0 and pos.y + c[2] > s.y + 0.5) or
|
||||
(f.z > 0 and pos.z + c[3] > s.z + 0.5) or
|
||||
(f.x < 0 and pos.x + c[4] < s.x - 0.5) or
|
||||
(f.z < 0 and pos.z + c[6] < s.z - 0.5)
|
||||
if ok then
|
||||
-- Item was successfully forced out
|
||||
self.force_out = nil
|
||||
self:enable_physics()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if not self.physical_state then
|
||||
return -- Don't do anything
|
||||
end
|
||||
|
||||
assert(moveresult,
|
||||
"Collision info missing, this is caused by an out-of-date/buggy mod or game")
|
||||
|
||||
if not moveresult.collides then
|
||||
-- future TODO: items should probably decelerate in air
|
||||
return
|
||||
end
|
||||
|
||||
-- Push item out when stuck inside solid node
|
||||
local is_stuck = false
|
||||
local snode = core.get_node_or_nil(pos)
|
||||
if snode then
|
||||
local sdef = core.registered_nodes[snode.name] or {}
|
||||
is_stuck = (sdef.walkable == nil or sdef.walkable == true)
|
||||
and (sdef.collision_box == nil or sdef.collision_box.type == "regular")
|
||||
and (sdef.node_box == nil or sdef.node_box.type == "regular")
|
||||
end
|
||||
|
||||
if is_stuck then
|
||||
local shootdir
|
||||
local order = {
|
||||
{x=1, y=0, z=0}, {x=-1, y=0, z= 0},
|
||||
{x=0, y=0, z=1}, {x= 0, y=0, z=-1},
|
||||
}
|
||||
|
||||
-- Check which one of the 4 sides is free
|
||||
for o = 1, #order do
|
||||
local cnode = core.get_node(vector.add(pos, order[o])).name
|
||||
local cdef = core.registered_nodes[cnode] or {}
|
||||
if cnode ~= "ignore" and cdef.walkable == false then
|
||||
shootdir = order[o]
|
||||
break
|
||||
end
|
||||
end
|
||||
-- If none of the 4 sides is free, check upwards
|
||||
if not shootdir then
|
||||
shootdir = {x=0, y=1, z=0}
|
||||
local cnode = core.get_node(vector.add(pos, shootdir)).name
|
||||
if cnode == "ignore" then
|
||||
shootdir = nil -- Do not push into ignore
|
||||
end
|
||||
end
|
||||
|
||||
if shootdir then
|
||||
-- Set new item moving speed accordingly
|
||||
local newv = vector.multiply(shootdir, 3)
|
||||
self:disable_physics()
|
||||
self.object:set_velocity(newv)
|
||||
|
||||
self.force_out = newv
|
||||
self.force_out_start = vector.round(pos)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
node = nil -- ground node we're colliding with
|
||||
if moveresult.touching_ground then
|
||||
for _, info in ipairs(moveresult.collisions) do
|
||||
if info.axis == "y" then
|
||||
node = core.get_node(info.node_pos)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Slide on slippery nodes
|
||||
local def = node and core.registered_nodes[node.name]
|
||||
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
|
||||
-- Horizontal deceleration
|
||||
local factor = math.min(4 / (slippery + 4) * dtime, 1)
|
||||
self.object:set_velocity({
|
||||
x = vel.x * (1 - factor),
|
||||
y = 0,
|
||||
z = vel.z * (1 - factor)
|
||||
})
|
||||
keep_movement = true
|
||||
end
|
||||
end
|
||||
|
||||
if not keep_movement then
|
||||
self.object:set_velocity({x=0, y=0, z=0})
|
||||
end
|
||||
|
||||
if self.moving_state == keep_movement then
|
||||
-- Do not update anything until the moving state changes
|
||||
return
|
||||
end
|
||||
self.moving_state = keep_movement
|
||||
|
||||
-- Only collect items if not moving
|
||||
if self.moving_state then
|
||||
return
|
||||
end
|
||||
-- Collect the items around to merge with
|
||||
local own_stack = ItemStack(self.itemstring)
|
||||
if own_stack:get_free_space() == 0 then
|
||||
return
|
||||
end
|
||||
local objects = core.get_objects_inside_radius(pos, 1.0)
|
||||
for k, obj in pairs(objects) do
|
||||
local entity = obj:get_luaentity()
|
||||
if entity and entity.name == "__builtin:item" then
|
||||
if self:try_merge_with(own_stack, obj, entity) then
|
||||
own_stack = ItemStack(self.itemstring)
|
||||
if own_stack:get_free_space() == 0 then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
on_punch = function(self, hitter)
|
||||
local inv = hitter:get_inventory()
|
||||
if inv and self.itemstring ~= "" then
|
||||
local left = inv:add_item("main", self.itemstring)
|
||||
if left and not left:is_empty() then
|
||||
self:set_item(left)
|
||||
return
|
||||
end
|
||||
end
|
||||
self.itemstring = ""
|
||||
self.object:remove()
|
||||
end,
|
||||
})
|
46
builtin/game/knockback.lua
Normal file
46
builtin/game/knockback.lua
Normal file
@ -0,0 +1,46 @@
|
||||
-- can be overriden by mods
|
||||
function core.calculate_knockback(player, hitter, time_from_last_punch, tool_capabilities, dir, distance, damage)
|
||||
if damage == 0 or player:get_armor_groups().immortal then
|
||||
return 0.0
|
||||
end
|
||||
|
||||
local m = 8
|
||||
-- solve m - m*e^(k*4) = 4 for k
|
||||
local k = -0.17328
|
||||
local res = m - m * math.exp(k * damage)
|
||||
|
||||
if distance < 2.0 then
|
||||
res = res * 1.1 -- more knockback when closer
|
||||
elseif distance > 4.0 then
|
||||
res = res * 0.9 -- less when far away
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
-- Server::handleCommand_Interact() adds eye offset to one but not the other
|
||||
-- so the direction is slightly off, calculate it ourselves
|
||||
local dir = vector.subtract(player:get_pos(), hitter:get_pos())
|
||||
local d = vector.length(dir)
|
||||
if d ~= 0.0 then
|
||||
dir = vector.divide(dir, d)
|
||||
end
|
||||
|
||||
local k = core.calculate_knockback(player, hitter, time_from_last_punch, tool_capabilities, dir, d, damage)
|
||||
|
||||
local kdir = vector.multiply(dir, k)
|
||||
if vector_absmax(kdir) < 1.0 then
|
||||
return -- barely noticeable, so don't even send
|
||||
end
|
||||
|
||||
player:add_player_velocity(kdir)
|
||||
end)
|
268
builtin/game/misc.lua
Normal file
268
builtin/game/misc.lua
Normal file
@ -0,0 +1,268 @@
|
||||
-- Minetest: builtin/misc.lua
|
||||
|
||||
--
|
||||
-- Misc. API functions
|
||||
--
|
||||
|
||||
function core.check_player_privs(name, ...)
|
||||
if core.is_player(name) then
|
||||
name = name:get_player_name()
|
||||
elseif type(name) ~= "string" then
|
||||
error("core.check_player_privs expects a player or playername as " ..
|
||||
"argument.", 2)
|
||||
end
|
||||
|
||||
local requested_privs = {...}
|
||||
local player_privs = core.get_player_privs(name)
|
||||
local missing_privileges = {}
|
||||
|
||||
if type(requested_privs[1]) == "table" then
|
||||
-- We were provided with a table like { privA = true, privB = true }.
|
||||
for priv, value in pairs(requested_privs[1]) do
|
||||
if value and not player_privs[priv] then
|
||||
missing_privileges[#missing_privileges + 1] = priv
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Only a list, we can process it directly.
|
||||
for key, priv in pairs(requested_privs) do
|
||||
if not player_privs[priv] then
|
||||
missing_privileges[#missing_privileges + 1] = priv
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #missing_privileges > 0 then
|
||||
return false, missing_privileges
|
||||
end
|
||||
|
||||
return true, ""
|
||||
end
|
||||
|
||||
|
||||
function core.send_join_message(player_name)
|
||||
if not core.is_singleplayer() then
|
||||
core.chat_send_all("*** " .. player_name .. " joined the game.")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function core.send_leave_message(player_name, timed_out)
|
||||
local announcement = "*** " .. player_name .. " left the game."
|
||||
if timed_out then
|
||||
announcement = announcement .. " (timed out)"
|
||||
end
|
||||
core.chat_send_all(announcement)
|
||||
end
|
||||
|
||||
|
||||
core.register_on_joinplayer(function(player)
|
||||
local player_name = player:get_player_name()
|
||||
if not core.is_singleplayer() then
|
||||
local status = core.get_server_status(player_name, true)
|
||||
if status and status ~= "" then
|
||||
core.chat_send_player(player_name, status)
|
||||
end
|
||||
end
|
||||
core.send_join_message(player_name)
|
||||
end)
|
||||
|
||||
|
||||
core.register_on_leaveplayer(function(player, timed_out)
|
||||
local player_name = player:get_player_name()
|
||||
core.send_leave_message(player_name, timed_out)
|
||||
end)
|
||||
|
||||
|
||||
function core.is_player(player)
|
||||
-- a table being a player is also supported because it quacks sufficiently
|
||||
-- like a player if it has the is_player function
|
||||
local t = type(player)
|
||||
return (t == "userdata" or t == "table") and
|
||||
type(player.is_player) == "function" and player:is_player()
|
||||
end
|
||||
|
||||
|
||||
function core.player_exists(name)
|
||||
return core.get_auth_handler().get_auth(name) ~= nil
|
||||
end
|
||||
|
||||
|
||||
-- Returns two position vectors representing a box of `radius` in each
|
||||
-- direction centered around the player corresponding to `player_name`
|
||||
|
||||
function core.get_player_radius_area(player_name, radius)
|
||||
local player = core.get_player_by_name(player_name)
|
||||
if player == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local p1 = player:get_pos()
|
||||
local p2 = p1
|
||||
|
||||
if radius then
|
||||
p1 = vector.subtract(p1, radius)
|
||||
p2 = vector.add(p2, radius)
|
||||
end
|
||||
|
||||
return p1, p2
|
||||
end
|
||||
|
||||
|
||||
function core.hash_node_position(pos)
|
||||
return (pos.z + 32768) * 65536 * 65536
|
||||
+ (pos.y + 32768) * 65536
|
||||
+ pos.x + 32768
|
||||
end
|
||||
|
||||
|
||||
function core.get_position_from_hash(hash)
|
||||
local pos = {}
|
||||
pos.x = (hash % 65536) - 32768
|
||||
hash = math.floor(hash / 65536)
|
||||
pos.y = (hash % 65536) - 32768
|
||||
hash = math.floor(hash / 65536)
|
||||
pos.z = (hash % 65536) - 32768
|
||||
return pos
|
||||
end
|
||||
|
||||
|
||||
function core.get_item_group(name, group)
|
||||
if not core.registered_items[name] or not
|
||||
core.registered_items[name].groups[group] then
|
||||
return 0
|
||||
end
|
||||
return core.registered_items[name].groups[group]
|
||||
end
|
||||
|
||||
|
||||
function core.get_node_group(name, group)
|
||||
core.log("deprecated", "Deprecated usage of get_node_group, use get_item_group instead")
|
||||
return core.get_item_group(name, group)
|
||||
end
|
||||
|
||||
|
||||
function core.setting_get_pos(name)
|
||||
local value = core.settings:get(name)
|
||||
if not value then
|
||||
return nil
|
||||
end
|
||||
return core.string_to_pos(value)
|
||||
end
|
||||
|
||||
|
||||
-- See l_env.cpp for the other functions
|
||||
function core.get_artificial_light(param1)
|
||||
return math.floor(param1 / 16)
|
||||
end
|
||||
|
||||
|
||||
-- To be overriden by protection mods
|
||||
|
||||
function core.is_protected(pos, name)
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function core.record_protection_violation(pos, name)
|
||||
for _, func in pairs(core.registered_on_protection_violation) do
|
||||
func(pos, name)
|
||||
end
|
||||
end
|
||||
|
||||
-- To be overridden by Creative mods
|
||||
|
||||
local creative_mode_cache = core.settings:get_bool("creative_mode")
|
||||
function core.is_creative_enabled(name)
|
||||
return creative_mode_cache
|
||||
end
|
||||
|
||||
-- Checks if specified volume intersects a protected volume
|
||||
|
||||
function core.is_area_protected(minp, maxp, player_name, interval)
|
||||
-- 'interval' is the largest allowed interval for the 3D lattice of checks.
|
||||
|
||||
-- Compute the optimal float step 'd' for each axis so that all corners and
|
||||
-- borders are checked. 'd' will be smaller or equal to 'interval'.
|
||||
-- Subtracting 1e-4 ensures that the max co-ordinate will be reached by the
|
||||
-- for loop (which might otherwise not be the case due to rounding errors).
|
||||
|
||||
-- Default to 4
|
||||
interval = interval or 4
|
||||
local d = {}
|
||||
|
||||
for _, c in pairs({"x", "y", "z"}) do
|
||||
if minp[c] > maxp[c] then
|
||||
-- Repair positions: 'minp' > 'maxp'
|
||||
local tmp = maxp[c]
|
||||
maxp[c] = minp[c]
|
||||
minp[c] = tmp
|
||||
end
|
||||
|
||||
if maxp[c] > minp[c] then
|
||||
d[c] = (maxp[c] - minp[c]) /
|
||||
math.ceil((maxp[c] - minp[c]) / interval) - 1e-4
|
||||
else
|
||||
d[c] = 1 -- Any value larger than 0 to avoid division by zero
|
||||
end
|
||||
end
|
||||
|
||||
for zf = minp.z, maxp.z, d.z do
|
||||
local z = math.floor(zf + 0.5)
|
||||
for yf = minp.y, maxp.y, d.y do
|
||||
local y = math.floor(yf + 0.5)
|
||||
for xf = minp.x, maxp.x, d.x do
|
||||
local x = math.floor(xf + 0.5)
|
||||
local pos = {x = x, y = y, z = z}
|
||||
if core.is_protected(pos, player_name) then
|
||||
return pos
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local raillike_ids = {}
|
||||
local raillike_cur_id = 0
|
||||
function core.raillike_group(name)
|
||||
local id = raillike_ids[name]
|
||||
if not id then
|
||||
raillike_cur_id = raillike_cur_id + 1
|
||||
raillike_ids[name] = raillike_cur_id
|
||||
id = raillike_cur_id
|
||||
end
|
||||
return id
|
||||
end
|
||||
|
||||
|
||||
-- HTTP callback interface
|
||||
|
||||
function core.http_add_fetch(httpenv)
|
||||
httpenv.fetch = function(req, callback)
|
||||
local handle = httpenv.fetch_async(req)
|
||||
|
||||
local function update_http_status()
|
||||
local res = httpenv.fetch_async_get(handle)
|
||||
if res.completed then
|
||||
callback(res)
|
||||
else
|
||||
core.after(0, update_http_status)
|
||||
end
|
||||
end
|
||||
core.after(0, update_http_status)
|
||||
end
|
||||
|
||||
return httpenv
|
||||
end
|
||||
|
||||
|
||||
function core.close_formspec(player_name, formname)
|
||||
return core.show_formspec(player_name, formname, "")
|
||||
end
|
||||
|
||||
|
||||
function core.cancel_shutdown_requests()
|
||||
core.request_shutdown("", false, -1)
|
||||
end
|
101
builtin/game/privileges.lua
Normal file
101
builtin/game/privileges.lua
Normal file
@ -0,0 +1,101 @@
|
||||
-- Minetest: builtin/privileges.lua
|
||||
|
||||
--
|
||||
-- Privileges
|
||||
--
|
||||
|
||||
core.registered_privileges = {}
|
||||
|
||||
function core.register_privilege(name, param)
|
||||
local function fill_defaults(def)
|
||||
if def.give_to_singleplayer == nil then
|
||||
def.give_to_singleplayer = true
|
||||
end
|
||||
if def.give_to_admin == nil then
|
||||
def.give_to_admin = def.give_to_singleplayer
|
||||
end
|
||||
if def.description == nil then
|
||||
def.description = "(no description)"
|
||||
end
|
||||
end
|
||||
local def
|
||||
if type(param) == "table" then
|
||||
def = param
|
||||
else
|
||||
def = {description = param}
|
||||
end
|
||||
fill_defaults(def)
|
||||
core.registered_privileges[name] = def
|
||||
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")
|
||||
core.register_privilege("privs", "Can modify privileges")
|
||||
|
||||
core.register_privilege("teleport", {
|
||||
description = "Can teleport self",
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("bring", {
|
||||
description = "Can teleport other players",
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("settime", {
|
||||
description = "Can set the time of day using /time",
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("server", {
|
||||
description = "Can do server maintenance stuff",
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
core.register_privilege("protection_bypass", {
|
||||
description = "Can bypass node protection in the world",
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("ban", {
|
||||
description = "Can ban and unban players",
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
core.register_privilege("kick", {
|
||||
description = "Can kick players",
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
core.register_privilege("give", {
|
||||
description = "Can use /give and /giveme",
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("password", {
|
||||
description = "Can use /setpassword and /clearpassword",
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
core.register_privilege("fly", {
|
||||
description = "Can use fly mode",
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("fast", {
|
||||
description = "Can use fast mode",
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("noclip", {
|
||||
description = "Can fly through solid nodes using noclip mode",
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("rollback", {
|
||||
description = "Can use the rollback functionality",
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("debug", {
|
||||
description = "Allows enabling various debug options that may affect gameplay",
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
|
||||
core.register_can_bypass_userlimit(function(name, ip)
|
||||
local privs = core.get_player_privs(name)
|
||||
return privs["server"] or privs["ban"] or privs["privs"] or privs["password"]
|
||||
end)
|
625
builtin/game/register.lua
Normal file
625
builtin/game/register.lua
Normal file
@ -0,0 +1,625 @@
|
||||
-- Minetest: builtin/misc_register.lua
|
||||
|
||||
--
|
||||
-- Make raw registration functions inaccessible to anyone except this file
|
||||
--
|
||||
|
||||
local register_item_raw = core.register_item_raw
|
||||
core.register_item_raw = nil
|
||||
|
||||
local unregister_item_raw = core.unregister_item_raw
|
||||
core.unregister_item_raw = nil
|
||||
|
||||
local register_alias_raw = core.register_alias_raw
|
||||
core.register_alias_raw = nil
|
||||
|
||||
--
|
||||
-- Item / entity / ABM / LBM registration functions
|
||||
--
|
||||
|
||||
core.registered_abms = {}
|
||||
core.registered_lbms = {}
|
||||
core.registered_entities = {}
|
||||
core.registered_items = {}
|
||||
core.registered_nodes = {}
|
||||
core.registered_craftitems = {}
|
||||
core.registered_tools = {}
|
||||
core.registered_aliases = {}
|
||||
|
||||
-- For tables that are indexed by item name:
|
||||
-- If table[X] does not exist, default to table[core.registered_aliases[X]]
|
||||
local alias_metatable = {
|
||||
__index = function(t, name)
|
||||
return rawget(t, core.registered_aliases[name])
|
||||
end
|
||||
}
|
||||
setmetatable(core.registered_items, alias_metatable)
|
||||
setmetatable(core.registered_nodes, alias_metatable)
|
||||
setmetatable(core.registered_craftitems, alias_metatable)
|
||||
setmetatable(core.registered_tools, alias_metatable)
|
||||
|
||||
-- These item names may not be used because they would interfere
|
||||
-- with legacy itemstrings
|
||||
local forbidden_item_names = {
|
||||
MaterialItem = true,
|
||||
MaterialItem2 = true,
|
||||
MaterialItem3 = true,
|
||||
NodeItem = true,
|
||||
node = true,
|
||||
CraftItem = true,
|
||||
craft = true,
|
||||
MBOItem = true,
|
||||
ToolItem = true,
|
||||
tool = true,
|
||||
}
|
||||
|
||||
local function check_modname_prefix(name)
|
||||
if name:sub(1,1) == ":" then
|
||||
-- If the name starts with a colon, we can skip the modname prefix
|
||||
-- mechanism.
|
||||
return name:sub(2)
|
||||
else
|
||||
-- Enforce that the name starts with the correct mod name.
|
||||
local expected_prefix = core.get_current_modname() .. ":"
|
||||
if name:sub(1, #expected_prefix) ~= expected_prefix then
|
||||
error("Name " .. name .. " does not follow naming conventions: " ..
|
||||
"\"" .. expected_prefix .. "\" or \":\" prefix required")
|
||||
end
|
||||
|
||||
-- Enforce that the name only contains letters, numbers and underscores.
|
||||
local subname = name:sub(#expected_prefix+1)
|
||||
if subname:find("[^%w_]") then
|
||||
error("Name " .. name .. " does not follow naming conventions: " ..
|
||||
"contains unallowed characters")
|
||||
end
|
||||
|
||||
return name
|
||||
end
|
||||
end
|
||||
|
||||
function core.register_abm(spec)
|
||||
-- Add to core.registered_abms
|
||||
assert(type(spec.action) == "function", "Required field 'action' of type function")
|
||||
core.registered_abms[#core.registered_abms + 1] = spec
|
||||
spec.mod_origin = core.get_current_modname() or "??"
|
||||
end
|
||||
|
||||
function core.register_lbm(spec)
|
||||
-- Add to core.registered_lbms
|
||||
check_modname_prefix(spec.name)
|
||||
assert(type(spec.action) == "function", "Required field 'action' of type function")
|
||||
core.registered_lbms[#core.registered_lbms + 1] = spec
|
||||
spec.mod_origin = core.get_current_modname() or "??"
|
||||
end
|
||||
|
||||
function core.register_entity(name, prototype)
|
||||
-- Check name
|
||||
if name == nil then
|
||||
error("Unable to register entity: Name is nil")
|
||||
end
|
||||
name = check_modname_prefix(tostring(name))
|
||||
|
||||
prototype.name = name
|
||||
prototype.__index = prototype -- so that it can be used as a metatable
|
||||
|
||||
-- Add to core.registered_entities
|
||||
core.registered_entities[name] = prototype
|
||||
prototype.mod_origin = core.get_current_modname() or "??"
|
||||
end
|
||||
|
||||
function core.register_item(name, itemdef)
|
||||
-- Check name
|
||||
if name == nil then
|
||||
error("Unable to register item: Name is nil")
|
||||
end
|
||||
name = check_modname_prefix(tostring(name))
|
||||
if forbidden_item_names[name] then
|
||||
error("Unable to register item: Name is forbidden: " .. name)
|
||||
end
|
||||
itemdef.name = name
|
||||
|
||||
-- default short_description to first line of description
|
||||
itemdef.short_description = itemdef.short_description or
|
||||
(itemdef.description or ""):gsub("\n.*","")
|
||||
|
||||
-- Apply defaults and add to registered_* table
|
||||
if itemdef.type == "node" then
|
||||
-- Use the nodebox as selection box if it's not set manually
|
||||
if itemdef.drawtype == "nodebox" and not itemdef.selection_box then
|
||||
itemdef.selection_box = itemdef.node_box
|
||||
elseif itemdef.drawtype == "fencelike" and not itemdef.selection_box then
|
||||
itemdef.selection_box = {
|
||||
type = "fixed",
|
||||
fixed = {-1/8, -1/2, -1/8, 1/8, 1/2, 1/8},
|
||||
}
|
||||
end
|
||||
if itemdef.light_source and itemdef.light_source > core.LIGHT_MAX then
|
||||
itemdef.light_source = core.LIGHT_MAX
|
||||
core.log("warning", "Node 'light_source' value exceeds maximum," ..
|
||||
" limiting to maximum: " ..name)
|
||||
end
|
||||
setmetatable(itemdef, {__index = core.nodedef_default})
|
||||
core.registered_nodes[itemdef.name] = itemdef
|
||||
elseif itemdef.type == "craft" then
|
||||
setmetatable(itemdef, {__index = core.craftitemdef_default})
|
||||
core.registered_craftitems[itemdef.name] = itemdef
|
||||
elseif itemdef.type == "tool" then
|
||||
setmetatable(itemdef, {__index = core.tooldef_default})
|
||||
core.registered_tools[itemdef.name] = itemdef
|
||||
elseif itemdef.type == "none" then
|
||||
setmetatable(itemdef, {__index = core.noneitemdef_default})
|
||||
else
|
||||
error("Unable to register item: Type is invalid: " .. dump(itemdef))
|
||||
end
|
||||
|
||||
-- Flowing liquid uses param2
|
||||
if itemdef.type == "node" and itemdef.liquidtype == "flowing" then
|
||||
itemdef.paramtype2 = "flowingliquid"
|
||||
end
|
||||
|
||||
-- BEGIN Legacy stuff
|
||||
if itemdef.cookresult_itemstring ~= nil and itemdef.cookresult_itemstring ~= "" then
|
||||
core.register_craft({
|
||||
type="cooking",
|
||||
output=itemdef.cookresult_itemstring,
|
||||
recipe=itemdef.name,
|
||||
cooktime=itemdef.furnace_cooktime
|
||||
})
|
||||
end
|
||||
if itemdef.furnace_burntime ~= nil and itemdef.furnace_burntime >= 0 then
|
||||
core.register_craft({
|
||||
type="fuel",
|
||||
recipe=itemdef.name,
|
||||
burntime=itemdef.furnace_burntime
|
||||
})
|
||||
end
|
||||
-- END Legacy stuff
|
||||
|
||||
itemdef.mod_origin = core.get_current_modname() or "??"
|
||||
|
||||
-- Disable all further modifications
|
||||
getmetatable(itemdef).__newindex = {}
|
||||
|
||||
--core.log("Registering item: " .. itemdef.name)
|
||||
core.registered_items[itemdef.name] = itemdef
|
||||
core.registered_aliases[itemdef.name] = nil
|
||||
register_item_raw(itemdef)
|
||||
end
|
||||
|
||||
function core.unregister_item(name)
|
||||
if not core.registered_items[name] then
|
||||
core.log("warning", "Not unregistering item " ..name..
|
||||
" because it doesn't exist.")
|
||||
return
|
||||
end
|
||||
-- Erase from registered_* table
|
||||
local type = core.registered_items[name].type
|
||||
if type == "node" then
|
||||
core.registered_nodes[name] = nil
|
||||
elseif type == "craft" then
|
||||
core.registered_craftitems[name] = nil
|
||||
elseif type == "tool" then
|
||||
core.registered_tools[name] = nil
|
||||
end
|
||||
core.registered_items[name] = nil
|
||||
|
||||
|
||||
unregister_item_raw(name)
|
||||
end
|
||||
|
||||
function core.register_node(name, nodedef)
|
||||
nodedef.type = "node"
|
||||
core.register_item(name, nodedef)
|
||||
end
|
||||
|
||||
function core.register_craftitem(name, craftitemdef)
|
||||
craftitemdef.type = "craft"
|
||||
|
||||
-- BEGIN Legacy stuff
|
||||
if craftitemdef.inventory_image == nil and craftitemdef.image ~= nil then
|
||||
craftitemdef.inventory_image = craftitemdef.image
|
||||
end
|
||||
-- END Legacy stuff
|
||||
|
||||
core.register_item(name, craftitemdef)
|
||||
end
|
||||
|
||||
function core.register_tool(name, tooldef)
|
||||
tooldef.type = "tool"
|
||||
tooldef.stack_max = 1
|
||||
|
||||
-- BEGIN Legacy stuff
|
||||
if tooldef.inventory_image == nil and tooldef.image ~= nil then
|
||||
tooldef.inventory_image = tooldef.image
|
||||
end
|
||||
if tooldef.tool_capabilities == nil and
|
||||
(tooldef.full_punch_interval ~= nil or
|
||||
tooldef.basetime ~= nil or
|
||||
tooldef.dt_weight ~= nil or
|
||||
tooldef.dt_crackiness ~= nil or
|
||||
tooldef.dt_crumbliness ~= nil or
|
||||
tooldef.dt_cuttability ~= nil or
|
||||
tooldef.basedurability ~= nil or
|
||||
tooldef.dd_weight ~= nil or
|
||||
tooldef.dd_crackiness ~= nil or
|
||||
tooldef.dd_crumbliness ~= nil or
|
||||
tooldef.dd_cuttability ~= nil) then
|
||||
tooldef.tool_capabilities = {
|
||||
full_punch_interval = tooldef.full_punch_interval,
|
||||
basetime = tooldef.basetime,
|
||||
dt_weight = tooldef.dt_weight,
|
||||
dt_crackiness = tooldef.dt_crackiness,
|
||||
dt_crumbliness = tooldef.dt_crumbliness,
|
||||
dt_cuttability = tooldef.dt_cuttability,
|
||||
basedurability = tooldef.basedurability,
|
||||
dd_weight = tooldef.dd_weight,
|
||||
dd_crackiness = tooldef.dd_crackiness,
|
||||
dd_crumbliness = tooldef.dd_crumbliness,
|
||||
dd_cuttability = tooldef.dd_cuttability,
|
||||
}
|
||||
end
|
||||
-- END Legacy stuff
|
||||
|
||||
-- This isn't just legacy, but more of a convenience feature
|
||||
local toolcaps = tooldef.tool_capabilities
|
||||
if toolcaps and toolcaps.punch_attack_uses == nil then
|
||||
for _, cap in pairs(toolcaps.groupcaps or {}) do
|
||||
local level = (cap.maxlevel or 0) - 1
|
||||
if (cap.uses or 0) ~= 0 and level >= 0 then
|
||||
toolcaps.punch_attack_uses = cap.uses * (3 ^ level)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
core.register_item(name, tooldef)
|
||||
end
|
||||
|
||||
function core.register_alias(name, convert_to)
|
||||
if forbidden_item_names[name] then
|
||||
error("Unable to register alias: Name is forbidden: " .. name)
|
||||
end
|
||||
if core.registered_items[name] ~= nil then
|
||||
core.log("warning", "Not registering alias, item with same name" ..
|
||||
" is already defined: " .. name .. " -> " .. convert_to)
|
||||
else
|
||||
--core.log("Registering alias: " .. name .. " -> " .. convert_to)
|
||||
core.registered_aliases[name] = convert_to
|
||||
register_alias_raw(name, convert_to)
|
||||
end
|
||||
end
|
||||
|
||||
function core.register_alias_force(name, convert_to)
|
||||
if forbidden_item_names[name] then
|
||||
error("Unable to register alias: Name is forbidden: " .. name)
|
||||
end
|
||||
if core.registered_items[name] ~= nil then
|
||||
core.unregister_item(name)
|
||||
core.log("info", "Removed item " ..name..
|
||||
" while attempting to force add an alias")
|
||||
end
|
||||
--core.log("Registering alias: " .. name .. " -> " .. convert_to)
|
||||
core.registered_aliases[name] = convert_to
|
||||
register_alias_raw(name, convert_to)
|
||||
end
|
||||
|
||||
function core.on_craft(itemstack, player, old_craft_list, craft_inv)
|
||||
for _, func in ipairs(core.registered_on_crafts) do
|
||||
itemstack = func(itemstack, player, old_craft_list, craft_inv) or itemstack
|
||||
end
|
||||
return itemstack
|
||||
end
|
||||
|
||||
function core.craft_predict(itemstack, player, old_craft_list, craft_inv)
|
||||
for _, func in ipairs(core.registered_craft_predicts) do
|
||||
itemstack = func(itemstack, player, old_craft_list, craft_inv) or itemstack
|
||||
end
|
||||
return itemstack
|
||||
end
|
||||
|
||||
-- Alias the forbidden item names to "" so they can't be
|
||||
-- created via itemstrings (e.g. /give)
|
||||
for name in pairs(forbidden_item_names) do
|
||||
core.registered_aliases[name] = ""
|
||||
register_alias_raw(name, "")
|
||||
end
|
||||
|
||||
|
||||
-- Obsolete:
|
||||
-- Aliases for core.register_alias (how ironic...)
|
||||
-- core.alias_node = core.register_alias
|
||||
-- core.alias_tool = core.register_alias
|
||||
-- core.alias_craftitem = core.register_alias
|
||||
|
||||
--
|
||||
-- Built-in node definitions. Also defined in C.
|
||||
--
|
||||
|
||||
core.register_item(":unknown", {
|
||||
type = "none",
|
||||
description = "Unknown Item",
|
||||
inventory_image = "unknown_item.png",
|
||||
on_place = core.item_place,
|
||||
on_secondary_use = core.item_secondary_use,
|
||||
on_drop = core.item_drop,
|
||||
groups = {not_in_creative_inventory=1},
|
||||
diggable = true,
|
||||
})
|
||||
|
||||
core.register_node(":air", {
|
||||
description = "Air",
|
||||
inventory_image = "air.png",
|
||||
wield_image = "air.png",
|
||||
drawtype = "airlike",
|
||||
paramtype = "light",
|
||||
sunlight_propagates = true,
|
||||
walkable = false,
|
||||
pointable = false,
|
||||
diggable = false,
|
||||
buildable_to = true,
|
||||
floodable = true,
|
||||
air_equivalent = true,
|
||||
drop = "",
|
||||
groups = {not_in_creative_inventory=1},
|
||||
})
|
||||
|
||||
core.register_node(":ignore", {
|
||||
description = "Ignore",
|
||||
inventory_image = "ignore.png",
|
||||
wield_image = "ignore.png",
|
||||
drawtype = "airlike",
|
||||
paramtype = "none",
|
||||
sunlight_propagates = false,
|
||||
walkable = false,
|
||||
pointable = false,
|
||||
diggable = false,
|
||||
buildable_to = true, -- A way to remove accidentally placed ignores
|
||||
air_equivalent = true,
|
||||
drop = "",
|
||||
groups = {not_in_creative_inventory=1},
|
||||
on_place = function(itemstack, placer, pointed_thing)
|
||||
core.chat_send_player(
|
||||
placer:get_player_name(),
|
||||
core.colorize("#FF0000",
|
||||
"You can't place 'ignore' nodes!"))
|
||||
return ""
|
||||
end,
|
||||
})
|
||||
|
||||
-- The hand (bare definition)
|
||||
core.register_item(":", {
|
||||
type = "none",
|
||||
wield_image = "wieldhand.png",
|
||||
groups = {not_in_creative_inventory=1},
|
||||
})
|
||||
|
||||
|
||||
function core.override_item(name, redefinition)
|
||||
if redefinition.name ~= nil then
|
||||
error("Attempt to redefine name of "..name.." to "..dump(redefinition.name), 2)
|
||||
end
|
||||
if redefinition.type ~= nil then
|
||||
error("Attempt to redefine type of "..name.." to "..dump(redefinition.type), 2)
|
||||
end
|
||||
local item = core.registered_items[name]
|
||||
if not item then
|
||||
error("Attempt to override non-existent item "..name, 2)
|
||||
end
|
||||
for k, v in pairs(redefinition) do
|
||||
rawset(item, k, v)
|
||||
end
|
||||
register_item_raw(item)
|
||||
end
|
||||
|
||||
|
||||
core.callback_origins = {}
|
||||
|
||||
function core.run_callbacks(callbacks, mode, ...)
|
||||
assert(type(callbacks) == "table")
|
||||
local cb_len = #callbacks
|
||||
if cb_len == 0 then
|
||||
if mode == 2 or mode == 3 then
|
||||
return true
|
||||
elseif mode == 4 or mode == 5 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
local ret = nil
|
||||
for i = 1, cb_len do
|
||||
local origin = core.callback_origins[callbacks[i]]
|
||||
if origin then
|
||||
core.set_last_run_mod(origin.mod)
|
||||
end
|
||||
local cb_ret = callbacks[i](...)
|
||||
|
||||
if mode == 0 and i == 1 then
|
||||
ret = cb_ret
|
||||
elseif mode == 1 and i == cb_len then
|
||||
ret = cb_ret
|
||||
elseif mode == 2 then
|
||||
if not cb_ret or i == 1 then
|
||||
ret = cb_ret
|
||||
end
|
||||
elseif mode == 3 then
|
||||
if cb_ret then
|
||||
return cb_ret
|
||||
end
|
||||
ret = cb_ret
|
||||
elseif mode == 4 then
|
||||
if (cb_ret and not ret) or i == 1 then
|
||||
ret = cb_ret
|
||||
end
|
||||
elseif mode == 5 and cb_ret then
|
||||
return cb_ret
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function core.run_priv_callbacks(name, priv, caller, method)
|
||||
local def = core.registered_privileges[priv]
|
||||
if not def or not def["on_" .. method] or
|
||||
not def["on_" .. method](name, caller) then
|
||||
for _, func in ipairs(core["registered_on_priv_" .. method]) do
|
||||
if not func(name, caller, priv) then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Callback registration
|
||||
--
|
||||
|
||||
local function make_registration()
|
||||
local t = {}
|
||||
local registerfunc = function(func)
|
||||
t[#t + 1] = func
|
||||
core.callback_origins[func] = {
|
||||
mod = core.get_current_modname() or "??",
|
||||
name = debug.getinfo(1, "n").name or "??"
|
||||
}
|
||||
--local origin = core.callback_origins[func]
|
||||
--print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func))
|
||||
end
|
||||
return t, registerfunc
|
||||
end
|
||||
|
||||
local function make_registration_reverse()
|
||||
local t = {}
|
||||
local registerfunc = function(func)
|
||||
table.insert(t, 1, func)
|
||||
core.callback_origins[func] = {
|
||||
mod = core.get_current_modname() or "??",
|
||||
name = debug.getinfo(1, "n").name or "??"
|
||||
}
|
||||
--local origin = core.callback_origins[func]
|
||||
--print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func))
|
||||
end
|
||||
return t, registerfunc
|
||||
end
|
||||
|
||||
local function make_registration_wrap(reg_fn_name, clear_fn_name)
|
||||
local list = {}
|
||||
|
||||
local orig_reg_fn = core[reg_fn_name]
|
||||
core[reg_fn_name] = function(def)
|
||||
local retval = orig_reg_fn(def)
|
||||
if retval ~= nil then
|
||||
if def.name ~= nil then
|
||||
list[def.name] = def
|
||||
else
|
||||
list[retval] = def
|
||||
end
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
local orig_clear_fn = core[clear_fn_name]
|
||||
core[clear_fn_name] = function()
|
||||
for k in pairs(list) do
|
||||
list[k] = nil
|
||||
end
|
||||
return orig_clear_fn()
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
local function make_wrap_deregistration(reg_fn, clear_fn, list)
|
||||
local unregister = function (key)
|
||||
if type(key) ~= "string" then
|
||||
error("key is not a string", 2)
|
||||
end
|
||||
if not list[key] then
|
||||
error("Attempt to unregister non-existent element - '" .. key .. "'", 2)
|
||||
end
|
||||
local temporary_list = table.copy(list)
|
||||
clear_fn()
|
||||
for k,v in pairs(temporary_list) do
|
||||
if key ~= k then
|
||||
reg_fn(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
return unregister
|
||||
end
|
||||
|
||||
core.registered_on_player_hpchanges = { modifiers = { }, loggers = { } }
|
||||
|
||||
function core.registered_on_player_hpchange(player, hp_change, reason)
|
||||
local last
|
||||
for i = #core.registered_on_player_hpchanges.modifiers, 1, -1 do
|
||||
local func = core.registered_on_player_hpchanges.modifiers[i]
|
||||
hp_change, last = func(player, hp_change, reason)
|
||||
if type(hp_change) ~= "number" then
|
||||
local debuginfo = debug.getinfo(func)
|
||||
error("The register_on_hp_changes function has to return a number at " ..
|
||||
debuginfo.short_src .. " line " .. debuginfo.linedefined)
|
||||
end
|
||||
if last then
|
||||
break
|
||||
end
|
||||
end
|
||||
for i, func in ipairs(core.registered_on_player_hpchanges.loggers) do
|
||||
func(player, hp_change, reason)
|
||||
end
|
||||
return hp_change
|
||||
end
|
||||
|
||||
function core.register_on_player_hpchange(func, modifier)
|
||||
if modifier then
|
||||
core.registered_on_player_hpchanges.modifiers[#core.registered_on_player_hpchanges.modifiers + 1] = func
|
||||
else
|
||||
core.registered_on_player_hpchanges.loggers[#core.registered_on_player_hpchanges.loggers + 1] = func
|
||||
end
|
||||
core.callback_origins[func] = {
|
||||
mod = core.get_current_modname() or "??",
|
||||
name = debug.getinfo(1, "n").name or "??"
|
||||
}
|
||||
end
|
||||
|
||||
core.registered_biomes = make_registration_wrap("register_biome", "clear_registered_biomes")
|
||||
core.registered_ores = make_registration_wrap("register_ore", "clear_registered_ores")
|
||||
core.registered_decorations = make_registration_wrap("register_decoration", "clear_registered_decorations")
|
||||
|
||||
core.unregister_biome = make_wrap_deregistration(core.register_biome,
|
||||
core.clear_registered_biomes, core.registered_biomes)
|
||||
|
||||
core.registered_on_chat_messages, core.register_on_chat_message = make_registration()
|
||||
core.registered_on_chatcommands, core.register_on_chatcommand = make_registration()
|
||||
core.registered_globalsteps, core.register_globalstep = make_registration()
|
||||
core.registered_playerevents, core.register_playerevent = make_registration()
|
||||
core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration()
|
||||
core.registered_on_shutdown, core.register_on_shutdown = make_registration()
|
||||
core.registered_on_punchnodes, core.register_on_punchnode = make_registration()
|
||||
core.registered_on_placenodes, core.register_on_placenode = make_registration()
|
||||
core.registered_on_dignodes, core.register_on_dignode = make_registration()
|
||||
core.registered_on_generateds, core.register_on_generated = make_registration()
|
||||
core.registered_on_newplayers, core.register_on_newplayer = make_registration()
|
||||
core.registered_on_dieplayers, core.register_on_dieplayer = make_registration()
|
||||
core.registered_on_respawnplayers, core.register_on_respawnplayer = make_registration()
|
||||
core.registered_on_prejoinplayers, core.register_on_prejoinplayer = make_registration()
|
||||
core.registered_on_joinplayers, core.register_on_joinplayer = make_registration()
|
||||
core.registered_on_leaveplayers, core.register_on_leaveplayer = make_registration()
|
||||
core.registered_on_player_receive_fields, core.register_on_player_receive_fields = make_registration_reverse()
|
||||
core.registered_on_cheats, core.register_on_cheat = make_registration()
|
||||
core.registered_on_crafts, core.register_on_craft = make_registration()
|
||||
core.registered_craft_predicts, core.register_craft_predict = make_registration()
|
||||
core.registered_on_protection_violation, core.register_on_protection_violation = make_registration()
|
||||
core.registered_on_item_eats, core.register_on_item_eat = make_registration()
|
||||
core.registered_on_punchplayers, core.register_on_punchplayer = make_registration()
|
||||
core.registered_on_priv_grant, core.register_on_priv_grant = make_registration()
|
||||
core.registered_on_priv_revoke, core.register_on_priv_revoke = make_registration()
|
||||
core.registered_on_authplayers, core.register_on_authplayer = make_registration()
|
||||
core.registered_can_bypass_userlimit, core.register_can_bypass_userlimit = make_registration()
|
||||
core.registered_on_modchannel_message, core.register_on_modchannel_message = make_registration()
|
||||
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()
|
||||
|
||||
--
|
||||
-- Compatibility for on_mapgen_init()
|
||||
--
|
||||
|
||||
core.register_on_mapgen_init = function(func) func(core.get_mapgen_params()) end
|
184
builtin/game/statbars.lua
Normal file
184
builtin/game/statbars.lua
Normal file
@ -0,0 +1,184 @@
|
||||
-- cache setting
|
||||
local enable_damage = core.settings:get_bool("enable_damage")
|
||||
|
||||
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)},
|
||||
}
|
||||
|
||||
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)},
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if name == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local flags = player:hud_get_flags()
|
||||
if not hud_ids[name] then
|
||||
hud_ids[name] = {}
|
||||
-- flags are not transmitted to client on connect, we need to make sure
|
||||
-- our current flags are transmitted by sending them actively
|
||||
player:hud_set_flags(flags)
|
||||
end
|
||||
local hud = 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
|
||||
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
|
||||
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
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
local function cleanup_builtin_statbars(player)
|
||||
local name = player:get_player_name()
|
||||
|
||||
if name == "" then
|
||||
return
|
||||
end
|
||||
|
||||
hud_ids[name] = nil
|
||||
end
|
||||
|
||||
local function player_event_handler(player,eventname)
|
||||
assert(player:is_player())
|
||||
|
||||
local name = player:get_player_name()
|
||||
|
||||
if name == "" or not hud_ids[name] then
|
||||
return
|
||||
end
|
||||
|
||||
if eventname == "health_changed" then
|
||||
update_builtin_statbars(player)
|
||||
|
||||
if hud_ids[name].id_healthbar then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if eventname == "breath_changed" then
|
||||
update_builtin_statbars(player)
|
||||
|
||||
if hud_ids[name].id_breathbar then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if eventname == "hud_changed" or eventname == "properties_changed" then
|
||||
update_builtin_statbars(player)
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function core.hud_replace_builtin(hud_name, definition)
|
||||
|
||||
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
|
||||
|
||||
for name, ids in pairs(hud_ids) do
|
||||
local player = core.get_player_by_name(name)
|
||||
if player and ids.id_breathbar then
|
||||
player:hud_remove(ids.id_breathbar)
|
||||
ids.id_breathbar = nil
|
||||
update_builtin_statbars(player)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
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)
|
23
builtin/game/static_spawn.lua
Normal file
23
builtin/game/static_spawn.lua
Normal file
@ -0,0 +1,23 @@
|
||||
-- Minetest: builtin/static_spawn.lua
|
||||
|
||||
local static_spawnpoint_string = core.settings:get("static_spawnpoint")
|
||||
if static_spawnpoint_string and
|
||||
static_spawnpoint_string ~= "" and
|
||||
not core.setting_get_pos("static_spawnpoint") then
|
||||
error('The static_spawnpoint setting is invalid: "' ..
|
||||
static_spawnpoint_string .. '"')
|
||||
end
|
||||
|
||||
local function put_player_in_spawn(player_obj)
|
||||
local static_spawnpoint = core.setting_get_pos("static_spawnpoint")
|
||||
if not static_spawnpoint then
|
||||
return false
|
||||
end
|
||||
core.log("action", "Moving " .. player_obj:get_player_name() ..
|
||||
" to static spawnpoint at " .. core.pos_to_string(static_spawnpoint))
|
||||
player_obj:set_pos(static_spawnpoint)
|
||||
return true
|
||||
end
|
||||
|
||||
core.register_on_newplayer(put_player_in_spawn)
|
||||
core.register_on_respawnplayer(put_player_in_spawn)
|
132
builtin/game/voxelarea.lua
Normal file
132
builtin/game/voxelarea.lua
Normal file
@ -0,0 +1,132 @@
|
||||
VoxelArea = {
|
||||
MinEdge = {x=1, y=1, z=1},
|
||||
MaxEdge = {x=0, y=0, z=0},
|
||||
ystride = 0,
|
||||
zstride = 0,
|
||||
}
|
||||
|
||||
function VoxelArea:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
|
||||
local e = o:getExtent()
|
||||
o.ystride = e.x
|
||||
o.zstride = e.x * e.y
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function VoxelArea:getExtent()
|
||||
local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge
|
||||
return {
|
||||
x = MaxEdge.x - MinEdge.x + 1,
|
||||
y = MaxEdge.y - MinEdge.y + 1,
|
||||
z = MaxEdge.z - MinEdge.z + 1,
|
||||
}
|
||||
end
|
||||
|
||||
function VoxelArea:getVolume()
|
||||
local e = self:getExtent()
|
||||
return e.x * e.y * e.z
|
||||
end
|
||||
|
||||
function VoxelArea:index(x, y, z)
|
||||
local MinEdge = self.MinEdge
|
||||
local i = (z - MinEdge.z) * self.zstride +
|
||||
(y - MinEdge.y) * self.ystride +
|
||||
(x - MinEdge.x) + 1
|
||||
return math.floor(i)
|
||||
end
|
||||
|
||||
function VoxelArea:indexp(p)
|
||||
local MinEdge = self.MinEdge
|
||||
local i = (p.z - MinEdge.z) * self.zstride +
|
||||
(p.y - MinEdge.y) * self.ystride +
|
||||
(p.x - MinEdge.x) + 1
|
||||
return math.floor(i)
|
||||
end
|
||||
|
||||
function VoxelArea:position(i)
|
||||
local p = {}
|
||||
local MinEdge = self.MinEdge
|
||||
|
||||
i = i - 1
|
||||
|
||||
p.z = math.floor(i / self.zstride) + MinEdge.z
|
||||
i = i % self.zstride
|
||||
|
||||
p.y = math.floor(i / self.ystride) + MinEdge.y
|
||||
i = i % self.ystride
|
||||
|
||||
p.x = math.floor(i) + MinEdge.x
|
||||
|
||||
return p
|
||||
end
|
||||
|
||||
function VoxelArea:contains(x, y, z)
|
||||
local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge
|
||||
return (x >= MinEdge.x) and (x <= MaxEdge.x) and
|
||||
(y >= MinEdge.y) and (y <= MaxEdge.y) and
|
||||
(z >= MinEdge.z) and (z <= MaxEdge.z)
|
||||
end
|
||||
|
||||
function VoxelArea:containsp(p)
|
||||
local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge
|
||||
return (p.x >= MinEdge.x) and (p.x <= MaxEdge.x) and
|
||||
(p.y >= MinEdge.y) and (p.y <= MaxEdge.y) and
|
||||
(p.z >= MinEdge.z) and (p.z <= MaxEdge.z)
|
||||
end
|
||||
|
||||
function VoxelArea:containsi(i)
|
||||
return (i >= 1) and (i <= self:getVolume())
|
||||
end
|
||||
|
||||
function VoxelArea:iter(minx, miny, minz, maxx, maxy, maxz)
|
||||
local i = self:index(minx, miny, minz) - 1
|
||||
local xrange = maxx - minx + 1
|
||||
local nextaction = i + 1 + xrange
|
||||
|
||||
local y = 0
|
||||
local yrange = maxy - miny + 1
|
||||
local yreqstride = self.ystride - xrange
|
||||
|
||||
local z = 0
|
||||
local zrange = maxz - minz + 1
|
||||
local multistride = self.zstride - ((yrange - 1) * self.ystride + xrange)
|
||||
|
||||
return function()
|
||||
-- continue i until it needs to jump
|
||||
i = i + 1
|
||||
if i ~= nextaction then
|
||||
return i
|
||||
end
|
||||
|
||||
-- continue y until maxy is exceeded
|
||||
y = y + 1
|
||||
if y ~= yrange then
|
||||
-- set i to index(minx, miny + y, minz + z) - 1
|
||||
i = i + yreqstride
|
||||
nextaction = i + xrange
|
||||
return i
|
||||
end
|
||||
|
||||
-- continue z until maxz is exceeded
|
||||
z = z + 1
|
||||
if z == zrange then
|
||||
-- cuboid finished, return nil
|
||||
return
|
||||
end
|
||||
|
||||
-- set i to index(minx, miny, minz + z) - 1
|
||||
i = i + multistride
|
||||
|
||||
y = 0
|
||||
nextaction = i + xrange
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
function VoxelArea:iterp(minp, maxp)
|
||||
return self:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z)
|
||||
end
|
52
builtin/init.lua
Normal file
52
builtin/init.lua
Normal file
@ -0,0 +1,52 @@
|
||||
--
|
||||
-- This file contains built-in stuff in Minetest implemented in Lua.
|
||||
--
|
||||
-- It is always loaded and executed after registration of the C API,
|
||||
-- before loading and running any mods.
|
||||
--
|
||||
|
||||
-- Initialize some very basic things
|
||||
function core.debug(...) core.log(table.concat({...}, "\t")) end
|
||||
if core.print then
|
||||
local core_print = core.print
|
||||
-- Override native print and use
|
||||
-- terminal if that's turned on
|
||||
function print(...)
|
||||
local n, t = select("#", ...), {...}
|
||||
for i = 1, n do
|
||||
t[i] = tostring(t[i])
|
||||
end
|
||||
core_print(table.concat(t, "\t"))
|
||||
end
|
||||
core.print = nil -- don't pollute our namespace
|
||||
end
|
||||
math.randomseed(os.time())
|
||||
minetest = core
|
||||
|
||||
-- Load other files
|
||||
local scriptdir = core.get_builtin_path()
|
||||
local gamepath = scriptdir .. "game" .. DIR_DELIM
|
||||
local clientpath = scriptdir .. "client" .. DIR_DELIM
|
||||
local commonpath = scriptdir .. "common" .. DIR_DELIM
|
||||
local asyncpath = scriptdir .. "async" .. DIR_DELIM
|
||||
|
||||
dofile(commonpath .. "strict.lua")
|
||||
dofile(commonpath .. "serialize.lua")
|
||||
dofile(commonpath .. "misc_helpers.lua")
|
||||
|
||||
if INIT == "game" then
|
||||
dofile(gamepath .. "init.lua")
|
||||
elseif INIT == "mainmenu" then
|
||||
local mm_script = core.settings:get("main_menu_script")
|
||||
if mm_script and mm_script ~= "" then
|
||||
dofile(mm_script)
|
||||
else
|
||||
dofile(core.get_mainmenu_path() .. DIR_DELIM .. "init.lua")
|
||||
end
|
||||
elseif INIT == "async" then
|
||||
dofile(asyncpath .. "init.lua")
|
||||
elseif INIT == "client" then
|
||||
dofile(clientpath .. "init.lua")
|
||||
else
|
||||
error(("Unrecognized builtin initialization type %s!"):format(tostring(INIT)))
|
||||
end
|
32
builtin/mainmenu/async_event.lua
Normal file
32
builtin/mainmenu/async_event.lua
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
core.async_jobs = {}
|
||||
|
||||
local function handle_job(jobid, serialized_retval)
|
||||
local retval = core.deserialize(serialized_retval)
|
||||
assert(type(core.async_jobs[jobid]) == "function")
|
||||
core.async_jobs[jobid](retval)
|
||||
core.async_jobs[jobid] = nil
|
||||
end
|
||||
|
||||
core.async_event_handler = handle_job
|
||||
|
||||
function core.handle_async(func, parameter, callback)
|
||||
-- Serialize function
|
||||
local serialized_func = string.dump(func)
|
||||
|
||||
assert(serialized_func ~= nil)
|
||||
|
||||
-- Serialize parameters
|
||||
local serialized_param = core.serialize(parameter)
|
||||
|
||||
if serialized_param == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
local jobid = core.do_async_callback(serialized_func, serialized_param)
|
||||
|
||||
core.async_jobs[jobid] = callback
|
||||
|
||||
return true
|
||||
end
|
||||
|
349
builtin/mainmenu/common.lua
Normal file
349
builtin/mainmenu/common.lua
Normal file
@ -0,0 +1,349 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
--------------------------------------------------------------------------------
|
||||
-- Global menu data
|
||||
--------------------------------------------------------------------------------
|
||||
menudata = {}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Local cached values
|
||||
--------------------------------------------------------------------------------
|
||||
local min_supp_proto, max_supp_proto
|
||||
|
||||
function common_update_cached_supp_proto()
|
||||
min_supp_proto = core.get_min_supp_proto()
|
||||
max_supp_proto = core.get_max_supp_proto()
|
||||
end
|
||||
common_update_cached_supp_proto()
|
||||
--------------------------------------------------------------------------------
|
||||
-- Menu helper functions
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function render_client_count(n)
|
||||
if n > 99 then return '99+'
|
||||
elseif n >= 0 then return tostring(n)
|
||||
else return '?' end
|
||||
end
|
||||
|
||||
local function configure_selected_world_params(idx)
|
||||
local worldconfig = pkgmgr.get_worldconfig(menudata.worldlist:get_list()[idx].path)
|
||||
if worldconfig.creative_mode then
|
||||
core.settings:set("creative_mode", worldconfig.creative_mode)
|
||||
end
|
||||
if worldconfig.enable_damage then
|
||||
core.settings:set("enable_damage", worldconfig.enable_damage)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function image_column(tooltip, flagname)
|
||||
return "image,tooltip=" .. core.formspec_escape(tooltip) .. "," ..
|
||||
"0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. "," ..
|
||||
"1=" .. core.formspec_escape(defaulttexturedir ..
|
||||
(flagname and "server_flags_" .. flagname .. ".png" or "blank.png")) .. "," ..
|
||||
"2=" .. core.formspec_escape(defaulttexturedir .. "server_ping_4.png") .. "," ..
|
||||
"3=" .. core.formspec_escape(defaulttexturedir .. "server_ping_3.png") .. "," ..
|
||||
"4=" .. core.formspec_escape(defaulttexturedir .. "server_ping_2.png") .. "," ..
|
||||
"5=" .. core.formspec_escape(defaulttexturedir .. "server_ping_1.png")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function order_favorite_list(list)
|
||||
local res = {}
|
||||
--orders the favorite list after support
|
||||
for i = 1, #list do
|
||||
local fav = list[i]
|
||||
if is_server_protocol_compat(fav.proto_min, fav.proto_max) then
|
||||
res[#res + 1] = fav
|
||||
end
|
||||
end
|
||||
for i = 1, #list do
|
||||
local fav = list[i]
|
||||
if not is_server_protocol_compat(fav.proto_min, fav.proto_max) then
|
||||
res[#res + 1] = fav
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function render_serverlist_row(spec, is_favorite)
|
||||
local text = ""
|
||||
if spec.name then
|
||||
text = text .. core.formspec_escape(spec.name:trim())
|
||||
elseif spec.address then
|
||||
text = text .. spec.address:trim()
|
||||
if spec.port then
|
||||
text = text .. ":" .. spec.port
|
||||
end
|
||||
end
|
||||
|
||||
local grey_out = not is_server_protocol_compat(spec.proto_min, spec.proto_max)
|
||||
|
||||
local details
|
||||
if is_favorite then
|
||||
details = "1,"
|
||||
else
|
||||
details = "0,"
|
||||
end
|
||||
|
||||
if spec.ping then
|
||||
local ping = spec.ping * 1000
|
||||
if ping <= 50 then
|
||||
details = details .. "2,"
|
||||
elseif ping <= 100 then
|
||||
details = details .. "3,"
|
||||
elseif ping <= 250 then
|
||||
details = details .. "4,"
|
||||
else
|
||||
details = details .. "5,"
|
||||
end
|
||||
else
|
||||
details = details .. "0,"
|
||||
end
|
||||
|
||||
if spec.clients and spec.clients_max then
|
||||
local clients_percent = 100 * spec.clients / spec.clients_max
|
||||
|
||||
-- Choose a color depending on how many clients are connected
|
||||
-- (relatively to clients_max)
|
||||
local clients_color
|
||||
if grey_out then clients_color = '#aaaaaa'
|
||||
elseif spec.clients == 0 then clients_color = '' -- 0 players: default/white
|
||||
elseif clients_percent <= 60 then clients_color = '#a1e587' -- 0-60%: green
|
||||
elseif clients_percent <= 90 then clients_color = '#ffdc97' -- 60-90%: yellow
|
||||
elseif clients_percent == 100 then clients_color = '#dd5b5b' -- full server: red (darker)
|
||||
else clients_color = '#ffba97' -- 90-100%: orange
|
||||
end
|
||||
|
||||
details = details .. clients_color .. ',' ..
|
||||
render_client_count(spec.clients) .. ',/,' ..
|
||||
render_client_count(spec.clients_max) .. ','
|
||||
|
||||
elseif grey_out then
|
||||
details = details .. '#aaaaaa,?,/,?,'
|
||||
else
|
||||
details = details .. ',?,/,?,'
|
||||
end
|
||||
|
||||
if spec.creative then
|
||||
details = details .. "1,"
|
||||
else
|
||||
details = details .. "0,"
|
||||
end
|
||||
|
||||
if spec.damage then
|
||||
details = details .. "1,"
|
||||
else
|
||||
details = details .. "0,"
|
||||
end
|
||||
|
||||
if spec.pvp then
|
||||
details = details .. "1,"
|
||||
else
|
||||
details = details .. "0,"
|
||||
end
|
||||
|
||||
return details .. (grey_out and '#aaaaaa,' or ',') .. text
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
os.tempfolder = function()
|
||||
if core.settings:get("TMPFolder") then
|
||||
return core.settings:get("TMPFolder") .. DIR_DELIM .. "MT_" .. math.random(0,10000)
|
||||
end
|
||||
|
||||
local filetocheck = os.tmpname()
|
||||
os.remove(filetocheck)
|
||||
|
||||
-- luacheck: ignore
|
||||
-- https://blogs.msdn.microsoft.com/vcblog/2014/06/18/c-runtime-crt-features-fixes-and-breaking-changes-in-visual-studio-14-ctp1/
|
||||
-- The C runtime (CRT) function called by os.tmpname is tmpnam.
|
||||
-- Microsofts tmpnam implementation in older CRT / MSVC releases is defective.
|
||||
-- tmpnam return values starting with a backslash characterize this behavior.
|
||||
-- https://sourceforge.net/p/mingw-w64/bugs/555/
|
||||
-- MinGW tmpnam implementation is forwarded to the CRT directly.
|
||||
-- https://sourceforge.net/p/mingw-w64/discussion/723797/thread/55520785/
|
||||
-- MinGW links to an older CRT release (msvcrt.dll).
|
||||
-- Due to legal concerns MinGW will never use a newer CRT.
|
||||
--
|
||||
-- Make use of TEMP to compose the temporary filename if an old
|
||||
-- style tmpnam return value is detected.
|
||||
if filetocheck:sub(1, 1) == "\\" then
|
||||
local tempfolder = os.getenv("TEMP")
|
||||
return tempfolder .. filetocheck
|
||||
end
|
||||
|
||||
local randname = "MTTempModFolder_" .. math.random(0,10000)
|
||||
local backstring = filetocheck:reverse()
|
||||
return filetocheck:sub(0, filetocheck:len() - backstring:find(DIR_DELIM) + 1) ..
|
||||
randname
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function menu_render_worldlist()
|
||||
local retval = ""
|
||||
local current_worldlist = menudata.worldlist:get_list()
|
||||
|
||||
for i, v in ipairs(current_worldlist) do
|
||||
if retval ~= "" then retval = retval .. "," end
|
||||
retval = retval .. core.formspec_escape(v.name) ..
|
||||
" \\[" .. core.formspec_escape(v.gameid) .. "\\]"
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function menu_handle_key_up_down(fields, textlist, settingname)
|
||||
local oldidx, newidx = core.get_textlist_index(textlist), 1
|
||||
if fields.key_up or fields.key_down then
|
||||
if fields.key_up and oldidx and oldidx > 1 then
|
||||
newidx = oldidx - 1
|
||||
elseif fields.key_down and oldidx and
|
||||
oldidx < menudata.worldlist:size() then
|
||||
newidx = oldidx + 1
|
||||
end
|
||||
core.settings:set(settingname, menudata.worldlist:get_raw_index(newidx))
|
||||
configure_selected_world_params(newidx)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function asyncOnlineFavourites()
|
||||
if not menudata.public_known then
|
||||
menudata.public_known = {{
|
||||
name = fgettext("Loading..."),
|
||||
description = fgettext_ne("Try reenabling public serverlist and check your internet connection.")
|
||||
}}
|
||||
end
|
||||
menudata.favorites = menudata.public_known
|
||||
menudata.favorites_is_public = true
|
||||
|
||||
if not menudata.public_downloading then
|
||||
menudata.public_downloading = true
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
core.handle_async(
|
||||
function(param)
|
||||
return core.get_favorites("online")
|
||||
end,
|
||||
nil,
|
||||
function(result)
|
||||
menudata.public_downloading = nil
|
||||
local favs = order_favorite_list(result)
|
||||
if favs[1] then
|
||||
menudata.public_known = favs
|
||||
menudata.favorites = menudata.public_known
|
||||
menudata.favorites_is_public = true
|
||||
end
|
||||
core.event_handler("Refresh")
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function text2textlist(xpos, ypos, width, height, tl_name, textlen, text, transparency)
|
||||
local textlines = core.wrap_text(text, textlen, true)
|
||||
local retval = "textlist[" .. xpos .. "," .. ypos .. ";" .. width ..
|
||||
"," .. height .. ";" .. tl_name .. ";"
|
||||
|
||||
for i = 1, #textlines do
|
||||
textlines[i] = textlines[i]:gsub("\r", "")
|
||||
retval = retval .. core.formspec_escape(textlines[i]) .. ","
|
||||
end
|
||||
|
||||
retval = retval .. ";0;"
|
||||
if transparency then retval = retval .. "true" end
|
||||
retval = retval .. "]"
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function is_server_protocol_compat(server_proto_min, server_proto_max)
|
||||
if (not server_proto_min) or (not server_proto_max) then
|
||||
-- There is no info. Assume the best and act as if we would be compatible.
|
||||
return true
|
||||
end
|
||||
return min_supp_proto <= server_proto_max and max_supp_proto >= server_proto_min
|
||||
end
|
||||
--------------------------------------------------------------------------------
|
||||
function is_server_protocol_compat_or_error(server_proto_min, server_proto_max)
|
||||
if not is_server_protocol_compat(server_proto_min, server_proto_max) then
|
||||
local server_prot_ver_info, client_prot_ver_info
|
||||
local s_p_min = server_proto_min
|
||||
local s_p_max = server_proto_max
|
||||
|
||||
if s_p_min ~= s_p_max then
|
||||
server_prot_ver_info = fgettext_ne("Server supports protocol versions between $1 and $2. ",
|
||||
s_p_min, s_p_max)
|
||||
else
|
||||
server_prot_ver_info = fgettext_ne("Server enforces protocol version $1. ",
|
||||
s_p_min)
|
||||
end
|
||||
if min_supp_proto ~= max_supp_proto then
|
||||
client_prot_ver_info= fgettext_ne("We support protocol versions between version $1 and $2.",
|
||||
min_supp_proto, max_supp_proto)
|
||||
else
|
||||
client_prot_ver_info = fgettext_ne("We only support protocol version $1.", min_supp_proto)
|
||||
end
|
||||
gamedata.errormessage = fgettext_ne("Protocol version mismatch. ")
|
||||
.. server_prot_ver_info
|
||||
.. client_prot_ver_info
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
--------------------------------------------------------------------------------
|
||||
function menu_worldmt(selected, setting, value)
|
||||
local world = menudata.worldlist:get_list()[selected]
|
||||
if world then
|
||||
local filename = world.path .. DIR_DELIM .. "world.mt"
|
||||
local world_conf = Settings(filename)
|
||||
|
||||
if value then
|
||||
if not world_conf:write() then
|
||||
core.log("error", "Failed to write world config file")
|
||||
end
|
||||
world_conf:set(setting, value)
|
||||
world_conf:write()
|
||||
else
|
||||
return world_conf:get(setting)
|
||||
end
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
function menu_worldmt_legacy(selected)
|
||||
local modes_names = {"creative_mode", "enable_damage", "server_announce"}
|
||||
for _, mode_name in pairs(modes_names) do
|
||||
local mode_val = menu_worldmt(selected, mode_name)
|
||||
if mode_val then
|
||||
core.settings:set(mode_name, mode_val)
|
||||
else
|
||||
menu_worldmt(selected, mode_name, core.settings:get(mode_name))
|
||||
end
|
||||
end
|
||||
end
|
306
builtin/mainmenu/dlg_config_world.lua
Normal file
306
builtin/mainmenu/dlg_config_world.lua
Normal file
@ -0,0 +1,306 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local enabled_all = false
|
||||
|
||||
local function modname_valid(name)
|
||||
return not name:find("[^a-z0-9_]")
|
||||
end
|
||||
|
||||
local function init_data(data)
|
||||
data.list = filterlist.create(
|
||||
pkgmgr.preparemodlist,
|
||||
pkgmgr.comparemod,
|
||||
function(element, uid)
|
||||
if element.name == uid then
|
||||
return true
|
||||
end
|
||||
end,
|
||||
function(element, criteria)
|
||||
if criteria.hide_game and
|
||||
element.is_game_content then
|
||||
return false
|
||||
end
|
||||
|
||||
if criteria.hide_modpackcontents and
|
||||
element.modpack ~= nil then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end,
|
||||
{
|
||||
worldpath = data.worldspec.path,
|
||||
gameid = data.worldspec.gameid
|
||||
})
|
||||
|
||||
if data.selected_mod > data.list:size() then
|
||||
data.selected_mod = 0
|
||||
end
|
||||
|
||||
data.list:set_filtercriteria({
|
||||
hide_game = data.hide_gamemods,
|
||||
hide_modpackcontents = data.hide_modpackcontents
|
||||
})
|
||||
data.list:add_sort_mechanism("alphabetic", sort_mod_list)
|
||||
data.list:set_sortmode("alphabetic")
|
||||
end
|
||||
|
||||
local function get_formspec(data)
|
||||
if not data.list then
|
||||
init_data(data)
|
||||
end
|
||||
|
||||
local mod = data.list:get_list()[data.selected_mod] or {name = ""}
|
||||
|
||||
local retval =
|
||||
"size[11.5,7.5,true]" ..
|
||||
"label[0.5,0;" .. fgettext("World:") .. "]" ..
|
||||
"label[1.75,0;" .. data.worldspec.name .. "]"
|
||||
|
||||
if mod.is_modpack or mod.type == "game" then
|
||||
local info = minetest.formspec_escape(
|
||||
core.get_content_info(mod.path).description)
|
||||
if info == "" then
|
||||
if mod.is_modpack then
|
||||
info = fgettext("No modpack description provided.")
|
||||
else
|
||||
info = fgettext("No game description provided.")
|
||||
end
|
||||
end
|
||||
retval = retval ..
|
||||
"textarea[0.25,0.7;5.75,7.2;;" .. info .. ";]"
|
||||
else
|
||||
local hard_deps, soft_deps = pkgmgr.get_dependencies(mod.path)
|
||||
local hard_deps_str = table.concat(hard_deps, ",")
|
||||
local soft_deps_str = table.concat(soft_deps, ",")
|
||||
|
||||
retval = retval ..
|
||||
"label[0,0.7;" .. fgettext("Mod:") .. "]" ..
|
||||
"label[0.75,0.7;" .. mod.name .. "]"
|
||||
|
||||
if hard_deps_str == "" then
|
||||
if soft_deps_str == "" then
|
||||
retval = retval ..
|
||||
"label[0,1.25;" ..
|
||||
fgettext("No (optional) dependencies") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"label[0,1.25;" .. fgettext("No hard dependencies") ..
|
||||
"]" ..
|
||||
"label[0,1.75;" .. fgettext("Optional dependencies:") ..
|
||||
"]" ..
|
||||
"textlist[0,2.25;5,4;world_config_optdepends;" ..
|
||||
soft_deps_str .. ";0]"
|
||||
end
|
||||
else
|
||||
if soft_deps_str == "" then
|
||||
retval = retval ..
|
||||
"label[0,1.25;" .. fgettext("Dependencies:") .. "]" ..
|
||||
"textlist[0,1.75;5,4;world_config_depends;" ..
|
||||
hard_deps_str .. ";0]" ..
|
||||
"label[0,6;" .. fgettext("No optional dependencies") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"label[0,1.25;" .. fgettext("Dependencies:") .. "]" ..
|
||||
"textlist[0,1.75;5,2.125;world_config_depends;" ..
|
||||
hard_deps_str .. ";0]" ..
|
||||
"label[0,3.9;" .. fgettext("Optional dependencies:") ..
|
||||
"]" ..
|
||||
"textlist[0,4.375;5,1.8;world_config_optdepends;" ..
|
||||
soft_deps_str .. ";0]"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
retval = retval ..
|
||||
"button[3.25,7;2.5,0.5;btn_config_world_save;" ..
|
||||
fgettext("Save") .. "]" ..
|
||||
"button[5.75,7;2.5,0.5;btn_config_world_cancel;" ..
|
||||
fgettext("Cancel") .. "]" ..
|
||||
"button[9,7;2.5,0.5;btn_config_world_cdb;" ..
|
||||
fgettext("Find More Mods") .. "]"
|
||||
|
||||
if mod.name ~= "" and not mod.is_game_content then
|
||||
if mod.is_modpack then
|
||||
|
||||
if pkgmgr.is_modpack_entirely_enabled(data, mod.name) then
|
||||
retval = retval ..
|
||||
"button[5.5,0.125;3,0.5;btn_mp_disable;" ..
|
||||
fgettext("Disable modpack") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"button[5.5,0.125;3,0.5;btn_mp_enable;" ..
|
||||
fgettext("Enable modpack") .. "]"
|
||||
end
|
||||
else
|
||||
retval = retval ..
|
||||
"checkbox[5.5,-0.125;cb_mod_enable;" .. fgettext("enabled") ..
|
||||
";" .. tostring(mod.enabled) .. "]"
|
||||
end
|
||||
end
|
||||
if enabled_all then
|
||||
retval = retval ..
|
||||
"button[8.95,0.125;2.5,0.5;btn_disable_all_mods;" ..
|
||||
fgettext("Disable all") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"button[8.95,0.125;2.5,0.5;btn_enable_all_mods;" ..
|
||||
fgettext("Enable all") .. "]"
|
||||
end
|
||||
return retval ..
|
||||
"tablecolumns[color;tree;text]" ..
|
||||
"table[5.5,0.75;5.75,6;world_config_modlist;" ..
|
||||
pkgmgr.render_packagelist(data.list) .. ";" .. data.selected_mod .."]"
|
||||
end
|
||||
|
||||
local function handle_buttons(this, fields)
|
||||
if fields.world_config_modlist then
|
||||
local event = core.explode_table_event(fields.world_config_modlist)
|
||||
this.data.selected_mod = event.row
|
||||
core.settings:set("world_config_selected_mod", event.row)
|
||||
|
||||
if event.type == "DCL" then
|
||||
pkgmgr.enable_mod(this)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.key_enter then
|
||||
pkgmgr.enable_mod(this)
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.cb_mod_enable ~= nil then
|
||||
pkgmgr.enable_mod(this, core.is_yes(fields.cb_mod_enable))
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mp_enable ~= nil or
|
||||
fields.btn_mp_disable then
|
||||
pkgmgr.enable_mod(this, fields.btn_mp_enable ~= nil)
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_config_world_save then
|
||||
local filename = this.data.worldspec.path .. DIR_DELIM .. "world.mt"
|
||||
|
||||
local worldfile = Settings(filename)
|
||||
local mods = worldfile:to_table()
|
||||
|
||||
local rawlist = this.data.list:get_raw_list()
|
||||
|
||||
for i = 1, #rawlist do
|
||||
local mod = rawlist[i]
|
||||
if not mod.is_modpack and
|
||||
not mod.is_game_content then
|
||||
if modname_valid(mod.name) then
|
||||
worldfile:set("load_mod_" .. mod.name,
|
||||
mod.enabled and "true" or "false")
|
||||
elseif mod.enabled then
|
||||
gamedata.errormessage = fgettext_ne("Failed to enable mo" ..
|
||||
"d \"$1\" as it contains disallowed characters. " ..
|
||||
"Only characters [a-z0-9_] are allowed.",
|
||||
mod.name)
|
||||
end
|
||||
mods["load_mod_" .. mod.name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove mods that are not present anymore
|
||||
for key in pairs(mods) do
|
||||
if key:sub(1, 9) == "load_mod_" then
|
||||
worldfile:remove(key)
|
||||
end
|
||||
end
|
||||
|
||||
if not worldfile:write() then
|
||||
core.log("error", "Failed to write world config file")
|
||||
end
|
||||
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_config_world_cancel then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_config_world_cdb then
|
||||
this.data.list = nil
|
||||
|
||||
local dlg = create_store_dlg("mod")
|
||||
dlg:set_parent(this)
|
||||
this:hide()
|
||||
dlg:show()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_enable_all_mods then
|
||||
local list = this.data.list:get_raw_list()
|
||||
|
||||
for i = 1, #list do
|
||||
if not list[i].is_game_content
|
||||
and not list[i].is_modpack then
|
||||
list[i].enabled = true
|
||||
end
|
||||
end
|
||||
enabled_all = true
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_disable_all_mods then
|
||||
local list = this.data.list:get_raw_list()
|
||||
|
||||
for i = 1, #list do
|
||||
if not list[i].is_game_content
|
||||
and not list[i].is_modpack then
|
||||
list[i].enabled = false
|
||||
end
|
||||
end
|
||||
enabled_all = false
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function create_configure_world_dlg(worldidx)
|
||||
local dlg = dialog_create("sp_config_world", get_formspec, handle_buttons)
|
||||
|
||||
dlg.data.selected_mod = tonumber(
|
||||
core.settings:get("world_config_selected_mod")) or 0
|
||||
|
||||
dlg.data.worldspec = core.get_worlds()[worldidx]
|
||||
if not dlg.data.worldspec then
|
||||
dlg:delete()
|
||||
return
|
||||
end
|
||||
|
||||
dlg.data.worldconfig = pkgmgr.get_worldconfig(dlg.data.worldspec.path)
|
||||
|
||||
if not dlg.data.worldconfig or not dlg.data.worldconfig.id or
|
||||
dlg.data.worldconfig.id == "" then
|
||||
dlg:delete()
|
||||
return
|
||||
end
|
||||
|
||||
return dlg
|
||||
end
|
614
builtin/mainmenu/dlg_contentstore.lua
Normal file
614
builtin/mainmenu/dlg_contentstore.lua
Normal file
@ -0,0 +1,614 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2018-20 rubenwardy
|
||||
--
|
||||
--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
|
||||
--(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 minetest.get_http_api then
|
||||
function create_store_dlg()
|
||||
return messagebox("store",
|
||||
fgettext("ContentDB is not available when Minetest was compiled without cURL"))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local store = { packages = {}, packages_full = {} }
|
||||
|
||||
local http = minetest.get_http_api()
|
||||
|
||||
-- Screenshot
|
||||
local screenshot_dir = core.get_cache_path() .. DIR_DELIM .. "cdb"
|
||||
assert(core.create_dir(screenshot_dir))
|
||||
local screenshot_downloading = {}
|
||||
local screenshot_downloaded = {}
|
||||
|
||||
-- Filter
|
||||
local search_string = ""
|
||||
local cur_page = 1
|
||||
local num_per_page = 5
|
||||
local filter_type = 1
|
||||
local filter_types_titles = {
|
||||
fgettext("All packages"),
|
||||
-- fgettext("Games"),
|
||||
fgettext("Clientmods"),
|
||||
fgettext("Texture packs"),
|
||||
}
|
||||
|
||||
local number_downloading = 0
|
||||
local download_queue = {}
|
||||
|
||||
local filter_types_type = {
|
||||
nil,
|
||||
-- "game",
|
||||
"mod",
|
||||
"txp",
|
||||
}
|
||||
|
||||
|
||||
local function download_package(param)
|
||||
if core.download_file(param.package.url, param.filename) then
|
||||
return {
|
||||
filename = param.filename,
|
||||
successful = true,
|
||||
}
|
||||
else
|
||||
core.log("error", "downloading " .. dump(param.package.url) .. " failed")
|
||||
return {
|
||||
successful = false,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local function start_install(package)
|
||||
local params = {
|
||||
package = package,
|
||||
filename = os.tempfolder() .. "_MODNAME_" .. package.name .. ".zip",
|
||||
}
|
||||
|
||||
number_downloading = number_downloading + 1
|
||||
|
||||
local function callback(result)
|
||||
if result.successful then
|
||||
local path, msg = pkgmgr.install(package.type,
|
||||
result.filename, package.name,
|
||||
package.path)
|
||||
if not path then
|
||||
gamedata.errormessage = msg
|
||||
else
|
||||
core.log("action", "Installed package to " .. path)
|
||||
|
||||
local conf_path
|
||||
local name_is_title = false
|
||||
if package.type == "mod" then
|
||||
local actual_type = pkgmgr.get_folder_type(path)
|
||||
if actual_type.type == "modpack" then
|
||||
conf_path = path .. DIR_DELIM .. "modpack.conf"
|
||||
else
|
||||
conf_path = path .. DIR_DELIM .. "mod.conf"
|
||||
end
|
||||
elseif package.type == "game" then
|
||||
conf_path = path .. DIR_DELIM .. "game.conf"
|
||||
name_is_title = true
|
||||
elseif package.type == "txp" then
|
||||
conf_path = path .. DIR_DELIM .. "texture_pack.conf"
|
||||
end
|
||||
|
||||
if conf_path then
|
||||
local conf = Settings(conf_path)
|
||||
if name_is_title then
|
||||
conf:set("name", package.title)
|
||||
else
|
||||
conf:set("title", package.title)
|
||||
conf:set("name", package.name)
|
||||
end
|
||||
if not conf:get("description") then
|
||||
conf:set("description", package.short_description)
|
||||
end
|
||||
conf:set("author", package.author)
|
||||
conf:set("release", package.release)
|
||||
conf:write()
|
||||
end
|
||||
end
|
||||
os.remove(result.filename)
|
||||
else
|
||||
gamedata.errormessage = fgettext("Failed to download $1", package.name)
|
||||
end
|
||||
|
||||
package.downloading = false
|
||||
|
||||
number_downloading = number_downloading - 1
|
||||
|
||||
local next = download_queue[1]
|
||||
if next then
|
||||
table.remove(download_queue, 1)
|
||||
|
||||
start_install(next)
|
||||
end
|
||||
|
||||
ui.update()
|
||||
end
|
||||
|
||||
package.queued = false
|
||||
package.downloading = true
|
||||
|
||||
if not core.handle_async(download_package, params, callback) then
|
||||
core.log("error", "ERROR: async event failed")
|
||||
gamedata.errormessage = fgettext("Failed to download $1", package.name)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local function queue_download(package)
|
||||
local max_concurrent_downloads = tonumber(minetest.settings:get("contentdb_max_concurrent_downloads"))
|
||||
if number_downloading < max_concurrent_downloads then
|
||||
start_install(package)
|
||||
else
|
||||
table.insert(download_queue, package)
|
||||
package.queued = true
|
||||
end
|
||||
end
|
||||
|
||||
local function get_file_extension(path)
|
||||
local parts = path:split(".")
|
||||
return parts[#parts]
|
||||
end
|
||||
|
||||
local function get_screenshot(package)
|
||||
if not package.thumbnail then
|
||||
return defaulttexturedir .. "no_screenshot.png"
|
||||
elseif screenshot_downloading[package.thumbnail] then
|
||||
return defaulttexturedir .. "loading_screenshot.png"
|
||||
end
|
||||
|
||||
-- Get tmp screenshot path
|
||||
local ext = get_file_extension(package.thumbnail)
|
||||
local filepath = screenshot_dir .. DIR_DELIM ..
|
||||
("%s-%s-%s.%s"):format(package.type, package.author, package.name, ext)
|
||||
|
||||
-- Return if already downloaded
|
||||
local file = io.open(filepath, "r")
|
||||
if file then
|
||||
file:close()
|
||||
return filepath
|
||||
end
|
||||
|
||||
-- Show error if we've failed to download before
|
||||
if screenshot_downloaded[package.thumbnail] then
|
||||
return defaulttexturedir .. "error_screenshot.png"
|
||||
end
|
||||
|
||||
-- Download
|
||||
|
||||
local function download_screenshot(params)
|
||||
return core.download_file(params.url, params.dest)
|
||||
end
|
||||
local function callback(success)
|
||||
screenshot_downloading[package.thumbnail] = nil
|
||||
screenshot_downloaded[package.thumbnail] = true
|
||||
if not success then
|
||||
core.log("warning", "Screenshot download failed for some reason")
|
||||
end
|
||||
ui.update()
|
||||
end
|
||||
if core.handle_async(download_screenshot,
|
||||
{ dest = filepath, url = package.thumbnail }, callback) then
|
||||
screenshot_downloading[package.thumbnail] = true
|
||||
else
|
||||
core.log("error", "ERROR: async event failed")
|
||||
return defaulttexturedir .. "error_screenshot.png"
|
||||
end
|
||||
|
||||
return defaulttexturedir .. "loading_screenshot.png"
|
||||
end
|
||||
|
||||
function store.load()
|
||||
local version = core.get_version()
|
||||
local base_url = core.settings:get("contentdb_url")
|
||||
local url = base_url ..
|
||||
"/api/packages/?type=mod&type=game&type=txp&protocol_version=" ..
|
||||
core.get_max_supp_proto() .. "&engine_version=" .. version.string
|
||||
|
||||
for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do
|
||||
item = item:trim()
|
||||
if item ~= "" then
|
||||
url = url .. "&hide=" .. item
|
||||
end
|
||||
end
|
||||
|
||||
local timeout = tonumber(minetest.settings:get("curl_file_download_timeout"))
|
||||
local response = http.fetch_sync({ url = url, timeout = timeout })
|
||||
if not response.succeeded then
|
||||
return
|
||||
end
|
||||
|
||||
store.packages_full = core.parse_json(response.data) or {}
|
||||
|
||||
for _, package in pairs(store.packages_full) do
|
||||
package.url = base_url .. "/packages/" ..
|
||||
package.author .. "/" .. package.name ..
|
||||
"/releases/" .. package.release .. "/download/"
|
||||
|
||||
local name_len = #package.name
|
||||
if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then
|
||||
package.id = package.author:lower() .. "/" .. package.name:sub(1, name_len - 5)
|
||||
else
|
||||
package.id = package.author:lower() .. "/" .. package.name
|
||||
end
|
||||
end
|
||||
|
||||
store.packages = store.packages_full
|
||||
store.loaded = true
|
||||
end
|
||||
|
||||
function store.update_paths()
|
||||
local mod_hash = {}
|
||||
pkgmgr.refresh_globals()
|
||||
for _, mod in pairs(pkgmgr.clientmods:get_list()) do
|
||||
if mod.author then
|
||||
mod_hash[mod.author:lower() .. "/" .. mod.name] = mod
|
||||
end
|
||||
end
|
||||
|
||||
local game_hash = {}
|
||||
pkgmgr.update_gamelist()
|
||||
for _, game in pairs(pkgmgr.games) do
|
||||
if game.author ~= "" then
|
||||
game_hash[game.author:lower() .. "/" .. game.id] = game
|
||||
end
|
||||
end
|
||||
|
||||
local txp_hash = {}
|
||||
for _, txp in pairs(pkgmgr.get_texture_packs()) do
|
||||
if txp.author then
|
||||
txp_hash[txp.author:lower() .. "/" .. txp.name] = txp
|
||||
end
|
||||
end
|
||||
|
||||
for _, package in pairs(store.packages_full) do
|
||||
local content
|
||||
if package.type == "mod" then
|
||||
content = mod_hash[package.id]
|
||||
elseif package.type == "game" then
|
||||
content = game_hash[package.id]
|
||||
elseif package.type == "txp" then
|
||||
content = txp_hash[package.id]
|
||||
end
|
||||
|
||||
if content then
|
||||
package.path = content.path
|
||||
package.installed_release = content.release or 0
|
||||
else
|
||||
package.path = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function store.filter_packages(query)
|
||||
if query == "" and filter_type == 1 then
|
||||
store.packages = store.packages_full
|
||||
return
|
||||
end
|
||||
|
||||
local keywords = {}
|
||||
for word in query:lower():gmatch("%S+") do
|
||||
table.insert(keywords, word)
|
||||
end
|
||||
|
||||
local function matches_keywords(package)
|
||||
for k = 1, #keywords do
|
||||
local keyword = keywords[k]
|
||||
|
||||
if string.find(package.name:lower(), keyword, 1, true) or
|
||||
string.find(package.title:lower(), keyword, 1, true) or
|
||||
string.find(package.author:lower(), keyword, 1, true) or
|
||||
string.find(package.short_description:lower(), keyword, 1, true) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
store.packages = {}
|
||||
for _, package in pairs(store.packages_full) do
|
||||
if (query == "" or matches_keywords(package)) and
|
||||
(filter_type == 1 or package.type == filter_types_type[filter_type]) then
|
||||
store.packages[#store.packages + 1] = package
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function store.get_formspec(dlgdata)
|
||||
store.update_paths()
|
||||
|
||||
dlgdata.pagemax = math.max(math.ceil(#store.packages / num_per_page), 1)
|
||||
if cur_page > dlgdata.pagemax then
|
||||
cur_page = 1
|
||||
end
|
||||
|
||||
local W = 15.75
|
||||
local H = 9.5
|
||||
|
||||
local formspec
|
||||
if #store.packages_full > 0 then
|
||||
formspec = {
|
||||
"formspec_version[3]",
|
||||
"size[15.75,9.5]",
|
||||
"position[0.5,0.55]",
|
||||
|
||||
"style[status;border=false]",
|
||||
|
||||
"container[0.375,0.375]",
|
||||
"field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]",
|
||||
"field_close_on_enter[search_string;false]",
|
||||
"button[7.225,0;2,0.8;search;", fgettext("Search"), "]",
|
||||
"dropdown[9.6,0;2.4,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]",
|
||||
"container_end[]",
|
||||
|
||||
-- Page nav buttons
|
||||
"container[0,", H - 0.8 - 0.375, "]",
|
||||
"button[0.375,0;4,0.8;back;", fgettext("Back to Main Menu"), "]",
|
||||
|
||||
"container[", W - 0.375 - 0.8*4 - 2, ",0]",
|
||||
"image_button[0,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]",
|
||||
"image_button[0.8,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]",
|
||||
"style[pagenum;border=false]",
|
||||
"button[1.6,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]",
|
||||
"image_button[3.6,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]",
|
||||
"image_button[4.4,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]",
|
||||
"container_end[]",
|
||||
|
||||
"container_end[]",
|
||||
}
|
||||
|
||||
if number_downloading > 0 then
|
||||
formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;status;"
|
||||
if #download_queue > 0 then
|
||||
formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued", number_downloading, #download_queue)
|
||||
else
|
||||
formspec[#formspec + 1] = fgettext("$1 downloading...", number_downloading)
|
||||
end
|
||||
formspec[#formspec + 1] = "]"
|
||||
else
|
||||
local num_avail_updates = 0
|
||||
for i=1, #store.packages_full do
|
||||
local package = store.packages_full[i]
|
||||
if package.path and package.installed_release < package.release and
|
||||
not (package.downloading or package.queued) then
|
||||
num_avail_updates = num_avail_updates + 1
|
||||
end
|
||||
end
|
||||
|
||||
if num_avail_updates == 0 then
|
||||
formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;status;"
|
||||
formspec[#formspec + 1] = fgettext("No updates")
|
||||
formspec[#formspec + 1] = "]"
|
||||
else
|
||||
formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;update_all;"
|
||||
formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates)
|
||||
formspec[#formspec + 1] = "]"
|
||||
end
|
||||
end
|
||||
|
||||
if #store.packages == 0 then
|
||||
formspec[#formspec + 1] = "label[4,3;"
|
||||
formspec[#formspec + 1] = fgettext("No results")
|
||||
formspec[#formspec + 1] = "]"
|
||||
end
|
||||
else
|
||||
formspec = {
|
||||
"size[12,7]",
|
||||
"position[0.5,0.55]",
|
||||
"label[4,3;", fgettext("No packages could be retrieved"), "]",
|
||||
"container[0,", H - 0.8 - 0.375, "]",
|
||||
"button[0,0;4,0.8;back;", fgettext("Back to Main Menu"), "]",
|
||||
"container_end[]",
|
||||
}
|
||||
end
|
||||
|
||||
local start_idx = (cur_page - 1) * num_per_page + 1
|
||||
for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do
|
||||
local package = store.packages[i]
|
||||
formspec[#formspec + 1] = "container[0.375,"
|
||||
formspec[#formspec + 1] = (i - start_idx) * 1.375 + (2*0.375 + 0.8)
|
||||
formspec[#formspec + 1] = "]"
|
||||
|
||||
-- image
|
||||
formspec[#formspec + 1] = "image[0,0;1.5,1;"
|
||||
formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package))
|
||||
formspec[#formspec + 1] = "]"
|
||||
|
||||
-- title
|
||||
formspec[#formspec + 1] = "label[1.875,0.1;"
|
||||
formspec[#formspec + 1] = core.formspec_escape(
|
||||
minetest.colorize(mt_color_green, package.title) ..
|
||||
minetest.colorize("#BFBFBF", " by " .. package.author))
|
||||
formspec[#formspec + 1] = "]"
|
||||
|
||||
-- buttons
|
||||
local description_width = W - 0.375*5 - 1 - 2*1.5
|
||||
formspec[#formspec + 1] = "container["
|
||||
formspec[#formspec + 1] = W - 0.375*2
|
||||
formspec[#formspec + 1] = ",0.1]"
|
||||
|
||||
if package.downloading then
|
||||
formspec[#formspec + 1] = "button[-3.5,0;2,0.8;status;"
|
||||
formspec[#formspec + 1] = fgettext("Downloading...")
|
||||
formspec[#formspec + 1] = "]"
|
||||
elseif package.queued then
|
||||
formspec[#formspec + 1] = "button[-3.5,0;2,0.8;status;"
|
||||
formspec[#formspec + 1] = fgettext("Queued")
|
||||
formspec[#formspec + 1] = "]"
|
||||
elseif not package.path then
|
||||
formspec[#formspec + 1] = "button[-3,0;1.5,0.8;install_"
|
||||
formspec[#formspec + 1] = tostring(i)
|
||||
formspec[#formspec + 1] = ";"
|
||||
formspec[#formspec + 1] = fgettext("Install")
|
||||
formspec[#formspec + 1] = "]"
|
||||
else
|
||||
if package.installed_release < package.release then
|
||||
description_width = description_width - 1.5
|
||||
|
||||
-- The install_ action also handles updating
|
||||
formspec[#formspec + 1] = "button[-4.5,0;1.5,0.8;install_"
|
||||
formspec[#formspec + 1] = tostring(i)
|
||||
formspec[#formspec + 1] = ";"
|
||||
formspec[#formspec + 1] = fgettext("Update")
|
||||
formspec[#formspec + 1] = "]"
|
||||
end
|
||||
|
||||
formspec[#formspec + 1] = "button[-3,0;1.5,0.8;uninstall_"
|
||||
formspec[#formspec + 1] = tostring(i)
|
||||
formspec[#formspec + 1] = ";"
|
||||
formspec[#formspec + 1] = fgettext("Uninstall")
|
||||
formspec[#formspec + 1] = "]"
|
||||
end
|
||||
|
||||
formspec[#formspec + 1] = "button[-1.5,0;1.5,0.8;view_"
|
||||
formspec[#formspec + 1] = tostring(i)
|
||||
formspec[#formspec + 1] = ";"
|
||||
formspec[#formspec + 1] = fgettext("View")
|
||||
formspec[#formspec + 1] = "]"
|
||||
formspec[#formspec + 1] = "container_end[]"
|
||||
|
||||
-- description
|
||||
formspec[#formspec + 1] = "textarea[1.855,0.3;"
|
||||
formspec[#formspec + 1] = tostring(description_width)
|
||||
formspec[#formspec + 1] = ",0.8;;;"
|
||||
formspec[#formspec + 1] = core.formspec_escape(package.short_description)
|
||||
formspec[#formspec + 1] = "]"
|
||||
|
||||
formspec[#formspec + 1] = "container_end[]"
|
||||
end
|
||||
|
||||
return table.concat(formspec, "")
|
||||
end
|
||||
|
||||
function store.handle_submit(this, fields)
|
||||
if fields.search or fields.key_enter_field == "search_string" then
|
||||
search_string = fields.search_string:trim()
|
||||
cur_page = 1
|
||||
store.filter_packages(search_string)
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.back then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.pstart then
|
||||
cur_page = 1
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.pend then
|
||||
cur_page = this.data.pagemax
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.pnext then
|
||||
cur_page = cur_page + 1
|
||||
if cur_page > this.data.pagemax then
|
||||
cur_page = 1
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.pback then
|
||||
if cur_page == 1 then
|
||||
cur_page = this.data.pagemax
|
||||
else
|
||||
cur_page = cur_page - 1
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.type then
|
||||
local new_type = table.indexof(filter_types_titles, fields.type)
|
||||
if new_type ~= filter_type then
|
||||
filter_type = new_type
|
||||
store.filter_packages(search_string)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if fields.update_all then
|
||||
for i=1, #store.packages_full do
|
||||
local package = store.packages_full[i]
|
||||
if package.path and package.installed_release < package.release and
|
||||
not (package.downloading or package.queued) then
|
||||
queue_download(package)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local start_idx = (cur_page - 1) * num_per_page + 1
|
||||
assert(start_idx ~= nil)
|
||||
for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do
|
||||
local package = store.packages[i]
|
||||
assert(package)
|
||||
|
||||
if fields["install_" .. i] then
|
||||
queue_download(package)
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["uninstall_" .. i] then
|
||||
local dlg_delmod = create_delete_content_dlg(package)
|
||||
dlg_delmod:set_parent(this)
|
||||
this:hide()
|
||||
dlg_delmod:show()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["view_" .. i] then
|
||||
local url = ("%s/packages/%s/%s?protocol_version=%d"):format(
|
||||
core.settings:get("contentdb_url"),
|
||||
package.author, package.name, core.get_max_supp_proto())
|
||||
core.open_url(url)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function create_store_dlg(type)
|
||||
if not store.loaded or #store.packages_full == 0 then
|
||||
store.load()
|
||||
end
|
||||
|
||||
search_string = ""
|
||||
cur_page = 1
|
||||
|
||||
if type then
|
||||
-- table.indexof does not work on tables that contain `nil`
|
||||
for i, v in pairs(filter_types_type) do
|
||||
if v == type then
|
||||
filter_type = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
store.filter_packages(search_string)
|
||||
|
||||
return dialog_create("store",
|
||||
store.get_formspec,
|
||||
store.handle_submit,
|
||||
nil)
|
||||
end
|
477
builtin/mainmenu/dlg_create_world.lua
Normal file
477
builtin/mainmenu/dlg_create_world.lua
Normal file
@ -0,0 +1,477 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
local worldname = ""
|
||||
|
||||
local function table_to_flags(ftable)
|
||||
-- Convert e.g. { jungles = true, caves = false } to "jungles,nocaves"
|
||||
local str = {}
|
||||
for flag, is_set in pairs(ftable) do
|
||||
str[#str + 1] = is_set and flag or ("no" .. flag)
|
||||
end
|
||||
return table.concat(str, ",")
|
||||
end
|
||||
|
||||
-- Same as check_flag but returns a string
|
||||
local function strflag(flags, flag)
|
||||
return (flags[flag] == true) and "true" or "false"
|
||||
end
|
||||
|
||||
local cb_caverns = { "caverns", fgettext("Caverns"), "caverns",
|
||||
fgettext("Very large caverns deep in the underground") }
|
||||
local tt_sea_rivers = fgettext("Sea level rivers")
|
||||
|
||||
local flag_checkboxes = {
|
||||
v5 = {
|
||||
cb_caverns,
|
||||
},
|
||||
v7 = {
|
||||
cb_caverns,
|
||||
{ "ridges", fgettext("Rivers"), "ridges", tt_sea_rivers },
|
||||
{ "mountains", fgettext("Mountains"), "mountains" },
|
||||
{ "floatlands", fgettext("Floatlands (experimental)"), "floatlands",
|
||||
fgettext("Floating landmasses in the sky") },
|
||||
},
|
||||
carpathian = {
|
||||
cb_caverns,
|
||||
{ "rivers", fgettext("Rivers"), "rivers", tt_sea_rivers },
|
||||
},
|
||||
valleys = {
|
||||
{ "altitude-chill", fgettext("Altitude chill"), "altitude_chill",
|
||||
fgettext("Reduces heat with altitude") },
|
||||
{ "altitude-dry", fgettext("Altitude dry"), "altitude_dry",
|
||||
fgettext("Reduces humidity with altitude") },
|
||||
{ "humid-rivers", fgettext("Humid rivers"), "humid_rivers",
|
||||
fgettext("Increases humidity around rivers") },
|
||||
{ "vary-river-depth", fgettext("Vary river depth"), "vary_river_depth",
|
||||
fgettext("Low humidity and high heat causes shallow or dry rivers") },
|
||||
},
|
||||
flat = {
|
||||
cb_caverns,
|
||||
{ "hills", fgettext("Hills"), "hills" },
|
||||
{ "lakes", fgettext("Lakes"), "lakes" },
|
||||
},
|
||||
fractal = {
|
||||
{ "terrain", fgettext("Additional terrain"), "terrain",
|
||||
fgettext("Generate non-fractal terrain: Oceans and underground") },
|
||||
},
|
||||
v6 = {
|
||||
{ "trees", fgettext("Trees and jungle grass"), "trees" },
|
||||
{ "flat", fgettext("Flat terrain"), "flat" },
|
||||
{ "mudflow", fgettext("Mud flow"), "mudflow",
|
||||
fgettext("Terrain surface erosion") },
|
||||
-- Biome settings are in mgv6_biomes below
|
||||
},
|
||||
}
|
||||
|
||||
local mgv6_biomes = {
|
||||
{
|
||||
fgettext("Temperate, Desert, Jungle, Tundra, Taiga"),
|
||||
{jungles = true, snowbiomes = true}
|
||||
},
|
||||
{
|
||||
fgettext("Temperate, Desert, Jungle"),
|
||||
{jungles = true, snowbiomes = false}
|
||||
},
|
||||
{
|
||||
fgettext("Temperate, Desert"),
|
||||
{jungles = false, snowbiomes = false}
|
||||
},
|
||||
}
|
||||
|
||||
local function create_world_formspec(dialogdata)
|
||||
|
||||
-- Error out when no games found
|
||||
if #pkgmgr.games == 0 then
|
||||
return "size[12.25,3,true]" ..
|
||||
"box[0,0;12,2;#ff8800]" ..
|
||||
"textarea[0.3,0;11.7,2;;;"..
|
||||
fgettext("You have no games installed.") .. "\n" ..
|
||||
fgettext("Download one from minetest.net") .. "]" ..
|
||||
"button[4.75,2.5;3,0.5;world_create_cancel;" .. fgettext("Cancel") .. "]"
|
||||
end
|
||||
|
||||
local mapgens = core.get_mapgen_names()
|
||||
|
||||
local current_seed = core.settings:get("fixed_map_seed") or ""
|
||||
local current_mg = core.settings:get("mg_name")
|
||||
local gameid = core.settings:get("menu_last_game")
|
||||
|
||||
local flags = {
|
||||
main = core.settings:get_flags("mg_flags"),
|
||||
v5 = core.settings:get_flags("mgv5_spflags"),
|
||||
v6 = core.settings:get_flags("mgv6_spflags"),
|
||||
v7 = core.settings:get_flags("mgv7_spflags"),
|
||||
fractal = core.settings:get_flags("mgfractal_spflags"),
|
||||
carpathian = core.settings:get_flags("mgcarpathian_spflags"),
|
||||
valleys = core.settings:get_flags("mgvalleys_spflags"),
|
||||
flat = core.settings:get_flags("mgflat_spflags"),
|
||||
}
|
||||
|
||||
local gameidx = 0
|
||||
if gameid ~= nil then
|
||||
local _
|
||||
_, gameidx = pkgmgr.find_by_gameid(gameid)
|
||||
|
||||
if gameidx == nil then
|
||||
gameidx = 0
|
||||
end
|
||||
end
|
||||
|
||||
local game_by_gameidx = core.get_game(gameidx)
|
||||
local disallowed_mapgen_settings = {}
|
||||
if game_by_gameidx ~= nil then
|
||||
local gamepath = game_by_gameidx.path
|
||||
local gameconfig = Settings(gamepath.."/game.conf")
|
||||
|
||||
local allowed_mapgens = (gameconfig:get("allowed_mapgens") or ""):split()
|
||||
for key, value in pairs(allowed_mapgens) do
|
||||
allowed_mapgens[key] = value:trim()
|
||||
end
|
||||
|
||||
local disallowed_mapgens = (gameconfig:get("disallowed_mapgens") or ""):split()
|
||||
for key, value in pairs(disallowed_mapgens) do
|
||||
disallowed_mapgens[key] = value:trim()
|
||||
end
|
||||
|
||||
if #allowed_mapgens > 0 then
|
||||
for i = #mapgens, 1, -1 do
|
||||
if table.indexof(allowed_mapgens, mapgens[i]) == -1 then
|
||||
table.remove(mapgens, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if disallowed_mapgens then
|
||||
for i = #mapgens, 1, -1 do
|
||||
if table.indexof(disallowed_mapgens, mapgens[i]) > 0 then
|
||||
table.remove(mapgens, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ds = (gameconfig:get("disallowed_mapgen_settings") or ""):split()
|
||||
for _, value in pairs(ds) do
|
||||
disallowed_mapgen_settings[value:trim()] = true
|
||||
end
|
||||
end
|
||||
|
||||
local mglist = ""
|
||||
local selindex
|
||||
local i = 1
|
||||
local first_mg
|
||||
for k,v in pairs(mapgens) do
|
||||
if not first_mg then
|
||||
first_mg = v
|
||||
end
|
||||
if current_mg == v then
|
||||
selindex = i
|
||||
end
|
||||
i = i + 1
|
||||
mglist = mglist .. v .. ","
|
||||
end
|
||||
if not selindex then
|
||||
selindex = 1
|
||||
current_mg = first_mg
|
||||
end
|
||||
mglist = mglist:sub(1, -2)
|
||||
|
||||
local mg_main_flags = function(mapgen, y)
|
||||
if mapgen == "singlenode" then
|
||||
return "", y
|
||||
end
|
||||
if disallowed_mapgen_settings["mg_flags"] then
|
||||
return "", y
|
||||
end
|
||||
|
||||
local form = "checkbox[0," .. y .. ";flag_mg_caves;" ..
|
||||
fgettext("Caves") .. ";"..strflag(flags.main, "caves").."]"
|
||||
y = y + 0.5
|
||||
|
||||
form = form .. "checkbox[0,"..y..";flag_mg_dungeons;" ..
|
||||
fgettext("Dungeons") .. ";"..strflag(flags.main, "dungeons").."]"
|
||||
y = y + 0.5
|
||||
|
||||
local d_name = fgettext("Decorations")
|
||||
local d_tt
|
||||
if mapgen == "v6" then
|
||||
d_tt = fgettext("Structures appearing on the terrain (no effect on trees and jungle grass created by v6)")
|
||||
else
|
||||
d_tt = fgettext("Structures appearing on the terrain, typically trees and plants")
|
||||
end
|
||||
form = form .. "checkbox[0,"..y..";flag_mg_decorations;" ..
|
||||
d_name .. ";" ..
|
||||
strflag(flags.main, "decorations").."]" ..
|
||||
"tooltip[flag_mg_decorations;" ..
|
||||
d_tt ..
|
||||
"]"
|
||||
y = y + 0.5
|
||||
|
||||
form = form .. "tooltip[flag_mg_caves;" ..
|
||||
fgettext("Network of tunnels and caves")
|
||||
.. "]"
|
||||
return form, y
|
||||
end
|
||||
|
||||
local mg_specific_flags = function(mapgen, y)
|
||||
if not flag_checkboxes[mapgen] then
|
||||
return "", y
|
||||
end
|
||||
if disallowed_mapgen_settings["mg"..mapgen.."_spflags"] then
|
||||
return "", y
|
||||
end
|
||||
local form = ""
|
||||
for _,tab in pairs(flag_checkboxes[mapgen]) do
|
||||
local id = "flag_mg"..mapgen.."_"..tab[1]
|
||||
form = form .. ("checkbox[0,%f;%s;%s;%s]"):
|
||||
format(y, id, tab[2], strflag(flags[mapgen], tab[3]))
|
||||
|
||||
if tab[4] then
|
||||
form = form .. "tooltip["..id..";"..tab[4].."]"
|
||||
end
|
||||
y = y + 0.5
|
||||
end
|
||||
|
||||
if mapgen ~= "v6" then
|
||||
-- No special treatment
|
||||
return form, y
|
||||
end
|
||||
-- Special treatment for v6 (add biome widgets)
|
||||
|
||||
-- Biome type (jungles, snowbiomes)
|
||||
local biometype
|
||||
if flags.v6.snowbiomes == true then
|
||||
biometype = 1
|
||||
elseif flags.v6.jungles == true then
|
||||
biometype = 2
|
||||
else
|
||||
biometype = 3
|
||||
end
|
||||
y = y + 0.3
|
||||
|
||||
form = form .. "label[0,"..(y+0.1)..";" .. fgettext("Biomes") .. "]"
|
||||
y = y + 0.6
|
||||
|
||||
form = form .. "dropdown[0,"..y..";6.3;mgv6_biomes;"
|
||||
for b=1, #mgv6_biomes do
|
||||
form = form .. mgv6_biomes[b][1]
|
||||
if b < #mgv6_biomes then
|
||||
form = form .. ","
|
||||
end
|
||||
end
|
||||
form = form .. ";" .. biometype.. "]"
|
||||
|
||||
-- biomeblend
|
||||
y = y + 0.55
|
||||
form = form .. "checkbox[0,"..y..";flag_mgv6_biomeblend;" ..
|
||||
fgettext("Biome blending") .. ";"..strflag(flags.v6, "biomeblend").."]" ..
|
||||
"tooltip[flag_mgv6_biomeblend;" ..
|
||||
fgettext("Smooth transition between biomes") .. "]"
|
||||
|
||||
return form, y
|
||||
end
|
||||
|
||||
current_seed = core.formspec_escape(current_seed)
|
||||
|
||||
local y_start = 0.0
|
||||
local y = y_start
|
||||
local str_flags, str_spflags
|
||||
local label_flags, label_spflags = "", ""
|
||||
y = y + 0.3
|
||||
str_flags, y = mg_main_flags(current_mg, y)
|
||||
if str_flags ~= "" then
|
||||
label_flags = "label[0,"..y_start..";" .. fgettext("Mapgen flags") .. "]"
|
||||
y_start = y + 0.4
|
||||
else
|
||||
y_start = 0.0
|
||||
end
|
||||
y = y_start + 0.3
|
||||
str_spflags = mg_specific_flags(current_mg, y)
|
||||
if str_spflags ~= "" then
|
||||
label_spflags = "label[0,"..y_start..";" .. fgettext("Mapgen-specific flags") .. "]"
|
||||
end
|
||||
|
||||
-- Warning if only devtest is installed
|
||||
local devtest_only = ""
|
||||
local gamelist_height = 2.3
|
||||
if #pkgmgr.games == 1 and pkgmgr.games[1].id == "devtest" then
|
||||
devtest_only = "box[0,0;5.8,1.7;#ff8800]" ..
|
||||
"textarea[0.3,0;6,1.8;;;"..
|
||||
fgettext("Warning: The Development Test is meant for developers.") .. "\n" ..
|
||||
fgettext("Download a game, such as Minetest Game, from minetest.net") .. "]"
|
||||
gamelist_height = 0.5
|
||||
end
|
||||
|
||||
local retval =
|
||||
"size[12.25,7,true]" ..
|
||||
|
||||
-- Left side
|
||||
"container[0,0]"..
|
||||
"field[0.3,0.6;6,0.5;te_world_name;" ..
|
||||
fgettext("World name") ..
|
||||
";" .. core.formspec_escape(worldname) .. "]" ..
|
||||
|
||||
"field[0.3,1.7;6,0.5;te_seed;" ..
|
||||
fgettext("Seed") ..
|
||||
";".. current_seed .. "]" ..
|
||||
|
||||
"label[0,2;" .. fgettext("Mapgen") .. "]"..
|
||||
"dropdown[0,2.5;6.3;dd_mapgen;" .. mglist .. ";" .. selindex .. "]" ..
|
||||
|
||||
"label[0,3.35;" .. fgettext("Game") .. "]"..
|
||||
"textlist[0,3.85;5.8,"..gamelist_height..";games;" ..
|
||||
pkgmgr.gamelist() .. ";" .. gameidx .. ";false]" ..
|
||||
"container[0,4.5]" ..
|
||||
devtest_only ..
|
||||
"container_end[]" ..
|
||||
"container_end[]" ..
|
||||
|
||||
-- Right side
|
||||
"container[6.2,0]"..
|
||||
label_flags .. str_flags ..
|
||||
label_spflags .. str_spflags ..
|
||||
"container_end[]"..
|
||||
|
||||
-- Menu buttons
|
||||
"button[3.25,6.5;3,0.5;world_create_confirm;" .. fgettext("Create") .. "]" ..
|
||||
"button[6.25,6.5;3,0.5;world_create_cancel;" .. fgettext("Cancel") .. "]"
|
||||
|
||||
return retval
|
||||
|
||||
end
|
||||
|
||||
local function create_world_buttonhandler(this, fields)
|
||||
|
||||
if fields["world_create_confirm"] or
|
||||
fields["key_enter"] then
|
||||
|
||||
local worldname = fields["te_world_name"]
|
||||
local gameindex = core.get_textlist_index("games")
|
||||
|
||||
if gameindex ~= nil then
|
||||
-- For unnamed worlds use the generated name 'world<number>',
|
||||
-- where the number increments: it is set to 1 larger than the largest
|
||||
-- generated name number found.
|
||||
if worldname == "" then
|
||||
local worldnum_max = 0
|
||||
for _, world in ipairs(menudata.worldlist:get_list()) do
|
||||
if world.name:match("^world%d+$") then
|
||||
local worldnum = tonumber(world.name:sub(6))
|
||||
worldnum_max = math.max(worldnum_max, worldnum)
|
||||
end
|
||||
end
|
||||
worldname = "world" .. worldnum_max + 1
|
||||
end
|
||||
|
||||
core.settings:set("fixed_map_seed", fields["te_seed"])
|
||||
|
||||
local message
|
||||
if not menudata.worldlist:uid_exists_raw(worldname) then
|
||||
core.settings:set("mg_name",fields["dd_mapgen"])
|
||||
message = core.create_world(worldname,gameindex)
|
||||
else
|
||||
message = fgettext("A world named \"$1\" already exists", worldname)
|
||||
end
|
||||
|
||||
if message ~= nil then
|
||||
gamedata.errormessage = message
|
||||
else
|
||||
core.settings:set("menu_last_game",pkgmgr.games[gameindex].id)
|
||||
if this.data.update_worldlist_filter then
|
||||
menudata.worldlist:set_filtercriteria(pkgmgr.games[gameindex].id)
|
||||
mm_texture.update("singleplayer", pkgmgr.games[gameindex].id)
|
||||
end
|
||||
menudata.worldlist:refresh()
|
||||
core.settings:set("mainmenu_last_selected_world",
|
||||
menudata.worldlist:raw_index_by_uid(worldname))
|
||||
end
|
||||
else
|
||||
gamedata.errormessage = fgettext("No game selected")
|
||||
end
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
worldname = fields.te_world_name
|
||||
|
||||
if fields["games"] then
|
||||
local gameindex = core.get_textlist_index("games")
|
||||
core.settings:set("menu_last_game", pkgmgr.games[gameindex].id)
|
||||
return true
|
||||
end
|
||||
|
||||
for k,v in pairs(fields) do
|
||||
local split = string.split(k, "_", nil, 3)
|
||||
if split and split[1] == "flag" then
|
||||
local setting
|
||||
if split[2] == "mg" then
|
||||
setting = "mg_flags"
|
||||
else
|
||||
setting = split[2].."_spflags"
|
||||
end
|
||||
-- We replaced the underscore of flag names with a dash.
|
||||
local flag = string.gsub(split[3], "-", "_")
|
||||
local ftable = core.settings:get_flags(setting)
|
||||
if v == "true" then
|
||||
ftable[flag] = true
|
||||
else
|
||||
ftable[flag] = false
|
||||
end
|
||||
local flags = table_to_flags(ftable)
|
||||
core.settings:set(setting, flags)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if fields["world_create_cancel"] then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["mgv6_biomes"] then
|
||||
local entry = minetest.formspec_escape(fields["mgv6_biomes"])
|
||||
for b=1, #mgv6_biomes do
|
||||
if entry == mgv6_biomes[b][1] then
|
||||
local ftable = core.settings:get_flags("mgv6_spflags")
|
||||
ftable.jungles = mgv6_biomes[b][2].jungles
|
||||
ftable.snowbiomes = mgv6_biomes[b][2].snowbiomes
|
||||
local flags = table_to_flags(ftable)
|
||||
core.settings:set("mgv6_spflags", flags)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if fields["dd_mapgen"] then
|
||||
core.settings:set("mg_name", fields["dd_mapgen"])
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function create_create_world_dlg(update_worldlistfilter)
|
||||
worldname = ""
|
||||
local retval = dialog_create("sp_create_world",
|
||||
create_world_formspec,
|
||||
create_world_buttonhandler,
|
||||
nil)
|
||||
retval.update_worldlist_filter = update_worldlistfilter
|
||||
|
||||
return retval
|
||||
end
|
75
builtin/mainmenu/dlg_delete_content.lua
Normal file
75
builtin/mainmenu/dlg_delete_content.lua
Normal file
@ -0,0 +1,75 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function delete_content_formspec(dialogdata)
|
||||
local retval =
|
||||
"size[11.5,4.5,true]" ..
|
||||
"label[2,2;" ..
|
||||
fgettext("Are you sure you want to delete \"$1\"?", dialogdata.content.name) .. "]"..
|
||||
"style[dlg_delete_content_confirm;bgcolor=red]" ..
|
||||
"button[3.25,3.5;2.5,0.5;dlg_delete_content_confirm;" .. fgettext("Delete") .. "]" ..
|
||||
"button[5.75,3.5;2.5,0.5;dlg_delete_content_cancel;" .. fgettext("Cancel") .. "]"
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function delete_content_buttonhandler(this, fields)
|
||||
if fields["dlg_delete_content_confirm"] ~= nil then
|
||||
|
||||
if this.data.content.path ~= nil and
|
||||
this.data.content.path ~= "" and
|
||||
this.data.content.path ~= core.get_modpath() and
|
||||
this.data.content.path ~= core.get_gamepath() and
|
||||
this.data.content.path ~= core.get_texturepath() then
|
||||
if not core.delete_dir(this.data.content.path) then
|
||||
gamedata.errormessage = fgettext("pkgmgr: failed to delete \"$1\"", this.data.content.path)
|
||||
end
|
||||
|
||||
if this.data.content.type == "game" then
|
||||
pkgmgr.update_gamelist()
|
||||
else
|
||||
pkgmgr.refresh_globals()
|
||||
end
|
||||
else
|
||||
gamedata.errormessage = fgettext("pkgmgr: invalid path \"$1\"", this.data.content.path)
|
||||
end
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["dlg_delete_content_cancel"] then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function create_delete_content_dlg(content)
|
||||
assert(content.name)
|
||||
|
||||
local retval = dialog_create("dlg_delete_content",
|
||||
delete_content_formspec,
|
||||
delete_content_buttonhandler,
|
||||
nil)
|
||||
retval.data.content = content
|
||||
return retval
|
||||
end
|
62
builtin/mainmenu/dlg_delete_world.lua
Normal file
62
builtin/mainmenu/dlg_delete_world.lua
Normal file
@ -0,0 +1,62 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
|
||||
local function delete_world_formspec(dialogdata)
|
||||
local retval =
|
||||
"size[10,2.5,true]" ..
|
||||
"label[0.5,0.5;" ..
|
||||
fgettext("Delete World \"$1\"?", dialogdata.delete_name) .. "]" ..
|
||||
"style[world_delete_confirm;bgcolor=red]" ..
|
||||
"button[0.5,1.5;2.5,0.5;world_delete_confirm;" .. fgettext("Delete") .. "]" ..
|
||||
"button[7.0,1.5;2.5,0.5;world_delete_cancel;" .. fgettext("Cancel") .. "]"
|
||||
return retval
|
||||
end
|
||||
|
||||
local function delete_world_buttonhandler(this, fields)
|
||||
if fields["world_delete_confirm"] then
|
||||
if this.data.delete_index > 0 and
|
||||
this.data.delete_index <= #menudata.worldlist:get_raw_list() then
|
||||
core.delete_world(this.data.delete_index)
|
||||
menudata.worldlist:refresh()
|
||||
end
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["world_delete_cancel"] then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function create_delete_world_dlg(name_to_del, index_to_del)
|
||||
assert(name_to_del ~= nil and type(name_to_del) == "string" and name_to_del ~= "")
|
||||
assert(index_to_del ~= nil and type(index_to_del) == "number")
|
||||
|
||||
local retval = dialog_create("delete_world",
|
||||
delete_world_formspec,
|
||||
delete_world_buttonhandler,
|
||||
nil)
|
||||
retval.data.delete_name = name_to_del
|
||||
retval.data.delete_index = index_to_del
|
||||
|
||||
return retval
|
||||
end
|
73
builtin/mainmenu/dlg_rename_modpack.lua
Normal file
73
builtin/mainmenu/dlg_rename_modpack.lua
Normal file
@ -0,0 +1,73 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function rename_modpack_formspec(dialogdata)
|
||||
local retval =
|
||||
"size[11.5,4.5,true]" ..
|
||||
"button[3.25,3.5;2.5,0.5;dlg_rename_modpack_confirm;"..
|
||||
fgettext("Accept") .. "]" ..
|
||||
"button[5.75,3.5;2.5,0.5;dlg_rename_modpack_cancel;"..
|
||||
fgettext("Cancel") .. "]"
|
||||
|
||||
local input_y = 2
|
||||
if dialogdata.mod.is_name_explicit then
|
||||
retval = retval .. "textarea[1,0.2;10,2;;;" ..
|
||||
fgettext("This modpack has an explicit name given in its modpack.conf " ..
|
||||
"which will override any renaming here.") .. "]"
|
||||
input_y = 2.5
|
||||
end
|
||||
retval = retval ..
|
||||
"field[2.5," .. input_y .. ";7,0.5;te_modpack_name;" ..
|
||||
fgettext("Rename Modpack:") .. ";" .. dialogdata.mod.dir_name .. "]"
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function rename_modpack_buttonhandler(this, fields)
|
||||
if fields["dlg_rename_modpack_confirm"] ~= nil then
|
||||
local oldpath = this.data.mod.path
|
||||
local targetpath = this.data.mod.parent_dir .. DIR_DELIM .. fields["te_modpack_name"]
|
||||
os.rename(oldpath, targetpath)
|
||||
pkgmgr.refresh_globals()
|
||||
pkgmgr.selected_mod = pkgmgr.global_mods:get_current_index(
|
||||
pkgmgr.global_mods:raw_index_by_uid(fields["te_modpack_name"]))
|
||||
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["dlg_rename_modpack_cancel"] then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function create_rename_modpack_dlg(modpack)
|
||||
|
||||
local retval = dialog_create("dlg_delete_mod",
|
||||
rename_modpack_formspec,
|
||||
rename_modpack_buttonhandler,
|
||||
nil)
|
||||
retval.data.mod = modpack
|
||||
return retval
|
||||
end
|
1136
builtin/mainmenu/dlg_settings_advanced.lua
Normal file
1136
builtin/mainmenu/dlg_settings_advanced.lua
Normal file
File diff suppressed because it is too large
Load Diff
129
builtin/mainmenu/generate_from_settingtypes.lua
Normal file
129
builtin/mainmenu/generate_from_settingtypes.lua
Normal file
@ -0,0 +1,129 @@
|
||||
local settings = ...
|
||||
|
||||
local concat = table.concat
|
||||
local insert = table.insert
|
||||
local sprintf = string.format
|
||||
local rep = string.rep
|
||||
|
||||
local minetest_example_header = [[
|
||||
# This file contains a list of all available settings and their default value for minetest.conf
|
||||
|
||||
# By default, all the settings are commented and not functional.
|
||||
# Uncomment settings by removing the preceding #.
|
||||
|
||||
# minetest.conf is read by default from:
|
||||
# ../minetest.conf
|
||||
# ../../minetest.conf
|
||||
# Any other path can be chosen by passing the path as a parameter
|
||||
# to the program, eg. "minetest.exe --config ../minetest.conf.example".
|
||||
|
||||
# Further documentation:
|
||||
# http://wiki.minetest.net/
|
||||
|
||||
]]
|
||||
|
||||
local group_format_template = [[
|
||||
# %s = {
|
||||
# offset = %s,
|
||||
# scale = %s,
|
||||
# spread = (%s, %s, %s),
|
||||
# seed = %s,
|
||||
# octaves = %s,
|
||||
# persistence = %s,
|
||||
# lacunarity = %s,
|
||||
# flags = %s
|
||||
# }
|
||||
|
||||
]]
|
||||
|
||||
local function create_minetest_conf_example()
|
||||
local result = { minetest_example_header }
|
||||
for _, entry in ipairs(settings) do
|
||||
if entry.type == "category" then
|
||||
if entry.level == 0 then
|
||||
insert(result, "#\n# " .. entry.name .. "\n#\n\n")
|
||||
else
|
||||
insert(result, rep("#", entry.level))
|
||||
insert(result, "# " .. entry.name .. "\n\n")
|
||||
end
|
||||
else
|
||||
local group_format = false
|
||||
if entry.noise_params and entry.values then
|
||||
if entry.type == "noise_params_2d" or entry.type == "noise_params_3d" then
|
||||
group_format = true
|
||||
end
|
||||
end
|
||||
if entry.comment ~= "" then
|
||||
for _, comment_line in ipairs(entry.comment:split("\n", true)) do
|
||||
insert(result, "# " .. comment_line .. "\n")
|
||||
end
|
||||
end
|
||||
insert(result, "# type: " .. entry.type)
|
||||
if entry.min then
|
||||
insert(result, " min: " .. entry.min)
|
||||
end
|
||||
if entry.max then
|
||||
insert(result, " max: " .. entry.max)
|
||||
end
|
||||
if entry.values and entry.noise_params == nil then
|
||||
insert(result, " values: " .. concat(entry.values, ", "))
|
||||
end
|
||||
if entry.possible then
|
||||
insert(result, " possible values: " .. concat(entry.possible, ", "))
|
||||
end
|
||||
insert(result, "\n")
|
||||
if group_format == true then
|
||||
insert(result, sprintf(group_format_template, entry.name, entry.values[1],
|
||||
entry.values[2], entry.values[3], entry.values[4], entry.values[5],
|
||||
entry.values[6], entry.values[7], entry.values[8], entry.values[9],
|
||||
entry.values[10]))
|
||||
else
|
||||
local append
|
||||
if entry.default ~= "" then
|
||||
append = " " .. entry.default
|
||||
end
|
||||
insert(result, sprintf("# %s =%s\n\n", entry.name, append or ""))
|
||||
end
|
||||
end
|
||||
end
|
||||
return concat(result)
|
||||
end
|
||||
|
||||
local translation_file_header = [[
|
||||
// This file is automatically generated
|
||||
// It conatins a bunch of fake gettext calls, to tell xgettext about the strings in config files
|
||||
// To update it, refer to the bottom of builtin/mainmenu/dlg_settings_advanced.lua
|
||||
|
||||
fake_function() {]]
|
||||
|
||||
local function create_translation_file()
|
||||
local result = { translation_file_header }
|
||||
for _, entry in ipairs(settings) do
|
||||
if entry.type == "category" then
|
||||
insert(result, sprintf("\tgettext(%q);", entry.name))
|
||||
else
|
||||
if entry.readable_name then
|
||||
insert(result, sprintf("\tgettext(%q);", entry.readable_name))
|
||||
end
|
||||
if entry.comment ~= "" then
|
||||
local comment_escaped = entry.comment:gsub("\n", "\\n")
|
||||
comment_escaped = comment_escaped:gsub("\"", "\\\"")
|
||||
insert(result, "\tgettext(\"" .. comment_escaped .. "\");")
|
||||
end
|
||||
end
|
||||
end
|
||||
insert(result, "}\n")
|
||||
return concat(result, "\n")
|
||||
end
|
||||
|
||||
local file = assert(io.open("minetest.conf.example", "w"))
|
||||
file:write(create_minetest_conf_example())
|
||||
file:close()
|
||||
|
||||
file = assert(io.open("src/settings_translation_file.cpp", "w"))
|
||||
-- If 'minetest.conf.example' appears in the 'bin' folder, the line below may have to be
|
||||
-- used instead. The file will also appear in the 'bin' folder.
|
||||
--file = assert(io.open("settings_translation_file.cpp", "w"))
|
||||
file:write(create_translation_file())
|
||||
file:close()
|
||||
|
117
builtin/mainmenu/init.lua
Normal file
117
builtin/mainmenu/init.lua
Normal file
@ -0,0 +1,117 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
mt_color_grey = "#AAAAAA"
|
||||
mt_color_blue = "#6389FF"
|
||||
mt_color_green = "#72FF63"
|
||||
mt_color_dark_green = "#25C191"
|
||||
|
||||
local menupath = core.get_mainmenu_path()
|
||||
local basepath = core.get_builtin_path()
|
||||
defaulttexturedir = core.get_texturepath_share() .. DIR_DELIM .. "base" ..
|
||||
DIR_DELIM .. "pack" .. DIR_DELIM
|
||||
|
||||
dofile(basepath .. "common" .. DIR_DELIM .. "filterlist.lua")
|
||||
dofile(basepath .. "fstk" .. DIR_DELIM .. "buttonbar.lua")
|
||||
dofile(basepath .. "fstk" .. DIR_DELIM .. "dialog.lua")
|
||||
dofile(basepath .. "fstk" .. DIR_DELIM .. "tabview.lua")
|
||||
dofile(basepath .. "fstk" .. DIR_DELIM .. "ui.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "async_event.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "common.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "pkgmgr.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "textures.lua")
|
||||
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_config_world.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_settings_advanced.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_contentstore.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_create_world.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_delete_content.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_delete_world.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_rename_modpack.lua")
|
||||
|
||||
local tabs = {}
|
||||
|
||||
tabs.settings = dofile(menupath .. DIR_DELIM .. "tab_settings.lua")
|
||||
tabs.content = dofile(menupath .. DIR_DELIM .. "tab_content.lua")
|
||||
tabs.credits = dofile(menupath .. DIR_DELIM .. "tab_credits.lua")
|
||||
tabs.local_game = dofile(menupath .. DIR_DELIM .. "tab_local.lua")
|
||||
tabs.play_online = dofile(menupath .. DIR_DELIM .. "tab_online.lua")
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function main_event_handler(tabview, event)
|
||||
if event == "MenuQuit" then
|
||||
core.close()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function init_globals()
|
||||
-- Init gamedata
|
||||
gamedata.worldindex = 0
|
||||
|
||||
menudata.worldlist = filterlist.create(
|
||||
core.get_worlds,
|
||||
compare_worlds,
|
||||
-- Unique id comparison function
|
||||
function(element, uid)
|
||||
return element.name == uid
|
||||
end,
|
||||
-- Filter function
|
||||
function(element, gameid)
|
||||
return element.gameid == gameid
|
||||
end
|
||||
)
|
||||
|
||||
menudata.worldlist:add_sort_mechanism("alphabetic", sort_worlds_alphabetic)
|
||||
menudata.worldlist:set_sortmode("alphabetic")
|
||||
|
||||
if not core.settings:get("menu_last_game") then
|
||||
local default_game = core.settings:get("default_game") or "minetest"
|
||||
core.settings:set("menu_last_game", default_game)
|
||||
end
|
||||
|
||||
mm_texture.init()
|
||||
|
||||
-- Create main tabview
|
||||
local tv_main = tabview_create("maintab", {x = 12, y = 5.4}, {x = 0, y = 0})
|
||||
|
||||
tv_main:set_autosave_tab(true)
|
||||
tv_main:add(tabs.local_game)
|
||||
tv_main:add(tabs.play_online)
|
||||
|
||||
tv_main:add(tabs.content)
|
||||
tv_main:add(tabs.settings)
|
||||
tv_main:add(tabs.credits)
|
||||
|
||||
tv_main:set_global_event_handler(main_event_handler)
|
||||
tv_main:set_fixed_size(false)
|
||||
|
||||
local last_tab = core.settings:get("maintab_LAST")
|
||||
if last_tab and tv_main.current_tab ~= last_tab then
|
||||
tv_main:set_tab(last_tab)
|
||||
end
|
||||
ui.set_default("maintab")
|
||||
tv_main:show()
|
||||
|
||||
ui.update()
|
||||
|
||||
core.sound_play("main_menu", true)
|
||||
end
|
||||
|
||||
init_globals()
|
||||
|
972
builtin/mainmenu/pkgmgr.lua
Normal file
972
builtin/mainmenu/pkgmgr.lua
Normal file
@ -0,0 +1,972 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function get_last_folder(text,count)
|
||||
local parts = text:split(DIR_DELIM)
|
||||
|
||||
if count == nil then
|
||||
return parts[#parts]
|
||||
end
|
||||
|
||||
local retval = ""
|
||||
for i=1,count,1 do
|
||||
retval = retval .. parts[#parts - (count-i)] .. DIR_DELIM
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
local function cleanup_path(temppath)
|
||||
|
||||
local parts = temppath:split("-")
|
||||
temppath = ""
|
||||
for i=1,#parts,1 do
|
||||
if temppath ~= "" then
|
||||
temppath = temppath .. "_"
|
||||
end
|
||||
temppath = temppath .. parts[i]
|
||||
end
|
||||
|
||||
parts = temppath:split(".")
|
||||
temppath = ""
|
||||
for i=1,#parts,1 do
|
||||
if temppath ~= "" then
|
||||
temppath = temppath .. "_"
|
||||
end
|
||||
temppath = temppath .. parts[i]
|
||||
end
|
||||
|
||||
parts = temppath:split("'")
|
||||
temppath = ""
|
||||
for i=1,#parts,1 do
|
||||
if temppath ~= "" then
|
||||
temppath = temppath .. ""
|
||||
end
|
||||
temppath = temppath .. parts[i]
|
||||
end
|
||||
|
||||
parts = temppath:split(" ")
|
||||
temppath = ""
|
||||
for i=1,#parts,1 do
|
||||
if temppath ~= "" then
|
||||
temppath = temppath
|
||||
end
|
||||
temppath = temppath .. parts[i]
|
||||
end
|
||||
|
||||
return temppath
|
||||
end
|
||||
|
||||
function get_mods(path,retval,modpack)
|
||||
local mods = core.get_dir_list(path, true)
|
||||
|
||||
for _, name in ipairs(mods) do
|
||||
if name:sub(1, 1) ~= "." then
|
||||
local prefix = path .. DIR_DELIM .. name
|
||||
local toadd = {
|
||||
dir_name = name,
|
||||
parent_dir = path,
|
||||
}
|
||||
retval[#retval + 1] = toadd
|
||||
|
||||
-- Get config file
|
||||
local mod_conf
|
||||
local modpack_conf = io.open(prefix .. DIR_DELIM .. "modpack.conf")
|
||||
if modpack_conf then
|
||||
toadd.is_modpack = true
|
||||
modpack_conf:close()
|
||||
|
||||
mod_conf = Settings(prefix .. DIR_DELIM .. "modpack.conf"):to_table()
|
||||
if mod_conf.name then
|
||||
name = mod_conf.name
|
||||
toadd.is_name_explicit = true
|
||||
end
|
||||
else
|
||||
mod_conf = Settings(prefix .. DIR_DELIM .. "mod.conf"):to_table()
|
||||
if mod_conf.name then
|
||||
name = mod_conf.name
|
||||
toadd.is_name_explicit = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Read from config
|
||||
toadd.name = name
|
||||
toadd.author = mod_conf.author
|
||||
toadd.release = tonumber(mod_conf.release or "0")
|
||||
toadd.path = prefix
|
||||
toadd.type = "mod"
|
||||
|
||||
-- Check modpack.txt
|
||||
-- Note: modpack.conf is already checked above
|
||||
local modpackfile = io.open(prefix .. DIR_DELIM .. "modpack.txt")
|
||||
if modpackfile then
|
||||
modpackfile:close()
|
||||
toadd.is_modpack = true
|
||||
end
|
||||
|
||||
-- Deal with modpack contents
|
||||
if modpack and modpack ~= "" then
|
||||
toadd.modpack = modpack
|
||||
elseif toadd.is_modpack then
|
||||
toadd.type = "modpack"
|
||||
toadd.is_modpack = true
|
||||
get_mods(prefix, retval, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--modmanager implementation
|
||||
pkgmgr = {}
|
||||
|
||||
function pkgmgr.get_texture_packs()
|
||||
local txtpath = core.get_texturepath()
|
||||
local list = core.get_dir_list(txtpath, true)
|
||||
local retval = {}
|
||||
|
||||
local current_texture_path = core.settings:get("texture_path")
|
||||
|
||||
for _, item in ipairs(list) do
|
||||
if item ~= "base" then
|
||||
local name = item
|
||||
|
||||
local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM
|
||||
if path == current_texture_path then
|
||||
name = fgettext("$1 (Enabled)", name)
|
||||
end
|
||||
|
||||
local conf = Settings(path .. "texture_pack.conf")
|
||||
|
||||
retval[#retval + 1] = {
|
||||
name = item,
|
||||
author = conf:get("author"),
|
||||
release = tonumber(conf:get("release") or "0"),
|
||||
list_name = name,
|
||||
type = "txp",
|
||||
path = path,
|
||||
enabled = path == current_texture_path,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(retval, function(a, b)
|
||||
return a.name > b.name
|
||||
end)
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.extract(modfile)
|
||||
if modfile.type == "zip" then
|
||||
local tempfolder = os.tempfolder()
|
||||
|
||||
if tempfolder ~= nil and
|
||||
tempfolder ~= "" then
|
||||
core.create_dir(tempfolder)
|
||||
if core.extract_zip(modfile.name,tempfolder) then
|
||||
return tempfolder
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function pkgmgr.get_folder_type(path)
|
||||
local testfile = io.open(path .. DIR_DELIM .. "init.lua","r")
|
||||
if testfile ~= nil then
|
||||
testfile:close()
|
||||
return { type = "mod", path = path }
|
||||
end
|
||||
|
||||
testfile = io.open(path .. DIR_DELIM .. "modpack.conf","r")
|
||||
if testfile ~= nil then
|
||||
testfile:close()
|
||||
return { type = "modpack", path = path }
|
||||
end
|
||||
|
||||
testfile = io.open(path .. DIR_DELIM .. "modpack.txt","r")
|
||||
if testfile ~= nil then
|
||||
testfile:close()
|
||||
return { type = "modpack", path = path }
|
||||
end
|
||||
|
||||
testfile = io.open(path .. DIR_DELIM .. "game.conf","r")
|
||||
if testfile ~= nil then
|
||||
testfile:close()
|
||||
return { type = "game", path = path }
|
||||
end
|
||||
|
||||
testfile = io.open(path .. DIR_DELIM .. "texture_pack.conf","r")
|
||||
if testfile ~= nil then
|
||||
testfile:close()
|
||||
return { type = "txp", path = path }
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
function pkgmgr.get_base_folder(temppath)
|
||||
if temppath == nil then
|
||||
return { type = "invalid", path = "" }
|
||||
end
|
||||
|
||||
local ret = pkgmgr.get_folder_type(temppath)
|
||||
if ret then
|
||||
return ret
|
||||
end
|
||||
|
||||
local subdirs = core.get_dir_list(temppath, true)
|
||||
if #subdirs == 1 then
|
||||
ret = pkgmgr.get_folder_type(temppath .. DIR_DELIM .. subdirs[1])
|
||||
if ret then
|
||||
return ret
|
||||
else
|
||||
return { type = "invalid", path = temppath .. DIR_DELIM .. subdirs[1] }
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.isValidModname(modpath)
|
||||
if modpath:find("-") ~= nil then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.parse_register_line(line)
|
||||
local pos1 = line:find("\"")
|
||||
local pos2 = nil
|
||||
if pos1 ~= nil then
|
||||
pos2 = line:find("\"",pos1+1)
|
||||
end
|
||||
|
||||
if pos1 ~= nil and pos2 ~= nil then
|
||||
local item = line:sub(pos1+1,pos2-1)
|
||||
|
||||
if item ~= nil and
|
||||
item ~= "" then
|
||||
local pos3 = item:find(":")
|
||||
|
||||
if pos3 ~= nil then
|
||||
local retval = item:sub(1,pos3-1)
|
||||
if retval ~= nil and
|
||||
retval ~= "" then
|
||||
return retval
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.parse_dofile_line(modpath,line)
|
||||
local pos1 = line:find("\"")
|
||||
local pos2 = nil
|
||||
if pos1 ~= nil then
|
||||
pos2 = line:find("\"",pos1+1)
|
||||
end
|
||||
|
||||
if pos1 ~= nil and pos2 ~= nil then
|
||||
local filename = line:sub(pos1+1,pos2-1)
|
||||
|
||||
if filename ~= nil and
|
||||
filename ~= "" and
|
||||
filename:find(".lua") then
|
||||
return pkgmgr.identify_modname(modpath,filename)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.identify_modname(modpath,filename)
|
||||
local testfile = io.open(modpath .. DIR_DELIM .. filename,"r")
|
||||
if testfile ~= nil then
|
||||
local line = testfile:read()
|
||||
|
||||
while line~= nil do
|
||||
local modname = nil
|
||||
|
||||
if line:find("minetest.register_tool") then
|
||||
modname = pkgmgr.parse_register_line(line)
|
||||
end
|
||||
|
||||
if line:find("minetest.register_craftitem") then
|
||||
modname = pkgmgr.parse_register_line(line)
|
||||
end
|
||||
|
||||
|
||||
if line:find("minetest.register_node") then
|
||||
modname = pkgmgr.parse_register_line(line)
|
||||
end
|
||||
|
||||
if line:find("dofile") then
|
||||
modname = pkgmgr.parse_dofile_line(modpath,line)
|
||||
end
|
||||
|
||||
if modname ~= nil then
|
||||
testfile:close()
|
||||
return modname
|
||||
end
|
||||
|
||||
line = testfile:read()
|
||||
end
|
||||
testfile:close()
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.render_packagelist(render_list)
|
||||
if not render_list then
|
||||
if not pkgmgr.global_mods then
|
||||
pkgmgr.refresh_globals()
|
||||
end
|
||||
render_list = pkgmgr.global_mods
|
||||
end
|
||||
|
||||
local list = render_list:get_list()
|
||||
local retval = {}
|
||||
for i, v in ipairs(list) do
|
||||
local color = ""
|
||||
if v.is_modpack then
|
||||
local rawlist = render_list:get_raw_list()
|
||||
color = mt_color_dark_green
|
||||
|
||||
for j = 1, #rawlist, 1 do
|
||||
if rawlist[j].modpack == list[i].name and
|
||||
not rawlist[j].enabled then
|
||||
-- Modpack not entirely enabled so showing as grey
|
||||
color = mt_color_grey
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif v.is_game_content or v.type == "game" then
|
||||
color = mt_color_blue
|
||||
elseif v.enabled or v.type == "txp" then
|
||||
color = mt_color_green
|
||||
end
|
||||
|
||||
retval[#retval + 1] = color
|
||||
if v.modpack ~= nil or v.loc == "game" then
|
||||
retval[#retval + 1] = "1"
|
||||
else
|
||||
retval[#retval + 1] = "0"
|
||||
end
|
||||
retval[#retval + 1] = core.formspec_escape(v.list_name or v.name)
|
||||
end
|
||||
|
||||
return table.concat(retval, ",")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_dependencies(path)
|
||||
if path == nil then
|
||||
return {}, {}
|
||||
end
|
||||
|
||||
local info = core.get_content_info(path)
|
||||
return info.depends or {}, info.optional_depends or {}
|
||||
end
|
||||
|
||||
----------- tests whether all of the mods in the modpack are enabled -----------
|
||||
function pkgmgr.is_modpack_entirely_enabled(data, name)
|
||||
local rawlist = data.list:get_raw_list()
|
||||
for j = 1, #rawlist do
|
||||
if rawlist[j].modpack == name and not rawlist[j].enabled then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---------- toggles or en/disables a mod or modpack and its dependencies --------
|
||||
function pkgmgr.enable_mod(this, toset)
|
||||
local list = this.data.list:get_list()
|
||||
local mod = list[this.data.selected_mod]
|
||||
|
||||
-- Game mods can't be enabled or disabled
|
||||
if mod.is_game_content then
|
||||
return
|
||||
end
|
||||
|
||||
local toggled_mods = {}
|
||||
|
||||
local enabled_mods = {}
|
||||
if not mod.is_modpack then
|
||||
-- Toggle or en/disable the mod
|
||||
if toset == nil then
|
||||
toset = not mod.enabled
|
||||
end
|
||||
if mod.enabled ~= toset then
|
||||
mod.enabled = toset
|
||||
toggled_mods[#toggled_mods+1] = mod.name
|
||||
end
|
||||
if toset then
|
||||
-- Mark this mod for recursive dependency traversal
|
||||
enabled_mods[mod.name] = true
|
||||
end
|
||||
else
|
||||
-- Toggle or en/disable every mod in the modpack,
|
||||
-- interleaved unsupported
|
||||
for i = 1, #list do
|
||||
if list[i].modpack == mod.name then
|
||||
if toset == nil then
|
||||
toset = not list[i].enabled
|
||||
end
|
||||
if list[i].enabled ~= toset then
|
||||
list[i].enabled = toset
|
||||
toggled_mods[#toggled_mods+1] = list[i].name
|
||||
end
|
||||
if toset then
|
||||
enabled_mods[list[i].name] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not toset then
|
||||
-- Mod(s) were disabled, so no dependencies need to be enabled
|
||||
table.sort(toggled_mods)
|
||||
minetest.log("info", "Following mods were disabled: " ..
|
||||
table.concat(toggled_mods, ", "))
|
||||
return
|
||||
end
|
||||
|
||||
-- Enable mods' depends after activation
|
||||
|
||||
-- Make a list of mod ids indexed by their names
|
||||
local mod_ids = {}
|
||||
for id, mod2 in pairs(list) do
|
||||
if mod2.type == "mod" and not mod2.is_modpack then
|
||||
mod_ids[mod2.name] = id
|
||||
end
|
||||
end
|
||||
|
||||
-- to_enable is used as a DFS stack with sp as stack pointer
|
||||
local to_enable = {}
|
||||
local sp = 0
|
||||
for name in pairs(enabled_mods) do
|
||||
local depends = pkgmgr.get_dependencies(list[mod_ids[name]].path)
|
||||
for i = 1, #depends do
|
||||
local dependency_name = depends[i]
|
||||
if not enabled_mods[dependency_name] then
|
||||
sp = sp+1
|
||||
to_enable[sp] = dependency_name
|
||||
end
|
||||
end
|
||||
end
|
||||
-- If sp is 0, every dependency is already activated
|
||||
while sp > 0 do
|
||||
local name = to_enable[sp]
|
||||
sp = sp-1
|
||||
|
||||
if not enabled_mods[name] then
|
||||
enabled_mods[name] = true
|
||||
local mod_to_enable = list[mod_ids[name]]
|
||||
if not mod_to_enable then
|
||||
minetest.log("warning", "Mod dependency \"" .. name ..
|
||||
"\" not found!")
|
||||
else
|
||||
if mod_to_enable.enabled == false then
|
||||
mod_to_enable.enabled = true
|
||||
toggled_mods[#toggled_mods+1] = mod_to_enable.name
|
||||
end
|
||||
-- Push the dependencies of the dependency onto the stack
|
||||
local depends = pkgmgr.get_dependencies(mod_to_enable.path)
|
||||
for i = 1, #depends do
|
||||
if not enabled_mods[name] then
|
||||
sp = sp+1
|
||||
to_enable[sp] = depends[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Log the list of enabled mods
|
||||
table.sort(toggled_mods)
|
||||
minetest.log("info", "Following mods were enabled: " ..
|
||||
table.concat(toggled_mods, ", "))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_worldconfig(worldpath)
|
||||
local filename = worldpath ..
|
||||
DIR_DELIM .. "world.mt"
|
||||
|
||||
local worldfile = Settings(filename)
|
||||
|
||||
local worldconfig = {}
|
||||
worldconfig.global_mods = {}
|
||||
worldconfig.game_mods = {}
|
||||
|
||||
for key,value in pairs(worldfile:to_table()) do
|
||||
if key == "gameid" then
|
||||
worldconfig.id = value
|
||||
elseif key:sub(0, 9) == "load_mod_" then
|
||||
-- Compatibility: Check against "nil" which was erroneously used
|
||||
-- as value for fresh configured worlds
|
||||
worldconfig.global_mods[key] = value ~= "false" and value ~= "nil"
|
||||
and value
|
||||
else
|
||||
worldconfig[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
--read gamemods
|
||||
local gamespec = pkgmgr.find_by_gameid(worldconfig.id)
|
||||
pkgmgr.get_game_mods(gamespec, worldconfig.game_mods)
|
||||
|
||||
return worldconfig
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.install_dir(type, path, basename, targetpath)
|
||||
local basefolder = pkgmgr.get_base_folder(path)
|
||||
|
||||
-- There's no good way to detect a texture pack, so let's just assume
|
||||
-- it's correct for now.
|
||||
if type == "txp" then
|
||||
if basefolder and basefolder.type ~= "invalid" and basefolder.type ~= "txp" then
|
||||
return nil, fgettext("Unable to install a $1 as a texture pack", basefolder.type)
|
||||
end
|
||||
|
||||
local from = basefolder and basefolder.path or path
|
||||
if targetpath then
|
||||
core.delete_dir(targetpath)
|
||||
core.create_dir(targetpath)
|
||||
else
|
||||
targetpath = core.get_texturepath() .. DIR_DELIM .. basename
|
||||
end
|
||||
if not core.copy_dir(from, targetpath) then
|
||||
return nil,
|
||||
fgettext("Failed to install $1 to $2", basename, targetpath)
|
||||
end
|
||||
return targetpath, nil
|
||||
|
||||
elseif not basefolder then
|
||||
return nil, fgettext("Unable to find a valid mod or modpack")
|
||||
end
|
||||
|
||||
--
|
||||
-- Get destination
|
||||
--
|
||||
if basefolder.type == "modpack" then
|
||||
if type ~= "mod" then
|
||||
return nil, fgettext("Unable to install a modpack as a $1", type)
|
||||
end
|
||||
|
||||
-- Get destination name for modpack
|
||||
if targetpath then
|
||||
core.delete_dir(targetpath)
|
||||
core.create_dir(targetpath)
|
||||
else
|
||||
local clean_path = nil
|
||||
if basename ~= nil then
|
||||
clean_path = basename
|
||||
end
|
||||
if not clean_path then
|
||||
clean_path = get_last_folder(cleanup_path(basefolder.path))
|
||||
end
|
||||
if clean_path then
|
||||
targetpath = core.get_clientmodpath() .. DIR_DELIM .. clean_path
|
||||
else
|
||||
return nil,
|
||||
fgettext("Install Mod: Unable to find suitable folder name for modpack $1",
|
||||
path)
|
||||
end
|
||||
end
|
||||
elseif basefolder.type == "mod" then
|
||||
if type ~= "mod" then
|
||||
return nil, fgettext("Unable to install a mod as a $1", type)
|
||||
end
|
||||
|
||||
if targetpath then
|
||||
core.delete_dir(targetpath)
|
||||
core.create_dir(targetpath)
|
||||
else
|
||||
local targetfolder = basename
|
||||
if targetfolder == nil then
|
||||
targetfolder = pkgmgr.identify_modname(basefolder.path, "init.lua")
|
||||
end
|
||||
|
||||
-- If heuristic failed try to use current foldername
|
||||
if targetfolder == nil then
|
||||
targetfolder = get_last_folder(basefolder.path)
|
||||
end
|
||||
|
||||
if targetfolder ~= nil and pkgmgr.isValidModname(targetfolder) then
|
||||
targetpath = core.get_clientmodpath() .. DIR_DELIM .. targetfolder
|
||||
else
|
||||
return nil, fgettext("Install Mod: Unable to find real mod name for: $1", path)
|
||||
end
|
||||
end
|
||||
|
||||
elseif basefolder.type == "game" then
|
||||
if type ~= "game" then
|
||||
return nil, fgettext("Unable to install a game as a $1", type)
|
||||
end
|
||||
|
||||
if targetpath then
|
||||
core.delete_dir(targetpath)
|
||||
core.create_dir(targetpath)
|
||||
else
|
||||
targetpath = core.get_gamepath() .. DIR_DELIM .. basename
|
||||
end
|
||||
end
|
||||
|
||||
-- Copy it
|
||||
if not core.copy_dir(basefolder.path, targetpath) then
|
||||
return nil,
|
||||
fgettext("Failed to install $1 to $2", basename, targetpath)
|
||||
end
|
||||
|
||||
if basefolder.type == "game" then
|
||||
pkgmgr.update_gamelist()
|
||||
else
|
||||
pkgmgr.refresh_globals()
|
||||
end
|
||||
|
||||
return targetpath, nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.install(type, modfilename, basename, dest)
|
||||
local archive_info = pkgmgr.identify_filetype(modfilename)
|
||||
local path = pkgmgr.extract(archive_info)
|
||||
|
||||
if path == nil then
|
||||
return nil,
|
||||
fgettext("Install: file: \"$1\"", archive_info.name) .. "\n" ..
|
||||
fgettext("Install: Unsupported file type \"$1\" or broken archive",
|
||||
archive_info.type)
|
||||
end
|
||||
|
||||
local targetpath, msg = pkgmgr.install_dir(type, path, basename, dest)
|
||||
core.delete_dir(path)
|
||||
return targetpath, msg
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.prepareclientmodlist(data)
|
||||
local retval = {}
|
||||
|
||||
local clientmods = {}
|
||||
|
||||
--read clientmods
|
||||
local modpath = core.get_clientmodpath()
|
||||
|
||||
if modpath ~= nil and
|
||||
modpath ~= "" then
|
||||
get_mods(modpath,clientmods)
|
||||
end
|
||||
|
||||
for i=1,#clientmods,1 do
|
||||
clientmods[i].type = "mod"
|
||||
clientmods[i].loc = "global"
|
||||
clientmods[i].is_clientside = true
|
||||
retval[#retval + 1] = clientmods[i]
|
||||
end
|
||||
|
||||
--read mods configuration
|
||||
local filename = modpath ..
|
||||
DIR_DELIM .. "mods.conf"
|
||||
|
||||
local conffile = Settings(filename)
|
||||
|
||||
for key,value in pairs(conffile:to_table()) do
|
||||
if key:sub(1, 9) == "load_mod_" then
|
||||
key = key:sub(10)
|
||||
local element = nil
|
||||
for i=1,#retval,1 do
|
||||
if retval[i].name == key and
|
||||
not retval[i].is_modpack then
|
||||
element = retval[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
if element ~= nil then
|
||||
element.enabled = value ~= "false" and value ~= "nil" and value
|
||||
else
|
||||
core.log("info", "Clientmod: " .. key .. " " .. dump(value) .. " but not found")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
function pkgmgr.preparemodlist(data)
|
||||
local retval = {}
|
||||
|
||||
local global_mods = {}
|
||||
local game_mods = {}
|
||||
|
||||
--read global mods
|
||||
local modpath = core.get_modpath()
|
||||
|
||||
if modpath ~= nil and
|
||||
modpath ~= "" then
|
||||
get_mods(modpath,global_mods)
|
||||
end
|
||||
|
||||
for i=1,#global_mods,1 do
|
||||
global_mods[i].type = "mod"
|
||||
global_mods[i].loc = "global"
|
||||
retval[#retval + 1] = global_mods[i]
|
||||
end
|
||||
|
||||
--read game mods
|
||||
local gamespec = pkgmgr.find_by_gameid(data.gameid)
|
||||
pkgmgr.get_game_mods(gamespec, game_mods)
|
||||
|
||||
if #game_mods > 0 then
|
||||
-- Add title
|
||||
retval[#retval + 1] = {
|
||||
type = "game",
|
||||
is_game_content = true,
|
||||
name = fgettext("$1 mods", gamespec.name),
|
||||
path = gamespec.path
|
||||
}
|
||||
end
|
||||
|
||||
for i=1,#game_mods,1 do
|
||||
game_mods[i].type = "mod"
|
||||
game_mods[i].loc = "game"
|
||||
game_mods[i].is_game_content = true
|
||||
retval[#retval + 1] = game_mods[i]
|
||||
end
|
||||
|
||||
if data.worldpath == nil then
|
||||
return retval
|
||||
end
|
||||
|
||||
--read world mod configuration
|
||||
local filename = data.worldpath ..
|
||||
DIR_DELIM .. "world.mt"
|
||||
|
||||
local worldfile = Settings(filename)
|
||||
|
||||
for key,value in pairs(worldfile:to_table()) do
|
||||
if key:sub(1, 9) == "load_mod_" then
|
||||
key = key:sub(10)
|
||||
local element = nil
|
||||
for i=1,#retval,1 do
|
||||
if retval[i].name == key and
|
||||
not retval[i].is_modpack then
|
||||
element = retval[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
if element ~= nil then
|
||||
element.enabled = value ~= "false" and value ~= "nil" and value
|
||||
else
|
||||
core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
function pkgmgr.compare_package(a, b)
|
||||
return a and b and a.name == b.name and a.path == b.path
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.comparemod(elem1,elem2)
|
||||
if elem1 == nil or elem2 == nil then
|
||||
return false
|
||||
end
|
||||
if elem1.name ~= elem2.name then
|
||||
return false
|
||||
end
|
||||
if elem1.is_modpack ~= elem2.is_modpack then
|
||||
return false
|
||||
end
|
||||
if elem1.type ~= elem2.type then
|
||||
return false
|
||||
end
|
||||
if elem1.modpack ~= elem2.modpack then
|
||||
return false
|
||||
end
|
||||
|
||||
if elem1.path ~= elem2.path then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.mod_exists(basename)
|
||||
|
||||
if pkgmgr.global_mods == nil then
|
||||
pkgmgr.refresh_globals()
|
||||
end
|
||||
|
||||
if pkgmgr.global_mods:raw_index_by_uid(basename) > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_global_mod(idx)
|
||||
|
||||
if pkgmgr.global_mods == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
if idx == nil or idx < 1 or
|
||||
idx > pkgmgr.global_mods:size() then
|
||||
return nil
|
||||
end
|
||||
|
||||
return pkgmgr.global_mods:get_list()[idx]
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.refresh_globals()
|
||||
local function is_equal(element,uid) --uid match
|
||||
if element.name == uid then
|
||||
return true
|
||||
end
|
||||
end
|
||||
pkgmgr.global_mods = filterlist.create(pkgmgr.preparemodlist,
|
||||
pkgmgr.comparemod, is_equal, nil, {})
|
||||
pkgmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
|
||||
pkgmgr.global_mods:set_sortmode("alphabetic")
|
||||
pkgmgr.clientmods = filterlist.create(pkgmgr.prepareclientmodlist,
|
||||
pkgmgr.comparemod, is_equal, nil, {})
|
||||
pkgmgr.clientmods:add_sort_mechanism("alphabetic", sort_mod_list)
|
||||
pkgmgr.clientmods:set_sortmode("alphabetic")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.identify_filetype(name)
|
||||
|
||||
if name:sub(-3):lower() == "zip" then
|
||||
return {
|
||||
name = name,
|
||||
type = "zip"
|
||||
}
|
||||
end
|
||||
|
||||
if name:sub(-6):lower() == "tar.gz" or
|
||||
name:sub(-3):lower() == "tgz"then
|
||||
return {
|
||||
name = name,
|
||||
type = "tgz"
|
||||
}
|
||||
end
|
||||
|
||||
if name:sub(-6):lower() == "tar.bz2" then
|
||||
return {
|
||||
name = name,
|
||||
type = "tbz"
|
||||
}
|
||||
end
|
||||
|
||||
if name:sub(-2):lower() == "7z" then
|
||||
return {
|
||||
name = name,
|
||||
type = "7z"
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
name = name,
|
||||
type = "ukn"
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.find_by_gameid(gameid)
|
||||
for i=1,#pkgmgr.games,1 do
|
||||
if pkgmgr.games[i].id == gameid then
|
||||
return pkgmgr.games[i], i
|
||||
end
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_game_mods(gamespec, retval)
|
||||
if gamespec ~= nil and
|
||||
gamespec.gamemods_path ~= nil and
|
||||
gamespec.gamemods_path ~= "" then
|
||||
get_mods(gamespec.gamemods_path, retval)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_game_modlist(gamespec)
|
||||
local retval = ""
|
||||
local game_mods = {}
|
||||
pkgmgr.get_game_mods(gamespec, game_mods)
|
||||
for i=1,#game_mods,1 do
|
||||
if retval ~= "" then
|
||||
retval = retval..","
|
||||
end
|
||||
retval = retval .. game_mods[i].name
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_game(index)
|
||||
if index > 0 and index <= #pkgmgr.games then
|
||||
return pkgmgr.games[index]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.update_gamelist()
|
||||
pkgmgr.games = core.get_games()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.gamelist()
|
||||
local retval = ""
|
||||
if #pkgmgr.games > 0 then
|
||||
retval = retval .. core.formspec_escape(pkgmgr.games[1].name)
|
||||
|
||||
for i=2,#pkgmgr.games,1 do
|
||||
retval = retval .. "," .. core.formspec_escape(pkgmgr.games[i].name)
|
||||
end
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- read initial data
|
||||
--------------------------------------------------------------------------------
|
||||
pkgmgr.update_gamelist()
|
289
builtin/mainmenu/tab_content.lua
Normal file
289
builtin/mainmenu/tab_content.lua
Normal file
@ -0,0 +1,289 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
local packages_raw
|
||||
local packages
|
||||
|
||||
local function modname_valid(name)
|
||||
return not name:find("[^a-z0-9_]")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function get_formspec(tabview, name, tabdata)
|
||||
if pkgmgr.global_mods == nil then
|
||||
pkgmgr.refresh_globals()
|
||||
end
|
||||
if pkgmgr.games == nil then
|
||||
pkgmgr.update_gamelist()
|
||||
end
|
||||
|
||||
if packages == nil then
|
||||
packages_raw = {}
|
||||
table.insert_all(packages_raw, pkgmgr.games)
|
||||
table.insert_all(packages_raw, pkgmgr.get_texture_packs())
|
||||
table.insert_all(packages_raw, pkgmgr.global_mods:get_list())
|
||||
table.insert_all(packages_raw, pkgmgr.clientmods:get_list())
|
||||
|
||||
local function get_data()
|
||||
return packages_raw
|
||||
end
|
||||
|
||||
local function is_equal(element, uid) --uid match
|
||||
return (element.type == "game" and element.id == uid) or
|
||||
element.name == uid
|
||||
end
|
||||
|
||||
packages = filterlist.create(get_data, pkgmgr.compare_package,
|
||||
is_equal, nil, {})
|
||||
|
||||
local filename = core.get_clientmodpath() .. DIR_DELIM .. "mods.conf"
|
||||
|
||||
local conffile = Settings(filename)
|
||||
local mods = conffile:to_table()
|
||||
|
||||
for i = 1, #packages_raw do
|
||||
local mod = packages_raw[i]
|
||||
if mod.is_clientside and not mod.is_modpack then
|
||||
if modname_valid(mod.name) then
|
||||
conffile:set("load_mod_" .. mod.name,
|
||||
mod.enabled and "true" or "false")
|
||||
elseif mod.enabled then
|
||||
gamedata.errormessage = fgettext_ne("Failed to enable clientmo" ..
|
||||
"d \"$1\" as it contains disallowed characters. " ..
|
||||
"Only characters [a-z0-9_] are allowed.",
|
||||
mod.name)
|
||||
end
|
||||
mods["load_mod_" .. mod.name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove mods that are not present anymore
|
||||
for key in pairs(mods) do
|
||||
if key:sub(1, 9) == "load_mod_" then
|
||||
conffile:remove(key)
|
||||
end
|
||||
end
|
||||
|
||||
if not conffile:write() then
|
||||
core.log("error", "Failed to write clientmod config file")
|
||||
end
|
||||
end
|
||||
|
||||
if tabdata.selected_pkg == nil then
|
||||
tabdata.selected_pkg = 1
|
||||
end
|
||||
|
||||
|
||||
local retval =
|
||||
"label[0.05,-0.25;".. fgettext("Installed Packages:") .. "]" ..
|
||||
"tablecolumns[color;tree;text]" ..
|
||||
"table[0,0.25;5.1,4.3;pkglist;" ..
|
||||
pkgmgr.render_packagelist(packages) ..
|
||||
";" .. tabdata.selected_pkg .. "]" ..
|
||||
"button[0,4.85;5.25,0.5;btn_contentdb;".. fgettext("Browse online content") .. "]"
|
||||
|
||||
|
||||
local selected_pkg
|
||||
if filterlist.size(packages) >= tabdata.selected_pkg then
|
||||
selected_pkg = packages:get_list()[tabdata.selected_pkg]
|
||||
end
|
||||
|
||||
if selected_pkg ~= nil then
|
||||
--check for screenshot beeing available
|
||||
local screenshotfilename = selected_pkg.path .. DIR_DELIM .. "screenshot.png"
|
||||
local screenshotfile, error = io.open(screenshotfilename, "r")
|
||||
|
||||
local modscreenshot
|
||||
if error == nil then
|
||||
screenshotfile:close()
|
||||
modscreenshot = screenshotfilename
|
||||
end
|
||||
|
||||
if modscreenshot == nil then
|
||||
modscreenshot = defaulttexturedir .. "no_screenshot.png"
|
||||
end
|
||||
|
||||
local info = core.get_content_info(selected_pkg.path)
|
||||
local desc = fgettext("No package description available")
|
||||
if info.description and info.description:trim() ~= "" then
|
||||
desc = info.description
|
||||
end
|
||||
|
||||
retval = retval ..
|
||||
"image[5.5,0;3,2;" .. core.formspec_escape(modscreenshot) .. "]" ..
|
||||
"label[8.25,0.6;" .. core.formspec_escape(selected_pkg.name) .. "]" ..
|
||||
"box[5.5,2.2;6.15,2.35;#000]"
|
||||
|
||||
if selected_pkg.type == "mod" then
|
||||
if selected_pkg.is_modpack then
|
||||
if selected_pkg.is_clientside then
|
||||
if pkgmgr.is_modpack_entirely_enabled({list = packages}, selected_pkg.name) then
|
||||
retval = retval ..
|
||||
"button[8.65,4.65;3.25,1;btn_mod_mgr_mp_disable;" ..
|
||||
fgettext("Disable modpack") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"button[8.65,4.65;3.25,1;btn_mod_mgr_mp_enable;" ..
|
||||
fgettext("Enable modpack") .. "]"
|
||||
end
|
||||
else
|
||||
retval = retval ..
|
||||
"button[8.65,4.65;3.25,1;btn_mod_mgr_rename_modpack;" ..
|
||||
fgettext("Rename") .. "]"
|
||||
end
|
||||
else
|
||||
--show dependencies
|
||||
desc = desc .. "\n\n"
|
||||
local toadd_hard = table.concat(info.depends or {}, "\n")
|
||||
local toadd_soft = table.concat(info.optional_depends or {}, "\n")
|
||||
if toadd_hard == "" and toadd_soft == "" then
|
||||
desc = desc .. fgettext("No dependencies.")
|
||||
else
|
||||
if toadd_hard ~= "" then
|
||||
desc = desc ..fgettext("Dependencies:") ..
|
||||
"\n" .. toadd_hard
|
||||
end
|
||||
if toadd_soft ~= "" then
|
||||
if toadd_hard ~= "" then
|
||||
desc = desc .. "\n\n"
|
||||
end
|
||||
desc = desc .. fgettext("Optional dependencies:") ..
|
||||
"\n" .. toadd_soft
|
||||
end
|
||||
end
|
||||
if selected_pkg.is_clientside then
|
||||
if selected_pkg.enabled then
|
||||
retval = retval ..
|
||||
"button[8.65,4.65;3.25,1;btn_mod_mgr_disable_mod;" ..
|
||||
fgettext("Disable") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"button[8.65,4.65;3.25,1;btn_mod_mgr_enable_mod;" ..
|
||||
fgettext("Enable") .. "]"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
if selected_pkg.type == "txp" then
|
||||
if selected_pkg.enabled then
|
||||
retval = retval ..
|
||||
"button[8.65,4.65;3.25,1;btn_mod_mgr_disable_txp;" ..
|
||||
fgettext("Disable Texture Pack") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"button[8.65,4.65;3.25,1;btn_mod_mgr_use_txp;" ..
|
||||
fgettext("Use Texture Pack") .. "]"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
retval = retval .. "textarea[5.85,2.2;6.35,2.9;;" ..
|
||||
fgettext("Information:") .. ";" .. desc .. "]"
|
||||
|
||||
if core.may_modify_path(selected_pkg.path) then
|
||||
retval = retval ..
|
||||
"button[5.5,4.65;3.25,1;btn_mod_mgr_delete_mod;" ..
|
||||
fgettext("Uninstall Package") .. "]"
|
||||
end
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function handle_buttons(tabview, fields, tabname, tabdata)
|
||||
if fields["pkglist"] ~= nil then
|
||||
local event = core.explode_table_event(fields["pkglist"])
|
||||
tabdata.selected_pkg = event.row
|
||||
local mod = packages:get_list()[tabdata.selected_pkg]
|
||||
|
||||
if event.type == "DCL" and mod.is_clientside then
|
||||
pkgmgr.enable_mod({data = {list = packages, selected_mod = tabdata.selected_pkg}})
|
||||
packages = nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mod_mgr_mp_enable ~= nil or
|
||||
fields.btn_mod_mgr_mp_disable ~= nil then
|
||||
pkgmgr.enable_mod({data = {list = packages, selected_mod = tabdata.selected_pkg}}, fields.btn_mod_mgr_mp_enable ~= nil)
|
||||
packages = nil
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mod_mgr_enable_mod ~= nil or
|
||||
fields.btn_mod_mgr_disable_mod ~= nil then
|
||||
pkgmgr.enable_mod({data = {list = packages, selected_mod = tabdata.selected_pkg}}, fields.btn_mod_mgr_enable_mod ~= nil)
|
||||
packages = nil
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["btn_contentdb"] ~= nil then
|
||||
local dlg = create_store_dlg()
|
||||
dlg:set_parent(tabview)
|
||||
tabview:hide()
|
||||
dlg:show()
|
||||
packages = nil
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["btn_mod_mgr_rename_modpack"] ~= nil then
|
||||
local mod = packages:get_list()[tabdata.selected_pkg]
|
||||
local dlg_renamemp = create_rename_modpack_dlg(mod)
|
||||
dlg_renamemp:set_parent(tabview)
|
||||
tabview:hide()
|
||||
dlg_renamemp:show()
|
||||
packages = nil
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["btn_mod_mgr_delete_mod"] ~= nil then
|
||||
local mod = packages:get_list()[tabdata.selected_pkg]
|
||||
local dlg_delmod = create_delete_content_dlg(mod)
|
||||
dlg_delmod:set_parent(tabview)
|
||||
tabview:hide()
|
||||
dlg_delmod:show()
|
||||
packages = nil
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mod_mgr_use_txp then
|
||||
local txp = packages:get_list()[tabdata.selected_pkg]
|
||||
core.settings:set("texture_path", txp.path)
|
||||
packages = nil
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
if fields.btn_mod_mgr_disable_txp then
|
||||
core.settings:set("texture_path", "")
|
||||
packages = nil
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
return {
|
||||
name = "content",
|
||||
caption = fgettext("Content"),
|
||||
cbf_formspec = get_formspec,
|
||||
cbf_button_handler = handle_buttons,
|
||||
on_change = pkgmgr.update_gamelist
|
||||
}
|
135
builtin/mainmenu/tab_credits.lua
Normal file
135
builtin/mainmenu/tab_credits.lua
Normal file
@ -0,0 +1,135 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local dragonfire_team = {
|
||||
"Elias Fleckenstein [Main Developer]",
|
||||
"cora [Core Developer]",
|
||||
"emilia [Core Developer]",
|
||||
"oneplustwo [Developer]",
|
||||
"joshia_wi [Developer]",
|
||||
"Code-Sploit [Developer]",
|
||||
"DerZombiiie [User Support]",
|
||||
}
|
||||
|
||||
local core_developers = {
|
||||
"Perttu Ahola (celeron55) <celeron55@gmail.com>",
|
||||
"sfan5 <sfan5@live.de>",
|
||||
"Nathanaël Courant (Nore/Ekdohibs) <nore@mesecons.net>",
|
||||
"Loic Blot (nerzhul/nrz) <loic.blot@unix-experience.fr>",
|
||||
"paramat",
|
||||
"Auke Kok (sofar) <sofar@foo-projects.org>",
|
||||
"Andrew Ward (rubenwardy) <rw@rubenwardy.com>",
|
||||
"Krock/SmallJoker <mk939@ymail.com>",
|
||||
"Lars Hofhansl <larsh@apache.org>",
|
||||
}
|
||||
|
||||
local active_contributors = {
|
||||
"Hugues Ross [Formspecs]",
|
||||
"Maksim (MoNTE48) [Android]",
|
||||
"DS [Formspecs]",
|
||||
"pyrollo [Formspecs: Hypertext]",
|
||||
"v-rob [Formspecs]",
|
||||
"Jordach [set_sky]",
|
||||
"random-geek [Formspecs]",
|
||||
"Wuzzy [Pathfinder, builtin, translations]",
|
||||
"ANAND (ClobberXD) [Fixes, per-player FOV]",
|
||||
"Warr1024 [Fixes]",
|
||||
"Paul Ouellette (pauloue) [Fixes, Script API]",
|
||||
"Jean-Patrick G (kilbith) <jeanpatrick.guerrero@gmail.com> [Audiovisuals]",
|
||||
"HybridDog [Script API]",
|
||||
"dcbrwn [Object shading]",
|
||||
"srifqi [Fixes]",
|
||||
}
|
||||
|
||||
local previous_core_developers = {
|
||||
"BlockMen",
|
||||
"Maciej Kasatkin (RealBadAngel) [RIP]",
|
||||
"Lisa Milne (darkrose) <lisa@ltmnet.com>",
|
||||
"proller",
|
||||
"Ilya Zhuravlev (xyz) <xyz@minetest.net>",
|
||||
"PilzAdam <pilzadam@minetest.net>",
|
||||
"est31 <MTest31@outlook.com>",
|
||||
"kahrl <kahrl@gmx.net>",
|
||||
"Ryan Kwolek (kwolekr) <kwolekr@minetest.net>",
|
||||
"sapier",
|
||||
"Zeno",
|
||||
"ShadowNinja <shadowninja@minetest.net>",
|
||||
}
|
||||
|
||||
local previous_contributors = {
|
||||
"Nils Dagsson Moskopp (erlehmann) <nils@dieweltistgarnichtso.net> [Minetest Logo]",
|
||||
"Dániel Juhász (juhdanad) <juhdanad@gmail.com>",
|
||||
"red-001 <red-001@outlook.ie>",
|
||||
"numberZero [Audiovisuals: meshgen]",
|
||||
"Giuseppe Bilotta",
|
||||
"MirceaKitsune <mirceakitsune@gmail.com>",
|
||||
"Constantin Wenger (SpeedProg)",
|
||||
"Ciaran Gultnieks (CiaranG)",
|
||||
"stujones11 [Android UX improvements]",
|
||||
"Jeija <jeija@mesecons.net> [HTTP, particles]",
|
||||
"Vincent Glize (Dumbeldor) [Cleanups, CSM APIs]",
|
||||
"Ben Deutsch [Rendering, Fixes, SQLite auth]",
|
||||
"TeTpaAka [Hand overriding, nametag colors]",
|
||||
"Rui [Sound Pitch]",
|
||||
"Duane Robertson <duane@duanerobertson.com> [MGValleys]",
|
||||
"Raymoo [Tool Capabilities]",
|
||||
"Rogier <rogier777@gmail.com> [Fixes]",
|
||||
"Gregory Currie (gregorycu) [optimisation]",
|
||||
"TriBlade9 <triblade9@mail.com> [Audiovisuals]",
|
||||
"T4im [Profiler]",
|
||||
"Jurgen Doser (doserj) <jurgen.doser@gmail.com>",
|
||||
}
|
||||
|
||||
local function buildCreditList(source)
|
||||
local ret = {}
|
||||
for i = 1, #source do
|
||||
ret[i] = core.formspec_escape(source[i])
|
||||
end
|
||||
return table.concat(ret, ",,")
|
||||
end
|
||||
|
||||
return {
|
||||
name = "credits",
|
||||
caption = fgettext("Credits"),
|
||||
cbf_formspec = function(tabview, name, tabdata)
|
||||
local logofile = defaulttexturedir .. "logo.png"
|
||||
local version = core.get_version()
|
||||
return "image[0.5,1;" .. core.formspec_escape(logofile) .. "]" ..
|
||||
"label[0.5,2.8;" .. version.project .. " " .. version.string .. "]" ..
|
||||
"button[0.5,3;2,2;homepage;minetest.net]" ..
|
||||
"tablecolumns[color;text]" ..
|
||||
"tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
|
||||
"table[3.5,-0.25;8.5,6.05;list_credits;" ..
|
||||
"#FFFF00," .. fgettext("Dragonfire Team") .. ",," ..
|
||||
buildCreditList(dragonfire_team) .. ",,," ..
|
||||
"#FFFF00," .. fgettext("Core Developers") .. ",," ..
|
||||
buildCreditList(core_developers) .. ",,," ..
|
||||
"#FFFF00," .. fgettext("Active Contributors") .. ",," ..
|
||||
buildCreditList(active_contributors) .. ",,," ..
|
||||
"#FFFF00," .. fgettext("Previous Core Developers") ..",," ..
|
||||
buildCreditList(previous_core_developers) .. ",,," ..
|
||||
"#FFFF00," .. fgettext("Previous Contributors") .. ",," ..
|
||||
buildCreditList(previous_contributors) .. "," ..
|
||||
";1]"
|
||||
end,
|
||||
cbf_button_handler = function(this, fields, name, tabdata)
|
||||
if fields.homepage then
|
||||
core.open_url("https://www.minetest.net")
|
||||
end
|
||||
end,
|
||||
}
|
333
builtin/mainmenu/tab_local.lua
Normal file
333
builtin/mainmenu/tab_local.lua
Normal file
@ -0,0 +1,333 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
|
||||
local enable_gamebar = PLATFORM ~= "Android"
|
||||
local current_game, singleplayer_refresh_gamebar
|
||||
if enable_gamebar then
|
||||
function current_game()
|
||||
local last_game_id = core.settings:get("menu_last_game")
|
||||
local game = pkgmgr.find_by_gameid(last_game_id)
|
||||
|
||||
return game
|
||||
end
|
||||
|
||||
function singleplayer_refresh_gamebar()
|
||||
|
||||
local old_bar = ui.find_by_name("game_button_bar")
|
||||
|
||||
if old_bar ~= nil then
|
||||
old_bar:delete()
|
||||
end
|
||||
|
||||
local function game_buttonbar_button_handler(fields)
|
||||
if fields.game_open_cdb then
|
||||
local maintab = ui.find_by_name("maintab")
|
||||
local dlg = create_store_dlg("game")
|
||||
dlg:set_parent(maintab)
|
||||
maintab:hide()
|
||||
dlg:show()
|
||||
return true
|
||||
end
|
||||
|
||||
for key,value in pairs(fields) do
|
||||
for j=1,#pkgmgr.games,1 do
|
||||
if ("game_btnbar_" .. pkgmgr.games[j].id == key) then
|
||||
mm_texture.update("singleplayer", pkgmgr.games[j])
|
||||
core.set_topleft_text(pkgmgr.games[j].name)
|
||||
core.settings:set("menu_last_game",pkgmgr.games[j].id)
|
||||
menudata.worldlist:set_filtercriteria(pkgmgr.games[j].id)
|
||||
local index = filterlist.get_current_index(menudata.worldlist,
|
||||
tonumber(core.settings:get("mainmenu_last_selected_world")))
|
||||
if not index or index < 1 then
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
if selected ~= nil and selected < #menudata.worldlist:get_list() then
|
||||
index = selected
|
||||
else
|
||||
index = #menudata.worldlist:get_list()
|
||||
end
|
||||
end
|
||||
menu_worldmt_legacy(index)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local btnbar = buttonbar_create("game_button_bar",
|
||||
game_buttonbar_button_handler,
|
||||
{x=-0.3,y=5.9}, "horizontal", {x=12.4,y=1.15})
|
||||
|
||||
for i=1,#pkgmgr.games,1 do
|
||||
local btn_name = "game_btnbar_" .. pkgmgr.games[i].id
|
||||
|
||||
local image = nil
|
||||
local text = nil
|
||||
local tooltip = core.formspec_escape(pkgmgr.games[i].name)
|
||||
|
||||
if pkgmgr.games[i].menuicon_path ~= nil and
|
||||
pkgmgr.games[i].menuicon_path ~= "" then
|
||||
image = core.formspec_escape(pkgmgr.games[i].menuicon_path)
|
||||
else
|
||||
|
||||
local part1 = pkgmgr.games[i].id:sub(1,5)
|
||||
local part2 = pkgmgr.games[i].id:sub(6,10)
|
||||
local part3 = pkgmgr.games[i].id:sub(11)
|
||||
|
||||
text = part1 .. "\n" .. part2
|
||||
if part3 ~= nil and
|
||||
part3 ~= "" then
|
||||
text = text .. "\n" .. part3
|
||||
end
|
||||
end
|
||||
btnbar:add_button(btn_name, text, image, tooltip)
|
||||
end
|
||||
|
||||
local plus_image = core.formspec_escape(defaulttexturedir .. "plus.png")
|
||||
btnbar:add_button("game_open_cdb", "", plus_image, fgettext("Install games from ContentDB"))
|
||||
end
|
||||
else
|
||||
function current_game()
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local function get_formspec(tabview, name, tabdata)
|
||||
local retval = ""
|
||||
|
||||
local index = filterlist.get_current_index(menudata.worldlist,
|
||||
tonumber(core.settings:get("mainmenu_last_selected_world"))
|
||||
)
|
||||
|
||||
retval = retval ..
|
||||
"button[3.9,3.8;2.8,1;world_delete;".. fgettext("Delete") .. "]" ..
|
||||
"button[6.55,3.8;2.8,1;world_configure;".. fgettext("Configure") .. "]" ..
|
||||
"button[9.2,3.8;2.8,1;world_create;".. fgettext("New") .. "]" ..
|
||||
"label[3.9,-0.05;".. fgettext("Select World:") .. "]"..
|
||||
"checkbox[0,-0.20;cb_creative_mode;".. fgettext("Creative Mode") .. ";" ..
|
||||
dump(core.settings:get_bool("creative_mode")) .. "]"..
|
||||
"checkbox[0,0.25;cb_enable_damage;".. fgettext("Enable Damage") .. ";" ..
|
||||
dump(core.settings:get_bool("enable_damage")) .. "]"..
|
||||
"checkbox[0,0.7;cb_server;".. fgettext("Host Server") ..";" ..
|
||||
dump(core.settings:get_bool("enable_server")) .. "]" ..
|
||||
"textlist[3.9,0.4;7.9,3.45;sp_worlds;" ..
|
||||
menu_render_worldlist() ..
|
||||
";" .. index .. "]"
|
||||
|
||||
if core.settings:get_bool("enable_server") then
|
||||
retval = retval ..
|
||||
"button[7.9,4.75;4.1,1;play;".. fgettext("Host Game") .. "]" ..
|
||||
"checkbox[0,1.15;cb_server_announce;" .. fgettext("Announce Server") .. ";" ..
|
||||
dump(core.settings:get_bool("server_announce")) .. "]" ..
|
||||
"field[0.3,2.85;3.8,0.5;te_playername;" .. fgettext("Name") .. ";" ..
|
||||
core.formspec_escape(core.settings:get("name")) .. "]" ..
|
||||
"pwdfield[0.3,4.05;3.8,0.5;te_passwd;" .. fgettext("Password") .. "]"
|
||||
|
||||
local bind_addr = core.settings:get("bind_address")
|
||||
if bind_addr ~= nil and bind_addr ~= "" then
|
||||
retval = retval ..
|
||||
"field[0.3,5.25;2.5,0.5;te_serveraddr;" .. fgettext("Bind Address") .. ";" ..
|
||||
core.formspec_escape(core.settings:get("bind_address")) .. "]" ..
|
||||
"field[2.85,5.25;1.25,0.5;te_serverport;" .. fgettext("Port") .. ";" ..
|
||||
core.formspec_escape(core.settings:get("port")) .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"field[0.3,5.25;3.8,0.5;te_serverport;" .. fgettext("Server Port") .. ";" ..
|
||||
core.formspec_escape(core.settings:get("port")) .. "]"
|
||||
end
|
||||
else
|
||||
retval = retval ..
|
||||
"button[7.9,4.75;4.1,1;play;" .. fgettext("Play Game") .. "]"
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
local function main_button_handler(this, fields, name, tabdata)
|
||||
|
||||
assert(name == "local")
|
||||
|
||||
local world_doubleclick = false
|
||||
|
||||
if fields["sp_worlds"] ~= nil then
|
||||
local event = core.explode_textlist_event(fields["sp_worlds"])
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
|
||||
menu_worldmt_legacy(selected)
|
||||
|
||||
if event.type == "DCL" then
|
||||
world_doubleclick = true
|
||||
end
|
||||
|
||||
if event.type == "CHG" and selected ~= nil then
|
||||
core.settings:set("mainmenu_last_selected_world",
|
||||
menudata.worldlist:get_raw_index(selected))
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if menu_handle_key_up_down(fields,"sp_worlds","mainmenu_last_selected_world") then
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["cb_creative_mode"] then
|
||||
core.settings:set("creative_mode", fields["cb_creative_mode"])
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
menu_worldmt(selected, "creative_mode", fields["cb_creative_mode"])
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["cb_enable_damage"] then
|
||||
core.settings:set("enable_damage", fields["cb_enable_damage"])
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
menu_worldmt(selected, "enable_damage", fields["cb_enable_damage"])
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["cb_server"] then
|
||||
core.settings:set("enable_server", fields["cb_server"])
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["cb_server_announce"] then
|
||||
core.settings:set("server_announce", fields["cb_server_announce"])
|
||||
local selected = core.get_textlist_index("srv_worlds")
|
||||
menu_worldmt(selected, "server_announce", fields["cb_server_announce"])
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["play"] ~= nil or world_doubleclick or fields["key_enter"] then
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
gamedata.selected_world = menudata.worldlist:get_raw_index(selected)
|
||||
|
||||
if selected == nil or gamedata.selected_world == 0 then
|
||||
gamedata.errormessage =
|
||||
fgettext("No world created or selected!")
|
||||
return true
|
||||
end
|
||||
|
||||
-- Update last game
|
||||
local world = menudata.worldlist:get_raw_element(gamedata.selected_world)
|
||||
if world then
|
||||
local game = pkgmgr.find_by_gameid(world.gameid)
|
||||
core.settings:set("menu_last_game", game.id)
|
||||
end
|
||||
|
||||
if core.settings:get_bool("enable_server") then
|
||||
gamedata.playername = fields["te_playername"]
|
||||
gamedata.password = fields["te_passwd"]
|
||||
gamedata.port = fields["te_serverport"]
|
||||
gamedata.address = ""
|
||||
|
||||
core.settings:set("port",gamedata.port)
|
||||
if fields["te_serveraddr"] ~= nil then
|
||||
core.settings:set("bind_address",fields["te_serveraddr"])
|
||||
end
|
||||
else
|
||||
gamedata.singleplayer = true
|
||||
end
|
||||
|
||||
core.start()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["world_create"] ~= nil then
|
||||
local create_world_dlg = create_create_world_dlg(true)
|
||||
create_world_dlg:set_parent(this)
|
||||
this:hide()
|
||||
create_world_dlg:show()
|
||||
mm_texture.update("singleplayer", current_game())
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["world_delete"] ~= nil then
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
if selected ~= nil and
|
||||
selected <= menudata.worldlist:size() then
|
||||
local world = menudata.worldlist:get_list()[selected]
|
||||
if world ~= nil and
|
||||
world.name ~= nil and
|
||||
world.name ~= "" then
|
||||
local index = menudata.worldlist:get_raw_index(selected)
|
||||
local delete_world_dlg = create_delete_world_dlg(world.name,index)
|
||||
delete_world_dlg:set_parent(this)
|
||||
this:hide()
|
||||
delete_world_dlg:show()
|
||||
mm_texture.update("singleplayer",current_game())
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["world_configure"] ~= nil then
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
if selected ~= nil then
|
||||
local configdialog =
|
||||
create_configure_world_dlg(
|
||||
menudata.worldlist:get_raw_index(selected))
|
||||
|
||||
if (configdialog ~= nil) then
|
||||
configdialog:set_parent(this)
|
||||
this:hide()
|
||||
configdialog:show()
|
||||
mm_texture.update("singleplayer",current_game())
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local on_change
|
||||
if enable_gamebar then
|
||||
function on_change(type, old_tab, new_tab)
|
||||
if (type == "ENTER") then
|
||||
local game = current_game()
|
||||
|
||||
if game then
|
||||
menudata.worldlist:set_filtercriteria(game.id)
|
||||
core.set_topleft_text(game.name)
|
||||
mm_texture.update("singleplayer",game)
|
||||
end
|
||||
|
||||
singleplayer_refresh_gamebar()
|
||||
ui.find_by_name("game_button_bar"):show()
|
||||
else
|
||||
menudata.worldlist:set_filtercriteria(nil)
|
||||
local gamebar = ui.find_by_name("game_button_bar")
|
||||
if gamebar then
|
||||
gamebar:hide()
|
||||
end
|
||||
core.set_topleft_text("")
|
||||
mm_texture.update(new_tab,nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
return {
|
||||
name = "local",
|
||||
caption = fgettext("Start Game"),
|
||||
cbf_formspec = get_formspec,
|
||||
cbf_button_handler = main_button_handler,
|
||||
on_change = on_change
|
||||
}
|
360
builtin/mainmenu/tab_online.lua
Normal file
360
builtin/mainmenu/tab_online.lua
Normal file
@ -0,0 +1,360 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function get_formspec(tabview, name, tabdata)
|
||||
-- Update the cached supported proto info,
|
||||
-- it may have changed after a change by the settings menu.
|
||||
common_update_cached_supp_proto()
|
||||
local fav_selected
|
||||
if menudata.search_result then
|
||||
fav_selected = menudata.search_result[tabdata.fav_selected]
|
||||
else
|
||||
fav_selected = menudata.favorites[tabdata.fav_selected]
|
||||
end
|
||||
|
||||
if not tabdata.search_for then
|
||||
tabdata.search_for = ""
|
||||
end
|
||||
|
||||
local retval =
|
||||
-- Search
|
||||
"field[0.15,0.075;5.91,1;te_search;;" .. core.formspec_escape(tabdata.search_for) .. "]" ..
|
||||
"button[5.62,-0.25;1.5,1;btn_mp_search;" .. fgettext("Search") .. "]" ..
|
||||
"image_button[6.97,-.165;.83,.83;" .. core.formspec_escape(defaulttexturedir .. "refresh.png")
|
||||
.. ";btn_mp_refresh;]" ..
|
||||
|
||||
-- Address / Port
|
||||
"label[7.75,-0.25;" .. fgettext("Address / Port") .. "]" ..
|
||||
"field[8,0.65;3.25,0.5;te_address;;" ..
|
||||
core.formspec_escape(core.settings:get("address")) .. "]" ..
|
||||
"field[11.1,0.65;1.4,0.5;te_port;;" ..
|
||||
core.formspec_escape(core.settings:get("remote_port")) .. "]" ..
|
||||
|
||||
-- Name / Password
|
||||
"label[7.75,0.95;" .. fgettext("Name / Password") .. "]" ..
|
||||
"field[8,1.85;2.9,0.5;te_name;;" ..
|
||||
core.formspec_escape(core.settings:get("name")) .. "]" ..
|
||||
"pwdfield[10.73,1.85;1.77,0.5;te_pwd;]" ..
|
||||
|
||||
-- Description Background
|
||||
"box[7.73,2.25;4.25,2.6;#999999]"..
|
||||
|
||||
-- Connect
|
||||
"button[9.88,4.9;2.3,1;btn_mp_connect;" .. fgettext("Connect") .. "]"
|
||||
|
||||
if tabdata.fav_selected and fav_selected then
|
||||
if gamedata.fav then
|
||||
retval = retval .. "button[7.73,4.9;2.3,1;btn_delete_favorite;" ..
|
||||
fgettext("Del. Favorite") .. "]"
|
||||
end
|
||||
if fav_selected.description then
|
||||
retval = retval .. "textarea[8.1,2.3;4.23,2.9;;;" ..
|
||||
core.formspec_escape((gamedata.serverdescription or ""), true) .. "]"
|
||||
end
|
||||
end
|
||||
|
||||
--favourites
|
||||
retval = retval .. "tablecolumns[" ..
|
||||
image_column(fgettext("Favorite"), "favorite") .. ";" ..
|
||||
image_column(fgettext("Ping")) .. ",padding=0.25;" ..
|
||||
"color,span=3;" ..
|
||||
"text,align=right;" .. -- clients
|
||||
"text,align=center,padding=0.25;" .. -- "/"
|
||||
"text,align=right,padding=0.25;" .. -- clients_max
|
||||
image_column(fgettext("Creative mode"), "creative") .. ",padding=1;" ..
|
||||
image_column(fgettext("Damage enabled"), "damage") .. ",padding=0.25;" ..
|
||||
--~ PvP = Player versus Player
|
||||
image_column(fgettext("PvP enabled"), "pvp") .. ",padding=0.25;" ..
|
||||
"color,span=1;" ..
|
||||
"text,padding=1]" ..
|
||||
"table[-0.15,0.6;7.75,5.15;favourites;"
|
||||
|
||||
if menudata.search_result then
|
||||
for i = 1, #menudata.search_result do
|
||||
local favs = core.get_favorites("local")
|
||||
local server = menudata.search_result[i]
|
||||
|
||||
for fav_id = 1, #favs do
|
||||
if server.address == favs[fav_id].address and
|
||||
server.port == favs[fav_id].port then
|
||||
server.is_favorite = true
|
||||
end
|
||||
end
|
||||
|
||||
if i ~= 1 then
|
||||
retval = retval .. ","
|
||||
end
|
||||
|
||||
retval = retval .. render_serverlist_row(server, server.is_favorite)
|
||||
end
|
||||
elseif #menudata.favorites > 0 then
|
||||
local favs = core.get_favorites("local")
|
||||
if #favs > 0 then
|
||||
for i = 1, #favs do
|
||||
for j = 1, #menudata.favorites do
|
||||
if menudata.favorites[j].address == favs[i].address and
|
||||
menudata.favorites[j].port == favs[i].port then
|
||||
table.insert(menudata.favorites, i, table.remove(menudata.favorites, j))
|
||||
end
|
||||
end
|
||||
if favs[i].address ~= menudata.favorites[i].address then
|
||||
table.insert(menudata.favorites, i, favs[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
retval = retval .. render_serverlist_row(menudata.favorites[1], (#favs > 0))
|
||||
for i = 2, #menudata.favorites do
|
||||
retval = retval .. "," .. render_serverlist_row(menudata.favorites[i], (i <= #favs))
|
||||
end
|
||||
end
|
||||
|
||||
if tabdata.fav_selected then
|
||||
retval = retval .. ";" .. tabdata.fav_selected .. "]"
|
||||
else
|
||||
retval = retval .. ";0]"
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function main_button_handler(tabview, fields, name, tabdata)
|
||||
local serverlist = menudata.search_result or menudata.favorites
|
||||
|
||||
if fields.te_name then
|
||||
gamedata.playername = fields.te_name
|
||||
core.settings:set("name", fields.te_name)
|
||||
end
|
||||
|
||||
if fields.favourites then
|
||||
local event = core.explode_table_event(fields.favourites)
|
||||
local fav = serverlist[event.row]
|
||||
|
||||
if event.type == "DCL" then
|
||||
if event.row <= #serverlist then
|
||||
if menudata.favorites_is_public and
|
||||
not is_server_protocol_compat_or_error(
|
||||
fav.proto_min, fav.proto_max) then
|
||||
return true
|
||||
end
|
||||
|
||||
gamedata.address = fav.address
|
||||
gamedata.port = fav.port
|
||||
gamedata.playername = fields.te_name
|
||||
gamedata.selected_world = 0
|
||||
|
||||
if fields.te_pwd then
|
||||
gamedata.password = fields.te_pwd
|
||||
end
|
||||
|
||||
gamedata.servername = fav.name
|
||||
gamedata.serverdescription = fav.description
|
||||
|
||||
if gamedata.address and gamedata.port then
|
||||
core.settings:set("address", gamedata.address)
|
||||
core.settings:set("remote_port", gamedata.port)
|
||||
core.start()
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if event.type == "CHG" then
|
||||
if event.row <= #serverlist then
|
||||
gamedata.fav = false
|
||||
local favs = core.get_favorites("local")
|
||||
local address = fav.address
|
||||
local port = fav.port
|
||||
gamedata.serverdescription = fav.description
|
||||
|
||||
for i = 1, #favs do
|
||||
if fav.address == favs[i].address and
|
||||
fav.port == favs[i].port then
|
||||
gamedata.fav = true
|
||||
end
|
||||
end
|
||||
|
||||
if address and port then
|
||||
core.settings:set("address", address)
|
||||
core.settings:set("remote_port", port)
|
||||
end
|
||||
tabdata.fav_selected = event.row
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if fields.key_up or fields.key_down then
|
||||
local fav_idx = core.get_table_index("favourites")
|
||||
local fav = serverlist[fav_idx]
|
||||
|
||||
if fav_idx then
|
||||
if fields.key_up and fav_idx > 1 then
|
||||
fav_idx = fav_idx - 1
|
||||
elseif fields.key_down and fav_idx < #menudata.favorites then
|
||||
fav_idx = fav_idx + 1
|
||||
end
|
||||
else
|
||||
fav_idx = 1
|
||||
end
|
||||
|
||||
if not menudata.favorites or not fav then
|
||||
tabdata.fav_selected = 0
|
||||
return true
|
||||
end
|
||||
|
||||
local address = fav.address
|
||||
local port = fav.port
|
||||
gamedata.serverdescription = fav.description
|
||||
if address and port then
|
||||
core.settings:set("address", address)
|
||||
core.settings:set("remote_port", port)
|
||||
end
|
||||
|
||||
tabdata.fav_selected = fav_idx
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_delete_favorite then
|
||||
local current_favourite = core.get_table_index("favourites")
|
||||
if not current_favourite then return end
|
||||
|
||||
core.delete_favorite(current_favourite)
|
||||
asyncOnlineFavourites()
|
||||
tabdata.fav_selected = nil
|
||||
|
||||
core.settings:set("address", "")
|
||||
core.settings:set("remote_port", "30000")
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mp_search or fields.key_enter_field == "te_search" then
|
||||
tabdata.fav_selected = 1
|
||||
local input = fields.te_search:lower()
|
||||
tabdata.search_for = fields.te_search
|
||||
|
||||
if #menudata.favorites < 2 then
|
||||
return true
|
||||
end
|
||||
|
||||
menudata.search_result = {}
|
||||
|
||||
-- setup the keyword list
|
||||
local keywords = {}
|
||||
for word in input:gmatch("%S+") do
|
||||
word = word:gsub("(%W)", "%%%1")
|
||||
table.insert(keywords, word)
|
||||
end
|
||||
|
||||
if #keywords == 0 then
|
||||
menudata.search_result = nil
|
||||
return true
|
||||
end
|
||||
|
||||
-- Search the serverlist
|
||||
local search_result = {}
|
||||
for i = 1, #menudata.favorites do
|
||||
local server = menudata.favorites[i]
|
||||
local found = 0
|
||||
for k = 1, #keywords do
|
||||
local keyword = keywords[k]
|
||||
if server.name then
|
||||
local sername = server.name:lower()
|
||||
local _, count = sername:gsub(keyword, keyword)
|
||||
found = found + count * 4
|
||||
end
|
||||
|
||||
if server.description then
|
||||
local desc = server.description:lower()
|
||||
local _, count = desc:gsub(keyword, keyword)
|
||||
found = found + count * 2
|
||||
end
|
||||
end
|
||||
if found > 0 then
|
||||
local points = (#menudata.favorites - i) / 5 + found
|
||||
server.points = points
|
||||
table.insert(search_result, server)
|
||||
end
|
||||
end
|
||||
if #search_result > 0 then
|
||||
table.sort(search_result, function(a, b)
|
||||
return a.points > b.points
|
||||
end)
|
||||
menudata.search_result = search_result
|
||||
local first_server = search_result[1]
|
||||
core.settings:set("address", first_server.address)
|
||||
core.settings:set("remote_port", first_server.port)
|
||||
gamedata.serverdescription = first_server.description
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mp_refresh then
|
||||
asyncOnlineFavourites()
|
||||
return true
|
||||
end
|
||||
|
||||
if (fields.btn_mp_connect or fields.key_enter)
|
||||
and fields.te_address ~= "" and fields.te_port then
|
||||
gamedata.playername = fields.te_name
|
||||
gamedata.password = fields.te_pwd
|
||||
gamedata.address = fields.te_address
|
||||
gamedata.port = fields.te_port
|
||||
gamedata.selected_world = 0
|
||||
local fav_idx = core.get_table_index("favourites")
|
||||
local fav = serverlist[fav_idx]
|
||||
|
||||
if fav_idx and fav_idx <= #serverlist and
|
||||
fav.address == fields.te_address and
|
||||
fav.port == fields.te_port then
|
||||
|
||||
gamedata.servername = fav.name
|
||||
gamedata.serverdescription = fav.description
|
||||
|
||||
if menudata.favorites_is_public and
|
||||
not is_server_protocol_compat_or_error(
|
||||
fav.proto_min, fav.proto_max) then
|
||||
return true
|
||||
end
|
||||
else
|
||||
gamedata.servername = ""
|
||||
gamedata.serverdescription = ""
|
||||
end
|
||||
|
||||
core.settings:set("address", fields.te_address)
|
||||
core.settings:set("remote_port", fields.te_port)
|
||||
|
||||
core.start()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function on_change(type, old_tab, new_tab)
|
||||
if type == "LEAVE" then return end
|
||||
asyncOnlineFavourites()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
return {
|
||||
name = "online",
|
||||
caption = fgettext("Join Game"),
|
||||
cbf_formspec = get_formspec,
|
||||
cbf_button_handler = main_button_handler,
|
||||
on_change = on_change
|
||||
}
|
344
builtin/mainmenu/tab_settings.lua
Normal file
344
builtin/mainmenu/tab_settings.lua
Normal file
@ -0,0 +1,344 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local labels = {
|
||||
leaves = {
|
||||
fgettext("Opaque Leaves"),
|
||||
fgettext("Simple Leaves"),
|
||||
fgettext("Fancy Leaves")
|
||||
},
|
||||
node_highlighting = {
|
||||
fgettext("Node Outlining"),
|
||||
fgettext("Node Highlighting"),
|
||||
fgettext("None")
|
||||
},
|
||||
filters = {
|
||||
fgettext("No Filter"),
|
||||
fgettext("Bilinear Filter"),
|
||||
fgettext("Trilinear Filter")
|
||||
},
|
||||
mipmap = {
|
||||
fgettext("No Mipmap"),
|
||||
fgettext("Mipmap"),
|
||||
fgettext("Mipmap + Aniso. Filter")
|
||||
},
|
||||
antialiasing = {
|
||||
fgettext("None"),
|
||||
fgettext("2x"),
|
||||
fgettext("4x"),
|
||||
fgettext("8x")
|
||||
}
|
||||
}
|
||||
|
||||
local dd_options = {
|
||||
leaves = {
|
||||
table.concat(labels.leaves, ","),
|
||||
{"opaque", "simple", "fancy"}
|
||||
},
|
||||
node_highlighting = {
|
||||
table.concat(labels.node_highlighting, ","),
|
||||
{"box", "halo", "none"}
|
||||
},
|
||||
filters = {
|
||||
table.concat(labels.filters, ","),
|
||||
{"", "bilinear_filter", "trilinear_filter"}
|
||||
},
|
||||
mipmap = {
|
||||
table.concat(labels.mipmap, ","),
|
||||
{"", "mip_map", "anisotropic_filter"}
|
||||
},
|
||||
antialiasing = {
|
||||
table.concat(labels.antialiasing, ","),
|
||||
{"0", "2", "4", "8"}
|
||||
}
|
||||
}
|
||||
|
||||
local getSettingIndex = {
|
||||
Leaves = function()
|
||||
local style = core.settings:get("leaves_style")
|
||||
for idx, name in pairs(dd_options.leaves[2]) do
|
||||
if style == name then return idx end
|
||||
end
|
||||
return 1
|
||||
end,
|
||||
NodeHighlighting = function()
|
||||
local style = core.settings:get("node_highlighting")
|
||||
for idx, name in pairs(dd_options.node_highlighting[2]) do
|
||||
if style == name then return idx end
|
||||
end
|
||||
return 1
|
||||
end,
|
||||
Filter = function()
|
||||
if core.settings:get(dd_options.filters[2][3]) == "true" then
|
||||
return 3
|
||||
elseif core.settings:get(dd_options.filters[2][3]) == "false" and
|
||||
core.settings:get(dd_options.filters[2][2]) == "true" then
|
||||
return 2
|
||||
end
|
||||
return 1
|
||||
end,
|
||||
Mipmap = function()
|
||||
if core.settings:get(dd_options.mipmap[2][3]) == "true" then
|
||||
return 3
|
||||
elseif core.settings:get(dd_options.mipmap[2][3]) == "false" and
|
||||
core.settings:get(dd_options.mipmap[2][2]) == "true" then
|
||||
return 2
|
||||
end
|
||||
return 1
|
||||
end,
|
||||
Antialiasing = function()
|
||||
local antialiasing_setting = core.settings:get("fsaa")
|
||||
for i = 1, #dd_options.antialiasing[2] do
|
||||
if antialiasing_setting == dd_options.antialiasing[2][i] then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return 1
|
||||
end
|
||||
}
|
||||
|
||||
local function antialiasing_fname_to_name(fname)
|
||||
for i = 1, #labels.antialiasing do
|
||||
if fname == labels.antialiasing[i] then
|
||||
return dd_options.antialiasing[2][i]
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function formspec(tabview, name, tabdata)
|
||||
local tab_string =
|
||||
"box[0,0;3.75,4.5;#999999]" ..
|
||||
"checkbox[0.25,0;cb_smooth_lighting;" .. fgettext("Smooth Lighting") .. ";"
|
||||
.. dump(core.settings:get_bool("smooth_lighting")) .. "]" ..
|
||||
"checkbox[0.25,0.5;cb_particles;" .. fgettext("Particles") .. ";"
|
||||
.. dump(core.settings:get_bool("enable_particles")) .. "]" ..
|
||||
"checkbox[0.25,1;cb_3d_clouds;" .. fgettext("3D Clouds") .. ";"
|
||||
.. dump(core.settings:get_bool("enable_3d_clouds")) .. "]" ..
|
||||
"checkbox[0.25,1.5;cb_opaque_water;" .. fgettext("Opaque Water") .. ";"
|
||||
.. dump(core.settings:get_bool("opaque_water")) .. "]" ..
|
||||
"checkbox[0.25,2.0;cb_connected_glass;" .. fgettext("Connected Glass") .. ";"
|
||||
.. dump(core.settings:get_bool("connected_glass")) .. "]" ..
|
||||
"dropdown[0.25,2.8;3.5;dd_node_highlighting;" .. dd_options.node_highlighting[1] .. ";"
|
||||
.. getSettingIndex.NodeHighlighting() .. "]" ..
|
||||
"dropdown[0.25,3.6;3.5;dd_leaves_style;" .. dd_options.leaves[1] .. ";"
|
||||
.. getSettingIndex.Leaves() .. "]" ..
|
||||
"box[4,0;3.75,4.5;#999999]" ..
|
||||
"label[4.25,0.1;" .. fgettext("Texturing:") .. "]" ..
|
||||
"dropdown[4.25,0.55;3.5;dd_filters;" .. dd_options.filters[1] .. ";"
|
||||
.. getSettingIndex.Filter() .. "]" ..
|
||||
"dropdown[4.25,1.35;3.5;dd_mipmap;" .. dd_options.mipmap[1] .. ";"
|
||||
.. getSettingIndex.Mipmap() .. "]" ..
|
||||
"label[4.25,2.15;" .. fgettext("Antialiasing:") .. "]" ..
|
||||
"dropdown[4.25,2.6;3.5;dd_antialiasing;" .. dd_options.antialiasing[1] .. ";"
|
||||
.. getSettingIndex.Antialiasing() .. "]" ..
|
||||
"label[4.25,3.45;" .. fgettext("Screen:") .. "]" ..
|
||||
"checkbox[4.25,3.6;cb_autosave_screensize;" .. fgettext("Autosave Screen Size") .. ";"
|
||||
.. dump(core.settings:get_bool("autosave_screensize")) .. "]" ..
|
||||
"box[8,0;3.75,4.5;#999999]"
|
||||
|
||||
local video_driver = core.settings:get("video_driver")
|
||||
local shaders_enabled = core.settings:get_bool("enable_shaders")
|
||||
if video_driver == "opengl" then
|
||||
tab_string = tab_string ..
|
||||
"checkbox[8.25,0;cb_shaders;" .. fgettext("Shaders") .. ";"
|
||||
.. tostring(shaders_enabled) .. "]"
|
||||
elseif video_driver == "ogles2" then
|
||||
tab_string = tab_string ..
|
||||
"checkbox[8.25,0;cb_shaders;" .. fgettext("Shaders (experimental)") .. ";"
|
||||
.. tostring(shaders_enabled) .. "]"
|
||||
else
|
||||
core.settings:set_bool("enable_shaders", false)
|
||||
shaders_enabled = false
|
||||
tab_string = tab_string ..
|
||||
"label[8.38,0.2;" .. core.colorize("#888888",
|
||||
fgettext("Shaders (unavailable)")) .. "]"
|
||||
end
|
||||
|
||||
tab_string = tab_string ..
|
||||
"button[8,4.75;3.95,1;btn_change_keys;"
|
||||
.. fgettext("Change Keys") .. "]"
|
||||
|
||||
tab_string = tab_string ..
|
||||
"button[0,4.75;3.95,1;btn_advanced_settings;"
|
||||
.. fgettext("All Settings") .. "]"
|
||||
|
||||
|
||||
if core.settings:get("touchscreen_threshold") ~= nil then
|
||||
tab_string = tab_string ..
|
||||
"label[4.3,4.2;" .. fgettext("Touchthreshold: (px)") .. "]" ..
|
||||
"dropdown[4.25,4.65;3.5;dd_touchthreshold;0,10,20,30,40,50;" ..
|
||||
((tonumber(core.settings:get("touchscreen_threshold")) / 10) + 1) ..
|
||||
"]box[4.0,4.5;3.75,1.0;#999999]"
|
||||
end
|
||||
|
||||
if shaders_enabled then
|
||||
tab_string = tab_string ..
|
||||
"checkbox[8.25,0.5;cb_tonemapping;" .. fgettext("Tone Mapping") .. ";"
|
||||
.. dump(core.settings:get_bool("tone_mapping")) .. "]" ..
|
||||
"checkbox[8.25,1;cb_waving_water;" .. fgettext("Waving Liquids") .. ";"
|
||||
.. dump(core.settings:get_bool("enable_waving_water")) .. "]" ..
|
||||
"checkbox[8.25,1.5;cb_waving_leaves;" .. fgettext("Waving Leaves") .. ";"
|
||||
.. dump(core.settings:get_bool("enable_waving_leaves")) .. "]" ..
|
||||
"checkbox[8.25,2;cb_waving_plants;" .. fgettext("Waving Plants") .. ";"
|
||||
.. dump(core.settings:get_bool("enable_waving_plants")) .. "]"
|
||||
else
|
||||
tab_string = tab_string ..
|
||||
"label[8.38,0.7;" .. core.colorize("#888888",
|
||||
fgettext("Tone Mapping")) .. "]" ..
|
||||
"label[8.38,1.2;" .. core.colorize("#888888",
|
||||
fgettext("Waving Liquids")) .. "]" ..
|
||||
"label[8.38,1.7;" .. core.colorize("#888888",
|
||||
fgettext("Waving Leaves")) .. "]" ..
|
||||
"label[8.38,2.2;" .. core.colorize("#888888",
|
||||
fgettext("Waving Plants")) .. "]"
|
||||
end
|
||||
|
||||
return tab_string
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function handle_settings_buttons(this, fields, tabname, tabdata)
|
||||
|
||||
if fields["btn_advanced_settings"] ~= nil then
|
||||
local adv_settings_dlg = create_adv_settings_dlg()
|
||||
adv_settings_dlg:set_parent(this)
|
||||
this:hide()
|
||||
adv_settings_dlg:show()
|
||||
--mm_texture.update("singleplayer", current_game())
|
||||
return true
|
||||
end
|
||||
if fields["cb_smooth_lighting"] then
|
||||
core.settings:set("smooth_lighting", fields["cb_smooth_lighting"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_particles"] then
|
||||
core.settings:set("enable_particles", fields["cb_particles"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_3d_clouds"] then
|
||||
core.settings:set("enable_3d_clouds", fields["cb_3d_clouds"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_opaque_water"] then
|
||||
core.settings:set("opaque_water", fields["cb_opaque_water"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_connected_glass"] then
|
||||
core.settings:set("connected_glass", fields["cb_connected_glass"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_autosave_screensize"] then
|
||||
core.settings:set("autosave_screensize", fields["cb_autosave_screensize"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_shaders"] then
|
||||
if (core.settings:get("video_driver") == "direct3d8" or
|
||||
core.settings:get("video_driver") == "direct3d9") then
|
||||
core.settings:set("enable_shaders", "false")
|
||||
gamedata.errormessage = fgettext("To enable shaders the OpenGL driver needs to be used.")
|
||||
else
|
||||
core.settings:set("enable_shaders", fields["cb_shaders"])
|
||||
end
|
||||
return true
|
||||
end
|
||||
if fields["cb_tonemapping"] then
|
||||
core.settings:set("tone_mapping", fields["cb_tonemapping"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_waving_water"] then
|
||||
core.settings:set("enable_waving_water", fields["cb_waving_water"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_waving_leaves"] then
|
||||
core.settings:set("enable_waving_leaves", fields["cb_waving_leaves"])
|
||||
end
|
||||
if fields["cb_waving_plants"] then
|
||||
core.settings:set("enable_waving_plants", fields["cb_waving_plants"])
|
||||
return true
|
||||
end
|
||||
if fields["btn_change_keys"] then
|
||||
core.show_keys_menu()
|
||||
return true
|
||||
end
|
||||
if fields["cb_touchscreen_target"] then
|
||||
core.settings:set("touchtarget", fields["cb_touchscreen_target"])
|
||||
return true
|
||||
end
|
||||
|
||||
--Note dropdowns have to be handled LAST!
|
||||
local ddhandled = false
|
||||
|
||||
for i = 1, #labels.leaves do
|
||||
if fields["dd_leaves_style"] == labels.leaves[i] then
|
||||
core.settings:set("leaves_style", dd_options.leaves[2][i])
|
||||
ddhandled = true
|
||||
end
|
||||
end
|
||||
for i = 1, #labels.node_highlighting do
|
||||
if fields["dd_node_highlighting"] == labels.node_highlighting[i] then
|
||||
core.settings:set("node_highlighting", dd_options.node_highlighting[2][i])
|
||||
ddhandled = true
|
||||
end
|
||||
end
|
||||
if fields["dd_filters"] == labels.filters[1] then
|
||||
core.settings:set("bilinear_filter", "false")
|
||||
core.settings:set("trilinear_filter", "false")
|
||||
ddhandled = true
|
||||
elseif fields["dd_filters"] == labels.filters[2] then
|
||||
core.settings:set("bilinear_filter", "true")
|
||||
core.settings:set("trilinear_filter", "false")
|
||||
ddhandled = true
|
||||
elseif fields["dd_filters"] == labels.filters[3] then
|
||||
core.settings:set("bilinear_filter", "false")
|
||||
core.settings:set("trilinear_filter", "true")
|
||||
ddhandled = true
|
||||
end
|
||||
if fields["dd_mipmap"] == labels.mipmap[1] then
|
||||
core.settings:set("mip_map", "false")
|
||||
core.settings:set("anisotropic_filter", "false")
|
||||
ddhandled = true
|
||||
elseif fields["dd_mipmap"] == labels.mipmap[2] then
|
||||
core.settings:set("mip_map", "true")
|
||||
core.settings:set("anisotropic_filter", "false")
|
||||
ddhandled = true
|
||||
elseif fields["dd_mipmap"] == labels.mipmap[3] then
|
||||
core.settings:set("mip_map", "true")
|
||||
core.settings:set("anisotropic_filter", "true")
|
||||
ddhandled = true
|
||||
end
|
||||
if fields["dd_antialiasing"] then
|
||||
core.settings:set("fsaa",
|
||||
antialiasing_fname_to_name(fields["dd_antialiasing"]))
|
||||
ddhandled = true
|
||||
end
|
||||
if fields["dd_touchthreshold"] then
|
||||
core.settings:set("touchscreen_threshold", fields["dd_touchthreshold"])
|
||||
ddhandled = true
|
||||
end
|
||||
|
||||
return ddhandled
|
||||
end
|
||||
|
||||
return {
|
||||
name = "settings",
|
||||
caption = fgettext("Settings"),
|
||||
cbf_formspec = formspec,
|
||||
cbf_button_handler = handle_settings_buttons
|
||||
}
|
191
builtin/mainmenu/textures.lua
Normal file
191
builtin/mainmenu/textures.lua
Normal file
@ -0,0 +1,191 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
|
||||
mm_texture = {}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_texture.init()
|
||||
mm_texture.defaulttexturedir = core.get_texturepath() .. DIR_DELIM .. "base" ..
|
||||
DIR_DELIM .. "pack" .. DIR_DELIM
|
||||
mm_texture.basetexturedir = mm_texture.defaulttexturedir
|
||||
|
||||
mm_texture.texturepack = core.settings:get("texture_path")
|
||||
|
||||
mm_texture.gameid = nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_texture.update(tab,gamedetails)
|
||||
if tab ~= "singleplayer" then
|
||||
mm_texture.reset()
|
||||
return
|
||||
end
|
||||
|
||||
if gamedetails == nil then
|
||||
return
|
||||
end
|
||||
|
||||
mm_texture.update_game(gamedetails)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_texture.reset()
|
||||
mm_texture.gameid = nil
|
||||
local have_bg = false
|
||||
--local have_overlay = mm_texture.set_generic("overlay")
|
||||
|
||||
core.set_clouds(false)
|
||||
mm_texture.clear("header")
|
||||
mm_texture.clear("footer")
|
||||
core.set_clouds(false)
|
||||
mm_texture.set_generic("footer")
|
||||
mm_texture.set_generic("header")
|
||||
local minimalpath = defaulttexturedir .. "menu_bg.png"
|
||||
core.set_background("background", minimalpath, false, 128)
|
||||
if true then return end
|
||||
if not have_overlay then
|
||||
have_bg = mm_texture.set_generic("background")
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if not have_bg then
|
||||
if core.settings:get_bool("menu_clouds") then
|
||||
core.set_clouds(false)
|
||||
else
|
||||
mm_texture.set_dirt_bg()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_texture.update_game(gamedetails)
|
||||
if mm_texture.gameid == gamedetails.id then
|
||||
return
|
||||
end
|
||||
|
||||
local have_bg = false
|
||||
local have_overlay = mm_texture.set_game("overlay",gamedetails)
|
||||
|
||||
if not have_overlay then
|
||||
have_bg = mm_texture.set_game("background",gamedetails)
|
||||
end
|
||||
|
||||
mm_texture.clear("header")
|
||||
mm_texture.clear("footer")
|
||||
core.set_clouds(false)
|
||||
|
||||
if not have_bg then
|
||||
|
||||
if core.settings:get_bool("menu_clouds") then
|
||||
core.set_clouds(true)
|
||||
else
|
||||
mm_texture.set_dirt_bg()
|
||||
end
|
||||
end
|
||||
|
||||
mm_texture.set_game("footer",gamedetails)
|
||||
mm_texture.set_game("header",gamedetails)
|
||||
|
||||
mm_texture.gameid = gamedetails.id
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_texture.clear(identifier)
|
||||
core.set_background(identifier,"")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_texture.set_generic(identifier)
|
||||
--try texture pack first
|
||||
if mm_texture.texturepack ~= nil then
|
||||
local path = mm_texture.texturepack .. DIR_DELIM .."menu_" ..
|
||||
identifier .. ".png"
|
||||
if core.set_background(identifier,path) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if mm_texture.defaulttexturedir ~= nil then
|
||||
local path = mm_texture.defaulttexturedir .. DIR_DELIM .."menu_" ..
|
||||
identifier .. ".png"
|
||||
if core.set_background(identifier,path) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_texture.set_game(identifier, gamedetails)
|
||||
|
||||
if gamedetails == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
if mm_texture.texturepack ~= nil then
|
||||
local path = mm_texture.texturepack .. DIR_DELIM ..
|
||||
gamedetails.id .. "_menu_" .. identifier .. ".png"
|
||||
if core.set_background(identifier, path) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Find out how many randomized textures the game provides
|
||||
local n = 0
|
||||
local filename
|
||||
local menu_files = core.get_dir_list(gamedetails.path .. DIR_DELIM .. "menu", false)
|
||||
for i = 1, #menu_files do
|
||||
filename = identifier .. "." .. i .. ".png"
|
||||
if table.indexof(menu_files, filename) == -1 then
|
||||
n = i - 1
|
||||
break
|
||||
end
|
||||
end
|
||||
-- Select random texture, 0 means standard texture
|
||||
n = math.random(0, n)
|
||||
if n == 0 then
|
||||
filename = identifier .. ".png"
|
||||
else
|
||||
filename = identifier .. "." .. n .. ".png"
|
||||
end
|
||||
|
||||
local path = gamedetails.path .. DIR_DELIM .. "menu" ..
|
||||
DIR_DELIM .. filename
|
||||
if core.set_background(identifier, path) then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function mm_texture.set_dirt_bg()
|
||||
--if mm_texture.texturepack ~= nil then
|
||||
--local path = mm_texture.texturepack .. DIR_DELIM .."default_dirt.png"
|
||||
--if core.set_background("background", path, true, 128) then
|
||||
-- return true
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- Use universal fallback texture in textures/base/pack
|
||||
local minimalpath = defaulttexturedir .. "menu_bg.png"
|
||||
core.set_background("background", minimalpath, true, 128)
|
||||
end
|
80
builtin/profiler/init.lua
Normal file
80
builtin/profiler/init.lua
Normal file
@ -0,0 +1,80 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2016 T4im
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
local function get_bool_default(name, default)
|
||||
local val = core.settings:get_bool(name)
|
||||
if val == nil then
|
||||
return default
|
||||
end
|
||||
return val
|
||||
end
|
||||
|
||||
local profiler_path = core.get_builtin_path().."profiler"..DIR_DELIM
|
||||
local profiler = {}
|
||||
local sampler = assert(loadfile(profiler_path .. "sampling.lua"))(profiler)
|
||||
local instrumentation = assert(loadfile(profiler_path .. "instrumentation.lua"))(profiler, sampler, get_bool_default)
|
||||
local reporter = dofile(profiler_path .. "reporter.lua")
|
||||
profiler.instrument = instrumentation.instrument
|
||||
|
||||
---
|
||||
-- Delayed registration of the /profiler chat command
|
||||
-- Is called later, after `core.register_chatcommand` was set up.
|
||||
--
|
||||
function profiler.init_chatcommand()
|
||||
local instrument_profiler = get_bool_default("instrument.profiler", false)
|
||||
if instrument_profiler then
|
||||
instrumentation.init_chatcommand()
|
||||
end
|
||||
|
||||
local param_usage = "print [filter] | dump [filter] | save [format [filter]] | reset"
|
||||
core.register_chatcommand("profiler", {
|
||||
description = "handle the profiler and profiling data",
|
||||
params = param_usage,
|
||||
privs = { server=true },
|
||||
func = function(name, param)
|
||||
local command, arg0 = string.match(param, "([^ ]+) ?(.*)")
|
||||
local args = arg0 and string.split(arg0, " ")
|
||||
|
||||
if command == "dump" then
|
||||
core.log("action", reporter.print(sampler.profile, arg0))
|
||||
return true, "Statistics written to action log"
|
||||
elseif command == "print" then
|
||||
return true, reporter.print(sampler.profile, arg0)
|
||||
elseif command == "save" then
|
||||
return reporter.save(sampler.profile, args[1] or "txt", args[2])
|
||||
elseif command == "reset" then
|
||||
sampler.reset()
|
||||
return true, "Statistics were reset"
|
||||
end
|
||||
|
||||
return false, string.format(
|
||||
"Usage: %s\n" ..
|
||||
"Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).",
|
||||
param_usage
|
||||
)
|
||||
end
|
||||
})
|
||||
|
||||
if not instrument_profiler then
|
||||
instrumentation.init_chatcommand()
|
||||
end
|
||||
end
|
||||
|
||||
sampler.init()
|
||||
instrumentation.init()
|
||||
|
||||
return profiler
|
233
builtin/profiler/instrumentation.lua
Normal file
233
builtin/profiler/instrumentation.lua
Normal file
@ -0,0 +1,233 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2016 T4im
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
local format, pairs, type = string.format, pairs, type
|
||||
local core, get_current_modname = core, core.get_current_modname
|
||||
local profiler, sampler, get_bool_default = ...
|
||||
|
||||
local instrument_builtin = get_bool_default("instrument.builtin", false)
|
||||
|
||||
local register_functions = {
|
||||
register_globalstep = 0,
|
||||
register_playerevent = 0,
|
||||
register_on_placenode = 0,
|
||||
register_on_dignode = 0,
|
||||
register_on_punchnode = 0,
|
||||
register_on_generated = 0,
|
||||
register_on_newplayer = 0,
|
||||
register_on_dieplayer = 0,
|
||||
register_on_respawnplayer = 0,
|
||||
register_on_prejoinplayer = 0,
|
||||
register_on_joinplayer = 0,
|
||||
register_on_leaveplayer = 0,
|
||||
register_on_cheat = 0,
|
||||
register_on_chat_message = 0,
|
||||
register_on_player_receive_fields = 0,
|
||||
register_on_craft = 0,
|
||||
register_craft_predict = 0,
|
||||
register_on_protection_violation = 0,
|
||||
register_on_item_eat = 0,
|
||||
register_on_punchplayer = 0,
|
||||
register_on_player_hpchange = 0,
|
||||
}
|
||||
|
||||
---
|
||||
-- Create an unique instrument name.
|
||||
-- Generate a missing label with a running index number.
|
||||
--
|
||||
local counts = {}
|
||||
local function generate_name(def)
|
||||
local class, label, func_name = def.class, def.label, def.func_name
|
||||
if label then
|
||||
if class or func_name then
|
||||
return format("%s '%s' %s", class or "", label, func_name or ""):trim()
|
||||
end
|
||||
return format("%s", label):trim()
|
||||
elseif label == false then
|
||||
return format("%s", class or func_name):trim()
|
||||
end
|
||||
|
||||
local index_id = def.mod .. (class or func_name)
|
||||
local index = counts[index_id] or 1
|
||||
counts[index_id] = index + 1
|
||||
return format("%s[%d] %s", class or func_name, index, class and func_name or ""):trim()
|
||||
end
|
||||
|
||||
---
|
||||
-- Keep `measure` and the closure in `instrument` lean, as these, and their
|
||||
-- directly called functions are the overhead that is caused by instrumentation.
|
||||
--
|
||||
local time, log = core.get_us_time, sampler.log
|
||||
local function measure(modname, instrument_name, start, ...)
|
||||
log(modname, instrument_name, time() - start)
|
||||
return ...
|
||||
end
|
||||
--- Automatically instrument a function to measure and log to the sampler.
|
||||
-- def = {
|
||||
-- mod = "",
|
||||
-- class = "",
|
||||
-- func_name = "",
|
||||
-- -- if nil, will create a label based on registration order
|
||||
-- label = "" | false,
|
||||
-- }
|
||||
local function instrument(def)
|
||||
if not def or not def.func then
|
||||
return
|
||||
end
|
||||
def.mod = def.mod or get_current_modname() or "??"
|
||||
local modname = def.mod
|
||||
local instrument_name = generate_name(def)
|
||||
local func = def.func
|
||||
|
||||
if not instrument_builtin and modname == "*builtin*" then
|
||||
return func
|
||||
end
|
||||
|
||||
return function(...)
|
||||
-- This tail-call allows passing all return values of `func`
|
||||
-- also called https://en.wikipedia.org/wiki/Continuation_passing_style
|
||||
-- Compared to table creation and unpacking it won't lose `nil` returns
|
||||
-- and is expected to be faster
|
||||
-- `measure` will be executed after time() and func(...)
|
||||
return measure(modname, instrument_name, time(), func(...))
|
||||
end
|
||||
end
|
||||
|
||||
local function can_be_called(func)
|
||||
-- It has to be a function or callable table
|
||||
return type(func) == "function" or
|
||||
((type(func) == "table" or type(func) == "userdata") and
|
||||
getmetatable(func) and getmetatable(func).__call)
|
||||
end
|
||||
|
||||
local function assert_can_be_called(func, func_name, level)
|
||||
if not can_be_called(func) then
|
||||
-- Then throw an *helpful* error, by pointing on our caller instead of us.
|
||||
error(format("Invalid argument to %s. Expected function-like type instead of '%s'.",
|
||||
func_name, type(func)), level + 1)
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
-- Wraps a registration function `func` in such a way,
|
||||
-- that it will automatically instrument any callback function passed as first argument.
|
||||
--
|
||||
local function instrument_register(func, func_name)
|
||||
local register_name = func_name:gsub("^register_", "", 1)
|
||||
return function(callback, ...)
|
||||
assert_can_be_called(callback, func_name, 2)
|
||||
register_functions[func_name] = register_functions[func_name] + 1
|
||||
return func(instrument {
|
||||
func = callback,
|
||||
func_name = register_name
|
||||
}, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function init_chatcommand()
|
||||
if get_bool_default("instrument.chatcommand", true) then
|
||||
local orig_register_chatcommand = core.register_chatcommand
|
||||
core.register_chatcommand = function(cmd, def)
|
||||
def.func = instrument {
|
||||
func = def.func,
|
||||
label = "/" .. cmd,
|
||||
}
|
||||
orig_register_chatcommand(cmd, def)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
-- Start instrumenting selected functions
|
||||
--
|
||||
local function init()
|
||||
if get_bool_default("instrument.entity", true) then
|
||||
-- Explicitly declare entity api-methods.
|
||||
-- Simple iteration would ignore lookup via __index.
|
||||
local entity_instrumentation = {
|
||||
"on_activate",
|
||||
"on_step",
|
||||
"on_punch",
|
||||
"on_rightclick",
|
||||
"get_staticdata",
|
||||
}
|
||||
-- Wrap register_entity() to instrument them on registration.
|
||||
local orig_register_entity = core.register_entity
|
||||
core.register_entity = function(name, prototype)
|
||||
local modname = get_current_modname()
|
||||
for _, func_name in pairs(entity_instrumentation) do
|
||||
prototype[func_name] = instrument {
|
||||
func = prototype[func_name],
|
||||
mod = modname,
|
||||
func_name = func_name,
|
||||
label = prototype.label,
|
||||
}
|
||||
end
|
||||
orig_register_entity(name,prototype)
|
||||
end
|
||||
end
|
||||
|
||||
if get_bool_default("instrument.abm", true) then
|
||||
-- Wrap register_abm() to automatically instrument abms.
|
||||
local orig_register_abm = core.register_abm
|
||||
core.register_abm = function(spec)
|
||||
spec.action = instrument {
|
||||
func = spec.action,
|
||||
class = "ABM",
|
||||
label = spec.label,
|
||||
}
|
||||
orig_register_abm(spec)
|
||||
end
|
||||
end
|
||||
|
||||
if get_bool_default("instrument.lbm", true) then
|
||||
-- Wrap register_lbm() to automatically instrument lbms.
|
||||
local orig_register_lbm = core.register_lbm
|
||||
core.register_lbm = function(spec)
|
||||
spec.action = instrument {
|
||||
func = spec.action,
|
||||
class = "LBM",
|
||||
label = spec.label or spec.name,
|
||||
}
|
||||
orig_register_lbm(spec)
|
||||
end
|
||||
end
|
||||
|
||||
if get_bool_default("instrument.global_callback", true) then
|
||||
for func_name, _ in pairs(register_functions) do
|
||||
core[func_name] = instrument_register(core[func_name], func_name)
|
||||
end
|
||||
end
|
||||
|
||||
if get_bool_default("instrument.profiler", false) then
|
||||
-- Measure overhead of instrumentation, but keep it down for functions
|
||||
-- So keep the `return` for better optimization.
|
||||
profiler.empty_instrument = instrument {
|
||||
func = function() return end,
|
||||
mod = "*profiler*",
|
||||
class = "Instrumentation overhead",
|
||||
label = false,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
register_functions = register_functions,
|
||||
instrument = instrument,
|
||||
init = init,
|
||||
init_chatcommand = init_chatcommand,
|
||||
}
|
277
builtin/profiler/reporter.lua
Normal file
277
builtin/profiler/reporter.lua
Normal file
@ -0,0 +1,277 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2016 T4im
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
|
||||
local DIR_DELIM, LINE_DELIM = DIR_DELIM, "\n"
|
||||
local table, unpack, string, pairs, io, os = table, unpack, string, pairs, io, os
|
||||
local rep, sprintf, tonumber = string.rep, string.format, tonumber
|
||||
local core, settings = core, core.settings
|
||||
local reporter = {}
|
||||
|
||||
---
|
||||
-- Shorten a string. End on an ellipsis if shortened.
|
||||
--
|
||||
local function shorten(str, length)
|
||||
if str and str:len() > length then
|
||||
return "..." .. str:sub(-(length-3))
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
local function filter_matches(filter, text)
|
||||
return not filter or string.match(text, filter)
|
||||
end
|
||||
|
||||
local function format_number(number, fmt)
|
||||
number = tonumber(number)
|
||||
if not number then
|
||||
return "N/A"
|
||||
end
|
||||
return sprintf(fmt or "%d", number)
|
||||
end
|
||||
|
||||
local Formatter = {
|
||||
new = function(self, object)
|
||||
object = object or {}
|
||||
object.out = {} -- output buffer
|
||||
self.__index = self
|
||||
return setmetatable(object, self)
|
||||
end,
|
||||
__tostring = function (self)
|
||||
return table.concat(self.out, LINE_DELIM)
|
||||
end,
|
||||
print = function(self, text, ...)
|
||||
if (...) then
|
||||
text = sprintf(text, ...)
|
||||
end
|
||||
|
||||
if text then
|
||||
-- Avoid format unicode issues.
|
||||
text = text:gsub("Ms", "µs")
|
||||
end
|
||||
|
||||
table.insert(self.out, text or LINE_DELIM)
|
||||
end,
|
||||
flush = function(self)
|
||||
table.insert(self.out, LINE_DELIM)
|
||||
local text = table.concat(self.out, LINE_DELIM)
|
||||
self.out = {}
|
||||
return text
|
||||
end
|
||||
}
|
||||
|
||||
local widths = { 55, 9, 9, 9, 5, 5, 5 }
|
||||
local txt_row_format = sprintf(" %%-%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds", unpack(widths))
|
||||
|
||||
local HR = {}
|
||||
for i=1, #widths do
|
||||
HR[i]= rep("-", widths[i])
|
||||
end
|
||||
-- ' | ' should break less with github than '-+-', when people are pasting there
|
||||
HR = sprintf("-%s-", table.concat(HR, " | "))
|
||||
|
||||
local TxtFormatter = Formatter:new {
|
||||
format_row = function(self, modname, instrument_name, statistics)
|
||||
local label
|
||||
if instrument_name then
|
||||
label = shorten(instrument_name, widths[1] - 5)
|
||||
label = sprintf(" - %s %s", label, rep(".", widths[1] - 5 - label:len()))
|
||||
else -- Print mod_stats
|
||||
label = shorten(modname, widths[1] - 2) .. ":"
|
||||
end
|
||||
|
||||
self:print(txt_row_format, label,
|
||||
format_number(statistics.time_min),
|
||||
format_number(statistics.time_max),
|
||||
format_number(statistics:get_time_avg()),
|
||||
format_number(statistics.part_min, "%.1f"),
|
||||
format_number(statistics.part_max, "%.1f"),
|
||||
format_number(statistics:get_part_avg(), "%.1f")
|
||||
)
|
||||
end,
|
||||
format = function(self, filter)
|
||||
local profile = self.profile
|
||||
self:print("Values below show absolute/relative times spend per server step by the instrumented function.")
|
||||
self:print("A total of %d samples were taken", profile.stats_total.samples)
|
||||
|
||||
if filter then
|
||||
self:print("The output is limited to '%s'", filter)
|
||||
end
|
||||
|
||||
self:print()
|
||||
self:print(
|
||||
txt_row_format,
|
||||
"instrumentation", "min Ms", "max Ms", "avg Ms", "min %", "max %", "avg %"
|
||||
)
|
||||
self:print(HR)
|
||||
for modname,mod_stats in pairs(profile.stats) do
|
||||
if filter_matches(filter, modname) then
|
||||
self:format_row(modname, nil, mod_stats)
|
||||
|
||||
if mod_stats.instruments ~= nil then
|
||||
for instrument_name, instrument_stats in pairs(mod_stats.instruments) do
|
||||
self:format_row(nil, instrument_name, instrument_stats)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
self:print(HR)
|
||||
if not filter then
|
||||
self:format_row("total", nil, profile.stats_total)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local CsvFormatter = Formatter:new {
|
||||
format_row = function(self, modname, instrument_name, statistics)
|
||||
self:print(
|
||||
"%q,%q,%d,%d,%d,%d,%d,%f,%f,%f",
|
||||
modname, instrument_name,
|
||||
statistics.samples,
|
||||
statistics.time_min,
|
||||
statistics.time_max,
|
||||
statistics:get_time_avg(),
|
||||
statistics.time_all,
|
||||
statistics.part_min,
|
||||
statistics.part_max,
|
||||
statistics:get_part_avg()
|
||||
)
|
||||
end,
|
||||
format = function(self, filter)
|
||||
self:print(
|
||||
"%q,%q,%q,%q,%q,%q,%q,%q,%q,%q",
|
||||
"modname", "instrumentation",
|
||||
"samples",
|
||||
"time min µs",
|
||||
"time max µs",
|
||||
"time avg µs",
|
||||
"time all µs",
|
||||
"part min %",
|
||||
"part max %",
|
||||
"part avg %"
|
||||
)
|
||||
for modname, mod_stats in pairs(self.profile.stats) do
|
||||
if filter_matches(filter, modname) then
|
||||
self:format_row(modname, "*", mod_stats)
|
||||
|
||||
if mod_stats.instruments ~= nil then
|
||||
for instrument_name, instrument_stats in pairs(mod_stats.instruments) do
|
||||
self:format_row(modname, instrument_name, instrument_stats)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local function format_statistics(profile, format, filter)
|
||||
local formatter
|
||||
if format == "csv" then
|
||||
formatter = CsvFormatter:new {
|
||||
profile = profile
|
||||
}
|
||||
else
|
||||
formatter = TxtFormatter:new {
|
||||
profile = profile
|
||||
}
|
||||
end
|
||||
formatter:format(filter)
|
||||
return formatter:flush()
|
||||
end
|
||||
|
||||
---
|
||||
-- Format the profile ready for display and
|
||||
-- @return string to be printed to the console
|
||||
--
|
||||
function reporter.print(profile, filter)
|
||||
if filter == "" then filter = nil end
|
||||
return format_statistics(profile, "txt", filter)
|
||||
end
|
||||
|
||||
---
|
||||
-- Serialize the profile data and
|
||||
-- @return serialized data to be saved to a file
|
||||
--
|
||||
local function serialize_profile(profile, format, filter)
|
||||
if format == "lua" or format == "json" or format == "json_pretty" then
|
||||
local stats = filter and {} or profile.stats
|
||||
if filter then
|
||||
for modname, mod_stats in pairs(profile.stats) do
|
||||
if filter_matches(filter, modname) then
|
||||
stats[modname] = mod_stats
|
||||
end
|
||||
end
|
||||
end
|
||||
if format == "lua" then
|
||||
return core.serialize(stats)
|
||||
elseif format == "json" then
|
||||
return core.write_json(stats)
|
||||
elseif format == "json_pretty" then
|
||||
return core.write_json(stats, true)
|
||||
end
|
||||
end
|
||||
-- Fall back to textual formats.
|
||||
return format_statistics(profile, format, filter)
|
||||
end
|
||||
|
||||
local worldpath = core.get_worldpath()
|
||||
local function get_save_path(format, filter)
|
||||
local report_path = settings:get("profiler.report_path") or ""
|
||||
if report_path ~= "" then
|
||||
core.mkdir(sprintf("%s%s%s", worldpath, DIR_DELIM, report_path))
|
||||
end
|
||||
return (sprintf(
|
||||
"%s/%s/profile-%s%s.%s",
|
||||
worldpath,
|
||||
report_path,
|
||||
os.date("%Y%m%dT%H%M%S"),
|
||||
filter and ("-" .. filter) or "",
|
||||
format
|
||||
):gsub("[/\\]+", DIR_DELIM))-- Clean up delims
|
||||
end
|
||||
|
||||
---
|
||||
-- Save the profile to the world path.
|
||||
-- @return success, log message
|
||||
--
|
||||
function reporter.save(profile, format, filter)
|
||||
if not format or format == "" then
|
||||
format = settings:get("profiler.default_report_format") or "txt"
|
||||
end
|
||||
if filter == "" then
|
||||
filter = nil
|
||||
end
|
||||
|
||||
local path = get_save_path(format, filter)
|
||||
|
||||
local output, io_err = io.open(path, "w")
|
||||
if not output then
|
||||
return false, "Saving of profile failed with: " .. io_err
|
||||
end
|
||||
local content, err = serialize_profile(profile, format, filter)
|
||||
if not content then
|
||||
output:close()
|
||||
return false, "Saving of profile failed with: " .. err
|
||||
end
|
||||
output:write(content)
|
||||
output:close()
|
||||
|
||||
local logmessage = "Profile saved to " .. path
|
||||
core.log("action", logmessage)
|
||||
return true, logmessage
|
||||
end
|
||||
|
||||
return reporter
|
206
builtin/profiler/sampling.lua
Normal file
206
builtin/profiler/sampling.lua
Normal file
@ -0,0 +1,206 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2016 T4im
|
||||
--
|
||||
--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
|
||||
--(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.
|
||||
local setmetatable = setmetatable
|
||||
local pairs, format = pairs, string.format
|
||||
local min, max, huge = math.min, math.max, math.huge
|
||||
local core = core
|
||||
|
||||
local profiler = ...
|
||||
-- Split sampler and profile up, to possibly allow for rotation later.
|
||||
local sampler = {}
|
||||
local profile
|
||||
local stats_total
|
||||
local logged_time, logged_data
|
||||
|
||||
local _stat_mt = {
|
||||
get_time_avg = function(self)
|
||||
return self.time_all/self.samples
|
||||
end,
|
||||
get_part_avg = function(self)
|
||||
if not self.part_all then
|
||||
return 100 -- Extra handling for "total"
|
||||
end
|
||||
return self.part_all/self.samples
|
||||
end,
|
||||
}
|
||||
_stat_mt.__index = _stat_mt
|
||||
|
||||
function sampler.reset()
|
||||
-- Accumulated logged time since last sample.
|
||||
-- This helps determining, the relative time a mod used up.
|
||||
logged_time = 0
|
||||
-- The measurements taken through instrumentation since last sample.
|
||||
logged_data = {}
|
||||
|
||||
profile = {
|
||||
-- Current mod statistics (max/min over the entire mod lifespan)
|
||||
-- Mod specific instrumentation statistics are nested within.
|
||||
stats = {},
|
||||
-- Current stats over all mods.
|
||||
stats_total = setmetatable({
|
||||
samples = 0,
|
||||
time_min = huge,
|
||||
time_max = 0,
|
||||
time_all = 0,
|
||||
part_min = 100,
|
||||
part_max = 100
|
||||
}, _stat_mt)
|
||||
}
|
||||
stats_total = profile.stats_total
|
||||
|
||||
-- Provide access to the most recent profile.
|
||||
sampler.profile = profile
|
||||
end
|
||||
|
||||
---
|
||||
-- Log a measurement for the sampler to pick up later.
|
||||
-- Keep `log` and its often called functions lean.
|
||||
-- It will directly add to the instrumentation overhead.
|
||||
--
|
||||
function sampler.log(modname, instrument_name, time_diff)
|
||||
if time_diff <= 0 then
|
||||
if time_diff < 0 then
|
||||
-- This **might** have happened on a semi-regular basis with huge mods,
|
||||
-- resulting in negative statistics (perhaps midnight time jumps or ntp corrections?).
|
||||
core.log("warning", format(
|
||||
"Time travel of %s::%s by %dµs.",
|
||||
modname, instrument_name, time_diff
|
||||
))
|
||||
end
|
||||
-- Throwing these away is better, than having them mess with the overall result.
|
||||
return
|
||||
end
|
||||
|
||||
local mod_data = logged_data[modname]
|
||||
if mod_data == nil then
|
||||
mod_data = {}
|
||||
logged_data[modname] = mod_data
|
||||
end
|
||||
|
||||
mod_data[instrument_name] = (mod_data[instrument_name] or 0) + time_diff
|
||||
-- Update logged time since last sample.
|
||||
logged_time = logged_time + time_diff
|
||||
end
|
||||
|
||||
---
|
||||
-- Return a requested statistic.
|
||||
-- Initialize if necessary.
|
||||
--
|
||||
local function get_statistic(stats_table, name)
|
||||
local statistic = stats_table[name]
|
||||
if statistic == nil then
|
||||
statistic = setmetatable({
|
||||
samples = 0,
|
||||
time_min = huge,
|
||||
time_max = 0,
|
||||
time_all = 0,
|
||||
part_min = 100,
|
||||
part_max = 0,
|
||||
part_all = 0,
|
||||
}, _stat_mt)
|
||||
stats_table[name] = statistic
|
||||
end
|
||||
return statistic
|
||||
end
|
||||
|
||||
---
|
||||
-- Update a statistic table
|
||||
--
|
||||
local function update_statistic(stats_table, time)
|
||||
stats_table.samples = stats_table.samples + 1
|
||||
|
||||
-- Update absolute time (µs) spend by the subject
|
||||
stats_table.time_min = min(stats_table.time_min, time)
|
||||
stats_table.time_max = max(stats_table.time_max, time)
|
||||
stats_table.time_all = stats_table.time_all + time
|
||||
|
||||
-- Update relative time (%) of this sample spend by the subject
|
||||
local current_part = (time/logged_time) * 100
|
||||
stats_table.part_min = min(stats_table.part_min, current_part)
|
||||
stats_table.part_max = max(stats_table.part_max, current_part)
|
||||
stats_table.part_all = stats_table.part_all + current_part
|
||||
end
|
||||
|
||||
---
|
||||
-- Sample all logged measurements each server step.
|
||||
-- Like any globalstep function, this should not be too heavy,
|
||||
-- but does not add to the instrumentation overhead.
|
||||
--
|
||||
local function sample(dtime)
|
||||
-- Rare, but happens and is currently of no informational value.
|
||||
if logged_time == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
for modname, instruments in pairs(logged_data) do
|
||||
local mod_stats = get_statistic(profile.stats, modname)
|
||||
if mod_stats.instruments == nil then
|
||||
-- Current statistics for each instrumentation component
|
||||
mod_stats.instruments = {}
|
||||
end
|
||||
|
||||
local mod_time = 0
|
||||
for instrument_name, time in pairs(instruments) do
|
||||
if time > 0 then
|
||||
mod_time = mod_time + time
|
||||
local instrument_stats = get_statistic(mod_stats.instruments, instrument_name)
|
||||
|
||||
-- Update time of this sample spend by the instrumented function.
|
||||
update_statistic(instrument_stats, time)
|
||||
-- Reset logged data for the next sample.
|
||||
instruments[instrument_name] = 0
|
||||
end
|
||||
end
|
||||
|
||||
-- Update time of this sample spend by this mod.
|
||||
update_statistic(mod_stats, mod_time)
|
||||
end
|
||||
|
||||
-- Update the total time spend over all mods.
|
||||
stats_total.time_min = min(stats_total.time_min, logged_time)
|
||||
stats_total.time_max = max(stats_total.time_max, logged_time)
|
||||
stats_total.time_all = stats_total.time_all + logged_time
|
||||
|
||||
stats_total.samples = stats_total.samples + 1
|
||||
logged_time = 0
|
||||
end
|
||||
|
||||
---
|
||||
-- Setup empty profile and register the sampling function
|
||||
--
|
||||
function sampler.init()
|
||||
sampler.reset()
|
||||
|
||||
if core.settings:get_bool("instrument.profiler") then
|
||||
core.register_globalstep(function()
|
||||
if logged_time == 0 then
|
||||
return
|
||||
end
|
||||
return profiler.empty_instrument()
|
||||
end)
|
||||
core.register_globalstep(profiler.instrument {
|
||||
func = sample,
|
||||
mod = "*profiler*",
|
||||
class = "Sampler (update stats)",
|
||||
label = false,
|
||||
})
|
||||
else
|
||||
core.register_globalstep(sample)
|
||||
end
|
||||
end
|
||||
|
||||
return sampler
|
2373
builtin/settingtypes.txt
Normal file
2373
builtin/settingtypes.txt
Normal file
File diff suppressed because it is too large
Load Diff
23
client/shaders/3d_interlaced_merge/opengl_fragment.glsl
Normal file
23
client/shaders/3d_interlaced_merge/opengl_fragment.glsl
Normal file
@ -0,0 +1,23 @@
|
||||
uniform sampler2D baseTexture;
|
||||
uniform sampler2D normalTexture;
|
||||
uniform sampler2D textureFlags;
|
||||
|
||||
#define leftImage baseTexture
|
||||
#define rightImage normalTexture
|
||||
#define maskImage textureFlags
|
||||
|
||||
varying mediump vec2 varTexCoord;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec2 uv = varTexCoord.st;
|
||||
vec4 left = texture2D(leftImage, uv).rgba;
|
||||
vec4 right = texture2D(rightImage, uv).rgba;
|
||||
vec4 mask = texture2D(maskImage, uv).rgba;
|
||||
vec4 color;
|
||||
if (mask.r > 0.5)
|
||||
color = right;
|
||||
else
|
||||
color = left;
|
||||
gl_FragColor = color;
|
||||
}
|
7
client/shaders/3d_interlaced_merge/opengl_vertex.glsl
Normal file
7
client/shaders/3d_interlaced_merge/opengl_vertex.glsl
Normal file
@ -0,0 +1,7 @@
|
||||
varying mediump vec2 varTexCoord;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
varTexCoord = inTexCoord0;
|
||||
gl_Position = inVertexPosition;
|
||||
}
|
6
client/shaders/default_shader/opengl_fragment.glsl
Normal file
6
client/shaders/default_shader/opengl_fragment.glsl
Normal file
@ -0,0 +1,6 @@
|
||||
varying lowp vec4 varColor;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
gl_FragColor = varColor;
|
||||
}
|
7
client/shaders/default_shader/opengl_vertex.glsl
Normal file
7
client/shaders/default_shader/opengl_vertex.glsl
Normal file
@ -0,0 +1,7 @@
|
||||
varying lowp vec4 varColor;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
gl_Position = mWorldViewProj * inVertexPosition;
|
||||
varColor = inVertexColor;
|
||||
}
|
35
client/shaders/minimap_shader/opengl_fragment.glsl
Normal file
35
client/shaders/minimap_shader/opengl_fragment.glsl
Normal file
@ -0,0 +1,35 @@
|
||||
uniform sampler2D baseTexture;
|
||||
uniform sampler2D normalTexture;
|
||||
uniform vec3 yawVec;
|
||||
|
||||
varying lowp vec4 varColor;
|
||||
varying mediump vec2 varTexCoord;
|
||||
|
||||
void main (void)
|
||||
{
|
||||
vec2 uv = varTexCoord.st;
|
||||
|
||||
//texture sampling rate
|
||||
const float step = 1.0 / 256.0;
|
||||
float tl = texture2D(normalTexture, vec2(uv.x - step, uv.y + step)).r;
|
||||
float t = texture2D(normalTexture, vec2(uv.x - step, uv.y - step)).r;
|
||||
float tr = texture2D(normalTexture, vec2(uv.x + step, uv.y + step)).r;
|
||||
float r = texture2D(normalTexture, vec2(uv.x + step, uv.y)).r;
|
||||
float br = texture2D(normalTexture, vec2(uv.x + step, uv.y - step)).r;
|
||||
float b = texture2D(normalTexture, vec2(uv.x, uv.y - step)).r;
|
||||
float bl = texture2D(normalTexture, vec2(uv.x - step, uv.y - step)).r;
|
||||
float l = texture2D(normalTexture, vec2(uv.x - step, uv.y)).r;
|
||||
float dX = (tr + 2.0 * r + br) - (tl + 2.0 * l + bl);
|
||||
float dY = (bl + 2.0 * b + br) - (tl + 2.0 * t + tr);
|
||||
vec4 bump = vec4 (normalize(vec3 (dX, dY, 0.1)),1.0);
|
||||
float height = 2.0 * texture2D(normalTexture, vec2(uv.x, uv.y)).r - 1.0;
|
||||
vec4 base = texture2D(baseTexture, uv).rgba;
|
||||
vec3 L = normalize(vec3(0.0, 0.75, 1.0));
|
||||
float specular = pow(clamp(dot(reflect(L, bump.xyz), yawVec), 0.0, 1.0), 1.0);
|
||||
float diffuse = dot(yawVec, bump.xyz);
|
||||
|
||||
vec3 color = (1.1 * diffuse + 0.05 * height + 0.5 * specular) * base.rgb;
|
||||
vec4 col = vec4(color.rgb, base.a);
|
||||
col *= varColor;
|
||||
gl_FragColor = vec4(col.rgb, base.a);
|
||||
}
|
11
client/shaders/minimap_shader/opengl_vertex.glsl
Normal file
11
client/shaders/minimap_shader/opengl_vertex.glsl
Normal file
@ -0,0 +1,11 @@
|
||||
uniform mat4 mWorld;
|
||||
|
||||
varying lowp vec4 varColor;
|
||||
varying mediump vec2 varTexCoord;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
varTexCoord = inTexCoord0.st;
|
||||
gl_Position = mWorldViewProj * inVertexPosition;
|
||||
varColor = inVertexColor;
|
||||
}
|
95
client/shaders/nodes_shader/opengl_fragment.glsl
Normal file
95
client/shaders/nodes_shader/opengl_fragment.glsl
Normal file
@ -0,0 +1,95 @@
|
||||
uniform sampler2D baseTexture;
|
||||
|
||||
uniform vec4 skyBgColor;
|
||||
uniform float fogDistance;
|
||||
uniform vec3 eyePosition;
|
||||
|
||||
// The cameraOffset is the current center of the visible world.
|
||||
uniform vec3 cameraOffset;
|
||||
uniform float animationTimer;
|
||||
|
||||
varying vec3 vPosition;
|
||||
// World position in the visible world (i.e. relative to the cameraOffset.)
|
||||
// This can be used for many shader effects without loss of precision.
|
||||
// If the absolute position is required it can be calculated with
|
||||
// cameraOffset + worldPosition (for large coordinates the limits of float
|
||||
// precision must be considered).
|
||||
varying vec3 worldPosition;
|
||||
varying lowp vec4 varColor;
|
||||
varying mediump vec2 varTexCoord;
|
||||
varying vec3 eyeVec;
|
||||
|
||||
const float fogStart = FOG_START;
|
||||
const float fogShadingParameter = 1.0 / ( 1.0 - fogStart);
|
||||
|
||||
#ifdef ENABLE_TONE_MAPPING
|
||||
|
||||
/* Hable's UC2 Tone mapping parameters
|
||||
A = 0.22;
|
||||
B = 0.30;
|
||||
C = 0.10;
|
||||
D = 0.20;
|
||||
E = 0.01;
|
||||
F = 0.30;
|
||||
W = 11.2;
|
||||
equation used: ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F
|
||||
*/
|
||||
|
||||
vec3 uncharted2Tonemap(vec3 x)
|
||||
{
|
||||
return ((x * (0.22 * x + 0.03) + 0.002) / (x * (0.22 * x + 0.3) + 0.06)) - 0.03333;
|
||||
}
|
||||
|
||||
vec4 applyToneMapping(vec4 color)
|
||||
{
|
||||
color = vec4(pow(color.rgb, vec3(2.2)), color.a);
|
||||
const float gamma = 1.6;
|
||||
const float exposureBias = 5.5;
|
||||
color.rgb = uncharted2Tonemap(exposureBias * color.rgb);
|
||||
// Precalculated white_scale from
|
||||
//vec3 whiteScale = 1.0 / uncharted2Tonemap(vec3(W));
|
||||
vec3 whiteScale = vec3(1.036015346);
|
||||
color.rgb *= whiteScale;
|
||||
return vec4(pow(color.rgb, vec3(1.0 / gamma)), color.a);
|
||||
}
|
||||
#endif
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec3 color;
|
||||
vec2 uv = varTexCoord.st;
|
||||
|
||||
vec4 base = texture2D(baseTexture, uv).rgba;
|
||||
#ifdef USE_DISCARD
|
||||
// If alpha is zero, we can just discard the pixel. This fixes transparency
|
||||
// on GPUs like GC7000L, where GL_ALPHA_TEST is not implemented in mesa,
|
||||
// and also on GLES 2, where GL_ALPHA_TEST is missing entirely.
|
||||
if (base.a == 0.0) {
|
||||
discard;
|
||||
}
|
||||
#endif
|
||||
|
||||
color = base.rgb;
|
||||
|
||||
vec4 col = vec4(color.rgb * varColor.rgb, 1.0);
|
||||
|
||||
#ifdef ENABLE_TONE_MAPPING
|
||||
col = applyToneMapping(col);
|
||||
#endif
|
||||
|
||||
// Due to a bug in some (older ?) graphics stacks (possibly in the glsl compiler ?),
|
||||
// the fog will only be rendered correctly if the last operation before the
|
||||
// clamp() is an addition. Else, the clamp() seems to be ignored.
|
||||
// E.g. the following won't work:
|
||||
// float clarity = clamp(fogShadingParameter
|
||||
// * (fogDistance - length(eyeVec)) / fogDistance), 0.0, 1.0);
|
||||
// As additions usually come for free following a multiplication, the new formula
|
||||
// should be more efficient as well.
|
||||
// Note: clarity = (1 - fogginess)
|
||||
float clarity = clamp(fogShadingParameter
|
||||
- fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0);
|
||||
col = mix(skyBgColor, col, clarity);
|
||||
col = vec4(col.rgb, base.a);
|
||||
|
||||
gl_FragColor = col;
|
||||
}
|
156
client/shaders/nodes_shader/opengl_vertex.glsl
Normal file
156
client/shaders/nodes_shader/opengl_vertex.glsl
Normal file
@ -0,0 +1,156 @@
|
||||
uniform mat4 mWorld;
|
||||
|
||||
// Color of the light emitted by the sun.
|
||||
uniform vec3 dayLight;
|
||||
uniform vec3 eyePosition;
|
||||
|
||||
// The cameraOffset is the current center of the visible world.
|
||||
uniform vec3 cameraOffset;
|
||||
uniform float animationTimer;
|
||||
|
||||
varying vec3 vPosition;
|
||||
// World position in the visible world (i.e. relative to the cameraOffset.)
|
||||
// This can be used for many shader effects without loss of precision.
|
||||
// If the absolute position is required it can be calculated with
|
||||
// cameraOffset + worldPosition (for large coordinates the limits of float
|
||||
// precision must be considered).
|
||||
varying vec3 worldPosition;
|
||||
varying lowp vec4 varColor;
|
||||
varying mediump vec2 varTexCoord;
|
||||
varying vec3 eyeVec;
|
||||
|
||||
// Color of the light emitted by the light sources.
|
||||
const vec3 artificialLight = vec3(1.04, 1.04, 1.04);
|
||||
const float e = 2.718281828459;
|
||||
const float BS = 10.0;
|
||||
|
||||
|
||||
float smoothCurve(float x)
|
||||
{
|
||||
return x * x * (3.0 - 2.0 * x);
|
||||
}
|
||||
|
||||
|
||||
float triangleWave(float x)
|
||||
{
|
||||
return abs(fract(x + 0.5) * 2.0 - 1.0);
|
||||
}
|
||||
|
||||
|
||||
float smoothTriangleWave(float x)
|
||||
{
|
||||
return smoothCurve(triangleWave(x)) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
// OpenGL < 4.3 does not support continued preprocessor lines
|
||||
#if (MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_TRANSPARENT || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_OPAQUE || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_BASIC) && ENABLE_WAVING_WATER
|
||||
|
||||
//
|
||||
// Simple, fast noise function.
|
||||
// See: https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
|
||||
//
|
||||
vec4 perm(vec4 x)
|
||||
{
|
||||
return mod(((x * 34.0) + 1.0) * x, 289.0);
|
||||
}
|
||||
|
||||
float snoise(vec3 p)
|
||||
{
|
||||
vec3 a = floor(p);
|
||||
vec3 d = p - a;
|
||||
d = d * d * (3.0 - 2.0 * d);
|
||||
|
||||
vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0);
|
||||
vec4 k1 = perm(b.xyxy);
|
||||
vec4 k2 = perm(k1.xyxy + b.zzww);
|
||||
|
||||
vec4 c = k2 + a.zzzz;
|
||||
vec4 k3 = perm(c);
|
||||
vec4 k4 = perm(c + 1.0);
|
||||
|
||||
vec4 o1 = fract(k3 * (1.0 / 41.0));
|
||||
vec4 o2 = fract(k4 * (1.0 / 41.0));
|
||||
|
||||
vec4 o3 = o2 * d.z + o1 * (1.0 - d.z);
|
||||
vec2 o4 = o3.yw * d.x + o3.xz * (1.0 - d.x);
|
||||
|
||||
return o4.y * d.y + o4.x * (1.0 - d.y);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void main(void)
|
||||
{
|
||||
varTexCoord = inTexCoord0.st;
|
||||
|
||||
float disp_x;
|
||||
float disp_z;
|
||||
// OpenGL < 4.3 does not support continued preprocessor lines
|
||||
#if (MATERIAL_TYPE == TILE_MATERIAL_WAVING_LEAVES && ENABLE_WAVING_LEAVES) || (MATERIAL_TYPE == TILE_MATERIAL_WAVING_PLANTS && ENABLE_WAVING_PLANTS)
|
||||
vec4 pos2 = mWorld * inVertexPosition;
|
||||
float tOffset = (pos2.x + pos2.y) * 0.001 + pos2.z * 0.002;
|
||||
disp_x = (smoothTriangleWave(animationTimer * 23.0 + tOffset) +
|
||||
smoothTriangleWave(animationTimer * 11.0 + tOffset)) * 0.4;
|
||||
disp_z = (smoothTriangleWave(animationTimer * 31.0 + tOffset) +
|
||||
smoothTriangleWave(animationTimer * 29.0 + tOffset) +
|
||||
smoothTriangleWave(animationTimer * 13.0 + tOffset)) * 0.5;
|
||||
#endif
|
||||
|
||||
worldPosition = (mWorld * inVertexPosition).xyz;
|
||||
|
||||
// OpenGL < 4.3 does not support continued preprocessor lines
|
||||
#if (MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_TRANSPARENT || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_OPAQUE || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_BASIC) && ENABLE_WAVING_WATER
|
||||
// Generate waves with Perlin-type noise.
|
||||
// The constants are calibrated such that they roughly
|
||||
// correspond to the old sine waves.
|
||||
vec4 pos = inVertexPosition;
|
||||
vec3 wavePos = worldPosition + cameraOffset;
|
||||
// The waves are slightly compressed along the z-axis to get
|
||||
// wave-fronts along the x-axis.
|
||||
wavePos.x /= WATER_WAVE_LENGTH * 3.0;
|
||||
wavePos.z /= WATER_WAVE_LENGTH * 2.0;
|
||||
wavePos.z += animationTimer * WATER_WAVE_SPEED * 10.0;
|
||||
pos.y += (snoise(wavePos) - 1.0) * WATER_WAVE_HEIGHT * 5.0;
|
||||
gl_Position = mWorldViewProj * pos;
|
||||
#elif MATERIAL_TYPE == TILE_MATERIAL_WAVING_LEAVES && ENABLE_WAVING_LEAVES
|
||||
vec4 pos = inVertexPosition;
|
||||
pos.x += disp_x;
|
||||
pos.y += disp_z * 0.1;
|
||||
pos.z += disp_z;
|
||||
gl_Position = mWorldViewProj * pos;
|
||||
#elif MATERIAL_TYPE == TILE_MATERIAL_WAVING_PLANTS && ENABLE_WAVING_PLANTS
|
||||
vec4 pos = inVertexPosition;
|
||||
if (varTexCoord.y < 0.05) {
|
||||
pos.x += disp_x;
|
||||
pos.z += disp_z;
|
||||
}
|
||||
gl_Position = mWorldViewProj * pos;
|
||||
#else
|
||||
gl_Position = mWorldViewProj * inVertexPosition;
|
||||
#endif
|
||||
|
||||
|
||||
vPosition = gl_Position.xyz;
|
||||
|
||||
eyeVec = -(mWorldView * inVertexPosition).xyz;
|
||||
|
||||
// Calculate color.
|
||||
// Red, green and blue components are pre-multiplied with
|
||||
// the brightness, so now we have to multiply these
|
||||
// colors with the color of the incoming light.
|
||||
// The pre-baked colors are halved to prevent overflow.
|
||||
vec4 color;
|
||||
// The alpha gives the ratio of sunlight in the incoming light.
|
||||
float nightRatio = 1.0 - inVertexColor.a;
|
||||
color.rgb = inVertexColor.rgb * (inVertexColor.a * dayLight.rgb +
|
||||
nightRatio * artificialLight.rgb) * 2.0;
|
||||
color.a = 1.0;
|
||||
|
||||
// Emphase blue a bit in darker places
|
||||
// See C++ implementation in mapblock_mesh.cpp final_color_blend()
|
||||
float brightness = (color.r + color.g + color.b) / 3.0;
|
||||
color.b += max(0.0, 0.021 - abs(0.2 * brightness - 0.021) +
|
||||
0.07 * brightness);
|
||||
|
||||
varColor = clamp(color, 0.0, 1.0);
|
||||
}
|
96
client/shaders/object_shader/opengl_fragment.glsl
Normal file
96
client/shaders/object_shader/opengl_fragment.glsl
Normal file
@ -0,0 +1,96 @@
|
||||
uniform sampler2D baseTexture;
|
||||
|
||||
uniform vec4 emissiveColor;
|
||||
uniform vec4 skyBgColor;
|
||||
uniform float fogDistance;
|
||||
uniform vec3 eyePosition;
|
||||
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vPosition;
|
||||
varying vec3 worldPosition;
|
||||
varying lowp vec4 varColor;
|
||||
varying mediump vec2 varTexCoord;
|
||||
|
||||
varying vec3 eyeVec;
|
||||
varying float vIDiff;
|
||||
|
||||
const float e = 2.718281828459;
|
||||
const float BS = 10.0;
|
||||
const float fogStart = FOG_START;
|
||||
const float fogShadingParameter = 1.0 / (1.0 - fogStart);
|
||||
|
||||
#ifdef ENABLE_TONE_MAPPING
|
||||
|
||||
/* Hable's UC2 Tone mapping parameters
|
||||
A = 0.22;
|
||||
B = 0.30;
|
||||
C = 0.10;
|
||||
D = 0.20;
|
||||
E = 0.01;
|
||||
F = 0.30;
|
||||
W = 11.2;
|
||||
equation used: ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F
|
||||
*/
|
||||
|
||||
vec3 uncharted2Tonemap(vec3 x)
|
||||
{
|
||||
return ((x * (0.22 * x + 0.03) + 0.002) / (x * (0.22 * x + 0.3) + 0.06)) - 0.03333;
|
||||
}
|
||||
|
||||
vec4 applyToneMapping(vec4 color)
|
||||
{
|
||||
color = vec4(pow(color.rgb, vec3(2.2)), color.a);
|
||||
const float gamma = 1.6;
|
||||
const float exposureBias = 5.5;
|
||||
color.rgb = uncharted2Tonemap(exposureBias * color.rgb);
|
||||
// Precalculated white_scale from
|
||||
//vec3 whiteScale = 1.0 / uncharted2Tonemap(vec3(W));
|
||||
vec3 whiteScale = vec3(1.036015346);
|
||||
color.rgb *= whiteScale;
|
||||
return vec4(pow(color.rgb, vec3(1.0 / gamma)), color.a);
|
||||
}
|
||||
#endif
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec3 color;
|
||||
vec2 uv = varTexCoord.st;
|
||||
|
||||
vec4 base = texture2D(baseTexture, uv).rgba;
|
||||
|
||||
#ifdef USE_DISCARD
|
||||
// If alpha is zero, we can just discard the pixel. This fixes transparency
|
||||
// on GPUs like GC7000L, where GL_ALPHA_TEST is not implemented in mesa,
|
||||
// and also on GLES 2, where GL_ALPHA_TEST is missing entirely.
|
||||
if (base.a == 0.0) {
|
||||
discard;
|
||||
}
|
||||
#endif
|
||||
|
||||
color = base.rgb;
|
||||
|
||||
vec4 col = vec4(color.rgb, base.a);
|
||||
|
||||
col.rgb *= varColor.rgb;
|
||||
|
||||
col.rgb *= emissiveColor.rgb * vIDiff;
|
||||
|
||||
#ifdef ENABLE_TONE_MAPPING
|
||||
col = applyToneMapping(col);
|
||||
#endif
|
||||
|
||||
// Due to a bug in some (older ?) graphics stacks (possibly in the glsl compiler ?),
|
||||
// the fog will only be rendered correctly if the last operation before the
|
||||
// clamp() is an addition. Else, the clamp() seems to be ignored.
|
||||
// E.g. the following won't work:
|
||||
// float clarity = clamp(fogShadingParameter
|
||||
// * (fogDistance - length(eyeVec)) / fogDistance), 0.0, 1.0);
|
||||
// As additions usually come for free following a multiplication, the new formula
|
||||
// should be more efficient as well.
|
||||
// Note: clarity = (1 - fogginess)
|
||||
float clarity = clamp(fogShadingParameter
|
||||
- fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0);
|
||||
col = mix(skyBgColor, col, clarity);
|
||||
|
||||
gl_FragColor = vec4(col.rgb, base.a);
|
||||
}
|
49
client/shaders/object_shader/opengl_vertex.glsl
Normal file
49
client/shaders/object_shader/opengl_vertex.glsl
Normal file
@ -0,0 +1,49 @@
|
||||
uniform mat4 mWorld;
|
||||
|
||||
uniform vec3 eyePosition;
|
||||
uniform float animationTimer;
|
||||
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vPosition;
|
||||
varying vec3 worldPosition;
|
||||
varying lowp vec4 varColor;
|
||||
varying mediump vec2 varTexCoord;
|
||||
|
||||
varying vec3 eyeVec;
|
||||
varying float vIDiff;
|
||||
|
||||
const float e = 2.718281828459;
|
||||
const float BS = 10.0;
|
||||
|
||||
float directional_ambient(vec3 normal)
|
||||
{
|
||||
vec3 v = normal * normal;
|
||||
|
||||
if (normal.y < 0.0)
|
||||
return dot(v, vec3(0.670820, 0.447213, 0.836660));
|
||||
|
||||
return dot(v, vec3(0.670820, 1.000000, 0.836660));
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
varTexCoord = (mTexture * inTexCoord0).st;
|
||||
gl_Position = mWorldViewProj * inVertexPosition;
|
||||
|
||||
vPosition = gl_Position.xyz;
|
||||
vNormal = inVertexNormal;
|
||||
worldPosition = (mWorld * inVertexPosition).xyz;
|
||||
eyeVec = -(mWorldView * inVertexPosition).xyz;
|
||||
|
||||
#if (MATERIAL_TYPE == TILE_MATERIAL_PLAIN) || (MATERIAL_TYPE == TILE_MATERIAL_PLAIN_ALPHA)
|
||||
vIDiff = 1.0;
|
||||
#else
|
||||
// This is intentional comparison with zero without any margin.
|
||||
// If normal is not equal to zero exactly, then we assume it's a valid, just not normalized vector
|
||||
vIDiff = length(inVertexNormal) == 0.0
|
||||
? 1.0
|
||||
: directional_ambient(normalize(inVertexNormal));
|
||||
#endif
|
||||
|
||||
varColor = inVertexColor;
|
||||
}
|
12
client/shaders/selection_shader/opengl_fragment.glsl
Normal file
12
client/shaders/selection_shader/opengl_fragment.glsl
Normal file
@ -0,0 +1,12 @@
|
||||
uniform sampler2D baseTexture;
|
||||
|
||||
varying lowp vec4 varColor;
|
||||
varying mediump vec2 varTexCoord;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec2 uv = varTexCoord.st;
|
||||
vec4 color = texture2D(baseTexture, uv);
|
||||
color.rgb *= varColor.rgb;
|
||||
gl_FragColor = color;
|
||||
}
|
10
client/shaders/selection_shader/opengl_vertex.glsl
Normal file
10
client/shaders/selection_shader/opengl_vertex.glsl
Normal file
@ -0,0 +1,10 @@
|
||||
varying lowp vec4 varColor;
|
||||
varying mediump vec2 varTexCoord;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
varTexCoord = inTexCoord0.st;
|
||||
gl_Position = mWorldViewProj * inVertexPosition;
|
||||
|
||||
varColor = inVertexColor;
|
||||
}
|
6
client/shaders/stars_shader/opengl_fragment.glsl
Normal file
6
client/shaders/stars_shader/opengl_fragment.glsl
Normal file
@ -0,0 +1,6 @@
|
||||
uniform vec4 starColor;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
gl_FragColor = starColor;
|
||||
}
|
4
client/shaders/stars_shader/opengl_vertex.glsl
Normal file
4
client/shaders/stars_shader/opengl_vertex.glsl
Normal file
@ -0,0 +1,4 @@
|
||||
void main(void)
|
||||
{
|
||||
gl_Position = mWorldViewProj * inVertexPosition;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user