Subsections

2025-07-28 Creating the Minetest Tinctures mod

Minecraft's enchanting system does not look particularly interesting. There's also something blasphemous about having to spend character levels for enchantments; character attributes and equipment attributes should generally stay separate, and levels are usually permanent.

A much more exciting system was to be found in the text-based MMO "Revelation: Lands of Kaldana". Items known as "tinctures" would sometimes drop from high-level enemies. Applying the tinctures to your equipment would either give it a permanent stat increase or cause the equipment to be destroyed in an explosion. Besides being a more exciting system, the system also allowed for theoretically infinite but pragmatically finite progression due to the exponential difficulty of applying multiple tinctures to an item. It gave players a reason to farm bosses after obtaining the boss' gear since players could use the extras for tinctures. The system also prevents the unimmersive feeling that comes from selling the Epic Sword of Total Domination to the local grocer since, again, the extras are used for tinctures.

While there are no doubt Minetest mods which add systems similar to Minecraft's enchantment system, I figured it'd be much more fun to create a mod with Revelation's tincture system instead. I could also have a bit of fun creating a 3D explosion, too.

Implementation

With enough effort I actually was able to figure out everything needed to implement the Tinctures mod. There were a few rough points, but overall I found the modding API to be quite capable. The below details are filled in from my notes which date back almost two four years at their oldest, so I might get some of the details incorrect.

In Revelation, different colors of tinctures affect different stats. Since the default Minetest game isn't a fully-fledged RPG there aren't nearly as many stats to increase, but there a few. The first stat to implement would be mining speed & damage ("power") from red tinctures.

Adding red (and brown) tinctures

Before I could attempt to implement stat increases, though, I had to figure out how to trigger tincture usage. The most obvious method was to set the item's on_use() method so that left-clicking with a tincture in hand would apply the tincture to... what, exactly? I had no idea how to pop up any type of selection menu, so that was out. As a quick hack, I decided to use the first item in the player's wield inventory. Good enough, but how to figure out if the player is actually wielding something that can be tinctured? Rather than figure this out now, I observed that pickaxes belonged to a pickaxe group and checked to make sure that the item was a pickaxe. Good enough for now, as my current goal was to see whether or not it would be feasible to actually apply tinctures at all.

As for actually increasing the stats, some digging showed that tools had a number of fields known as capabilities. The problem was that these capabilities were determined at registration time, which was when the game would be initially loaded. Due to the combinatorial explosion of different tinctures that could be applied to even a single tool, it would be impractical to pre-register the needed combinations ahead of time. A gross but possibly workable solution would be to create a pool of pre-registered tools to draw from and update dynamically with override_item, but experimentation showed that this appeared to update the server but not the client. Registration was a dead end.

Thankfully, further searching revealed the get/set_tool_capabilities() methods. Now, the actual layout of capabilities is rather complex, involving "groupcaps" (capabilities depending on the type of material being mined), levels (of the material being mined), and actual mining time. For my purpose it was sufficient to find every groupcap and level and decrease its dig time by 10%. However, it wouldn't be sufficient to just decrease dig times by 10% and call it good; how would subsequent tincture applications be handled? I didn't want to compound tinctures on top of each other, so I needed a way to keep track of tincture counts, plus the original mining times for mathematical convenience. Helpfully, items had a "meta" object which could contain entries for dynamically-defined keys. Keeping track of tincture counts and original mining times was then simply a matter of defining and using the appropriate metadata fields at tincture use time.

Once I had this basic functionality implemented and proven working the next steps were to fix up the implementation a bit. One mistake I blundered into was assuming that all 3 "ratings" for a groupcap would be defined when in fact items like the wood pickaxe had only one level defined, meaning they could not mine lower-rated (more difficult to dig) nodes like obsidian; this was easily fixed with a dynamic loop. As for identifying any type of tool and not just pickaxes, I looked to the get_tool_capabilities() method; unfortunately, the method didn't actually identify whether or not the item was a tool or not, but I observed that non-tools had an oddly_breakable_by_hand capability, so I checked for absence of the capability in order to determine that the item was a tool (later on I learned about the minetest.registered_tools global). Now, in order to verify that tincture application was working as intended, I had been automatically logging the capabilities before and after tincture application. While this brute-force logging worked for development, I imagined that server admins wouldn't appreciate the noise in their logs and the messages were only helpful when applying tinctures and not, say, a few weeks later when I once again found time and energy for more development work. The capability values weren't something succinct enough for the item description (plus I hadn't gotten to that point in development yet) and were not something an average player would want to be exposed to, only a power user would. So I used the obvious feature for power users: the command line; I added a command named tinc_caps which would send the capabilities of the player's currently wielded item to said player via minetest.chat_send_player and removed the ad hoc server log messages. I also cleaned up the other player-specific messages to use minetest.chat_send_player instead of the server log as well.

Adding brown (durability) tinctures used mostly the same logic as with red tinctures, except each capability had an overall (instead of per-rating) uses value that I needed to increase, in this case by 100%; a bit of boilerplate for the registration and the new tincture type was easily added!

Adding a description

