Post your code!

Sokomine
Member
Posts: 4276
Joined: Sun Sep 09, 2012 17:31
GitHub: Sokomine
IRC: Sokomine
In-game: Sokomine

Re: Post your code!

by Sokomine » Post

sorcerykid wrote: A player on my sever recently notified me that the screwdriver can be used to rotate chests sideways. So I wrote a command-line Lua script to locate all such incorrectly oriented chests, furnaces, bookshelves, and vessels shelves within the world.
Don't change these things! There's got to be enough intresting stuff for lga's intresting museums :-)
A list of my mods can be found here.

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

Re: Post your code!

by sorcerykid » Post

I developed a raycast function for efficiently computing line of sight between two points in 3d space. It is loosely based on the nearest neighbor line-drawing algorithm, and thus focuses on speed rather than accuracy.

raytrace_simple( pos1, pos2, on_plot )
  • pos1 - the starting position of the ray as a vector
  • pos2 - the ending position of the ray as a vector
  • on_plot - the callback function for traversing the ray
The callback function will receive both the 1-based index and position of each successive node along the ray from pos1 through pos2. When an obstacle is encountered, the function should return false or nil to abort the raytrace.

Code: Select all

local function raytrace_simple( pos1, pos2, on_plot )
        local p1 = vector.round( pos1 )
        local p2 = vector.round( pos2 )
        local ax = abs( p2.x - p1.x )
        local ay = abs( p2.y - p1.y )
        local az = abs( p2.z - p1.z )
        local step = max( ax, ay, az )
        local dx = ( p2.x - p1.x ) / step;
        local dy = ( p2.y - p1.y ) / step;
        local dz = ( p2.z - p1.z ) / step;
        local x = p1.x
        local y = p1.y
        local z = p1.z

        for i = 1, step + 1 do
                if not on_plot( i, { x = floor( x + 0.5 ), y = floor( y + 0.5 ), z = floor( z + 0.5 ) } ) then
                        return false
                end

                x = x + dx
                y = y + dy
                z = z + dz
        end

        return true
end
Using the function above, a more flexible alternative to minetest.line_of_sight() can be devised:

Code: Select all

line_of_sight = function ( source_pos, target_pos )
        local unknown_ndef = { groups = { }, walkable = false }

        return raytrace_simple( source_pos, target_pos, function ( idx, pos )
                local node = minetest.get_node( pos )
                local ndef = core.registered_nodes[ node.name ] or unknown_ndef     -- account for unknown nodes
                local is_airlike = not ndef.walkable

                return is_airlike
        end )
end
Notice that the return value of raytrace_simple() reflects whether the callback succeeded or failed while traversing the ray.

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

Re: Post your code!

by sorcerykid » Post

This is a very simple Auth Redux ruleset for limiting new accounts to certain times of day, unless the server operator is online. Of course the range of hours can be changed by editing the pattern /8^19:?/t accordingly.

Code: Select all

try "Our server is only open to new players between 8:00-19:59 or when $owner is online"
fail all
if $is_new eq $true
unless $clock is /8^19:?/t
unless $owner in $users_list
continue

User avatar
Linuxdirk
Member
Posts: 3218
Joined: Wed Sep 17, 2014 11:21
In-game: Linuxdirk
Location: Germany
Contact:

Re: Post your code!

by Linuxdirk » Post

A function to get a configuration value as string from either ./minetest.conf, a world-specific ./worlds/worldname/modname.conf or a given default value.

It is also possible to mark values as not changeable via configuration options (which could be done simply by not using the function then, but it’s all about options, right?).

Simply add this to your configuration Lua file or add it to your mod’s global table’s function section (fits within the 80 characters rule, of course).

Code: Select all

local get_config_option = function (key_name, default_value, changeable)
    local mod = minetest.get_current_modname()
    local config_option = mod..'_'..key_name
    local value = default_value

    if changeable ~= false then
        local wc = Settings(minetest.get_worldpath()..DIR_DELIM..mod..'.conf')
        local global_setting = minetest.settings:get(config_option)
        local world_setting = wc:get(config_option)
        value = world_setting or global_setting or default_value or ''
    end

    return value
end
And use it in your code like this:

Code: Select all

local my_option = get_config_option('my_option', 'foo')
local my_other_option = get_config_option('my_other_option', 'bar')
local and_so_on = get_config_option('and_so_on', 'baz', false)
This automatically allows users to configure things to their liking using the following parameters (assuming mymod is your mod’s name).

Code: Select all

mymod_my_option = foo
mymod_my_other_option = bar
If you later decide to have the third option also being configurable, simply remove the false parameter and players can set mymod_and_so_on, too.

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

Re: Post your code!

by sorcerykid » Post

Here is a new Minetest API function to make including libraries and submodules easier at long last.

Before:
local helpers = dofile( minetest.get_modpath( "othermod" ) .. "/helpers.lua" )

After:
local helpers = minetest.include( "/othermod/helpers.lua" )

Before:
loadfile( minetest.get_modpath( "thismod" ) .. "/gui.lua" )( config, player_list )

After:
minetest.include( "gui.lua", config, player_list )

The minetest.include() function includes a file. It accepts a file specifier, which may be either a path relative to the currently loading mod or else relative to another mod with a leading slash. Any additional parameters are passed directly to the loaded script, which may be retrieved via local var1, var2, var3 = ... at the head of the included file.

Code: Select all

function minetest.include( filespec, ... )
        local mod_name, file_name = string.match( filespec, "^[/\](.-)[/\](.+)" )
        if not mod_name then
                mod_name = minetest.get_current_modname( )
                file_name = filespec
        end
        return loadfile( minetest.get_modpath( mod_name ) .. "/" .. file_name )( ... )
end

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

Re: Post your code!

by sorcerykid » Post

This is a handy command-line script for locating all "broken" beds in the world. It will output a list of bed parts that either
  • consist of only a top but no bottom,
  • consist of only a bottom but no top, or
  • have incongruent orientation of bottom and top
