Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

LWSP upgrade for Luanti 5.17.0

Luanti's recent release of version 5.17.0 added a feature that validates ContentDB package author using a simple regex, but Luanti WebServices Proxy overloads the author field with the backend server connection information, so I had to update LWSP to fix this.

The validations fit entirely in this block, in file builtin/mainmenu/content/contentdb.lua

for i = #packages, 1, -1 do
    local package = packages[i]
    if not (type(package.author) == "string" and
            type(package.name) == "string" and
            type(package.type) == "string" and
            type(package.release) == "number" and
            package.author:match("^[%w _%-.]+$") and
            package.name:match("^[a-z0-9_]+$")) then
        core.log("warning", "ContentDB: Dropping invalid package: " .. dump(package))
        table.remove(packages, i)
    end
end

So it's lua, thankfully, which is way easier for me to read than C++. An easy fix would be to adjust the client lua file to add a colon and tilde in the package author regex.

package.author:match("^[%w _%-.~:]+$") and

But seeing as how nobody else will adjust their client configs to allow that, so I adjusted lwsp to split author and host+proto with _AT_ rather than ~, and also replace a colon with _colon_. It's longer and a little less pretty, but it matches the vanilla client expectations. Not everyone who might use lwsp will use a stackrpms build of Luanti.

Librewolf 154 problem and downgrade problems

I don't have a good story today. I was a few months behind on my Librewolf releases, so I wanted to work on that. So last week I got 154.0.1 packaged.

And today I finally installed it, and then I learned the settings panel is completely empty! It's broken. But the app compiles and operates. I needed to tweak some settings, and I couldn't find them in about:config, so I wanted to use the about:preferences. It's only broken in my build, which I'm sure has something to do with my debianized-firefox+librewolf-patches process.

So in the meantime, I downgraded back to 150.0.1 and restored my browser profile from backup (that I take monthly). But my browser had deleted the extensions for passkeys, and I need passkeys unfortunately. So I had to go find those again. Apparently, the files had been deleted from the profile directory. It was shockingly hard to get ahold of the .xpi files directly; the Firefox extensions website really wants to have the browser use the file rather than letting you download it: "The appification of stuff" instead of letting a power user download the file and use it.

When I was able to have it try to use the file (begrudgingly directly from the website), the browser said it was corrupted and would not work. So it didn't even save the file anywhere.

I started panicking, because I absolutely need a passkey for certain sites. They would not let me log in with a password.

Remember, this was the previous version of the browser that had worked 1.5 hours ago before updating.

I reset my browser profile to fresh and tried the extensions again; I can just import my history and bookmarks if I have to. Unfortunately, it still thought the files were corrupted. So I started investigating those. I eventually found a way to "install addons temporarily" which showed me one error message.

Internal error: Invalid string format contentSecurityPolicySandbox

Well, that string didn't exist anywhere in these xpi files, and also I hadn't changed any settings about that. Even a fresh 150.0.1 profile malfunctioned too.

I started poking at the patches (more) to see if anything was related to that string, and I couldn't find anything.

For some reason, when I blanked the profile a second time, then I could install the extensions again. And then when I restored my backup of the browser profile, all of a sudden it was back, with the extensions working.

So for now, everything is working for me again, but on an old version of the browser.

And I still have to fix the debianized librewolf build process so that the settings page works. I don't know what level of effort that will take. I don't want to give up on debianizing librewolf. I'm sure the Debian people have good reasons to debianize Firefox, and I want that, plus the librewolf bits.

Run thumbnailer for arbitrary paths

For some reason, it's complex to tell a thumbnailer program to index arbitrary paths, such as a single directory or even single file. I don't relish using dbus, but begrudgingly use it to accomplish some goals. I'm pragmatic, to a degree.

Apparently tools like tumbler need dbus to receive messages about what paths to index.

So I vibecoded some syntax for dbus commands and some filetypes for a find operation, and wrote some functions that I find useful.