While I now had increased dig times through tincture application and the ability to manually query tool capabilities, the process of querying capabilities for each tool was cumbersome, so the next step was to change the tool's tooltip in order to show applied tinctures. Rather than list the number of applications with Arabic numerals, I decided to use Roman numerals for the aesthetics. It turns out that converting a number to Roman numerals is decently tricky, but after bashing my face against the keyboard a while I cranked out something that worked... I think. The tooltip that appeared was controlled by a string in the metadata field named description. Before modifying this string, however, I stored a copy of the original in a new field named tinctures:description so that I could easily construct a new string from the original. Then I used minetest.colorize() in order to append add a teal-colored (like World of Warcraft enchantments) line for each type of tincture applied along with its count, resulting in, for example, this:

Figure: Example tooltip of a tinctured item
Image 2025_07_28_tinctures

The effect looked just as I'd hoped it would!

Adding weapon damage

In addition to mining speed, the red (power) tincture was also supposed to add damage. Similar to mining speed, the damage done by a weapon was being defined in a tool capability; this time the damage groups capability. Examining the item registrations in the "default" mod showed that only the fleshy damage group was defined, but presumably it'd be possible to add elemental groups such as "fire" and "ice"! While increasing each damage group's value was simple enough, a complication arose in that the value appeared to be an integer and not a float, meaning that the minor increase which a single tincture would provide would get truncated into nothing. Multiple tinctures would eventually cause the integer value to increment, but until that threshold was reached there would be no tangible benefit. This was most extreme in the case of low-damage items such as the wood sword, where it would take 5 tinctures in order to see the damage increase from 2 to 3!

One idea I played around with was the notion of "carry" damage. The idea was that sub-integer portions of damage would be "stored" in the weapon and then applied when they crossed the integer threshold. For example, consider a weapon that did 3 damage and had one power tincture applied; rather than doing 3.3 damage per strike, which did not appear to be possible, at each strike the weapon would have done damage while "storing" sub-integer portions of the damage in it:

