Add server-side SSCSM support.
parent
7b37f2c1d8
commit
90211350a5
|
@ -34,5 +34,6 @@ dofile(gamepath .. "voxelarea.lua")
|
||||||
dofile(gamepath .. "forceloading.lua")
|
dofile(gamepath .. "forceloading.lua")
|
||||||
dofile(gamepath .. "statbars.lua")
|
dofile(gamepath .. "statbars.lua")
|
||||||
dofile(gamepath .. "knockback.lua")
|
dofile(gamepath .. "knockback.lua")
|
||||||
|
dofile(gamepath .. "sscsm" .. DIR_DELIM .. "init.lua")
|
||||||
|
|
||||||
profiler = nil
|
profiler = nil
|
||||||
|
|
|
@ -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 <interval> 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
|
|
@ -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
|
|
@ -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
|
|
@ -1124,7 +1124,7 @@ player_transfer_distance (Player transfer distance) int 0
|
||||||
enable_pvp (Player versus player) bool true
|
enable_pvp (Player versus player) bool true
|
||||||
|
|
||||||
# Enable mod channels support.
|
# 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.
|
# If this is set, players will always (re)spawn at the given position.
|
||||||
static_spawnpoint (Static spawnpoint) string
|
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
|
# LOOKUP_NODES_LIMIT: 16 (limits get_node call client-side to
|
||||||
# csm_restriction_noderange)
|
# csm_restriction_noderange)
|
||||||
# READ_PLAYERINFO: 32 (disable get_player_names call client-side)
|
# 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
|
# If the CSM restriction for node range is enabled, get_node calls are limited
|
||||||
# to this distance from the player to the node.
|
# to this distance from the player to the node.
|
||||||
|
|
104
doc/lua_api.txt
104
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
|
Helper functions
|
||||||
================
|
================
|
||||||
|
|
|
@ -1351,7 +1351,7 @@
|
||||||
|
|
||||||
# Enable mod channels support.
|
# Enable mod channels support.
|
||||||
# type: bool
|
# type: bool
|
||||||
# enable_mod_channels = false
|
# enable_mod_channels = true
|
||||||
|
|
||||||
# If this is set, players will always (re)spawn at the given position.
|
# If this is set, players will always (re)spawn at the given position.
|
||||||
# type: string
|
# type: string
|
||||||
|
@ -1589,7 +1589,7 @@
|
||||||
# csm_restriction_noderange)
|
# csm_restriction_noderange)
|
||||||
# READ_PLAYERINFO: 32 (disable get_player_names call client-side)
|
# READ_PLAYERINFO: 32 (disable get_player_names call client-side)
|
||||||
# type: int
|
# type: int
|
||||||
# csm_restriction_flags = 62
|
# csm_restriction_flags = 60
|
||||||
|
|
||||||
# If the CSM restriction for node range is enabled, get_node calls are limited
|
# If the CSM restriction for node range is enabled, get_node calls are limited
|
||||||
# to this distance from the player to the node.
|
# to this distance from the player to the node.
|
||||||
|
@ -3374,4 +3374,3 @@
|
||||||
# so see a full list at https://content.minetest.net/help/content_flags/
|
# so see a full list at https://content.minetest.net/help/content_flags/
|
||||||
# type: string
|
# type: string
|
||||||
# contentdb_flag_blacklist = nonfree, desktop_default
|
# contentdb_flag_blacklist = nonfree, desktop_default
|
||||||
|
|
||||||
|
|
|
@ -360,7 +360,7 @@ void set_default_settings(Settings *settings)
|
||||||
settings->setDefault("default_password", "");
|
settings->setDefault("default_password", "");
|
||||||
settings->setDefault("default_privs", "interact, shout");
|
settings->setDefault("default_privs", "interact, shout");
|
||||||
settings->setDefault("enable_pvp", "true");
|
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("disallow_empty_password", "false");
|
||||||
settings->setDefault("disable_anticheat", "false");
|
settings->setDefault("disable_anticheat", "false");
|
||||||
settings->setDefault("enable_rollback_recording", "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("max_block_send_distance", "10");
|
||||||
settings->setDefault("block_send_optimize_distance", "4");
|
settings->setDefault("block_send_optimize_distance", "4");
|
||||||
settings->setDefault("server_side_occlusion_culling", "true");
|
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("csm_restriction_noderange", "0");
|
||||||
settings->setDefault("max_clearobjects_extra_loaded_blocks", "4096");
|
settings->setDefault("max_clearobjects_extra_loaded_blocks", "4096");
|
||||||
settings->setDefault("time_speed", "72");
|
settings->setDefault("time_speed", "72");
|
||||||
|
|
Loading…
Reference in New Issue