local _ENV = mkmodule('plugins.dwarfvet') local argparse = require('argparse') local utils = require('utils') local GLOBAL_KEY = 'dwarfvet' -- used for reassigning animals to pastures after treatment tracked_patients = tracked_patients or {} local function persist_state() local state = {} for unit_id, zone_id in pairs(tracked_patients) do table.insert(state, {unit=unit_id, zone=zone_id}) end dfhack.persistent.saveSiteData(GLOBAL_KEY, state) end dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_MAP_UNLOADED then tracked_patients = {} return end if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then return end local state = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) tracked_patients = {} for _,elem in ipairs(state) do tracked_patients[elem.unit] = elem.zone end end local function is_valid_animal(unit) return unit and dfhack.units.isActive(unit) and dfhack.units.isAnimal(unit) and dfhack.units.isFortControlled(unit) and dfhack.units.isTame(unit) and not dfhack.units.isDead(unit) and not dfhack.units.getGeneralRef(unit, df.general_ref_type.BUILDING_CHAIN) and not dfhack.units.getGeneralRef(unit, df.general_ref_type.CONTAINED_IN_ITEM) end local function get_cur_patients() local cur_patients = {} for _,job in utils.listpairs(df.global.world.jobs.list) do if job.job_type ~= df.job_type.Rest then goto continue end local unit = dfhack.job.getWorker(job) if is_valid_animal(unit) then cur_patients[unit.id] = unit end ::continue:: end return cur_patients end local function get_new_patients(cur_patients) local new_patients = {} for _,unit in ipairs(df.global.world.units.active) do if unit.job.current_job then goto continue end if not unit.health or not unit.health.flags.needs_healthcare then goto continue end if cur_patients[unit.id] or not is_valid_animal(unit) then goto continue end new_patients[unit.id] = unit ::continue:: end return new_patients end local function print_status() print(('dwarfvet is %srunning'):format(isEnabled() and '' or 'not ')) print() print('The following animals are receiving treatment:') local cur_patients = get_cur_patients() if not next(cur_patients) then print(' None') else for _, unit in pairs(cur_patients) do print((' %s (%d)'):format(dfhack.units.getReadableName(unit), unit.id)) end end print() print('The following animals are injured and need treatment:') local new_patients = get_new_patients(cur_patients) if not next(new_patients) then print(' None') else for _, unit in pairs(new_patients) do print((' %s (%d)'):format(dfhack.units.getReadableName(unit), unit.id)) end end end HospitalZone = defclass(HospitalZone) HospitalZone.ATTRS{ zone=DEFAULT_NIL, } local ONE_TILE = xy2pos(1, 1) function HospitalZone:find_spot(unit_pos) self.x = self.x or self.zone.x1 self.y = self.y or self.zone.y1 local zone = self.zone for y=self.y,zone.y2 do for x=self.x,zone.x2 do local pos = xyz2pos(x, y, zone.z) if dfhack.maps.canWalkBetween(unit_pos, pos) and dfhack.buildings.containsTile(zone, x, y) and dfhack.buildings.checkFreeTiles(pos, ONE_TILE) then return pos end end end end local function detach_unit_from_pasture(bld, unit) if not bld or not df.building_civzonest:is_instance(bld) then return end for idx, id in ipairs(bld.assigned_units) do if id == unit.id then bld.assigned_units:erase(idx) break end end end local function unassign_and_remember_pasture(unit) for idx = #unit.general_refs-1, 0, -1 do local ref = unit.general_refs[idx] if df.general_ref_building_civzone_assignedst:is_instance(ref) then detach_unit_from_pasture(df.building.find(ref.building_id), unit) tracked_patients[unit.id] = ref.building_id unit.general_refs:erase(idx) ref:delete() end end persist_state() end local function attach_to_pasture(unit, zone) if not unit or not zone then return end local ref = df.new(df.general_ref_building_civzone_assignedst) ref.building_id = zone.id; unit.general_refs:insert('#', ref) utils.insert_sorted(zone.assigned_units, unit.id) end -- TODO: If health.should_not_move is set, the creature can't move under its own power -- and a Recover Wounded or Pen/Pasture job must be created by hand function HospitalZone:assign_spot(unit, unit_pos) local pos = self:find_spot(unit_pos) if not pos then return false end local job = df.job:new() dfhack.job.linkIntoWorld(job, true) job.pos.x = pos.x job.pos.y = pos.y job.pos.z = pos.z job.flags.special = true job.job_type = df.job_type.Rest local gref = df.new(df.general_ref_unit_workerst) gref.unit_id = unit.id job.general_refs:insert('#', gref) unit.job.current_job = job unassign_and_remember_pasture(unit) return true end local function get_hospital_zones() local hospital_zones = {} local site = dfhack.world.getCurrentSite() or {buildings={}} for _,location in ipairs(site.buildings) do if not df.abstract_building_hospitalst:is_instance(location) then goto continue end for _,bld_id in ipairs(location.contents.building_ids) do local zone = df.building.find(bld_id) if zone then table.insert(hospital_zones, HospitalZone{zone=zone}) end end ::continue:: end return hospital_zones end local function distance(zone, pos) return math.abs(zone.x1 - pos.x) + math.abs(zone.y1 - pos.y) + 50*math.abs(zone.z - pos.z) end local function reassign_healed_animals_to_pastures(cur_patients) for unit_id, zone_id in pairs(tracked_patients) do if cur_patients[unit_id] then goto continue end tracked_patients[unit_id] = nil local unit = df.unit.find(unit_id) if is_valid_animal(unit) and not dfhack.units.getGeneralRef(unit, df.general_ref_type.BUILDING_CIVZONE_ASSIGNED) then attach_to_pasture(unit, df.building.find(zone_id)) end ::continue:: end persist_state() end function checkup() local cur_patients = get_cur_patients() reassign_healed_animals_to_pastures(cur_patients) local new_patients = get_new_patients(cur_patients) if not next(new_patients) then return end local hospital_zones = get_hospital_zones() local count, assigned = 0, 0 for _, unit in pairs(new_patients) do count = count + 1 local unit_pos = xyz2pos(dfhack.units.getPosition(unit)) table.sort(hospital_zones, function(a, b) return distance(a.zone, unit_pos) < distance(b.zone, unit_pos) end) for _,hospital_zone in ipairs(hospital_zones) do if hospital_zone:assign_spot(unit, unit_pos) then assigned = assigned + 1 break end end end print(('dwarfvet scheduled treatment for %d of %d injured animals'):format(assigned, count)) end local function process_args(opts, args) if args[1] == 'help' then opts.help = true return end return argparse.processArgsGetopt(args, { {'h', 'help', handler=function() opts.help = true end}, }) end function parse_commandline(args) local opts = {} local positionals = process_args(opts, args) if opts.help or not positionals then return false end local command = positionals[1] if not command or command == 'status' then print_status() elseif command == 'now' then dwarfvet_cycle() else return false end return true end return _ENV