Figure: Theoretical example of "carry" damage being applied across 10 strikes of a weapon with non-integer damage
\begin{figure}\begin{tabular}{c\vert c\vert c\vert c\vert c\vert c\vert c\vert c...
...9 & 0.2 & 0.5 & 0.8 & 0.1 & 0.4 & 0.7 & 0.0 \\ \hline
\end{tabular}
\end{figure}
Clever players would of course figure out ways to "charge" their weapons before important encounters, but I don't think that would cause game-breaking behavior, especially since the damage would even out over time against an enemy with significant health (such as a boss). Unfortunately for me, there did not appear to be a portable way to implement the idea. minetest.register_on_punchnode appeared to work for digging nodes only, not punching entities. Looking at the cme mod, it appeared that the module actually overrode on_punch(), so any modification of damage application would have to be done in cme, or, assuming other entities mods used the same method in order to implement damage, whatever entities mod was in use, and not tinctures. I also noticed that the on_punch() method was also used in order to override the application of wear to a tool, meaning that there did not appear to be a way to have brown (durability) tinctures work without cme-specific support, either. Since there didn't appear to be a way to apply "carry" damage, I left the code as-is; players would just have to live with the fact that, depending on the weapon, they'd need to apply a certain number of tinctures before seeing any additional damage.

Adding the efficiency tincture

I thought it would also be worthwhile to create a tincture which can mine extra ore for the player, kind of like the luck enchantment in that other, unnamed, voxel-based game. Rather than a percent chance of extra ore, though, I decided that, for each ore mined, and based on the number of tinctures applied, a set percentage of that ore would be "stored" in the tinctures tool; when that percentage reached 100%, the player would get an extra ore! For the color of the tincture, I chose green. As with the tinctures themselves, the "extra ore" would be stored in the item metadata. In order to apply the desired effect, I had to modify how nodes were being dug; luckily, the Minetest engine provided a convenient register_on_dignode() method which I could use in order to hook into the digging logic. The basic premise of the implementation was straightforward: get the player's weapon, check for efficiency tinctures, check if the item dug was an ore, add extra ore and drops as required.

Naturally, a few things made this not as straightforward. First, in order to check that the item dug was an ore, I initially used the minetest.registered_ores map. However, it turned out that, in addition to what I considered ores, nodes like dirt, sand, and mese blocks were also registered as "ores"; this apparently had to do with how blobs of them were spawned underground in the map. The problem with this is that these "ores" drop themselves, meaning that a player could use a “place, dig, and repeat” cycle in order to generate an infinite supply of ores! There didn't appear to be any way to differentiate between these "ores" and actual ores based on their ore registration, so I had to figure out some other method in order to identify which dug nodes were ore. Short of a hardcoded list (eww), the only thing I found consistent with actual ores was that their names were all prefixed as stone_with_, so, I checked if the dug node's name matched this prefix, and, if so, considered the dug node an ore. Not pretty, but it worked. Next, there was the matter of how to give the extra ore to the player; while I could insert the ore into the player's inventory, that might not work if the player's inventory was already full. Searching the docs showed that Minetest helpfully provided the inventory:room_for_items() method that I could use in order to check whether or not the player's inventory would be able to hold the item; I then used this method in order to determine if the module would call inventory:add_item() in order to add the item to the player's inventory, or if the module would instead call minetest.add_item() in order to drop the item at the player's location. Last, when determining exactly which extra ore to generate, I used the minetest.get_node_drops() method, but one thing that wasn't clear from the method was how probability-based drops were handled. Would each call run the probability check so that multiple calls might return different results? Was the function run once before the dignode hooks so that the return values would be the same while the hooks were run? I'd guess the former, but I didn't try and find out and instead relied blindly on the provided return value. Good enough for now; the implementation worked and the efficiency tincture was implemented!

Adding brightness levels

The original incarnation of tinctures also contained various "strengths" of tinctures which were denoted by the brightness of a given tincture. The brighter the tincture, the more powerful the tincture, but also, according to hearsay, the higher the chance of an explosion. The brightness levels were, in order from least to most powerful: "dull", "bright", "brilliant", and "blinding". An effective use of this mechanism was to limit certain powerful stats to a minimum brightness of tinctures; for example, a 0.1 attack speed tincture (the lowest possible increment) required a "brilliant" tincture and did not have a "dull" or "bright" equivalent. I didn't have any such stat that I wanted to limit, so the brightness levels would just make for extra stats. As for the explosion probability, I didn't know what the original incarnation did, but my first intuition was to make the probability proportional to the extra stats; a tincture twice as powerful would be twice as likely to explode! While this makes sense, it runs into the problem that one regular-power tincture and one double-power tincture are equivalent in risk, making the brightnesses levels somewhat redundant. A way to fix this might be to decrease the risk from a brighter tincture, but this would render the dimmer tinctures semi-worthless since players would be inclined to only use the brighter tinctures in order to get the best weapons due to their lower overall risk; making the brighter tinctures higher in risk would instead make them semi-worthless in turn. Without the brightness levels gating a certain stat, brightness levels kind of felt like a feature looking for a use case. Regardless, I went ahead and implemented the feature. The actual implementation of the brightness levels was straightforward.

Adding explosions

This part was a lot of fun. While Revelation's red, bolded textual description from someone destroying their prized item was always amusing, having an interactive 3D space to simulate an actual explosion in would make for a more exciting experience. It would also require a fair bit of work, including: damage, knockback, sound, a crater, and debris. For extra oomph, I decided to have most of these increase in intensity based on the total number of tinctures applied to the weapon.

Adding damage was easy: call the method player:get_hp() in order to get the player's current health, subtract the damage done, then set the player's current health to the difference with player:set_hp(). Nothing in programming is ever that easy, though, and there was a slight (read: "major") problem with this. The on_use() callback returned an ItemStack which replaced the player's ItemStack; nominally, this meant subtracting a tincture from the ItemStack and returning the result. This worked, except, if the player died as a result of player:set_hp() called from on_use(), then the player would first drop all of their items and then on_use() would return the ItemStack into the player's inventory; this meant that if a player had 10 tinctures and died as a result of using one then there would be 10 tinctures left where the player had died and 9 on the dead player. Infinite tinctures! A few solutions such as spawning a delayed and external damage source or manually reaching into the player's inventory, removing a tincture, and then returning nothing from on_use() might have worked, but they were all relatively complex so I instead decided to leave the player (barely) alive for now and focus on the other work first.

For dramatic effect it was also absolutely necessary to knock the player back on a tinctured explosion. This would require rigorous and strenuous testing, though, by which I mean setting an artificially high tincture level and then having my character fly halfway across the map when the inevitable tincture failure happened. I first needed a way to set an artificially high tincture level, though, and a new chat command was the way to go. I'd already familiarized myself with the minetest.register_chatcommand() method in order to display tool capabilities, so I re-used this knowledge in order to create various tinc_set_EFFECT commands. For these commands I also added a parameter for the desired tincture level and required the debug privilege in order to invoke the command, but wasn't particularly satisfied with use of the debug privilege since it was normally just for viewing debugging data and not modifying the world; it was good enough for now, though. After my, ahem, testing infrastructure was in place, it was time to actually knock the player back. I decided to knock the player a little bit upwards and also a little bit sideways, adding a slight variation to each; 10 to 15 degrees vertical and up to 15 degrees to either side. The Minetest engine provided a convenient player:add_velocity() method in order to actually knock the player back, but first I had to calculate what velocity to apply to the player! I first used the player:get_look_horizontal() method in order to get the yaw in radians, then added somewhere between -15 and 15 degrees using a hardcoded conversion. Since the actual velocity would be in 3D, I next used minetest.yaw_to_dir() in order to convert to Minetest's helpful Spatial Vector class. In order to launch the user upwards I then used the vector:rotate() method, followed by vector:multiply() in order to increase the knock back based on tincture count, and finally called player:add_velocity() in order to actually knock the player back. After some playing around with vector magnitudes, I eventually got the knock back to where I wanted; a slight nudge for barely-tinctured items and a bit of oomph for highly-tinctured items. Improbably-tinctured items (1,000+) hilariously send the player flying across the map until they hit a load boundary and come to a dead stop before falling to their death on solid ground or a long swim home from the middle of the ocean.

Next up was to add a sound for the explosion. I first grabbed the explosion sound from the TNT included in the default game and then added a call to minetest.sound_play() in order to play the sound at the player's location. This worked as expected, but there was a problem; the explosion sounded too... "bassy". It just didn't work. So I went to Freesound in search of explosion sounds. After much searching, sampling, and testing I eventually settled on #473941: impact_.wav. I liked #625114: EXPLDsgn_Powerful Explosion 2.1_EM_(28lrs).wav, too, but it was for non-commercial use only and I didn't want to needlessly cripple my mod that way. A few others which I tested were:

An explosion wouldn't be any fun if it didn't also leave a crater, so the next step was to figure out how to remove nodes in order to leave a crater. Admittedly, I got really lazy with this code. The way to remove a node without destroying it is to "dig" it, so I called minetest.dig_node() on the nodes which I wanted to remove. In order to actually find the nodes I needed, well... I added two to the overall radius of the explosion, then iterated in a cube (!) over each node and removed ones that were within the explosion radius. Not very efficient. Nodes which were less than one node away from the radius were removed with a linear probability based on how close they were to one node away; a better implementation might determine a set number of nodes to remove based on the volume of the explosion which doesn't cleanly fall into its radius and then consistently remove that number of nodes. I made the volume of the explosion increase by 10% per tincture, meaning the increase in radius would be inverse cubic. As for the "dug" nodes from the explosion, I left them fall to the ground for now, though it might be fun to scatter them away from the explosion in the future...

Finally, I wanted some kind of debris effect from the exploding tool. As I had almost zero artistic talent, this would not be so easy. Naturally, I first tried the smoke from the TNT mod but the particulate matter felt excessive; however, the implementation of the smoke gave me a clue that a particle spawner might do the trick. It would make sense that chunks of an exploding tool would rapidly scatter away from the player, so I used minetest.add_particlespawner() in order to spawn particles radiating outwards from the explosion location. As for the particulate matter itself, the particle spawner had a node parameter which would create particles based on the node texture; the problem for me was that tools weren't nodes. Attempting to use the inventory image in the texture parameter resulted in "particles" which consisted of the entire tool image! There also didn't appear to be any way to specify random pixels from an image (or at all) without registering an entire node. Well, that wasn't ideal, so what I did was to shrink the image down to a small size so it both resembled the weapon and particulate matter; it didn't really make sense that a tool would explode into 64 smaller versions of itself, but one had to squint to see it!

With the damage, knockback, crater, sound, and particle spawner implemented, the explosion was complete! Below is an example of an explosion from a failed single tincture level:

Figure: Explosion from tincture level 1. Note: Taken using tinctures v0.2.
Image 2025_07_28_explosion_level_1
For fun, here is an example from an improbable 1,001 tincture level:
Figure: Explosion from tincture level 1,001. Note: Taken using tinctures v0.2.
Image 2025_07_28_explosion_level_1001
Ridiculous! Now that destruction was implemented, the next step was a bit of a thematic opposite as it was related to preservation: letting a player keep their tinctures after a weapon breaks from wear rather than losing them with the weapon.

Handling Tool Breakage

Having a permanent status boost on a tool isn't much use if the item quickly breaks. I wanted some way to preserve the successful tincture effect even though the player would have to repair the tool. Relying on the player repairing the tool in time didn't seem fair; one stupid mistake and a tinctured tool would be gone forever!

Unfortunately for me, there didn't seem to be any kind of on_break() method which I could override. There was after_use(), but that would require overriding every item during game load time. By using the minetest.registered_tools table I could iterate over the registered tools, but that was empty by default! It turned out that what was in the registered tools table was dependent on the module load order. The way to ensure that the default module's tools were in the table was to add a dependency on the default module by editing mod.conf and adding the optional dependency with optional_depends = default. This was hardly ideal since any module that registered tools would have to be declared as an optional dependency in order for tinctures to work, but that was okay for now. Even so, how exactly was I to override after_use()? If I let the "wear" reach zero then the tool would be instantly destroyed along with its tinctures; if I left the wear above zero then the player could continue to use the tool as before even though it should have been broken. I needed some kind of "inert" state for the tool.

After much contemplation, I decided to have the broken tool drop what was essentially a "memory" of its previous state. The memory would contain the tinctures applied to the tool as well as exactly what tool the tinctures had been applied to. I would call this memory an "essence", and it would be a regular item for players to use by combining it with the same kind of tool in order to regain the previous tool's tinctures. Generating the essence on breakage in after_use() was actually a little tricky. Since I was overriding the default usage behavior, I had to manually call the itemstack:add_wear() method in order to apply wear. If the wear fell to zero then the tool's metadata, which included the tincture metadata, would no longer be available, so I had to first extract an essence before calling the method to add wear from digging; after calling the method, I'd check the wear of the tool and, if non-zero (tool not broken), would return the tool, otherwise I'd return the previously-extracted essence for the tool.

Once I'd gotten the essence-generation working successfully, the next step was figuring out how to let players actually apply it to their tools. Rather than re-use the on_use() hack with tincture application in order to affect the first item in the player's inventory I decided to see if I could use the regular crafting grid. Creating the crafting recipe turned out to be a simple matter of using the minetest:register_craft() method in order to associate each essence with its corresponding tool, but this wasn't sufficient to actually apply any tinctures because by default it would just craft an untinctured tool. The actual application of tinctures would have to be done in minetest:register_on_craft() and its sister function minetest:register_on_craft_predict(). Inconveniently, the callback definition did not include the invoking recipe, so the first step of the callback implementation was to determine if the recipe was an essence recipe and the location of the tool and the essence by iterating through the crafting ingredients; after that it was simple enough to apply the essence to the tool and return the tinctured tool.

That worked for the simple case. What if the player tried applying an essence to an already-tinctured tool? The existing tinctures would have been replaced by the new tinctures, which is probably not what the player would want, especially if they had accidentally applied a lesser essence to their best tool. I tried to see if it was possible to block a recipe in the callback, but the only options I could find were to continue with the recipe unmodified (by returning nil) or modifying the result. My second guess was to just use the essence representing the most tinctures and to destroy the other essence, but after some reflection that seemed like just another landmine for players. Eventually I came to the conclusion that applying an essence to an already-tinctured tool would have to generate a new essence from the old tinctures. This would lead to the weird behavior that a player could hot-swap essences on a single tool, but I couldn't think of any major advantage this would offer since the essence would take up the same space as an actual tool anyways. Since the new essence wasn't a registered result of the craft recipe I had to manually add it to the player's inventory in the callback. I also had to take care to only manually add the new essence during actual crafting and not during craft prediction. This took care of already-tinctured tools, but one more challenged remained: tool repair.

The toolrepair recipe type in Minetest was... interesting. The recipe would combine two tools of the same type into a single tool, reducing the wear such that the new wear represented the remaining uses of the two tools combined, plus a little extra reduction in wear for a few extra uses. This recipe raised a number of interesting cases for the tincture crafting callback. The first two cases were obvious: two tools without tinctures should return a repaired tool without tinctures, and one tool with tinctures and one without should return a repaired tool with tinctures. How to handle two tinctured tools, though? As before, I wanted to prevent players from accidentally destroying their tinctured tools, but there was no realistic way to combine two tinctures into one tool. Taking a cue from applying essences to already-tinctured tools, I decided to have one of the essences apply to the repaired tool and place the other essence in the player's inventory. Which essence would apply depended on its position in the recipe, making the recipe not-quite-actually "shapeless", but, whatever (the code to determine it was a repair recipe was enough of a pain): a player could just subsequently apply the other essence to the repaired tool if they made a mistake. With tool repair finished, tinctures were properly handled in all crafting recipes!

While the essence functionality was correct, I'd been lazy and copied the essence image from the tool icon, making it impossible to visually distinguish an essence from a tool without hovering over the tool in order to view the tooltip. I needed a way to visually distinguish essences from tools in the tool's icon. After some digging I found this really neat feature called texture modifiers that allowed me to generate textures at runtime through my mod's code. What I wanted to do was create a sort of transparent and shimmering image of the original tool in order to both clearly identity which tool the essence applied to while also clearly distinguishing the essence from a usable tool. For the glow, I used the venerable GIMP's supernova effect, but at first I thought that transparency wouldn't work at all given the icons I was seeing:

Figure: The essence icon for a diamond pickaxe on an old machine which doesn't support transparency.
Image 2025_07_28_essence_icon_noalpha
...though when I tested on my newer desktop at home I found that transparency worked properly for the icons. On neither machine did transparency for the wielded item as the wield item's pixels were either a solid white or empty depending on how transparent the pixel was, but the effect was still good enough since players wouldn't normally be wielding an essence. Next, in order to emphasize the glow and de-emphasize the tool I used the combine texture modifier in order to place the 16x16 tool icon in the middle of the 32x32 glow icon, making the tool's icon smaller than it would normally be. Then I added a brighten texture modifier in order to make the tool appear somewhat ephemeral since I didn't believe transparency would work. The result looked like:
Figure: The essence icon for a diamond pickaxe.
Image 2025_07_28_essences
While not a stunning breakthrough in graphic design, the result was good enough to ship!

As a final piece of clean-up, I noticed in testing that replacing the broken tool with an essence prevented the item breakage sound from playing. The workaround was simple enough: when the would-be broken tool is replaced with an essence, check the item definition for a breakage sound and, if it exists, play it. Perhaps someday the engine behavior will change and the sound will begin playing twice, perhaps this will manifest in a weird echo from a playtime offset or perhaps it will just sound louder than it should. Regardless, at long last I had a solution to handle item repair and breakage; it was tricky and a bit hacky, but functional. This was the last of the functionality that I had any real concerns about being able to implement, so having it all working was a big relief. Next it was time to pay off some technical debt, and then, finally, prepare for the first release.

A brief durability revisit

This one really doesn't deserve its own section, but it doesn't fit in the others... Anyways, remember how I wasn't able to have durability apply to weapons because of how durability was subtracted in the mobs mod's on_punch() override? Well, at some point a punch_attack_uses attribute was added to the tool capabilities. It seemed that a mobs mod would have to actually make use of the attribute for it to have any effect, but increasing it as part of the durability tincture effect didn't seem to hurt anything, so I went ahead and committed that change.

Efficiency Tincture sound effect

One thing that felt missing from the efficiency tincture was a jolly sound effect upon attaining an extra ore. As with the explosion sound effect, I headed over to Freesound in order to find a good sound effect. I tried about 8 various samples in game before settling on the winner: Most remarkable to me was how important duration was. A sample might sound like a decent "bonus" effect on its own, but when placed in the context of an extra ore being mined it would feel clownishly overblown. Eventually I found a sample that was short, sweet, and somewhat subtle. As before, playing the effect was a simple matter of converting the file to OGG and then calling minetest.sound_play(). I made sure to only play the sound once no matter how many extra ore were added in order to prevent the sound effect from becoming unbearably loud.

Making better tincture icons

When I first started implementing tinctures I needed an icon right away, but I'm not much of an artist and didn't want to spend a bunch of time creating icons when I wasn't sure if I'd even be able to implement tinctures properly. So I did a quick and dirty job of it. What I had needed was some kind of small, cylindrical container containing a colored liquid and sealed with a cork cap. This was what I came up with:
Figure: The very first tinctures icon. Appalling, no?
Image 2025_07_28_tincture_icon_original
You can't really call it a vial... unless you call it an "American" vial because it's so fat. Anyways, besides its rotundness, the liquid in it is too homogeneous and saturated, and the whole thing is way too large. This was what I looked at during all of development, even for the supposedly "green" and "brown" tinctures, and certainly regardless of brightness. It very obviously needed to be replaced.

An unexpected benefit of my previous work from coding essences was that I could use my knowledge of texture modifiers when designing the new tincture graphics rather than naively creating a separate graphic for each color; it also meant that I could break the images into separate files and combine them in the Minetest engine rather than creating separate layers in GIMP. Since the vial would be static and the fluid inside it would vary depending on the type of tincture, I split the two into separate images so that I could apply color to one without affecting the other. This time I made the vial really small and also dithered the colors a bit in order to make the icon feel more natural and less artificial. In order to emphasize the magic nature of the tinctures I also used GIMP to generate a glow effect via Filters -> Light and Shadow -> Gradient Flare (Select: Default, X: 7, Y: 8, Vector length: 1, Angle: 249, Radius 3, 4, and 5 (dull, bright, and brilliant, respectively)) followed by a Colors -> Desaturate -> Desaturate. Thus I had the following set of images:

Figure: The set of images used to create the tincture icons. From left to right: the vial, the fluid, glow (dull), glow (bright), glow (brilliant). Note: your browser may anti-alias the images into a blur.
Image 2025_07_28_tincture_vial Image 2025_07_28_tincture_fluid Image 2025_07_28_tincture_glow_dull Image 2025_07_28_tincture_glow_bright Image 2025_07_28_tincture_glow_brilliant
With the three images ready, generating the new icons was a simple matter of using texture modifiers in order to colorize the fluid and glow and then combine all of the images together. For the actual color I normally used the Cascading Style Sheets (CSS) color name, except for some reason their brown is more of a reddish-clay color so I used saddlebrown instead. Now the tincture icons looked like:
Figure: The tincture icons generated from the above set of images. From top to bottom: red, brown, and green. From left to right: dull, bright, and brilliant tinctures.
Image 2025_07_28_tincture_icons_dynamic
The glow made use of the alpha channel, which didn't work on older hardware, but I decided it looked decent even without the alpha channel. In the end, creating better icons turned out to be easier than expected!

Refactoring tincture application into a recipe

Remember how when I first implemented tinctures I had the player apply the tincture by wielding and then using it and the tincture would automatically select the leftmost item in the wield inventory? Yeah, that was a hack which needed to be fixed, and, with my newfound knowledge of crafting recipes which I learned from implementing essences, a fix which I was ready to attempt! The first thing I did was slightly new: I added a group named tincture to each tincture's item definition. I also removed the on_use() callback since I would no longer need it. Then, when I was iterating through each tool in order to create essences for them, I now also added a craft recipe whose ingredients were the tool and the tincture group. As with essences, the craft recipe for tinctures wouldn't know how to apply the tincture effects ahead of time, so I had to move the tincture-application logic to the existing crafting callback. One trick with this was that I needed to destroy the tool on tincture explosion. Returning nil wasn't an option since that would return the unmodified tool but I needed it destroyed. What did work was to instead set the tool's wear to max (a.k.a. "broken") and return that! As a bonus, since I no longer had to worry about returning a partial itemstack of tinctures, I could now kill the player during the explosion! Don't worry: as long as you fill your health bar before tincing you won't ever be in danger of being one shot. Probably. As with refactoring the tincture icons, refactoring tincture application into a recipe had turned out to be pretty straightforward!

Pre-reveal clean-up

With all the big functionality changes done, it was time for a bit of clean-up before v0.1. I added a README.md, a description.txt, and even a screenshot.png, though I had no idea how to test it in the "mod store". I thought about changing the license to a "lesser" version of the AGPLv3 but wasn't able to find one... I moved the media directory to textures and made the depends.txt dependency on default optional. The ability to set tincture level on weapons was previously tied to the debug privilege so I created a special tincerer privilege for the ability. For good measure I followed rubenwardy's advice to lint with luacheck, which did catch a few things after I silenced its line-length warnings.

Migrating away from GitHub is one of those things I ought to get around to doing in my copious free time. Especially since it was acquired by Micro$oft. Well, a full migration might take a while, but this seemed like a good opportunity to try out another service provider. GitLab seemed a decent choice, but it's ridiculously JS-reliant and feels cumbersome. Codeberg, not-a-big, and disroot all seemed like FOSS-friendly options. There's also gitgud.io, but being based on GitLab it needed JS, the infra seemed a little buggy, and the Discord contact made this a no. In the end I decided on using Codeberg for the repo.

v0.1 release

After all my work thus far it was time for my first release! I posted create a forum post in Minetest's “Mods WIP” forum and got... no response. Well, that's life. At least I didn't get pilloried!

Refinements

With release v0.1 finished, it was time to start polishing all the things that didn't make it into the first release!

Allow configuring the tincture modifiers

The default modifiers for tinctures are certainly questionable; 10% for power and 100% for durability? Ridiculous! The numbers probably aren't very balanced, and whether one wants large modifiers or small modifiers is likely going to depend more on what kind of game the admin is hosting rather than a “one set of modifiers to rule them all” scenario. Naturally, I needed to make the modifiers configurable. Conveniently, Minetest, er, Luanti, came with a Settings object that allowed me to read in a configuration file. Rather than create an additional configuration file, I added additional settings to the default mod.conf file, separating the additional settings with an additional blank line. Oddly, in order to read the default configuration file I had to specify its path, and in order to get the module's path I had to call minetest.get_modpath("tinctures"); no "sane" defaults were provided by either call. With the Settings object I was able to easily read settings with the get method, though I quickly noticed a distinct lack of an optional parameter for a default value in the event that a setting was unset; as an easy workaround, right after instantiating the Settings object, I checked each of its settings and gave the setting a default value if it was undefined. After this, it was a simple matter of refactoring the code to use the settings instead of hardcoded values. One caveat with the current implementation, though, is that a tinctured tool's capabilities aren't automatically updated with the settings, meaning that changing the settings won't update an existing, tinctured tool's capabilities until another tincture is applied to the tool.

Trimming tool metadata

When I was initially writing the mod, I had to figure out how to modify the tool's capabilities yet still retain the original capabilities for reference in subsequent modifications. The solution I chose was to take each of the tool's original capabilities and store them in the tool's metadata. This worked, but I later learned about minetest.registered_tools, which contained each of the tool's default capabilities. Using this meant not duplicating the default capabilities across each tinctured tool, which seemed like a good idea. While this was mostly straightforward, there were a few interesting things. First, the tool registration did not have the normal ItemStack methods, so the capabilities had to be accessed directly under the tool_capabilities member. Second, I had to use minetest.registered_items when getting the description because some of the logic needed to be duplicated across both tools and essences. Lastly, the registration description had some unexpected characters in them; for example, the Diamond Pickaxe description was @default)Diamond Pickaxe. Perhaps this had something to do with translation? Well, keeping it didn't seem to break anything and I only cared about appending characters so I left it as-is. Overall, refactoring trimmed down both my code size, complexity, and tool metadata usage, so a very clear win!

