Added many forum scripts, and a makeshift onReport/onStrike event that will be merged into eventful/EventManager later.

develop
expwnent 2014-06-26 08:36:57 -04:00
parent f5de76e2cc
commit 596ab0e1b8
26 changed files with 4259 additions and 0 deletions

@ -0,0 +1,74 @@
--onReport.lua
--author expwnent
--contains the "ON_REPORT" event: triggered when there is a new report in df.global.world.status.reports
--example
--local onReport = require 'plugins.onReport'
--onReport.triggers.someName = function (reportId)
-- --do stuff with that id
--end
local _ENV = mkmodule('onReport')
local utils = require 'utils'
local repeatUtil = require 'plugins.repeatUtil'
lastReport = lastReport or -1
triggers = triggers or {}
monitorFrequency = monitorFrequency or nil
eventToDwarf = eventToDwarf or {}
function updateEventToDwarf(reportId)
if not eventToDwarf[reportId] then
eventToDwarf[reportId] = {}
end
for _,unit in ipairs(df.global.world.units.all) do
for _,reportType in ipairs(unit.reports.log) do
for _,report in ipairs(reportType) do
if report == reportId then
eventToDwarf[reportId][unit.id] = true
end
end
end
end
end
function monitor()
local reports = df.global.world.status.reports
if df.global.world.status.next_report_id-1 <= lastReport then
return
end
-- if #reports == 0 or reports[#reports-1].id <= lastReport then
-- return
-- end
_,_,start = utils.binsearch(reports,lastReport,"id")
while start < #reports and reports[start].id <= lastReport do
start = start+1
end
for i=start,#reports-1,1 do
updateEventToDwarf(reports[i].id)
for _,callBack in pairs(triggers) do
callBack(reports[i].id)
end
lastReport = reports[i].id
end
end
monitorEvery = function(n)
if n <= 0 then
print('cannot monitor onReport every '..n..' ticks.')
return
end
if monitorFrequency and monitorFrequency < n then
print('NOT decreasing frequency of onReport monitoring from every '..monitorFrequency..' ticks to every '..n..' ticks')
return
end
print('monitor onReport every '..n..' ticks')
monitorFrequency = n
repeatUtil.scheduleEvery('onReportMonitoring', n, 'ticks', monitor)
end
monitorEvery(1)
return _ENV

