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