init.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. --------------------------------------------------------
  2. -- Minetest :: ActiveFormspecs Mod v2.6 (formspecs)
  3. --
  4. -- See README.txt for licensing and release notes.
  5. -- Copyright (c) 2016-2019, Leslie Ellen Krause
  6. --
  7. -- ./games/just_test_tribute/mods/formspecs/init.lua
  8. --------------------------------------------------------
  9. print( "Loading ActiveFormspecs Mod" )
  10. minetest.FORMSPEC_SIGEXIT = "true" -- player clicked exit button or pressed esc key (boolean for backward compatibility)
  11. minetest.FORMSPEC_SIGQUIT = 1 -- player logged off
  12. minetest.FORMSPEC_SIGKILL = 2 -- player was killed
  13. minetest.FORMSPEC_SIGTERM = 3 -- server is shutting down
  14. minetest.FORMSPEC_SIGPROC = 4 -- procedural closure
  15. minetest.FORMSPEC_SIGTIME = 5 -- timeout reached
  16. minetest.FORMSPEC_SIGSTOP = 6 -- procedural closure (cannot be trapped)
  17. minetest.FORMSPEC_SIGHOLD = 7 -- child form opened, parent is suspended
  18. minetest.FORMSPEC_SIGCONT = 8 -- child form closed, parent can continue
  19. local afs = { } -- obtain localized, protected namespace
  20. afs.forms = { }
  21. afs.timers = { }
  22. afs.session_id = 0
  23. afs.session_seed = math.random( 0, 65535 )
  24. afs.stats = { active = 0, opened = 0, closed = 0 }
  25. afs.stats.on_open = function ( self )
  26. self.active = self.active + 1
  27. self.opened = self.opened + 1
  28. end
  29. afs.stats.on_close = function ( self )
  30. self.active = self.active - 1
  31. self.closed = self.closed + 1
  32. end
  33. -----------------------------------------------------------------
  34. -- trigger callbacks at set intervals within timer queue
  35. -----------------------------------------------------------------
  36. do
  37. -- localize needed object references for efficiency
  38. local get_us_time = minetest.get_us_time
  39. local timers = afs.timers
  40. local t_cur = get_us_time( )
  41. local t_off = -t_cur
  42. -- step monotonic clock with graceful 32-bit overflow
  43. local step_clock = function( )
  44. local t_new = get_us_time( )
  45. if t_new < t_cur then
  46. t_off = t_off + 4294967290
  47. end
  48. t_cur = t_new
  49. return t_off + t_new
  50. end
  51. afs.get_uptime = function( )
  52. return ( t_off + t_cur ) / 1000000
  53. end
  54. minetest.register_globalstep( function( dtime )
  55. --local x = get_us_time( )
  56. local curtime = step_clock( ) / 1000000
  57. local idx = #timers
  58. -- iterate through table in reverse order to allow removal
  59. while idx > 0 do
  60. local self = timers[ idx ]
  61. if curtime >= self.exptime then
  62. self.counter = self.counter + 1
  63. self.overrun = curtime - self.exptime
  64. self.exptime = curtime + self.form.timeout
  65. self.form.newtime = math.floor( curtime )
  66. self.form.on_close( self.form.meta, self.form.player, { quit = minetest.FORMSPEC_SIGTIME } )
  67. self.overrun = 0.0
  68. end
  69. idx = idx - 1
  70. end
  71. end )
  72. end
  73. -----------------------------------------------------------------
  74. -- override node registrations for attached formspecs
  75. -----------------------------------------------------------------
  76. local on_rightclick = function( pos, node, player )
  77. local nodedef = minetest.registered_nodes[ node.name ]
  78. local meta = nodedef.before_open and nodedef.before_open( pos, node, player ) or pos
  79. local formspec = nodedef.on_open( meta, player )
  80. if formspec then
  81. local player_name = player:get_player_name( )
  82. minetest.create_form( meta, player_name, formspec, nodedef.on_close )
  83. afs.forms[ player_name ].origin = node.name
  84. end
  85. end
  86. local old_register_node = minetest.register_node
  87. local old_override_item = minetest.override_item
  88. minetest.register_node = function ( name, def )
  89. if def.on_open and not def.on_rightclick then
  90. def.on_rightclick = on_rightclick
  91. end
  92. old_register_node( name, def )
  93. end
  94. minetest.override_item = function ( name, def )
  95. if minetest.registered_nodes[ name ] and def.on_open then
  96. def.on_rightclick = on_rightclick
  97. end
  98. old_override_item( name, def )
  99. end
  100. -----------------------------------------------------------------
  101. -- trigger callbacks during formspec events
  102. -----------------------------------------------------------------
  103. minetest.register_on_player_receive_fields( function( player, formname, fields )
  104. local player_name = player:get_player_name( )
  105. local form = afs.forms[ player_name ]
  106. -- perform a basic sanity check, since these shouldn't technically occur
  107. if not form or player ~= form.player or formname ~= form.name then return end
  108. -- handle reverse-lookups of dropdown indexes
  109. for name, keys in pairs( form.dropdowns ) do
  110. if fields[ name ] then
  111. fields[ name ] = keys[ fields[ name ] ]
  112. end
  113. end
  114. form.newtime = os.time( )
  115. form.on_close( form.meta, form.player, fields )
  116. -- end current session when closing formspec
  117. if fields.quit then
  118. minetest.get_form_timer( player_name ).stop( )
  119. afs.stats:on_close( )
  120. if form.parent_form then
  121. -- restore previous session
  122. form = form.parent_form
  123. afs.forms[ player_name ] = form
  124. -- delay a single tick to ensure formspec updates are handled by client
  125. minetest.after( 0.0, function ( )
  126. form.on_close( form.meta, form.player, { quit = minetest.FORMSPEC_SIGCONT } )
  127. end )
  128. else
  129. afs.forms[ player_name ] = nil
  130. end
  131. end
  132. end )
  133. -----------------------------------------------------------------
  134. -- expose timer functionality within a helper object
  135. -----------------------------------------------------------------
  136. minetest.get_form_timer = function ( player_name, form_name )
  137. local self = { }
  138. local form = afs.forms[ player_name ]
  139. if not form or form_name and form_name ~= form.name then return end
  140. self.start = function ( timeout )
  141. if not form.timeout and timeout >= 0.5 then
  142. local curtime = afs.get_uptime( )
  143. form.timeout = timeout
  144. table.insert( afs.timers, { form = form, counter = 0, oldtime = curtime, exptime = curtime + timeout, overrun = 0.0 } )
  145. end
  146. end
  147. self.stop = function ( )
  148. if not form.timeout then return end
  149. form.timeout = nil
  150. for i, v in ipairs( afs.timers ) do
  151. if v.form == form then
  152. table.remove( afs.timers, i )
  153. return
  154. end
  155. end
  156. end
  157. self.get_state = function ( )
  158. if not form.timeout then return end
  159. for i, v in ipairs( afs.timers ) do
  160. local curtime = afs.get_uptime( )
  161. if v.form == form then
  162. return { elapsed = curtime - v.oldtime, remain = v.exptime - curtime, overrun = v.overrun, counter = v.counter }
  163. end
  164. end
  165. end
  166. return self
  167. end
  168. -----------------------------------------------------------------
  169. -- parse specialized formspec elements and escapes codes
  170. -----------------------------------------------------------------
  171. local _
  172. local function is_match( str, pat )
  173. -- use array for captures
  174. _ = { string.match( str, pat ) }
  175. return #_ > 0 and _ or nil
  176. end
  177. local function escape( str )
  178. return string.gsub( str, "\\.",
  179. { ["\\]"] = "\\x5D", ["\\["] = "\\x5B", ["\\,"] = "\\x2C", ["\\;"] = "\\x3B" } )
  180. end
  181. local function unescape( str, is_raw )
  182. return string.gsub( str, "\\x..",
  183. { ["\\x5D"] = "\\]", ["\\x5B"] = "\\[", ["\\x2C"] = "\\,", ["\\x3B"] = "\\;" } )
  184. end
  185. local function unescape_raw( str, is_raw )
  186. return string.gsub( str, "\\x..",
  187. { ["\\x5D"] = "]", ["\\x5B"] = "[", ["\\x2C"] = ",", ["\\x3B"] = ";" } )
  188. end
  189. local function parse_elements( form, formspec )
  190. formspec = escape( formspec )
  191. form.dropdowns = { } -- reset the dropdown lookup
  192. -- dropdown elements can optionally return the selected
  193. -- index rather than the value of the option itself
  194. formspec = string.gsub( formspec, "dropdown%[(.-)%]", function( params )
  195. if is_match( params, "^([^;]*;[^;]*;([^;]*);([^;]*);[^;]*);([^;]*)$" ) then
  196. local prefix = _[ 1 ]
  197. local name = _[ 2 ]
  198. local options = _[ 3 ]
  199. local use_index = _[ 4 ]
  200. if use_index == "true" then
  201. form.dropdowns[ name ] = { }
  202. for idx, val in ipairs( string.split( options, ",", true ) ) do
  203. form.dropdowns[ name ][ unescape_raw( val ) ] = idx -- add to reverse lookup table
  204. end
  205. return string.format( "dropdown[%s]", prefix )
  206. elseif use_index == "false" or use_index == "" then
  207. return string.format( "dropdown[%s]", prefix )
  208. else
  209. return "" -- strip invalid dropdown elements
  210. end
  211. end
  212. return string.format( "dropdown[%s]", params )
  213. end )
  214. -- hidden elements only provide default, initial values
  215. -- for state table and are always stripped afterward
  216. formspec = string.gsub( formspec, "hidden%[(.-)%]", function( params )
  217. if is_match( params, "^([^;]*);([^;]*)$" ) or is_match( params, "^([^;]*);([^;]*);([^;]*)$" ) then
  218. local key = _[ 1 ]
  219. local value = _[ 2 ]
  220. local type = _[ 3 ]
  221. if key ~= "" and form.meta[ key ] == nil then
  222. -- parse according to specified data type
  223. if type == "string" or type == "" or type == nil then
  224. form.meta[ key ] = unescape_raw( value )
  225. elseif type == "number" then
  226. form.meta[ key ] = tonumber( value )
  227. elseif type == "boolean" then
  228. form.meta[ key ] = ( { ["1"] = true, ["0"] = false, ["true"] = true, ["false"] = false } )[ value ]
  229. end
  230. end
  231. end
  232. return "" -- strip hidden elements prior to showing formspec
  233. end )
  234. return unescape( formspec )
  235. end
  236. -----------------------------------------------------------------
  237. -- open detached formspec with session-based state table
  238. -----------------------------------------------------------------
  239. minetest.create_form = function ( meta, player_name, formspec, on_close, signal )
  240. -- short circuit whenever required params are missing
  241. if not player_name or not formspec then return end
  242. if type( player_name ) ~= "string" then
  243. player_name = player_name:get_player_name( )
  244. end
  245. local form = afs.forms[ player_name ]
  246. -- trigger previous callback before formspec closure
  247. if form then
  248. minetest.get_form_timer( player_name, form.name ).stop( )
  249. if signal ~= minetest.FORMSPEC_SIGSTOP then
  250. form.on_close( form.meta, form.player, { quit = signal or minetest.FORMSPEC_SIGPROC } )
  251. end
  252. if signal ~= minetest.FORMSPEC_SIGHOLD then
  253. form = nil
  254. afs.stats:on_close( )
  255. end
  256. end
  257. -- start new session when opening formspec
  258. afs.session_id = afs.session_id + 1
  259. form = { parent_form = form }
  260. form.id = afs.session_id
  261. form.name = minetest.get_password_hash( player_name, afs.session_seed + afs.session_id )
  262. form.player = minetest.get_player_by_name( player_name )
  263. form.origin = string.match( debug.getinfo( 2 ).source, "^@.*[/\\]mods[/\\](.-)[/\\]" ) or "?"
  264. form.on_close = on_close or function ( ) end
  265. form.meta = meta or { }
  266. form.oldtime = math.floor( afs.get_uptime( ) )
  267. form.newtime = form.oldtime
  268. afs.forms[ player_name ] = form
  269. afs.stats:on_open( )
  270. minetest.show_formspec( player_name, form.name, parse_elements( form, formspec ) )
  271. return form.name
  272. end
  273. minetest.update_form = function ( player, formspec )
  274. local pname = type( player ) == "string" and player or player:get_player_name( )
  275. local form = afs.forms[ pname ]
  276. if form then
  277. form.oldtime = math.floor( afs.get_uptime( ) )
  278. minetest.show_formspec( pname, form.name, parse_elements( form, formspec ) )
  279. end
  280. end
  281. minetest.destroy_form = function ( player, signal )
  282. local pname = type( player ) == "string" and player or player:get_player_name( )
  283. local form = afs.forms[ pname ]
  284. if form then
  285. minetest.close_formspec( pname, form.name )
  286. minetest.get_form_timer( pname ):stop( )
  287. if signal ~= minetest.FORMSPEC_SIGSTOP then
  288. form.on_close( form.meta, form.player, { quit = signal or minetest.FORMSPEC_SIGPROC } )
  289. end
  290. afs.stats:on_close( )
  291. afs.forms[ pname ] = nil
  292. end
  293. end
  294. -----------------------------------------------------------------
  295. -- trigger callbacks after unexpected formspec closure
  296. -----------------------------------------------------------------
  297. minetest.register_on_leaveplayer( function( player, is_timeout )
  298. local pname = player:get_player_name( )
  299. local form = afs.forms[ pname ]
  300. if form then
  301. minetest.get_form_timer( pname, form.name ).stop( )
  302. form.newtime = os.time( )
  303. form.on_close( form.meta, form.player, { quit = minetest.FORMSPEC_SIGQUIT } )
  304. afs.stats:on_close( )
  305. afs.forms[ pname ] = nil
  306. end
  307. end )
  308. minetest.register_on_dieplayer( function( player )
  309. local pname = player:get_player_name( )
  310. local form = afs.forms[ pname ]
  311. if form then
  312. minetest.get_form_timer( pname, form.name ).stop( )
  313. form.newtime = os.time( )
  314. form.on_close( form.meta, form.player, { quit = minetest.FORMSPEC_SIGKILL } )
  315. afs.stats:on_close( )
  316. afs.forms[ pname ] = nil
  317. end
  318. end )
  319. minetest.register_on_shutdown( function( )
  320. for _, form in pairs( afs.forms ) do
  321. minetest.get_form_timer( form.player:get_player_name( ), form.name ).stop( )
  322. form.newtime = os.time( )
  323. form.on_close( form.meta, form.player, { quit = minetest.FORMSPEC_SIGTERM } )
  324. afs.stats:on_close( )
  325. end
  326. afs.forms = { }
  327. end )
  328. -----------------------------------------------------------------
  329. -- display realtime information about form sessions
  330. -----------------------------------------------------------------
  331. minetest.register_chatcommand( "fs", {
  332. description = "Display realtime information about form sessions",
  333. privs = { server = true },
  334. func = function( pname, param )
  335. local page_idx = 1
  336. local page_size = 10
  337. local sorted_forms
  338. local get_sorted_forms = function( )
  339. local f = { }
  340. for k, v in pairs( afs.forms ) do
  341. table.insert( f, v )
  342. end
  343. table.sort( f, function( a, b ) return a.id < b.id end )
  344. return f
  345. end
  346. local get_formspec = function( )
  347. local uptime = math.floor( afs.get_uptime( ) )
  348. local formspec = "size[9.5,7.5]"
  349. .. default.gui_bg
  350. .. default.gui_bg_img
  351. .. "label[0.1,6.7;ActiveFormspecs v2.6"
  352. .. string.format( "label[0.1,0.0;%s]label[0.1,0.5;%d min %02d sec]",
  353. minetest.colorize( "#888888", "uptime:" ), math.floor( uptime / 60 ), uptime % 60 )
  354. .. string.format( "label[5.6,0.0;%s]label[5.6,0.5;%d]",
  355. minetest.colorize( "#888888", "active" ), afs.stats.active )
  356. .. string.format( "label[6.9,0.0;%s]label[6.9,0.5;%d]",
  357. minetest.colorize( "#888888", "opened" ), afs.stats.opened )
  358. .. string.format( "label[8.2,0.0;%s]label[8.2,0.5;%d]",
  359. minetest.colorize( "#888888", "closed" ), afs.stats.closed )
  360. .. string.format( "label[0.5,1.5;%s]label[3.5,1.5;%s]label[6.9,1.5;%s]label[8.2,1.5;%s]",
  361. minetest.colorize( "#888888", "player" ),
  362. minetest.colorize( "#888888", "origin" ),
  363. minetest.colorize( "#888888", "idletime" ),
  364. minetest.colorize( "#888888", "lifetime" )
  365. )
  366. .. "box[0,1.2;9.2,0.1;#111111]"
  367. .. "box[0,6.2;9.2,0.1;#111111]"
  368. local num = 0
  369. for idx = ( page_idx - 1 ) * page_size + 1, math.min( page_idx * page_size, #sorted_forms ) do
  370. local form = sorted_forms[ idx ]
  371. local player_name = form.player:get_player_name( )
  372. local lifetime = uptime - form.oldtime
  373. local idletime = uptime - form.newtime
  374. local vert = 2.0 + num * 0.5
  375. formspec = formspec
  376. .. string.format( "button[0.1,%0.1f;0.5,0.3;del:%s;x]", vert + 0.1, player_name )
  377. .. string.format( "label[0.5,%0.1f;%s]", vert, player_name )
  378. .. string.format( "label[3.5,%0.1f;%s]", vert, form.origin )
  379. .. string.format( "label[6.9,%0.1f;%dm %02ds]", vert, math.floor( idletime / 60 ), idletime % 60 )
  380. .. string.format( "label[8.2,%0.1f;%dm %02ds]", vert, math.floor( lifetime / 60 ), lifetime % 60 )
  381. num = num + 1
  382. end
  383. formspec = formspec
  384. .. "button[6.4,6.5;1,1;prev;<<]"
  385. .. string.format( "label[7.4,6.7;%d of %d]", page_idx, math.max( 1, math.ceil( #sorted_forms / page_size ) ) )
  386. .. "button[8.4,6.5;1,1;next;>>]"
  387. return formspec
  388. end
  389. local on_close = function( meta, player, fields )
  390. if fields.quit == minetest.FORMSPEC_SIGTIME then
  391. sorted_forms = get_sorted_forms( )
  392. minetest.update_form( pname, get_formspec( ) )
  393. elseif fields.prev and page_idx > 1 then
  394. page_idx = page_idx - 1
  395. minetest.update_form( pname, get_formspec( ) )
  396. elseif fields.next and page_idx < #sorted_forms / page_size then
  397. page_idx = page_idx + 1
  398. minetest.update_form( pname, get_formspec( ) )
  399. else
  400. local player_name = string.match( next( fields, nil ), "del:(.+)" )
  401. if player_name and afs.forms[ player_name ] then
  402. minetest.destroy_form( player_name )
  403. end
  404. end
  405. end
  406. sorted_forms = get_sorted_forms( )
  407. minetest.create_form( nil, pname, get_formspec( ), on_close )
  408. minetest.get_form_timer( pname ).start( 1 )
  409. return true
  410. end,
  411. } )