@ -0,0 +1,255 @@
local _ENV = mkmodule('onStrike')
local onReport = require 'plugins.onReport'
local utils = require 'utils'
debug = debug or true
triggers = triggers or {}
function getReportString(reportId)
local report = df.report.find(reportId)
local result = report.text
local i = 1
local report2 = df.report.find(reportId+i)
while report2 and report2.flags.continuation do
result = result .. ' ' .. report2.text
i = i+1
report2 = df.report.find(reportId+i)
end
return result
end
onReport.triggers.onStrike = function(reportId)
local report = df.report.find(reportId)
if report["type"] ~= df.announcement_type.COMBAT_STRIKE_DETAILS then
return
end
if report.flags.continuation then
return
end
-- print('\n')
local fighters = {}
for unitId,_ in pairs(onReport.eventToDwarf[reportId]) do
table.insert(fighters,unitId)
end
local reportString = getReportString(reportId)
if #fighters ~= 2 then
if debug then
local ok = string.find(reportString,' skids along ')
if not ok then
print('onStrike: #fighters = ' .. #fighters)
print(reportString)
df.global.pause_state = true
end
end
return
end
local info = {}
local count = 0
fighters[1] = df.unit.find(fighters[1])
fighters[2] = df.unit.find(fighters[2])
local function getWound(fighterA,fighterB)
local wound
for i=#fighterB.body.wounds-1,0,-1 do
local w = fighterB.body.wounds[i]
if w.unit_id == fighterA.id and w.age <= 1 then
wound = w
break
end
end
--[[name,_ = tryParse(reportString,getNames(fighterA))
if not name then
wound = nil
end
--]]
return wound
end
local wound1 = getWound(fighters[1],fighters[2])
local wound2 = getWound(fighters[2],fighters[1])
local flying = string.find(reportString,'The flying')
local name1
local name2
if flying then
name1 = findAny(reportString,getNames(fighters[2]))
name2 = findAny(reportString,getNames(fighters[1]))
else
name1 = tryParse(reportString,getNames(fighters[1]))
name2 = tryParse(reportString,getNames(fighters[2]))
end
if name1 and wound1 and name2 and wound2 then
if debug then
print('ambiguous wounds: ' .. reportString)
print('fighter1 = ' .. fighters[1].id)
print('fighter2 = ' .. fighters[2].id)
df.global.pause_state = true
end
return
elseif not wound1 and not wound2 then
local ok = fighters[1].flags1.dead or fighters[2].flags1.dead or string.find(reportString,' grabs ') or string.find(reportString,'snatches at') or string.find(reportString,'glances away!') or string.find(reportString, ' shakes ')
if not ok and debug then
print('neither wound works: ' .. reportString)
print('fighter1 = ' .. fighters[1].id)
print('fighter2 = ' .. fighters[2].id)
df.global.pause_state = true
end
return
elseif not name1 and not name2 and not string.find(reportString,'The flying ') then
if debug then
print('WTF?')
print('fighter1 = ' .. fighters[1].id)
print('fighter2 = ' .. fighters[2].id)
df.global.pause_state = true
end
return
elseif name1 and wound1 then
else
local temp = fighters[1]
fighters[1] = fighters[2]
fighters[2] = temp
end
local wound = wound1 or wound2
--is it a weapon attack?
local isWeaponAttack
if getWeapon(fighters[1]) and string.find(reportString,getWeaponString(fighters[1])) then
isWeaponAttack = true
else
isWeaponAttack = false
end
isWeaponAttack = isWeaponAttack or flying
local weapon
if isWeaponAttack then
weapon = getWeapon(fighters[1])
end
-- print('triggers')
for _,trigger in pairs(triggers) do
-- print('trigger')
trigger(fighters[1],fighters[2],weapon,wound)
end
end
function myConcat(table1,table2)
local result = {}
for _,v in pairs(table1) do
table.insert(result,v)
end
for _,v in pairs(table2) do
table.insert(result,v)
end
return result
end
function getUnitAttackStrings(unit)
local result = {}
for _,attack in ipairs(unit.body.body_plan.attacks) do
table.insert(result,attack.verb_3rd..' ')
end
return result
end
function getUnitAttack(unit,parsedAttack)
for _,attack in ipairs(unit.body.body_plan.attacks) do
if attack.verb_3rd..' ' == parsedAttack then
return result
end
end
return nil
end
function getWeapon(unit)
function dumb(item)
-- print('\n')
-- print(item)
-- printall(item)
if item.mode ~= df.unit_inventory_item.T_mode.Weapon then
--print('item.mode ' .. item.mode .. ' /= Weapon ' .. df.unit_inventory_item.T_mode.Weapon)
return false
end
if item.item._type ~= df.item_weaponst then
--print('item.item._type ' .. item.item._type .. ' /= df.item_weaponst ' .. df.item_weaponst)
return false
end
return true
end
for _,item in ipairs(unit.inventory) do
if dumb(item) then
return item.item
end
end
return nil
end
function getWeaponAttackStrings(unit)
local result = {}
local weapon = getWeapon(unit)
if not weapon then
print('no weapon')
return result
end
for _,attack in ipairs(weapon.subtype.attacks) do
table.insert(result,attack.verb_3rd..' ')
end
return result
end
function getWeaponAttack(unit,parsedAttack)
local weapon = getWeapon(unit)
if not weapon then
return nil
end
for _,attack in ipairs(weapon.subtype.attacks) do
if attack.verb_3rd..' ' == parsedAttack then
return attack
end
end
return nil
end
function getNames(unit)
local result = {}
table.insert(result,unit.name.first_name .. ' ')
table.insert(result,'The '..dfhack.units.getProfessionName(unit)..' ')
table.insert(result,'the '..dfhack.units.getProfessionName(unit)..' ')
table.insert(result,'The Stray '..dfhack.units.getProfessionName(unit)..' ')
table.insert(result,'the stray '..dfhack.units.getProfessionName(unit)..' ')
return result
end
function getWeaponString(unit,suffix)
local weapon = getWeapon(unit)
if not weapon then
return ''
end
local material = getMaterialString(weapon)
return material .. ' ' .. weapon.subtype.name .. (suffix or '')
end
function getMaterialString(item)
local material = dfhack.matinfo.decode(item.mat_type,item.mat_index)
return material.material.state_name[df.matter_state.Solid]
end
function findAny(parseString,strs)
for _,str in ipairs(strs) do
if string.find(parseString,str) then
return str
end
end
return nil
end
function tryParse(parseString,strs)
for _,str in ipairs(strs) do
if string.sub(parseString,1,#str) == str then
--print('\n"' .. str .. '" matches "' .. parseString .. '"\n')
return str,string.sub(parseString,#str+1,#parseString)
end
--print('\n"' .. str .. '" doesn\'t match "' .. parseString .. '"\n')
end
return nil,nil
end
return _ENV

@ -0,0 +1,34 @@
-- lua/plugins/repeatUtil.lua
-- author expwnent
-- vaguely based on a script by Putnam
local _ENV = mkmodule("repeatUtil")
repeating = repeating or {}
dfhack.onStateChange.repeatUtilStateChange = function(code)
if code == SC_WORLD_UNLOADED then
repeating = {}
end
end
function cancel(name)
if not repeating[name] then
return false
end
dfhack.timeout_active(repeating[name],nil)
repeating[name] = nil
return true
end
function scheduleEvery(name,time,timeUnits,func)
cancel(name)
local function helper()
func()
repeating[name] = dfhack.timeout(time,timeUnits,helper)
end
helper()
end
return _ENV

@ -0,0 +1,154 @@
--syndrome.lua
--author expwnent
--some utilities for adding syndromes to units
local _ENV = mkmodule("syndromeUtil")
local Utils = require("utils")
function findUnitSyndrome(unit,syn_id)
for index,syndrome in ipairs(unit.syndromes.active) do
if syndrome.type == syn_id then
return syndrome
end
end
return nil
end
--usage: syndrome.ResetPolicy.DoNothing, syndrome.ResetPolicy.ResetDuration, etc
ResetPolicy = ResetPolicy or Utils.reverse({
"DoNothing",
"ResetDuration",
"AddDuration",
"NewInstance"
})
function eraseSyndrome(unit,syndromeId,oldestFirst)
local i1
local iN
local d
if oldestFirst then
i1 = 0
iN = #unit.syndromes.active-1
d = 1
else
i1 = #unit.syndromes.active-1
iN = 0
d = -1
end
local syndromes = unit.syndromes.active
for i=i1,iN,d do
if syndromes[i].type == syndromeId then
syndromes:erase(i)
return true
end
end
return false
end
function eraseSyndromes(unit,syndromeId)
local count=0
while eraseSyndrome(unit,syndromeId,true) do
count = count+1
end
return count
end
--target is a df.unit, syndrome is a df.syndrome, resetPolicy is one of syndrome.ResetPolicy
--if the target has an instance of the syndrome already, the reset policy takes effect
--returns true if the unit did not have the syndrome before calling and false otherwise
function infectWithSyndrome(target,syndrome,resetPolicy)
local oldSyndrome = findUnitSyndrome(target,syndrome.id)
if oldSyndrome == nil or resetPolicy == nil or resetPolicy == ResetPolicy.NewInstance then
local unitSyndrome = df.unit_syndrome:new()
unitSyndrome.type = syndrome.id
unitSyndrome.year = df.global.cur_year
unitSyndrome.year_time = df.global.cur_year_tick
unitSyndrome.ticks = 0
unitSyndrome.wound_id = -1
unitSyndrome.flags.bits.active = 1
for k,v in ipairs(syndrome.ce) do
local symptom = df.unit_syndrome.T_symptoms:new()
symptom.quantity = 0
symptom.delay = 0
symptom.ticks = 0
symptom.flags.bits.active = true
unitSyndrome.symptoms:insert("#",symptom)
end
target.syndromes.active:insert("#",unitSyndrome)
elseif resetPolicy == ResetPolicy.DoNothing then
elseif resetPolicy == ResetPolicy.ResetDuration then
for k,symptom in ipairs(oldSyndrome.symptoms) do
symptom.ticks = 0
end
oldSyndrome.ticks = 0
elseif resetPolicy == ResetPolicy.AddDuration then
for k,symptom in ipairs(oldSyndrome.symptoms) do
--really it's syndrome.ce[k].end, but lua doesn't like that because keywords
if syndrome.ce[k]["end"] ~= -1 then
symptom.ticks = symptom.ticks - syndrome.ce[k]["end"]
end
end
else qerror("Bad reset policy: " .. resetPolicy)
end
return (oldSyndrome == nil)
end
function isValidTarget(unit,syndrome)
--mostly copied from itemsyndrome, which is based on autoSyndrome
if
#syndrome.syn_affected_class==0
and #syndrome.syn_affected_creature==0
and #syndrome.syn_affected_caste==0
and #syndrome.syn_immune_class==0
and #syndrome.syn_immune_creature==0
and #syndrome.syn_immune_caste==0
then
return true
end
local affected = false
local unitRaws = df.creature_raw.find(unit.race)
local casteRaws = unitRaws.caste[unit.caste]
local unitRaceName = unitRaws.creature_id
local casteName = casteRaws.caste_id
local unitClasses = casteRaws.creature_class
for _,unitClass in ipairs(unitClasses) do
for _,syndromeClass in ipairs(syndrome.syn_affected_class) do
if unitClass.value==syndromeClass.value then
affected = true
end
end
end
for caste,creature in ipairs(syndrome.syn_affected_creature) do
local affectedCreature = creature.value
local affectedCaste = syndrome.syn_affectedCaste[caste].value
if affectedCreature == unitRaceName and (affectedCaste == casteName or affectedCaste == "ALL") then
affected = true
end
end
for _,unitClass in ipairs(unitClasses) do
for _,syndromeClass in ipairs(syndrome.syn_immune_class) do
if unitClass.value == syndromeClass.value then
affected = false
end
end
end
for caste,creature in ipairs(syndrome,syn_immune_creature) do
local immuneCreature = creature.value
local immuneCaste = syndrome.syn_immune_caste[caste].value
if immuneCreature == unitRaceName and (immuneCaste == casteName or immuneCaste == "ALL") then
affected = false
end
end
return affected
end
function infectWithSyndromeIfValidTarget(target,syndrome,resetPolicy)
if isValidTarget(unit,syndrome) then
infectWithSyndrome(target,syndrome,resetPolicy)
return true
else
return false
end
end
return _ENV

File diff suppressed because it is too large Load Diff

@ -0,0 +1,260 @@
--Spawnunit.lua
--create unit at pointer. Usage e.g. "spawnunit DWARF 0 Dwarfy"
--author Warmist, Runrusher?
--edited by expwnent
function findCasteIndex(race_id,casteName)
local cr=df.creature_raw.find(race_id)
for casteIndex,caste in ipairs(cr.caste) do
if caste.caste_id == casteName then
return casteIndex
end
end
return nil
end
function getCaste(race_id,caste_id)
local cr=df.creature_raw.find(race_id)
return cr.caste[caste_id]
end
function genBodyModifier(body_app_mod)
local a=math.random(0,#body_app_mod.ranges-2)
return math.random(body_app_mod.ranges[a],body_app_mod.ranges[a+1])
end
function getBodySize(caste,time)
--todo real body size...
return caste.body_size_1[#caste.body_size_1-1] --returns last body size
end
function genAttribute(array)
local a=math.random(0,#array-2)
return math.random(array[a],array[a+1])
end
function norm()
return math.sqrt((-2)*math.log(math.random()))*math.cos(2*math.pi*math.random())
end
function normalDistributed(mean,sigma)
return mean+sigma*norm()
end
function clampedNormal(min,median,max)
local val=normalDistributed(median,math.sqrt(max-min))
if val<min then return min end
if val>max then return max end
return val
end
function makeSoul(unit,caste)
local tmp_soul=df.unit_soul:new()
tmp_soul.unit_id=unit.id
tmp_soul.name:assign(unit.name)
tmp_soul.race=unit.race
tmp_soul.sex=unit.sex
tmp_soul.caste=unit.caste
--todo skills,preferences,traits.
local attrs=caste.attributes
for k,v in pairs(attrs.ment_att_range) do
local max_percent=attrs.ment_att_cap_perc[k]/100
local cvalue=genAttribute(v)
tmp_soul.mental_attrs[k]={value=cvalue,max_value=cvalue*max_percent}
end
for k,v in pairs(tmp_soul.traits) do
local min,mean,max
min=caste.personality.a[k]
mean=caste.personality.b[k]
max=caste.personality.c[k]
tmp_soul.traits[k]=clampedNormal(min,mean,max)
end
unit.status.souls:insert("#",tmp_soul)
unit.status.current_soul=tmp_soul
end
function CreateUnit(race_id,caste_id)
local race=df.creature_raw.find(race_id)
if race==nil then error("Invalid race_id") end
local caste=getCaste(race_id,caste_id)
local unit=df.unit:new()
unit.race=race_id
unit.caste=caste_id
unit.id=df.global.unit_next_id
df.global.unit_next_id=df.global.unit_next_id+1
if caste.misc.maxage_max==-1 then
unit.relations.old_year=-1
else
unit.relations.old_year=math.random(caste.misc.maxage_min,caste.misc.maxage_max)
end
unit.sex=caste.gender
local body=unit.body
body.body_plan=caste.body_info
local body_part_count=#body.body_plan.body_parts
local layer_count=#body.body_plan.layer_part
--components
unit.relations.birth_year=df.global.cur_year
--unit.relations.birth_time=??
--unit.relations.old_time=?? --TODO add normal age
local cp=body.components
cp.body_part_status:resize(body_part_count)
cp.numbered_masks:resize(#body.body_plan.numbered_masks)
for num,v in ipairs(body.body_plan.numbered_masks) do
cp.numbered_masks[num]=v
end
cp.layer_status:resize(layer_count)
cp.layer_wound_area:resize(layer_count)
cp.layer_cut_fraction:resize(layer_count)
cp.layer_dent_fraction:resize(layer_count)
cp.layer_effect_fraction:resize(layer_count)
local attrs=caste.attributes
for k,v in pairs(attrs.phys_att_range) do
local max_percent=attrs.phys_att_cap_perc[k]/100
local cvalue=genAttribute(v)
unit.body.physical_attrs[k]={value=cvalue,max_value=cvalue*max_percent}
--unit.body.physical_attrs:insert(k,{new=true,max_value=genMaxAttribute(v),value=genAttribute(v)})
end
body.blood_max=getBodySize(caste,0) --TODO normal values
body.blood_count=body.blood_max
body.infection_level=0
unit.status2.body_part_temperature:resize(body_part_count)
for k,v in pairs(unit.status2.body_part_temperature) do
unit.status2.body_part_temperature[k]={new=true,whole=10067,fraction=0}
end
--------------------
local stuff=unit.enemy
stuff.body_part_878:resize(body_part_count) -- all = 3
stuff.body_part_888:resize(body_part_count) -- all = 3
stuff.body_part_relsize:resize(body_part_count) -- all =0
--TODO add correct sizes. (calculate from age)
local size=caste.body_size_2[#caste.body_size_2-1]
body.size_info.size_cur=size
body.size_info.size_base=size
body.size_info.area_cur=math.pow(size,0.666)
body.size_info.area_base=math.pow(size,0.666)
body.size_info.area_cur=math.pow(size*10000,0.333)
body.size_info.area_base=math.pow(size*10000,0.333)
stuff.were_race=race_id
stuff.were_caste=caste_id
stuff.normal_race=race_id
stuff.normal_caste=caste_id
stuff.body_part_8a8:resize(body_part_count) -- all = 1
stuff.body_part_base_ins:resize(body_part_count)
stuff.body_part_clothing_ins:resize(body_part_count)
stuff.body_part_8d8:resize(body_part_count)
unit.recuperation.healing_rate:resize(layer_count)
--appearance
local app=unit.appearance
app.body_modifiers:resize(#caste.body_appearance_modifiers) --3
for k,v in pairs(app.body_modifiers) do
app.body_modifiers[k]=genBodyModifier(caste.body_appearance_modifiers[k])
end
app.bp_modifiers:resize(#caste.bp_appearance.modifier_idx) --0
for k,v in pairs(app.bp_modifiers) do
app.bp_modifiers[k]=genBodyModifier(caste.bp_appearance.modifiers[caste.bp_appearance.modifier_idx[k]])
end
--app.unk_4c8:resize(33)--33
app.tissue_style:resize(#caste.bp_appearance.style_part_idx)
app.tissue_style_civ_id:resize(#caste.bp_appearance.style_part_idx)
app.tissue_style_id:resize(#caste.bp_appearance.style_part_idx)
app.tissue_style_type:resize(#caste.bp_appearance.style_part_idx)
app.tissue_length:resize(#caste.bp_appearance.style_part_idx)
app.genes.appearance:resize(#caste.body_appearance_modifiers+#caste.bp_appearance.modifiers) --3
app.genes.colors:resize(#caste.color_modifiers*2) --???
app.colors:resize(#caste.color_modifiers)--3
makeSoul(unit,caste)
df.global.world.units.all:insert("#",unit)
df.global.world.units.active:insert("#",unit)
--todo set weapon bodypart
local num_inter=#caste.body_info.interactions
--used to be anon_5 and anon_6: I guessed at what those were before the df-structures update. It seems to work at least a bit. ~expwnent
unit.curse.own_interaction:resize(num_inter)
unit.curse.own_interaction_delay:resize(num_inter)
return unit
end
function findRace(name)
for k,v in pairs(df.global.world.raws.creatures.all) do
if v.creature_id==name then
return k
end
end
qerror("Race:"..name.." not found!")
end
function PlaceUnit(raceName,casteName,name,position)
local pos
if position.x==-30000 then
pos = copyall(df.global.cursor)
else
pos = position
end
if pos.x==-30000 then
qerror("Spawnunit: specify location or place the cursor where you want the unit to be created.")
end
local race=findRace(raceName)
local caste=findCasteIndex(race,casteName)
local u=CreateUnit(race,tonumber(caste) or 0)
u.pos:assign(pos)
if name then
u.name.first_name=name
u.name.has_name=true
end
u.civ_id=df.global.ui.civ_id
local desig,ocupan=dfhack.maps.getTileFlags(pos)
ocupan.unit=true
--createNemesis(u)
end
function createFigure(trgunit)
local hf=df.historical_figure:new()
hf.id=df.global.hist_figure_next_id
hf.race=trgunit.race
hf.caste=trgunit.caste
df.global.hist_figure_next_id=df.global.hist_figure_next_id+1
hf.name.first_name=trgunit.name.first_name
hf.name.has_name=true
df.global.world.history.figures:insert("#",hf)
return hf
end
function createNemesis(trgunit)
local id=df.global.nemesis_next_id
local nem=df.nemesis_record:new()
nem.id=id
nem.unit_id=trgunit.id
nem.unit=trgunit
nem.flags:resize(1)
nem.flags[4]=true
nem.flags[5]=true
nem.flags[6]=true
nem.flags[7]=true
nem.flags[8]=true
nem.flags[9]=true
--[[for k=4,8 do
nem.flags[k]=true
end]]
df.global.world.nemesis.all:insert("#",nem)
df.global.nemesis_next_id=id+1
trgunit.general_refs:insert("#",{new=df.general_ref_is_nemesisst,nemesis_id=id})
trgunit.flags1.important_historical_figure=true
local gen=df.global.world.worldgen
nem.save_file_id=gen.next_unit_chunk_id;
gen.next_unit_chunk_id=gen.next_unit_chunk_id+1
gen.next_unit_chunk_offset=gen.next_unit_chunk_offset+1
--[[ local gen=df.global.world.worldgen
gen.next_unit_chunk_id
gen.next_unit_chunk_offset
]]
nem.figure=createFigure(trgunit)
end
args={...}
pos = df.new(df.coord)
if #args > 3 then
pos.x = tonumber(args[4]) or -30000
pos.y = tonumber(args[5]) or -30000
pos.z = tonumber(args[6]) or -30000
end
PlaceUnit(args[1],args[2],args[3],pos)

@ -0,0 +1,48 @@
--blooddel.lua
--makes it so that civs won't come with barrels full of blood, ichor, or goo
--author Urist Da Vinci
--edited by expwnent
local my_entity=df.historical_entity.find(df.global.ui.civ_id)
local sText=" "
local k=0
local v=1
for x,y in pairs(df.global.world.entities.all) do
my_entity=y
k=0
while k < #my_entity.resources.misc_mat.extracts.mat_index do
v=my_entity.resources.misc_mat.extracts.mat_type[k]
sText=dfhack.matinfo.decode(v,my_entity.resources.misc_mat.extracts.mat_index[k])
if (sText==nil) then
--LIQUID barrels
my_entity.resources.misc_mat.extracts.mat_type:erase(k)
my_entity.resources.misc_mat.extracts.mat_index:erase(k)
k=k-1
else
if(sText.material.id=="BLOOD") then
my_entity.resources.misc_mat.extracts.mat_type:erase(k)
my_entity.resources.misc_mat.extracts.mat_index:erase(k)
k=k-1
end
if(sText.material.id=="ICHOR") then
my_entity.resources.misc_mat.extracts.mat_type:erase(k)
my_entity.resources.misc_mat.extracts.mat_index:erase(k)
k=k-1
end
if(sText.material.id=="GOO") then
my_entity.resources.misc_mat.extracts.mat_type:erase(k)
my_entity.resources.misc_mat.extracts.mat_index:erase(k)
k=k-1
end
--VENOM
--POISON
--FLUID
--MILK
--EXTRACT
end
k=k+1
end
end

@ -0,0 +1,37 @@
-- feeding-timers.lua
-- original author: tejón
-- rewritten by expwnent
-- see repeat.lua for how to run this every so often automatically
local args = {...}
if args[1] ~= nil then
print("feeding-timers usage")
print(" feeding-timers")
print(" reset the feeding timers of all units as appropriate")
print(" feeding-timers help")
print(" print this help message")
print(" repeat enable [n] [years/months/ticks/days/etc] feeding-timers")
print(" run this script every n time units")
print(" repeat disable feeding-timers")
print(" stop automatically running this script")
return
end
local fixcount = 0
for _,unit in ipairs(df.global.world.units.all) do
if dfhack.units.isCitizen(unit) and not (unit.flags1.dead) then
for _,v in pairs(unit.status.misc_traits) do
local didfix = 0
if v.id == 0 then -- I think this should have additional conditions...
v.value = 0 -- GiveWater cooldown set to zero
didfix = 1
end
if v.id == 1 then -- I think this should have additional conditions...
v.value = 0 -- GiveFood cooldown set to zero
didfix = 1
end
fixcount = fixcount + didfix
end
end
end
print("Fixed feeding timers for " .. fixcount .. " citizens.")

@ -0,0 +1,84 @@
-- force.lua
-- author Putnam
-- edited by expwnent
-- Forces an event.
local function findCiv(arg)
local entities = df.global.world.entities.all
if tonumber(arg) then return arg end
if arg then
for eid,entity in ipairs(entities) do
if entity.entity_raw.code == arg then return entity end
end
end
return nil
end
local args = {...}
if not args or not args[1] then
qerror('Needs an argument. Valid arguments are caravan, migrants, diplomat, megabeast, curiousbeast, mischievousbeast, flier, siege and nightcreature. Second argument is civ, either raw entity ID or "player" for player\'s civ.')
end
local eventType = string.lower(args[1])
forceEntity = args[2]=="player" and df.historical_entity.find(df.global.ui.civ_id) or findCiv(args[2])
if (eventType == "caravan" or eventType == "diplomat" or eventType == "siege") and not forceEntity then
qerror('caravan, diplomat and siege require a civilization ID to be included.')
end
local function eventTypeIsNotValid()
local eventTypes = {
"caravan",
"migrants",
"diplomat",
"megabeast",
"curiousbeast",
"mischevousbeast",
"mischeviousbeast",
"flier",
"siege",
"nightcreature"
}
for _,v in ipairs(eventTypes) do
if args[1] == v then return false end
end
return true
end
--code may be kind of bad below :V Putnam ain't experienced in lua... --Putnam's comment, not mine ~expwnent
if eventTypeIsNotValid() then
qerror('Invalid argument. Valid arguments are caravan, migrants, diplomat, megabeast, curiousbeast, mischievousbeast, flier, siege and nightcreature.')
end
allEventTypes={}
allEventTypes["megabeast"]=function()
df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.Megabeast, season = df.global.cur_season, season_ticks = df.global.cur_season_tick } )
end
allEventTypes["migrants"]=function()
df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.Migrants, season = df.global.cur_season, season_ticks = df.global.cur_season_tick, entity = df.global.world.entities.all[df.global.ui.civ_id] } )
end
allEventTypes["caravan"]=function()
df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.Caravan, season = df.global.cur_season, season_ticks = df.global.cur_season_tick, entity = forceEntity } )
end
allEventTypes["diplomat"]=function()
df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.Diplomat, season = df.global.cur_season, season_ticks = df.global.cur_season_tick, entity = forceEntity } )
end
allEventTypes["curious"]=function()
df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.WildlifeCurious, season = df.global.cur_season, season_ticks = df.global.cur_season_tick } )
end
allEventTypes["mischevousbeast"]=function()
df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.WildlifeMichievous, season = df.global.cur_season, season_ticks = df.global.cur_season_tick } )
end
allEventTypes["flier"]=function()
df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.WildlifeFlier, season = df.global.cur_season, season_ticks = df.global.cur_season_tick } )
end
allEventTypes["siege"]=function()
df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.CivAttack, season = df.global.cur_season, season_ticks = df.global.cur_season_tick, entity = forceEntity } )
end
allEventTypes["nightcreature"]=function()
df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.NightCreature, season = df.global.cur_season, season_ticks = df.global.cur_season_tick } )
end
allEventTypes[eventType]()