Closing inventory window on explosion

After refactoring tincture application to be via recipe, I found that having the inventory window remain open during the explosion was rather frustrating as the window blocked control of my character and my view of the explosion. Naturally, I needed to find a way to close the window during the explosion. Enter the minetest.close_formspec method. The method accepted two parameters: a playername and a form name. Specifying an empty string for the form name would close all forms, though the documentation warned to “**USE THIS ONLY WHEN ABSOLUTELY NECESSARY!**”. Well, I couldn't find the form name for the inventory, and an explosion hitting the player seemed like a pretty necessary time to close all forms, so I went with it and it worked as expected! ...At least, that was my first impression.

Sometime while working on the next three commits (detailed in the following subsections), I noticed something strange happening when my player died, or, well, should have died. Instead of dying, the camera would switch to some weird 3rd person view, I would be able to move but not interact with anything, and I'd be confined to my current load boundaries. There was no respawn option, but if I logged out and logged in again then I'd be able to respawn. After some brief, ad hoc bisecting it turned out that closing all formspecs also closed the respawn formspec! Oops. So, the next question was, how could I close just the inventory formspec? I could use the player's get_inventory() method in order to get the inventory as an InvRef, but there was no clear way to get a form name from that. After some digging, I found that the default player inventory was the sfinv mod; maybe it would work to create a setting that closes a specific form name and have that form name default to the form name created by the sfinv mod? Well, the problem with that was that I couldn't even get it to work with sfinv! I tried sfinv:crafting, main, player:main... no luck. After some more searching I found the minetest.register_on_player_receive_fields hook which I eagerly utilized in order to get the following result:

