[Mod] ActiveFormspecs [formspecs]

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: [Mod] ActiveFormspecs [formspecs]

by sorcerykid » Post

Code Guide: Using Upvalues in Place of a State-Table

Although using a temporary table to track form state may prove helpful in some situations, it's not always necessary. Consider these two slightly different approaches:
  • Method 1: Using a temporary table to track form state

    Code: Select all

    minetest.register_chatcommand( "score", {
            func = function ( name, param )
                    local formspec = "size[4,4]" ..
                            "label[0.2,1.0;How lucky do you feel today?]" ..
                            "button[1,2;2,1;raise_score;Raise Score!]"
    
                    minetest.create_form( { score = 0, count = 0 }, name, formspec, function ( meta, player, fields )
                            if fields.raise_score then
                                    meta.count = meta.count + 1
    
                                    meta.score = meta.score + math.random( -5, 10 )
                                    if meta.score < 0 or meta.count > 25 then
                                            minetest.chat_send_player( name,
                                                    "Sorry buddy, you didn't win this time!" )
                                            minetest.destroy_form( name )
                                    elseif meta.score > 100 then
                                            minetest.chat_send_player( name,
                                                    "CONGRATULATIONS! You just won a pot of virtual gold!" )
                                            minetest.destroy_form( name )
                                    elseif meta.score > 50 then
                                            minetest.chat_send_player( name,
                                                    "You're a real winner. Keep going!" )
                                    elseif meta.score > 10 then
                                            minetest.chat_send_player( name,
                                                    "Now you're on a roll. Do it again!" )
                                    else
                                            minetest.chat_send_player( name,
                                                    "Don't give up. Try another round!" )
                                    end
    
                            elseif fields.quit == minetest.FORMSPEC_SIGEXIT then
                                    minetest.chat_send_player( name,
                                            "You scored " .. meta.score .. " after " .. meta.count .. "tries!" )
                            end
                    end )
            end
    } )
    
    Method 2: Using upvalues to track form state

    Code: Select all

    minetest.register_chatcommand( "score", {
            func = function ( name, param )
                    local score = 0
                    local count = 0
                    local formspec = "size[4,4]" ..
                            "label[0.2,1.0;How lucky do you feel today?]" ..
                            "button[1,2;2,1;raise_score;Raise Score!]"
    
                    minetest.create_form( nil, name, formspec, function ( meta, player, fields )
                            if fields.raise_score then
                                    count = count + 1
    
                                    score = score + math.random( -5, 10 )
                                    if score < 0 or count > 25 then
                                            minetest.chat_send_player( name,
                                                    "Sorry buddy, you didn't win this time!" )
                                            minetest.destroy_form( name )
                                    elseif score > 100 then
                                            minetest.chat_send_player( name,
                                                    "CONGRATULATIONS! You just won a pot of virtual gold!" )
                                            minetest.destroy_form( name )
                                    elseif score > 50 then
                                            minetest.chat_send_player( name,
                                                    "You're a real winner. Keep going!" )
                                    elseif score > 10 then
                                            minetest.chat_send_player( name,
                                                    "Now you're on a roll. Do it again!" )
                                    else
                                            minetest.chat_send_player( name,
                                                    "Don't give up. Try another round!" )
                                    end
    
                            elseif fields.quit == minetest.FORMSPEC_SIGEXIT then
                                    minetest.chat_send_player( name,
                                            "You scored " .. score .. " after " .. count .. "tries!" )
                            end
                    end )
            end
    } )
    
Notice that because the the callback function is nested within the same block as minetest.create_form(), it has access to the local variables of the parent scope. Lua handles all the housekeeping for you. In the end, your code is much cleaner and easier to follow :)

Therefore, whenever possible, you should take advantage of upvalues. And indeed this is yet another benefit of form functions, as described in the previous Code Guide. Good luck and keep having fun with formspecs!

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: [Mod] ActiveFormspecs [formspecs]

by sorcerykid » Post

Version 2.5 Released

It's been exactly two years and far too long since the last version of ActiveFormspecs! Part of the reason for such a long release cycle is that there were no known problems, so there wasn't much incentive for updates. Like the old adage goes "if it ain't broke, don't fix it." Nonetheless, version 2.5 is well overdue. It incorporates minor tweaks to the API and has been in use on JT2 since last February.
  • Made callback function optional with default no-op
  • Added non-trappable form session termination signal
  • Properly reset timestamp for lifetime calculation