@ -0,0 +1,125 @@
-- Save a copy of a text screen for the DF forums. Use 'forumdwarves help' for more details.
-- original author: Caldfir
-- edited by expwnent
local args = {...}
if args[1] == 'help' then
print([[
description:
This script will attempt to read the current df-screen, and if it is a
text-viewscreen (such as the dwarf 'thoughts' screen or an item
'description') then append a marked-up version of this text to the
target file. Previous entries in the file are not overwritten, so you
may use the 'forumdwarves' command multiple times to create a single
document containing the text from multiple screens (eg: text screens
from several dwarves, or text screens from multiple artifacts/items,
or some combination).
known screens:
The screens which have been tested and known to function properly with
this script are:
1: dwarf/unit 'thoughts' screen
2: item/art 'description' screen
3: individual 'historical item/figure' screens
There may be other screens to which the script applies. It should be
safe to attempt running the script with any screen active, with an
error message to inform you when the selected screen is not appropriate
for this script.
target file:
The target file's name is 'forumdwarves.txt'. A remider to this effect
will be displayed if the script is successful.
character encoding:
The text will likely be using system-default encoding, and as such
will likely NOT display special characters (eg:é,õ,ç) correctly. To
fix this, you need to modify the character set that you are reading
the document with. 'Notepad++' is a freely available program which
can do this using the following steps:
1: open the document in Notepad++
2: in the menu-bar, select
Encoding->Character Sets->Western European->OEM-US
3: copy the text normally to wherever you want to use it
]])
return
end
local utils = require 'utils'
local gui = require 'gui'
local dialog = require 'gui.dialogs'
local colors_css = {
[0] = 'black',
[1] = 'navy',
[2] = 'green',
[3] = 'teal',
[4] = 'maroon',
[5] = 'purple',
[6] = 'olive',
[7] = 'silver',
[8] = 'gray',
[9] = 'blue',
[10] = 'lime',
[11] = 'cyan',
[12] = 'red',
[13] = 'magenta',
[14] = 'yellow',
[15] = 'white'
}
local scrn = dfhack.gui.getCurViewscreen()
local flerb = dfhack.gui.getFocusString(scrn)
local function format_for_forum(strin)
local strout = strin
local newline_idx = string.find(strout, '[P]', 1, true)
while newline_idx ~= nil do
strout = string.sub(strout,1, newline_idx-1)..'\n'..string.sub(strout,newline_idx+3)
newline_idx = string.find(strout, '[P]', 1, true)
end
newline_idx = string.find(strout, '[B]', 1, true)
while newline_idx ~= nil do
strout = string.sub(strout,1, newline_idx-1)..'\n'..string.sub(strout,newline_idx+3)
newline_idx = string.find(strout, '[B]', 1, true)
end
newline_idx = string.find(strout, '[R]', 1, true)
while newline_idx ~= nil do
strout = string.sub(strout,1, newline_idx-1)..'\n'..string.sub(strout,newline_idx+3)
newline_idx = string.find(strout, '[R]', 1, true)
end
local color_idx = string.find(strout, '[C:', 1, true)
while color_idx ~= nil do
local colormatch = (string.byte(strout, color_idx+3)-48)+((string.byte(strout, color_idx+7)-48)*8)
strout = string.sub(strout,1, color_idx-1)..'[/color][color='..colors_css[colormatch]..']'..string.sub(strout,color_idx+9)
color_idx = string.find(strout, '[C:', 1, true)
end
return strout
end
if flerb == 'textviewer' then
print(scrn)
printall(scrn)
local lines = scrn.formatted_text
local line = ""
if lines ~= nil then
local log = io.open('forumdwarves.txt', 'a')
log:write("[color=silver]")
for n,x in ipairs(lines) do
print(x)
printall(x)
print(x.text)
printall(x.text)
if (x ~= nil) and (x.text ~= nil) then
log:write(format_for_forum(x.text), ' ')
--log:write(x[0],'\n')
end
end
log:write("[/color]\n")
log:close()
end
print 'data prepared for forum in \"forumdwarves.txt\"'
else
print 'this is not a textview screen'
end

@ -0,0 +1,127 @@
--fullheal.lua
--author Kurik Amudnil, Urist DaVinci
--edited by expwnent
-- attempt to fully heal a selected unit, option -r to attempt to resurrect the unit
local args = {...}
local resurrect = false
local i=0
for _,arg in ipairs(args) do
if arg == '-r' or arg == '-R' then
resurrect = true
elseif tonumber(arg) then
unit = df.unit.find(tonumber(arg))
elseif arg == 'help' or arg == '-help' or arg == '-h' then
print('fullheal: heal a unit completely from anything, optionally including death.')
print(' fullheal [unitId]')
print(' heal the unit with the given id')
print(' fullheal -r [unitId]')
print(' heal the unit with the given id and bring them back from death if they are dead')
print(' fullheal')
print(' heal the currently selected unit')
print(' fullheal -r')
print(' heal the currently selected unit and bring them back from death if they are dead')
print(' fullheal help')
print(' print this help message')
return
end
end
unit = unit or dfhack.gui.getSelectedUnit()
if not unit then
qerror('Error: please select a unit or pass its id as an argument.')
end
if unit then
if resurrect then
if unit.flags1.dead then
--print("Resurrecting...")
unit.flags2.slaughter = false
unit.flags3.scuttle = false
end
unit.flags1.dead = false
unit.flags2.killed = false
unit.flags3.ghostly = false
--unit.unk_100 = 3
end
--print("Erasing wounds...")
while #unit.body.wounds > 0 do
unit.body.wounds:erase(#unit.body.wounds-1)
end
unit.body.wound_next_id=1
--print("Refilling blood...")
unit.body.blood_count=unit.body.blood_max
--print("Resetting grasp/stand status...")
unit.status2.limbs_stand_count=unit.status2.limbs_stand_max
unit.status2.limbs_grasp_count=unit.status2.limbs_grasp_max
--print("Resetting status flags...")
unit.flags2.has_breaks=false
unit.flags2.gutted=false
unit.flags2.circulatory_spray=false
unit.flags2.vision_good=true
unit.flags2.vision_damaged=false
unit.flags2.vision_missing=false
unit.flags2.breathing_good=true
unit.flags2.breathing_problem=false
unit.flags2.calculated_nerves=false
unit.flags2.calculated_bodyparts=false
unit.flags2.calculated_insulation=false
unit.flags3.compute_health=true
--print("Resetting counters...")
unit.counters.winded=0
unit.counters.stunned=0
unit.counters.unconscious=0
unit.counters.webbed=0
unit.counters.pain=0
unit.counters.nausea=0
unit.counters.dizziness=0
unit.counters2.paralysis=0
unit.counters2.fever=0
unit.counters2.exhaustion=0
unit.counters2.hunger_timer=0
unit.counters2.thirst_timer=0
unit.counters2.sleepiness_timer=0
unit.counters2.vomit_timeout=0
--print("Resetting body part status...")
v=unit.body.components
for i=0,#v.nonsolid_remaining - 1,1 do
v.nonsolid_remaining[i] = 100 -- percent remaining of fluid layers (Urist Da Vinci)
end
v=unit.body.components
for i=0,#v.layer_wound_area - 1,1 do
v.layer_status[i].whole = 0 -- severed, leaking layers (Urist Da Vinci)
v.layer_wound_area[i] = 0 -- wound contact areas (Urist Da Vinci)
v.layer_cut_fraction[i] = 0 -- 100*surface percentage of cuts/fractures on the body part layer (Urist Da Vinci)
v.layer_dent_fraction[i] = 0 -- 100*surface percentage of dents on the body part layer (Urist Da Vinci)
v.layer_effect_fraction[i] = 0 -- 100*surface percentage of "effects" on the body part layer (Urist Da Vinci)
end
v=unit.body.components.body_part_status
for i=0,#v-1,1 do
v[i].on_fire = false
v[i].missing = false
v[i].organ_loss = false
v[i].organ_damage = false
v[i].muscle_loss = false
v[i].muscle_damage = false
v[i].bone_loss = false
v[i].bone_damage = false
v[i].skin_damage = false
v[i].motor_nerve_severed = false
v[i].sensory_nerve_severed = false
end
if unit.job.current_job and unit.job.current_job.job_type == df.job_type.Rest then
--print("Wake from rest -> clean self...")
unit.job.current_job = df.job_type.CleanSelf
end
end

@ -0,0 +1,26 @@
--growthbug: units only grow when the current tick is 0 mod 10, so only 1/10 units will grow naturally. this script periodically sets the birth time of each unit so that it will grow
--to run periodically, use "repeat enable 2 months growthBug now". see repeat.lua for details
-- author expwnent
local args = {...}
if args[1] ~= nil then
print("growthbug usage")
print(" growthbug")
print(" fix the growth bug for all units on the map")
print(" growthbug help")
print(" print this help message")
print(" repeat enable [n] [years/months/ticks/days/etc] growthbug")
print(" run this script every n time units")
print(" repeat disable growthbug")
print(" stop automatically running this script")
end
local count = 0
for _,unit in ipairs(df.world.units.all) do
local offset = unit.relations.birth_time % 10;
if offset ~= 0 then
count = count+1
unit.relations.birth_time = unit.relations.birth_time - offset
end
end
print("Fixed growth bug for "..count.." units.")

@ -0,0 +1,251 @@
-- hackWish.lua
-- Allows for script-based wishing.
-- author Putnam
-- edited by expwnent
function getGenderString(gender)
local genderStr
if gender==0 then
genderStr=string.char(12)
elseif gender==1 then
genderStr=string.char(11)
else
return ""
end
return string.char(40)..genderStr..string.char(41)
end
function getCreatureList()
local crList={}
for k,cr in ipairs(df.global.world.raws.creatures.alphabetic) do
for kk,ca in ipairs(cr.caste) do
local str=ca.caste_name[0]
str=str..' '..getGenderString(ca.gender)
table.insert(crList,{str,nil,ca})
end
end
return crList
end
function getMatFilter(itemtype)
local itemTypes={
SEEDS=function(mat,parent,typ,idx)
return mat.flags.SEED_MAT
end,
PLANT=function(mat,parent,typ,idx)
return mat.flags.STRUCTURAL_PLANT_MAT
end,
LEAVES=function(mat,parent,typ,idx)
return mat.flags.LEAF_MAT
end,
MEAT=function(mat,parent,typ,idx)
return mat.flags.MEAT
end,
CHEESE=function(mat,parent,typ,idx)
return (mat.flags.CHEESE_PLANT or mat.flags.CHEESE_CREATURE)
end,
LIQUID_MISC=function(mat,parent,typ,idx)
return (mat.flags.LIQUID_MISC_PLANT or mat.flags.LIQUID_MISC_CREATURE or mat.flags.LIQUID_MISC_OTHER)
end,
POWDER_MISC=function(mat,parent,typ,idx)
return (mat.flags.POWDER_MISC_PLANT or mat.flags.POWDER_MISC_CREATURE)
end,
DRINK=function(mat,parent,typ,idx)
return (mat.flags.ALCOHOL_PLANT or mat.flags.ALCOHOL_CREATURE)
end,
GLOB=function(mat,parent,typ,idx)
return (mat.flags.STOCKPILE_GLOB)
end,
WOOD=function(mat,parent,typ,idx)
return (mat.flags.WOOD)
end,
THREAD=function(mat,parent,typ,idx)
return (mat.flags.THREAD_PLANT)
end,
LEATHER=function(mat,parent,typ,idx)
return (mat.flags.LEATHER)
end
}
return itemTypes[df.item_type[itemtype]] or getRestrictiveMatFilter(itemtype)
end
function getRestrictiveMatFilter(itemType)
if not args.veryRestrictive then return nil else
local itemTypes={
WEAPON=function(mat,parent,typ,idx)
return (mat.flags.ITEMS_WEAPON or mat.flags.ITEMS_WEAPON_RANGED)
end,
AMMO=function(mat,parent,typ,idx)
return (mat.flags.ITEMS_AMMO)
end,
ARMOR=function(mat,parent,typ,idx)
return (mat.flags.ITEMS_ARMOR)
end,
SHOES,SHIELD,HELM,GLOVES=ARMOR,ARMOR,ARMOR,ARMOR,
INSTRUMENT=function(mat,parent,typ,idx)
return (mat.flags.ITEMS_HARD)
end,
GOBLET,FLASK,TOY,RING,CROWN,SCEPTER,FIGURINE,TOOL=INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,
AMULET=function(mat,parent,typ,idx)
return (mat.flags.ITEMS_SOFT or mat.flags.ITEMS_HARD)
end,
EARRING,BRACELET=AMULET,AMULET,
ROCK=function(mat,parent,typ,idx)
return (mat.flags.IS_STONE)
end,
BOULDER=ROCK,
BAR=function(mat,parent,typ,idx)
return (mat.flags.IS_METAL or mat.flags.SOAP or mat.id==COAL)
end
}
return itemTypes[df.item_type[itemType]]
end
end
function createItem(mat,itemType,quality,pos,description)
local item=df[df.item_type.attrs[itemType[1]].classname]:new()
item.id=df.global.item_next_id
df.global.world.items.all:insert('#',item)
df.global.item_next_id=df.global.item_next_id+1
item:setSubtype(itemType[2])
item:setMaterial(mat[1])
item:setMaterialIndex(mat[2])
if df.item_type[itemType[1]]=='EGG' then
local creature=df.creature_raw.find(mat[1])
local eggMat={}
eggMat[1]=dfhack.matinfo.find(creature.creature_id..':EGGSHELL')
if eggMat[1] then
eggMat[2]=dfhack.matinfo.find(creature.creature_id..':EGG_WHITE')
eggMat[3]=dfhack.matinfo.find(creature.creature_id..'EGG_YOLK')
for k,v in ipairs(eggMat) do
item.egg_materials.mat_type:insert('#',v.type)
item.egg_materials.mat_index:insert('#',v.index)
end
else
eggMat=dfhack.matinfo.find(creature.creature_id..':MUSCLE')
item.egg_materials.mat_type:insert('#',eggMat.type)
item.egg_materials.mat_index:insert('#',eggMat.index)
end
end
item:categorize(true)
item.flags.removed=true
item:setSharpness(1,0)
item:setQuality(quality-1)
if df.item_type[itemType[1]]=='SLAB' then
item.description=description
end
dfhack.items.moveToGround(item,{x=pos.x,y=pos.y,z=pos.z})
end
--TODO: should this be a function?
function qualityTable()
return {{'None'},
{'-Well-crafted-'},
{'+Finely-crafted+'},
{'*Superior*'},
{string.char(240)..'Exceptional'..string.char(240)},
{string.char(15)..'Masterwork'..string.char(15)}
}
end
local script=require('gui/script')
local guimaterials=require('gui.materials')
function showItemPrompt(text,item_filter,hide_none)
guimaterials.ItemTypeDialog{
prompt=text,
item_filter=item_filter,
hide_none=hide_none,
on_select=script.mkresume(true),
on_cancel=script.mkresume(false),
on_close=script.qresume(nil)
}:show()
return script.wait()
end
function showMaterialPrompt(title, prompt, filter, inorganic, creature, plant) --the one included with DFHack doesn't have a filter or the inorganic, creature, plant things available
guimaterials.MaterialDialog{
frame_title = title,
prompt = prompt,
mat_filter = filter,
use_inorganic = inorganic,
use_creature = creature,
use_plant = plant,
on_select = script.mkresume(true),
on_cancel = script.mkresume(false),
on_close = script.qresume(nil)
}:show()
return script.wait()
end
function usesCreature(itemtype)
typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PET=true,EGG=true,CORPSE=true,CORPSEPIECE=true}
return typesThatUseCreatures[df.item_type[itemtype]]
end
function getCreatureRaceAndCaste(caste)
return df.global.world.raws.creatures.list_creature[caste.index],df.global.world.raws.creatures.list_caste[caste.index]
end
function hackWish(posOrUnit)
local pos = df.unit:is_instance(posOrUnit) and posOrUnit.pos or posOrUnit
script.start(function()
--local amountok, amount
local matok,mattype,matindex,matFilter
local itemok,itemtype,itemsubtype=showItemPrompt('What item do you want?',function(itype) return df.item_type[itype]~='CORPSE' and df.item_type[itype]~='FOOD' end ,true)
if not args.notRestrictive then
matFilter=getMatFilter(itemtype)
end
if not usesCreature(itemtype) then
matok,mattype,matindex=showMaterialPrompt('Wish','And what material should it be made of?',matFilter)
else
local creatureok,useless,creatureTable=script.showListPrompt('Wish','What creature should it be?',COLOR_LIGHTGREEN,getCreatureList())
mattype,matindex=getCreatureRaceAndCaste(creatureTable[3])
end
local qualityok,quality=script.showListPrompt('Wish','What quality should it be?',COLOR_LIGHTGREEN,qualityTable())
local description
if df.item_type[itemtype]=='SLAB' then
local descriptionok
descriptionok,description=script.showInputPrompt('Slab','What should the slab say?',COLOR_WHITE)
end
--repeat amountok,amount=script.showInputPrompt('Wish','How many do you want? (numbers only!)',COLOR_LIGHTGREEN) until tonumber(amount)
if mattype and itemtype then
--for i=1,tonumber(amount) do
createItem({mattype,matindex},{itemtype,itemsubtype},quality,pos,description)
--end
end
end)
end
scriptArgs={...}
args={}
for k,v in ipairs(scriptArgs) do
v=v:lower()
if v=='startup' then args.startup=true end
if v=='all' then args.notRestrictive=true end
if v=='restrictive' then args.veryRestrictive=true end
if v=='unit' then args.unitNum=args[k+1] end
if v=='x' then args.x=args[k+1] end
if v=='y' then args.y=args[k+1] end
if v=='z' then args.z=args[k+1] end
end
eventful=require('plugins.eventful')
function posIsValid(pos)
return pos.x~=-30000 and pos or false
end
if not args.startup then
local posOrUnit=args.x and {x=args.x,y=args.y,z=args.z} or args.unitNum and df.unit.find(args.unitNum) or posIsValid(df.global.cursor) or dfhack.gui.getSelectedUnit(true)
hackWish(posOrUnit)
else
eventful.onReactionComplete.hackWishP=function(reaction,unit,input_items,input_reagents,output_items, call_native)
if not reaction.code:find('DFHACK_WISH') then return nil end
hackWish(unit)
end
end