At the head of the script are two variables that must be set accordingly:
  • source_path = full path of the map.sqlite database to be opened in read-only mode
  • search_area = the mapblock boundaries in which to limit the search (comment out to search the entire map)

Code: Select all

-- Find all mismatched bed parts (top or bottom)

package.path = "/home/minetest/maplib/?.lua;" .. package.path

local maplib = require "maplib"

local source_path = "/home/minetest/.minetest/worlds/world/map.sqlite"
--local search_area = MapArea( { x = -10, y = 0, z = -10 }, { x = 10, y = 0, z = 10 } )

---------------------------------------

local decode_node_pos = maplib.decode_node_pos
local hash_node_pos = maplib.hash_node_pos
local unhash_node_pos = maplib.unhash_node_pos
local pos_to_string = maplib.pos_to_string

local lower_parts = { }
local upper_parts = { }
local lower_count = 0
local upper_count = 0

local function analyze_block( block, index, node_list, nodename_map )
	local is_found = false

	for node_idx, node in ipairs( node_list ) do
		local node_name = nodename_map[ node.id ]

		if node_name == "beds:bed_bottom" or node_name == "beds:fancy_bed_bottom" then
			local node_pos = decode_node_pos( node_idx, index )
			local node_hash = hash_node_pos( node_pos )

			lower_parts[ node_hash ] = node.param2
			lower_count = lower_count + 1

		elseif node_name == "beds:bed_top" or node_name == "beds:fancy_bed_top" then
			local node_pos = decode_node_pos( node_idx, index )
			local node_hash = hash_node_pos( node_pos )

			upper_parts[ node_hash ] = node.param2
			upper_count = upper_count + 1
		end
	end
end

local function compare_parts( )
	for hash, param2 in pairs( lower_parts ) do
		local pos2 = unhash_node_pos( hash )

		if param2 == 0 then
			pos2.z = pos2.z + 1
		elseif param2 == 1 then
			pos2.x = pos2.x + 1
		elseif param2 == 2 then
			pos2.z = pos2.z - 1
		elseif param2 == 3 then
			pos2.x = pos2.x - 1
		else
			pos2 = nil
		end

		if pos2 then
			local hash2 = hash_node_pos( pos2 )

			if upper_parts[ hash2 ] == param2 then
				lower_parts[ hash ] = nil
				upper_parts[ hash2 ] = nil
			end
		end
	end

	print( string.format( "Mismatched 'beds:bed_bottom' and 'beds:fancy_bed_bottom' (%d total):", lower_count ) )
	for hash in pairs( lower_parts ) do
		local pos = unhash_node_pos( hash )
		print( string.format( "Found at %s", pos_to_string( pos ) ) )
	end

	print( string.format( "Mismatched 'beds:bed_top' and 'beds:fancy_bed_top' (%d total):", upper_count ) )
	for hash in pairs( upper_parts ) do
		local pos = unhash_node_pos( hash )
		print( string.format( "Found at %s", pos_to_string( pos ) ) )
	end
end

-----------------------------------------

local map_db = MapDatabase( source_path, false )

if search_area then
	print( "Creating cache..." )
	map_db.create_cache( false )
end

print( "Examining database..." )

local iterator = search_area and map_db.iterate_area( search_area ) or map_db.iterate( )

for index, block in iterator do
	local nodename_map = block.nodename_map

	for i, n in pairs( block.nodename_map ) do
		-- if there is at least one bed, then analyze mapblock
		if n == "beds:bed_bottom" or n == "beds:bed_top" or n == "beds:fancy_bed_bottom" or n == "beds:fancy_bed_top" then
			local node_list = block.get_node_list( )

			analyze_block( block, index, node_list, nodename_map )
			break
		end
	end
end

compare_parts( )

Termos
Member
Posts: 417
Joined: Sun Dec 16, 2018 12:50

Re: Post your code!

by Termos » Post

Minetest implementation of Midpoint Circle algorithm

Image

Code: Select all

local function circle(pos,r,plane,callback)
	local tpos = {}
	tpos[plane] = pos[plane]
	local ax = {'x','y','z'}
	
	for i,e in ipairs(ax) do
		if e==plane then 
			table.remove(ax,i)
			if #ax ~= 2 then 
				minetest.log("error","circle: bad plane argument")
				return
			end
		end
	end
	
	local dx, dy, err = r, 0, 1-r
	local c1, c2 = ax[1], ax[2]
	while dx >= dy do
		tpos[c1], tpos[c2] = pos[c1]+dx, pos[c2]+dy
		callback(tpos)		
		tpos[c1], tpos[c2] = pos[c1]-dx, pos[c2]+dy
		callback(tpos)		
		tpos[c1], tpos[c2] = pos[c1]+dx, pos[c2]-dy
		callback(tpos)		
		tpos[c1], tpos[c2] = pos[c1]-dx, pos[c2]-dy
		callback(tpos)		
		tpos[c1], tpos[c2] = pos[c1]+dy, pos[c2]+dx
		callback(tpos)		
		tpos[c1], tpos[c2] = pos[c1]-dy, pos[c2]+dx
		callback(tpos)		
		tpos[c1], tpos[c2] = pos[c1]-dy, pos[c2]+dx
		callback(tpos)		
		tpos[c1], tpos[c2] = pos[c1]+dy, pos[c2]-dx
		callback(tpos)		
		tpos[c1], tpos[c2] = pos[c1]-dy, pos[c2]-dx
		callback(tpos)
		dy = dy + 1
		if err < 0 then
			err = err + 2 * dy + 1
		else
			dx, err = dx-1, err + 2 * (dy - dx) + 1
		end
	end
end
Attachments
screenshot_20210112_104151.png
screenshot_20210112_104151.png (93.21 KiB) Viewed 6530 times

User avatar
AspireMint
Member
Posts: 415
Joined: Mon Jul 09, 2012 12:59
GitHub: AspireMint
IRC: AspireMint
In-game: AspireMint
Location: Stuck at spawn

