diff --git a/builtin/client/init.lua b/builtin/client/init.lua index 9633a7c71..74edcd3f3 100644 --- a/builtin/client/init.lua +++ b/builtin/client/init.lua @@ -9,3 +9,4 @@ dofile(commonpath .. "chatcommands.lua") dofile(clientpath .. "chatcommands.lua") dofile(commonpath .. "vector.lua") dofile(clientpath .. "death_formspec.lua") +dofile(clientpath .. "sscsm.lua") diff --git a/builtin/client/sscsm.lua b/builtin/client/sscsm.lua new file mode 100644 index 000000000..d4fef1320 --- /dev/null +++ b/builtin/client/sscsm.lua @@ -0,0 +1,312 @@ +-- +-- SSCSM: Server-Sent Client-Side Mods +-- +-- Copyright © 2019-2021 by luk3yx +-- Copyright © 2020-2021 MultiCraft Development Team +-- +-- This program is free software; you can redistribute it and/or modify +-- it under the terms of the GNU Lesser General Public License as published by +-- the Free Software Foundation; either version 3.0 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Lesser General Public License for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with this program; if not, write to the Free Software Foundation, +-- Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +-- + +-- For debugging, this can be a global variable. +local sscsm = {} + +-- Load the Env class +-- Mostly copied from https://stackoverflow.com/a/26367080 +-- Don't copy metatables +local function copy(obj, s) + if s and s[obj] ~= nil then return s[obj] end + if type(obj) ~= "table" then return obj end + s = s or {} + local res = {} + s[obj] = res + for k, v in pairs(obj) do res[copy(k, s)] = copy(v, s) end + return res +end + +-- Safe functions +local Env = {} +local safe_funcs = {} + +-- No getmetatable() +if rawget(_G, "getmetatable") then + safe_funcs[getmetatable] = function() end +end + +-- Get the current value of string.rep in case other CSMs decide to break +do + local rep = string.rep + safe_funcs[string.rep] = function(str, n) + if #str * n > 1048576 then + error("string.rep: string length overflow", 2) + end + return rep(str, n) + end + + local show_formspec = minetest.show_formspec + safe_funcs[show_formspec] = function(formname, ...) + if type(formname) == "string" then + return show_formspec("sscsm:_" .. formname, ...) + end + end + + local after = minetest.after + safe_funcs[after] = function(n, ...) + if type(n) == "number" then return after(n, pcall, ...) end + end + + local on_fs_input = minetest.register_on_formspec_input + safe_funcs[on_fs_input] = function(func) + on_fs_input(function(formname, fields) + if formname:sub(1, 7) == "sscsm:_" then + pcall(func, formname:sub(8), copy(fields)) + end + end) + end + + local deserialize = minetest.deserialize + safe_funcs[deserialize] = function(str) + return deserialize(str, true) + end + + local wrap = function(n) + local orig = minetest[n] or minetest[n .. "s"] + if type(orig) == "function" then + return function(func) + orig(function(...) + local r = {pcall(func, ...)} + if r[1] then + table.remove(r, 1) + return (table.unpack or unpack)(r) + else + minetest.log("error", "[SSCSM] " .. tostring(r[2])) + end + end) + end + end + end + + for _, k in ipairs({"register_globalstep", "register_on_death", + "register_on_hp_modification", "register_on_damage_taken", + "register_on_dignode", "register_on_punchnode", + "register_on_placenode", "register_on_item_use", + "register_on_modchannel_message", "register_on_modchannel_signal", + "register_on_inventory_open", "register_on_sending_chat_message", + "register_on_receiving_chat_message"}) do + safe_funcs[minetest[k]] = wrap(k) + end +end + +-- Environment +function Env.new_empty() + local self = {_raw = {}, _seen = copy(safe_funcs)} + self._raw["_G"] = self._raw + return setmetatable(self, {__index = Env}) or self +end +function Env:get(k) return self._raw[self._seen[k] or k] end +function Env:set(k, v) self._raw[copy(k, self._seen)] = copy(v, self._seen) end +function Env:set_copy(k, v) + self:set(k, v) + self._seen[k] = nil + self._seen[v] = nil +end +function Env:add_globals(...) + for i = 1, select("#", ...) do + local var = select(i, ...) + self:set(var, _G[var]) + end +end +function Env:update(data) for k, v in pairs(data) do self:set(k, v) end end +function Env:del(k) + if self._seen[k] then + self._raw[self._seen[k]] = nil + self._seen[k] = nil + end + self._raw[k] = nil +end + +function Env:copy() + local new = {_seen = copy(safe_funcs)} + new._raw = copy(self._raw, new._seen) + return setmetatable(new, {__index = Env}) or new +end + +-- Load code into a callable function. +function Env:loadstring(code, file) + if code:byte(1) == 27 then return nil, "Invalid code!" end + local f, msg = loadstring(code, ("=%q"):format(file)) + if not f then return nil, msg end + setfenv(f, self._raw) + return function(...) + local good, msg = pcall(f, ...) + if good then + return msg + else + minetest.log("error", "[SSCSM] " .. tostring(msg)) + end + end +end + +function Env:exec(code, file) + local f, msg = self:loadstring(code, file) + if not f then + minetest.log("error", "[SSCSM] Syntax error: " .. tostring(msg)) + return false + end + f() + return true +end + +-- Create the "base" environment +local base_env = Env:new_empty() +function Env.new() return base_env:copy() end + +-- Clone everything +base_env:add_globals("assert", "dump", "dump2", "error", "ipairs", "math", + "next", "pairs", "pcall", "select", "setmetatable", "string", "table", + "tonumber", "tostring", "type", "vector", "xpcall", "_VERSION", "utf8") + +base_env:set_copy("os", {clock = os.clock, difftime = os.difftime, + time = os.time}) + +-- Create a slightly locked down "minetest" table +do + local t = {} + for _, k in ipairs({"add_particle", "add_particlespawner", "after", + "camera", "clear_out_chat_queue", "colorize", "compress", "debug", + "decode_base64", "decompress", "delete_particlespawner", + "deserialize", "disconnect", "display_chat_message", + "encode_base64", "explode_scrollbar_event", "explode_table_event", + "explode_textlist_event", "find_node_near", "find_nodes_in_area", + "find_nodes_in_area_under_air", "find_nodes_with_meta", + "formspec_escape", "get_background_escape_sequence", + "get_color_escape_sequence", "get_day_count", "get_item_def", + "get_language", "get_meta", "get_node_def", "get_node_level", + "get_node_light", "get_node_max_level", "get_node_or_nil", + "get_player_names", "get_privilege_list", "get_server_info", + "get_timeofday", "get_translator", "get_us_time", "get_version", + "get_wielded_item", "gettext", "is_nan", "is_yes", "line_of_sight", + "localplayer", "log", "mod_channel_join", "parse_json", + "pointed_thing_to_face_pos", "pos_to_string", "privs_to_string", + "raycast", "register_globalstep", "register_on_damage_taken", + "register_on_death", "register_on_dignode", + "register_on_formspec_input", "register_on_hp_modification", + "register_on_inventory_open", "register_on_item_use", + "register_on_modchannel_message", "register_on_modchannel_signal", + "register_on_placenode", "register_on_punchnode", + "register_on_receiving_chat_message", + "register_on_sending_chat_message", "rgba", + "run_server_chatcommand", "send_chat_message", "send_respawn", + "serialize", "sha1", "show_formspec", "sound_play", "sound_stop", + "string_to_area", "string_to_pos", "string_to_privs", + "strip_background_colors", "strip_colors", + "strip_foreground_colors", "translate", "ui", "wrap_text", + "write_json"}) do + local func = minetest[k] + t[k] = safe_funcs[func] or func + end + + base_env:set_copy("minetest", t) +end + +-- Add table.unpack +if not table.unpack then + base_env._raw.table.unpack = unpack +end + +-- Make sure copy() worked correctly +assert(base_env._raw.minetest.register_on_sending_chat_message ~= + minetest.register_on_sending_chat_message, "Error in copy()!") + +-- SSCSM functions +-- When calling these from an SSCSM, make sure they exist first. +local mod_channel +local loaded_sscsms = {} +base_env:set("join_mod_channel", function() + if not mod_channel then + mod_channel = minetest.mod_channel_join("sscsm:exec_pipe") + end +end) + +base_env:set("leave_mod_channel", function() + if mod_channel then + mod_channel:leave() + mod_channel = nil + end +end) + +-- exec() code sent by the server. +minetest.register_on_modchannel_message(function(channel_name, sender, message) + if channel_name ~= "sscsm:exec_pipe" or (sender and sender ~= "") then + return + end + + -- The first character is currently a version code, currently 0. + -- Do not change unless absolutely necessary. + local version = message:sub(1, 1) + local name, code + if version == "0" then + local s, e = message:find("\n") + if not s or not e then return end + local target = message:sub(2, s - 1) + if target ~= minetest.localplayer:get_name() then return end + message = message:sub(e + 1) + s, e = message:find("\n") + if not s or not e then return end + name = message:sub(1, s - 1) + code = message:sub(e + 1) + else + return + end + + -- Create the environment + if not sscsm.env then sscsm.env = Env:new() end + + -- Don't load the same SSCSM twice + if not loaded_sscsms[name] then + minetest.log("action", "[SSCSM] Loading " .. name) + loaded_sscsms[name] = true + sscsm.env:exec(code, name) + end +end) + +-- Send "0" when the "sscsm:exec_pipe" channel is first joined. +local sent_request = false +minetest.register_on_modchannel_signal(function(channel_name, signal) + if sent_request or channel_name ~= "sscsm:exec_pipe" then + return + end + + if signal == 0 then + base_env._raw.minetest.localplayer = minetest.localplayer + base_env._raw.minetest.camera = minetest.camera + mod_channel:send_all("0") + sent_request = true + elseif signal == 1 then + mod_channel:leave() + mod_channel = nil + end +end) + +local function attempt_to_join_mod_channel() + -- Wait for minetest.localplayer to become available. + if not minetest.localplayer then + minetest.after(0.05, attempt_to_join_mod_channel) + return + end + + -- Join the mod channel + mod_channel = minetest.mod_channel_join("sscsm:exec_pipe") +end +minetest.after(0, attempt_to_join_mod_channel) diff --git a/builtin/game/sscsm/client.lua b/builtin/game/sscsm/client.lua index e0ecf2ce5..9c851cdd7 100644 --- a/builtin/game/sscsm/client.lua +++ b/builtin/game/sscsm/client.lua @@ -340,4 +340,4 @@ end) if not core.global_exists("utf8") or not utf8.lower then utf8 = string -end \ No newline at end of file +end diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 05378b3cb..d077e4065 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -975,7 +975,7 @@ enable_remote_media_server (Connect to external media server) bool true # Enable Lua modding support on client. # This support is experimental and API can change. -enable_client_modding (Client modding) bool false +enable_client_modding (Client modding) bool true # URL to the server list displayed in the Multiplayer Tab. serverlist_url (Serverlist URL) string servers.minetest.net diff --git a/multicraft.conf.example b/multicraft.conf.example index ae49e2c17..7292de2ed 100644 --- a/multicraft.conf.example +++ b/multicraft.conf.example @@ -1162,7 +1162,7 @@ # Enable Lua modding support on client. # This support is experimental and API can change. # type: bool -# enable_client_modding = false +# enable_client_modding = true # URL to the server list displayed in the Multiplayer Tab. # type: string diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 671cb211c..838c6a3ca 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -59,7 +59,7 @@ void set_default_settings(Settings *settings) settings->setDefault("curl_file_download_timeout", "300000"); settings->setDefault("curl_verify_cert", "true"); settings->setDefault("enable_remote_media_server", "true"); - settings->setDefault("enable_client_modding", "false"); + settings->setDefault("enable_client_modding", "true"); settings->setDefault("max_out_chat_queue_size", "20"); settings->setDefault("pause_on_lost_focus", "false"); settings->setDefault("enable_register_confirmation", "true");