Artifact c2949bfb63601d30fb25112933c29e18801aec27ba0c74bda24b93176f7e0715:
- File sorcery.md — part of check-in [ea6e475e44] at 2020-10-19 09:52:11 on branch trunk — continue dev on celestial mechanics, add melding+division spells (resonance), refine itemclasses, add keypunch and punchcards, add paper pulp, add a shitload of visuals, add convenience scripts for working with the wiki, make the flamebolt spell actually useful instead of just a pretty lightshow, add essences, inferno crystal, and other goodies; iterate on wands, lots of shit i can't remember, various bugfixes (user: lexi, size: 18874) [annotate] [blame] [check-ins using]
sorcery
sorcery
is a wide-ranging mod that adds many features, devices, powers, and mechanics to the minetest default game. it aims to add a distinctive cosmology, or world-system, to the game, based around mese, alchemy, spellcasting, and worship. it is intended to be pro-social, lore-flexible, and to interoperate well with other mods wherever possible.
dependencies
first-party
- default
- stairs
- screwdriver
- vessels
third-party
- xdecor for various tools and ingredients, especially honey and the hammer
- basic_materials for crafting ingredients
- instant_ores for ore generation. temporary, will be removed and replaced with home-grown mechanism soon
- farming redo for potion ingredients
- late for spell, potion, and gravitator effects
- note: in order for the gravitator to work, the late condition interval must be lowered from its default of 1.0 to 0.1. this currently can only be done by altering a variable at the top of conditions.lua, though a note in the source suggests a configuration option will be added eventually. hopefully this is so.
interoperability
sorcery has special functionality to ensure it can cooperate with various other modules, although they are not required for it to function.
xdecor
by default, sorcery
disables the xdecor enchanter, since sorcery
offers its own, much more sophisticated enchantment mechanism. however, the two can coexist if you really want; a configuration flag can be used to prevent sorcery
disabling the xdecor enchanter.
hopper
many sorcery
devices support interactions with the hopper. devices that produce outputs after a period of time like the infuser will automatically inject their output into a hopper if one is placed below it, and if the device has multiple slots, they can usually be accessed by configuring a hopper to insert from the side or from above.
others
- if
moreores
is in use,sorcery
will use its silver rather than creating its own. mithril is not used bysorcery
. sorcery
will usenew_campfire
's ash if it's available, otherwise creating its own.
lore
sorcery
supplies a default system of lore (that is, the arbitrary objects that the basic principles of the setting operate over) but this can be augmented or replaced on a per-world basis. for instance, you can substitute your own gods for the Harvest Goddess, Blood God, change their names, and so on, or simply make your own additions to the pantheon. since lore overrides are stored outside the minetest tree, it can be updated without destroying your changes.
lore is stored separately from the rest of the game logic, in the 'data' directory of the sorcery
mod. it is arranged in a hierarchy of thematically-organized tables. for instance, the table of gods can be found in data/gods.lua. ideally, lore tables should contain plain data, though they can also contain lambdas if necessary. lore files are evaluated in the second stage of the init process, after library code has been loaded but before any game logic has been instantiated. lore can thus depend on libraries where reasonable (though e.g. colors should be stored as 3-tuples rather than sorcery.lib.color
objects, and images as texture strings, unless there is a very good reason to do otherwise).
lore files should never depend on functions in the minetest
or core
namespace! if you really need such functionality, gate it off with an if statement and be sure to return something useful in the event that the namespace isn't defined. lore is just data: as a general principle, non-minetest code should be able to evaluate and make use of the lore files, for instance to produce an HTML file tabulating the various potions and how to create them. lore should also not mutate the global environment: while there is currently nothing preventing it from doing so, steps will likely be taken in the future to give each lore module a clean environment to keep it from contaminating the global namespace. right now, all functions and variables declared in a lore file should be defined local
. the only job of a lore file is to return a table.
sorcery
looks in a directory called sorcery
in the world-data directory for world-specific lore and other overrides. below are three example lore modifications to give you a sense for how this system works.
replacing the gods
this is probably the most common action you'll want to take -- if your world has its own defined setting and already has its own gods, or you just want to give it a bit of a local flavor, you probably won't want the default pantheon that sorcery
comes with.
to do this, we'll need to create a file called $world/sorcery/lore-gods.lua
where $world is the world's root data directory, e.g. /srv/mt/geographica
-- /srv/mt/geographica/sorcery/lore-gods.lua
return {
fire = {
name = 'Aduram the Scorching';
-- personality
laziness = 3; -- controls how frequently the god takes action upon the mortal world
stinginess = 5; -- the higher the stinginess value, the less frequently the god will give gifts to worshippers
generosity = 7; -- likeliness to overlook disfavored behavior and give items of high value
color = {255,0,0};
idol = {
desc = 'Fiery Idol';
width = 0.5, height = 1;
tex = { ‹list of textures› };
};
sacrifice = {
['tnt:gunpowder'] = 2; -- players can leave gunpowder on his altars to gain 2 points of favor
};
gifts = {
['fire:flint'] = {30, 3}; -- one-in-three likelihood of gifting flint at idols that have reached 30 favor
};
consecrate = {
['default:steel_ingot'] = {5, 'fire:flint_and_steel'} -- transform steel ingots on his altar into firestarter at a cost of 5 favor
};
bless = {
potions = {
Force = {favor=70, cost=5, chance=15} -- 1-in-15 chance of applying the Elixir of Force effect to a potion on his altar once the attached idol has reached 30 favor, at a cost of 5 favor
};
tools = {
speed = {favor=120, cost=25, chance=5} -- apply speed enchantment
};
}
};
water = {
name = 'Uskaluth the Drenched';
‹···›
};
}
when you define gods, be mindful of the personality you wish to create and worship practices you wish to provoke. how attentive must their worshippers be to maintain a useful relationship? how often are sacrifices required? how many sacrifices are necessary before the god will start performing miracles? it's easy to make gods completely overpowered gamebreakers, so be cautious, and don't gift anything terribly valuable -- or if you do, give it very rarely.
note that you would also need to supply idol models called sorcery-idol-fire.obj
and sorcery-idol-water.obj
. gods with the ids harvest
or blood
will use predefined idols.
adding gods
if you're happy with the default pantheon, but your spiritual life just isn't complete without the fiery rage of Aduram, you can change the name of sorcery/lore-gods.lua
to sorcery/lore-gods-extra.lua
. this way, the table it returns will be merged preferentially into the table of default gods.
modifying gods
more complex operations can also be performed. let's say the Blood God is ruining your vibe and you want to delete him (her? it?) from the pantheon. or the Harvest Goddess needs a name more in line with your local cosmology. these are both easy:
-- /srv/mt/geographica/sorcery/lore-gods.lua
local gods = ...
gods.harvest.name = 'Disastra bel-Malphegor'
gods.blood = nil
return gods
this has the handy property that any changes made upstream to the Harvest Goddess will be respected if you update the mod, without overriding the changes made in lore-gods.lua.
or, if you want to extract a specific god from the default pantheon and include it with your own:
-- /srv/mt/geographica/sorcery/lore-gods.lua
local gods = ...
local mygods = dofile('pantheon.lua')
mygods.harvest = gods.harvest
mygods.harvest.name = 'Disastra bel-Malphegor'
return mygods
compatibility tables
if you use a mod that neither supports sorcery
nor is supported by it, the proper solution is of course to open a ticket in the former's bug tracker, but as a quick fix you can also tweak the sorcery
compatibility tables with a $world/sorcery/lore-compat.lua
file.
-- /srv/mt/geographica/sorcery/lore-compat.lua
return sorcery.lib.tbl.deepmerge((...), {
grindables = {
['bonemeal:bone'] = {
powder = 'bonemeal:bonemeal';
hardness = 3;
value = 3;
grindcost = 1;
};
};
})
note that we have to use deepmerge instead of naming the file lore-compat-extra.lua
and returning a simple table because the init routine only performs a shallow merge, meaning that it would wipe out the entire provided version of sorcery.data.compat.grindables
! no bueno.
local tweaks
if you need to change sorcery
's behavior in a way that isn't possible through modifying the lore, you can create a file $world/sorcery/finalize.lua
which will be run at the end of the init process and which will have access to all of the machinery created by the various modules of the mod. worldbuilding.lua
is like finalize.lua
but it is run before any lore is loaded. finally, bootstrap.lua
will be run before anything else, including library code, is loaded, but after the core sorcery
structure and its loading functions have been created.
in the unlikely event that the lore-loading process itself is incompatible with the changes you're trying to make, you can create a loadlore.lua
file that will be run instead of the usual routine. you'll then be responsible for loading all of the game's lore; the default lore will not even be read! this will be most useful if you're loading or generating your own lore from an unusual source, such as somewhere on the network.
if you want to write a lore loader but include some of the default lore, you can use the loading function passed to loadlore.lua
:
-- /srv/mt/geographica/sorcery/loadlore.lua
local load_lore, load_module = ...
sorcery.data = dofile('load-remote-lore.lua')
load_lore {'enchants', 'spells'}
as you can see here, once the lore is loaded, it is stored in the variable data
. there is a subtle distinction you need to bear in mind: data
represents the full set of lore-derived information sorcery
has to work with, which is not necessarily the same as the lore itself. for example, certain modules could generate extra information and attach them to the entries in the data
table, which can be (and is) written to at runtime. the one invariant that the data
table should observe is that once a record is added, it may neither be mutated nor removed (though additional sub-records can always be added) -- sorcery
is not written to handle such eventualities and it may produce anything from glitches to crashes. the term lore
references specifically the core records stored on disk from which the data
table is generated, and a lorepack
is a full, integrated collection of lore. (there are cases where modifying or deleting from data
may be necessary for a sorcery
-aware mod, for instance to prevent players from creating a certain default potion, but this is very fraught and you should only ever do it if you thoroughly understand the internals and exactly what you're doing, with the caveat that what works in this version may break the next.)
writing sorcery-aware mods
sorcery
exposes an API that is usable by other mods. to use it, you need to understand a little about how sorcery
handles lore. local tweaks are simple because they inject changes into the pipeline before sorcery
makes use of any of the lore. part of the setup stage is the creation of various lore-derived recipes, items, nodes, and so on. so if your mod loads after sorcery
is finished loading (which it must, if it expects to call into the sorcery
API), it can't take advantage of the same mechanism that world tweaks can, as it needs to not just insert the data but notify sorcery
that something has been added so it can create the appropriate objects.
to handle this complex situation, sorcery
uses registers. a register is set of functions and data used for tracking actions and dependencies between them. registers are automatically created for each lore category, except those specifically excluded by the bootstrapping routine. for instance, the extract register can be found at sorcery.register.extract
. a register has several functions, only three of which are relevant to you: link
, meld
, and foreach
.
link
link([key,] value)
inserts a new entry into the database the register manages. so to add a new extract for, say, sand, we could call sorcery.register.extracts.link('sand',{'default:sand',{255,255,0})
. this would insert the record sand => {'default:sand, {255,255,0}}
into sorcery.data.extracts
. it would also take all of the actions that would have been taken if the entry had been present in sorcery.data.extracts
when the sorcery
mod bootstrapped itself.
sorcery.register.infusion.link {
infuse = 'farming:sugar';
into = 'sorcery:potion_serene';
output = 'mymod:sweet_potion';
}
sorcery.register.residue.link('farming:sugar','sorcery:ash')
this will permit use of the infuser to create a Sweet Potion (presumably defined by your own mod) by infusing sugar into a Serene Potion, leaving behind ash.
new alloys can also be registered for use with the smelter:
sorcery.register.alloys.link {
output = {
[1] = 'mymod:excelsium_fragment';
[4] = 'mymod:excelsium_ingot';
[4 * 9] = 'mymod:excelsium_block';
};
cooktime = 69;
metals = {
lithium = 1;
silver = 4;
aluminum = 4;
};
}
this defines an alloy called Excelsium that can be produced by mixing four parts silver, four parts aluminum, and one part lithium -- e.g. one lithium fragment, one aluminum ingot, and one silver ingot. metals
must specify metals registered with the sorcery
mod; output
can be either the name of a registered metal or a table of items and part-values that can be produced. at least output[1] must always be present!
the kiln is not currently functional, but once it is, it will be possible to create kiln recipes with the sorcery.register.kilnrecs.link
function.
foreach
foreach(id,deps,fn)
should be called for any registration actions that need to take place for a registry's contents. let's say you want to create a container than holds large volumes of an extract, like an extract keg. to do this, you'd need to create a node for each extract. but you can't just iterate sorcery.data.extracts
because new extracts might be added long after your code is run. the solution is foreach
: you give it an identifier, a list of dependency identifiers, and a function, and it then ensures that function will be called once for everything that winds up in sorcery.data.extracts
.
it is crucial to understand the difference between a data for
loop and registry foreach
, because they are not interchangeable. a function passed to foreach
may be called immediately, ten minutes later, both, or never (if there are missing dependencies). for instance, you cannot write a function that uses foreach
to tally the mass of all metals, because it would run whenever a new metal was created, long after your function had stopped running, and it would only run once ever for each metal. in that case, you would need to use for
.
let's consider our keg storage mod. we would want to create several things: first, the individual per-extract keg nodes; second, a node that takes an empty keg and, say, 99 of an extract, and transforms them into an extract keg. to register the kegs, we might use code like
minetest.register_node('keg:empty', { description = 'Empty Keg'; ‹···›})
sorcery.register.extracts.foreach('keg:generate',{},function(name,data)
minetest.register_node('keg:extract_' .. name, {
description = sorcery.lib.str.capitalize(name) .. ' Extract Keg';
color = sorcery.lib.color(data[2]):hex();
‹···›
})
end)
but in the on_metadata_inventory_put
code for keg-filling node, to identify the proper keg, we might instead use code like
local inv = minetest.get_meta(pos):get_inventory()
local fluid = inv:get_stack('fluid',1)
if fluid:get_count() < 99 then return end
local fname = fluid:get_name()
if minetest.get_item_group(fname, 'sorcery_extract') ~= 0 then
for k,v in pairs(sorcery.data.extracts)
if fname == 'sorcery:extract_' .. k then
inv:set_stack('preview',1,ItemStack('keg:extract_' .. k))
return
end
end
end
foreach
could NOT be used in this case.
every time you register a foreach
, you need to give it a unique identifier and list its dependencies. the identifier need only be unique among functions linked to that specific registry. dependencies are used so that when new items are linked to a registry, the queued functions are executed in the correct order. for instance, if you had a foreach function in the extracts registry that modified the definition table of the keg nodes, you would need to list 'keg:generate'
in its dependency array.
spelling dependencies correctly is crucial. if you call foreach
with the name of a dependency that is not yet known to the registry, it will not be executed until that dependency is added. so if you misspell a dependency, the foreach
function you add will never execute!
it's helpful therefore to insert calls to sorcery.registry.defercheck()
at code boundaries, e.g. the end of your init file. this will print a warning to the logs and return false if there are any deferred iterators that have not yet been run. otherwise it will silently return true. sorcery
checks its own iterators this way.
meld
registries also have the meld(tbl)
convenience function, which is useful if you have a sorcery
-like lore table you want to merge in, say a table of metals your mod adds. in this case, you could simply call sorcery.register.metals.meld(mymod.data.metals)
instead of faffing around with for loops.
creating registries
you're not restricted to the registries already in existence. you can create registries of your own with the function sorcery.registry.mk(name[,db])
. this will create (or overwrite!) registries in the sorcery.register
table, and return a reference to that registry. the db argument can be absent, in which case it will look for a database under sorcery.data[name]
or fail if one cannot be found. this will generally not be what you want if you're using it from another mod, so you'll want to either pass a reference to an existing table under your control, or the argument false
which will tell mk
to create a store in the registry itself under the table db
, which is useful for ephemeral data that is generated entirely at runtime from existing lore entries like alloys or infusion residue.