Proof-of-concept Server-Sent CSMs
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

304 lines
8.3KB

  1. --
  2. -- SSCSM: Server-Sent Client-Side Mods proof-of-concept
  3. --
  4. -- Copyright © 2019 by luk3yx
  5. --
  6. local modname = minetest.get_current_modname()
  7. -- If this is running as a CSM (improper installation), load the CSM code.
  8. if INIT == 'client' then
  9. local modpath
  10. if minetest.get_modpath then
  11. modpath = minetest.get_modpath(modname)
  12. else
  13. modpath = modname .. ':'
  14. end
  15. dofile(modpath .. 'csm/init.lua')
  16. return
  17. end
  18. local sscsm = {minify=true}
  19. _G[modname] = sscsm
  20. local modpath = minetest.get_modpath(modname)
  21. -- Remove excess whitespace from code to allow larger files to be sent.
  22. if sscsm.minify then
  23. local f = loadfile(modpath .. '/minify.lua')
  24. if f then
  25. sscsm.minify_code = f()
  26. else
  27. minetest.log('warning', '[SSCSM] Could not load minify.lua!')
  28. end
  29. end
  30. if not sscsm.minify_code then
  31. function sscsm.minify_code(code)
  32. assert(type(code) == 'string')
  33. return code
  34. end
  35. end
  36. -- Register code
  37. sscsm.registered_csms = {}
  38. local csm_order = false
  39. -- Recalculate the CSM loading order
  40. -- TODO: Make this nicer
  41. local function recalc_csm_order()
  42. local loaded = {}
  43. local staging = {}
  44. local order = {':init'}
  45. local unsatisfied = {}
  46. for name, def in pairs(sscsm.registered_csms) do
  47. assert(name == def.name)
  48. if name:sub(1, 1) == ':' then
  49. loaded[name] = true
  50. elseif not def.depends or #def.depends == 0 then
  51. loaded[name] = true
  52. table.insert(staging, name)
  53. else
  54. unsatisfied[name] = {}
  55. for _, mod in ipairs(def.depends) do
  56. if mod:sub(1, 1) ~= ':' then
  57. unsatisfied[name][mod] = true
  58. end
  59. end
  60. end
  61. end
  62. while #staging > 0 do
  63. local name = staging[1]
  64. for name2, u in pairs(unsatisfied) do
  65. if u[name] then
  66. u[name] = nil
  67. if #u == 0 then
  68. table.insert(staging, name2)
  69. end
  70. end
  71. end
  72. table.insert(order, name)
  73. table.remove(staging, 1)
  74. end
  75. for name, u in pairs(unsatisfied) do
  76. if next(u) then
  77. local msg = 'SSCSM "' .. name .. '" has unsatisfied dependencies: '
  78. local n = false
  79. for dep, _ in pairs(u) do
  80. if n then msg = msg .. ', ' else n = true end
  81. msg = msg .. '"' .. dep .. '"'
  82. end
  83. minetest.log('error', msg)
  84. end
  85. end
  86. -- Set csm_order
  87. table.insert(order, ':cleanup')
  88. csm_order = order
  89. end
  90. -- Register SSCSMs
  91. local block_colon = false
  92. sscsm.registered_csms = {}
  93. function sscsm.register(def)
  94. -- Read files now in case MT decides to block access later.
  95. if not def.code and def.file then
  96. local f = io.open(def.file, 'rb')
  97. if not f then
  98. error('Invalid "file" parameter passed to sscsm.register_csm.', 2)
  99. end
  100. def.code = f:read('*a')
  101. f:close()
  102. def.file = nil
  103. end
  104. if type(def.name) ~= 'string' or def.name:find('\n')
  105. or (def.name:sub(1, 1) == ':' and block_colon) then
  106. error('Invalid "name" parameter passed to sscsm.register_csm.', 2)
  107. end
  108. if type(def.code) ~= 'string' then
  109. error('Invalid "code" parameter passed to sscsm.register_csm.', 2)
  110. end
  111. def.code = sscsm.minify_code(def.code)
  112. if (#def.name + #def.code) > 65300 then
  113. error('The code (or name) passed to sscsm.register_csm is too large.'
  114. .. ' Consider refactoring your SSCSM code.', 2)
  115. end
  116. -- Copy the table to prevent mods from betraying our trust.
  117. sscsm.registered_csms[def.name] = table.copy(def)
  118. if csm_order then recalc_csm_order() end
  119. end
  120. function sscsm.unregister(name)
  121. sscsm.registered_csms[name] = nil
  122. if csm_order then recalc_csm_order() end
  123. end
  124. -- Recalculate the CSM order once all other mods are loaded
  125. minetest.register_on_mods_loaded(recalc_csm_order)
  126. -- Handle players joining
  127. local has_sscsms = {}
  128. local mod_channel = minetest.mod_channel_join('sscsm:exec_pipe')
  129. minetest.register_on_modchannel_message(function(channel_name, sender, message)
  130. if channel_name ~= 'sscsm:exec_pipe' or not sender or
  131. not mod_channel:is_writeable() or message ~= '0' or
  132. sender:find('\n') or has_sscsms[sender] then
  133. return
  134. end
  135. minetest.log('action', '[SSCSM] Sending CSMs on request for ' .. sender
  136. .. '...')
  137. for _, name in ipairs(csm_order) do
  138. mod_channel:send_all('0' .. sender .. '\n' .. name
  139. .. '\n' .. sscsm.registered_csms[name].code)
  140. end
  141. end)
  142. -- Register the SSCSM "builtins"
  143. sscsm.register({
  144. name = ':init',
  145. file = modpath .. '/sscsm_init.lua'
  146. })
  147. sscsm.register({
  148. name = ':cleanup',
  149. code = 'sscsm._done_loading_()'
  150. })
  151. block_colon = true
  152. -- Set the CSM restriction flags
  153. local flags = tonumber(minetest.settings:get('csm_restriction_flags'))
  154. if not flags or flags ~= flags then
  155. flags = 62
  156. end
  157. flags = math.floor(math.max(math.min(flags, 63), 0))
  158. do
  159. local def = sscsm.registered_csms[':init']
  160. def.code = def.code:gsub('__FLAGS__', tostring(flags))
  161. end
  162. if math.floor(flags / 2) % 2 == 1 then
  163. minetest.log('warning', '[SSCSM] SSCSMs enabled, however CSMs cannot '
  164. .. 'send chat messages! This will prevent SSCSMs from sending '
  165. .. 'messages to the server.')
  166. sscsm.com_write_only = true
  167. else
  168. sscsm.com_write_only = false
  169. end
  170. -- SSCSM communication
  171. local function validate_channel(channel)
  172. if type(channel) ~= 'string' then
  173. error('SSCSM com channels must be strings!', 3)
  174. end
  175. if channel:find('\001', nil, true) then
  176. error('SSCSM com channels cannot contain U+0001!', 3)
  177. end
  178. end
  179. function sscsm.com_send(pname, channel, msg)
  180. if minetest.is_player(pname) then
  181. pname = pname:get_player_name()
  182. end
  183. validate_channel(channel)
  184. if type(msg) == 'string' then
  185. msg = '\002' .. msg
  186. else
  187. msg = assert(minetest.write_json(msg))
  188. end
  189. minetest.chat_send_player(pname, '\001SSCSM_COM\001' .. channel .. '\001'
  190. .. msg)
  191. end
  192. local registered_on_receive = {}
  193. function sscsm.register_on_com_receive(channel, func)
  194. if not registered_on_receive[channel] then
  195. registered_on_receive[channel] = {}
  196. end
  197. table.insert(registered_on_receive[channel], func)
  198. end
  199. local admin_func = minetest.registered_chatcommands['admin'].func
  200. minetest.override_chatcommand('admin', {
  201. func = function(name, param)
  202. local chan, msg = param:match('^\001SSCSM_COM\001([^\001]*)\001(.*)$')
  203. if not chan or not msg then
  204. return admin_func(name, param)
  205. end
  206. -- Get the callbacks
  207. local callbacks = registered_on_receive[chan]
  208. if not callbacks then return end
  209. -- Load the message
  210. if msg:sub(1, 1) == '\002' then
  211. msg = msg:sub(2)
  212. else
  213. msg = minetest.parse_json(msg)
  214. end
  215. -- Run callbacks
  216. for _, func in ipairs(callbacks) do
  217. func(name, msg)
  218. end
  219. end,
  220. })
  221. -- Add a callback for sscsm:com_test
  222. sscsm.register_on_com_receive('sscsm:com_test', function(name, msg)
  223. if type(msg) == 'table' and msg.flags == flags then
  224. has_sscsms[name] = true
  225. end
  226. end)
  227. function sscsm.has_sscsms_enabled(name)
  228. return has_sscsms[name] or false
  229. end
  230. minetest.register_on_leaveplayer(function(player)
  231. has_sscsms[player:get_player_name()] = nil
  232. end)
  233. function sscsm.com_send_all(channel, msg)
  234. for name, _ in pairs(has_sscsms) do
  235. sscsm.com_send(name, channel, msg)
  236. end
  237. end
  238. -- Testing
  239. minetest.after(1, function()
  240. -- Check if any other SSCSMs have been registered.
  241. local c = 0
  242. for k, v in pairs(sscsm.registered_csms) do
  243. c = c + 1
  244. if c > 2 then break end
  245. end
  246. if c ~= 2 then return end
  247. -- If not, enter testing mode.
  248. minetest.log('warning', '[SSCSM] Testing mode enabled.')
  249. sscsm.register({
  250. name = 'sscsm:testing_cmds',
  251. file = modpath .. '/sscsm_testing.lua',
  252. })
  253. sscsm.register({
  254. name = 'sscsm:another_test',
  255. code = 'yay()',
  256. depends = {'sscsm:testing_cmds'},
  257. })
  258. sscsm.register({
  259. name = 'sscsm:badtest',
  260. code = 'error("Oops, badtest loaded!")',
  261. depends = {':init', ':cleanup', 'bad_mod', ':bad2', 'bad3'},
  262. })
  263. end)