├── README ├── TODO ├── ai.rb └── ai ├── main.rb ├── population.rb └── stocks.rb /README: -------------------------------------------------------------------------------- 1 | Watch Dwarf Fortress play itself ! 2 | 3 | Script for DFHack 0.34.11-r3 4 | 5 | Installation: copy all files to df/hack/scripts 6 | 7 | Start a fresh embark on an area with no aquifer, and with the caverns not too close to the surface. 8 | For best results, choose a site with a river on the west side. 9 | 10 | Run 'ai start' in the dfhack console, thats all. 11 | 12 | Does not handle already started forts, or resume from saved game. 13 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | autobutcher 2 | autofarm 3 | move animals between pastures when no grass 4 | non-cheaty farming with no soil 5 | engrave unburiable ghosts 6 | militia patrols 7 | militia actions (siege, ambush, megabeast) 8 | watch towers 9 | animal taming 10 | war training 11 | fortress guards 12 | justice 13 | way to empty cages 14 | stripcaged 15 | smelt goblinite 16 | robust miner pathfinding 17 | actual miner exploration, no cheat 18 | dig to find noneconomic stone when needed 19 | magma forges 20 | handle bad embark situations 21 | no water 22 | no wood 23 | aquifer 24 | glacier 25 | evil biomes 26 | caverns close to surface 27 | trade with civs 28 | make whole map reachable (bridge over river) 29 | adaptative fortress plan 30 | save, resume from save 31 | burrows 32 | recover from cistern floodgate fail 33 | megaprojects 34 | 35 | fix layout furnish bug 36 | handle ethics in bone count 37 | -------------------------------------------------------------------------------- /ai.rb: -------------------------------------------------------------------------------- 1 | # a dwarf fortress autonomous artificial intelligence (more or less) 2 | 3 | case $script_args[0] 4 | when 'start' 5 | Dir['hack/scripts/ai/*.rb'].each { |f| load f } 6 | 7 | if df.curview._raw_rtti_classname == 'viewscreen_titlest' 8 | df.curview.feed_keys(:SELECT) 9 | df.curview.feed_keys(:SELECT) 10 | end 11 | 12 | $dwarfAI = DwarfAI.new 13 | 14 | df.onupdate_register_once('df-ai start') { 15 | if df.curview._raw_rtti_classname == 'viewscreen_dwarfmodest' 16 | begin 17 | $dwarfAI.onupdate_register 18 | $dwarfAI.startup 19 | df.curview.feed_keys(:D_PAUSE) if df.pause_state 20 | rescue Exception 21 | puts $!, $!.backtrace 22 | end 23 | true 24 | end 25 | } 26 | 27 | when 'end', 'stop' 28 | $dwarfAI.onupdate_unregister 29 | puts "removed onupdate" 30 | 31 | when 'patch', 'update' 32 | Dir['hack/scripts/ai/*.rb'].each { |f| load f } 33 | 34 | else 35 | if $dwarfAI 36 | puts $dwarfAI.status 37 | else 38 | puts "AI not started (hint: ai start)" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /ai/main.rb: -------------------------------------------------------------------------------- 1 | class DwarfAI 2 | attr_accessor :plan 3 | attr_accessor :pop 4 | attr_accessor :stocks 5 | 6 | def initialize 7 | @pop = Population.new(self) 8 | @plan = Plan.new(self) 9 | @stocks = Stocks.new(self) 10 | end 11 | 12 | def debug(str) 13 | puts "AI: #{df.cur_year}:#{df.cur_year_tick} #{str}" if $DEBUG 14 | end 15 | 16 | def startup 17 | @pop.startup 18 | @plan.startup 19 | @stocks.startup 20 | end 21 | 22 | def handle_pause_event(announce) 23 | # unsplit announce text 24 | fulltext = announce.text 25 | idx = df.world.status.announcements.index(announce) 26 | while announce.flags.continuation 27 | announce = df.world.status.announcements[idx -= 1] 28 | break if not announce 29 | fulltext = announce.text + ' ' + fulltext 30 | end 31 | puts " #{df.cur_year}:#{df.cur_year_tick} #{fulltext.inspect}" 32 | 33 | case announce.type 34 | when :MEGABEAST_ARRIVAL; puts 'AI: uh oh, megabeast...' 35 | when :BERSERK_CITIZEN; puts 'AI: berserk' 36 | when :UNDEAD_ATTACK; puts 'AI: i see dead people' 37 | when :CAVE_COLLAPSE; puts 'AI: kevin?' 38 | when :DIG_CANCEL_DAMP, :DIG_CANCEL_WARM; puts 'AI: lazy miners' 39 | when :BIRTH_CITIZEN; puts 'AI: newborn' 40 | when :BIRTH_ANIMAL 41 | when :D_MIGRANTS_ARRIVAL, :D_MIGRANT_ARRIVAL, :MIGRANT_ARRIVAL, :NOBLE_ARRIVAL 42 | puts 'AI: more minions' 43 | when :DIPLOMAT_ARRIVAL, :LIAISON_ARRIVAL, :CARAVAN_ARRIVAL, :TRADE_DIPLOMAT_ARRIVAL 44 | puts 'AI: visitors' 45 | when :STRANGE_MOOD, :MOOD_BUILDING_CLAIMED, :ARTIFACT_BEGUN, :MADE_ARTIFACT 46 | puts 'AI: mood' 47 | when :FEATURE_DISCOVERY, :STRUCK_DEEP_METAL; puts 'AI: dig dig dig' 48 | when :TRAINING_FULL_REVERSION; puts 'AI: born to be wild' 49 | when :NAMED_ARTIFACT; puts 'AI: hallo' 50 | else 51 | if announce.type.to_s =~ /^AMBUSH/ 52 | puts 'AI: an ambush!' 53 | else 54 | puts "AI: unhandled pausing event #{announce.type.inspect} #{announce.inspect}" 55 | return 56 | end 57 | end 58 | 59 | df.pause_state = false 60 | end 61 | 62 | def statechanged(st) 63 | # automatically unpause the game (only for game-generated pauses) 64 | if st == :PAUSED and 65 | la = df.world.status.announcements.to_a.reverse.find { |a| 66 | df.d_init.announcements.flags[a.type].PAUSE rescue nil 67 | } and la.year == df.cur_year and la.time == df.cur_year_tick 68 | handle_pause_event(la) 69 | 70 | elsif st == :VIEWSCREEN_CHANGED 71 | case cvname = df.curview._rtti_classname 72 | when :viewscreen_textviewerst 73 | text = df.curview.formatted_text.map { |t| 74 | t.text.to_s.strip.gsub(/\s+/, ' ') 75 | }.join(' ') 76 | 77 | if text =~ /I am your liaison from the Mountainhomes\. Let's discuss your situation\.|Farewell, .*I look forward to our meeting next year\.|A diplomat has left unhappy\./ 78 | puts "AI: exit diplomat textviewerst (#{text.inspect})" 79 | timeout_sameview { 80 | df.curview.feed_keys(:LEAVESCREEN) 81 | } 82 | 83 | elsif text =~ /A vile force of darkness has arrived!/ 84 | puts "AI: siege (#{text.inspect})" 85 | timeout_sameview { 86 | df.curview.feed_keys(:LEAVESCREEN) 87 | df.pause_state = false 88 | } 89 | 90 | elsif text =~ /Your strength has been broken\.|Your settlement has crumbled to its end\./ 91 | puts "AI: you just lost the game:", text.inspect, "Exiting AI." 92 | onupdate_unregister 93 | # dont unpause, to allow for 'die' 94 | 95 | else 96 | puts "AI: paused in unknown textviewerst #{text.inspect}" if $DEBUG 97 | end 98 | 99 | when :viewscreen_topicmeetingst 100 | timeout_sameview { 101 | df.curview.feed_keys(:OPTION1) 102 | } 103 | #puts "AI: exit diplomat topicmeetingst" 104 | 105 | when :viewscreen_topicmeeting_takerequestsst, :viewscreen_requestagreementst 106 | puts "AI: exit diplomat #{cvname}" 107 | timeout_sameview { 108 | df.curview.feed_keys(:LEAVESCREEN) 109 | } 110 | 111 | else 112 | @seen_cvname ||= { :viewscreen_dwarfmodest => true } 113 | puts "AI: paused in unknown viewscreen #{cvname}" if not @seen_cvname[cvname] and $DEBUG 114 | @seen_cvname[cvname] = true 115 | end 116 | end 117 | end 118 | 119 | def timeout_sameview(delay=8, &cb) 120 | curscreen = df.curview._rtti_classname 121 | timeoff = Time.now + delay 122 | 123 | df.onupdate_register_once('timeout') { 124 | next true if df.curview._rtti_classname != curscreen 125 | 126 | if Time.now >= timeoff 127 | cb.call 128 | true 129 | end 130 | } 131 | end 132 | 133 | def onupdate_register 134 | @pop.onupdate_register 135 | @plan.onupdate_register 136 | @stocks.onupdate_register 137 | @status_onupdate = df.onupdate_register('df-ai status', 3*28*1200, 3*28*1200) { puts status } 138 | 139 | df.onstatechange_register_once { |st| 140 | case st 141 | when :WORLD_UNLOADED 142 | puts 'AI: world unloaded, disabling self' 143 | onupdate_unregister 144 | true 145 | else 146 | statechanged(st) 147 | false 148 | end 149 | } 150 | end 151 | 152 | def onupdate_unregister 153 | @stocks.onupdate_unregister 154 | @plan.onupdate_unregister 155 | @pop.onupdate_unregister 156 | df.onupdate_unregister(@status_onupdate) 157 | end 158 | 159 | def status 160 | ["Plan: #{plan.status}", "Pop: #{pop.status}", "Stocks: #{stocks.status}"] 161 | end 162 | 163 | def inspect 164 | "#" 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /ai/population.rb: -------------------------------------------------------------------------------- 1 | class DwarfAI 2 | class Population 3 | class Citizen 4 | attr_accessor :id, :role, :idlecounter, :entitypos 5 | def initialize(id) 6 | @id = id 7 | @idlecounter = 0 8 | @entitypos = [] 9 | end 10 | 11 | def dfunit 12 | df.unit_find(@id) 13 | end 14 | end 15 | 16 | attr_accessor :ai, :citizen, :military, :pet 17 | attr_accessor :labor_worker, :worker_labor 18 | attr_accessor :onupdate_handle 19 | def initialize(ai) 20 | @ai = ai 21 | @citizen = {} 22 | @military = {} 23 | @pet = {} 24 | @update_counter = 0 25 | end 26 | 27 | def startup 28 | df.standing_orders_forbid_used_ammo = 0 29 | end 30 | 31 | def onupdate_register 32 | @onupdate_handle = df.onupdate_register('df-ai pop', 360, 10) { update } 33 | end 34 | 35 | def onupdate_unregister 36 | df.onupdate_unregister(@onupdate_handle) 37 | end 38 | 39 | def update 40 | @update_counter += 1 41 | @onupdate_handle.description = "df-ai pop #{@update_counter % 10}" 42 | case @update_counter % 10 43 | when 1; update_citizenlist 44 | when 2; update_nobles 45 | when 3; update_jobs 46 | when 4; update_military 47 | when 5; update_pets 48 | when 6; update_deads 49 | end 50 | @onupdate_handle.description = "df-ai pop" 51 | 52 | i = 0 53 | bga = df.onupdate_register('df-ai pop autolabors', 3) { 54 | bga.description = "df-ai pop autolabors #{i}" 55 | autolabors(i) 56 | df.onupdate_unregister(bga) if i > 10 57 | i += 1 58 | } if @update_counter % 3 == 0 59 | end 60 | 61 | def new_citizen(id) 62 | c = Citizen.new(id) 63 | @citizen[id] = c 64 | ai.plan.new_citizen(id) 65 | c 66 | end 67 | 68 | def del_citizen(id) 69 | @citizen.delete id 70 | @military.delete id 71 | ai.plan.del_citizen(id) 72 | end 73 | 74 | def update_citizenlist 75 | old = @citizen.dup 76 | 77 | # add new fort citizen to our list 78 | df.unit_citizens.each { |u| 79 | next if u.profession == :BABY 80 | @citizen[u.id] ||= new_citizen(u.id) 81 | @citizen[u.id].entitypos = df.unit_entitypositions(u) 82 | old.delete u.id 83 | } 84 | 85 | # del those who are no longer here 86 | old.each { |id, c| 87 | # u.counters.death_tg.flags.discovered dead/missing 88 | del_citizen(id) 89 | } 90 | end 91 | 92 | def update_jobs 93 | df.world.jobs.postings.each { |jp| 94 | jp.job.flags.suspend = false if jp.job and jp.job.flags.suspend and not jp.job.flags.repeat 95 | } 96 | end 97 | 98 | def update_deads 99 | # TODO engrave slabs for ghosts 100 | end 101 | 102 | def update_military 103 | # check for new soldiers, allocate barracks 104 | newsoldiers = [] 105 | 106 | df.unit_citizens.each { |u| 107 | if u.military.squad_id == -1 108 | if @military.delete u.id 109 | ai.plan.freesoldierbarrack(u.id) 110 | end 111 | else 112 | if not @military[u.id] 113 | @military[u.id] = u.military.squad_id 114 | newsoldiers << u.id 115 | end 116 | end 117 | } 118 | 119 | # enlist new soldiers if needed 120 | maydraft = df.unit_citizens.reject { |u| 121 | u.profession == :CHILD or 122 | u.profession == :BABY or 123 | u.mood != :None 124 | } 125 | while @military.length < maydraft.length/5 126 | ns = military_find_new_soldier(maydraft) 127 | break if not ns 128 | @military[ns.id] = ns.military.squad_id 129 | newsoldiers << ns.id 130 | end 131 | 132 | newsoldiers.each { |uid| 133 | ai.plan.getsoldierbarrack(uid) 134 | } 135 | 136 | df.ui.main.fortress_entity.squads_tg.each { |sq| 137 | soldier_count = sq.positions.find_all { |sp| sp.occupant != -1 }.length 138 | sq.schedule[1].each { |sc| 139 | sc.orders.each { |so| 140 | next unless so.order.kind_of?(DFHack::SquadOrderTrainst) 141 | so.min_count = (soldier_count > 3 ? soldier_count-1 : soldier_count) 142 | } 143 | } 144 | } 145 | end 146 | 147 | # returns an unit newly assigned to a military squad 148 | def military_find_new_soldier(unitlist) 149 | ns = unitlist.find_all { |u| 150 | u.military.squad_id == -1 151 | }.sort_by { |u| 152 | unit_totalxp(u) + 5000*df.unit_entitypositions(u).length 153 | }.first 154 | return if not ns 155 | 156 | squad_id = military_find_free_squad 157 | return if not squad_id 158 | squad = df.world.squads.all.binsearch(squad_id) 159 | pos = squad.positions.index { |p| p.occupant == -1 } 160 | return if not pos 161 | 162 | squad.positions[pos].occupant = ns.hist_figure_id 163 | ns.military.squad_id = squad_id 164 | ns.military.squad_position = pos 165 | 166 | ent = df.ui.main.fortress_entity 167 | if !ent.positions.assignments.find { |a| a.squad_id == squad_id } 168 | if ent.assignments_by_type[:MILITARY_STRATEGY].empty? 169 | assign_new_noble('MILITIA_COMMANDER', ns).squad_id = squad_id 170 | else 171 | assign_new_noble('MILITIA_CAPTAIN', ns).squad_id = squad_id 172 | end 173 | end 174 | 175 | ns 176 | end 177 | 178 | # return a squad index with an empty slot 179 | def military_find_free_squad 180 | squad_sz = 8 181 | squad_sz = 6 if @military.length < 4*6 182 | squad_sz = 4 if @military.length < 3*4 183 | 184 | if not squad_id = df.ui.main.fortress_entity.squads.find { |sqid| @military.count { |k, v| v == sqid } < squad_sz } 185 | 186 | # create a new squad from scratch 187 | squad_id = df.squad_next_id 188 | df.squad_next_id = squad_id+1 189 | 190 | squad = DFHack::Squad.cpp_new :id => squad_id 191 | 192 | squad.name.first_name = "AI squad #{squad_id}" 193 | squad.name.type = :NONE 194 | squad.name.has_name = true 195 | 196 | squad.cur_alert_idx = 1 # train 197 | squad.uniform_priority = 2 198 | squad.carry_food = 2 199 | squad.carry_water = 2 200 | 201 | item_type = { :Body => :ARMOR, :Head => :HELM, :Pants => :PANTS, 202 | :Gloves => :GLOVES, :Shoes => :SHOES, :Shield => :SHIELD, 203 | :Weapon => :WEAPON } 204 | # uniform 205 | 10.times { 206 | pos = DFHack::SquadPosition.cpp_new 207 | [:Body, :Head, :Pants, :Gloves, :Shoes, :Shield, :Weapon].each { |t| 208 | pos.uniform[t] << DFHack::SquadUniformSpec.cpp_new(:color => -1, 209 | :item_filter => {:item_type => item_type[t], :material_class => :Armor, 210 | :mattype => -1, :matindex => -1}) 211 | } 212 | pos.uniform[:Weapon][0].indiv_choice.melee = true 213 | pos.uniform[:Weapon][0].item_filter.material_class = :None 214 | pos.flags.exact_matches = true 215 | #pos.unk_118 = pos.unk_11c = -1 216 | squad.positions << pos 217 | } 218 | 219 | if df.ui.main.fortress_entity.squads.length % 3 == 2 220 | # ranged squad 221 | squad.positions.each { |pos| 222 | pos.uniform[:Weapon][0].indiv_choice.melee = false 223 | pos.uniform[:Weapon][0].indiv_choice.ranged = true 224 | } 225 | squad.ammunition << DFHack::SquadAmmoSpec.cpp_new(:item_filter => { :item_type => :AMMO, 226 | :item_subtype => 0, :material_class => :None}, # subtype = bolts 227 | :amount => 100, :flags => { :use_combat => true, :use_training => true }) 228 | end 229 | 230 | # schedule 231 | df.ui.alerts.list.each { |alert| 232 | # squad.schedule.index = alerts.list.index (!= alert.id) 233 | squad.schedule << DFHack.malloc(DFHack::SquadScheduleEntry._sizeof*12) 234 | 12.times { |i| 235 | scm = squad.schedule.last[i] 236 | scm._cpp_init 237 | 10.times { scm.order_assignments << -1 } 238 | 239 | case squad.schedule.length # currently definied alert + 1 240 | when 2 241 | # train for 2 month, free the 3rd 242 | next if i % 3 == df.ui.main.fortress_entity.squads.length % 3 243 | scm.orders << DFHack::SquadScheduleOrder.cpp_new(:min_count => 0, 244 | :positions => [false]*10, :order => DFHack::SquadOrderTrainst.cpp_new) 245 | end 246 | } 247 | } 248 | 249 | # link squad into world 250 | df.world.squads.all << squad 251 | df.ui.squads.list << squad 252 | df.ui.main.fortress_entity.squads << squad.id 253 | end 254 | 255 | squad_id 256 | end 257 | 258 | 259 | LaborList = DFHack::UnitLabor::ENUM.sort.transpose[1] - [:NONE] 260 | LaborTool = { :MINE => true, :CUTWOOD => true, :HUNT => true } 261 | LaborSkill = DFHack::JobSkill::Labor.invert 262 | 263 | LaborMin = Hash.new(2).update :DETAIL => 4, :PLANT => 4 264 | LaborMax = Hash.new(8).update :FISH => 0 265 | LaborMinPct = Hash.new(10).update :DETAIL => 20, :PLANT => 30, :FISH => 1 266 | LaborMaxPct = Hash.new(00).update :DETAIL => 40, :PLANT => 60, :FISH => 3 267 | LaborList.each { |lb| 268 | if lb.to_s =~ /HAUL/ 269 | LaborMinPct[lb] = 30 270 | LaborMaxPct[lb] = 60 271 | end 272 | } 273 | LaborWontWorkJob = { :AttendParty => true, :Rest => true, 274 | :UpdateStockpileRecords => true } 275 | 276 | def autolabors(step) 277 | case step 278 | when 2 279 | @workers = [] 280 | @idlers = [] 281 | @labor_needmore = Hash.new(0) 282 | nonworkers = [] 283 | 284 | citizen.each_value { |c| 285 | next if not u = c.dfunit 286 | if u.mood == :None and 287 | u.profession != :CHILD and 288 | u.profession != :BABY and 289 | !unit_hasmilitaryduty(u) and 290 | !unit_shallnotworknow(u) and 291 | (!u.job.current_job or !LaborWontWorkJob[u.job.current_job.job_type]) and 292 | not u.status.misc_traits.find { |mt| mt.id == :OnBreak } and 293 | not u.specific_refs.find { |sr| sr.type == :ACTIVITY } 294 | # TODO filter nobles that will not work 295 | @workers << c 296 | @idlers << c if not u.job.current_job 297 | else 298 | nonworkers << c 299 | end 300 | } 301 | 302 | # free non-workers 303 | nonworkers.each { |c| 304 | u = c.dfunit 305 | ul = u.status.labors 306 | LaborList.each { |lb| 307 | if ul[lb] 308 | # disable everything (may wait meeting) 309 | ul[lb] = false 310 | # free pick/axe/crossbow XXX does it work? 311 | u.military.pickup_flags.update = true if LaborTool[lb] 312 | end 313 | } 314 | } 315 | 316 | seen_workshop = {} 317 | df.world.jobs.list.each { |job| 318 | ref_bld = job.general_refs.grep(DFHack::GeneralRefBuildingHolderst).first 319 | ref_wrk = job.general_refs.grep(DFHack::GeneralRefUnitWorkerst).first 320 | next if ref_bld and seen_workshop[ref_bld.building_id] 321 | 322 | if not ref_wrk 323 | job_labor = DFHack::JobSkill::Labor[DFHack::JobType::Skill[job.job_type]] 324 | job_labor = DFHack::JobType::Labor.fetch(job.job_type, job_labor) 325 | if job_labor and job_labor != :NONE 326 | @labor_needmore[job_labor] += 1 327 | 328 | else 329 | case job.job_type 330 | when :ConstructBuilding, :DestroyBuilding 331 | # TODO 332 | when :PullLever 333 | when :CustomReaction 334 | reac = df.world.raws.reactions.reactions.find { |r| r.code == job.reaction_name } 335 | if reac and job_labor = DFHack::JobSkill::Labor[reac.skill] 336 | @labor_needmore[job_labor] += 1 if job_labor != :NONE 337 | end 338 | when :PenSmallAnimal, :PenLargeAnimal 339 | @labor_needmore[:HAUL_ANIMAL] += 1 340 | when :StoreItemInStockpile, :StoreItemInBag, :StoreItemInHospital, 341 | :StoreItemInChest, :StoreItemInCabinet, :StoreWeapon, 342 | :StoreArmor, :StoreItemInBarrel, :StoreItemInBin, :StoreItemInVehicle 343 | @labor_needmore[:HAUL_ITEM] += 1 344 | else 345 | if job.material_category.wood 346 | @labor_needmore[:CARPENTER] += 1 347 | elsif job.material_category.bone 348 | @labor_needmore[:BONE_CARVE] += 1 349 | elsif job.material_category.cloth 350 | @labor_needmore[:CLOTHESMAKER] += 1 351 | elsif job.mat_type == 0 352 | # XXX metalcraft ? 353 | @labor_needmore[:MASON] += 1 354 | elsif $DEBUG 355 | @seen_badwork ||= {} 356 | @ai.debug "unknown labor for #{job.job_type} #{job.inspect}" if not @seen_badwork[job.job_type] 357 | @seen_badwork[job.job_type] = true 358 | end 359 | end 360 | end 361 | end 362 | 363 | if ref_bld 364 | case ref_bld.building_tg 365 | when DFHack::BuildingFarmplotst 366 | # parallel work allowed 367 | else 368 | seen_workshop[ref_bld.building_id] = true 369 | end 370 | end 371 | 372 | } 373 | 374 | when 3 375 | # count active labors 376 | @labor_worker = LaborList.inject({}) { |h, lb| h.update lb => [] } 377 | @worker_labor = @workers.inject({}) { |h, c| h.update c.id => [] } 378 | @workers.each { |c| 379 | ul = c.dfunit.status.labors 380 | LaborList.each { |lb| 381 | if ul[lb] 382 | @labor_worker[lb] << c.id 383 | @worker_labor[c.id] << lb 384 | end 385 | } 386 | } 387 | 388 | # if one has too many labors, free him up (one per round) 389 | lim = 4*LaborList.length/[@workers.length, 1].max 390 | lim = 4 if lim < 4 391 | if cid = @worker_labor.keys.find { |id| @worker_labor[id].length > lim } 392 | c = citizen[cid] 393 | u = c.dfunit 394 | ul = u.status.labors 395 | 396 | LaborList.each { |lb| 397 | if ul[lb] 398 | @worker_labor[c.id].delete lb 399 | @labor_worker[lb].delete c.id 400 | ul[lb] = false 401 | u.military.pickup_flags.update = true if LaborTool[lb] 402 | end 403 | } 404 | end 405 | 406 | when 4 407 | labormin = LaborMin 408 | labormax = LaborMax 409 | laborminpct = LaborMinPct 410 | labormaxpct = LaborMaxPct 411 | 412 | # handle low-number of workers + tool labors 413 | mintool = LaborTool.keys.inject(0) { |s, lb| 414 | min = labormin[lb] 415 | minpc = laborminpct[lb] * @workers.length / 100 416 | min = minpc if minpc > min 417 | s + min 418 | } 419 | if @workers.length < mintool 420 | labormax = labormax.dup 421 | LaborTool.each_key { |lb| labormax[lb] = 0 } 422 | case @workers.length 423 | when 0 424 | # meh 425 | when 1 426 | # switch mine or cutwood based on time (1/2 dwarf month each) 427 | if (df.cur_year_tick / (1200*28/2)) % 2 == 0 428 | labormax[:MINE] = 1 429 | else 430 | labormax[:CUTWOOD] = 1 431 | end 432 | else 433 | @workers.length.times { |i| 434 | # divide equally between labors, with priority 435 | # to mine, then wood, then hunt 436 | # XXX new labortools ? 437 | lb = [:MINE, :CUTWOOD, :HUNT][i%3] 438 | labormax[lb] += 1 439 | } 440 | end 441 | end 442 | 443 | # list of dwarves with an exclusive labor 444 | exclusive = {} 445 | [ 446 | [:CARPENTER, lambda { r = ai.plan.find_room(:workshop) { |_r| _r.subtype == :Carpenters and _r.dfbuilding } and not r.dfbuilding.jobs.empty? }], 447 | [:MINE, lambda { ai.plan.digging? }], 448 | [:MASON, lambda { r = ai.plan.find_room(:workshop) { |_r| _r.subtype == :Masons and _r.dfbuilding } and not r.dfbuilding.jobs.empty? }], 449 | ].each { |lb, test| 450 | if @workers.length > exclusive.length+2 and test[] 451 | # keep last run's choice 452 | cid = @labor_worker[lb].sort_by { |i| @worker_labor[i].length }.first 453 | next if not cid 454 | exclusive[cid] = lb 455 | @idlers.delete_if { |_c| _c.id == cid } 456 | c = citizen[cid] 457 | @worker_labor[cid].dup.each { |llb| 458 | next if llb == lb 459 | autolabor_unsetlabor(c, llb) 460 | } 461 | end 462 | } 463 | 464 | # autolabor! 465 | LaborList.each { |lb| 466 | min = labormin[lb] 467 | max = labormax[lb] 468 | minpc = laborminpct[lb] * @workers.length / 100 469 | maxpc = labormaxpct[lb] * @workers.length / 100 470 | min = minpc if minpc > min 471 | max = maxpc if maxpc > max 472 | min = max if min > max 473 | min = @workers.length if min > @workers.length 474 | 475 | cnt = @labor_worker[lb].length 476 | if cnt > max 477 | sk = LaborSkill[lb] 478 | @labor_worker[lb] = @labor_worker[lb].sort_by { |_cid| 479 | if sk 480 | if usk = df.unit_find(_cid).status.current_soul.skills.find { |_usk| _usk.id == sk } 481 | DFHack::SkillRating.int(usk.rating) 482 | else 483 | 0 484 | end 485 | else 486 | rand 487 | end 488 | } 489 | (cnt-max).times { 490 | cid = @labor_worker[lb].shift 491 | autolabor_unsetlabor(citizen[cid], lb) 492 | } 493 | 494 | elsif cnt < min 495 | min += 1 if min < max and not LaborTool[lb] 496 | (min-cnt).times { 497 | c = @workers.sort_by { |_c| 498 | malus = @worker_labor[_c.id].length * 10 499 | malus += _c.entitypos.length * 40 500 | if sk = LaborSkill[lb] and usk = _c.dfunit.status.current_soul.skills.find { |_usk| _usk.id == sk } 501 | malus -= DFHack::SkillRating.int(usk.rating) * 4 # legendary => 15 502 | end 503 | [malus, rand] 504 | }.find { |_c| 505 | next if exclusive[_c.id] 506 | next if LaborTool[lb] and @worker_labor[_c.id].find { |_lb| LaborTool[_lb] } 507 | not @worker_labor[_c.id].include?(lb) 508 | } || @workers.find { |_c| not exclusive[_c.id] and not @worker_labor[_c.id].include?(lb) } 509 | 510 | autolabor_setlabor(c, lb) 511 | } 512 | 513 | elsif not @idlers.empty? 514 | @labor_needmore[lb].times { 515 | break if @labor_worker[lb].length >= max 516 | c = @idlers[rand(@idlers.length)] 517 | autolabor_setlabor(c, lb) 518 | } 519 | end 520 | } 521 | end 522 | end 523 | 524 | def autolabor_setlabor(c, lb) 525 | return if not c 526 | return if @worker_labor[c.id].include?(lb) 527 | @labor_worker[lb] << c.id 528 | @worker_labor[c.id] << lb 529 | u = c.dfunit 530 | if LaborTool[lb] 531 | LaborTool.keys.each { |_lb| u.status.labors[_lb] = false } 532 | u.military.pickup_flags.update = true 533 | end 534 | u.status.labors[lb] = true 535 | end 536 | 537 | def autolabor_unsetlabor(c, lb) 538 | return if not c 539 | @labor_worker[lb].delete c.id 540 | @worker_labor[c.id].delete lb 541 | u = c.dfunit 542 | u.status.labors[lb] = false 543 | u.military.pickup_flags.update = true if LaborTool[lb] 544 | end 545 | 546 | def unit_hasmilitaryduty(u) 547 | return if u.military.squad_id == -1 548 | squad = df.world.squads.all.binsearch(u.military.squad_id) 549 | curmonth = squad.schedule[squad.cur_alert_idx][df.cur_year_tick / (1200*28)] 550 | !curmonth.orders.empty? 551 | end 552 | 553 | def unit_shallnotworknow(u) 554 | # manager shall not work when unvalidated jobs are pending 555 | return true if df.world.manager_orders.last and df.world.manager_orders.last.status.validated == 0 and 556 | df.unit_entitypositions(u).find { |n| n.responsibilities[:MANAGE_PRODUCTION] } 557 | # TODO medical dwarf, broker 558 | end 559 | 560 | def unit_totalxp(u) 561 | u.status.current_soul.skills.inject(0) { |t, sk| 562 | rat = DFHack::SkillRating.int(sk.rating) 563 | t + 400*rat + 100*rat*(rat+1)/2 + sk.experience 564 | } 565 | end 566 | 567 | def update_nobles 568 | cz = df.unit_citizens.sort_by { |u| unit_totalxp(u) }.find_all { |u| u.profession != :BABY and u.profession != :CHILD } 569 | ent = df.ui.main.fortress_entity 570 | 571 | 572 | if ent.assignments_by_type[:MANAGE_PRODUCTION].empty? and tg = cz.find { |u| 573 | u.military.squad_id == -1 and !ent.positions.assignments.find { |a| a.histfig == u.hist_figure_id } 574 | } || cz.first 575 | # TODO do not hardcode position name, check population caps, ... 576 | assign_new_noble('MANAGER', tg) 577 | end 578 | 579 | 580 | if ent.assignments_by_type[:ACCOUNTING].empty? and tg = cz.find { |u| 581 | u.military.squad_id == -1 and !ent.positions.assignments.find { |a| a.histfig == u.hist_figure_id } and !u.status.labors[:MINE] 582 | } 583 | assign_new_noble('BOOKKEEPER', tg) 584 | df.ui.nobles.bookkeeper_settings = :AllAccurate 585 | end 586 | 587 | 588 | if ent.assignments_by_type[:HEALTH_MANAGEMENT].empty? and 589 | hosp = ai.plan.find_room(:infirmary) and hosp.status != :plan and tg = cz.find { |u| 590 | u.military.squad_id == -1 and !ent.positions.assignments.find { |a| a.histfig == u.hist_figure_id } 591 | } 592 | assign_new_noble('CHIEF_MEDICAL_DWARF', tg) 593 | elsif ass = ent.assignments_by_type[:HEALTH_MANAGEMENT].first and hf = df.world.history.figures.binsearch(ass.histfig) and doc = df.unit_find(hf.unit_id) 594 | # doc => healthcare 595 | LaborList.each { |lb| 596 | doc.status.labors[lb] = case lb 597 | when :DIAGNOSE, :SURGERY, :BONE_SETTING, :SUTURING, :DRESSING_WOUNDS, :FEED_WATER_CIVILIANS 598 | true 599 | end 600 | } 601 | end 602 | 603 | 604 | if ent.assignments_by_type[:TRADE].empty? and tg = cz.find { |u| 605 | u.military.squad_id == -1 and !ent.positions.assignments.find { |a| a.histfig == u.hist_figure_id } 606 | } 607 | assign_new_noble('BROKER', tg) 608 | end 609 | 610 | check_noble_appartments 611 | end 612 | 613 | def check_noble_appartments 614 | noble_ids = df.ui.main.fortress_entity.assignments_by_type.map { |alist| 615 | alist.map { |a| a.histfig_tg.unit_id } 616 | }.flatten.uniq.sort 617 | 618 | noble_ids.delete_if { |id| 619 | not df.unit_entitypositions(df.unit_find(id)).find { |ep| 620 | ep.required_office > 0 or ep.required_dining > 0 or ep.required_tomb > 0 621 | } 622 | } 623 | 624 | @ai.plan.attribute_noblerooms(noble_ids) 625 | end 626 | 627 | def assign_new_noble(pos_code, unit) 628 | ent = df.ui.main.fortress_entity 629 | 630 | pos = ent.positions.own.find { |p| p.code == pos_code } 631 | raise "no noble position #{pos_code}" if not pos 632 | 633 | if not assign = ent.positions.assignments.find { |a| a.position_id == pos.id and a.histfig == -1 } 634 | a_id = ent.positions.next_assignment_id 635 | ent.positions.next_assignment_id = a_id+1 636 | assign = DFHack::EntityPositionAssignment.cpp_new(:id => a_id, :position_id => pos.id) 637 | assign.flags.resize(ent.positions.assignments.first.flags.length/8) # XXX 638 | assign.flags[0] = true # XXX 639 | ent.positions.assignments << assign 640 | end 641 | 642 | poslink = DFHack::HistfigEntityLinkPositionst.cpp_new(:link_strength => 100, :start_year => df.cur_year) 643 | poslink.entity_id = df.ui.main.fortress_entity.id 644 | poslink.assignment_id = assign.id 645 | 646 | unit.hist_figure_tg.entity_links << poslink 647 | assign.histfig = unit.hist_figure_id 648 | 649 | pos.responsibilities.each_with_index { |r, k| 650 | ent.assignments_by_type[k] << assign if r 651 | } 652 | 653 | assign 654 | end 655 | 656 | def update_pets 657 | needmilk = -ai.stocks.find_manager_orders(:MilkCreature).inject(0) { |s, o| s + o.amount_left } 658 | needshear = -ai.stocks.find_manager_orders(:ShearCreature).inject(0) { |s, o| s + o.amount_left } 659 | 660 | np = @pet.dup 661 | df.world.units.active.each { |u| 662 | next if u.civ_id != df.ui.civ_id 663 | next if u.race == df.ui.race_id 664 | next if u.flags1.inactive or u.flags1.merchant or u.flags1.forest 665 | 666 | if @pet[u.id] 667 | if @pet[u.id].include?(:MILKABLE) and u.profession != :BABY and u.profession != :CHILD 668 | if not u.status.misc_traits.find { |mt| mt.id == :MilkCounter } 669 | needmilk += 1 670 | end 671 | end 672 | 673 | if @pet[u.id].include?(:SHEARABLE) and u.profession != :BABY and u.profession != :CHILD 674 | if u.caste_tg.shearable_tissue_layer.find { |stl| 675 | stl.bp_modifiers_idx.find { |bpi| 676 | u.appearance.bp_modifiers[bpi] >= stl.length 677 | } 678 | } 679 | needshear += 1 680 | end 681 | end 682 | 683 | np.delete u.id 684 | next 685 | end 686 | 687 | @pet[u.id] = [] 688 | 689 | cst = u.caste_tg 690 | 691 | if cst.flags[:MILKABLE] 692 | @pet[u.id] << :MILKABLE 693 | end 694 | 695 | if cst.shearable_tissue_layer.length > 0 696 | @pet[u.id] << :SHEARABLE 697 | end 698 | 699 | if cst.flags[:GRAZER] 700 | @pet[u.id] << :GRAZER 701 | 702 | if bld = @ai.plan.getpasture(u.id) 703 | assign_unit_to_zone(u, bld) 704 | # TODO monitor grass levels 705 | else 706 | # TODO slaughter best candidate, keep this one 707 | # also avoid killing named pets 708 | u.flags2.slaughter = true 709 | end 710 | end 711 | 712 | if cst.flags[:LAYS_EGGS] 713 | # TODO nest boxes 714 | end 715 | 716 | if cst.flags[:ADOPTS_OWNER] 717 | # keep only one 718 | oi = @pet.find_all { |i, t| t.include?(:ADOPTS_OWNER) } 719 | if oi.empty? 720 | @pet[u.id] << :ADOPTS_OWNER 721 | elsif u.caste != 0 and ou = df.unit_find(oi[0][0]) and ou.caste == 0 and !ou.flags2.slaughter 722 | # keep one male if possible 723 | @pet[u.id] << :ADOPTS_OWNER 724 | ou.flags2.slaughter = true 725 | else 726 | u.flags2.slaughter = true 727 | end 728 | end 729 | } 730 | 731 | np.each_key { |id| 732 | @ai.plan.freepasture(id) 733 | @pet.delete id 734 | } 735 | 736 | needmilk = 30 if needmilk > 30 737 | ai.stocks.add_manager_order(:MilkCreature, needmilk) if needmilk > 0 738 | 739 | needshear = 30 if needshear > 30 740 | ai.stocks.add_manager_order(:ShearCreature, needshear) if needshear > 0 741 | end 742 | 743 | def assign_unit_to_zone(u, bld) 744 | # remove existing zone assignments 745 | # TODO remove existing chains/cages ? 746 | while ridx = u.general_refs.index { |ref| ref.kind_of?(DFHack::GeneralRefBuildingCivzoneAssignedst) } 747 | ref = u.general_refs[ridx] 748 | cidx = ref.building_tg.assigned_units.index(u.id) 749 | ref.building_tg.assigned_units.delete_at(cidx) 750 | u.general_refs.delete_at(ridx) 751 | df.free(ref._memaddr) 752 | end 753 | 754 | u.general_refs << DFHack::GeneralRefBuildingCivzoneAssignedst.cpp_new(:building_id => bld.id) 755 | bld.assigned_units << u.id 756 | end 757 | 758 | def status 759 | "#{@citizen.length} citizen, #{@pet.length} pets" 760 | end 761 | end 762 | end 763 | -------------------------------------------------------------------------------- /ai/stocks.rb: -------------------------------------------------------------------------------- 1 | class DwarfAI 2 | # an object similar to a hash, that will evaluate and cache @proc for every key 3 | class CacheHash 4 | def initialize(&b) 5 | @h = {} 6 | @proc = b 7 | end 8 | 9 | def [](i) 10 | @h.fetch(i) { @h[i] = @proc.call(i) } 11 | end 12 | end 13 | 14 | class Stocks 15 | Needed = { 16 | :door => 4, :bed => 4, :bin => 4, :barrel => 4, 17 | :cabinet => 4, :chest => 4, :mechanism => 4, 18 | :bag => 3, :table => 3, :chair => 3, :cage => 3, 19 | :coffin => 2, :coffin_bld => 3, :coffin_bld_pet => 1, 20 | :food => 20, :drink => 20, :wood => 16, :bucket => 2, 21 | :pigtail_seeds => 10, :dimplecup_seeds => 10, :dimple_dye => 10, 22 | :weapon => 2, :armor => 2, :clothes => 2, :block => 6, 23 | :quiver => 2, :flask => 2, :backpack => 2, :wheelbarrow => 1, 24 | :splint => 1, :crutch => 1, :rope => 1, :weaponrack => 1, 25 | :armorstand => 1, :floodgate => 1, :traction_bench => 1, 26 | :soap => 1, :lye => 1, :ash => 1, :plasterpowder => 1, 27 | :coal => 3, :raw_coke => 1, :gypsum => 1, 28 | :giant_corkscrew => 1, :pipe_section => 1, 29 | :quern => 1, :minecart => 1, :nestbox => 1, :hive => 1, 30 | :jug => 1, 31 | :leather => 0, :tallow => 0, 32 | #:rock_noeco => 10, 33 | } 34 | NeededPerDwarf = Hash.new(0.0).update :food => 1, :drink => 2 35 | 36 | WatchStock = { :roughgem => 6, :pigtail => 10, :cloth_nodye => 10, 37 | :metal_ore => 6, :raw_coke => 2, :raw_adamantine => 2, 38 | :quarrybush => 4, :skull => 2, :bone => 8, :leaves => 5, 39 | :honeycomb => 1, :wool => 1, 40 | } 41 | 42 | attr_accessor :ai, :count 43 | attr_accessor :onupdate_handle 44 | def initialize(ai) 45 | @ai = ai 46 | reset 47 | end 48 | 49 | def reset 50 | @updating = [] 51 | @lastupdating = 0 52 | @count = {} 53 | end 54 | 55 | def startup 56 | end 57 | 58 | def onupdate_register 59 | reset 60 | @onupdate_handle = df.onupdate_register('df-ai stocks', 4800, 30) { update } 61 | end 62 | 63 | def onupdate_unregister 64 | df.onupdate_unregister(@onupdate_handle) 65 | end 66 | 67 | def status 68 | @count.map { |k, v| "#{k}: #{v}" }.sort.join(", ") 69 | end 70 | 71 | def update 72 | if not @updating.empty? and @lastupdating != @updating.length + @updating_count.length 73 | # avoid stall if cb_bg crashed and was unregistered 74 | @ai.debug 'not updating stocks' 75 | @lastupdating = @updating.length + @updating_count.length 76 | return 77 | end 78 | 79 | @last_unforbidall_year ||= df.cur_year 80 | if @last_unforbidall_year != df.cur_year 81 | @last_unforbidall_year = df.cur_year 82 | df.world.items.all.each { |i| i.flags.forbid = false } 83 | end 84 | 85 | # trim stalled manager orders once per month 86 | @last_managerstall ||= df.cur_year_tick / (1200*28) 87 | if @last_managerstall != df.cur_year_tick / (1200*28) 88 | @last_managerstall = df.cur_year_tick / (1200*28) 89 | if m = df.world.manager_orders.first and m.status.validated 90 | if m.job_type == @last_managerorder 91 | if m.amount_left > 3 92 | m.amount_left -= 3 93 | else 94 | df.world.manager_orders.delete_at(0) 95 | #m._cpp_delete # TODO once dfhack-0.34.11-r4 is out 96 | end 97 | else 98 | @last_managerorder = m.job_type 99 | end 100 | end 101 | end 102 | 103 | @updating = Needed.keys | WatchStock.keys 104 | @updating_count = @updating.dup 105 | @ai.debug 'updating stocks' 106 | 107 | # do stocks accounting 'in the background' (ie one bit at a time) 108 | cb_bg = df.onupdate_register('df-ai stocks bg', 8) { 109 | if key = @updating_count.shift 110 | cb_bg.description = "df-ai stocks bg count #{key}" 111 | @count[key] = count_stocks(key) 112 | elsif key = @updating.shift 113 | cb_bg.description = "df-ai stocks bg act #{key}" 114 | act(key) 115 | else 116 | # finished, dismiss callback 117 | df.onupdate_unregister(cb_bg) 118 | end 119 | } 120 | end 121 | 122 | def act(key) 123 | if amount = Needed[key] 124 | amount += (@ai.pop.citizen.length * NeededPerDwarf[key]).to_i 125 | queue_need(key, amount*3/2-@count[key]) if @count[key] < amount 126 | end 127 | 128 | if amount = WatchStock[key] 129 | queue_use(key, @count[key]-amount) if @count[key] > amount 130 | end 131 | end 132 | 133 | 134 | # count unused stocks of one type of item 135 | def count_stocks(k) 136 | case k 137 | when :bin 138 | df.world.items.other[:BIN].find_all { |i| i.stockpile.id == -1 } 139 | when :barrel 140 | df.world.items.other[:BARREL].find_all { |i| i.stockpile.id == -1 } 141 | when :bag 142 | df.world.items.other[:BOX].find_all { |i| df.decode_mat(i).plant } 143 | when :rope 144 | df.world.items.other[:CHAIN].find_all { |i| df.decode_mat(i).plant } 145 | when :bucket 146 | df.world.items.other[:BUCKET] 147 | when :food 148 | df.world.items.other[:ANY_GOOD_FOOD].reject { |i| 149 | case i 150 | when DFHack::ItemSeedsst, DFHack::ItemBoxst, DFHack::ItemFishRawst 151 | true 152 | end 153 | } 154 | when :drink 155 | df.world.items.other[:DRINK] 156 | when :soap, :coal, :ash 157 | mat_id = {:soap => 'SOAP', :coal => 'COAL', :ash => 'ASH'}[k] 158 | df.world.items.other[:BAR].find_all { |i| 159 | mat = df.decode_mat(i) and mat.material and mat.material.id == mat_id 160 | } 161 | when :wood 162 | df.world.items.other[:WOOD] 163 | when :roughgem 164 | df.world.items.other[:ROUGH].find_all { |i| i.mat_type == 0 } 165 | when :metal_ore 166 | df.world.items.other[:BOULDER].find_all { |i| is_metal_ore(i) } 167 | when :raw_coke 168 | df.world.items.other[:BOULDER].find_all { |i| is_raw_coke(i) } 169 | when :gypsum 170 | df.world.items.other[:BOULDER].find_all { |i| is_gypsum(i) } 171 | when :raw_adamantine 172 | df.world.items.other[:BOULDER].grep(df.decode_mat('INORGANIC:RAW_ADAMANTINE')) 173 | when :splint 174 | df.world.items.other[:SPLINT] 175 | when :crutch 176 | df.world.items.other[:CRUTCH] 177 | when :crossbow 178 | df.world.items.other[:WEAPON].find_all { |i| 179 | i.subtype.subtype == ManagerSubtype[:MakeBoneCrossbow] 180 | } 181 | when :pigtail, :dimplecup, :quarrybush 182 | # TODO generic handling, same as farm crops selection 183 | # TODO filter rotten 184 | mspec = { 185 | :pigtail => 'PLANT:GRASS_TAIL_PIG:STRUCTURAL', 186 | :dimplecup => 'PLANT:MUSHROOM_CUP_DIMPLE:STRUCTURAL', 187 | :quarrybush => 'PLANT:BUSH_QUARRY:STRUCTURAL', 188 | }[k] 189 | df.world.items.other[:PLANT].grep(df.decode_mat(mspec)) 190 | when :pigtail_seeds, :dimplecup_seeds 191 | mspec = { 192 | :pigtail_seeds => 'PLANT:GRASS_TAIL_PIG:SEED', 193 | :dimplecup_seeds => 'PLANT:MUSHROOM_CUP_DIMPLE:SEED', 194 | }[k] 195 | df.world.items.other[:SEEDS].grep(df.decode_mat(mspec)) 196 | when :dimple_dye 197 | mspec = 'PLANT:MUSHROOM_CUP_DIMPLE:MILL' 198 | df.world.items.other[:POWDER_MISC].grep(df.decode_mat(mspec)) 199 | when :leaves 200 | df.world.items.other[:PLANT_GROWTH] 201 | when :block 202 | df.world.items.other[:BLOCKS] 203 | when :skull 204 | # XXX exclude dwarf skulls ? 205 | df.world.items.other[:CORPSEPIECE].find_all { |i| 206 | i.corpse_flags.skull1 and not i.corpse_flags.unbutchered 207 | } 208 | when :bone 209 | return df.world.items.other[:CORPSEPIECE].find_all { |i| 210 | i.corpse_flags.bone and not i.corpse_flags.unbutchered 211 | }.inject(0) { |s, i| 212 | # corpsepieces uses this instead of i.stack_size 213 | s + i.material_amount[:Bone] 214 | } 215 | when :wool 216 | df.world.items.other[:CORPSEPIECE].find_all { |i| 217 | i.corpse_flags.hair_wool or i.corpse_flags.yarn 218 | #}.inject(0) { |s, i| 219 | # used for SpinThread which currently ignores the material_amount 220 | # note: if it didn't, use either HairWool or Yarn but not both 221 | } 222 | when :bonebolts 223 | df.world.items.other[:AMMO].find_all { |i| 224 | i.skill_used == :BONECARVE 225 | } 226 | when :cloth 227 | df.world.items.other[:CLOTH] 228 | when :cloth_nodye 229 | df.world.items.other[:CLOTH].find_all { |i| 230 | !i.improvements.find { |imp| imp.dye.mat_type != -1 } 231 | } 232 | when :mechanism 233 | df.world.items.other[:TRAPPARTS] 234 | when :cage 235 | df.world.items.other[:CAGE].reject { |i| 236 | i.general_refs.grep(DFHack::GeneralRefContainsUnitst).first or 237 | i.general_refs.grep(DFHack::GeneralRefContainsItemst).first or 238 | (ref = i.general_refs.grep(DFHack::GeneralRefBuildingHolderst).first and 239 | ref.building_tg.kind_of?(DFHack::BuildingTrapst)) 240 | } 241 | when :coffin_bld 242 | # count free constructed coffin buildings, not items 243 | return df.world.buildings.other[:COFFIN].find_all { |bld| !bld.owner }.length 244 | when :coffin_bld_pet 245 | return df.world.buildings.other[:COFFIN].find_all { |bld| !bld.owner and !bld.burial_mode.no_pets }.length 246 | when :weapon 247 | return count_stocks_weapon 248 | when :armor 249 | return count_stocks_armor 250 | when :clothes 251 | return count_stocks_clothes 252 | when :lye 253 | df.world.items.other[:LIQUID_MISC].find_all { |i| 254 | mat = df.decode_mat(i) and mat.material and mat.material.id == 'LYE' 255 | # TODO check container has no water 256 | } 257 | when :plasterpowder 258 | df.world.items.other[:POWDER_MISC].find_all { |i| 259 | mat = df.decode_mat(i) and mat.inorganic and mat.inorganic.id == 'PLASTER' 260 | } 261 | when :wheelbarrow, :minecart, :nestbox, :hive, :jug 262 | ord = FurnitureOrder[k] 263 | df.world.items.other[:TOOL].find_all { |i| 264 | i.subtype.subtype == ManagerSubtype[ord] and 265 | i.stockpile.id == -1 and 266 | (!i.vehicle_tg or i.vehicle_tg.route_id == -1) 267 | } 268 | when :honeycomb 269 | df.world.items.other[:TOOL].find_all { |i| i.subtype.id == 'ITEM_TOOL_HONEYCOMB' } 270 | when :quiver 271 | df.world.items.other[:QUIVER] 272 | when :flask 273 | df.world.items.other[:FLASK] 274 | when :backpack 275 | df.world.items.other[:BACKPACK] 276 | when :leather 277 | df.world.items.other[:SKIN_TANNED] 278 | when :tallow 279 | df.world.items.other[:GLOB].find_all { |i| 280 | mat = df.decode_mat(i) and mat.material and mat.material.id == 'TALLOW' 281 | # TODO filter rotten 282 | } 283 | when :giant_corkscrew 284 | df.world.items.other[:TRAPCOMP].find_all { |i| 285 | i.subtype.subtype == ManagerSubtype[:MakeGiantCorkscrew] 286 | } 287 | when :pipe_section 288 | df.world.items.other[:PIPE_SECTION] 289 | when :quern 290 | # include used in building 291 | return df.world.items.other[:QUERN].length 292 | else 293 | return find_furniture_itemcount(k) 294 | 295 | end.find_all { |i| 296 | is_item_free(i) 297 | }.inject(0) { |s, i| s + i.stack_size } 298 | end 299 | 300 | # return the minimum of the number of free weapons for each subtype used by current civ 301 | def count_stocks_weapon 302 | ue = df.ui.main.fortress_entity.entity_raw.equipment 303 | [[:WEAPON, ue.digger_tg], 304 | [:WEAPON, ue.weapon_tg]].map { |oidx, idefs| 305 | idefs.to_a.map { |idef| 306 | next if idef.flags[:TRAINING] 307 | df.world.items.other[oidx].find_all { |i| 308 | i.subtype.subtype == idef.subtype and 309 | i.mat_type == 0 and 310 | is_item_free(i) 311 | }.length 312 | }.compact 313 | }.flatten.min 314 | end 315 | 316 | # return the minimum count of free metal armor piece per subtype 317 | def count_stocks_armor 318 | ue = df.ui.main.fortress_entity.entity_raw.equipment 319 | [[:ARMOR, ue.armor_tg], 320 | [:SHIELD, ue.shield_tg], 321 | [:HELM, ue.helm_tg], 322 | [:PANTS, ue.pants_tg], 323 | [:GLOVES, ue.gloves_tg], 324 | [:SHOES, ue.shoes_tg]].map { |oidx, idefs| 325 | div = 1 326 | div = 2 if oidx == :GLOVES or oidx == :SHOES 327 | idefs.to_a.map { |idef| 328 | next unless idef.kind_of?(DFHack::ItemdefShieldst) or idef.props.flags[:METAL] 329 | df.world.items.other[oidx].find_all { |i| 330 | i.subtype.subtype == idef.subtype and 331 | i.mat_type == 0 and 332 | is_item_free(i) 333 | }.length / div 334 | }.compact 335 | }.flatten.min 336 | end 337 | 338 | def count_stocks_clothes 339 | ue = df.ui.main.fortress_entity.entity_raw.equipment 340 | [[:ARMOR, ue.armor_tg], 341 | [:HELM, ue.helm_tg], 342 | [:PANTS, ue.pants_tg], 343 | [:GLOVES, ue.gloves_tg], 344 | [:SHOES, ue.shoes_tg]].map { |oidx, idefs| 345 | div = 1 346 | div = 2 if oidx == :GLOVES or oidx == :SHOES 347 | idefs.to_a.map { |idef| 348 | next unless idef.props.flags[:SOFT] # XXX 349 | df.world.items.other[oidx].find_all { |i| 350 | i.subtype.subtype == idef.subtype and 351 | i.mat_type != 0 and # XXX 352 | i.wear == 0 and 353 | is_item_free(i) 354 | }.length / div 355 | }.compact 356 | }.flatten.min 357 | end 358 | 359 | 360 | # make it so the stocks of 'what' rises by 'amount' 361 | def queue_need(what, amount) 362 | case what 363 | when :weapon 364 | return queue_need_weapon 365 | when :armor 366 | return queue_need_armor 367 | when :clothes 368 | return queue_need_clothes 369 | when :coffin_bld 370 | return queue_need_coffin_bld(amount) 371 | when :coffin_bld_pet 372 | if count[:coffin_bld] >= Needed[:coffin_bld] and 373 | cof = df.world.buildings.other[:COFFIN].find { |bld| !bld.owner and bld.burial_mode.no_pets } 374 | cof.burial_mode.no_pets = false 375 | end 376 | return 377 | when :raw_coke 378 | if @ai.plan.past_initial_phase and raw = @ai.plan.map_veins.keys.find { |k| is_raw_coke(k) } 379 | @ai.plan.dig_vein(raw) 380 | end 381 | return 382 | when :gypsum 383 | if @ai.plan.past_initial_phase and raw = @ai.plan.map_veins.keys.find { |k| is_gypsum(k) } 384 | @ai.plan.dig_vein(raw) 385 | end 386 | return 387 | when :food 388 | # XXX fish/hunt/cook ? 389 | @last_warn_food ||= Time.now-610 # warn every 10mn 390 | if @last_warn_food < Time.now-600 391 | puts "AI: need #{amount} more food" 392 | @last_warn_food = Time.now 393 | end 394 | return 395 | 396 | when :pigtail_seeds, :dimplecup_seeds 397 | # only useful at game start, with low seeds stocks 398 | input = { 399 | :pigtail_seeds => [:pigtail], 400 | :dimplecup_seeds => [:dimplecup, :bag], 401 | }[what] 402 | 403 | order = { 404 | :pigtail_seeds => :ProcessPlants, 405 | :dimplecup_seeds => :MillPlants, 406 | }[what] 407 | 408 | when :dimple_dye 409 | order = :MillPlants 410 | input = [:dimplecup, :bag] 411 | 412 | when :wood 413 | # dont bother if the last designated tree is not cut yet 414 | return if @last_cutpos and @last_cutpos.offset(0, 0, 0).designation.dig == :Default 415 | @log_per_tree = 6 416 | 417 | amount *= 2 418 | tl = tree_list 419 | tl.each { |t| amount -= @log_per_tree if df.map_tile_at(t).designation.dig == :Default } 420 | @last_cutpos = cuttrees(amount/@log_per_tree, tl) if amount 421 | return 422 | 423 | when :drink 424 | if @count[:food] <= 0 425 | amount = 0 426 | elsif false 427 | mt = df.world.raws.mat_table 428 | mt.organic_types[:Plants].length.times { |i| 429 | plant = df.decode_mat(mt.organic_types[:Plants][i], mt.organic_indexes[:Plants][i]).plant 430 | t.plants[i]= plant.flags[:DRINK] if plant 431 | } 432 | # TODO count brewable 433 | end 434 | amount = (amount+4)/5 # accounts for brewer yield, but not for input stack size 435 | 436 | when :block 437 | amount = (amount+3)/4 438 | # no stone => make wooden blocks (needed for pumps for aquifer handling) 439 | if not df.world.items.other[:BOULDER].find { |i| 440 | is_item_free(i) and !df.ui.economic_stone[i.mat_index] 441 | # TODO check the boulders we find there are reachable.. 442 | } 443 | amount = 2 if amount > 2 444 | order = :ConstructWoodenBlocks 445 | end 446 | 447 | when :coal 448 | # dont use wood -> charcoal if we have bituminous coal 449 | # (except for bootstraping) 450 | amount = 2-@count[:coal] if amount > 2-@count[:coal] and @count[:raw_coke] > WatchStock[:raw_coke] 451 | 452 | when :ash 453 | input = [:wood] 454 | when :lye 455 | input = [:ash, :bucket] 456 | when :soap 457 | input = [:lye, :tallow] 458 | when :plasterpowder 459 | input = [:gypsum, :bag] 460 | end 461 | 462 | amount = 30 if amount > 30 463 | order ||= FurnitureOrder[what] 464 | if input 465 | i_amount = input.map { |i| count[i] || count_stocks(i) }.min 466 | amount = i_amount if amount > i_amount 467 | end 468 | if matcat = ManagerMatCategory[order] 469 | i_amount = (count[matcat] || count_stocks(matcat)) - count_manager_orders_matcat(matcat, order) 470 | amount = i_amount if amount > i_amount 471 | end 472 | find_manager_orders(order).each { |o| amount -= o.amount_left } 473 | return if amount <= 0 474 | 475 | @ai.debug "stocks: queue #{amount} #{order}" 476 | add_manager_order(order, amount) 477 | end 478 | 479 | # forge weapons 480 | def queue_need_weapon 481 | bars = Hash.new(0) 482 | coal_bars = @count[:coal] 483 | coal_bars = 50000 if !df.world.buildings.other[:FURNACE_SMELTER_MAGMA].empty? 484 | 485 | df.world.items.other[:BAR].each { |i| 486 | bars[i.mat_index] += i.stack_size if i.mat_type == 0 487 | } 488 | 489 | # rough account of already queued jobs consumption 490 | df.world.manager_orders.each { |mo| 491 | if mo.mat_type == 0 492 | bars[mo.mat_index] -= 4*mo.amount_total 493 | coal_bars -= mo.amount_total 494 | end 495 | } 496 | 497 | @metal_digger_pref ||= (0...df.world.raws.inorganics.length).find_all { |mi| 498 | df.world.raws.inorganics[mi].material.flags[:ITEMS_DIGGER] 499 | }.sort_by { |mi| # should roughly order metals by effectiveness 500 | - df.world.raws.inorganics[mi].material.strength.yield[:IMPACT] 501 | } 502 | 503 | @metal_weapon_pref ||= (0...df.world.raws.inorganics.length).find_all { |mi| 504 | df.world.raws.inorganics[mi].material.flags[:ITEMS_WEAPON] 505 | }.sort_by { |mi| 506 | - df.world.raws.inorganics[mi].material.strength.yield[:IMPACT] 507 | } 508 | 509 | may_forge_cache = CacheHash.new { |mi| may_forge_bars(mi) } 510 | 511 | ue = df.ui.main.fortress_entity.entity_raw.equipment 512 | [[ue.digger_tg, @metal_digger_pref], 513 | [ue.weapon_tg, @metal_weapon_pref]].each { |idefs, pref| 514 | idefs.each { |idef| 515 | next if idef.flags[:TRAINING] 516 | 517 | cnt = Needed[:weapon] 518 | cnt -= df.world.items.other[:WEAPON].find_all { |i| 519 | i.subtype.subtype == idef.subtype and is_item_free(i) 520 | }.length 521 | df.world.manager_orders.each { |mo| 522 | cnt -= mo.amount_total if mo.job_type == :MakeWeapon and mo.item_subtype == idef.subtype 523 | } 524 | next if cnt <= 0 525 | 526 | need_bars = idef.material_size / 3 # need this many bars to forge one idef item 527 | need_bars = 1 if need_bars < 1 528 | 529 | pref.each { |mi| 530 | break if bars[mi] < need_bars and may_forge_cache[mi] 531 | nw = bars[mi] / need_bars 532 | nw = coal_bars if nw > coal_bars 533 | nw = cnt if nw > cnt 534 | next if nw <= 0 535 | 536 | @ai.debug "stocks: queue #{nw} MakeWeapon #{df.world.raws.inorganics[mi].id} #{idef.id}" 537 | df.world.manager_orders << DFHack::ManagerOrder.cpp_new(:job_type => :MakeWeapon, :item_type => -1, 538 | :item_subtype => idef.subtype, :mat_type => 0, :mat_index => mi, :amount_left => nw, :amount_total => nw) 539 | bars[mi] -= nw * need_bars 540 | coal_bars -= nw 541 | cnt -= nw 542 | break if may_forge_cache[mi] # dont use lesser metal 543 | } 544 | } 545 | } 546 | end 547 | 548 | # forge armor pieces 549 | def queue_need_armor 550 | bars = Hash.new(0) 551 | coal_bars = @count[:coal] 552 | coal_bars = 50000 if !df.world.buildings.other[:FURNACE_SMELTER_MAGMA].empty? 553 | 554 | df.world.items.other[:BAR].each { |i| 555 | bars[i.mat_index] += i.stack_size if i.mat_type == 0 556 | } 557 | 558 | # rough account of already queued jobs consumption 559 | df.world.manager_orders.each { |mo| 560 | if mo.mat_type == 0 and bars.has_key?(mo.mat_index) 561 | bars[mo.mat_index] -= 4*mo.amount_total 562 | coal_bars -= mo.amount_total 563 | end 564 | } 565 | 566 | @metal_armor_pref ||= (0...df.world.raws.inorganics.length).find_all { |mi| 567 | df.world.raws.inorganics[mi].material.flags[:ITEMS_ARMOR] 568 | }.sort_by { |mi| 569 | - df.world.raws.inorganics[mi].material.strength.yield[:IMPACT] 570 | } 571 | 572 | may_forge_cache = CacheHash.new { |mi| may_forge_bars(mi) } 573 | 574 | ue = df.ui.main.fortress_entity.entity_raw.equipment 575 | [[:ARMOR, ue.armor_tg], 576 | [:SHIELD, ue.shield_tg], 577 | [:HELM, ue.helm_tg], 578 | [:PANTS, ue.pants_tg], 579 | [:GLOVES, ue.gloves_tg], 580 | [:SHOES, ue.shoes_tg]].each { |oidx, idefs| 581 | div = 1 582 | div = 2 if oidx == :GLOVES or oidx == :SHOES 583 | idefs.each { |idef| 584 | next unless idef.kind_of?(DFHack::ItemdefShieldst) or idef.props.flags[:METAL] 585 | 586 | job = DFHack::JobType::Item.index(oidx) # :GLOVES => :MakeGloves 587 | 588 | cnt = Needed[:armor] 589 | cnt -= df.world.items.other[oidx].find_all { |i| 590 | i.subtype.subtype == idef.subtype and i.mat_type == 0 and is_item_free(i) 591 | }.length / div 592 | 593 | df.world.manager_orders.each { |mo| 594 | cnt -= mo.amount_total if mo.job_type == job and mo.item_subtype == idef.subtype 595 | } 596 | next if cnt <= 0 597 | 598 | need_bars = idef.material_size / 3 # need this many bars to forge one idef item 599 | need_bars = 1 if need_bars < 1 600 | 601 | @metal_armor_pref.each { |mi| 602 | break if bars[mi] < need_bars and may_forge_cache[mi] 603 | nw = bars[mi] / need_bars 604 | nw = coal_bars if nw > coal_bars 605 | nw = cnt if nw > cnt 606 | next if nw <= 0 607 | 608 | @ai.debug "stocks: queue #{nw} #{job} #{df.world.raws.inorganics[mi].id} #{idef.id}" 609 | df.world.manager_orders << DFHack::ManagerOrder.cpp_new(:job_type => job, :item_type => -1, 610 | :item_subtype => idef.subtype, :mat_type => 0, :mat_index => mi, :amount_left => nw, :amount_total => nw) 611 | bars[mi] -= nw * need_bars 612 | coal_bars -= nw 613 | cnt -= nw 614 | break if may_forge_cache[mi] 615 | } 616 | } 617 | } 618 | end 619 | 620 | def queue_need_clothes 621 | # -10 to try to avoid cancel spam 622 | available_cloth = count_stocks(:cloth) - 20 623 | 624 | ue = df.ui.main.fortress_entity.entity_raw.equipment 625 | [[:ARMOR, ue.armor_tg], 626 | [:HELM, ue.helm_tg], 627 | [:PANTS, ue.pants_tg], 628 | [:GLOVES, ue.gloves_tg], 629 | [:SHOES, ue.shoes_tg]].map { |oidx, idefs| 630 | div = 1 631 | div = 2 if oidx == :GLOVES or oidx == :SHOES 632 | idefs.to_a.map { |idef| 633 | next unless idef.props.flags[:SOFT] # XXX 634 | 635 | job = DFHack::JobType::Item.index(oidx) 636 | 637 | cnt = Needed[:clothes] 638 | cnt -= df.world.items.other[oidx].find_all { |i| 639 | i.subtype.subtype == idef.subtype and 640 | i.mat_type != 0 and 641 | i.wear == 0 and 642 | is_item_free(i) 643 | }.length / div 644 | 645 | df.world.manager_orders.each { |mo| 646 | cnt -= mo.amount_total if mo.job_type == job and mo.item_subtype == idef.subtype 647 | # TODO subtract available_cloth too 648 | } 649 | cnt = available_cloth if cnt > available_cloth 650 | next if cnt <= 0 651 | 652 | @ai.debug "stocks: queue #{cnt} #{job} cloth #{idef.id}" 653 | df.world.manager_orders << DFHack::ManagerOrder.cpp_new(:job_type => job, :item_type => -1, 654 | :item_subtype => idef.subtype, :mat_type => -1, :mat_index => -1, 655 | :material_category => { :cloth => true }, 656 | :amount_left => cnt, :amount_total => cnt) 657 | 658 | available_cloth -= cnt 659 | } 660 | } 661 | end 662 | 663 | def queue_need_coffin_bld(amount) 664 | # dont dig too early 665 | return if not @ai.plan.past_initial_phase 666 | 667 | # count actually allocated (plan wise) coffin buildings 668 | return if @ai.plan.find_room(:cemetary) { |r| 669 | r.layout.each { |f| 670 | amount -= 1 if f[:item] == :coffin and not f[:bld_id] and not f[:ignore] 671 | } 672 | amount <= 0 673 | } 674 | 675 | amount.times { ai.plan.getcoffin } 676 | end 677 | 678 | 679 | # make it so the stocks of 'what' decrease by 'amount' 680 | def queue_use(what, amount) 681 | case what 682 | when :metal_ore 683 | queue_use_metal_ore(amount) 684 | return 685 | 686 | when :raw_coke 687 | queue_use_raw_coke(amount) 688 | return 689 | 690 | when :roughgem 691 | queue_use_gems(amount) 692 | return 693 | 694 | when :raw_adamantine 695 | order = :ExtractMetalStrands 696 | 697 | when :pigtail, :dimplecup, :quarrybush 698 | order = { 699 | :pigtail => :ProcessPlants, 700 | :dimplecup => :MillPlants, 701 | :quarrybush => :ProcessPlantsBag, 702 | }[what] 703 | # stuff may rot/be brewn before we can process it 704 | amount /= 2 if amount > 10 705 | amount /= 2 if amount > 4 706 | input = [:bag] if order == :MillPlants or order == :ProcessPlantsBag 707 | 708 | when :leaves 709 | order = :PrepareMeal 710 | amount = (amount + 4) / 5 711 | 712 | when :skull 713 | order = :MakeTotem 714 | 715 | when :bone 716 | nhunters = @ai.pop.labor_worker[:HUNT].length if @ai.pop.labor_worker 717 | return if not nhunters 718 | need_crossbow = nhunters + 1 - count_stocks(:crossbow) 719 | if need_crossbow > 0 720 | order = :MakeBoneCrossbow 721 | amount = need_crossbow if amount > need_crossbow 722 | else 723 | order = :MakeBoneBolt 724 | stock = count_stocks(:bonebolts) 725 | amount = 1000 - stock if amount > 1000 - stock 726 | amount /= 2 if amount > 10 727 | amount /= 2 if amount > 4 728 | end 729 | 730 | when :wool 731 | order = :SpinThread 732 | 733 | when :cloth_nodye 734 | order = :DyeCloth 735 | input = [:dimple_dye] 736 | amount /= 2 if amount > 10 737 | amount /= 2 if amount > 4 738 | 739 | when :honeycomb 740 | order = :PressHoneycomb 741 | end 742 | 743 | if input 744 | i_amount = input.map { |i| count[i] || count_stocks(i) }.min 745 | amount = i_amount if amount > i_amount 746 | end 747 | 748 | amount = 30 if amount > 30 749 | 750 | find_manager_orders(order).each { |o| amount -= o.amount_total } 751 | 752 | return if amount <= 0 753 | 754 | @ai.debug "stocks: queue #{amount} #{order}" 755 | add_manager_order(order, amount) 756 | end 757 | 758 | 759 | # cut gems 760 | def queue_use_gems(amount) 761 | return if df.world.manager_orders.find { |mo| mo.job_type == :CutGems } 762 | return if not i = df.world.items.other[:ROUGH].find { |_i| _i.mat_type == 0 and is_item_free(_i) } 763 | this_amount = df.world.items.other[:ROUGH].find_all { |_i| 764 | _i.mat_type == i.mat_type and _i.mat_index == i.mat_index and is_item_free(_i) 765 | }.length 766 | amount = this_amount if this_amount < amount 767 | amount = amount*3/4 if amount >= 10 768 | amount = 30 if amount > 30 769 | 770 | @ai.debug "stocks: queue #{amount} CutGems #{df.decode_mat(i)}" 771 | df.world.manager_orders << DFHack::ManagerOrder.cpp_new(:job_type => :CutGems, :item_type => -1, :item_subtype => -1, 772 | :mat_type => i.mat_type, :mat_index => i.mat_index, :amount_left => amount, :amount_total => amount) 773 | end 774 | 775 | 776 | # smelt metal ores 777 | def queue_use_metal_ore(amount) 778 | # make coke from bituminous coal has priority 779 | return if @count[:raw_coke] > WatchStock[:raw_coke] and @count[:coal] < 100 780 | 781 | return if df.world.manager_orders.find { |mo| mo.job_type == :SmeltOre } 782 | 783 | i = df.world.items.other[:BOULDER].find { |_i| is_metal_ore(_i) and is_item_free(_i) } 784 | this_amount = df.world.items.other[:BOULDER].find_all { |_i| 785 | _i.mat_type == i.mat_type and _i.mat_index == i.mat_index and is_item_free(_i) 786 | }.length 787 | amount = this_amount if this_amount < amount 788 | amount = amount*3/4 if amount >= 10 789 | amount = 30 if amount > 30 790 | 791 | if df.world.buildings.other[:FURNACE_SMELTER_MAGMA].empty? 792 | amount = @count[:coal] if amount > @count[:coal] 793 | return if amount <= 0 794 | end 795 | 796 | @ai.debug "stocks: queue #{amount} SmeltOre #{df.decode_mat(i)}" 797 | df.world.manager_orders << DFHack::ManagerOrder.cpp_new(:job_type => :SmeltOre, :item_type => -1, :item_subtype => -1, 798 | :mat_type => i.mat_type, :mat_index => i.mat_index, :amount_left => amount, :amount_total => amount) 799 | end 800 | 801 | 802 | # bituminous_coal -> coke 803 | def queue_use_raw_coke(amount) 804 | is_raw_coke(nil) # populate @raw_coke_cache 805 | inv = @raw_coke_cache.invert 806 | return if df.world.manager_orders.find { |mo| mo.job_type == :CustomReaction and inv[mo.reaction_name] } 807 | 808 | i = df.world.items.other[:BOULDER].find { |_i| is_raw_coke(_i) and is_item_free(_i) } 809 | return if not i or not reaction = is_raw_coke(i) 810 | 811 | this_amount = df.world.items.other[:BOULDER].find_all { |_i| 812 | _i.mat_index == i.mat_index and is_item_free(_i) 813 | }.length 814 | amount = this_amount if this_amount < amount 815 | amount = amount*3/4 if amount >= 10 816 | amount = 30 if amount > 30 817 | 818 | if df.world.buildings.other[:FURNACE_SMELTER_MAGMA].empty? 819 | # need at least 1 unit of fuel to bootstrap 820 | return if @count[:coal] <= 0 821 | end 822 | 823 | @ai.debug "stocks: queue #{amount} #{reaction}" 824 | df.world.manager_orders << DFHack::ManagerOrder.cpp_new(:job_type => :CustomReaction, :item_type => -1, :item_subtype => -1, 825 | :mat_type => -1, :mat_index => -1, :amount_left => amount, :amount_total => amount, :reaction_name => reaction) 826 | end 827 | 828 | 829 | 830 | # designate some trees for woodcutting 831 | def cuttrees(amount, list=tree_list) 832 | # return the bottom-rightest designated tree 833 | br = nil 834 | list.each { |tree| 835 | t = df.map_tile_at(tree) 836 | next if t.tilemat != :TREE 837 | next if t.designation.dig == :Default 838 | next if list.length > 4*amount and rand(4) != 0 839 | t.dig(:Default) 840 | br = t if not br or (br.x & -16) < (t.x & -16) or 841 | ((br.x & -16) == (t.x & -16) and (br.y & -16) < (t.y & -16)) 842 | amount -= 1 843 | break if amount <= 0 844 | } 845 | br 846 | end 847 | 848 | # return a list of trees on the map 849 | # lists only visible trees, sorted by distance from the fort entrance 850 | # expensive method, dont call often 851 | def tree_list 852 | fe = @ai.plan.fort_entrance 853 | # avoid re-scanning full map if there was no visible tree last time 854 | return [] if @last_treelist and @last_treelist.empty? and rand(6) > 0 855 | 856 | @last_treelist = (df.world.plants.tree_dry.to_a + df.world.plants.tree_wet.to_a).find_all { |p| 857 | t = df.map_tile_at(p) and 858 | t.tilemat == :TREE and 859 | not t.designation.hidden 860 | }.sort_by { |p| 861 | (p.pos.x-fe.x)**2 + (p.pos.y-fe.y)**2 + ((p.pos.z-fe.z2)*4)**2 862 | } 863 | end 864 | 865 | 866 | 867 | # check if an item is free to use 868 | def is_item_free(i, allow_nonempty=false) 869 | !i.flags.trader and # merchant's item 870 | !i.flags.in_job and # current job input 871 | !i.flags.construction and 872 | !i.flags.removed and # deleted object 873 | !i.flags.forbid and # user forbidden (or dumped) 874 | !i.flags.in_chest and # in infirmary (XXX dwarf owned items ?) 875 | (!i.flags.container or allow_nonempty or 876 | !i.general_refs.find { |ir| ir.kind_of?(DFHack::GeneralRefContainsItemst) }) and # is empty 877 | (!i.flags.in_inventory or 878 | (!i.general_refs.find { |ir| ir.kind_of?(DFHack::GeneralRefUnitHolderst) and # is not in an unit's inventory (ignore if it is simply hauled) 879 | ir.unit_tg.inventory.find { |ii| ii.item == i and ii.mode != :Hauled } } and 880 | !i.general_refs.find { |ir| ir.kind_of?(DFHack::GeneralRefContainedInItemst) and 881 | !is_item_free(ir.item_tg, true) })) and 882 | (!i.flags.in_building or !i.general_refs.find { |ir| ir.kind_of?(DFHack::GeneralRefBuildingHolderst) and # is not part of a building construction materials 883 | ir.building_tg.contained_items.find { |bi| bi.use_mode == 2 and bi.item == i } }) and 884 | (!i.flags.on_ground or !df.map_tile_at(i).designation.hidden) # i.flags.unk11? 885 | end 886 | 887 | 888 | def is_metal_ore(i) 889 | # mat_index => bool 890 | @metal_ore_cache ||= CacheHash.new { |mi| df.world.raws.inorganics[mi].flags[:METAL_ORE] } 891 | (i.kind_of?(Integer) and @metal_ore_cache[i]) or 892 | (i.kind_of?(DFHack::ItemBoulderst) and i.mat_type == 0 and @metal_ore_cache[i.mat_index]) 893 | end 894 | 895 | def is_raw_coke(i) 896 | # mat_index => custom reaction name 897 | @raw_coke_cache ||= df.world.raws.reactions.reactions.inject({}) { |h, r| 898 | if r.reagents.length == 1 and r.reagents.find { |rr| 899 | rr.kind_of?(DFHack::ReactionReagentItemst) and rr.item_type == :BOULDER and rr.mat_type == 0 900 | } and r.products.find { |rp| 901 | rp.kind_of?(DFHack::ReactionProductItemst) and rp.item_type == :BAR and 902 | mt = df.decode_mat(rp) and mt.material and mt.material.id == 'COAL' 903 | } 904 | # XXX check input size vs output size ? 905 | h.update r.reagents[0].mat_index => r.code 906 | else 907 | h 908 | end 909 | } 910 | (i.kind_of?(Integer) and @raw_coke_cache[i]) or 911 | (i.kind_of?(DFHack::ItemBoulderst) and i.mat_type == 0 and @raw_coke_cache[i.mat_index]) 912 | end 913 | 914 | def is_gypsum(i) 915 | # mat_index => bool 916 | @gypsum_cache ||= CacheHash.new { |mi| df.world.raws.inorganics[mi].material.reaction_class.include?('GYPSUM') } 917 | (i.kind_of?(Integer) and @gypsum_cache[i]) or 918 | (i.kind_of?(DFHack::ItemBoulderst) and i.mat_type == 0 and @gypsum_cache[i.mat_index]) 919 | end 920 | 921 | # determine if we may be able to generate metal bars for this metal 922 | # may queue manager_jobs to do so 923 | # recursive (eg steel need pig_iron) 924 | # return the potential number of bars available (in dimensions, eg 1 bar => 150) 925 | def may_forge_bars(mat_index, div=1) 926 | # simple metal ore 927 | moc = CacheHash.new { |mi| 928 | df.world.raws.inorganics[mi].metal_ore.mat_index.include?(mat_index) 929 | } 930 | 931 | can_melt = df.world.items.other[:BOULDER].find_all { |i| 932 | is_metal_ore(i) and moc[i.mat_index] and is_item_free(i) 933 | }.length 934 | 935 | @ai.plan.map_veins.keys.find { |k| 936 | can_melt += @ai.plan.dig_vein(k) if moc[k] 937 | } if can_melt < WatchStock[:metal_ore] and @ai.plan.past_initial_phase 938 | 939 | if can_melt > WatchStock[:metal_ore] 940 | return 4*150*(can_melt - WatchStock[:metal_ore]) 941 | end 942 | 943 | 944 | # "make bars" customreaction 945 | df.world.raws.reactions.reactions.each { |r| 946 | # XXX choose best reaction from all reactions 947 | prod_mult = nil 948 | next unless r.products.find { |rp| 949 | prod_mult = rp.product_dimension if rp.kind_of?(DFHack::ReactionProductItemst) and 950 | rp.item_type == :BAR and rp.mat_type == 0 and rp.mat_index == mat_index 951 | } 952 | 953 | can_reaction = 30 954 | future = false 955 | if r.reagents.all? { |rr| 956 | # XXX may queue forge reagents[1] even if we dont handle reagents[2] 957 | next if not rr.kind_of?(DFHack::ReactionReagentItemst) 958 | next if rr.item_type != :BAR and rr.item_type != :BOULDER 959 | has = 0 960 | df.world.items.other[rr.item_type].each { |i| 961 | next if rr.mat_type != -1 and i.mat_type != rr.mat_type 962 | next if rr.mat_index != -1 and i.mat_index != rr.mat_index 963 | next if not is_item_free(i) 964 | next if rr.reaction_class != '' and (!(mi = df.decode_mat(i)) or !mi.material or !mi.material.reaction_class.include?(rr.reaction_class)) 965 | next if rr.metal_ore != -1 and i.mat_type == 0 and !df.world.raws.inorganics[i.mat_index].metal_ore.mat_index.include?(rr.metal_ore) 966 | if rr.item_type == :BAR 967 | has += i.dimension 968 | else 969 | has += 1 970 | end 971 | } 972 | if has <= 0 and rr.item_type == :BOULDER and rr.mat_type == 0 and rr.mat_index != -1 and @ai.plan.past_initial_phase 973 | has += @ai.plan.dig_vein(rr.mat_index) 974 | future = true if has > 0 975 | end 976 | has /= rr.quantity 977 | 978 | if has <= 0 and rr.item_type == :BAR and rr.mat_type == 0 and rr.mat_index != -1 979 | future = true 980 | # 'div' tries to ensure that eg making pig iron wont consume all available iron 981 | # and leave some to make steel 982 | has = may_forge_bars(rr.mat_index, div+1) 983 | next if not has 984 | end 985 | 986 | can_reaction = has/div if can_reaction > has/div 987 | 988 | true 989 | } 990 | next if can_reaction <= 0 991 | 992 | if not future and not df.world.manager_orders.find { |mo| 993 | mo.job_type == :CustomReaction and mo.reaction_name == r.code 994 | } 995 | @ai.debug "stocks: queue #{can_reaction} #{r.code}" 996 | df.world.manager_orders << DFHack::ManagerOrder.cpp_new(:job_type => :CustomReaction, :item_type => -1, 997 | :item_subtype => -1, :reaction_name => r.code, :mat_type => -1, :mat_index => -1, 998 | :amount_left => can_reaction, :amount_total => can_reaction) 999 | end 1000 | return prod_mult * can_reaction 1001 | end 1002 | } 1003 | nil 1004 | end 1005 | 1006 | ManagerRealOrder = { 1007 | :BrewDrink => :CustomReaction, 1008 | :MakeSoap => :CustomReaction, 1009 | :MakePlasterPowder => :CustomReaction, 1010 | :PressHoneycomb => :CustomReaction, 1011 | :MakeBag => :ConstructChest, 1012 | :MakeRope => :MakeChain, 1013 | :MakeWoodenWheelbarrow => :MakeTool, 1014 | :MakeWoodenMinecart => :MakeTool, 1015 | :MakeRockNestbox => :MakeTool, 1016 | :MakeRockHive => :MakeTool, 1017 | :MakeRockJug => :MakeTool, 1018 | :MakeBoneBolt => :MakeAmmo, 1019 | :MakeBoneCrossbow => :MakeWeapon, 1020 | :MakeTrainingAxe => :MakeWeapon, 1021 | :MakeTrainingShortSword => :MakeWeapon, 1022 | :MakeTrainingSpear => :MakeWeapon, 1023 | :MakeGiantCorkscrew => :MakeTrapComponent, 1024 | :ConstructWoodenBlocks => :ConstructBlocks, 1025 | } 1026 | ManagerMatCategory = { 1027 | :MakeRope => :cloth, :MakeBag => :cloth, 1028 | :ConstructBed => :wood, :MakeBarrel => :wood, :MakeBucket => :wood, :ConstructBin => :wood, 1029 | :MakeWoodenWheelbarrow => :wood, :MakeWoodenMinecart => :wood, :MakeTrainingAxe => :wood, 1030 | :MakeTrainingShortSword => :wood, :MakeTrainingSpear => :wood, 1031 | :ConstructCrutch => :wood, :ConstructSplint => :wood, :MakeCage => :wood, 1032 | :MakeGiantCorkscrew => :wood, :MakePipeSection => :wood, :ConstructWoodenBlocks => :wood, 1033 | :MakeBoneBolt => :bone, :MakeBoneCrossbow => :bone, 1034 | :MakeQuiver => :leather, :MakeFlask => :leather, :MakeBackpack => :leather, 1035 | } 1036 | ManagerType = { # no MatCategory => mat_type = 0 (ie generic rock), unless specified here 1037 | :ProcessPlants => -1, :ProcessPlantsBag => -1, :MillPlants => -1, :BrewDrink => -1, 1038 | :ConstructTractionBench => -1, :MakeSoap => -1, :MakeLye => -1, :MakeAsh => -1, 1039 | :MakeTotem => -1, :MakeCharcoal => -1, :MakePlasterPowder => -1, :PrepareMeal => 4, 1040 | :DyeCloth => -1, :MilkCreature => -1, :PressHoneycomb => -1, 1041 | } 1042 | ManagerCustom = { 1043 | :BrewDrink => 'BREW_DRINK_FROM_PLANT', 1044 | :MakeSoap => 'MAKE_SOAP_FROM_TALLOW', 1045 | :MakePlasterPowder => 'MAKE_PLASTER_POWDER', 1046 | :PressHoneycomb => 'PRESS_HONEYCOMB', 1047 | } 1048 | ManagerSubtype = { 1049 | # depends on raws.itemdefs, wait until a world is loaded 1050 | } 1051 | 1052 | def self.init_manager_subtype 1053 | ManagerSubtype.update \ 1054 | :MakeWoodenWheelbarrow => df.world.raws.itemdefs.tools.find { |d| d.tool_use.include?(:HEAVY_OBJECT_HAULING) }.subtype, 1055 | :MakeWoodenMinecart => df.world.raws.itemdefs.tools.find { |d| d.tool_use.include?(:TRACK_CART) }.subtype, 1056 | :MakeRockNestbox => df.world.raws.itemdefs.tools.find { |d| d.tool_use.include?(:NEST_BOX) }.subtype, 1057 | :MakeRockHive => df.world.raws.itemdefs.tools.find { |d| d.tool_use.include?(:HIVE) }.subtype, 1058 | :MakeRockJug => df.world.raws.itemdefs.tools.find { |d| d.tool_use.include?(:LIQUID_CONTAINER) }.subtype, 1059 | :MakeTrainingAxe => df.world.raws.itemdefs.weapons.find { |d| d.id == 'ITEM_WEAPON_AXE_TRAINING' }.subtype, 1060 | :MakeTrainingShortSword => df.world.raws.itemdefs.weapons.find { |d| d.id == 'ITEM_WEAPON_SWORD_SHORT_TRAINING' }.subtype, 1061 | :MakeTrainingSpear => df.world.raws.itemdefs.weapons.find { |d| d.id == 'ITEM_WEAPON_SPEAR_TRAINING' }.subtype, 1062 | :MakeGiantCorkscrew => df.world.raws.itemdefs.trapcomps.find { |d| d.id == 'ITEM_TRAPCOMP_ENORMOUSCORKSCREW' }.subtype, 1063 | :MakeBoneBolt => df.world.raws.itemdefs.ammo.find { |d| d.id == 'ITEM_AMMO_BOLTS' }.subtype, 1064 | :MakeBoneCrossbow => df.world.raws.itemdefs.weapons.find { |d| d.id == 'ITEM_WEAPON_CROSSBOW' }.subtype 1065 | end 1066 | 1067 | df.onstatechange_register { |st| 1068 | init_manager_subtype if st == :WORLD_LOADED 1069 | } 1070 | init_manager_subtype if not df.world.raws.itemdefs.ammo.empty? 1071 | 1072 | def find_manager_orders(order) 1073 | _order = ManagerRealOrder[order] || order 1074 | matcat = ManagerMatCategory[order] 1075 | type = ManagerType[order] 1076 | subtype= ManagerSubtype[order] 1077 | custom = ManagerCustom[order] 1078 | 1079 | df.world.manager_orders.find_all { |_o| 1080 | _o.job_type == _order and 1081 | _o.mat_type == (type || (matcat ? -1 : 0)) and 1082 | (matcat ? _o.material_category.send(matcat) : _o.material_category._whole == 0) and 1083 | (not subtype or subtype == _o.item_subtype) and 1084 | (not custom or custom == _o.reaction_name) 1085 | } 1086 | end 1087 | 1088 | # return the number of current manager orders that share the same material (leather, cloth) 1089 | # ignore inorganics, ignore order 1090 | def count_manager_orders_matcat(matcat, order=nil) 1091 | cnt = 0 1092 | df.world.manager_orders.each { |_o| 1093 | cnt += _o.amount_total if _o.material_category.send(matcat) and _o.job_type != order 1094 | } 1095 | cnt 1096 | end 1097 | 1098 | def add_manager_order(order, amount=1, maxmerge=30) 1099 | @ai.debug "add_manager #{order} #{amount}" 1100 | _order = ManagerRealOrder[order] || order 1101 | matcat = ManagerMatCategory[order] 1102 | type = ManagerType[order] 1103 | subtype = ManagerSubtype[order] 1104 | custom = ManagerCustom[order] 1105 | 1106 | if not o = find_manager_orders(order).find { |_o| _o.amount_total+amount <= maxmerge } 1107 | # try to merge with last manager_order, upgrading maxmerge to 30 1108 | o = df.world.manager_orders.last 1109 | if o and o.job_type == _order and 1110 | o.amount_total + amount < 30 and 1111 | o.mat_type == (type || (matcat ? -1 : 0)) and 1112 | (matcat ? o.material_category.send(matcat) : o.material_category._whole == 0) and 1113 | (not subtype or subtype == o.item_subtype) and 1114 | (not custom or custom == o.reaction_name) and 1115 | o.amount_total + amount < 30 1116 | o.amount_total += amount 1117 | o.amount_left += amount 1118 | else 1119 | o = DFHack::ManagerOrder.cpp_new(:job_type => _order, :item_type => -1, :item_subtype => (subtype || -1), 1120 | :mat_type => (type || (matcat ? -1 : 0)), :mat_index => -1, :amount_left => amount, :amount_total => amount) 1121 | o.material_category.send("#{matcat}=", true) if matcat 1122 | o.reaction_name = custom if custom 1123 | o.mat_index = df.decode_mat('INORGANIC:RAW_ADAMANTINE').mat_index if _order == :ExtractMetalStrands 1124 | df.world.manager_orders << o 1125 | end 1126 | else 1127 | o.amount_total += amount 1128 | o.amount_left += amount 1129 | end 1130 | end 1131 | 1132 | FurnitureOrder = Hash.new { |h, k| 1133 | h[k] = "Construct#{k.to_s.capitalize}".to_sym 1134 | }.update :chair => :ConstructThrone, 1135 | :traction_bench => :ConstructTractionBench, 1136 | :weaponrack => :ConstructWeaponRack, 1137 | :armorstand => :ConstructArmorStand, 1138 | :bucket => :MakeBucket, 1139 | :barrel => :MakeBarrel, 1140 | :bin => :ConstructBin, 1141 | :drink => :BrewDrink, 1142 | :crutch => :ConstructCrutch, 1143 | :splint => :ConstructSplint, 1144 | :bag => :MakeBag, 1145 | :block => :ConstructBlocks, 1146 | :mechanism => :ConstructMechanisms, 1147 | :trap => :ConstructMechanisms, 1148 | :cage => :MakeCage, 1149 | :soap => :MakeSoap, 1150 | :rope => :MakeRope, 1151 | :lye => :MakeLye, 1152 | :ash => :MakeAsh, 1153 | :plasterpowder => :MakePlasterPowder, 1154 | :wheelbarrow => :MakeWoodenWheelbarrow, 1155 | :minecart => :MakeWoodenMinecart, 1156 | :nestbox => :MakeRockNestbox, 1157 | :hive => :MakeRockHive, 1158 | :jug => :MakeRockJug, 1159 | :quiver => :MakeQuiver, 1160 | :flask => :MakeFlask, 1161 | :backpack => :MakeBackpack, 1162 | :giant_corkscrew => :MakeGiantCorkscrew, 1163 | :pipe_section => :MakePipeSection, 1164 | :coal => :MakeCharcoal 1165 | 1166 | FurnitureFind = Hash.new { |h, k| 1167 | sym = "item_#{k}st".to_sym 1168 | h[k] = lambda { |o| o._rtti_classname == sym } 1169 | }.update :chest => lambda { |o| o._rtti_classname == :item_boxst and o.mat_type == 0 }, 1170 | :hive => lambda { |o| o._rtti_classname == :item_toolst and o.subtype.subtype == ManagerSubtype[FurnitureOrder[:hive]] }, 1171 | :nestbox => lambda { |o| o._rtti_classname == :item_toolst and o.subtype.subtype == ManagerSubtype[FurnitureOrder[:nestbox]] }, 1172 | :trap => lambda { |o| o._rtti_classname == :item_trappartsst } 1173 | 1174 | # find one item of this type (:bed, etc) 1175 | def find_furniture_item(itm) 1176 | find = FurnitureFind[itm] 1177 | oidx = DFHack::JobType::Item.fetch(FurnitureOrder[itm], 1178 | DFHack::JobType::Item.fetch(ManagerRealOrder[FurnitureOrder[itm]], :IN_PLAY)) 1179 | df.world.items.other[oidx].find { |i| find[i] and df.item_isfree(i) } 1180 | end 1181 | 1182 | # return nr of free items of this type 1183 | def find_furniture_itemcount(itm) 1184 | find = FurnitureFind[itm] 1185 | oidx = DFHack::JobType::Item.fetch(FurnitureOrder[itm], 1186 | DFHack::JobType::Item.fetch(ManagerRealOrder[FurnitureOrder[itm]], :IN_PLAY)) 1187 | df.world.items.other[oidx].find_all { |i| find[i] and df.item_isfree(i) }.length 1188 | rescue 1189 | puts_err "df-ai stocks: cannot itemcount #{itm.inspect}", $!, $!.backtrace 1190 | 0 1191 | end 1192 | end 1193 | end 1194 | --------------------------------------------------------------------------------