If you can tell me how to target creatures inside of/next to workshops with a script, which is triggered by a different unit that runs a reaction in said workshop..
I modified the reaction-trigger script a while back to do exactly that so I could use it for giving cybernetics/drugs to other units, rehabilitating slaves etc in FoE. Here:
-- trigger commands when custom reactions complete
-- author expwnent, tweaks by thefriendlyhacker
-- replaces autoSyndrome
--@ module = true
local usage = [====[
foe/reaction-trigger
=========================
Triggers dfhack commands when custom reactions complete, regardless of whether
it produced anything, once per completion. Arguments::
-clear
unregister all reaction hooks
-reactionName name
specify the name of the reaction
-syndrome name
specify the name of the syndrome to be applied to the targets
-allowNonworkerTargets
allow other units to be targeted if the worker is immune or ignored. Only one is affected by a syndrome
commands must be paired with syndromes, or the command will execute on some/all creatures within range.
overridden by -allowMultipleTargets
-allowMultipleTargets
allow all targets within range to be affected by the command or syndrome
if absent:
if running a script, only one target will be used
if applying a syndrome, then only one target will be infected
-ignoreWorker
does not target the worker. Only makes sense with -allowMultipleTargets or -allowNonworkerTargets.
-range [ x y z ]
controls how far elligible creatures can be from the workshop. Defaults to [ 0 0 0 ] (within the workshop).
Negative x/y numbers can be used to ignore outer squares of the workshop. The worker is always within range.
Line of sight is not respected.
-resetPolicy policy
the policy in the case that the syndrome is already present
policy
NewInstance (default)
DoNothing
ResetDuration
AddDuration
-command [ commandStrs ]
specify the command to be run on the target(s).
if a syndrome is also provided, the command will not execute on targets that are invalid for the syndrome.
special args
\\WORKER_ID
\\TARGET_ID
\\BUILDING_ID
\\LOCATION
\\REACTION_NAME
\\anything -> \anything
anything -> anything
]====]
local eventful = require 'plugins.eventful'
local syndromeUtil = require 'syndrome-util'
local utils = require 'utils'
reactionHooks = reactionHooks or {}
eventful.enableEvent(eventful.eventType.UNLOAD,1)
eventful.onUnload.reactionTrigger = function()
reactionHooks = {}
end
function getWorkerAndBuilding(job)
local workerId = -1
local buildingId = -1
for _,generalRef in ipairs(job.general_refs) do
if generalRef:getType() == df.general_ref_type.UNIT_WORKER then
if workerId ~= -1 then
print(job)
printall(job)
error('reaction-trigger: two workers on same job: ' .. workerId .. ', ' .. generalRef.unit_id)
else
workerId = generalRef.unit_id
if workerId == -1 then
print(job)
printall(job)
error('reaction-trigger: invalid worker')
end
end
elseif generalRef:getType() == df.general_ref_type.BUILDING_HOLDER then
if buildingId ~= -1 then
print(job)
printall(job)
error('reaction-trigger: two buildings same job: ' .. buildingId .. ', ' .. generalRef.building_id)
else
buildingId = generalRef.building_id
if buildingId == -1 then
print(job)
printall(job)
error('reaction-trigger: invalid building')
end
end
end
end
return workerId,buildingId
end
local function processCommand(job, worker, target, building, command)
local result = {}
for _,arg in ipairs(command) do
if arg == '\\WORKER_ID' then
table.insert(result,''..worker.id)
elseif arg == '\\TARGET_ID' then
table.insert(result,''..target.id)
elseif arg == '\\BUILDING_ID' then
table.insert(result,''..building.id)
elseif arg == '\\LOCATION' then
table.insert(result,''..job.pos.x)
table.insert(result,''..job.pos.y)
table.insert(result,''..job.pos.z)
elseif arg == '\\REACTION_NAME' then
table.insert(result,''..job.reaction_name)
elseif string.sub(arg,1,1) == '\\' then
table.insert(result,string.sub(arg,2))
else
table.insert(result,arg)
end
end
return result
end
eventful.onJobCompleted.reactionTrigger = function(job)
if job.completion_timer > 0 then
return
end
-- if job.job_type ~= df.job_type.CustomReaction then
-- --TODO: support builtin reaction triggers if someone asks
-- return
-- end
if not job.reaction_name or job.reaction_name == '' then
return
end
-- print('reaction name: ' .. job.reaction_name)
if not job.reaction_name or not reactionHooks[job.reaction_name] then
return
end
local worker,building = getWorkerAndBuilding(job)
worker = df.unit.find(worker)
building = df.building.find(building)
if not worker or not building then
--this probably means that it finished before EventManager could get a copy of the job while the job was running
--TODO: consider printing a warning once
return
end
local function doAction(action)
local didSomething
local xRange, yRange, zRange
if action.range then
xRange,yRange,zRange = tonumber(action.range[1]), tonumber(action.range[2]), tonumber(action.range[3])
else
xRange,yRange,zRange = 0, 0, 0
end
if action.syndrome and not action.ignoreWorker then
didSomething = syndromeUtil.infectWithSyndromeIfValidTarget(worker, action.syndrome, action.resetPolicy) or didSomething
end
if action.command and not action.ignoreWorker and ( not action.syndrome or didSomething ) then
local processed = processCommand(job, worker, worker, building, action.command)
dfhack.run_command(table.unpack(processed))
end
if didSomething and not action.allowMultipleTargets then
return
end
if not action.allowNonworkerTargets and not action.allowMultipleTargets then
return
end
local function foreach(unit)
if unit == worker then
return false
elseif unit.pos.z < building.z - zRange or unit.pos.z > building.z + zRange then
return false
elseif unit.pos.x < building.x1 - xRange or unit.pos.x > building.x2 + xRange then
return false
elseif unit.pos.y < building.y1 - yRange or unit.pos.y > building.y2 + yRange then
return false
else
if action.syndrome then
didSomething = syndromeUtil.infectWithSyndromeIfValidTarget(unit,action.syndrome,action.resetPolicy) or didSomething
end
if action.command and ( not action.syndrome or didSomething ) then
local processed=processCommand(job, worker, unit, building, action.command)
dfhack.run_command(table.unpack(processed))
end
if didSomething and not action.allowMultipleTargets then
return true
end
return false
end
end
for _,unit in ipairs(df.global.world.units.all) do
if foreach(unit) then
break
end
end
end
for _,action in ipairs(reactionHooks[job.reaction_name]) do
doAction(action)
end
end
eventful.enableEvent(eventful.eventType.JOB_COMPLETED,0) --0 is necessary to catch cancelled jobs and not trigger them
validArgs = validArgs or utils.invert({
'help',
'clear',
'reactionName',
'syndrome',
'command',
'allowNonworkerTargets',
'allowMultipleTargets',
'range',
'ignoreWorker',
'resetPolicy'
})
if moduleMode then
return
end
local args = utils.processArgs({...}, validArgs)
if args.help then
print(usage)
return
end
if args.clear then
reactionHooks = {}
end
if not args.reactionName then
return
end
if not reactionHooks[args.reactionName] then
reactionHooks[args.reactionName] = {}
end
if args.syndrome then
local foundIt
for _,syndrome in ipairs(df.global.world.raws.syndromes.all) do
if syndrome.syn_name == args.syndrome then
args.syndrome = syndrome
foundIt = true
break
end
end
if not foundIt then
error('Could not find syndrome ' .. args.syndrome)
end
end
table.insert(reactionHooks[args.reactionName], args)
For reference, here is a script call I have lying around that will probably look similar to what you want:
foe/reaction-trigger -reactionName REHABILITATE_SLAVE -allowNonworkerTargets -range [ 5 5 0 ] -syndrome "rehabilitated (em)" -command [ foe/fix-unit-history -unit \\TARGET_ID -clearTamed ]
Of course, the above script call assumes you stick the script in a file called reaction-trigger.lua in the new folder hack/scripts/foe.
If you are running a command and you want it to only apply to certain kinds of units, you need to include a dummy syndrome that can only be applied to those units. The script will apply the dummy syndrome (which does nothing) and then run the command on that unit.
If you need something which is sensitive to what syndromes a creature has already, I will leave this syndrome swapping monster of a script here, which can be used to swap out a dummy syndrome applied by the above with the syndrome you want if the target has/doesn't have the correct syndromes (good luck):
-- this script swaps syndromes with other syndromes, only on certain conditions
--written by thefriendlyhacker
local usage = [====[
foe/swap-syndrome
=====================
This script applies syndromes with other syndromes on certain conditions. It can also be used to apply syndromes to all creatures (again, on certain conditions).
Arguments::
-disable name
removes a syndrome replacement registration.
-help
prints help and ends
-noRepeat
only executes once, does not repeat on subsequent ticks
-name string
names the syndrome replacement registration. Required unless -noRepeat
-tickRate nbr
defines how often the syndrome(s) is replaced. Defaults to 100 ticks
-dontDisperseCalls
by default, the first repeat is randomly offset so that all swap-syndrome calls don't end up on the same tick (potentially creating lag spikes). This disables that behavior.
-oldSyn syndrome or oldSyn [ syndromes ]
defines which syndromes are to be replaced. If both oldSyn and oldSynClass are undefined, then the new syndrome is applied
-oldSynClass synClass or -oldSynClass [ synClasses ]
defines which syndrome classes are to be replaced
-newSyn syndrome or -newSyn [ syndromes ]
defines which syndrome(s) is to be applied
-resetPolicy policy
specify a policy of what to do if the unit already has an instance of the syndrome.
NewInstance (default)
DoNothing
ResetDuration
AddDuration
-reqSyn syndrome or reqSyn [ syndromes ]
only apply the syndrome if one of the syndrome(s) is present
-reqSynClass synClass or reqSynClass [ synClasses ]
only apply the syndrome if one of the syndrome class(es) is present. If both reqSyn and reqSynClass are args, only one need match to apply the syndrome.
-notSyn syndrome or -notSyn [ syndromes ]
only apply the syndrome if the syndrome(s) is not present
-notSynClass synClass or -notSynClass [ synClasses ]
only apply the syndrome if the syndrome class(es) is not present
]====]
local eventful = require 'plugins.eventful'
local utils = require 'utils'
local repeatUtil = require 'repeat-util'
local syndromeUtil = require 'syndrome-util'
defaultTickRate=100 -- on low tickrates this may hurt fps severely
defaultResetPolicy=syndromeUtil.ResetPolicy["ResetDuration"]
function applySyndrome(unit,syndrome,resetPolicy)
syndromeUtil.infectWithSyndromeIfValidTarget(unit,find_syndrome(syndrome),resetPolicy)
end
--func must take unit only, not nil
function checkSynConds(unit,reqSyns,reqNotSyns,reqSynClasses,reqNotSynClasses)
local hasReqSyn=false
if not next(reqSynClasses) and not next(reqSyns) then
hasReqSyn=true
end
if hasReqSyn and not (next(reqNotSyns) or next(reqNotSynClasses)) then
return true -- when there are no conditions
end
for _,active_syn in pairs(unit.syndromes.active) do
local syn= df.syndrome.find(active_syn.type)
for _,reqSyn in ipairs(reqSyns) do
if syn==reqSyn then
hasReqSyn=true
end
end
for _,reqNotSyn in ipairs(reqNotSyns) do
if syn==reqNotSyn then
return false
end
end
for _,class in ipairs(syn.syn_class) do
for _,reqSynClass in reqSynClasses do
if class.value==reqSynClass then
hasReqSyn=true
end
end
for _,invalidSynClass in reqNotSynClasses do
if class.value == invalidSynClass then
return false
end
end
end
end
if hasReqSyn then return true end
return false
end
function find_syndrome(syn_name)
local syndrome
for _,syn in ipairs(df.global.world.raws.syndromes.all) do
if syn.syn_name == syn_name then
syndrome = syn
break
end
end
if not syndrome then
error ('error - cannot find: ' .. syn_name)
end
return syndrome
end
function stripSyndrome(unit,synName)
syndromeUtil.eraseSyndromes(unit,find_syndrome(synName).id)
end
function stripSyndromeByClass(unit,synClass)
for _,syn in unit.syndromes.active do
for _,unitSynClass in syn.syn_class do
if synClass==unitSynClass then
stripSyndrome(unit,syn.id)
end
end
end
end
function registerScript(name,func,tickRate,dontDisperse)
assert(name,'error - no name defined')
if not tickRate then tickRate=defaultTickRate end
repeatUtil.scheduleEvery(name,tickRate,'ticks',func)
if not dontDisperse then
--yeah, I am using impl details, it is the nicest way to do this
local callback=dfhack.timeout_active(repeatUtil.repeating[name],nil)
dfhack.timeout(tickRate-math.random(0,tickRate-1),'ticks',callback)
end
end
function searchUnitForSyndromes(unit,syns,synClasses)
for _,unitSyn in ipairs(unit.syndromes.active) do
local synType=df.syndrome.find(unitSyn.type)
for _,unitSynClass in ipairs(synType.syn_class) do
for _,synClass in ipairs(synClasses) do
if synClass==unitSynClass then return true end
end
end
for _,syn in ipairs(syns) do
if synType.syn_name==syn then return true end
end
end
return false
end
function swapSyns(unit,oldSyns,oldSynCs,newSyns,resetPolicy)
for _,syn in ipairs(oldSyns) do
stripSyndrome(unit,syn)
end
for _,synC in ipairs(oldSynCs) do
stripSyndromeByClass(unit,synC)
end
for _,newSyn in ipairs(newSyns) do
applySyndrome(unit,newSyn,resetPolicy)
end
end
--this is a desperate attempt to reduce runtime costs by writing code such that everything done in the callback proper is a local variable lookup.
function getCallbackInner(oldSyns,oldSynCs,newSyns,reqSyns,reqSynCs,notSyns,notSynCs, resetP)
return function()
for _,unit in ipairs(df.global.world.units.all) do
if searchUnitForSyndromes(unit,oldSyns,oldSynCs) then
if checkSynConds(unit,reqSyns,reqSynCs,notSyns,notSynCs) then
swapSyns(unit,oldSyns,oldSynCs,newSyns,resetP)
end
end
end
end
end
function getCallback(args)
return getCallbackInner(args.oldSyn,args.oldSynClass,args.newSyn,args.reqSyn,args.reqSynClass,args.notSyn,args.notSynClass,args.resetPolicy)
end
--note - repeatCall is for internal use only
validArgs = validArgs or utils.invert({
'disable',
'help',
'noRepeat',
'name',
'tickRate',
'dontDisperseCalls',
'oldSyn',
'oldSynClass',
'newSyn',
'resetPolicy',
'reqSyn',
'reqSynClass',
'notSyn',
'notSynClass'
})
local args = utils.processArgs({...}, validArgs)
if args.help then
print(usage)
return
end
if args.disable then
repeatUtil.cancel(args.name)
return
end
if not args.tickRate then
args.tickRate=defaultTickRate
end
if type(args.oldSyn)=="string" then
args.oldSyn= {args.oldSyn}
end
if not args.oldSyn then
args.oldSyn={}
end
if type(args.oldSynClass)=="string" then
args.oldSynClass= {args.oldSynClass}
end
if not args.oldSynClass then
args.oldSynClass= {}
end
if type(args.newSyn)=="string" then
args.newSyn= {args.newSyn}
end
if not args.newSyn then
args.newSyn={}
end
if type(args.reqSyn)=="string" then
args.reqSyn={args.reqSyn}
end
if not args.reqSyn then
args.reqSyn={}
end
if type(args.reqSynClass)=="string" then
args.reqSynClass={args.reqSynClass}
end
if not args.reqSynClass then
args.reqSynClass={}
end
if type(args.notSyn)=="string" then
args.notSyn= {args.notSyn}
end
if not args.notSyn then
args.notSyn= {}
end
if type(args.notSynClass)=="string" then
args.notSynClass= {args.notSynClass}
end
if not args.notSynClass then
args.notSynClass= {}
end
if not args.resetPolicy then args.resetPolicy=defaultResetPolicy
else
assert( syndromeUtil.ResetPolicy[args.resetPolicy],"error - resetPolicy must be: DoNothing, ResetDuration, AddDuration or NewInstance")
args.resetPolicy=syndromeUtil.ResetPolicy[args.resetPolicy]
end
callback=getCallback(args)
if not args.noRepeat then
registerScript(args.name,callback,args.tickRate,args.dontDisperseCalls)
end
callback()
This one isn't as thoroughly tested as I would like, so tell me if anything breaks. Here is an example call from Fallout Equestra.
foe/swap-syndrome -name "Time Stutter" -tickRate 2000 -oldSyn "Time Stutter School Tag" -newSyn "Time Stutter Apprentice School Knowledge" -notSynClass [ "CONJURATION_APPRENTICE_SCHOOL" "DESTRUCTION_APPRENTICE_SCHOOL" "ILLUSION_APPRENTICE_SCHOOL" "RESTORATION_APPRENTICE_SCHOOL" ] -resetPolicy DoNothing