- 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 · Tile page · 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 |
Lua |
Scripting · Examples · Functions |
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.
Code Samples
Divine language
This is the divine language, which generates a bunch of random-sounding words from a set of syllables.
languages.GEN_DIVINE=function()
local letters={}
letters.vowel={}
letters.cons={}
letters.vowel.COMMON_NUM=5
letters.vowel.NUM=35
letters.cons.COMMON_NUM=12
letters.cons.NUM=22
letters.vowel.lookup={
"a","e","i","o","u",
"ae","ai","ao","au","ea","ei","eo","eu","ia","ie","io","iu","oa","oe","oi","ou","ua","ue","ui","uo","ah","eh","ih","oh","uh","ay","ey","iy","oy","uy"
}
letters.cons.lookup={
"b","p","g","k","c","z","s","d","t","m","n","ng",
"v","f","w","h","j","l","r","q","x","y"
}
for k,v in pairs(letters) do
v.common={}
v.rare={}
for i=1,5 do
if trandom(5)~=0 then v.common[i]=v.lookup[trandom(v.COMMON_NUM)+1] else v.common[i]=v.lookup[trandom(v.NUM)+1] end
end
for i=1,15 do
v.rare[i]=v.lookup[trandom(v.NUM)+1]
end
end
local function letter(t)
if trandom(5)~=0 then
return pick_random(t.common)
else
return pick_random(t.rare)
end
end
local gen_divine={}
for k,v in ipairs(world.language.word) do
local str=""
if trandom(2)~=0 then
str=str..letter(letters.cons)
str=str..letter(letters.vowel)
else
str=str..letter(letters.vowel)
end
local num_letters=trandom(3)
str=str..letter(letters.cons)
if num_letters>0 then str=str..letter(letters.vowel) end
if num_letters>1 then str=str..letter(letters.cons) end
gen_divine[v.token]=str
end
return gen_divine
end
Identity language
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.
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
|
New divine metal
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
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
creatures.fb.default=nil
Adamantine alloys
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
|