- v50 information can now be added to pages in the main namespace. v0.47 information can still be found in the DF2014 namespace. See here for more details on the new versioning policy.
- Use this page to report any issues related to the migration.
Lua scripting
![]() |
Research Pending! This article or section is incomplete/under construction (likely due to recent changes) and may still be outdated or missing details. Feel free to do some testing and expand it. |
Modding |
---|
Tokens |
Audio · Biome · Graphics · Interaction · Mod info · Plant · Speech · Sphere · Syndrome · World |
Body tokens |
Body · Body detail plan · Bodygloss · Tissue |
Creature tokens |
Creature · Creature mannerism · Personality facet · Creature variation · Procedural graphics layer |
Descriptor tokens |
Descriptor color · Color · Descriptor pattern · Descriptor shape |
Entity tokens |
Entity · Ethic · Language · Value · Position |
Job tokens |
Building · Labor · Reaction · Skill · Unit type |
Item tokens |
Item type · Item definition · Ammo · Armor · Instrument · Tool · Trap component · Weapon |
Material tokens |
Material type · Material definition · Inorganic material definition |
This article is about procedural raw generation. Information on Utility:DFHack scripting can be found at https://docs.dfhack.org/en/stable/.
Lua scripting is an experimental featurev51.06. It is used to create custom procedurally-generated objects that were previously created by hardcoded methods. It was announced in a video, with the stated goal of "supporting future magical endeavors."
Inorganic materials, languages, creatures, interactions, items (currently excluding instruments), reactions, entities, and plants are open to this system.
Scripts are loaded from a mod's scripts/init.lua
file, as well as from any included files.
Structure[edit]
As of right now, Lua scripting is confined to generation of procedural objects. This is done by running the generate
function, a global function loaded in data/init/generators.lua
. It runs unit tests, preprocess, materials, items, languages, creatures, interactions, entities and postprocessing, in that order.
When random objects are first generated, the game populates two global tables, world
and random_object_parameters
. world
contains info about the world currently being generated (or, in the future, played in), while random_object_parameters
contains what the game expects to be generated. The most important thing between these is random_object_parameters.main_world_randoms
, which is true
for exactly one generation at the start of worldgen; it's what you want to check for if you're generating your own objects
You can set the global debug_level
variable to print some debug info. It's a number, but what numbers are there are completely arbitrary. If it's >0, it'll run unit tests; if it's >=0.5, it'll display what step of generation it's at, at every step. You can use get_debug_logger(x)
to return a function that logs to lualog.txt
if the debug level is at least x
.
Unit tests are functions that return a table, containing good
, which, if truthy, is considered passed, and info
, which is a string that contains information on said pass or fail. These unit tests should have no side effects, i.e. they shouldn't muck with global state any. Here's an example unit test, one that was used during development (but had no reason to be removed):
get_random_creature |
---|
get_random_creature=function()
local cr=world.creature.get_random_creature()
local res={}
res.good=type(cr)=='table'
res.info=res.good and ("Got a random creature: "..cr.token) or "No random creature could be gotten, even at most permissive!"
return res
end
|
preprocess
is just a table of functions. It runs each function, one at a time. This is where you want your side effects, and, if you're adding an entirely new procedural object type, that's what you probably want. You should also use it if you want to mess around with random_object_parameters
, which is allowed (it's how demon types are assigned in vanilla, and you can change the proportions as an end user if you want). The "adamantine alloys" example below is an example of what can be done with preprocess (and postprocess, which is mostly identical except it happens after the rest of generation).
The game then generates all of the individual objects; the general procedure for this is that the game calls the generate_from_list
function on a table of functions, which calls every function and picks one of the resulting tables at random depending on their weights. For example, the interactions.secrets
table contains one entry, that for necromancers; it returns a table containing three entries: {interaction=tbl,weight=1,spheres=spheres}
. interaction
is the full raw text of the interaction; weight
is the random weight for the interaction, i.e. if you add another one which returns a table containing weight=2
, that will be twice as likely as necromancers. spheres=spheres
is some extra data the generator might be able to use. It actually doesn't, at this point, but one could override generate_random_interactions
with their own version that takes into account spheres
and, say, tries to evenly distribute generated secrets over available spheres. (This didn't end up in vanilla primarily out of concerns of bug-like behavior cropping up).
Languages are special, though; as can be seen below, the languages
table just expects a table containing translations, e.g. tbl["ABBEY"]="abbey"
. If you want to procedurally add words or symbols (and yes, these are both doable), you can use preprocess or postprocess.
C++ Function Calls[edit]
Function | Description |
---|---|
userdata get_random(table t) | Returns a random value from a table. Uses DF's own RNG. |
int trandom(int n) | Returns a 32-bit integer from 1 to n. Uses DF's own RNG. |
str capitalize_string_words(str s) | Capitalizes every word in a string. |
str capitalize_string_first_word(str s) | Capitalizes the first word in a string. |
str utterance() | Returns a word from the kobold language. |
void lua_log(str s) | Prints a string to `Dwarf Fortress/lualog.txt`. The log() function is more robust and should be used instead.
|
Creatures[edit]
Creatures have a lot more to them than other procedural objects. Forgotten beasts are, in a sense, the simplest of them:
creatures.fb.default |
---|
creatures.fb.default=function(layer_type,tok)
local tbl={}
local options={
strong_attack_tweak=true,
spheres={CAVERNS=true},
is_evil=true,
sickness_name="beast sickness",
token=tok
}
tbl=split_to_lines(tbl,[[
[FEATURE_BEAST]
[ATTACK_TRIGGER:0:0:2]
[NAME:forgotten beast:forgotten beasts:forgotten beast]
[CASTE_NAME:forgotten beast:forgotten beasts:forgotten beast]
[NO_GENDER]
[CARNIVORE]
[DIFFICULTY:10]
[NATURAL_SKILL:WRESTLING:6]
[NATURAL_SKILL:BITE:6]
[NATURAL_SKILL:GRASP_STRIKE:6]
[NATURAL_SKILL:STANCE_STRIKE:6]
[NATURAL_SKILL:MELEE_COMBAT:6]
[NATURAL_SKILL:DODGING:6]
[NATURAL_SKILL:SITUATIONAL_AWARENESS:6]
[LARGE_PREDATOR]
]])
add_regular_tokens(tbl,options)
tbl[#tbl+1]=layer_type==0 and "[BIOME:SUBTERRANEAN_WATER]" or "[BIOME:SUBTERRANEAN_CHASM]"
if layer_type==0 then options.spheres.WATER=true end
options.spheres[pick_random(evil_spheres)]=true
options.do_water=layer_type==0
populate_sphere_info(tbl,options)
local rcp=get_random_creature_profile(options)
add_body_size(tbl,math.max(10000000,rcp.min_size),options)
tbl[#tbl+1]="[CREATURE_TILE:"..tile_string(rcp.tile).."]"
build_procgen_creature(rcp,tbl,options)
return {creature=tbl,weight=1}
end
|
This is a lot of info! First, you build an options
table; it's possible to make a full list of options used in vanilla, but other mods can also use arbitrary options. It then adds all the usual special-to-forgotten-beast tokens, in a big string, followed by calling add_regular_tokens(tbl,options)
, which adds some stuff common to all (vanilla) procedural creatures, based on the options given. It sets do_water
and the WATER sphere if the FB is in a water cavern, an option which whitelists certain random creature profiles, as well as adding a random evil sphere. populate_sphere_info
is similar to add_regular_tokens
; it adds all of the spheres in options.spheres
to the creature, using the SPHERE token, then, if certain options are set, does more. Then, it gets a random creature profile using get_random_creature_profile
and the options, uses add_body_size
to set the BODY_SIZE tokens and attendant things that come with it, sets the creature tile, and finally runs the Big Function, build_procgen_creature
, which creates the description, body, tissues, et cetera.
Random Creature Profiles[edit]
A random creature profile is a type of "thing" a generated creature can be. For example:
random_creature_types.GENERAL_QUADRUPED |
---|
GENERAL_QUADRUPED={
name_string="quadruped",
tile='Q',
body_base="QUADRUPED",
c_class="UNIFORM",
cannot_have_get_more_legs=true,
min_size=70000,
weight=1000
},
|
Of these, only cannot_have_get_more_legs
is optional. build_procgen_creature
has direct access to the RCP, as the first argument, and thus extra table entries can be used however you like.
Other stuff[edit]
TODO: Tweaks, random creature materials, random creature classes, color pickers, function that build_procgen_creature
calls in the process of building that can be used to inject your own logic into creature building (e.g. btc1_tweaks
), etc.
Code Samples[edit]
Snippets of vanilla generation can be found in Category:Lua script pages, and all vanilla scripts can be found in data/vanilla/vanilla_procedural/scripts/
.
Identity language[edit]
This makes a language called GEN_IDENTITY
which is like: "Abbey abbeyabbeys the abbey of abbeys" - i.e. it's the "English" language you might see occasionally. It is present in vanilla_procedural
and can be used for [TRANSLATION]
by default.
GEN_IDENTITY |
---|
languages.GEN_IDENTITY=function()
-- just to demonstrate the absolute most basic method of generating one of these
-- also so that you can just mod stuff to use GEN_IDENTITY
local tbl={}
local unempty = function(str1, str2)
return str1=='' and str2 or str1
end
for k,v in ipairs(world.language.word) do
local str=''
str=unempty(str,v.NOUN_SING)
str=unempty(str,v.ADJ)
str=unempty(str,v.VERB_FIRST_PRES)
str=unempty(str,string.lower(v.token))
tbl[v.token]=str
end
return tbl
end
|
Search by reaction class[edit]
This script returns a table of all inorganic materials with a given [REACTION_CLASS]
. The mat
table also has reaction_product_class
, which includes both [MATERIAL_REACTION_PRODUCT]
and [ITEM_REACTION_PRODUCT]
IDs.
get_all_by_reaction_class() |
---|
function get_all_by_reaction_class(rc)
local valid={}
for i,inorg in ipairs(world.inorganic.inorganic) do
for _,class in inorg.mat.reaction_class do
if class==rc then
valid[#valid+1]=inorg
end
end
end
return valid
end
|
Kobold language[edit]
This generates a language made of [UTTERANCES]
. This is essentially a proper translation based on the kobold language. Note that the hardcoded utterance()
function generates words independently of any existing words in the language, so you may get duplicate words.
GEN_KOBOLD |
---|
languages.GEN_KOBOLD=function()
local tbl={}
for k,v in ipairs(world.language.word) do
tbl[v.token]=utterance()
end
return tbl
end
|
New divine metal[edit]
You can add new metal descriptions for divine metal pretty easily, for example:
Laughing metal |
---|
metal_by_sphere.CHILDREN={
name="laughing metal",
col="7:0:1",
color="WHITE"
}
|
New forgotten beast[edit]
Add a new kind of forgotten beast.
Unbidden spirit |
---|
creatures.fb.unbidden=function(layer_type,tok)
if layer_type==0 then return nil end -- land only
local tbl={}
local options={
strong_attack_tweak=true,
always_make_uniform=true,
always_insubstantial=true,
intangible_flier=true,
spheres={CAVERNS=true},
is_evil=true,
sickness_name="beast sickness",
token=tok
}
tbl=split_to_lines(tbl,[[
[FEATURE_BEAST]
[ATTACK_TRIGGER:0:0:2]
[NAME:unbidden spirit:unbidden spirit:unbidden spirit]
[CASTE_NAME:unbidden spirit:unbidden spirit:unbidden spirit]
[NO_GENDER]
[CARNIVORE]
[DIFFICULTY:10]
[NATURAL_SKILL:WRESTLING:6]
[NATURAL_SKILL:BITE:6]
[NATURAL_SKILL:GRASP_STRIKE:6]
[NATURAL_SKILL:STANCE_STRIKE:6]
[NATURAL_SKILL:MELEE_COMBAT:6]
[NATURAL_SKILL:DODGING:6]
[NATURAL_SKILL:SITUATIONAL_AWARENESS:6]
[LARGE_PREDATOR]
]])
add_regular_tokens(tbl,options)
tbl[#tbl+1]=layer_type==0 and "[BIOME:SUBTERRANEAN_WATER]" or "[BIOME:SUBTERRANEAN_CHASM]"
if layer_type==0 then options.spheres.WATER=true end
options.spheres[pick_random(evil_spheres)]=true
options.do_water=layer_type==0
populate_sphere_info(tbl,options)
local rcp=get_random_creature_profile(options)
add_body_size(tbl,math.max(10000000,rcp.min_size),options)
tbl[#tbl+1]="[CREATURE_TILE:"..tile_string(rcp.tile).."]"
build_procgen_creature(rcp,tbl,options)
-- Weight is a float; all vanilla objects have weight 1
return {creature=tbl,weight=0.5}
end
|
Remove default forgotten beast[edit]
creatures.fb.default=nil
Adamantine alloys[edit]
You can add your own arbitrary generated objects, though as of right now there's no way to make settings for them. This allows for some truly wild stuff; here's a fun example: adamantine-metal alloys for every single non-special metal, giving you an average of the properties of them.
Adamantine alloys |
---|
preprocess.adamantine_alloys=function()
if not random_object_parameters.main_world_randoms then return end
local l=get_debug_logger(2)
local lines={}
local reaction_lines={}
local reaction_names={}
local adamantine=world.inorganic.inorganic.ADAMANTINE
if not adamantine then return end
local adamantine_color=world.descriptor.color[world.descriptor.color_pattern[adamantine.material.color_pattern.SOLID].color[1]]
local adamantine_modulus = 2500000 --mildly arbitrary, just below the theoretical limit
l("Starting")
local done_category=false
for k,v in ipairs(world.inorganic.inorganic) do
if not v.flags.SPECIAL and v.material.flags.IS_METAL then
l(v.token)
local token="GEN_ADAMANTINE_"..v.token
lines[#lines+1]="[INORGANIC:"..token.."]"
add_generated_info(lines)
lines[#lines+1]="[USE_MATERIAL_TEMPLATE:METAL_TEMPLATE]"
for kk,vv in pairs(v.material.adj) do
lines[#lines+1]="[STATE_ADJ:"..kk..":adamantine "..vv.."]" --"adamantine molten steel"? it's fine
end
for kk,vv in pairs(v.material.name) do
lines[#lines+1]="[STATE_NAME:"..kk..":adamantine "..vv.."]"
end
l(2)
local mat_values={}
-- Find the ratio for which you get closest to (but not below) 2000000 in the material's worst property
local worst=math.min(v.material.yield.IMPACT,v.material.fracture.SHEAR)
local wafers=1
local bars=1
if worst < 2000000 then
local ratio = (2000000-3*worst)/1000000
local best_diff=1
for i=1,10 do
local wafer_amt=i*ratio
if wafer_amt>1 and wafer_amt<20 and math.ceil(wafer_amt)-wafer_amt<best_diff then
best_diff=math.ceil(wafer_amt)-wafer_amt
wafers=math.ceil(wafer_amt)
bars=i
end
end
end
local avg_denom=1/(bars*3+wafers) -- Multiplication just a bit faster than division, we're rounding at the end anyway
local solid_cl=nil
for kk,vv in pairs(v.material.color_pattern) do
-- time to get silly
local this_color=world.descriptor.color[world.descriptor.color_pattern[vv].color[1]]
local wanted_color={
r=(this_color.r*bars*3+adamantine_color.r*wafers)*avg_denom,
g=(this_color.g*bars*3+adamantine_color.g*wafers)*avg_denom,
b=(this_color.b*bars*3+adamantine_color.b*wafers)*avg_denom,
}
local best_total_diff=1000000000
local best_clp=nil
for _,clp in ipairs(world.descriptor.color_pattern) do
if clp.pattern=="MONOTONE" then
local cl=world.descriptor.color[clp.color[1]]
local diff=math.abs(wanted_color.r-cl.r)+math.abs(wanted_color.b-cl.b)+math.abs(wanted_color.g-cl.g)
if diff<best_total_diff then
best_clp=clp
best_total_diff=diff
end
end
end
lines[#lines+1]="[STATE_COLOR:"..kk..":"..best_clp.token.."]"
if kk=="SOLID" then solid_cl=world.descriptor.color[best_clp.color[1]] end
end
local color_str=solid_cl.col_f..":0:"..solid_cl.col_br
l(color_str)
lines[#lines+1]="[DISPLAY_COLOR:"..color_str.."]"
lines[#lines+1]="[BUILD_COLOR:"..color_str.."]"
lines[#lines+1]="[ITEMS_METAL][ITEMS_HARD][ITEMS_SCALED][ITEMS_BARRED]"
lines[#lines+1]="[SPECIAL]"
if v.material.flags.ITEMS_DIGGER then
lines[#lines+1]="[ITEMS_DIGGER]"
end
local function new_value(str)
mat_values[str]=mat_values[str] or math.floor((adamantine.material[str]*wafers+v.material[str]*bars*3)*avg_denom+0.5)
l(str,mat_values[str])
return mat_values[str]
end
local function new_value_nested(str1,str2)
mat_values[str1..str2]=mat_values[str1..str2] or math.floor((adamantine.material[str1][str2]*wafers+v.material[str1][str2]*bars*3)/(bars*3+wafers)+0.5)
l(str1..str2,mat_values[str1..str2])
return mat_values[str1..str2]
end
if new_value_nested("fracture","SHEAR")>170000 or new_value_nested("yield","IMPACT")>245000 then
lines[#lines+1]="[ITEMS_WEAPON][ITEMS_AMMO]"
if new_value("solid_density")<10000 then
lines[#lines+1]="[ITEMS_WEAPON_RANGED][ITEMS_ARMOR]"
end
end
lines[#lines+1]="[MATERIAL_VALUE:"..new_value("base_value").."]"
lines[#lines+1]="[SPEC_HEAT:"..new_value("temp_spec_heat").."]"
lines[#lines+1]="[MELTING_POINT:"..new_value("temp_melting_point").."]"
lines[#lines+1]="[BOILING_POINT:"..new_value("temp_boiling_point").."]"
lines[#lines+1]="[SOLID_DENSITY:"..new_value("solid_density").."]"
lines[#lines+1]="[LIQUID_DENSITY:"..new_value("liquid_density").."]"
lines[#lines+1]="[MOLAR_MASS:"..new_value("molar_mass").."]" -- i don't think this is actually correct
for _,thing in ipairs({"yield","fracture"}) do
for force,_ in pairs(v.material[thing]) do
lines[#lines+1]="["..string.upper(force).."_"..string.upper(thing)..":"..new_value_nested(thing,force).."]"
end
end
for _,force in ipairs("IMPACT","COMPRESSIVE","TENSILE","TORSION","SHEAR","BENDING") do
local modulus = v.yield[force] / v.elasticity[force]
local average_modulus = (adamantine_modulus*wafers + modulus*bars*3)*avg_denom
local strain_at_yield = math.floor(new_value_nested("yield",force) / average_modulus + 0.5) -- usually zero, but can be 1 or 2 sometimes
lines[#lines+1]="["..string.upper(force).."_YIELD:"..new_value_nested("yield",force).."]"
lines[#lines+1]="["..string.upper(force).."_FRACTURE:"..new_value_nested("fracture",force).."]"
lines[#lines+1]="["..string.upper(force).."_STRAIN_AT_YIELD:"..strain_at_yield.."]"
end
lines[#lines+1]="[MAX_EDGE:"..new_value("max_edge").."]"
local reaction_token=token.."_MAKING"
reaction_lines[#reaction_lines+1]="[REACTION:"..reaction_token.."]"
add_generated_info(reaction_lines)
reaction_lines[#reaction_lines+1]="[NAME:make adamantine "..v.material.name.SOLID.." (use bars)]"
reaction_lines[#reaction_lines+1]="[BUILDING:SMELTER:NONE]"
reaction_lines[#reaction_lines+1]="[REAGENT:A:"..tostring(150*wafers)..":BAR:NO_SUBTYPE:METAL:ADAMANTINE]"
reaction_lines[#reaction_lines+1]="[REAGENT:B:"..tostring(150*bars)..":BAR:NO_SUBTYPE:METAL:"..v.token.."]"
reaction_lines[#reaction_lines+1]="[PRODUCT:100:"..tostring(bars+wafers)..":BAR:NO_SUBTYPE:METAL:"..token.."][PRODUCT_DIMENSION:150]"
reaction_lines[#reaction_lines+1]="[FORTRESS_MODE_ENABLED]"
reaction_lines[#reaction_lines+1]="[CATEGORY:ADAMANTINE_ALLOYS]"
if not done_category then
done_category=true
reaction_lines[#reaction_lines+1]="[CATEGORY_NAME:Adamantine alloys]"
reaction_lines[#reaction_lines+1]="[CATEGORY_DESCRIPTION:Debase adamantine with other metals to get extremely strong alloys.]"
reaction_lines[#reaction_lines+1]="[CATEGORY_KEY:CUSTOM_SHIFT_A]"
end
reaction_lines[#reaction_lines+1]="[FUEL]"
reaction_lines[#reaction_lines+1]="[SKILL:SMELT]"
end
end
local entity_lines={}
raws.register_inorganics(lines)
-- not used in vanilla right now, due to lack of instruments, but you CAN do this
raws.register_reactions(reaction_lines)
end
|