From 90211350a5d2c92e207cd983c24d5f94262a63bf Mon Sep 17 00:00:00 2001 From: luk3yx Date: Thu, 30 Jul 2020 19:46:46 +1200 Subject: [PATCH] Add server-side SSCSM support. --- builtin/game/init.lua | 1 + builtin/game/sscsm/client.lua | 343 ++++++++++++++++++++++++++++++++++ builtin/game/sscsm/init.lua | 315 +++++++++++++++++++++++++++++++ builtin/game/sscsm/minify.lua | 132 +++++++++++++ builtin/settingtypes.txt | 4 +- doc/lua_api.txt | 104 +++++++++++ multicraft.conf.example | 61 +++--- src/defaultsettings.cpp | 4 +- 8 files changed, 929 insertions(+), 35 deletions(-) create mode 100644 builtin/game/sscsm/client.lua create mode 100644 builtin/game/sscsm/init.lua create mode 100644 builtin/game/sscsm/minify.lua diff --git a/builtin/game/init.lua b/builtin/game/init.lua index 1d62be019..b3b6ac11c 100644 --- a/builtin/game/init.lua +++ b/builtin/game/init.lua @@ -34,5 +34,6 @@ dofile(gamepath .. "voxelarea.lua") dofile(gamepath .. "forceloading.lua") dofile(gamepath .. "statbars.lua") dofile(gamepath .. "knockback.lua") +dofile(gamepath .. "sscsm" .. DIR_DELIM .. "init.lua") profiler = nil diff --git a/builtin/game/sscsm/client.lua b/builtin/game/sscsm/client.lua new file mode 100644 index 000000000..e0ecf2ce5 --- /dev/null +++ b/builtin/game/sscsm/client.lua @@ -0,0 +1,343 @@ +-- +-- SSCSM: Server-Sent Client-Side Mods +-- Initial code sent to the client +-- +-- Copyright © 2019-2021 by luk3yx +-- Copyright © 2020-2021 MultiCraft Development Team +-- License: GNU LGPL 3.0+ +-- +-- 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. +-- + +-- Make sure both table.unpack and unpack exist. +if table.unpack then + unpack = table.unpack +else + table.unpack = unpack -- luacheck: ignore +end + +-- Make sure a few basic functions exist, these may have been blocked because +-- of security or laziness. +if not rawget then function rawget(n, name) return n[name] end end +if not rawset then function rawset(n, k, v) n[k] = v end end +if not rawequal then function rawequal(a, b) return a == b end end + +-- Older versions of the CSM don't provide assert(), this function exists for +-- compatibility. +if not assert then + function assert(value, ...) + if value then + return value, ... + else + error(... or 'assertion failed!', 2) + end + end +end + +-- Create the API +sscsm = {} +function sscsm.global_exists(name) + return rawget(_G, name) ~= nil +end + +if sscsm.global_exists('minetest') then + core = minetest +else + minetest = assert(core, 'No "minetest" global found!') +end + +core.global_exists = sscsm.global_exists + +-- Check if join_mod_channel and leave_mod_channel exist. +if sscsm.global_exists('join_mod_channel') + and sscsm.global_exists('leave_mod_channel') then + sscsm.join_mod_channel = join_mod_channel + sscsm.leave_mod_channel = leave_mod_channel + join_mod_channel, leave_mod_channel = nil, nil +else + local dummy = function() end + sscsm.join_mod_channel = dummy + sscsm.leave_mod_channel = dummy +end + +-- Add print() +function print(...) + local msg = '[SSCSM] ' + for i = 1, select('#', ...) do + if i > 1 then msg = msg .. '\t' end + msg = msg .. tostring(select(i, ...)) + end + core.log('none', msg) +end + +-- Add register_on_mods_loaded +do + local funcs = {} + function sscsm.register_on_mods_loaded(callback) + if funcs then table.insert(funcs, callback) end + end + + function sscsm._done_loading_() + sscsm._done_loading_ = nil + for _, func in ipairs(funcs) do func() end + funcs = nil + end +end + +-- Helper functions +if not core.get_node then + function core.get_node(pos) + return core.get_node_or_nil(pos) or {name = 'ignore', param1 = 0, + param2 = 0} + end +end + +-- Make core.run_server_chatcommand allow param to be unspecified. +function core.run_server_chatcommand(cmd, param) + core.send_chat_message('/' .. cmd .. ' ' .. (param or '')) +end + +-- Register "server-side" chatcommands +-- Can allow instantaneous responses in some cases. +sscsm.registered_chatcommands = {} +local function on_chat_message(msg) + if msg:sub(1, 1) ~= '/' then return false end + + local cmd, param = msg:match('^/([^ ]+) *(.*)') + if not cmd then + core.display_chat_message('-!- Empty command') + return true + end + + if not sscsm.registered_chatcommands[cmd] then return false end + + local _, res = sscsm.registered_chatcommands[cmd].func(param or '') + if res then core.display_chat_message(tostring(res)) end + + return true +end + +function sscsm.register_chatcommand(cmd, def) + if type(def) == 'function' then + def = {func = def} + elseif type(def.func) ~= 'function' then + error('Invalid definition passed to sscsm.register_chatcommand.') + end + + sscsm.registered_chatcommands[cmd] = def + + if on_chat_message then + core.register_on_sending_chat_message(on_chat_message) + on_chat_message = nil + end +end + +function sscsm.unregister_chatcommand(cmd) + sscsm.registered_chatcommands[cmd] = nil +end + +-- A proper get_player_control didn't exist before Minetest 5.3.0. +if core.localplayer.get_control then + -- Preserve API compatibility + if core.localplayer:get_control().LMB == nil then + -- MT 5.4+ + function sscsm.get_player_control() + local c = core.localplayer:get_control() + c.LMB, c.RMB = c.dig, c.place + return c + end + else + -- MT 5.3 + function sscsm.get_player_control() + local c = core.localplayer:get_control() + c.dig, c.place = c.LMB, c.RMB + return c + end + end +else + -- MT 5.0 to 5.2 + local floor = math.floor + function sscsm.get_player_control() + local n = core.localplayer:get_key_pressed() + return { + up = n % 2 == 1, + down = floor(n / 2) % 2 == 1, + left = floor(n / 4) % 2 == 1, + right = floor(n / 8) % 2 == 1, + jump = floor(n / 16) % 2 == 1, + aux1 = floor(n / 32) % 2 == 1, + sneak = floor(n / 64) % 2 == 1, + LMB = floor(n / 128) % 2 == 1, + RMB = floor(n / 256) % 2 == 1, + dig = floor(n / 128) % 2 == 1, + place = floor(n / 256) % 2 == 1, + } + end + + -- In Minetest 5.2.0, core.get_node_light() segfaults. + core.get_node_light = nil +end + +-- Call func(...) every seconds. +local function sscsm_every(interval, func, ...) + core.after(interval, sscsm_every, interval, func, ...) + return func(...) +end + +function sscsm.every(interval, func, ...) + assert(type(interval) == 'number' and type(func) == 'function', + 'Invalid sscsm.every() invocation.') + return sscsm_every(interval, func, ...) +end + +-- Allow SSCSMs to know about CSM restriction flags. +-- "__FLAGS__" is replaced with the actual value in init.lua. +-- luacheck: globals __FLAGS__ __MAPGEN_LIMIT__ +local flags = __FLAGS__ +sscsm.restriction_flags = assert(flags) +sscsm.restrictions = { + chat_messages = math.floor(flags / 2) % 2 == 1, + read_itemdefs = math.floor(flags / 4) % 2 == 1, + read_nodedefs = math.floor(flags / 8) % 2 == 1, + lookup_nodes_limit = math.floor(flags / 16) % 2 == 1, + read_playerinfo = math.floor(flags / 32) % 2 == 1, +} +sscsm.restrictions.lookup_nodes = sscsm.restrictions.lookup_nodes_limit + +-- Add core.get_csm_restrictions() if it doesn't exist already. +if not core.get_csm_restrictions then + function core.get_csm_restrictions() + return table.copy(sscsm.restrictions) + end +end + +local mapgen_limit = __MAPGEN_LIMIT__ +function core.is_valid_pos(pos) + if not pos or type(pos) ~= "table" then + return false + end + for _, v in ipairs({"x", "y", "z"}) do + if not pos[v] or pos[v] ~= pos[v] or + pos[v] < -mapgen_limit or pos[v] > mapgen_limit then + return false + end + end + + return true +end + +-- SSCSM communication +-- A lot of this is copied from init.lua. +local function validate_channel(channel) + if type(channel) ~= 'string' then + error('SSCSM com channels must be strings!', 3) + end + if channel:find('\001', nil, true) then + error('SSCSM com channels cannot contain U+0001!', 3) + end +end + +function sscsm.com_send(channel, msg) + assert(not sscsm.restrictions.chat_messages, 'Server restrictions ' .. + 'prevent SSCSM com messages from being sent!') + validate_channel(channel) + if type(msg) == 'string' then + msg = '\002' .. msg + else + msg = core.write_json(msg) + end + core.run_server_chatcommand('admin', '\001SSCSM_COM\001' .. channel .. + '\001' .. msg) +end + +local registered_on_receive = {} +function sscsm.register_on_com_receive(channel, func) + if not registered_on_receive[channel] then + registered_on_receive[channel] = {} + end + table.insert(registered_on_receive[channel], func) +end + +-- Load split messages +local incoming_messages = {} +local function load_split_message(chan, msg) + local id, i, l, pkt = msg:match('^\1([^\1]+)\1([^\1]+)\1([^\1]+)\1(.*)$') + id, i, l = tonumber(id), tonumber(i), tonumber(l) + + if not incoming_messages[id] then + incoming_messages[id] = {} + end + local msgs = incoming_messages[id] + msgs[i] = pkt + + -- Return true if all the messages have been received + if #msgs < l then return end + for j = 1, l do + if not msgs[j] then + return + end + end + incoming_messages[id] = nil + return table.concat(msgs, '') +end + +-- Detect messages and handle them +core.register_on_receiving_chat_message(function(message) + if type(message) == 'table' then + message = message.message + end + + local chan, msg = message:match('^\001SSCSM_COM\001([^\001]*)\001(.*)$') + if not chan or not msg then return end + + -- Get the callbacks + local callbacks = registered_on_receive[chan] + if not callbacks then return true end + + -- Handle split messages + local prefix = msg:sub(1, 1) + if prefix == '\001' then + msg = load_split_message(chan, msg) + if not msg then + return true + end + prefix = msg:sub(1, 1) + end + + -- Load the message + if prefix == '\002' then + msg = msg:sub(2) + else + msg = core.parse_json(msg) + end + + -- Run callbacks + for _, func in ipairs(callbacks) do + local ok, err = pcall(func, msg) + if not ok then + core.log('error', '[SSCSM] ' .. tostring(err)) + end + end + return true +end) + +sscsm.register_on_mods_loaded(function() + sscsm.leave_mod_channel() + sscsm.com_send('sscsm:com_test', {flags = sscsm.restriction_flags}) +end) + +if not core.global_exists("utf8") or not utf8.lower then + utf8 = string +end \ No newline at end of file diff --git a/builtin/game/sscsm/init.lua b/builtin/game/sscsm/init.lua new file mode 100644 index 000000000..e6351a7f9 --- /dev/null +++ b/builtin/game/sscsm/init.lua @@ -0,0 +1,315 @@ +-- +-- 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. +-- + +sscsm = {minify=true} +local modpath = core.get_builtin_path() .. "game" .. DIR_DELIM .. + "sscsm" .. DIR_DELIM + +-- Remove excess whitespace from code to allow larger files to be sent. +if sscsm.minify then + local f = loadfile(modpath .. "minify.lua") + if f then + sscsm.minify_code = f() + else + core.log("warning", "[SSCSM] Could not load minify.lua!") + end +end + +if not sscsm.minify_code then + function sscsm.minify_code(code) + assert(type(code) == "string") + return code + end +end + +-- Register code +sscsm.registered_csms = {} +local csm_order = false + +-- Recalculate the CSM loading order +-- TODO: Make this nicer +local function recalc_csm_order() + local staging = {} + local order = {":init"} + local unsatisfied = {} + for name, def in pairs(sscsm.registered_csms) do + assert(name == def.name) + if name:sub(1, 1) ~= ":" then + if not def.depends or #def.depends == 0 then + table.insert(staging, name) + else + unsatisfied[name] = {} + for _, mod in ipairs(def.depends) do + if mod:sub(1, 1) ~= ":" then + unsatisfied[name][mod] = true + end + end + end + end + end + while #staging > 0 do + local name = staging[1] + for name2, u in pairs(unsatisfied) do + if u[name] then + u[name] = nil + if #u == 0 then + table.insert(staging, name2) + end + end + end + + table.insert(order, name) + table.remove(staging, 1) + end + + for name, u in pairs(unsatisfied) do + if next(u) then + local msg = 'SSCSM "' .. name .. '" has unsatisfied dependencies: ' + local n = false + for dep, _ in pairs(u) do + if n then msg = msg .. ", " else n = true end + msg = msg .. '"' .. dep .. '"' + end + core.log("error", msg) + end + end + + -- Set csm_order + table.insert(order, ":cleanup") + csm_order = order +end + +-- Register SSCSMs +local block_colon = false +sscsm.registered_csms = {} +function sscsm.register(def) + -- Read files now in case MT decides to block access later. + if not def.code and def.file then + local f = io.open(def.file, "rb") + if not f then + error('Invalid "file" parameter passed to sscsm.register_csm.', 2) + end + def.code = f:read("*a") + f:close() + def.file = nil + end + + if type(def.name) ~= "string" or def.name:find("\n") + or (def.name:sub(1, 1) == ":" and block_colon) then + error('Invalid "name" parameter passed to sscsm.register_csm.', 2) + end + + if type(def.code) ~= "string" then + error('Invalid "code" parameter passed to sscsm.register_csm.', 2) + end + + def.code = sscsm.minify_code(def.code) + if (#def.name + #def.code) > 65300 then + error("The code (or name) passed to sscsm.register_csm is too large." + .. " Consider refactoring your SSCSM code.", 2) + end + + -- Copy the table to prevent mods from betraying our trust. + sscsm.registered_csms[def.name] = table.copy(def) + if csm_order then recalc_csm_order() end +end + +function sscsm.unregister(name) + sscsm.registered_csms[name] = nil + if csm_order then recalc_csm_order() end +end + +-- Recalculate the CSM order once all other mods are loaded +core.register_on_mods_loaded(recalc_csm_order) + +-- Handle players joining +local has_sscsms = {} +local mod_channel = core.mod_channel_join("sscsm:exec_pipe") +if not core.is_singleplayer() then + core.register_on_modchannel_message(function(channel_name, sender, message) + if channel_name ~= "sscsm:exec_pipe" or not sender or + not mod_channel:is_writeable() or message ~= "0" or + sender:find("\n") or has_sscsms[sender] then + return + end + core.log("action", "[SSCSM] Sending CSMs on request for " .. sender + .. "...") + for _, name in ipairs(csm_order) do + local def = sscsm.registered_csms[name] + if not def.is_enabled_for or def.is_enabled_for(sender) then + mod_channel:send_all("0" .. sender .. "\n" .. name + .. "\n" .. sscsm.registered_csms[name].code) + end + end + end) +end + +-- Register the SSCSM "builtins" +sscsm.register({ + name = ":init", + file = modpath .. "client.lua" +}) + +sscsm.register({ + name = ":cleanup", + code = "sscsm._done_loading_()" +}) + +block_colon = true + +-- Set the CSM restriction flags +local flags = tonumber(core.settings:get("csm_restriction_flags")) +if not flags or flags ~= flags then + flags = 62 +end +flags = math.floor(math.max(flags, 0)) % 64 + +do + local def = sscsm.registered_csms[":init"] + def.code = def.code:gsub("__FLAGS__", tostring(flags)) + + local mapgen_limit = tonumber(core.settings:get("mapgen_limit")) + def.code = def.code:gsub("__MAPGEN_LIMIT__", tostring(mapgen_limit)) +end + +if math.floor(flags / 2) % 2 == 1 then + core.log("warning", "[SSCSM] SSCSMs enabled, however CSMs cannot " + .. "send chat messages! This will prevent SSCSMs from sending " + .. "messages to the server.") + sscsm.com_write_only = true +else + sscsm.com_write_only = false +end + +-- SSCSM communication +local function validate_channel(channel) + if type(channel) ~= "string" then + error("SSCSM com channels must be strings!", 3) + end + if channel:find("\001", nil, true) then + error("SSCSM com channels cannot contain U+0001!", 3) + end +end + +local msgids = {} +function sscsm.com_send(pname, channel, msg) + if core.is_player(pname) then + pname = pname:get_player_name() + end + validate_channel(channel) + if type(msg) == "string" then + msg = "\002" .. msg + else + msg = assert(core.write_json(msg)) + end + + -- Short messages can be sent all at once + local prefix = "\001SSCSM_COM\001" .. channel .. "\001" + if #msg < 65300 then + core.chat_send_player(pname, prefix .. msg) + return + end + + -- You should never send messages over 128MB to clients + assert(#msg < 134217728) + + -- Otherwise split the message into multiple chunks + prefix = prefix .. "\001" + local id = #msgids + 1 + local i = 0 + msgids[id] = true + local total_msgs = math.ceil(#msg / 65000) + repeat + i = i + 1 + core.chat_send_player(pname, prefix .. id .. "\001" .. i .. + "\001" .. total_msgs .. "\001" .. msg:sub(1, 65000)) + msg = msg:sub(65001) + until msg == "" + + -- Allow the ID to be reused on the next globalstep. + core.after(0, function() + msgids[id] = nil + end) +end + +local registered_on_receive = {} +function sscsm.register_on_com_receive(channel, func) + if not registered_on_receive[channel] then + registered_on_receive[channel] = {} + end + table.insert(registered_on_receive[channel], func) +end + +local admin_func = core.registered_chatcommands["admin"].func +core.override_chatcommand("admin", { + func = function(name, param) + local chan, msg = param:match("^\001SSCSM_COM\001([^\001]*)\001(.*)$") + if not chan or not msg then + return admin_func(name, param) + end + + -- Get the callbacks + local callbacks = registered_on_receive[chan] + if not callbacks then return end + + -- Load the message + if msg:sub(1, 1) == "\002" then + msg = msg:sub(2) + else + msg = core.parse_json(msg) + end + + -- Run callbacks + for _, func in ipairs(callbacks) do + func(name, msg) + end + end, +}) + +-- Add a callback for sscsm:com_test +local registered_on_loaded = {} +function sscsm.register_on_sscsms_loaded(func) + table.insert(registered_on_loaded, func) +end + +sscsm.register_on_com_receive("sscsm:com_test", function(name, msg) + if type(msg) ~= "table" or msg.flags ~= flags or has_sscsms[name] then + return + end + has_sscsms[name] = true + for _, func in ipairs(registered_on_loaded) do + func(name) + end +end) + +function sscsm.has_sscsms_enabled(name) + return has_sscsms[name] or false +end + +core.register_on_leaveplayer(function(player) + has_sscsms[player:get_player_name()] = nil +end) + +function sscsm.com_send_all(channel, msg) + for name, _ in pairs(has_sscsms) do + sscsm.com_send(name, channel, msg) + end +end diff --git a/builtin/game/sscsm/minify.lua b/builtin/game/sscsm/minify.lua new file mode 100644 index 000000000..47bb5f4f1 --- /dev/null +++ b/builtin/game/sscsm/minify.lua @@ -0,0 +1,132 @@ +-- +-- A primitive code minifier +-- +-- 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. +-- + +-- Find multiple patterns +local function find_multiple(text, ...) + local n = select('#', ...) + local s, e, pattern + for i = 1, n do + local p = select(i, ...) + local s2, e2 = text:find(p) + if s2 and (not s or s2 < s) then + s, e, pattern = s2, e2 or s2, p + end + end + return s, e, pattern +end + +-- Matches +-- These take 2-3 arguments (code, res, char) and should return code and res. +local matches = { + -- Handle multi-line strings + ['%[=*%['] = function(code, res, char) + res = res .. char + char = char:sub(2, -2) + local s, e = code:find(']' .. char .. ']', nil, true) + if not s or not e then return code, res end + return code:sub(e + 1), res .. code:sub(1, e) + end, + + -- Handle regular comments + ['--'] = function(code, res, char) + local s, e = code:find('\n', nil, true) + if not s or not e then return '', res end + + -- Don't remove copyright or license information. + if e >= 7 then + local first_word = (code:match('^[ \t]*(%w+)') or ''):lower() + if first_word == 'copyright' or first_word == 'license' then + return code:sub(s), res .. char .. code:sub(1, s - 1) + end + end + + -- Shift trailing spaces back + local spaces = res:match('(%s*)$') or '' + return spaces .. code:sub(s), res:sub(1, #res - #spaces) + end, + + -- Handle multi-line comments + ['%-%-%[=*%['] = function(code, res, char) + char = char:sub(4, -2) + local s, e = code:find(']' .. char .. ']', nil, true) + if not s or not e then return code, res end + + -- Shift trailing spaces back + local spaces = res:match('(%s*)$') or '' + return spaces .. code:sub(e + 1), res:sub(1, #res - #spaces) + end, + + -- Handle quoted text + ['"'] = function(code, res, char) + res = res .. char + + -- Handle backslashes + repeat + local _, e, pattern = find_multiple(code, '\\', char) + if pattern == char then + res = res .. code:sub(1, e) + code = code:sub(e + 1) + elseif pattern then + res = res .. code:sub(1, e + 1) + code = code:sub(e + 2) + end + until not pattern or pattern == char + + return code, res + end, + + ['%s*[\r\n]%s*'] = function(code, res, char) + return code, res .. '\n' + end, + + ['[ \t]+'] = function(code, res, char) + return code, res .. ' ' + end, +} + +-- Give the functions alternate names +matches["'"] = matches['"'] + +-- The actual transpiler +return function(code) + assert(type(code) == 'string') + + local res = '' + + -- Split the code by "tokens" + while true do + -- Search for special characters + local s, e, pattern = find_multiple(code, '[\'"\\]', '%-%-%[=*%[', + '%-%-', '%[=*%[', '%s*[\r\n]%s*', '[ \t]+') + if not s then break end + + -- Add non-matching characters + res = res .. code:sub(1, math.max(s - 1, 0)) + + -- Call the correct function + local char = code:sub(s, e) + local func = matches[char] or matches[pattern] + assert(func, 'No function found for pattern!') + code, res = func(code:sub(e + 1), res, char) + end + + return (res .. code):trim() +end diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index c787aea2c..05378b3cb 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -1124,7 +1124,7 @@ player_transfer_distance (Player transfer distance) int 0 enable_pvp (Player versus player) bool true # Enable mod channels support. -enable_mod_channels (Mod channels) bool false +enable_mod_channels (Mod channels) bool true # If this is set, players will always (re)spawn at the given position. static_spawnpoint (Static spawnpoint) string @@ -1315,7 +1315,7 @@ server_side_occlusion_culling (Server side occlusion culling) bool true # LOOKUP_NODES_LIMIT: 16 (limits get_node call client-side to # csm_restriction_noderange) # READ_PLAYERINFO: 32 (disable get_player_names call client-side) -csm_restriction_flags (Client side modding restrictions) int 62 +csm_restriction_flags (Client side modding restrictions) int 60 # If the CSM restriction for node range is enabled, get_node calls are limited # to this distance from the player to the node. diff --git a/doc/lua_api.txt b/doc/lua_api.txt index 43d57aa4f..05a35ede6 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -3033,6 +3033,110 @@ angles in radians. +SSCSM +===== + +MultiCraft has a built-in SSCSM implementation that will load SSCSMs onto +MultiCraft clients, as well as non-MultiCraft clients if they have the correct +CSM installed. + + +Server-side API +--------------- + +* `sscsm.register(SSCSM definition table)`: + * Registers a server-provided CSM with an [SSCSM definition table]. +* `sscsm.register_on_sscsms_loaded(function(name))`: + * Registers a callback that will be called when SSCSMs have been loaded + on a client. +* `sscsm.has_sscsms_enabled(name)`: + * Returns a boolean. + * Returns `true` if the client supports SSCSMs and SSCSMs have been loaded. + * Note that this will not return `true` immediately after joining. +* `sscsm.com_send(player_or_name, channel, msg)`: + * Sends a com message to a specific client. + * `msg` can be any object that can be serialized with JSON. +* `sscsm.com_send_all(channel, msg)`: + * Sends a com message to all clients. +* `sscsm.register_on_com_receive(channel, function(name, msg))`: + * Registers a function to be called when a message on channel is received + from the client. + * Note that `msg` may be any JSON-compatible type, so checking the type of + this object is strongly recommended. + +Client-side API +--------------- + +SSCSMs have access most of the API functions mentioned in client_lua_api.txt, +along with an extra `sscsm` namespace: + +* `sscsm.register_on_mods_loaded(function())`: + * Runs the provided callback once all SSCSMs are loaded. +* `sscsm.register_chatcommand(command, function(param))`: + * Similar to `minetest.register_chatcommand`, however overrides commands + starting in `/` instead. This can be used to make some commands have + instantaneous responses. +* `sscsm.unregister_chatcommand(command)`: + * Unregisters a chatcommand. +* `sscsm.get_player_control()`: + * Returns a table similar to the server-side `player:get_player_control()`. +* `sscsm.every(interval, func, ...)`: + * Calls `func` every `interval` seconds with any extra parameters specified. + * Use `minetest.register_globalstep` instead if `interval` is zero. +* `sscsm.com_send(channel, msg)`: + * Sends `msg` (a JSON-compatible object) to the server. + * Note that client-to-server messages cannot be long, for plain strings + the channel and message combined must be at most 492 characters. +* `sscsm.register_on_com_receive(channel, function(msg))`: + * Registers a function to be called when a message on `channel` is received + from the server. + +This namespace has the following constants: +* `sscsm.restriction_flags`: + * The `csm_restriction_flags` setting set in the server's `multicraft.conf`. +* `sscsm.restrictions`: A table based on `csm_restriction_flags`: + * `chat_messages`: When `true`, SSCSMs can't send chat messages or run + server chatcommands. + * `read_itemdefs`: When `true`, SSCSMs can't read item definitions. + * `read_nodedefs`: When `true`, SSCSMs can't read node definitions. + * `lookup_nodes_limit`: When `true`, any get_node calls are restricted. + * `read_playerinfo`: When `true`, `minetest.get_player_names()` will return + `nil`. + + +SSCSM definition table +---------------------- + +```lua +sscsm.register({ + -- The name of the server-provided CSM. Using `modname` or + -- `modname:sscsmname` is strongly recommended. This name cannot start with + -- a colon or contain newlines. + name = "mymod", + + -- The code to be sent to clients. + -- code = "print('Hello world!')", + + -- The file to read code from. + -- This should be used instead of `code`. + file = minetest.get_modpath("mymod") .. DIR_DELIM .. "sscsm.lua", + + -- An optional list of SSCSMs that this one depends on. + -- depends = {"othermod"}, +}) +``` + +Security considerations +----------------------- + +Do not trust any input sent to the server via SSCSMs (and do not store +sensitive data in SSCSM code), as malicious users can and will inspect code and +modify the output from SSCSMs. + +I repeat, **do not trust the client** and/or SSCSMs with any sensitive +information and do not trust any output from the client and/or SSCSMs. Make +sure you rerun any privilege checks on the server. + Helper functions ================ diff --git a/multicraft.conf.example b/multicraft.conf.example index af7dc72e6..ae49e2c17 100644 --- a/multicraft.conf.example +++ b/multicraft.conf.example @@ -1351,7 +1351,7 @@ # Enable mod channels support. # type: bool -# enable_mod_channels = false +# enable_mod_channels = true # If this is set, players will always (re)spawn at the given position. # type: string @@ -1389,7 +1389,7 @@ # ask_reconnect_on_crash = false # From how far clients know about objects, stated in mapblocks (16 nodes). -# +# # Setting this larger than active_block_range will also cause the server # to maintain active objects up to this distance in the direction the # player is looking. (This can avoid mobs suddenly disappearing from view) @@ -1589,7 +1589,7 @@ # csm_restriction_noderange) # READ_PLAYERINFO: 32 (disable get_player_names call client-side) # type: int -# csm_restriction_flags = 62 +# csm_restriction_flags = 60 # If the CSM restriction for node range is enabled, get_node calls are limited # to this distance from the player to the node. @@ -1942,7 +1942,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # Second of two 3D noises that together define tunnels. @@ -1955,7 +1955,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # 3D noise defining giant caverns. @@ -1968,7 +1968,7 @@ # octaves = 5, # persistence = 0.63, # lacunarity = 2.0, -# flags = +# flags = # } # 3D noise defining terrain. @@ -1994,7 +1994,7 @@ # octaves = 2, # persistence = 0.8, # lacunarity = 2.0, -# flags = +# flags = # } ## Mapgen V6 @@ -2381,7 +2381,7 @@ # octaves = 5, # persistence = 0.63, # lacunarity = 2.0, -# flags = +# flags = # } # 3D noise defining structure of river canyon walls. @@ -2394,7 +2394,7 @@ # octaves = 4, # persistence = 0.75, # lacunarity = 2.0, -# flags = +# flags = # } # 3D noise defining structure of floatlands. @@ -2410,7 +2410,7 @@ # octaves = 4, # persistence = 0.75, # lacunarity = 1.618, -# flags = +# flags = # } # 3D noise defining giant caverns. @@ -2423,7 +2423,7 @@ # octaves = 5, # persistence = 0.63, # lacunarity = 2.0, -# flags = +# flags = # } # First of two 3D noises that together define tunnels. @@ -2436,7 +2436,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # Second of two 3D noises that together define tunnels. @@ -2449,7 +2449,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # 3D noise that determines number of dungeons per mapchunk. @@ -2462,7 +2462,7 @@ # octaves = 2, # persistence = 0.8, # lacunarity = 2.0, -# flags = +# flags = # } ## Mapgen Carpathian @@ -2705,7 +2705,7 @@ # octaves = 5, # persistence = 0.55, # lacunarity = 2.0, -# flags = +# flags = # } # First of two 3D noises that together define tunnels. @@ -2718,7 +2718,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # Second of two 3D noises that together define tunnels. @@ -2731,7 +2731,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # 3D noise defining giant caverns. @@ -2744,7 +2744,7 @@ # octaves = 5, # persistence = 0.63, # lacunarity = 2.0, -# flags = +# flags = # } # 3D noise that determines number of dungeons per mapchunk. @@ -2757,7 +2757,7 @@ # octaves = 2, # persistence = 0.8, # lacunarity = 2.0, -# flags = +# flags = # } ## Mapgen Flat @@ -2867,7 +2867,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # Second of two 3D noises that together define tunnels. @@ -2880,7 +2880,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # 3D noise that determines number of dungeons per mapchunk. @@ -2893,7 +2893,7 @@ # octaves = 2, # persistence = 0.8, # lacunarity = 2.0, -# flags = +# flags = # } ## Mapgen Fractal @@ -3067,7 +3067,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # Second of two 3D noises that together define tunnels. @@ -3080,7 +3080,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # 3D noise that determines number of dungeons per mapchunk. @@ -3093,7 +3093,7 @@ # octaves = 2, # persistence = 0.8, # lacunarity = 2.0, -# flags = +# flags = # } ## Mapgen Valleys @@ -3183,7 +3183,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # Second of two 3D noises that together define tunnels. @@ -3196,7 +3196,7 @@ # octaves = 3, # persistence = 0.5, # lacunarity = 2.0, -# flags = +# flags = # } # The depth of dirt or other biome filler node. @@ -3222,7 +3222,7 @@ # octaves = 6, # persistence = 0.63, # lacunarity = 2.0, -# flags = +# flags = # } # Defines large-scale river channel structure. @@ -3274,7 +3274,7 @@ # octaves = 6, # persistence = 0.8, # lacunarity = 2.0, -# flags = +# flags = # } # Amplifies the valleys. @@ -3313,7 +3313,7 @@ # octaves = 2, # persistence = 0.8, # lacunarity = 2.0, -# flags = +# flags = # } ## Advanced @@ -3374,4 +3374,3 @@ # so see a full list at https://content.minetest.net/help/content_flags/ # type: string # contentdb_flag_blacklist = nonfree, desktop_default - diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index a517ff3ec..671cb211c 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -360,7 +360,7 @@ void set_default_settings(Settings *settings) settings->setDefault("default_password", ""); settings->setDefault("default_privs", "interact, shout"); settings->setDefault("enable_pvp", "true"); - settings->setDefault("enable_mod_channels", "false"); + settings->setDefault("enable_mod_channels", "true"); settings->setDefault("disallow_empty_password", "false"); settings->setDefault("disable_anticheat", "false"); settings->setDefault("enable_rollback_recording", "false"); @@ -383,7 +383,7 @@ void set_default_settings(Settings *settings) settings->setDefault("max_block_send_distance", "10"); settings->setDefault("block_send_optimize_distance", "4"); settings->setDefault("server_side_occlusion_culling", "true"); - settings->setDefault("csm_restriction_flags", "62"); + settings->setDefault("csm_restriction_flags", "60"); settings->setDefault("csm_restriction_noderange", "0"); settings->setDefault("max_clearobjects_extra_loaded_blocks", "4096"); settings->setDefault("time_speed", "72");