@ -0,0 +1,223 @@
-- itemsyndrome.lua
-- author: Putnam
-- edited by expwnent
-- Checks for inventory changes and applies or removes syndromes that items or their materials have. Use "disable" (minus quotes) to disable and "help" to get help.
eventful=eventful or require("plugins.eventful")
syndromeUtil = syndromeUtil or require("plugins.syndromeUtil")
local function printItemSyndromeHelp()
print("Arguments (non case-sensitive):")
print(' "help": displays this dialogue.')
print(" ")
print(' "disable": disables the script.')
print(" ")
print(' "debugon/debugoff": debug mode.')
print(" ")
print(' "contaminantson/contaminantsoff": toggles searching for contaminants.')
print(' Disabling speeds itemsyndrome up greatly.')
print(' "transformReEquipOn/TransformReEquipOff": toggles transformation auto-reequip.')
end
itemsyndromedebug=false
local args = {...}
for k,v in ipairs(args) do
v=v:lower()
if v == "help" then printItemSyndromeHelp() return end
if v == "debugon" then itemsyndromedebug = true end
if v == "debugoff" then itemsyndromedebug = false end
if v == "contaminantson" then itemsyndromecontaminants = true end
if v == "contaminantsoff" then itemsyndromecontaminants = false end
if v == "transformreequipon" then transformationReEquip = true end
if v == "transformreequipoff" then transformationReEquip = false end
end
local function getMaterial(item)
--What does this line mean?
local material = dfhack.matinfo.decode(item) and dfhack.matinfo.decode(item) or false
if not material then return nil end
if material.mode ~= "inorganic" then
return nil
else
return material.material --the "material" thing up there contains a bit more info which is all pretty important but impertinent, like the creature that the material comes from
end
end
local function findItemSyndromeInorganic()
local allInorganics = {}
for matID,material in ipairs(df.global.world.raws.inorganics) do
if string.find(material.id,"DFHACK_ITEMSYNDROME_MATERIAL_") then table.insert(allInorganics,matID) end --the last underscore is needed to prevent duped raws; I want good modder courtesy if it kills me, dammit!
end
if itemsyndromedebug then printall(allInorganics) end
if #allInorganics>0 then return allInorganics else return nil end
end
local function getAllItemSyndromeMats(itemSyndromeMatIDs)
local allActualInorganics = {}
for _,itemSyndromeMatID in ipairs(itemSyndromeMatIDs) do
table.insert(allActualInorganics,df.global.world.raws.inorganics[itemSyndromeMatID].material)
end
if itemsyndromedebug then printall(allActualInorganics) end
return allActualInorganics
end
local function syndromeIsDfHackSyndrome(syndrome)
for k,v in ipairs(syndrome.syn_class) do
if v.value=="DFHACK_ITEM_SYNDROME" then
if itemsyndromedebug then print('Syndrome is DFHack syndrome, checking if creature is affected...') end
return true
end
end
if itemsyndromedebug then print('Syndrome is not DFHack syndrome. Cancelling.') end
return false
end
local function itemHasNoSubtype(item)
return item:getSubtype()==-1
end
local function itemHasSyndrome(item)
if itemHasNoSubtype(item) or not itemSyndromeMats then return nil end
local allItemSyndromes={}
for _,material in ipairs(itemSyndromeMats) do
for __,syndrome in ipairs(material.syndrome) do
if syndrome.syn_name == item.subtype.name then table.insert(allItemSyndromes,syndrome) end
end
end
return #allItemSyndromes>0 and allItemSyndromes or false
end
local function getValidPositions(syndrome)
local returnTable={AffectsHauler=false,AffectsStuckins=false,IsArmorOnly=false,IsWieldedOnly=false,OnlyAffectsStuckins=false}
for k,v in ipairs(syndrome.syn_class) do
if v.value:find('DFHACK') then
if v.value=="DFHACK_AFFECTS_HAULER" then returnTable.AffectsHauler=true end
if v.value=="DFHACK_AFFECTS_STUCKIN" then returnTable.AffectsStuckins=true end
if v.value=="DFHACK_STUCKINS_ONLY" then returnTable.OnlyAffectsStuckins=true end
if v.value=="DFHACK_WIELDED_ONLY" then returnTable.IsWieldedOnly=true end
if v.value=="DFHACK_ARMOR_ONLY" then returnTable.IsArmorOnly=true end
end
end
return returnTable
end
local function itemIsInValidPosition(item_inv, syndrome)
local item = getValidPositions(syndrome)
if not item_inv then print("You shouldn't see this error! At all! Putnam f'd up! Tell him off!") return false end
local isInValidPosition=not ((item_inv.mode == 0 and not item.AffectsHauler) or (item_inv.mode == 7 and not item.AffectsStuckins) or (item_inv.mode ~= 2 and item.IsArmorOnly) or (item_inv.mode ~=1 and item.IsWieldedOnly) or (item_inv.mode ~=7 and item.OnlyAffectsStuckins))
if itemsyndromedebug then print(isInValidPosition and 'Item is in correct position.' or 'Item is not in correct position.') end
return isInValidPosition
end
local function syndromeIsTransformation(syndrome)
for _,effect in ipairs(syndrome.ce) do
if df.creature_interaction_effect_body_transformationst:is_instance(effect) then return true end
end
return false
end
local function rememberInventory(unit)
local invCopy = {}
for inv_id,item_inv in ipairs(unit.inventory) do
invCopy[inv_id+1] = {}
local itemToWorkOn = invCopy[inv_id+1]
itemToWorkOn.item = item_inv.item
itemToWorkOn.mode = item_inv.mode
itemToWorkOn.body_part_id = item_inv.body_part_id
end
return invCopy
end
local function moveAllToInventory(unit,invTable)
for _,item_inv in ipairs(invTable) do
dfhack.items.moveToInventory(item_inv.item,unit,item_inv.mode,item_inv.body_part_id)
end
end
local function syndromeIsOnUnequip(syndrome)
for k,v in ipairs(syndrome.syn_class) do
if v.value:upper()=='DFHACK_ON_UNEQUIP' then return true end
end
return false
end
local function addOrRemoveSyndromeDepending(unit,old_equip,new_equip,syndrome)
local item_inv=new_equip or old_equip
if not syndromeIsDfHackSyndrome(syndrome) then
return
end
local equippedOld = itemIsInValidPosition(old_equip,syndrome)
local equippedNew = itemIsInValidPosition(new_equip,syndrome)
if equippedOld == equippedNew then
return
end
local isOnEquip = not syndromeIsOnUnequip(syndrome)
local apply = (isOnEquip and equippedNew) or (not isOnEquip and not equippedNew)
if apply then
syndromeUtil.infectWithSyndrome(unit,syndrome,syndromeUtil.ResetPolicy.ResetDuration)
else
syndromeUtil.eraseSyndrome(unit,syndrome)
end
end
eventful.enableEvent(eventful.eventType.INVENTORY_CHANGE,5)
eventful.onInventoryChange.itemsyndrome=function(unit_id,item_id,old_equip,new_equip)
local item = df.item.find(item_id)
--if not item then return false end
if not item then
return
end
local unit = df.unit.find(unit_id)
if unit.flags1.dead then return false end
if itemsyndromedebug then print("Checking unit #" .. unit_id) end
local transformation = false
if itemsyndromedebug then print("checking item #" .. item_id .." on unit #" .. unit_id) end
local itemMaterial=getMaterial(item)
local function manageSyndromes(syndromes)
for k,syndrome in ipairs(syndromes) do
if itemsyndromedebug then print("item has a syndrome, checking if syndrome is valid for application...") end
if syndromeIsTransformation(syndrome) then
--unitInventory = rememberInventory(unit)
rememberInventory(unit)
transformation = true
end
addOrRemoveSyndromeDepending(unit,old_equip,new_equip,syndrome)
end
end
if itemMaterial then
manageSyndromes(itemMaterial.syndrome)
end
local itemSyndromes = itemHasSyndrome(item)
if itemSyndromes then
if itemsyndromedebug then print("Item itself has a syndrome, checking if item is in correct position and creature is affected") end
manageSyndromes(itemSyndromes)
end
if itemsyndromecontaminants and item.contaminants then
if itemsyndromedebug then print("Item has contaminants. Checking for syndromes...") end
for _,contaminant in ipairs(item.contaminants) do
local contaminantMaterial=getMaterial(contaminant)
if contaminantMaterial then
manageSyndromes(contaminantMaterial.syndrome)
end
end
end
if transformation and transformationReEquip then dfhack.timeout(2,"ticks",function() moveAllToInventory(unit,unitInventory) end) end
end
dfhack.onStateChange.itemsyndrome=function(code)
if code==SC_WORLD_LOADED then
itemSyndromeMatIDs = findItemSyndromeInorganic()
if itemSyndromeMatIDs then itemSyndromeMats = getAllItemSyndromeMats(itemSyndromeMatIDs) end
end
end
if disable then
eventful.onInventoryChange.itemsyndrome=nil
print("Disabled itemsyndrome.")
disable = false
else
print("Enabled itemsyndrome.")
end

