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.

init.lua 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. --
  2. -- SSCSM: Server-Sent Client-Side Mods proof-of-concept
  3. --
  4. -- © 2019 by luk3yx
  5. --
  6. local sscsm = {minify=true}
  7. local modname = minetest.get_current_modname()
  8. _G[modname] = sscsm
  9. local modpath = minetest.get_modpath(modname)
  10. -- Remove excess whitespace from code to allow larger files to be sent.
  11. if sscsm.minify then
  12. local f = loadfile(modpath .. '/minify.lua')
  13. if f then
  14. sscsm.minify_code = f()
  15. else
  16. minetest.log('warning', '[SSCSM] Could not load minify.lua!')
  17. end
  18. end
  19. if not sscsm.minify_code then
  20. function sscsm.minify_code(code)
  21. assert(type(code) == 'string')
  22. return code
  23. end
  24. end
  25. -- Register code
  26. sscsm.registered_csms = {}
  27. local csm_order = false
  28. -- Recalculate the CSM loading order
  29. -- TODO: Make this nicer
  30. local function recalc_csm_order()
  31. local loaded = {}
  32. local staging = {}
  33. local order = {':init'}
  34. local unsatisfied = {}
  35. for name, def in pairs(sscsm.registered_csms) do
  36. assert(name == def.name)
  37. if name:sub(1, 1) == ':' then
  38. loaded[name] = true
  39. elseif not def.depends or #def.depends == 0 then
  40. loaded[name] = true
  41. table.insert(staging, name)
  42. else
  43. unsatisfied[name] = {}
  44. for _, mod in ipairs(def.depends) do
  45. if mod:sub(1, 1) ~= ':' then
  46. unsatisfied[name][mod] = true
  47. end
  48. end
  49. end
  50. end
  51. while #staging > 0 do
  52. local name = staging[1]
  53. for name2, u in pairs(unsatisfied) do
  54. if u[name] then
  55. u[name] = nil
  56. if #u == 0 then
  57. table.insert(staging, name2)
  58. end
  59. end
  60. end
  61. table.insert(order, name)
  62. table.remove(staging, 1)
  63. end
  64. for name, u in pairs(unsatisfied) do
  65. if next(u) then
  66. local msg = 'SSCSM "' .. name .. '" has unsatisfied dependencies: '
  67. local n = false
  68. for dep, _ in pairs(u) do
  69. if n then msg = msg .. ', ' else n = true end
  70. msg = msg .. '"' .. dep .. '"'
  71. end
  72. minetest.log('error', msg)
  73. end
  74. end
  75. -- Set csm_order
  76. table.insert(order, ':cleanup')
  77. csm_order = order
  78. end
  79. -- Register SSCSMs
  80. -- TODO: Automatically minify code (remove whitespace+comments)
  81. local block_colon = false
  82. sscsm.registered_csms = {}
  83. function sscsm.register(def)
  84. -- Read files now in case MT decides to block access later.
  85. if not def.code and def.file then
  86. local f = io.open(def.file, 'rb')
  87. if not f then
  88. error('Invalid "file" parameter passed to sscsm.register_csm.', 2)
  89. end
  90. def.code = f:read('*a')
  91. f:close()
  92. def.file = nil
  93. end
  94. if type(def.name) ~= 'string' or def.name:find('\n')
  95. or (def.name:sub(1, 1) == ':' and block_colon) then
  96. error('Invalid "name" parameter passed to sscsm.register_csm.', 2)
  97. end
  98. if type(def.code) ~= 'string' then
  99. error('Invalid "code" parameter passed to sscsm.register_csm.', 2)
  100. end
  101. def.code = sscsm.minify_code(def.code)
  102. if (#def.name + #def.code) > 65300 then
  103. error('The code (or name) passed to sscsm.register_csm is too large.'
  104. .. ' Consider refactoring your SSCSM code.', 2)
  105. end
  106. -- Copy the table to prevent mods from betraying our trust.
  107. sscsm.registered_csms[def.name] = table.copy(def)
  108. if csm_order then recalc_csm_order() end
  109. end
  110. function sscsm.unregister(name)
  111. sscsm.registered_csms[name] = nil
  112. if csm_order then recalc_csm_order() end
  113. end
  114. -- Recalculate the CSM order once all other mods are loaded
  115. minetest.register_on_mods_loaded(recalc_csm_order)
  116. -- Handle players joining
  117. local mod_channel = minetest.mod_channel_join('sscsm:exec_pipe')
  118. minetest.register_on_modchannel_message(function(channel_name, sender, message)
  119. if channel_name ~= 'sscsm:exec_pipe' or not sender or
  120. not mod_channel:is_writeable() or message ~= '0' or
  121. sender:find('\n') then
  122. return
  123. end
  124. minetest.log('action', '[SSCSM] Sending CSMs on request for ' .. sender
  125. .. '...')
  126. for _, name in ipairs(csm_order) do
  127. mod_channel:send_all('0' .. sender .. '\n' .. name
  128. .. '\n' .. sscsm.registered_csms[name].code)
  129. end
  130. end)
  131. -- Register the SSCSM "builtins"
  132. sscsm.register({
  133. name = ':init',
  134. file = modpath .. '/sscsm_init.lua'
  135. })
  136. sscsm.register({
  137. name = ':cleanup',
  138. code = 'sscsm._done_loading_()'
  139. })
  140. block_colon = true
  141. -- Testing
  142. minetest.after(1, function()
  143. local c = 0
  144. for k, v in pairs(sscsm.registered_csms) do
  145. c = c + 1
  146. if c > 2 then break end
  147. end
  148. if c == 2 then
  149. minetest.log('warning', '[SSCSM] Testing mode enabled.')
  150. sscsm.register({
  151. name = 'sscsm:testing_cmds',
  152. file = modpath .. '/sscsm_testing.lua',
  153. })
  154. sscsm.register({
  155. name = 'sscsm:another_test',
  156. code = 'yay()',
  157. depends = {'sscsm:testing_cmds'},
  158. })
  159. sscsm.register({
  160. name = 'sscsm:badtest',
  161. code = 'error("Oops, badtest loaded!")',
  162. depends = {':init', ':cleanup', 'bad_mod', ':bad2', 'bad3'},
  163. })
  164. end
  165. end)