Re: Post your code!

by AspireMint » Post

Code: Select all

wrap = function(t)
	local w = {}
	setmetatable(w, { __index = t })
	return w
end

unwrap = function(w)
	local mt = getmetatable(w)
	return mt and mt.__index or w
end
With this you can override global table without affecting other mods.
Example:

Code: Select all

local M = wrap(minetest)
M.chat_send_all("yay A")
M.chat_send_all = function(msg)
	print("shhh")
	minetest.chat_send_all(msg)
	-- or unwrap(M).chat_send_all(msg)
end
M.chat_send_all("yay two")
minetest.chat_send_all("yay 3")
Probably useful (?) when you want to improve minetest table but only inside of your mod and not break any other mod.

talamh
Member
Posts: 156
Joined: Sun Nov 12, 2017 18:24

Re: Post your code!

by talamh » Post

SQL query for sqlite databases that returns all mapblocks in a specified volume, corrects for off by -1 errors that can occur when extracting x, y and z via bit manipulation. i.e. if x > 2048 value for y would be 1 less than correct value, likewise if the corrected value for y > 2048 the value for z would be 1 less than its correct value.

Code: Select all

SELECT pos,
       data
	
FROM	(
		SELECT pos,
			CASE 
				WHEN (pos & 0xfff) > 2048 
               			THEN (pos & 0xfff) - 4096
                		ELSE pos & 0xfff
            		END AS x,
               	
            		CASE
               			WHEN (((pos & 0xfff000) >> 12) + ((pos & 0xfff) > 2048)) > 2048
             			THEN ((pos & 0xfff000) >> 12) + ((pos & 0xfff) > 2048) - 4096
                		ELSE ((pos & 0xfff000) >> 12) + ((pos & 0xfff) > 2048)
            		END AS y,
               
            		CASE
                		WHEN (((pos & 0xfff000) >> 12) + ((pos & 0xfff) > 2048)) > 2048
               			THEN
                  			CASE
                     				WHEN (((pos & 0xfff000000) >> 24) + 1) > 2048 
                     				THEN (((pos & 0xfff000000) >> 24) + 1) - 4096
                     				ELSE (((pos & 0xfff000000) >> 24) + 1)
                   			END
                 	ELSE
                   		CASE
                     			WHEN ((pos & 0xfff000000) >> 24) > 2048 
                     			THEN ((pos & 0xfff000000) >> 24) - 4096
                     			ELSE ((pos & 0xfff000000) >> 24)
                   		END
           		END AS z,
            data
        	FROM blocks
		)
        	
WHERE x BETWEEN -2 AND 2 AND
	y BETWEEN -2 AND 2 AND
	z BETWEEN -2 AND 2


Edit: Added Python code to dehash pos using bit manipulation:

Code: Select all

def getIntegerAsBlock(pos):

    x = pos & 4095
    y = ((pos & 16773120) >> 12)
    z = ((pos & 68702699520) >> 24)

    if x > 2048:
        x -= 4096
        y += 1

    if y > 2048:
        y -= 4096
        z += 1

    if z > 2048:
        z -= 4096

    return x, y, z

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

Re: Post your code!

by sorcerykid » Post

Here is a helper function for obtaining the daypart as a string, useful for ensuring that certain events only occur at specific periods of the day; e.g. having a cicada sound only play during the evening hours.

Code: Select all

function to_daypart( time )
        -- by half:
        -- * 6am - 6pm (daytime)
        -- * 6pm - 6am (nighttime)
        -- by quarter:
        -- * 6am - noon (morning)
        -- * noon - 6pm (afternoon)
        -- * 6pm - midnight (evening)
        -- * midnight - 6am (overnight)
        local quarter, half

        if time == nil then time = minetest.get_timeofday( ) end

        if time < 0.25 then
                quarter = "overnight"
        elseif time < 0.5 then
                quarter = "morning"
        elseif time < 0.75 then
                quarter = "afternoon"
        else
                quarter = "evening"
        end

        if time < 0.25 or time > 0.75 then
                half = "nighttime"
        else
                half = "daytime"
        end

        return { quarter = quarter, half = half }
end
The function returns a table with two key/value pairs: quarter and half. Both are fairly self-explanatory. For example, when the time of day is 13:15, the result will be { quarter = "afternoon", half = "daytime" }.

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

Re: Post your code!

by sorcerykid » Post

Here is a much improved version of the string matching helper function that I posted back in 2019. Unlike the previous implementation, this one does not pollute the global environment. And the capture groups are stored in a dedicated _C variable, rather than recycling the commonly use underscore variable.

Code: Select all

function string.is_match()
        local _C = { }
        return function ( text, pattern )
                local res = { string.match( text, pattern ) }
                setmetatable( _C, { __index = res } )
                return #res > 0
        end, _C
end
Notice that string.is_match() is now a factory. It returns the actual string-matching function along with the table that will be used to store the capture groups. Of course, these variables should be lexically scoped (that is, local to the mod itself or to the current block).

local is_match, _C = string.is_match()

This is an example of how it can be used in a scenario where you want to accept input either in the form of [distance] [axis] or else [xpos] [ypos] [zpos], where numbers can be decimals or floating points.

Code: Select all

local input = "3 X"
local is_match, _C = string.is_match( )

local function move_by( a, b )
        print( a, b )
end

local function move_to( a, b, c )
        print( a, b, c )
end

if is_match( "(-?%d) ([XxYyZz])" ) or is_match"(-?%d%.%d) ([XxYyZz])" ) then
        move_by( string.lower( _C[ 2 ] ), tonumber( _C[ 1 ] ) )

elseif is_match( "(-?%d+)[, ](-?%d+)[, ](-?%d+)" ) or
                is_match( "(-?%d%.%d+)[, ](-?%d%.%d+)[, ](-?%d%.%d+)" ) then
        move_to( tonumber( _C[ 1 ] ), tonumber( _C[ 2 ] ), tonumber( _C[ 3 ] ) )
else
        print( "Invalid input!" )