@ -0,0 +1,152 @@
-- moddableGods.lua
-- Sets player-defined gods to correct civilizations.
-- author: Putnam
-- edited by expwnent
--[[Here's an example of how to make a god:
[CREATURE:SHEOGORATH]
[DOES_NOT_EXIST]
[MALE]
[NAME:jovial man:Daedra:madness] "Sheogorath, madness god." "Often depicted as a jovial man"
[CASTE_NAME:Sheogorath:Sheogorath:Sheogorath]
[DESCRIPTION:The Daedric Prince of madness.]
[CREATURE_CLASS:DFHACK_GOD]
[SPHERE:MUSIC]
[SPHERE:ART]
[SPHERE:CHAOS]
]]
local function getCreatureClasses(creatureRaw)
local creatureClasses = {}
for _,caste in ipairs(creatureRaw.caste) do
for k,class in ipairs(caste.creature_class) do
table.insert(creatureClasses,class.value)
end
end
return creatureClasses
end
local function deityIsOfSpecialCreature(creatureRaw)
for k,class in ipairs(getCreatureClasses(creatureRaw)) do
if class=="DFHACK_GOD" then return true end
end
return false
end
local function scriptAlreadyRunOnThisWorld()
local figures = df.global.world.history.figures
for i=#figures-1,0,-1 do --goes through the table backwards because the particular hist figs involved are probably going to be the last
local figure = figures[i]
if not df.isnull(figure.flags) and figure.flags.deity and deityIsOfSpecialCreature(df.global.world.raws.creatures.all[figure.race]) then
return true
end
end
return false
end
local function findAGod()
for k,fig in ipairs(df.global.world.history.figures) do
if fig.flags.deity then
return fig
end
end
return nil
end
local function putListOfAllSpecialCreatureGodsTogether()
local specialCreatures = {}
for k,creatureRaw in ipairs(df.global.world.raws.creatures.all) do
if deityIsOfSpecialCreature(creatureRaw) then
table.insert(specialCreatures,{creatureRaw,k})
end
end
return specialCreatures
end
local function stringStarts(String,Start)
return string.sub(String,1,string.len(Start))==Start
end
local function getRacesOfGod(god)
local civList={}
for k,class in ipairs(getCreatureClasses(god)) do
if stringStarts(class,"WORSHIP_ENTITY_") then
table.insert(civList,string.sub(class,15))
end
end
return civList
end
local function entityIsInGodsDomain(entity,entityRacesTable)
for k,v in ipairs(entityRacesTable) do
if v==entity.entity_raw.code then
return true
end
end
return false
end
local function setUpTemplate(godFig,templateGod)
godFig.appeared_year=-1
godFig.born_year=-1
godFig.born_seconds=-1
godFig.curse_year=-1
godFig.curse_seconds=-1
godFig.old_year=-1
godFig.old_seconds=-1
godFig.died_year=-1
godFig.died_seconds=-1
godFig.name.has_name=true
godFig.breed_id=-1
godFig.flags:assign(templateGod.flags)
godFig.id = df.global.hist_figure_next_id
godFig.info = df.historical_figure_info:new()
godFig.info.spheres={new=true}
godFig.info.secret=df.historical_figure_info.T_secret:new()
end
local function setUpGod(god,godID,templateGod)
local godFig = df.historical_figure:new()
setUpTemplate(godFig,templateGod)
godFig.sex=god.caste[0].gender
godFig.race=godID
godFig.name.first_name=god.caste[0].caste_name[2] --the adjectival form of the caste_name is used for the god's name, E.G, [CASTE_NAME:god:god:armok]
for k,v in ipairs(god.sphere) do --assigning spheres
godFig.info.spheres:insert('#',v)
end
df.global.world.history.figures:insert('#',godFig)
df.global.hist_figure_next_id=df.global.hist_figure_next_id+1
return godFig
end
--[[this function isn't really working right now so it's dummied out
function setGodAsOfficialDeityOfItsParticularEntity(god,godFig)
local entityRaces=getRacesOfGod(god)
for k,entity in ipairs(df.global.world.entities.all) do
if entityIsInGodsDomain(entity,entityRaces) then
entity.unknown1b.worship:insert('#',godFig.id)
end
end
end
]]
local function moddableGods()
if scriptAlreadyRunOnThisWorld() then
print("Already run on world...")
return false
end
local gods = putListOfAllSpecialCreatureGodsTogether()
local templateGod=findAGod()
for k,v in ipairs(gods) do --creature raws first
local god = v[1]
local godID = v[2]
local godFig = setUpGod(god,godID,templateGod)
--setGodAsOfficialDeityOfItsParticularEntity(god,godFig)
end
end
dfhack.onStateChange.letThereBeModdableGods = function(state)
if state == SC_WORLD_LOADED and df.global.gamemode~=3 then --make sure that the gods show up only after the rest of the histfigs do
moddableGods()
end
end

@ -0,0 +1,19 @@
local onReport = require 'plugins.onReport'
reload('plugins.onReport')
onReport.triggers.onReportExample = function(reportId)
-- print('report '..reportId..' happened!')
local report = df.report.find(reportId)
if not report then
return
end
-- printall(report)
-- print('\n')
print(reportId .. ': ' .. df.announcement_type[report["type"]])
for unitId,_ in pairs(onReport.eventToDwarf[reportId]) do
print('relevant dwarf: ' .. unitId)
end
end

@ -0,0 +1,16 @@
local onStrike = require 'plugins.onStrike'
local eventful = require 'plugins.eventful'
--print(onStrike)
--onStrike.triggers.onStrikeExample = function(information)
-- print(information.attacker .. ' attacks ' .. information.defender .. ': ' .. information.announcement)
onStrike.triggers.onStrikeExample = function(attacker, defender, weapon, wound)
if weapon then
print(attacker.id..' weapon attacks '..defender.id .. ' with ' .. weapon.id)
--df.global.pause_state = true
else
print(attacker.id..' attacks '..defender.id)
end
end

@ -0,0 +1,9 @@
--printArgs.lua
--author expwnent
--prints all the arguments on their own line with quotes around them. useful for debugging
local args = {...}
print("printArgs")
for _,arg in ipairs(args) do
print("'"..arg.."'")
end

@ -0,0 +1,190 @@
-- projectileExpansion.lua
-- author: Putnam
-- edited by expwnent
-- Adds extra functionality to projectiles. Use the argument "disable" (minus quotes) to disable.
local syndromeUtil = require("syndromeUtil")
local events=require "plugins.eventful"
local flowtypes = {
miasma = 0,
mist = 1,
mist2 = 2,
dust = 3,
lavaMist = 4,
smoke = 5,
dragonFire = 6,
fireBreath = 7,
web = 8,
undirectedGas = 9,
undirectedVapor = 10,
oceanWave = 11,
seaFoam = 12
}
local function posIsEqual(pos1,pos2)
if pos1.x ~= pos2.x or pos1.y ~= pos2.y or pos1.z ~= pos2.z then return false end
return true
end
local function getMaterial(item)
if not dfhack.matinfo.decode(item) then return nil end
local matinfo = dfhack.matinfo.decode(item)
return matinfo and matinfo.material
-- return dfhack.matinfo.decode(item).material
end
local function getSyndrome(material)
if material==nil then return nil end
if #material.syndrome>0 then return material.syndrome[0]
else return nil end
end
local function removeItem(item)
item.flags.garbage_collect = true
end
local function findInorganicWithName(matString)
for inorganicID,material in ipairs(df.global.world.raws.inorganics) do
if material.id == matString then return inorganicID end
end
return nil
end
local function getScriptFromMaterial(material)
local commandStart
local commandEnd
local reactionClasses = material.reaction_class
for classNumber,reactionClass in ipairs(reactionClasses) do
if reactionClass.value == "\\COMMAND" then commandStart = classNumber end
if reactionClass.value == "\\ENDCOMMAND" then commandEnd = classNumber break end
end
local script = {}
if commandStart and commandEnd then
for i = commandStart+1, commandEnd-1, 1 do
table.insert(script,reactionClasses[i].value)
end
end
return script
end
local function getUnitHitByProjectile(projectile)
for uid,unit in ipairs(df.global.world.units.active) do
if posIsEqual(unit.pos,projectile.cur_pos) then return uid,unit end
end
return nil
end
local function matCausesSyndrome(material)
for _,reactionClass in ipairs(material.reaction_class) do
if reactionClass.value == "DFHACK_CAUSES_SYNDROME" then return true end --the syndrome is the syndrome local to the projectile material
end
return false
end
projectileExpansionFlags = projectilExpansionFlags or {
matWantsSpecificInorganic = 0,
matWantsSpecificSize = 50000,
matCausesDragonFire = false,
matCausesMiasma = false,
matCausesMist = false,
matCausesMist2 = false,
matCausesDust = false,
matCausesLavaMist = false,
matCausesSmoke = false,
matCausesFireBreath = false,
matCausesWeb = false,
matCausesUndirectedGas = false,
matCausesUndirectedVapor = false,
matCausesOceanWave = false,
matCausesSeaFoam = false,
matHasScriptAttached = false,
matCausesSyndrome = false,
returnLocation = false,
matDisappearsOnHit = false
}
local function getProjectileExpansionFlags(material)
local matName = nil
for k,reactionClass in ipairs(material.reaction_class) do
if debugProjExp then print("checking reaction class #" .. k .. "...",reactionClass.value) end
if string.find(reactionClass.value,"DFHACK") then
if debugProjExp then print("DFHack reaction class found!") end
if reactionClass.value == "DFHACK_SPECIFIC_MAT" then matName = material.reaction_class[k+1].value end
if reactionClass.value == "DFHACK_FLOW_SIZE" then projectileExpansionFlags.matWantsSpecificSize = tonumber(material.reaction_class[k+1].value) end
if reactionClass.value == "DFHACK_CAUSES_SYNDROME" then projectileExpansionFlags.matCausesSyndrome = true end
if reactionClass.value == "DFHACK_DRAGONFIRE" then projectileExpansionFlags.matCausesDragonFire = true end
if reactionClass.value == "DFHACK_MIASMA" then projectileExpansionFlags.matCausesMiasma = true end
if reactionClass.value == "DFHACK_MIST" then projectileExpansionFlags.matCausesMist = true end
if reactionClass.value == "DFHACK_MIST2" then projectileExpansionFlags.matCausesMist2 = true end
if reactionClass.value == "DFHACK_DUST" then projectileExpansionFlags.matCausesDust = true end
if reactionClass.value == "DFHACK_LAVAMIST" then projectileExpansionFlags.matCausesLavaMist = true end
if reactionClass.value == "DFHACK_SMOKE" then projectileExpansionFlags.matCausesSmoke = true end
if reactionClass.value == "DFHACK_FIREBREATH" then projectileExpansionFlags.matCausesFireBreath = true end
if reactionClass.value == "DFHACK_WEB" then projectileExpansionFlags.matCausesWeb = true end
if reactionClass.value == "DFHACK_GAS_UNDIRECTED" then projectileExpansionFlags.matCausesUndirectedGas = true end
if reactionClass.value == "DFHACK_VAPOR_UNDIRECTED" then projectileExpansionFlags.matCausesUndirectedVapor = true end
if reactionClass.value == "DFHACK_OCEAN_WAVE" then projectileExpansionFlags.matCausesOceanWave = true end
if reactionClass.value == "DFHACK_SEA_FOAM" then projectileExpansionFlags.matCausesSeaFoam = true end
if reactionClass.value == "DFHACK_DISAPPEARS" then projectileExpansionFlags.matDisappearsOnHit = true end
end
if reactionClass.value == "\\COMMAND" then projectileExpansionFlags.matHasScriptAttached = true end
end
if matName then projectileExpansionFlags.matWantsSpecificInorganic = findInorganicWithName(matName) end
return projectileExpansionFlags
end
debugProjExp=false
events.onProjItemCheckImpact.expansion=function(projectile)
if debugProjExp then print("Thwack! Projectile item hit. Running projectileExpansion.") end
if projectile then
if debugProjExp then print("Found the item. Working on it.") end
local material = getMaterial(projectile.item)
if not material then return nil end
local projectileExpansionFlags=getProjectileExpansionFlags(material)
if debugProjExp then print(projectileExpansionFlags) printall(projectileExpansionFlags) end
local syndrome = getSyndrome(material)
local emissionMat = projectileExpansionFlags.matWantsSpecificInorganic --defaults to iron
local flowSize = projectileExpansionFlags.matWantsSpecificSize --defaults to 50000
if projectileExpansionFlags.matCausesDragonFire then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.dragonFire,0,0,flowSize) end
if projectileExpansionFlags.matCausesMiasma then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.miasma,0,0,flowSize) end
if projectileExpansionFlags.matCausesMist then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.mist,0,0,flowSize) end
if projectileExpansionFlags.matCausesMist2 then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.mist2,0,0,flowSize) end
if projectileExpansionFlags.matCausesDust then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.dust,0,emissionMat,flowSize) end
if projectileExpansionFlags.matCausesLavaMist then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.lavaMist,0,emissionMat,flowSize) end
if projectileExpansionFlags.matCausesSmoke then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.smoke,0,0,flowSize) end
if projectileExpansionFlags.matCausesFireBreath then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.fireBreath,0,0,flowSize) end
if projectileExpansionFlags.matCausesWeb then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.web,0,emissionMat,flowSize) end
if projectileExpansionFlags.matCausesUndirectedGas then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.undirectedGas,0,emissionMat,flowSize) end
if projectileExpansionFlags.matCausesUndirectedVapor then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.undirectedVapor,0,emissionMat,flowSize) end
if projectileExpansionFlags.matCausesOceanWave then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.oceanWave,0,0,flowSize) end
if projectileExpansionFlags.matCausesSeaFoam then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.seaFoam,0,0,flowSize) end
if projectileExpansionFlags.matHasScriptAttached or projectileExpansionFlags.matCausesSyndrome then
local uid,unit = getUnitHitByProjectile(projectile)
if projectileExpansionFlags.matHasScriptAttached then
local script = getScriptFromMaterial(material)
for k,v in ipairs(script) do
if script[k] == "\\UNIT_HIT_ID" then script[k] = unit.id end
if script[k] == "\\LOCATION" then
script[k] = projectile.cur_pos.x
table.insert(script,projectile.cur_pos.y,k+1)
table.insert(script,projectile.cur_pos.z,k+2)
end
end
dfhack.run_script(table.unpack(script))
end
if projectileExpansionFlags.matCausesSyndrome then syndromeUtil.infectWithSyndrome(unit,syndrome.id) end
end
--if projectileExpansionFlags.matDisappearsOnHit then dfhack.items.remove(projectile) end
end
return true
end
if ... ~= "disable" then
print("Enabled projectileExpansion.")
else
events.onProjItemCheckImpact.expansion = nil
print("Disabled projectileExpansion.")
end

@ -0,0 +1,56 @@
-- Resets all items in your fort to 0 wear
-- original author: Laggy
-- edited by expwnent
local args = {...}
if args[1] == 'help' then
print([[removewear - this script removes wear from all items, or from individual ones
removewear all
remove wear from all items
removewear n1 n2 n3 ...
remove wear from items with the given ids. order does not matter
repeat enable 2 months removewear all
remove wear from all items every 2 months. see repeat.lua for details
]])
do return end
elseif args[1] == 'all' then
local count = 0;
for _,item in ipairs(df.global.world.items.all) do
if (item.wear > 0) then
item:setWear(0)
count = count+1
end
end
print('removewear removed wear from 'count' objects')
else
local argIndex = 1
local isCompleted = {}
for i,x in ipairs(args) do
args[i] = tonumber(x)
end
table.sort(args)
for _,item in ipairs(df.global.world.items.all) do
local function loop()
if argIndex > #args then
return
elseif item.id > args[argIndex] then
argIndex = argIndex+1
loop()
return
elseif item.id == args[argIndex] then
--print('removing wear from item with id ' .. args[argIndex])
item:setWear(0)
isCompleted[args[argIndex]] = true
argIndex = argIndex+1
end
end
loop()
end
for _,arg in ipairs(args) do
if isCompleted[arg] ~= true then
print('failed to remove wear from item ' .. arg .. ': could not find item with that id')
end
end
end

