-- -- Animated formspecs -- -- Copyright © 2019 by luk3yx -- formspec_anim = {} sscsm.register({ name = 'formspec_anim', file = minetest.get_modpath('formspec_anim') .. '/sscsm.lua', }) local get_player_by_name if minetest.global_exists('cloaking') then get_player_by_name = cloaking.get_player_by_name else get_player_by_name = minetest.get_player_by_name end local clients = {} local open = {} minetest.register_on_leaveplayer(function(player) local name = player:get_player_name() clients[name] = nil open[name] = nil end) -- Convert animated_image[] to a special image[] that can then be found and -- replaced inside the SSCSM using a pattern. local function parse_texture(raw, parse) local texture, count, frame_duration = parse.string(raw[3]):match('^(.*):([0-9]+),([0-9]+)$') count, frame_duration = tonumber(count), tonumber(frame_duration) assert(texture and count and frame_duration and not texture:find('/', nil, true), 'Expected :,') count = math.floor(count) assert(count > 0 and frame_duration > 0, 'Frame count and duration must be above 0!') assert(not texture:find('\001', nil, true), 'animated_image[] textures cannot contain U+0001!') local texture_modifier = '' local s, e = texture:find('%s', nil, true) if s and e and not texture:find('%s', e + 1, true) then -- The string contains a single %s, use this as a replacement character. texture_modifier = texture:sub(e + 1) texture = texture:sub(1, s - 1) else -- Otherwise just append _%s.png to the base texture local s, e = texture:find('^', nil, true) if s then texture_modifier = texture:sub(s) texture = texture:sub(1, s - 1) end texture = texture .. '_' texture_modifier = '.png' .. texture_modifier end return texture, texture_modifier, count, frame_duration / 1000, parse.boolean(raw[4] or '') end local custom_handlers = {} function custom_handlers.animated_image(raw, parse) local texture, texture_modifier, count, duration, one_shot = parse_texture(raw, parse) return { type = 'image', x = parse.number(raw[1][1]), y = parse.number(raw[1][2]), w = parse.number(raw[2][1]), h = parse.number(raw[2][2]), texture_name = '[\001formspec_anim\001:' .. count .. (one_shot and '.' or '+') .. ',' .. duration .. ',' .. texture .. '\001' .. texture_modifier, } end custom_handlers['formspec_anim:animated_image'] = custom_handlers.animated_image -- If the client doesn't support SSCSMs, show a fallback. -- Looping animations will have their first frame displayed and one-shot -- animations will show their last. local fallback_handlers = {} function fallback_handlers.animated_image(raw, parse) local texture, texture_modifier, count, duration, one_shot = parse_texture(raw, parse) return { type = 'image', x = parse.number(raw[1][1]), y = parse.number(raw[1][2]), w = parse.number(raw[2][1]), h = parse.number(raw[2][2]), texture_name = texture .. (one_shot and count or 1) .. texture_modifier, } end fallback_handlers['formspec_anim:animated_image'] = fallback_handlers.animated_image -- Show formspecs -- ^AFORMSPEC_ANIM^A^A local sscsm_msg_prefix = '\001FORMSPEC_ANIM\001' function formspec_anim.show_formspec(pname, formname, formspec) if minetest.is_player(pname) then pname = pname:get_player_name() elseif not get_player_by_name(pname) then return false, 'Unknown player!' elseif formname:find('\001', nil, true) then return false, 'The formname cannot contain \\x01!' end local handlers = clients[pname] and custom_handlers or fallback_handlers local formspec, err = formspec_ast.interpret(formspec, handlers) if not formspec then return false, err end if clients[pname] then open[pname] = formname minetest.chat_send_player(pname, sscsm_msg_prefix .. formname .. '\001' .. formspec) else minetest.show_formspec(pname, formname, formspec) end return true end -- Override minetest.show_formspec so that the SSCSM has time to stop the -- animation. local show_formspec = minetest.show_formspec function minetest.show_formspec(pname, ...) if open[pname] then open[pname] = nil minetest.chat_send_player(pname, sscsm_msg_prefix) minetest.after(0, show_formspec, pname, ...) else return show_formspec(pname, ...) end end -- Handle formspec input minetest.register_chatcommand('formspec_anim', { description = 'Displays an animation if your client supports it.', func = function(name, param) -- Just show a formspec if the SSCSM didn't send this command. if param:sub(1, 1) ~= '\001' then local err, msg = formspec_anim.show_formspec(name, 'formspec_anim:test', -- 'size[8,9]animated_image[0,0;5,5;formspec_anim_water:16,50]' -- .. 'field[5,0;3,1;name;label;Default text]' -- .. 'button[5,1;3,1;button1;Button 1]' -- .. 'button[5,2;3,1;quit;Stop animation]' -- .. 'list[current_player;main;0,5;8,4;]') 'formspec_version[2]size[10,4]' -- .. 'animated_image[0.25,3;9.5,0.6;busy^no_screenshot.png:72,25;' .. -- tostring(param == 'oneshot') .. ']' .. 'animated_image[4.25,2;1.5,1.5;gui_furnace_arrow_bg.png' .. '^[lowpart:%s:gui_furnace_arrow_fg.png' .. '^[transformR270:100,10;' .. tostring(param == 'oneshot') .. ']' .. 'formspec_ast:centered_label[0,1;10,1;Loading...]') return err, err and 'Opened formspec!' or tostring(msg) end local player = get_player_by_name(name) if not player then return end param = param:sub(2) if param == 'OK' then clients[name] = true return end -- Otherwise handle formspec input. local s, e = param:find('\001', nil, true) if not s or not e then return false, '[formspec_anim] Invalid data sent!' end local formname = param:sub(1, s - 1) if open[name] ~= formname then return end local fields = minetest.parse_json(param:sub(e + 1)) if type(fields) ~= 'table' then return end -- Ensure all fields are strings. for k, v in pairs(fields) do if type(k) ~= 'string' or type(v) ~= 'string' then fields[k] = nil end end -- "Close" the formspec on fields.quit if fields.quit then open[name] = nil end -- Call all register_on_player_receive_fields functions. local funcs = minetest.registered_on_player_receive_fields for i = #funcs, 1, -1 do if funcs[i](player, formname, fields) then return end end end, })