end


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

Re: Post your code!

by sorcerykid » Post

The following Lua helper function can be used to split the lines of a given input string (such as from a plain text file), into a table. Optionally, blank lines can be suppressed by passing true for the second parameter.

lines = splitlines( str, has_blanks )

I ended up writing this function because the solutions I found online failed to handle blank lines correctly. Some would treat a trailing newline character as a blank line, even though that's just the EOF. Others would interpret an empty string as a single line, even though it's really no lines. Still others would completely "collapse" any blank lines, rather than preserving them as individual empty strings.

For example: https://stackoverflow.com/questions/328 ... ing-gmatch

Here are the test cases:
  • An empty text file should return an empty table.
    Correct: "" becomes { }
    Incorrect: "" becomes { "" }
    Incorrect: "" becomes nil
    .
  • A text file consisting of just a newline should return a table with one empty string:
    Correct: "\n" becomes { "" }
    Incorrect: "\n" becomes { "", "" }
    Incorrect: "\n" becomes { }
    .
  • A text file consisting of empty lines should return a table with empty strings in-tact:
    Correct: "Hello\nWorld\n\n" becomes { "Hello", "World", "" }
    Incorrect: "Hello\nWorld\n\n" becomes { "Hello", "World" }
    Incorrect: "Hello\nWorld\n\n" becomes { "Hello", "World", "", "" }
It's worth noting that this function is also cross-platform aware, so it can accept strings even with a mixture of line-endings: Windows OS, Macintosh OS (legacy), or Linux/BSD are all supported.

Code: Select all

local function splitlines( str, has_blanks )
        res = { }
        str = string.gsub( str, "\r\n", "\n" )  -- normalize CRLF (used by Windows OS)
        str = string.gsub( str, "\r", "\n" )    -- normalize CR (used by Macintosh OS)

        if str ~= "" then
                str = string.gsub( str, "\n$", "" )     -- ignore trailing newline
                for val in string.gmatch( str .. "\n", "(.-)\n" ) do
                        if val ~= "" or has_blanks then
                                table.insert( res, val )
                        end
                end
        end
        return res
end

User avatar
MisterE
Member
Posts: 693
Joined: Sun Feb 16, 2020 21:06
GitHub: MisterE123
IRC: MisterE
In-game: MisterE

Re: Post your code!

by MisterE » Post

sorcerykid wrote:
Thu Jun 02, 2022 19:57
The following Lua helper function can be used to split the lines of a given input string (such as from a plain text file), into a table. Optionally, blank lines can be suppressed by passing true for the second parameter.

lines = splitlines( str, has_blanks )

I ended up writing this function because the solutions I found online failed to handle blank lines correctly. Some would treat a trailing newline character as a blank line, even though that's just the EOF. Others would interpret an empty string as a single line, even though it's really no lines. Still others would completely "collapse" any blank lines, rather than preserving them as individual empty strings.

For example: https://stackoverflow.com/questions/328 ... ing-gmatch

Here are the test cases:
  • An empty text file should return an empty table.
    Correct: "" becomes { }
    Incorrect: "" becomes { "" }
    Incorrect: "" becomes nil
    .
  • A text file consisting of just a newline should return a table with one empty string:
    Correct: "\n" becomes { "" }
    Incorrect: "\n" becomes { "", "" }
    Incorrect: "\n" becomes { }
    .
  • A text file consisting of empty lines should return a table with empty strings in-tact:
    Correct: "Hello\nWorld\n\n" becomes { "Hello", "World", "" }
    Incorrect: "Hello\nWorld\n\n" becomes { "Hello", "World" }
    Incorrect: "Hello\nWorld\n\n" becomes { "Hello", "World", "", "" }
It's worth noting that this function is also cross-platform aware, so it can accept strings even with a mixture of line-endings: Windows OS, Macintosh OS (legacy), or Linux/BSD are all supported.

Code: Select all

local function splitlines( str, has_blanks )
        res = { }
        str = string.gsub( str, "\r\n", "\n" )  -- normalize CRLF (used by Windows OS)
        str = string.gsub( str, "\r", "\n" )    -- normalize CR (used by Macintosh OS)

        if str ~= "" then
                str = string.gsub( str, "\n$", "" )     -- ignore trailing newline
                for val in string.gmatch( str .. "\n", "(.-)\n" ) do
                        if val ~= "" or has_blanks then
                                table.insert( res, val )
                        end
                end
        end
        return res
end
what is the license for your code snippets here?

User avatar
LMD
Member
Posts: 1386
Joined: Sat Apr 08, 2017 08:16
GitHub: appgurueu
IRC: appguru[eu]
In-game: LMD
Location: Germany
Contact:

Re: Post your code!

by LMD » Post

For files for line in io.readlines(filename) do ... end or file = io.open(filename); while true do line = file:read(); if not line then break end ... end can be used.
My stuff: Projects - Mods - Website

User avatar
LMD
Member
Posts: 1386
Joined: Sat Apr 08, 2017 08:16
GitHub: appgurueu
IRC: appguru[eu]
In-game: LMD
Location: Germany
Contact:

Re: Post your code!

by LMD » Post

sorcerykid wrote:
Thu Jun 02, 2022 19:57

Code: Select all

local function splitlines( str, has_blanks )
        res = { }
        str = string.gsub( str, "\r\n", "\n" )  -- normalize CRLF (used by Windows OS)
        str = string.gsub( str, "\r", "\n" )    -- normalize CR (used by Macintosh OS)

        if str ~= "" then
                str = string.gsub( str, "\n$", "" )     -- ignore trailing newline
                for val in string.gmatch( str .. "\n", "(.-)\n" ) do
                        if val ~= "" or has_blanks then
                                table.insert( res, val )
                        end
                end
        end
        return res
end
You can get rid of two string copies:

1. Use str = string.gsub( str, "\r\n?", "\n" ) to normalize the line endings
2. Check whether there is a trailing newline and don't add one in that case: str = str:match"\n$" and str or (str .. "\n")