First and foremost, the on_close parameter to minetest.create_form() is no longer required. This is particularly helpful for the non-interactive forms, where the callback function would just be a no-op anyway.

Secondly, minetest.update_form() now resets the form session timestamp, so the in-game session monitor will calculate the correct lifetime. In this way "lifetime" now measures the length of time since the server sent a form to the client, and "idletime" measures the length of time since the server received a form-related event or signal, thus triggering a callback.

Lastly, I've added support for a non-trappable SIGSTOP signal. This signal can be passed to minetest.destroy_form( ) or minetest.create_form( ) to forcibly kill the current form session, thereby avoiding recursion in callbacks during a procedural form closure. It also permits seamless chaining of form-sessions.

Code: Select all

local function open_alertbox( player_name, message )
	local function get_formspec( )
		local formspec = <generate a basic formspec string here>

		return formspec
	end

	minetest.create_form( nil, player_name, get_formspec, nil, minetest.FORMSPEC_SIGSTOP )
end
Previously, I have warned against ever calling minetest.create_form() within the on_close callback. But that's not entirely true so long as there are appropriate sanity checks in place for the SIGPROC signal. However, in special circumstances when form closure may produce unanticipated side-effects, then the SIGSTOP signal can prove essential to avoid or even defer such actions. The alert box above is a prime example of such a use-case, where the intention is to immediately interrupt an active form session.

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: [Mod] ActiveFormspecs [formspecs]

by sorcerykid » Post

Version 2.7 is now underway!

Here's the current alpha branch: https://github.com/sorcerykid/formspecs/tree/2.7-alpha

I've overhauled the form timer queue, by removing the extraneous calculations to compensate for 32-bit overflow. Since the builtin monotonic clock was improved back in 2017, I decided to finally ditch compatibility with Minetest 0.4.14 going forward. While the original queue did not introduce much overhead, this still has the advantage that the calculations are vastly simpler now. So from a performance and maintenance standpoint, this is a plus.

The other change that I've made is to integrate all of the formspec background properties from Minetest Game into ActiveFormspecs. This means that mods will no longer have to depend on the Default mod to display forms with a standard-looking background. Just change the following variable and function references in your formspec strings:

Code: Select all

old value                      new value
-----------------------------------------------------------
default.gui_bg              -> minetest.gui_bg
default.gui_bg_img          -> minetest.gui_bg_img
default.gui_slots           -> minetest.gui_slots
default.get_hotbar_bg(0,5)  -> minetest.get_hotbar_bg(0,5)
And last but not least, I resolved a couple longstanding bugs with internal uptime and idletime calculations, which should have had a fairly negligible impact, but needed to be corrected nonetheless.

User avatar
runs
Member
Posts: 3225
Joined: Sat Oct 27, 2018 08:32

Re: [Mod] ActiveFormspecs [formspecs]

by runs » Post


User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: [Mod] ActiveFormspecs [formspecs]

by sorcerykid » Post

That's not currently possible due to limitations of engine. However, I recently published a proof-of-concept for dynamically updating sections of forms without having to reconstruct the entire formspec string. It very experimental, but it essentially emulates the API that I proposed in Issue 9364 on GitHub, but with a few alterations.

Simply wrap elements within new group[<name>] and group_end[] tags and then call minetest.update_form() with a table of key-value pairs for the specific groups to be updated. I included a sample file called "groups.lua" that helps to illustrate the methodology.

https://github.com/sorcerykid/formspecs/tree/2.8-alpha

Image

User avatar
runs
Member
Posts: 3225
Joined: Sat Oct 27, 2018 08:32

Re: [Mod] ActiveFormspecs [formspecs]

by runs » Post

I will try to make my Dialogue Mod as Lucas Games.

Image

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: [Mod] ActiveFormspecs [formspecs]

by sorcerykid » Post

Version 2.6 Released

A new version of ActiveFormspecs is ready for download. Here is a complete changelog:
  • Added callback to preset state of node formspecs
  • Removed experimental property from node definition
  • Implemented reverse-lookups for dropdown fields
  • Extended dropdown element with optional parameter
  • Added signal to suspend and restore form sessions
  • Combined element parsers into dedicated function
  • Added functions for escaping formspec strings
  • Revamped element parsers to ignore malformed tags
  • Added conditional pattern matching helper function
  • Compatibility bumped to Minetest 0.4.15+
