Have you ever wondered why there are so many circular dependencies in your project? Maybe you’ve made reusable code in your project and made everything into components (or at least a good number of them, like Drupal 9 tried to do better than previous versions), but the order in which dependencies should be determined is unclear. This is not as important in true object-oriented programming (OOP) languages like C#, but in Lua or even C++ there is still an issue of order-of-definition. I started to write a long “readme” for why I forked animalmaterials in 2021 (about 4 years after it was abandoned) and it became this article. The animalmaterials modpack is an excellent case study on the general Computer Science topic of why definitions should be in a module that is separate from the behaviors.
The purpose of a low-level module (such as a low-level minetest mod that primarily provides registrations) is to define things that multiple other modules can use (In game modding, mod is short for modification rather than module, but the principle applies to both). For example, the Minetest cooking mod depends on animalmaterials and doesn’t have to depend on any specific mobs mod. My fork is a fork of AntumMT’s fork of sapier’s animalmaterials modpack. The dependency on mobs was removed. AntumMT’s antum branch had added the “mobs” dependency. I commented on the related commits (f6fc55b and 00fd6a5) as follows, which is also the TLDR version of this article:
I don’t know if you care about this mod anymore, but this commit and 00fd6a5 defeat the single advantage of animalmaterials: to be a low-level dependency that other mobs mods and various mods can utilize to help them integrate with each other and not register duplicate craftitems. Every time you add a dependency, that mod and mods that mod requires can no longer utilize animalmaterials, because they must load before it. Therefore, the better way to integrate the mods listed in this commit are to make them utilize a common low-level mod (animalmaterials). minetest.override_item is useful in those cases from their end since those high-level mods personalize behavior as players and server owners wish. With minetest_game planned to not ship with minetest anymore and @rubenwardy encouraging people to fork it, mods such as basic_materials and possibly even animalmaterials will become more important for the reasons above. Core devs are even encouraging mods not to depend on default. My fork based on this antum branch will be useful to what I’m doing. I can’t guarantee anyone else will care about this modpack in particular due to being tied to mobf, but I also plan to make mobf optional in other mods in this modpack (even making it optional forces it to be a high-level mod, but that is probably ok for mods in the animalmaterials modpack other than the animalmaterials mod).https://github.com/AntumMT/mp-animalmaterials/commit/f6fc55bc54d327307b01c6144c751cbe8a2e1e0a
What can existing mods do better?
You can change your mobs mod to depend on the animalmaterials mod or a mod like it (mods like default and basic_materials use the same idea, being low-level mods which primarily define materials for use by other mods). Then mods don’t have to depend on your specific mobs mod and assume which materials are present. The headings below show why this is better.
1. Let people choose how their project behaves.
If this article applies to you, someone is already going to use your mod. You don’t have to make them use behaviors from some other mod. You can give them free choice by depending on a mod that offers definitions (registrations) rather than behaviors. Even then, they may not want all of the behaviors in your mod, so if you put your registrations in a separate mod, users can choose whether to use the behaviors your programmed or just your nodes/craftitems.
2. Reduce the number of assumptions regarding names of accessors.
What I’ve explained so far indicates that a high-level mod like mobs redo assumes many things by the time it is loaded. A worse aspect of the situation is that if you changed mobs redo or another high-level mob like it, the code would become very divergent (you couldn’t reuse as much code). For example, even just the fact that mobs redo defines chicken meat instead of using animalmaterials means that every mod that affects chicken must use the “mobs” namespace to access it, or add checks to see whether that or a different chicken is present–none of that would have to happen if mobs redo instead simply used animalmaterials and improved it if necessary (and for compatibility like my animalmaterials fork does, kept the exact same set of craftitems).
3. Allow more components to reuse your code.
The number of assumptions isn’t the worst part about placing low-level items in a high-level mod like mobs. The worst part is that every lower-level mod in the tree of dependencies including the things that those depend on and so on have no access to whatever mobs defines–such as meats in the case of mobs_redo–no mod already loaded (due to being in the dependency tree) can use them! There are probably cases other than mobs redo where this problem is even more disruptive. Even if mod B risked using the materials from the high-level mod A, there are at least two problems with that:
- Mod B can’t add the mod as a dependency nor an optional dependency because that would create a circular dependency. Not adding the mod as a dependency breaks the concept of dependencies: Mod B would assume things are there without listing requirements.
- Mod B can’t check for registered craftitems nor nodes: Mod B can’t reliably check if there is one or another version of the mod if the mod changes or a different fork or replacement version of the mod is used. For example, if a mod depends on “farming”, the mod still has to check whether certain craftitems or nodes are defined in order to determine whether minetest_game/farming or farming redo is installed. In that case, farming is a low-level mod and doesn’t have any problems. However, if the mod that registered the items was a high-level mod with several dependencies like mobs redo, the circular dependency problem would occur: The mod wouldn’t be loaded yet and mod B would have no way to check if something was registered. The solution is for both mod B and mobs redo to depend on a low-level mod like animalmaterials. Mod B can check for registrations after that, but doing so still isn’t ideal: See “Adding everything to the core doesn’t reduce work–It creates technological debt” below regarding why not to keep expanding mod A (default or some other mod) continuously.
4. Adding everything to the core doesn’t reduce work–It creates technological debt.
The main disadvantage to a monolithic solution is that you and those using your product have to deal with not just a moving target, but a target that acquires parts from other things. The other things still exist. Whether you use them or not, someone else does or their user-generated content such as a Minetest world may use them. Therefore, the downstream mod maintainer is left to support both your version and the componentized version.
Having a low-level mod is a better solution than adding everything to the core (or the Minetest default mod, which functions as a core component for perhaps the majority of mods). If the registrations are in a separate mod, mod B and the high-level mod can require the low-level mod and have a reasonable assumption of what is in there, and if there is a known variant, the two mods can check what is registered. Checking for what is registered by default can become a problem if low-level items used for recipes, treasure/prizes mods, minigames, etc. may use them. Each of those uses are entire categories of mods. Having boilerplate such as:
if minetest.registered_nodes["default:permafrost"] then
…in every mod that depends on default is already technically necessary (if the node is used). That node isn’t very important for recipes, treasure/prizes mods, minigames, etc., but it is already used by some mods and maybe a mob mod will want the creature to spawn there or a plantlife mapgen mod will want a certain plant to spawn there.
- If the core or default succumbs to feature creep (or scope creep) to encompass nodes or craftitems those categories of mods use, then the registration checks will have to creep into mods in those or similar categories, in other words, such ever-expanding boilerplate will be necessary to maintain in a virtually unlimited and ever-expanding number of mods.
- The problem can get more complex if either default or another mod may define it. Then each high-level mod must optionally depend on both low-level mods, check for the registration in both namespaces, and store the id string of the one that is available in a variable for later use in the mod (The namespace is unknown so the ID can’t be used directly).
5. Cooperate with others for better results.
As a positive solution, create or reuse low-level registrations as a separate dependency. Doing registrations in a low-level mod helps you cooperate with others by not creating duplicate materials such as ingredients or mapgen nodes in mods where you add features.
This mod can solve the most frequent problems.
It is a mod which provides recipes for raw meat(s): This is a frequent issue since several advanced cooking mods exist which use specific meats or meat groups.
The animalmaterials mod in (my fork of) animalmaterials modpack can be used by:
- Any mod which provides specific mob(s)
- Any mod which provides recipes for raw meat(s) (such as cooking)
nether (my fork)
My fork of nether can be used by:
- a mod which defines the nether
*(this would be a fork of nether or a new nether or nether-like mapgen mod)
- some odd mod which lets you obtain nether materials some other way
*, such as a prizes, loot, or minigame mod.
- a mod which defines nether fences but doesn’t require a nether realm mod (Maybe someone wants a museum or creative world with the materials but not the maintenance and storage involved in having a nether realm).
This mod solves the most disruptive problems.
The basic_materials mod can be used by:
- mesecons but isn’t yet… See “mesecons should use my basic_materials mod” mesecons issue by VanessaE
- any mod that wants the materials for recipes or mapgen but doesn’t want to require mesecons (or only wants mesecons or technic but not both)
*: Examples marked with ‘*’ are only hypothetical and don’t represent any mod(s) that I know to exist.
What if I want different features?
You may ask, “What if I want different features than these simplistic items offer?” Depend on animalmaterials then use
minetest.override_item. The Minetest lua API has the function for one reason: to solve your issue! Not everyone wants your specific lasso, but you can still have all the features you want when you or they are using your specific lasso mod–problem solved. Then multiple mods can utilize the same lasso or make new recipes for it and they only ever need animalmaterials not some lasso in some specific namespace.
If you make animalmaterials an optional dependency rather than a required one, simply check for it in your code (such as via
rawget (_G, "animalmaterials")), then if animalmaterials isn’t present, call
minetest.register_craftitem instead of
minetest.override_item. Then in the same case you can optionally make an alias from yours to the animalmaterials, such as if you want to use an old world that contains items from an old version of your mod that was monolithic.
I welcome comments if they are courteous critique with logic or examples–should we use data-oriented design, or functional programming? You can decide for yourself whether my explanation defeats the concept of OOP, which includes behaviors in definitions! Probably not, since OOP (C++ etc) usually involves abstract classes, interfaces, along with inheritance. Using separate registration mods along with minetest.override_item is analogous to OOP patterns, validating my points.