I'd also suggest making this an iterator by doing return string.gmatch( str .. "\n", "(.-)\n" ) instead of running the for loop; has_blanks could be implemented by changing the pattern: has_blanks and "(.-)\n" or "(..-)\n".
My stuff: Projects - Mods - Website

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

Re: Post your code!

by sorcerykid » Post

LMD wrote:
Tue Jun 07, 2022 07:37
For files for line in io.readlines(filename) do ... end or file = io.open(filename); while true do line = file:read(); if not line then break end ... end can be used.
That requires knowing that you are working only with a file. It doesn't help for general purpose parsing of textual data streams.

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

Re: Post your code!

by sorcerykid » Post

I just finished work on a lightweight templating class for JS. This makes creating basic dynamic webpages so much easier. No need for intermixing long, obtuse strings of HTML with Javascript. Now any given sections of an HTML document can be designated as a template, and values can be substituted on-the-fly afterward.

Note: This isn't to be confused with Javascript's builtin "template literals". I much prefer the flexibility and convenience of C's sprintf() function, including the most common format specifiers: %s for string, %d for integer, and %f for floating point, with options for padding, field-width, and precision.

Code: Select all

function MarkupTemplate( id, args )
{
        let elem = document.getElementById( id );
        let tmpl = elem.innerHTML;

        if( tmpl.search( /^<!--\s[^]+\s-->$/ ) == -1 )
                return null;

        tmpl = tmpl.slice( 5, -4 );

        this.remove = function ( )
        {
                elem.innerHTML = '';
        }

        this.update = function ( )
        {
                let regex = /%([-0])?([0-9]+)?(\.[0-9]+)?([sdf%])/g;
                let args = arguments;
                let idx = 0;

                elem.innerHTML = tmpl.replace( regex, function ( exp, p0, p1, p2, p3 )
                {
                        if( exp == '%%' )
                                return '%';
                        else if( idx >= args.length )
                                return 'undefined';

                        let type = p3;
                        let prec = p2 != undefined ? parseInt( p2.substr( 1 ) ) : 6;
                        let size = p1 != undefined ? parseInt( p1 ) : 0;
                        let fill = p0 == '0' && type != 's' ? '0' : ' ';
                        let is_sign = type != 's' && args[ idx ] < 0;
                        let is_left = p0 == '-';

                        let str;
                        switch( type ) {
                                case 's':
                                        str = args[ idx ];
                                        break;
                                case 'd':
                                        str = parseFloat( args[ idx ] ).toFixed( 0 );
                                        break;
                                case 'f':
                                        str = parseFloat( args[ idx ] ).toFixed( prec );
                                        break;
                        }
                        while( str.length < size ) {
                                str = is_left ? str + ' ' : fill + str;
                                if( fill == '0' && !is_left && is_sign )
                                        str = '-0' + str.substr( 2 );  // fun corner case!
                        }

                        idx++;

                        return str;
                } )
        }

        this.listen = function ( type, listener )
        {
                elem.addEventListener( type, listener );
        }
        
        if( args )
                this.update( ...args );  // use initial values
        else
                this.remove( );

        return this;
}
For example, here's the weather dialog for the JT2 server. This is all the code it takes to update after an AJAX request:

Code: Select all

locale_stats1.update( res.locale_stats.amb_desc, w_captions[ res.locale_stats.amb_desc ] );
locale_stats2.update( res.locale_stats.amb_temp, res.locale_stats.amb_cool, res.locale_stats.amb_wind );
And here's the HTML with the designated template sections (note that the comment tags are required):

Code: Select all

<TABLE CLASS="infocard" WIDTH="400" BORDER="0" CELLPADDING="8" CELLSPACING="0">
<TR><TH CLASS="titlebar" COLSPAN="2">Weather Conditions</TH></TR>
<TR>
<TH WIDTH="50%" ID="locale_stats1"><!-- <P><IMG SRC="assets/weather_%s.png"><BR>%s</P> --></TH>
<TD WIDTH="50%" ID="locale_stats2"><!-- <H1>%d&deg;F</H1><P><B>Wind Chill:</B> %d&deg;F<BR><B>Wind Speed:</B> %0.1f mph</P> --></TD>
</TR>
</TABLE>
The basic structure of a template section is as follows:

Code: Select all

<ELEM ID="tpl"><!-- HTML to format with support for %d, %s, and %f --></ELEM>
Each template section is defined individually at the end of the document via the MarkupTemplate class:
  • tpl = MarkupTemplate( id, args )
The id is the unique ID of the template section. Most block elements are supported, although a DIV usually works best. The args are an array of values to format according to the template. If omitted, then the template will be cleared.

The MarkupTemplate class provides three methods:
  • tpl.remove( )
    Clear the template section.

    tpl.update( ... )
    Format the template section with the given arguments.

User avatar
Blockhead
Member
Posts: 1624
Joined: Wed Jul 17, 2019 10:14
GitHub: Montandalar
IRC: Blockhead256
In-game: Blockhead Blockhead256
Location: Land Down Under
Contact:

Re: Post your code!

by Blockhead » Post

sorcerykid wrote:
Thu Jun 23, 2022 20:27
And here's the HTML with the designated template sections (note that the comment tags are required):

Code: Select all

<TABLE CLASS="infocard" WIDTH="400" BORDER="0" CELLPADDING="8" CELLSPACING="0">
<TR><TH CLASS="titlebar" COLSPAN="2">Weather Conditions</TH></TR>
<TR>
<TH WIDTH="50%" ID="locale_stats1"><!-- <P><IMG SRC="assets/weather_%s.png"><BR>%s</P> --></TH>
<TD WIDTH="50%" ID="locale_stats2"><!-- <H1>%d&deg;F</H1><P><B>Wind Chill:</B> %d&deg;F<BR><B>Wind Speed:</B> %0.1f mph</P> --></TD>
</TR>
</TABLE>
Uppercase tags - Woohoo back to the 90s! Width, border, cellpaddding, cellspacing attributes would be replaced with CSS styles nowadays too. No indictment on your JavaScript or the validity of the HTML, just poking fun at how it's "out of style".