2025-02-22 13:15:45: WARNING[Server]: player: 'userdata: 0xffff7c061948', formname: ”, fields: 'table: 0xffff7c284a20'
An empty string?! WTF?! Did the default player inventory even have a form name?! I despaired thinking how I was going to track down this form name, but eventually I came up with an easier solution: only close forms when the explosion wouldn't kill the player. Not particularly elegant, but it worked. I still have no idea what the inventory's form name is.

Adding an efficiency tincture whitelist

Actually, my first goal with this was to make the efficiency logic dynamic instead of static; instead of matching against ores named stone_with_%a+, I was going to check the node drops for the node itself and exclude items which dropped themselves in order to prevent infinite multiplication of a single node. One problem with this was that get_node_drops() depended on the tool doing the digging, so a static list of nodes was infeasible. A bigger problem presented itself with items like default:clay which dropped 4 default:clay_lump, so therefore didn't drop itself, but then 4 default:clay_lump can be crafted into a default:clay, therefore the item indirectly drops itself. The more I thought about the check, the less I liked it as the consequences of being too liberal in item drops seemed far worse than the consequences of being too conservative. I needed a better solution.

A truly l33t solution would involve traversing each node and recursing through its recipes for all possible paths leading to item duplication, properly taking into account loops, probabilistic drops, and whatnot. I went with an explicit whitelist instead. The whitelist was a setting named efficiency_items and would take an array of itemstrings which the efficiency tincture would apply to. Actually, the Settings object had no support for arrays so the actual argument was one giant string with each item separated by whitespace. In order to prevent needing to manually fill in the list for the default game, I also added an efficiency_defaults boolean option which, when true, would populate the whitelist based on the original, name-matching logic. It wasn't fancy, but it worked.