@ -0,0 +1,73 @@
-- repeat.lua
-- repeatedly calls a lua script, eg "repeat enable 1 months cleanowned"; to disable "repeat disable cleanowned"
-- author expwnent
-- vaguely based on a script by Putnam
-- repeat -help for details
local repeatUtil = require 'plugins.repeatUtil'
local args = {...}
if args[1] == '-cancel' then
repeatUtil.cancel(args[2])
return
elseif args[1] == '-help' then
print([[repeat.lua
repeat -help
print this help message
repeat -cancel bob
cancels the repetition with the name bob
repeat -name jim -time delay timeUnits -printResult true -command printArgs 3 1 2
except for -command, arguments can go in any order
-name sets the name for the purposes of cancelling and making sure you don't schedule the same repeating event twice
if not specified, it's set to the first argument after -command
-time delay timeUnits
delay is some positive integer
timeUnits is some valid time unit for dfhack.timeout(delay,timeUnits,function)
-printResult true
print the results of the command
-printResult false
suppress the results of the command
-command ...
specify the command to be run
]])
end
local name=nil
local time
local timeUnits
local i=1
local command={}
local printResult=true
while i <= #args do
if args[i] == '-name' then
name = args[i+1]
i = i + 2
elseif args[i] == '-time' then
time = tonumber(args[i+1])
timeUnits = args[i+2]
i = i+3
elseif args[i] == '-command' then
name = name or args[i+1]
for j=i+1,#args,1 do
table.insert(command,args[j])
end
break
elseif args[i] == '-printResult' then
if args[i+1] == "true" then
printOutput = true
elseif args[i+1] == "false" then
printOutput = false
else
qerror("repeat -printResult " .. args[i+1] .. ": expected true or false")
end
i = i+2
end
end
repeatUtil.scheduleEvery(name,time,timeUnits,function()
result = dfhack.run_command(table.unpack(command))
if printResult then
print(result)
end
end)