Also I thought temperature survive also supported metric as an option somewhere, I've seen Wuzzy with °C in screenshots. How will you support that?
/˳˳_˳˳]_[˳˳_˳˳]_[˳˳_˳˳\ Advtrains enthusiast | My map: Noah's Railyard | My Content on ContentDB ✝️♂

User avatar
iisu
Member
Posts: 220
Joined: Tue Mar 28, 2017 20:13
GitHub: iisu
IRC: iisu
In-game: iisu
Location: Internet

Re: Post your code!

by iisu » Post

From my little juice and ice cream mod I made for JT2*, registering O(n^3) items in one crafting recipe:

Code: Select all

local icecream_flavors = {
	["default:cactus"] = "cactus",
	["default:orange"] = "orange",
	["default:apple"] = "apple",
	["farming:blueberries"] = "blueberry",
	["farming:raspberries"] = "raspberry",
	["farming:melon"] = "melon",
	["easter_eggs:chocolate_egg"] = "chocolate",
	["easter_eggs:chocolate_egg_dark"] = "chocolate"
}

local add_item_to_group = function (item, group)
	local v = minetest.registered_items[item]
	if v ~= nil then
		local groups = v.groups
		groups[group] = 1
		
		minetest.override_item(item, {
			groups = groups
		})
	end
end

minetest.register_craftitem(mod_name .. ":ice_cream", {
	description = S("Ice Cream"),
	inventory_image = mod_name .. "_icecream.png",
	stack_max = 1
})

minetest.register_craft({
	output = mod_name .. ":ice_cream",
	recipe = {
		{ "default:snow", "group:chocolate_egg", "default:snow" },
		{ "default:snow", "group:icecream_flavor", "default:snow" },
		{ "group:icecream_flavor", "vessels:drinking_glass", "group:icecream_flavor" }
	}
})

for _, v in pairs(icecream_flavors) do
	for _, w in pairs(icecream_flavors) do
		for _, x in pairs(icecream_flavors) do
			if w ~= v and x ~= v then
				minetest.register_craftitem(mod_name .. ":ice_cream_" .. v .. "_" .. w .. "_" .. x, {
					description = S(v .. ", " .. w .. " and " .. x .. " Ice Cream"),
					inventory_image = mod_name .. "_icecream_" .. v .. "_left_ball.png^" ..
						mod_name .. "_icecream_" .. w .. "_top_ball.png^" ..
						mod_name .. "_icecream_" .. x .. "_right_ball.png^" ..
						mod_name .. "_icecream_cup.png",
					groups = { vessel = 1, not_in_creative_inventory = 1 },
					stack_max = 1,
					on_use = function (itemstack, user, pointed_thing)
						--apply status effect here
						
						return minetest.item_eat(3, "vessels:drinking_glass")(itemstack, user, pointed_thing)
					end
				})
			end
		end
	end
end

local select_flavors = function (itemstack, player, old_craft_grid, craft_inv)
	if itemstack:get_name() == mod_name .. ":ice_cream" then
		local v = icecream_flavors[old_craft_grid[7]:get_name()]
		local w = icecream_flavors[old_craft_grid[5]:get_name()]
		local x = icecream_flavors[old_craft_grid[9]:get_name()]
		if w == v or x == v or x == w then
			--effectively disables crafting
			itemstack:clear()
		else
			itemstack:set_name(mod_name .. ":ice_cream_" .. v .. "_" .. w .. "_" .. x)
		end
	end
end

minetest.register_craft_predict(select_flavors)
minetest.register_on_craft(select_flavors)
Results:
1. Clean crafting guide.
screenshot_20220704_221405.png
screenshot_20220704_221405.png (349.58 KiB) Viewed 5514 times
2. An assortment of craftable items.
screenshot_20220704_221413.png
screenshot_20220704_221413.png (361.27 KiB) Viewed 5514 times
Could be useful with stairs or something.

*The algorithm was not used on the server but you can still hop in there and enjoy fresh juice and ice cream!
Roses are red, violets are blue. Omae wa mou shindeiru.

User avatar
jwmhjwmh
Developer
Posts: 125
Joined: Sun Jul 18, 2021 14:10
GitHub: TurkeyMcMac

Re: Post your code!

by jwmhjwmh » Post

Code: Select all

local function interval_step(interval, correction_rate, step)
	local dtime_threshold = interval
	local dtime_accum = 0

	return function(dtime)
		dtime_accum = dtime_accum + dtime
		if dtime_accum < dtime_threshold then
			return
		end

		step(dtime_accum)

		dtime_threshold = math.max(dtime_threshold + (interval - dtime_accum) * correction_rate, 0)

		dtime_accum = 0
	end
end
Here's some code for scheduling a function to run at a certain interval. Unlike the "naive" implementation, this one ensures the average interval is very close to the desired interval, and is not biased above it. correction_rate is how much this code will change its internal threshold relative to the deviation between the desired interval and the actual interval. 0.1 seems to be a good value for this parameter.

Here's a usage example:

Code: Select all

minetest.register_globalstep(interval_step(1, 0.1, function(dtime)
	minetest.chat_send_all(dtime)
end))
When I run this on a dedicated server with step time 0.09, I get this repeating pattern in the chat (numbers slightly rounded):

Code: Select all

1.08
0.99
0.99
0.99
0.99
0.99
0.99
0.99
0.99
As you can see, there are eight intervals of 0.99 for every one interval of 1.08. The average is 1, as requested in the example code.

I discovered this mechanism by chance, basically, but it seems to work quite well. Pipeworks uses it now.

User avatar
Blockhead
Member
Posts: 1624
Joined: Wed Jul 17, 2019 10:14
GitHub: Montandalar
IRC: Blockhead256
In-game: Blockhead Blockhead256
Location: Land Down Under
Contact:

Re: Post your code!

by Blockhead » Post

jwmhjwmh wrote:
Fri Jul 08, 2022 15:59
Here's some code for scheduling a function to run at a certain interval. Unlike the "naive" implementation, this one ensures the average interval is very close to the desired interval, and is not biased above it.
Would registering multiple timers this way tend to result in them all trying to run at almost the same time? Wouldn't that cause lag spikes? How does it compare to sorcerykid's timekeeper?
jwmhjwmh wrote:
Fri Jul 08, 2022 15:59
Pipeworks uses it now.
Trialling it on your local copy? Otherwise I can't see it online, at least not among your various pipeworks branches on GitHub/TurkeyMcMac.
/˳˳_˳˳]_[˳˳_˳˳]_[˳˳_˳˳\ Advtrains enthusiast | My map: Noah's Railyard | My Content on ContentDB ✝️♂

User avatar
jwmhjwmh
Developer
Posts: 125
Joined: Sun Jul 18, 2021 14:10
GitHub: TurkeyMcMac

Re: Post your code!

by jwmhjwmh » Post

Blockhead wrote:
Sat Jul 09, 2022 03:26
Would registering multiple timers this way tend to result in them all trying to run at almost the same time? Wouldn't that cause lag spikes?
Probably. One would need some functionality to stagger the timers in order to prevent this.
Blockhead wrote:
Sat Jul 09, 2022 03:26
How does it compare to sorcerykid's timekeeper?
I haven't seen that before. It looks like it might track expiries in the same way as minetest.after, in which case (actual period) >= (desired period).
Blockhead wrote:
Sat Jul 09, 2022 03:26
jwmhjwmh wrote:
Fri Jul 08, 2022 15:59
Pipeworks uses it now.
Trialling it on your local copy? Otherwise I can't see it online, at least not among your various pipeworks branches on GitHub/TurkeyMcMac.
Here's the commit where the mechanism was introduced: https://github.com/mt-mods/pipeworks/co ... 81a70b8db0. This allows Pipeworks to run at an average interval of precisely 0.19, for example, as discussed in the associated PR.

Astrobe
Member
Posts: 571
Joined: Sun Apr 01, 2018 10:46

Re: Post your code!

by Astrobe » Post

I had a lot of trouble with that, so if you want a sure-fire (maybe) to force certain mapgen params:

Code: Select all

minetest.set_mapgen_params
{
	mgname="v7",
	flags="dungeons,caves",
}

minetest.register_on_mapgen_init(function(mgp)
	for k,v in pairs{
		mgv7_spflags="mountains,noridges,nofloatlands,nocaverns",
	} do minetest.set_mapgen_setting(k, v, true) end end)
I mention this because I once saw a Twitch streamer who had trouble when trying various games because they fiddled too much with the GUI and were starting game worlds with the "singlenode" mapgen. If your game is tied with (or simply just "best played with") a particular mapgen, do this.
The for loop is obviously not necessary in this case (that's a remain of my many tries), but can be useful in other cases.

User avatar
Skamiz Kazzarch
Member
Posts: 613
Joined: Fri Mar 09, 2018 20:34
GitHub: Skamiz
In-game: Skamiz
Location: la lojbaugag.

Re: Post your code!

by Skamiz Kazzarch » Post

A ~110 lines long proof of concept gesture detection mod.

What it does?
Adds a wand. Use the dig key to draw patterns in the air. Mod detects pattern (a sequence of "up", "down", etc...) and... prints it to chat, since it's just a PoC. But it should be simple enough to detect when a 'correct' pattern was entered and make something happen.
This is inspired by the game Arx Fatalis, where you would draw a sequence of runes to cast your spells. Though for my use case I was mostly thinking of it as a very simple way to trigger a wide variety of effects from a single item. An alternative input method, which for many patterns takes less then a second.

Why post the code here, instead of making it a proper mod?

Because while I think its a neat mechanic, I can't be bothered to spend several hours turning this into a fully featured API only for it to maybe get two responses if any, and then fall into obscurity. (yes, that's happened before -.-)

What will I need?

A texture for the wand.
A texture for the particles it places in the air to help you see what you are drawing.

Additional notes:
The gestures depend solely on the player yaw and pitch. Walking around while drawing them has no effect.
In this state it detects only 4 directions, but by changing one line you can change it to 8. It should be pretty obvious where.
On the off chance that this actually tickles someones fancy and they want help getting this to work / an explanation for 'whatever' in the code / anything else, feel free to PM me.

Code: Select all

local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)