Improving the explosion particles

When generating particles for the explosion, I ran into an issue where I wasn't able to use dig particles for the explosion because the tool was registered as a craftitem and not a node, and I didn't want to pollute the craft inventory by registering tons of bogus nodes. I spent a fair bit of time looking at the Tile Animation object, but I didn't see any way to simultaneously reduce the image to a single pixel and set the pixel to a random frame on a per-particle basis. Well, I figured I'd at least try out registering a node for each tool and seeing if the particles looked better. When I was working on this, I noticed a rather interesting group named not_in_creative_inventory. This group worked exactly as expected, and the particles ended up looking much, much better. Amusingly enough, I tried removing the group and the nodes didn't appear in the inventory anyways unless I added a description (tooltip) to them. The nodes looked as freakish as expected:

Figure: Frankenodes creates by registering a node using the tool inventory image as the node tiles.
Image 2025_07_28_frankenodes

I made sure to remove both the description and re-add the not_in_creative_inventory group before committing.

Scattering exploded nodes

It just didn't feel right having the node drops lethargically fall while the player was forcefully knocked back. The nodes should have flown away from the explosion just like the player! Unfortunately, dig_node() only returned true or false with nothing of the drop, so I couldn't iterate over the returned drops in order to apply a velocity to them. I took a look at the tnt mod for inspiration, but it appeared to collect, destroy, and drop the nodes using its own logic, and I didn't want to add that kind of complexity for fear of corner-cases. After some more searching I found the handle_node_drops() method, but this would override all node drops! Then it occurred to me that if I could dynamically set handle_node_drops() then I could set it to an explosion-specific function before the exploded nodes are “dug”, then restore its default value afterwards. This actually worked as expected, except the digger parameter in the object was neither a player nor a table (who knows what it was!) so I instead used a closure in order to dynamically create the explosion-specific function with the player in the closure's scope. As for scattering the nodes, I sort of arbitrarily lowered the center of the explosion then added some velocity directed from the center towards the node, resulting in the nodes flying upwards and away. In practice this seems to throw the nodes just a smidge or really far. Well, the values aren't perfect, but the effect is much better than before!