@ -0,0 +1,97 @@
-- shapechange.lua
-- transforms unit (by number) into another creature, choice given to user. Syntax is: unitID tickamount maxsize namefilter. A size of 0 is ignored. A length of 0 is also ignored. If no filter, all units will be sorted. A filter of ALL will also work with all units.
-- author Putnam
-- edited by expwnent
--shapechange gui [unitId] [duration] [maxsize] [namefilter]
--shapechange manual [unitId] [creature name] [caste name] [duration]
local dialog = require('gui.dialogs')
local script = require('gui.script')
function transform(target,race,caste,length)
if target==nil then
qerror("Not a valid target")
end
local defaultRace = target.enemy.normal_race
local defaultCaste = target.enemy.normal_caste
target.enemy.normal_race = race --that's it???
target.enemy.normal_caste = caste; --that's it!
if length and length>0 then dfhack.timeout(length,'ticks',function() target.enemy.normal_race = defaultRace target.enemy.normal_caste = defaultCaste end) end
end
function getBodySize(caste)
return caste.body_size_1[#caste.body_size_1-1]
end
function selectCreature(unitID,length,size,filter) --taken straight from here, but edited so I can understand it better: https://gist.github.com/warmist/4061959/... again. Also edited for syndromeTrigger, but in a completely different way.
size = size or 0
filter = filter or "all"
length = length or 2400
local creatures=df.global.world.raws.creatures.all
local tbl={}
local tunit=df.unit.find(unitID)
for cr_k,creature in ipairs(creatures) do
for ca_k,caste in ipairs(creature.caste) do
local name=caste.caste_name[0]
if name=="" then name="?" end
if (not filter or string.find(name,filter) or string.lower(filter)=="all") and (not size or size>getBodySize(caste) or size<1 and not creature.flags.DOES_NOT_EXIST) then table.insert(tbl,{name,nil,cr_k,ca_k}) end
end
end
table.sort(tbl,function(a,b) return a[1]<b[1] end)
local f=function(name,C)
transform(tunit,C[3],C[4],length)
end
script.start(function()
local ok =
script.showYesNoPrompt(
"Just checking","Do you want "
.. dfhack.TranslateName(dfhack.units.getVisibleName(tunit))
.. " to transform into a creature of size below "..NEWLINE
.. (not not size and size>1 and size or "infinity")
.. " ("
.. size/(getBodySize(df.creature_raw.find(tunit.race).caste[tunit.caste]))*100
.. "% of current size) for "
..length
.." ticks ("
..length/1200
.." days, ~"
..length/df.global.enabler.fps
.." seconds)?",
COLOR_LIGHTRED
)
if ok then dialog.showListPrompt("Creature Selection","Choose creature:",COLOR_WHITE,tbl,f) end
end)
end
local args = {...}
--unit id, length, size, filter
if args[1] == 'gui' then
selectCreature(tonumber(args[2]),tonumber(args[3]),tonumber(args[4]),args[5])
else
local race-- = df.creature_raw.find(args[3])
local raceIndex
for index,raceCandidate in ipairs(df.global.world.raws.creatures.all) do
if raceCandidate.creature_id == args[3] then
raceIndex = index
race = raceCandidate
break
end
end
local caste
local casteIndex
if race then
for index,casteCandidate in ipairs(race.caste) do
if casteCandidate.caste_id == args[4] then
caste = casteCandidate
casteIndex = index
break
end
end
end
if not race or not caste then
print("shapechange error: couldn't find " .. args[3] .. " " .. args[4])
return
end
transform(df.unit.find(tonumber(args[2])), raceIndex, casteIndex, args[5] and tonumber(args[5]))
end

@ -0,0 +1,47 @@
-- skillChange.lua
-- Allows skills to be changed with DFHack.
-- author Putnam
-- edited by expwnent
--[[
Args are arranged like this:
unitID SKILL level ADD/SUBTRACT/SET
SKILL being anything such as DETAILSTONE, level being a number that it will be set to (15 is legendary, 0 is dabbling).
Add will add skill levels, subtract will subtract, set will place it at exactly the level you put in.
]]
local utils=require('utils')
local args={...}
local skills=df.unit.find(args[1]).status.current_soul.skills
local skill=df.job_skill[tostring(args[2]):upper()]
local level=tonumber(args[3]) or 0
local argfunctions={
__index=function(t,k)
qerror(k..' is not a valid setting! Valid settings are SET, ADD and SUBTRACT.')
end,
set=function(skills,skill,level)
utils.insert_or_update(skills, {new=true, id=skill, rating=level}, 'id')
end,
add=function(skills,skill,level)
local skillExists,oldSkill=utils.linear_index(skills,skill,'id')
if skillExists then
oldSkill.rating=level+oldSkill.rating
else
argfunctions.set(skills,skill,level)
end
end,
subtract=function(skills,skill,level)
local skillExists,oldSkill=utils.linear_index(skills,skill,'id')
if skillExists then
local newRating=oldSkill['rating'] or 0
oldSkill.rating=oldSkill.rating-level
if oldSkill.rating<0 or (oldSkill.rating==0 and oldSkill.experience<=0) then skills:erase(skillExists) end
end
end
}
setmetatable(argfunctions,argfunctions)
argfunctions[args[4]:lower()](skills,skill,level)

@ -0,0 +1,60 @@
-- skillroll.lua
-- Allows skills to activate lua scripts.
-- author Putnam
-- edited by expwnent
--[[Example usage:
...syndrome stuff...
[SYN_CLASS:\COMMAND][SYN_CLASS:skillroll][SYN_CLASS:\WORKER_ID] For autoSyndrome/syndromeTrigger.
[SYN_CLASS:MELEE_COMBAT] Can use any skill, including NONE (no bonus)
[SYN_CLASS:20] Rolls uniformly from 1 to 20 inclusive. Skill will be weighted to this value.
[SYN_CLASS:DICEROLL_1] If diceroll ends up as one...
[SYN_CLASS:kill][SYN_CLASS:\SKILL_UNIT_ID] Theoretical kill-given-unit-id command; slayrace doesn't do so.
[SYN_CLASS:DICEROLL_10] If diceroll is between 1 and 10 (2-10, inclusive)...
[SYN_CLASS:force][SYN_CLASS:migrants][SYN_CLASS:player] Force migrants.
[SYN_CLASS:DICEROLL_19] If diceroll is between 10 and 19 (11-19, inclusive)...
[SYN_CLASS:fullheal][SYN_CLASS:\SKILL_UNIT_ID] Fully heals unit.
[SYN_CLASS:DICEROLL_20] If diceroll is at least 20...
[SYN_CLASS:shapechange][SYN_CLASS:\SKILL_UNIT_ID] Turns unit into any creature permanently.
or from the console
skillroll workerId MELEE_COMBAT 20 DICEROLL_1 kill workerId DICEROLL_10 force migrants player DICEROLL_19 fullheal workerId DICEROLL_20 shapechange workerId
]]
local args={...}
if args[1]=='dryrun' then
unit=df.global.world.units.all[0]
end
local unit = unit or df.unit.find(args[1])
rando=rando or dfhack.random.new()
local roll=rando:random(tonumber(args[3]))
if args[2] ~= 'NONE' then
local result=roll+(dfhack.units.getEffectiveSkill(unit,df.job_skill[args[2]])*(tonumber(args[3])/20))
result = result%1<.5 and math.floor(result) or math.ceil(result)
roll = result
end
local i=4
local command={}
local scriptIsFinished
repeat
local arg=args[i]
if arg:find('DICEROLL') then
local dicerollnumber=tonumber(arg:match('%d+')) --yes this is truly naive as hell; I imagine if you put DICEROLL3%moa5oam3 it'll return 353.
if dicerollnumber>=roll then
repeat
i=i+1
if i<=#args and (not args[i]:find('DICEROLL')) then
if args[i]~='\\SKILL_UNIT_ID' then table.insert(command,args[i]) else table.insert(command,args[1]) end
end
until i>#args or args[i]:find('DICEROLL')
local out = dfhack.run_command(table.unpack(command))
print(out)
scriptIsFinished=true
else
i=i+1
end
else
i=i+1
end
until i>#args or scriptIsFinished

@ -0,0 +1,36 @@
-- teleport.lua
-- teleports a unit to a location
-- author Putnam
-- edited by expwnent
local function teleport(unit,pos)
local unitoccupancy = dfhack.maps.getTileBlock(unit.pos).occupancy[unit.pos.x%16][unit.pos.y%16]
unit.pos.x = pos.x
unit.pos.y = pos.y
unit.pos.z = pos.z
if not unit.flags1.on_ground then unitoccupancy.unit = false else unitoccupancy.unit_grounded = false end
end
local function getArgsTogether(args)
local settings={pos={}}
for k,v in ipairs(args) do
v=string.lower(v)
if v=="unit" then settings.unitID=tonumber(args[k+1]) end
if v=="x" then settings.pos['x']=tonumber(args[k+1]) end
if v=="y" then settings.pos['y']=tonumber(args[k+1]) end
if v=="z" then settings.pos['z']=tonumber(args[k+1]) end
if v=="showunitid" then print(dfhack.gui.getSelectedUnit(true).id) end
if v=="showpos" then printall(df.global.cursor) end
end
if not settings.pos.x or not settings.pos.y or not settings.pos.z then settings.pos=nil end
if not settings.unitID and not settings.pos.x then qerror("Needs a position, a unit ID or both, but not neither!") end
return settings
end
local args = {...}
local teleportSettings=getArgsTogether(args)
local unit = teleportSettings.unitID and df.unit.find(teleportSettings.unitID) or dfhack.gui.getSelectedUnit(true)
local pos = teleportSettings.pos and teleportSettings.pos or df.global.cursor
teleport(unit,pos)

@ -0,0 +1,793 @@
-- unit-info-viewer.lua
-- Displays age, birth, maxage, shearing, milking, grazing, egg laying, body size, and death info about a unit. Recommended keybinding Alt-I
-- version 1.04
-- original author: Kurik Amudnil
-- edited by expwnent
local gui = require 'gui'
local widgets =require 'gui.widgets'
local utils = require 'utils'
local DEBUG = false
if DEBUG then print('-----') end
local pens = {
BLACK = dfhack.pen.parse{fg=COLOR_BLACK,bg=0},
BLUE = dfhack.pen.parse{fg=COLOR_BLUE,bg=0},
GREEN = dfhack.pen.parse{fg=COLOR_GREEN,bg=0},
CYAN = dfhack.pen.parse{fg=COLOR_CYAN,bg=0},
RED = dfhack.pen.parse{fg=COLOR_RED,bg=0},
MAGENTA = dfhack.pen.parse{fg=COLOR_MAGENTA,bg=0},
BROWN = dfhack.pen.parse{fg=COLOR_BROWN,bg=0},
GREY = dfhack.pen.parse{fg=COLOR_GREY,bg=0},
DARKGREY = dfhack.pen.parse{fg=COLOR_DARKGREY,bg=0},
LIGHTBLUE = dfhack.pen.parse{fg=COLOR_LIGHTBLUE,bg=0},
LIGHTGREEN = dfhack.pen.parse{fg=COLOR_LIGHTGREEN,bg=0},
LIGHTCYAN = dfhack.pen.parse{fg=COLOR_LIGHTCYAN,bg=0},
LIGHTRED = dfhack.pen.parse{fg=COLOR_LIGHTRED,bg=0},
LIGHTMAGENTA = dfhack.pen.parse{fg=COLOR_LIGHTMAGENTA,bg=0},
YELLOW = dfhack.pen.parse{fg=COLOR_YELLOW,bg=0},
WHITE = dfhack.pen.parse{fg=COLOR_WHITE,bg=0},
}
function getUnit_byID(id) -- get unit by id from units.all via binsearch
if type(id) == 'number' then
-- (vector,key,field,cmpfun,min,max) { item/nil , found true/false , idx/insert at }
return utils.binsearch(df.global.world.units.all,id,'id')
end
end
function getUnit_byVS(silent) -- by view screen mode
silent = silent or false
-- if not world loaded, return nil ?
local u,tmp -- u: the unit to return ; tmp: temporary for intermediate tests/return values
local v = dfhack.gui.getCurViewscreen()
u = dfhack.gui.getSelectedUnit(true) -- supports gui scripts/plugin that provide a hook for getSelectedUnit()
if u then
return u
-- else: contexts not currently supported by dfhack.gui.getSelectedUnit()
elseif df.viewscreen_dwarfmodest:is_instance(v) then
tmp = df.global.ui.main.mode
if tmp == 17 or tmp == 42 or tmp == 43 then
-- context: @dwarfmode/QueryBuiding/Some/Cage -- (q)uery cage
-- context: @dwarfmode/ZonesPenInfo/AssignUnit -- i (zone) -> pe(N)
-- context: @dwarfmode/ZonesPitInfo -- i (zone) -> (P)it
u = df.global.ui_building_assign_units[df.global.ui_building_item_cursor]
elseif tmp == 49 and df.global.ui.burrows.in_add_units_mode then
-- @dwarfmode/Burrows/AddUnits
u = df.global.ui.burrows.list_units[ df.global.ui.burrows.unit_cursor_pos ]
elseif df.global.ui.follow_unit ~= -1 then
-- context: follow unit mode
u = getUnit_byID(df.global.ui.follow_unit)
end -- end viewscreen_dwarfmodest
elseif df.viewscreen_petst:is_instance(v) then
-- context: @pet/List/Unit -- z (status) -> animals
if v.mode == 0 then -- List
if not v.is_vermin[v.cursor] then
u = v.animal[v.cursor].unit
end
--elseif v.mode = 1 then -- training knowledge (no unit reference)
elseif v.mode == 2 then -- select trainer
u = v.trainer_unit[v.trainer_cursor]
end
elseif df.viewscreen_layer_workshop_profilest:is_instance(v) then
-- context: @layer_workshop_profile/Unit -- (q)uery workshop -> (P)rofile -- df.global.ui.main.mode == 17
u = v.workers[v.layer_objects[0].cursor]
elseif df.viewscreen_layer_overall_healthst:is_instance(v) then
-- context @layer_overall_health/Units -- z -> health
u = v.unit[v.layer_objects[0].cursor]
elseif df.viewscreen_layer_militaryst:is_instance(v) then
local PG_ASSIGNMENTS = 0
local PG_EQUIPMENT = 2
local TB_POSITIONS = 1
local TB_CANDIDATES = 2
-- layer_objects[0: squads list; 1: positions list; 2: candidates list]
-- page 0:positions/assignments 1:alerts 2:equipment 3:uniforms 4:supplies 5:ammunition
if v.page == PG_ASSIGNMENTS and v.layer_objects[TB_CANDIDATES].enabled and v.layer_objects[TB_CANDIDATES].active then
-- context: @layer_military/Positions/Position/Candidates -- m -> Candidates
u = v.positions.candidates[v.layer_objects[TB_CANDIDATES].cursor]
elseif v.page == PG_ASSIGNMENTS and v.layer_objects[TB_POSITIONS].enabled and v.layer_objects[TB_POSITIONS].active then
-- context: @layer_military/Positions/Position -- m -> Positions
u = v.positions.assigned[v.layer_objects[TB_POSITIONS].cursor]
elseif v.page == PG_EQUIPMENT and v.layer_objects[TB_POSITIONS].enabled and v.layer_objects[TB_POSITIONS].active then
-- context: @layer_military/Equip/Customize/View/Position -- m -> (e)quip -> Positions
-- context: @layer_military/Equip/Uniform/Positions -- m -> (e)quip -> assign (U)niforms -> Positions
u = v.equip.units[v.layer_objects[TB_POSITIONS].cursor]
end
elseif df.viewscreen_layer_noblelistst:is_instance(v) then
if v.mode == 0 then
-- context: @layer_noblelist/List -- (n)obles
u = v.info[v.layer_objects[v.mode].cursor].unit
elseif v.mode == 1 then
-- context: @layer_noblelist/Appoint -- (n)obles -> (r)eplace
u = v.candidates[v.layer_objects[v.mode].cursor].unit
end
elseif df.viewscreen_unitst:is_instance(v) then
-- @unit -- (v)unit -> z ; loo(k) -> enter ; (n)obles -> enter ; others
u = v.unit
elseif df.viewscreen_customize_unitst:is_instance(v) then
-- @customize_unit -- @unit -> y
u = v.unit
elseif df.viewscreen_layer_unit_healthst:is_instance(v) then
-- @layer_unit_health -- @unit -> h ; @layer_overall_health/Units -> enter
if df.viewscreen_layer_overall_healthst:is_instance(v.parent) then
-- context @layer_overall_health/Units -- z (status)-> health
u = v.parent.unit[v.parent.layer_objects[0].cursor]
elseif df.viewscreen_unitst:is_instance(v.parent) then
-- @unit -- (v)unit -> z ; loo(k) -> enter ; (n)obles -> enter ; others
u = v.parent.unit
end
elseif df.viewscreen_textviewerst:is_instance(v) then
-- @textviewer -- @unit -> enter (thoughts and preferences)
if df.viewscreen_unitst:is_instance(v.parent) then
-- @unit -- @unit -> enter (thoughts and preferences)
u = v.parent.unit
elseif df.viewscreen_itemst:is_instance(v.parent) then
tmp = v.parent.entry_ref[v.parent.cursor_pos]
if df.general_ref_unit:is_instance(tmp) then -- general_ref_unit and derived ; general_ref_contains_unitst ; others?
u = getUnit_byID(tmp.unit_id)
end
elseif df.viewscreen_dwarfmodest:is_instance(v.parent) then
tmp = df.global.ui.main.mode
if tmp == 24 then -- (v)iew units {g,i,p,w} -> z (thoughts and preferences)
-- context: @dwarfmode/ViewUnits/...
--if df.global.ui_selected_unit > -1 then -- -1 = 'no units nearby'
u = df.global.world.units.active[df.global.ui_selected_unit]
--end
elseif tmp == 25 then -- loo(k) unit -> enter (thoughs and preferences)
-- context: @dwarfmode/LookAround/Unit
tmp = df.global.ui_look_list.items[df.global.ui_look_cursor]
if tmp.type == 2 then -- 0:item 1:terrain >>2: unit<< 3:building 4:colony/vermin 7:spatter
u = tmp.unit
end
end
elseif df.viewscreen_unitlistst:is_instance(v.parent) then -- (u)nit list -> (v)iew unit (not citizen)
-- context: @unitlist/Citizens ; @unitlist/Livestock ; @unitlist/Others ; @unitlist/Dead
u = v.parent.units[v.parent.page][ v.parent.cursor_pos[v.parent.page] ]
end
end -- switch viewscreen
if not u and not silent then
dfhack.printerr('No unit is selected in the UI or context not supported.')
end
return u
end -- getUnit_byVS()
--http://lua-users.org/wiki/StringRecipes ----------
function str2FirstUpper(str)
return str:gsub("^%l", string.upper)
end
--------------------------------------------------
--http://lua-users.org/wiki/StringRecipes ----------
local function tchelper(first, rest)
return first:upper()..rest:lower()
end
-- Add extra characters to the pattern if you need to. _ and ' are
-- found in the middle of identifiers and English words.
-- We must also put %w_' into [%w_'] to make it handle normal stuff
-- and extra stuff the same.
-- This also turns hex numbers into, eg. 0Xa7d4
function str2TitleCase(str)
return str:gsub("(%a)([%w_']*)", tchelper)
end
--------------------------------------------------
--isBlank suggestion by http://stackoverflow.com/a/10330861
function isBlank(x)
x = tostring(x) or ""
-- returns (not not match_begin), _ = match_end => not not true , _ => true
-- returns not not nil => false (no match)
return not not x:find("^%s*$")
end
--http://lua-users.org/wiki/StringRecipes (removed indents since I am not using them)
function wrap(str, limit)--, indent, indent1)
--indent = indent or ""
--indent1 = indent1 or indent
local limit = limit or 72
local here = 1 ---#indent1
return str:gsub("(%s+)()(%S+)()", --indent1..str:gsub(
function(sp, st, word, fi)
if fi-here > limit then
here = st -- - #indent
return "\n"..word --..indent..word
end
end)
end
--------------------------------------------------
---------------------- Time ----------------------
--------------------------------------------------
local TU_PER_DAY = 1200
--[[
if advmode then TU_PER_DAY = 86400 ? or only for cur_year_tick?
advmod_TU / 72 = ticks
--]]
local TU_PER_MONTH = TU_PER_DAY * 28
local TU_PER_YEAR = TU_PER_MONTH * 12
local MONTHS = {
'Granite',
'Slate',
'Felsite',
'Hematite',
'Malachite',
'Galena',
'Limestone',
'Sandstone',
'Timber',
'Moonstone',
'Opal',
'Obsidian',
}
Time = defclass(Time)
function Time:init(args)
self.year = args.year or 0
self.ticks = args.ticks or 0
end
function Time:getDays() -- >>float<< Days as age (including years)
return self.year * 336 + (self.ticks / TU_PER_DAY)
end
function Time:getMonths() -- >>int<< Months as age (not including years)
return math.floor (self.ticks / TU_PER_MONTH)
end
function Time:getMonthStr() -- Month as date
return MONTHS[self:getMonths()+1] or 'error'
end
function Time:getDayStr() -- Day as date
local d = math.floor ( (self.ticks % TU_PER_MONTH) / TU_PER_DAY ) + 1
if d == 11 or d == 12 or d == 13 then
d = tostring(d)..'th'
elseif d % 10 == 1 then
d = tostring(d)..'st'
elseif d % 10 == 2 then
d = tostring(d)..'nd'
elseif d % 10 == 3 then
d = tostring(d)..'rd'
else
d = tostring(d)..'th'
end
return d
end
--function Time:__add()
--end
function Time:__sub(other)
if DEBUG then print(self.year,self.ticks) end
if DEBUG then print(other.year,other.ticks) end
if self.ticks < other.ticks then
return Time{ year = (self.year - other.year - 1) , ticks = (TU_PER_YEAR + self.ticks - other.ticks) }
else
return Time{ year = (self.year - other.year) , ticks = (self.ticks - other.ticks) }
end
end
--------------------------------------------------
--------------------------------------------------
--------------------------------------------------
-------------------- Identity --------------------
--------------------------------------------------
local SINGULAR = 0
local PLURAL = 1
--local POSSESSIVE = 2
local PRONOUNS = {
[0]='She',
[1]='He',
[2]='It',
}
local BABY = 0
local CHILD = 1
local ADULT = 2
local TRAINING_LEVELS = {
[0] = ' (Semi-Wild)', -- Semi-wild
' (Trained)', -- Trained
' (-Trained-)', -- Well-trained
' (+Trained+)', -- Skillfully trained
' (*Trained*)', -- Expertly trained
' ('..string.char(240)..'Trained'..string.char(240)..')', -- Exceptionally trained
' ('..string.char(15)..'Trained'..string.char(15)..')', -- Masterully Trained
' (Tame)', -- Domesticated
'', -- undefined
'', -- wild/untameable
}
local DEATH_TYPES = {
[0] = ' died of old age', -- OLD_AGE
' starved to death', -- HUNGER
' died of dehydration', -- THIRST
' was shot and killed', -- SHOT
' bled to death', -- BLEED
' drowned', -- DROWN
' suffocated', -- SUFFOCATE
' was struck down', -- STRUCK_DOWN
' was scuttled', -- SCUTTLE
" didn't survive a collision", -- COLLISION
' took a magma bath', -- MAGMA
' took a magma shower', -- MAGMA_MIST
' was incinerated by dragon fire', -- DRAGONFIRE
' was killed by fire', -- FIRE
' experienced death by SCALD', -- SCALD
' was crushed by cavein', -- CAVEIN
' was smashed by a drawbridge', -- DRAWBRIDGE
' was killed by falling rocks', -- FALLING_ROCKS
' experienced death by CHASM', -- CHASM
' experienced death by CAGE', -- CAGE
' was murdered', -- MURDER
' was killed by a trap', -- TRAP
' vanished', -- VANISH
' experienced death by QUIT', -- QUIT
' experienced death by ABANDON', -- ABANDON
' suffered heat stroke', -- HEAT
' died of hypothermia', -- COLD
' experienced death by SPIKE', -- SPIKE
' experienced death by ENCASE_LAVA', -- ENCASE_LAVA
' experienced death by ENCASE_MAGMA', -- ENCASE_MAGMA
' was preserved in ice', -- ENCASE_ICE
' became headless', -- BEHEAD
' was crucified', -- CRUCIFY
' experienced death by BURY_ALIVE', -- BURY_ALIVE
' experienced death by DROWN_ALT', -- DROWN_ALT
' experienced death by BURN_ALIVE', -- BURN_ALIVE
' experienced death by FEED_TO_BEASTS', -- FEED_TO_BEASTS
' experienced death by HACK_TO_PIECES', -- HACK_TO_PIECES
' choked on air', -- LEAVE_OUT_IN_AIR
' experienced death by BOIL', -- BOIL
' melted', -- MELT
' experienced death by CONDENSE', -- CONDENSE
' experienced death by SOLIDIFY', -- SOLIDIFY
' succumbed to infection', -- INFECTION
"'s ghost was put to rest with a memorial", -- MEMORIALIZE
' scared to death', -- SCARE
' experienced death by DARKNESS', -- DARKNESS
' experienced death by COLLAPSE', -- COLLAPSE
' was drained of blood', -- DRAIN_BLOOD
' was slaughtered', -- SLAUGHTER
' became roadkill', -- VEHICLE
' killed by a falling object', -- FALLING_OBJECT
}
--GHOST_TYPES[unit.relations.ghost_info.type].." This spirit has not been properly memorialized or buried."
local GHOST_TYPES = {
[0]="A murderous ghost.",
"A sadistic ghost.",
"A secretive ghost.",
"An energetic poltergeist.",
"An angry ghost.",
"A violent ghost.",
"A moaning spirit returned from the dead. It will generally trouble one unfortunate at a time.",
"A howling spirit. The ceaseless noise is making sleep difficult.",
"A troublesome poltergeist.",
"A restless haunt, generally troubling past acquaintances and relatives.",
"A forlorn haunt, seeking out known locations or drifting around the place of death.",
}
Identity = defclass(Identity)
function Identity:init(args)
local u = args.unit
self.ident = dfhack.units.getIdentity(u)
self.unit = u
self.name = dfhack.TranslateName( dfhack.units.getVisibleName(u) )
self.name_en = dfhack.TranslateName( dfhack.units.getVisibleName(u) , true)
self.raw_prof = dfhack.units.getProfessionName(u)
self.pronoun = PRONOUNS[u.sex] or 'It'
if self.ident then
self.birth_date = Time{year = self.ident.birth_year, ticks = self.ident.birth_second}
self.race_id = self.ident.race
self.caste_id = self.ident.caste
if self.ident.histfig_id > -1 then
self.hf_id = self.ident.histfig_id
end
else
self.birth_date = Time{year = self.unit.relations.birth_year, ticks = self.unit.relations.birth_time}
self.race_id = u.race
self.caste_id = u.caste
if u.hist_figure_id > -1 then
self.hf_id = u.hist_figure_id
end
end
self.race = df.global.world.raws.creatures.all[self.race_id]
self.caste = self.race.caste[self.caste_id]
self.isCivCitizen = (df.global.ui.civ_id == u.civ_id)
self.isStray = u.flags1.tame --self.isCivCitizen and not u.flags1.merchant
self.cur_date = Time{year = df.global.cur_year, ticks = df.global.cur_year_tick}
------------ death ------------
self.dead = u.flags1.dead
self.ghostly = u.flags3.ghostly
self.undead = u.enemy.undead
if self.dead and self.hf_id then -- dead-dead not undead-dead
local events = df.global.world.history.events2
local e
for idx = #events - 1,0,-1 do
e = events[idx]
if df.history_event_hist_figure_diedst:is_instance(e) and e.victim_hf == self.hf_id then
self.death_event = e
break
end
end
end
if u.counters.death_id > -1 then -- if undead/ghostly dead or dead-dead
self.death_info = df.global.world.deaths.all[u.counters.death_id]
if not self.death_info.flags.discovered then
self.missing = true
end
end
-- slaughtered?
if self.death_event then
self.death_date = Time{year = self.death_event.year, ticks = self.death_event.seconds}
elseif self.death_info then
self.death_date = Time{year = self.death_info.event_year, ticks = self.death_info.event_time}
end
-- age now or age death?
if self.dead and self.death_date then -- if cursed with no age? -- if hacked a ressurection, such that they aren't dead anymore, don't use the death date
self.age_time = self.death_date - self.birth_date
else
self.age_time = self.cur_date - self.birth_date
end
if DEBUG then print( self.age_time.year,self.age_time.ticks,self.age_time:getMonths() ) end
---------- ---------- ----------
---------- caste_name ----------
self.caste_name = {}
if isBlank(self.caste.caste_name[SINGULAR]) then
self.caste_name[SINGULAR] = self.race.name[SINGULAR]
else
self.caste_name[SINGULAR] = self.caste.caste_name[SINGULAR]
end
if isBlank(self.caste.caste_name[PLURAL]) then
self.caste_name[PLURAL] = self.race.name[PLURAL]
else
self.caste_name[PLURAL] = self.caste.caste_name[PLURAL]
end
---------- ---------- ----------
--------- growth_status ---------
-- 'baby_age' is the age the baby becomes a child
-- 'child_age' is the age the child becomes an adult
if self.age_time.year >= self.caste.misc.child_age then -- has child come of age becoming adult?
self.growth_status = ADULT
elseif self.age_time.year >= self.caste.misc.baby_age then -- has baby come of age becoming child?
self.growth_status = CHILD
else
self.growth_status = BABY
end
---------- ---------- ----------
-------- aged_caste_name --------
local caste_name, race_name
if self.growth_status == ADULT then
caste_name = self.caste.caste_name[SINGULAR]
race_name = self.race.name[SINGULAR]
elseif self.growth_status == CHILD then
caste_name = self.caste.child_name[SINGULAR]
race_name = self.race.general_child_name[SINGULAR]
else --if self.growth_status == BABY then
caste_name = self.caste.baby_name[SINGULAR]
race_name = self.race.general_baby_name[SINGULAR]
end
self.aged_caste_name = {}
if isBlank(caste_name[SINGULAR]) then
self.aged_caste_name[SINGULAR] = race_name[SINGULAR]
else
self.aged_caste_name[SINGULAR] = caste_name[SINGULAR]
end
if isBlank(caste_name[PLURAL]) then
self.aged_caste_name[PLURAL] = race_name[PLURAL]
else
self.aged_caste_name[PLURAL] = caste_name[PLURAL]
end
---------- ---------- ----------
----- Profession adjustment -----
local prof = self.raw_prof
if self.undead then
prof = str2TitleCase( self.caste_name[SINGULAR] )
if isBlank(u.enemy.undead.anon_7) then
prof = prof..' Corpse'
else
prof = u.enemy.undead.anon_7 -- a reanimated body part will use this string instead
end
end
--[[
if self.ghostly then
prof = 'Ghostly '..prof
end
--]]
if u.curse.name_visible and not isBlank(u.curse.name) then
prof = prof..' '..u.curse.name
end
if isBlank(self.name) then
if self.isStray then
prof = 'Stray '..prof --..TRAINING_LEVELS[u.training_level]
end
end
self.prof = prof
---------- ---------- ----------
end
--------------------------------------------------
--------------------------------------------------
--[[
prof_id ?
group_id ?
fort_race_id
fort_civ_id
--fort_group_id?
--]]
UnitInfoViewer = defclass(UnitInfoViewer, gui.FramedScreen)
UnitInfoViewer.focus_path = 'unitinfoviewer' -- -> dfhack/lua/unitinfoviewer
UnitInfoViewer.ATTRS={
frame_style = gui.GREY_LINE_FRAME,
frame_inset = 2, -- used by init
frame_outset = 1,--3, -- new, used by init; 0 = full screen, suggest 0, 1, or 3 or maybe 5
--frame_title , -- not used
--frame_width,frame_height calculated by frame inset and outset in init
}
function UnitInfoViewer:init(args) -- requires args.unit
--if DEBUG then print('-----') end
local x,y = dfhack.screen.getWindowSize()
-- what if inset or outset are defined as {l,r,t,b}?
x = x - 2*(self.frame_inset + 1 + self.frame_outset) -- 1=frame border thickness
y = y - 2*(self.frame_inset + 1 + self.frame_outset) -- 1=frame border thickness
self.frame_width = args.frame_width or x
self.frame_height = args.frame_height or y
self.text = {}
if df.unit:is_instance(args.unit) then
self.ident = Identity{ unit = args.unit }
if not isBlank(self.ident.name_en) then
self.frame_title = 'Unit: '..self.ident.name_en
elseif not isBlank(self.ident.prof) then
self.frame_title = 'Unit: '..self.ident.prof
if self.ident.isStray then
self.frame_title = self.frame_title..TRAINING_LEVELS[self.ident.unit.training_level]
end
end
self:chunk_Name()
self:chunk_Description()
if not (self.ident.dead or self.ident.undead or self.ident.ghostly) then --not self.dead
if self.ident.isCivCitizen then
self:chunk_Age()
self:chunk_MaxAge()
end
if self.ident.isStray then
if self.ident.growth_status == ADULT then
self:chunk_Milkable()
end
self:chunk_Grazer()
if self.ident.growth_status == ADULT then
self:chunk_Shearable()
end
if self.ident.growth_status == ADULT then
self:chunk_EggLayer()
end
end
self:chunk_BodySize()
elseif self.ident.ghostly then
self:chunk_Dead()
self:chunk_Ghostly()
elseif self.ident.undead then
self:chunk_BodySize()
self:chunk_Dead()
else
self:chunk_Dead()
end
else
self:insert_chunk("No unit is selected in the UI or context not supported.",pens.LIGHTRED)
end
self:addviews{ widgets.Label{ frame={yalign=0}, text=self.text } }
end
function UnitInfoViewer:onInput(keys)
if keys.LEAVESCREEN or keys.SELECT then
self:dismiss()
end
end
function UnitInfoViewer:onGetSelectedUnit()
return self.ident.unit
end
function UnitInfoViewer:insert_chunk(str,pen)
local lines = utils.split_string( wrap(str,self.frame_width) , NEWLINE )
for i = 1,#lines do
table.insert(self.text,{text=lines[i],pen=pen})
table.insert(self.text,NEWLINE)
end
table.insert(self.text,NEWLINE)
end
function UnitInfoViewer:chunk_Name()
local i = self.ident
local u = i.unit
local prof = i.prof
local color = dfhack.units.getProfessionColor(u)
local blurb
if i.ghostly then
prof = 'Ghostly '..prof
end
if i.isStray then
prof = prof..TRAINING_LEVELS[u.training_level]
end
if isBlank(i.name) then
if isBlank(prof) then
blurb = 'I am a mystery'
else
blurb = prof
end
else
if isBlank(prof) then
blurb=i.name
else
blurb=i.name..', '..prof
end
end
self:insert_chunk(blurb,dfhack.pen.parse{fg=color,bg=0})
end
function UnitInfoViewer:chunk_Description()
local dsc = self.ident.caste.description
if not isBlank(dsc) then
self:insert_chunk(dsc,pens.WHITE)
end
end
function UnitInfoViewer:chunk_Age()
local i = self.ident
local age_str -- = ''
if i.age_time.year > 1 then
age_str = tostring(i.age_time.year)..' years old'
elseif i.age_time.year > 0 then -- == 1
age_str = '1 year old'
else --if age_time.year == 0 then
local age_m = i.age_time:getMonths() -- math.floor
if age_m > 1 then
age_str = tostring(age_m)..' months old'
elseif age_m > 0 then -- age_m == 1
age_str = '1 month old'
else -- if age_m == 0 then -- and age_m < 0 which would be an error
age_str = 'a newborn'
end
end
local blurb = i.pronoun..' is '..age_str
if i.race_id == df.global.ui.race_id then
blurb = blurb..', born on the '..i.birth_date:getDayStr()..' of '..i.birth_date:getMonthStr()..' in the year '..tostring(i.birth_date.year)..PERIOD
else
blurb = blurb..PERIOD
end
self:insert_chunk(blurb,pens.YELLOW)
end
function UnitInfoViewer:chunk_MaxAge()
local i = self.ident
local maxage = math.floor( (i.caste.misc.maxage_max + i.caste.misc.maxage_min)/2 )
--or i.unit.curse.add_tags1.NO_AGING hidden ident?
if i.caste.misc.maxage_min == -1 then
maxage = ' die of unnatural causes.'
elseif maxage == 0 then
maxage = ' die at a very young age.'
elseif maxage == 1 then
maxage = ' live about '..tostring(maxage)..' year.'
else
maxage = ' live about '..tostring(maxage)..' years.'
end
--' is expected to '..
local blurb = str2FirstUpper(i.caste_name[PLURAL])..maxage
self:insert_chunk(blurb,pens.DARKGREY)
end
function UnitInfoViewer:chunk_Grazer()
if self.ident.caste.flags.GRAZER then
local blurb = 'Grazing satisfies '..tostring(self.ident.caste.misc.grazer)..' units of hunger.'
self:insert_chunk(blurb,pens.LIGHTGREEN)
end
end
function UnitInfoViewer:chunk_EggLayer()
local caste = self.ident.caste
if caste.flags.LAYS_EGGS then
local clutch = math.floor( (caste.misc.clutch_size_max + caste.misc.clutch_size_min)/2 )
local blurb = 'Lays clutches of about '..tostring(clutch)
if clutch > 1 then
blurb = blurb..' eggs.'
else
blurb = blurb..' egg.'
end
self:insert_chunk(blurb,pens.GREEN)
end
end
function UnitInfoViewer:chunk_Milkable()
local i = self.ident
if i.caste.flags.MILKABLE then
local milk = dfhack.matinfo.decode( i.caste.extracts.milkable_mat , i.caste.extracts.milkable_matidx )
if milk then
local days,seconds = math.modf ( i.caste.misc.milkable / TU_PER_DAY )
days = (seconds > 0) and (tostring(days)..' to '..tostring(days + 1)) or tostring(days)
--local blurb = pronoun..' produces '..milk:toString()..' every '..days..' days.'
local blurb = (i.growth_status == ADULT) and (i.pronoun..' secretes ') or str2FirstUpper(i.caste_name[PLURAL])..' secrete '
blurb = blurb..milk:toString()..' every '..days..' days.'
self:insert_chunk(blurb,pens.LIGHTCYAN)
end
end
end
function UnitInfoViewer:chunk_Shearable()
local i = self.ident
local mat_types = i.caste.body_info.materials.mat_type
local mat_idxs = i.caste.body_info.materials.mat_index
local mat_info, blurb
for idx,mat_type in ipairs(mat_types) do
mat_info = dfhack.matinfo.decode(mat_type,mat_idxs[idx])
if mat_info and mat_info.material.flags.YARN then
blurb = (i.growth_status == ADULT) and (i.pronoun..' produces ') or str2FirstUpper(i.caste_name[PLURAL])..' produce '
blurb = blurb..mat_info:toString()..PERIOD
self:insert_chunk(blurb,pens.BROWN)
end
end
end
function UnitInfoViewer:chunk_BodySize()
local i = self.ident
local pat = i.unit.body.physical_attrs
local blurb = i.pronoun..' appears to be about '..pat.STRENGTH.value..':'..pat.AGILITY.value..' cubic decimeters in size.'
self:insert_chunk(blurb,pens.LIGHTBLUE)
end
function UnitInfoViewer:chunk_Ghostly()
local blurb = GHOST_TYPES[self.ident.unit.relations.ghost_info.type].." This spirit has not been properly memorialized or buried."
self:insert_chunk(blurb,pens.LIGHTMAGENTA)
-- Arose in relations.curse_year curse_time
end
function UnitInfoViewer:chunk_Dead()
local i = self.ident
local blurb, str, pen
if i.missing then --dfhack.units.isDead(unit)
str = ' is missing.'
pen = pens.WHITE
elseif i.death_event then
--str = "The Caste_name Unit_Name died in year #{e.year}"
--str << " (cause: #{e.death_cause.to_s.downcase}),"
--str << " killed by the #{e.slayer_race_tg.name[0]} #{e.slayer_hf_tg.name}" if e.slayer_hf != -1
--str << " using a #{df.world.raws.itemdefs.weapons[e.weapon.item_subtype].name}" if e.weapon.item_type == :WEAPON
--str << ", shot by a #{df.world.raws.itemdefs.weapons[e.weapon.bow_item_subtype].name}" if e.weapon.bow_item_type == :WEAPON
str = DEATH_TYPES[i.death_event.death_cause]..PERIOD
pen = pens.MAGENTA
elseif i.death_info then
--str = "The #{u.race_tg.name[0]}"
--str << " #{u.name}" if u.name.has_name
--str << " died"
--str << " in year #{death_info.event_year}" if death_info
--str << " (cause: #{u.counters.death_cause.to_s.downcase})," if u.counters.death_cause != -1
--str << " killed by the #{killer.race_tg.name[0]} #{killer.name}" if killer
str = DEATH_TYPES[i.death_info.death_cause]..PERIOD
pen = pens.MAGENTA
elseif i.unit.flags2.slaughter and i.unit.flags2.killed then
str = ' was slaughtered.'
pen = pens.MAGENTA
else
str = ' is dead.'
pen = pens.MAGENTA
end
if i.undead or i.ghostly then
str = ' is undead.'
pen = pens.GREY
end
blurb = 'The '..i.prof -- assume prof is not blank
if not isBlank(i.name) then
blurb = blurb..', '..i.name
end
blurb = blurb..str
self:insert_chunk(blurb,pen)
end
-- only show if UnitInfoViewer isn't the current focus
if dfhack.gui.getCurFocus() ~= 'dfhack/lua/'..UnitInfoViewer.focus_path then
local gui_no_unit = false -- show if not found?
local unit = getUnit_byVS(gui_no_unit) -- silent? or let the gui display
if unit or gui_no_unit then
local kan_viewscreen = UnitInfoViewer{unit = unit}
kan_viewscreen:show()
end
end