local function get_eye_pos(player)
	local player_pos = player:get_pos()
	local eye_height = player:get_properties().eye_height
	local eye_offset = player:get_eye_offset()

	-- player_pos.y = player_pos.y + eye_height
	return player_pos + eye_offset + vector.new(0, eye_height, 0)
end

local casters = {}

local function start_casting(player)
	local name = player:get_player_name()
	casters[name] = {
		player = player,
		last_ver = player:get_look_vertical(),
		last_hor = player:get_look_horizontal(),
		last_dir = "none",
		sequence = {},
	}
end

local function finish_casting(player)
	local name = player:get_player_name()
	minetest.chat_send_all("Sequence was:")
	minetest.chat_send_all(table.concat(casters[name].sequence, ", "))
	casters[name] = nil
end


minetest.register_craftitem(modname .. ":wand", {
	description = "wand",
	inventory_image = "foo_wand.png",
	on_use = function(itemstack, user, pointed_thing)
		start_casting(user)
	end,
	-- on righclick release stored spell
})

local function angle_to_dir_4(angle)
	if angle < 45 then return "up"
	elseif angle < 135 then return "left"
	elseif angle < 225 then return "down"
	elseif angle < 315 then return "right"
	else return "up"
	end
end

local function angle_to_dir_8(angle)
	if angle < 22.5 then return "up"
	elseif angle < 67.5 then return "up_left"
	elseif angle < 112.5 then return "left"
	elseif angle < 157.5 then return "down_left"
	elseif angle < 202.5 then return "down"
	elseif angle < 247.5 then return "down_right"
	elseif angle < 292.5 then return "right"
	elseif angle < 337.5 then return "up_right"
	else return "up"
	end