Adding a craft prediction success label

Refactoring tincture application to a recipe with tinctured description applied to the item was a huge step up from having the player use the tincture and the tincture arbitrarily applying itself to the first weapon slot. Still, there was no clear warning about the potential for failure, which would likely be an unwelcome surprise to an unsuspecting player. Since I already knew how to modify the craft prediction item's description, I decided to add a colored success chance to the predicted item. For the colors I'd use green for 100% success, yellow for 50%, orange for 25%, and red for 12.5%. Now, a real implementation would have dynamically interpolated a color for success chances between these set values, producing, for example, a yellowish green for 75% success, but I decided not to the bother with that. I also didn't bother to round arbitrary percentages to two decimal points. Neither of these things are possible with the mod as is, so my lazy code is hidden from most. The result looks like:

Figure: Craft prediction tooltip with the success probability added.
Image 2025_07_28_craft_predict_probability

...and does a much better job informing the player of the risks.

With this done, I'd completed all of the major polish items that I wasn't able to include in the v0.1 release.

v0.2 release

At last satisfied with my work, I tagged v0.2, pushed it, and updated my forum post with the new release. I've done pretty much everything that I wanted to do with this mod for now. Now I just need to write the game of my dreams and integrate this mod into it...


Generated using LaTeX2html: Source