files/2026/listings/thumbnail_functions.sh (Source)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#!/bin/sh
# File: thumbnail_functions.sh
# Location: https://bgstack15.ddns.net/blog/posts/2026/08/30/run-thumbnailer-for-arbitrary-paths/
# Author: bgstack15
# Startdate: 2026-08-17-2 19:29
# Title: Functions for Thumbnail Operations
# Purpose: Store all thumbnail operations in one location
# History:
# Usage:
#    source this file, and call the functions you want
#    . thumbnail_functions.sh
#    thumbnail_dir ~/Downloads
# Reference:
#    man busctl
# Improve:
# Dependencies:
#    A thumbnailer implementation, perhaps tumbler
# Documentation:
#    Running these commands in other windows:
#       busctl --user monitor org.freedesktop.thumbnails.Thumbnailer1
#       G_MESSAGES_PREFIXED= G_MESSAGES_DEBUG=all /usr/lib/x86_64-linux-gnu/tumbler-1/tumblerd
#    Bare command:
#       busctl --user call org.freedesktop.thumbnails.Thumbnailer1 /org/freedesktop/thumbnails/Thumbnailer1 org.freedesktop.thumbnails.Thumbnailer1 Queue asasssu 2 file:///home/bgstack15/Downloads/godzilla.jpg file:///home/bgstack15/Downloads/mary-had-a-little-lamb.jpg 2 "image/jpeg" "image/jpeg" "xx-large" "" 0
#       gdbus call --session --dest org.freedesktop.thumbnails.Thumbnailer1 --object-path /org/freedesktop/thumbnails/Thumbnailer1 --method org.freedesktop.thumbnails.Thumbnailer1.Queue "['file:///home/bgstack15/Downloads/godzilla.jpg']" "['image/jpeg']" "normal" "[]" "0"
#       WARNING: dbus-send does not correctly handle commas in the filenames. I was unable to discover how to escape the comma.
#       dbus-send --dest=org.freedesktop.thumbnails.Thumbnailer1 /org/freedesktop/thumbnails/Thumbnailer1 org.freedesktop.thumbnails.Thumbnailer1.Queue array:string:"file:///path/to/your/file.jpg" array:string:"image/jpeg" string:"normal" string:"leave-on-disk" uint32:0

thumbnail_dir() {
    _dir="${DIR:-${1:-.}}"
    _size="${THUMBNAIL_SIZE:-${2:-normal}}"
    _recursive="${RECURSIVE}"
    _max_depth="${MAX_DEPTH:-1}"
    if test -z "${_recursive}" ;
    then
        if echo " ${@} " | grep -qiE -e ' -r | --recursive ' 1>/dev/null 2>&1 ;
        then
            # safety max depth.
            _max_depth=10
        fi
    fi
    _infiles="$(
        find "${_dir}" -mindepth 1 -maxdepth "${_max_depth}" -type f \( \
            -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' -o -iname '*.gif' \
            -o -iname '*.bmp' -o -iname '*.tif' -o -iname '*.tiff' -o -iname '*.svg' -o -iname '*.avif' \
            -o -iname '*.heic' -o -iname '*.ico' -o -iname '*.jp2' -o -iname '*.jxl' -o -iname '*.apng' \
            \) -print0 \
        | sed -r -e 's/ /%20/g;' | xargs -0 -I{} printf '%s ' "{}"
    )"

    _uris="$(
        for _word in ${_infiles} ; do
            printf '"file://%s" ' "${_word}"
        done
    )"

    _mimetypes="$(
        for _word in ${_infiles} ; do
            _n="$( echo "${_word}" | sed -r -e 's/%20/ /g;' )"
            file -b --mime-type "${_n}" | xargs -I{} printf '%s ' "{}"
        done
    )"

    _len="$( printf '%s\n' "${_infiles}" | wc -w )"
    eval busctl --user call org.freedesktop.thumbnails.Thumbnailer1 /org/freedesktop/thumbnails/Thumbnailer1 \
        org.freedesktop.thumbnails.Thumbnailer1 Queue \
        "asasssu" \
        "${_len}" "${_uris}" \
        "${_len}" "${_mimetypes}" \
        "${_size}" \
        "\"\"" \
        0
}

thumbnail_file() {
    _file="${FILE:-${1}}"
    _size="${THUMBNAIL_SIZE:-${2:-normal}}"
    _mimetype="$( file -b --mime-type "${_file}" 2>/dev/null )"
    _file="$( echo "${_file}" | sed -r -e 's/ /%20/g;' )"
    busctl --user call org.freedesktop.thumbnails.Thumbnailer1 /org/freedesktop/thumbnails/Thumbnailer1 \
        org.freedesktop.thumbnails.Thumbnailer1 Queue \
        "asasssu" \
        "1" "file://${_file}" \
        "1" "${_mimetype}" \
        "${_size}" \
        "" \
        0
}