end

local function get_dir(last_ver, last_hor, new_ver, new_hor)
	local d_ver = new_ver - last_ver
	local d_hor = new_hor - last_hor

	if d_ver == 0 and d_hor == 0 then return end

	-- acount for wraparound
	if math.abs(d_hor) > math.pi then
		d_hor = (-math.sign(d_hor) * 2 * math.pi + new_hor) - last_hor
	end

	-- angle is 0 at 12 o'clock and increases counter clock wise
	local angle = math.atan2(-d_hor, d_ver) + math.pi
	angle = math.deg(angle)
	return angle_to_dir_4(angle)
end

minetest.register_globalstep(function(dtime)
	for name, cast in pairs (casters) do
		local player = cast.player
		if (not player:get_player_control().dig) or player:get_wielded_item():get_name() ~= modname .. ":wand" then
			finish_casting(player)
		end

		-- particles
		local start_pos = get_eye_pos(player)
		local look_dir = player:get_look_dir()
		minetest.add_particle({
			pos = start_pos + look_dir,
			expirationtime = 2,
			texture = "foo_particle.png",
			glow = 0,
		})

		-- actual mechanics
		local new_ver = player:get_look_vertical()
		local new_hor = player:get_look_horizontal()

		local new_dir = get_dir(cast.last_ver, cast.last_hor, new_ver, new_hor)

		cast.last_ver = new_ver
		cast.last_hor = new_hor

		if new_dir and new_dir ~= cast.last_dir then
			table.insert(cast.sequence, new_dir)
			cast.last_dir = new_dir
		end
	end
end)

loosewheel
Member
Posts: 155
Joined: Mon Dec 28, 2020 01:19
GitHub: loosewheel
In-game: loosewheel

Re: Post your code!

by loosewheel » Post

This was just an experiment to try and (easily?) breakup a long process into smaller running parts. This worked for luajit but not lua 5.1 from the console. It worked in game (luajit). I tried to use debug.sethook to inject yields, but fails with yield across c func boundary error. So a function has to be called to yield from the added function.

Code: Select all

-- long_process --------------------------------------------------------


local RUN_INTERVAL = 0.1



local processes = { index = 0, current = 0 }
local after_job



local function run_processes ()
   if #processes > 0 then
      processes.index = (processes.index % #processes) + 1

      processes.current = processes.index
      processes[processes.index].resume ()
      processes.current = 0
   end

   if #processes > 0 then
      after_job = minetest.after (RUN_INTERVAL, run_processes)
   else
      processes.index = 0
      processes.current = 0
      after_job = nil
   end
end



local long_process = { }



function long_process.remove (id)
   if id then
      for i = 1, #processes, 1 do
         if processes[i].id == id then
            if processes.current == i then
               processes.current = 0
            elseif processes.current > i then
               processes.current = processes.current - 1
            end

            if processes.index >= i then
               processes.index = processes.index - 1
            end

            table.remove (processes, i)

            return true
         end
      end
   elseif processes[processes.current] then
      if processes.index >= processes.current then
         processes.index = processes.index - 1
      end

      table.remove (processes, processes.current)
      processes.current = 0

      return true
   end

   return false
end



function long_process.breaker (id)
   if id then
      for i = 1, #processes, 1 do
         if processes[i].id == id then
            return processes[i].breaker
         end
      end
   elseif processes[processes.current] then
      return processes[processes.current].breaker
   end

   return nil
end



function long_process.add (func, callback, ... )
   local args = { ... }
   local id = math.random (1000000)
   local thread


   local function thread_breaker ()
      coroutine.yield (thread)
   end


   local function thread_func ()
      local results = { pcall (func, unpack (args)) }

      return unpack (results)
   end


   local function thread_resume ()
      local results = { coroutine.resume (thread) }

      if coroutine.status (thread) == "dead" then
         table.remove (results, 1)

         if callback then
            callback (unpack (results))
         end

         long_process.remove (id)
      end
   end

   thread = coroutine.create (thread_func)

   if thread and coroutine.status (thread) == "suspended" then
      processes[#processes + 1] =
      {
         id = id,
         resume = thread_resume,
         breaker = thread_breaker,
         callback = callback
      }

      if not after_job then
         after_job = minetest.after (RUN_INTERVAL, run_processes)
      end

      return id
   end

   return nil
end



-- implementation --------------------------------------------------------


local long_id


local function test (count)
   local breaker = long_process.breaker () or function () end

   for i = 1, count, 1 do
      for j = 1, 100000, 1 do
         local s = "test"
      end

      if (i % 1000) == 0 then
         print ((i / 1000).." break")
         breaker ()
      end

      --if i == 3000 then
         --print ("remove")
         --long_process.remove ()
      --end
   end

   return "test returned"
end


local function test_callback (result, ...)
   local args = { ... }

   print ("test_callback", result, unpack (args))
end


print (test (10000))
long_id = long_process.add (test, test_callback, 10000)

--minetest.after (0.5, function () long_process.remove (long_id) end)
This is the output of the above test.

Code: Select all

1 break
2 break
3 break
4 break
5 break
6 break
7 break
8 break
9 break
10 break
test returned
1 break
2 break
3 break
4 break
5 break
6 break
7 break
8 break
9 break
10 break
test_callback	true	test returned
EDIT: removing the pcall from the thread_func and leaving the first return value from coroutine.resume seems to work the same but also works with lua 5.1

Code: Select all

   local function thread_func ()
      return func (unpack (args))
   end


   --local function thread_func ()
      --local results = { pcall (func, unpack (args)) }

      --return unpack (results)
   --end


   local function thread_resume ()
      local results = { coroutine.resume (thread) }

      if coroutine.status (thread) == "dead" then
         --table.remove (results, 1)

         if callback then
            callback (unpack (results))
         end

         long_process.remove (id)
      end
   end

Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot] and 24 guests