Previously there was no "clean" way to to preset the state table of node formspecs. So I've introduced a before_open() callback (which first appeared in the 3.0 branch of ActiveFormspecs). Now it is no longer necessary to modify the "pos" parameter, which was a very hacky and error-prone workaround. Simply return the state table, and it twill be subsequently passed to the on_open() and on_close() callbacks.

Code: Select all

minetest.register_node( "mymod:mynode", {
        :
        before_open = function ( pos, node, player )
                return { player_name = player:get_player_name( ), node_name = node.name, pos = pos }
        end
        on_open = function ( state, player )
                local formspec = "size[8.5,10.0]"
                :
If the before_open() callback is not defined or it returns nil, then the node position will be passed to on_open() as usual.

Another new feature, which I had announced previously, is that the selected index of dropdowns can be included within the fields passed to the callback instead of the option value itself. Therefore, reverse-lookup tables can be completely avoided, It's that easy!
  • dropdown[<X>,<Y>;<W>;<name>;<item 1>,<item 2>, ...,<item n>;<selected idx>;<use_index>]
By setting the parameter use_index to true, the callback will automatically receive the selected index whenever the dropdown is changed or the form is submitted.

And lastly I've added support for cascading forms such as dialog boxes, alerts, etc. These can be implemented through the use of two additional signals: SIGHOLD and SIGCONT.

By passing a SIGHOLD signal to minetest.create_form(), the callback will be notified of the signal and the form session will be suspended (this also stops any form timers). As soon as the child form is closed, the previous form session will be restored and the callback will be notified of a SIGCONT signal. ActiveFormspecs manages all of the session housekeeping behind the scenes.

Here is an example of a popup alert that, when opened, will temporarily suspend the current form session until closed:

Code: Select all

local function open_alert( player_name, message )
        local get_formspec = function ( )
                local formspec = "size[4,3]"
                        .. default.gui_bg_img
                        .. string.format( "label[0.5,0.5;%s]", minetest.formspec_escape( message ) )
                        .. "button_exit[1.0,2;2.0,1;close;Close]"
                return formspec
        end

        minetest.create_form( nil, player_name, get_formspec( ), nil, minetest.FORMSPEC_SIGHOLD )
end
One caveat is that the parent form must be updated whenever a SIGCONT signal is received. This is for security reasons, given that element states (like dropdowns, scrollbars, etc.) cannot be preserved automatically, and some elements may even contain stale data (as with the case of textareas, fields, etc.) which should not be preserved anyway. Thankfully, only one line of code is required:

Code: Select all

if fields.quit == minetest.FORMSPEC_SIGCONT then minetest.update_form( player_name, get_formspec( ) ) end

User avatar
1faco
Member
Posts: 84
Joined: Tue Sep 08, 2020 20:32
GitHub: minefaco
In-game: faco

Re: [Mod] ActiveFormspecs [formspecs]

by 1faco » Post

ModError: Failed to load and run script from /home/frank/.minetest/mods/protector/init.lua:
/home/frank/.minetest/mods/formspecs/init.lua:121: Attempt to override non-existent item doors:door_steel
stack traceback:
[C]: in function 'error'
/usr/bin/../share/minetest/builtin/game/register.lua:402: in function 'old_override_item'
/home/frank/.minetest/mods/formspecs/init.lua:121: in function 'override_item'
/home/frank/.minetest/mods/protector/init.lua:503: in main chunk
Check debug.txt for details.

Minetest 5.3.0 stable

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: [Mod] ActiveFormspecs [formspecs]

by sorcerykid » Post

This is actually a problem with the Protector Redux mod, as it attempts to override the steel door from Minetest Game. You can simply comment out the following section of code from protector/init.lua accordingly:

Code: Select all

--[[
minetest.override_item( "doors:door_steel", {
        allow_place = function ( target_pos, player )
		local player_name = player:get_player_name( )
		if not allow_place( target_pos, player_name, "doors:door_steel" ) then
	                minetest.chat_send_player( player_name, "You are not allowed to place steel doors here!" )
			return false
		end
                return true
        end
} )
]]
I will be sure to update Protector Redux with the correct dependencies and also add a check for the Doors mod.

User avatar
1faco
Member
Posts: 84
Joined: Tue Sep 08, 2020 20:32
GitHub: minefaco
In-game: faco

Re: [Mod] ActiveFormspecs [formspecs]

by 1faco » Post

sorcerykid wrote:
Mon Sep 14, 2020 11:41
This is actually a problem with the Protector Redux mod, as it attempts to override the steel door from Minetest Game. You can simply comment out the following section of code from protector/init.lua accordingly:

Code: Select all

--[[
minetest.override_item( "doors:door_steel", {
        allow_place = function ( target_pos, player )
		local player_name = player:get_player_name( )
		if not allow_place( target_pos, player_name, "doors:door_steel" ) then
	                minetest.chat_send_player( player_name, "You are not allowed to place steel doors here!" )
			return false
		end
                return true
        end
} )
]]
I will be sure to update Protector Redux with the correct dependencies and also add a check for the Doors mod.
it works, thank you.

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: [Mod] ActiveFormspecs [formspecs]

by sorcerykid » Post

Code Guide: Creating Fully Interactive Node Formspecs

One of the key benefits of ActiveFormspecs is that it provides an abstraction layer to display dynamic node formspecs via its own "on_open" callback. Although this mechanism has advantages for GUIs that are non-interactive, it isn't particularly suitable when you need to update forms in response to user input, timers, and other events.

In those situations, you should instead use minetest.create_form() within the context of the node's "on_rightclick" callback. Here is how we could adapt the "score" chat command described in the last code guide as a fully interactive node-based formspec:

Code: Select all

minetest.register_node( "default:heart_block", {
        description = "Why's Heart Block",
        paramtype2 = "facedir",
        place_param2 = 0,
        tiles = { "default_stone.png^heart.png" },
        is_ground_content = false,
        groups = { cracky = 2, stone = 1, dig_immediate = 2 },
        sounds = default.node_sound_wood_defaults( ),

        on_rightclick = function ( pos, node, player )
                local name = player:get_player_name( )
                local score = 0
                local count = 0
                local max_count = 25
                
                local function get_formspec( )
                        local formspec = "size[4,4]" ..
                                "label[0.2,1.0;How lucky do you feel today?]" ..
                                "button[1,2;2,1;raise_score;Raise Score!]" ..
                                "box[0.5,3;3,1;#222222]" ..
                                string.format( "label[1,3.2;Your Score: %03d]", score )

                        return formspec
                end

                minetest.create_form( nil, name, get_formspec( ), function ( state, player, fields )
                        if fields.raise_score then
                                count = count + 1

                                score = score + math.random( -5, 10 )
                                if score < 0 or count > max_count then
                                        minetest.chat_send_player( name,
                                                "Sorry buddy, you didn't win this time!" )
                                        minetest.destroy_form( name )
                                        
                                elseif math.random( count + 15 ) == 1 then
                                        minetest.chat_send_player( name,
                                                "CONGRATULATIONS! You just won a pot of virtual gold!" )
                                        minetest.spawn_item( vector.offset( pos, 0, 1, 0 ),
                                                "default:gold_lump " .. ( max_count - count ), 0, 5 )
                                        minetest.destroy_form( name )
                                        
                                elseif score >= 100 then
                                        minetest.chat_send_player( name,
                                                "CONGRATULATIONS! You just won the jackpot!" )
                                        minetest.spawn_item( vector.offset_y( pos ), "default:gold_lump 100" )
                                        minetest.destroy_form( name )
                                        
                                elseif score >= 50 then
                                        minetest.update_form( name, get_formspec( ) )
                                        minetest.chat_send_player( name,
                                                "You're a real winner. Keep going!" )
                                                
                                elseif score >= 10 then
                                        minetest.update_form( name, get_formspec( ) )
                                        minetest.chat_send_player( name,
                                                "Now you're on a roll. Do it again!" )
                                                
                                else
                                        minetest.update_form( name, get_formspec( ) )
                                        minetest.chat_send_player( name,
                                                "Don't give up. Try another round!" )
                                end

                        elseif fields.quit == minetest.FORMSPEC_SIGEXIT then
                                minetest.chat_send_player( name,
                                        "You scored " .. score .. " after " .. count .. " tries!" )
                        end
                end )
        end
} )
For most ordinary node-based formspecs, the "on_open" callback will likely suffice. But when the need arises, you can take advantage of the full ActiveFormspecs API as shown above. Good luck and keep having fun with formspecs!

User avatar
Noodlemire
Member
Posts: 61
Joined: Sun May 27, 2018 00:07
GitHub: Noodlemire
In-game: Noodlemire

Re: [Mod] ActiveFormspecs [formspecs]

by Noodlemire » Post

The /fs command is currently broken in two ways. First off, it uses default's gui_bg and gui_bg_img, which means that it has a secret dependency on default just for the sake of visuals. Second, the "ActiveFormspecs v2.6" label breaks itself and the "uptime" label my missing a ].

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: [Mod] ActiveFormspecs [formspecs]

by sorcerykid » Post

Noodlemire wrote:
Sun Sep 20, 2020 01:45
The /fs command is currently broken in two ways. First off, it uses default's gui_bg and gui_bg_img, which means that it has a secret dependency on default just for the sake of visuals. Second, the "ActiveFormspecs v2.6" label breaks itself and the "uptime" label my missing a ].
Both of those issues were resolved in Version 2.7-alpha, which is available on GitHub:

https://github.com/sorcerykid/formspecs/tree/2.7-alpha

Since the chat command is only an ancillary feature, I don't really consider it critical bug.

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: [Mod] ActiveFormspecs [formspecs]

by sorcerykid » Post

Code Guide: Principles of Form Pagination

Handling paginated data in forms might seem daunting at first, but there are a few algorithms that can prove beneficial in for most situations. Let's assume that you plan to index an array, name "high_scores". Here is how you might construct the loop in the get_formspec() function:

Code: Select all

local function get_formspec( )
        :
        local page_size = 10
        for idx = ( page_idx - 1 ) * page_size + 1, math.min( page_idx * page_size, #high_scores ) do
                local score = high_scores[ idx ].score
                local name = high_scores[ idx ].name
                :
As you can see, the "page_size" variable determines the number of entries per page, in this case 10, whereas the "idx" variable is the actual index into the "high_scores" array. By iterating the array index ourselves, rather than relying on ipairs(), we are able lookup only those entries that are specifically within the range of this page.

Of course, it's essential that Back and Next buttons be provided so that the user can advance through the pages in either direction. The logic for such buttons is fairly straightforward:

Code: Select all

on_close = function ( state, player, fields )
		:
		elseif fields.prev_page then
			if page_idx > 1 then
				page_idx = page_idx - 1
				minetest.update_form( player_name, get_formspec( ) )
			end

		elseif fields.next_page then
			if page_idx < max_skin_idx / page_size  then
				page_idx = page_idx + 1
				minetest.update_form( player_name, get_formspec( ) )
			end
		end
		
What about buttons that loop from beginning to end and vice-versa? That only requires a small change to logic:

Code: Select all

on_close = function ( state, player, fields )
		:
		elseif fields.prev_page then
			if page_idx > 1 then
				page_idx = page_idx - 1
				minetest.update_form( player_name, get_formspec( ) )
                        elseif #high_scores > 1 then
                                page_idx = math.ceil( #high_scores / page_size )
                                minetest.update_form( player_name, get_formspec( ) )
			end

		elseif fields.next_page then
			if page_idx < #high_scores / page_size  then
				page_idx = page_idx + 1
				minetest.update_form( player_name, get_formspec( ) )
                        elseif #high_scores > 1 then
                                data.page = 1
                                minetest.update_form( player_name, get_formspec( ) )
			end
		end

User avatar
Wuzzy
Member
Posts: 4803
Joined: Mon Sep 24, 2012 15:01
GitHub: Wuzzy2
IRC: Wuzzy
In-game: Wuzzy
Contact:

Re: [Mod] ActiveFormspecs [formspecs]

by Wuzzy » Post

I really don't like that this mod populates the minetest table. This table should be reserved for ... well ... Minetest only.
Why not putting everything in literally any other global table instead?

But I didn't really look at anything else of the mod, but that one thing stood out to me immediately.

AmyMoriyama
Member
Posts: 107
Joined: Wed Jun 30, 2021 14:53
GitHub: AmyMoriyama

Re: [Mod] ActiveFormspecs [formspecs]

by AmyMoriyama » Post

Your mirror (github) is outdated.

Post Reply

Who is online

Users browsing this forum: TPH and 34 guests