So now I can run the following, to force the thumbnailer to interpret a directory.

. ./thumbnail_functions.sh
thumbnail_dir ~/Downloads

mcl_villager_cheats now includes a "reload trades" button

I was inspired by some recent conversations about an upcoming new Luanti mod about randomizing the trades a villager offers, and I wrote my own implementation! It involved copying the entirety of the villager code from Mineclonia (ContentDB) and adding a small patch. Well, and the logic for changing the trades too.

The concept of rotating trades of a villager can be done in-game, for a villager who has never traded, by removing his job block and replacing it. This button short-circuits that. And my implementation leaves it possible even after the villager has participated in a trade. So yes, it's cheating, but that's in the name of the mod. I don't even have a configuration setting for disabling it if the villager xp > 0 (that is, he has traded at all). I could add it, if anybody is interested.

files/2026/listings/diff-villager.patch (Source)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Date: 2026-08-11
Message: The difference between Mineclonia villager.lua and mcl_villager_cheats villager.lua
Last-Version: 0.123.0
Author: bgstack15
Command: diff -aur ~/.minetest/games/mineclonia/mods/ENTITIES/mobs_mc/villager.lua ~/.minetest/mods/mcl_villager_cheats/villager.lua
--- .minetest/games/mineclonia/mods/ENTITIES/mobs_mc/villager.lua	2026-07-26 14:20:37.152618130 -0400
+++ .minetest/mods/mcl_villager_cheats/villager.lua	                2026-08-11 11:52:24.416485282 -0400
@@ -711,6 +711,12 @@
  list[current_player;main;3.97,7.98;9,1;]
 
 ]]
+-- stackrpms,6
+fs_footer_template = fs_footer_template ..
+	"image_button[4.0,0.2;1.0,1.0;mcl_villager_cheats_reload.png;reload;]" ..
+	"tooltip[reload;Randomize available trades" ..
+	core.colorize("#47f424","\n(" .. core.get_current_modname() .. ")") ..
+	";#000000;#ffffff]"
 
 -- arg 1 = %s = wanted
 -- arg 2 = %s = wanted tooltip
@@ -903,6 +909,15 @@
 
 core.register_on_player_receive_fields (function (player, formname, fields)
 	if formname == "mobs_mc:trading_formspec" then
+		--stackrpms,9
+		if fields.reload then
+			local trader = trading_players[player]
+			if trader and is_valid (trader) then
+				mcl_villager_cheats.reload_trades(trader)
+				local entity = trader:get_luaentity ()
+				entity:reload_trades()
+			end
+		end
 		if fields.quit then
 			return_fields (player)
 			local trader = trading_players[player]
@@ -942,6 +957,11 @@
 		type = "detached",
 		name = inv_name,
 	})
+	-- stackrpms,5 Need to remove the previous inventory, so this new inventory
+	if inv then
+		core.remove_detached_inventory(inv_name)
+		inv = nil
+	end
 	if not inv then
 		inv = core.create_detached_inventory (inv_name, inv_class,
 							  playername)
@@ -6783,4 +6803,5 @@
 -- Villager spawning.
 ------------------------------------------------------------------------
 
-mcl_mobs.register_egg ("mobs_mc:villager", S("Villager"), "#563d33", "#bc8b72", 0)
+-- stackrpms,2 Do not run this again
+--mcl_mobs.register_egg ("mobs_mc:villager", S("Villager"), "#563d33", "#bc8b72", 0)

Mineclonia in Devuan Ceres needs SDL2

In Devuan Ceres (unstable), I had a problem with Mineclonia (my build of Luanti). Shift+left click would not move items in the inventory screens. A shift+left click normally moves the entire stack (to whichever destination). Shift+middle click still works. So somehow it's just shift+left click.

I was not the first to discover this bug.

Come to find out, in Devuan unstable, libsdl2-2.0-0 version 2.32.70+ds-1 is a sdl2-compat layer for sdl3. That is, it uses SDL3 but with the bindings for sdl2 so an application can use it like it uses sdl2.

Apparently I wanted to install libsdl2-classic which is real SDL2. And then I need to add this to my invocation of my game:

SDL_DYNAMIC_API='/usr/$LIB/sdl2-classic/libSDL2-2.0.so.0' mineclonia

I can live with that. I'm sure libsdl2-classic will go away eventually, but then also eventually hopefully they'll fix their bug in SDL3.