Package horizons :: Package world :: Package units :: Package collectors :: Module buildingcollector
[hide private]
[frames] | no frames]

Source Code for Module horizons.world.units.collectors.buildingcollector

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  import weakref 
 23  from collections import deque 
 24   
 25  from horizons.component.collectingcomponent import CollectingComponent 
 26  from horizons.component.storagecomponent import StorageComponent 
 27  from horizons.constants import BUILDINGS, COLLECTORS 
 28  from horizons.scheduler import Scheduler 
 29  from horizons.util.pathfinding.pather import BuildingCollectorPather, RoadPather 
 30  from horizons.util.python.callback import Callback 
 31  from horizons.util.shapes import RadiusRect 
 32  from horizons.util.worldobject import WorldObject 
 33  from horizons.world.units.collectors.collector import Collector, Job, JobList 
 34  from horizons.world.units.unitexeptions import MoveNotPossible 
 35   
 36   
37 -class BuildingCollector(Collector):
38 """Collector, that works for a building and gets its needed resources. 39 Essentially extends the Collector by a home building. 40 41 Nearly all of the time, the collector has to have a home_building. 42 Only fisher ships violate this rule in case their home building gets demolished. 43 Therefore, this class is not functional with home_building == None, 44 but basic facilities (esp. save/load) have to work. 45 """ 46 job_ordering = JobList.order_by.fewest_available_and_distance # type: ignore 47 pather_class = BuildingCollectorPather 48
49 - def __init__(self, home_building, **kwargs):
50 kwargs['x'] = home_building.position.origin.x 51 kwargs['y'] = home_building.position.origin.y 52 super().__init__(**kwargs) 53 self._job_history = deque() 54 self._creation_tick = Scheduler().cur_tick + 1 # adjusted for the initial delay 55 self.__init(home_building)
56
57 - def __init(self, home_building):
58 self.home_building = home_building 59 if home_building is not None: 60 self.register_at_home_building() 61 # save whether it's possible for this instance to access a target 62 # @chachedmethod is not applicable since it stores hard refs in the arguments 63 self._target_possible_cache = weakref.WeakKeyDictionary()
64
65 - def save(self, db):
66 super().save(db) 67 self._clean_job_history_log() 68 current_tick = Scheduler().cur_tick 69 70 # save home_building and creation tick 71 # pre-translate the tick number for the loading process 72 translated_creation_tick = self._creation_tick - current_tick + 1 73 db("INSERT INTO building_collector(rowid, home_building, creation_tick) VALUES(?, ?, ?)", 74 self.worldid, self.home_building.worldid if self.home_building is not None else None, 75 translated_creation_tick) 76 77 # save job history 78 for tick, utilization in self._job_history: 79 # pre-translate the tick number for the loading process 80 translated_tick = tick - current_tick + Scheduler.FIRST_TICK_ID 81 db("INSERT INTO building_collector_job_history(collector, tick, utilisation) VALUES(?, ?, ?)", 82 self.worldid, translated_tick, utilization)
83
84 - def load(self, db, worldid):
85 # we have to call __init here before super().load, because a superclass uses a method, 86 # which is overwritten here, that uses a member, which has to be initialized via __init. 87 88 # load home_building 89 home_building_id, self._creation_tick = db.get_building_collectors_data(worldid) 90 self.__init(None if home_building_id is None else WorldObject.get_object_by_id(home_building_id)) 91 92 super().load(db, worldid) 93 94 if home_building_id is None: 95 self.show() # make sure that homebuildingsless units are visible on startup 96 # TODO: fix "homebuildingless buildingcollectors". 97 # perhaps a new unit should be created, because a fisher ship without a 98 # fisher basically isn't a buildingcollector anymore. 99 100 # load job search failures 101 # the tick values were translated to assume that it is currently tick -1 102 assert Scheduler().cur_tick == Scheduler.FIRST_TICK_ID - 1 103 self._job_history = db.get_building_collector_job_history(worldid)
104
105 - def register_at_home_building(self, unregister=False):
106 """Creates reference for self at home building (only hard reference except for 107 in job.object) 108 @param unregister: whether to reverse registration 109 """ 110 # TODO: figure out why the home_building can be None when this is run in session.end() 111 if self.home_building is not None: 112 if unregister: 113 self.home_building.get_component(CollectingComponent).remove_local_collector(self) 114 else: 115 self.home_building.get_component(CollectingComponent).add_local_collector(self)
116
117 - def apply_state(self, state, remaining_ticks=None):
118 super().apply_state(state, remaining_ticks) 119 if state == self.states.moving_home: 120 # collector is on its way home 121 self.add_move_callback(self.reached_home) 122 self.add_blocked_callback(self.handle_path_home_blocked) 123 self.show()
124
125 - def remove(self):
126 self.register_at_home_building(unregister=True) 127 self.home_building = None 128 super().remove()
129
131 """Makes collector survive deletion of home building.""" 132 self.cancel(continue_action=lambda: 42) # don't continue 133 self.stop() 134 self.register_at_home_building(unregister=True) 135 self.home_building = None 136 self.state = self.states.decommissioned 137 self.show() # make sure collector is not pretending to be inside somewhere
138
139 - def get_home_inventory(self):
140 return self.home_building.get_component(StorageComponent).inventory
141
142 - def get_colleague_collectors(self):
143 colls = self.home_building.get_component(CollectingComponent).get_local_collectors() 144 return (coll for coll in colls if coll is not self)
145
146 - def get_job(self):
147 """Returns the next job or None""" 148 if self.home_building is None: 149 return None 150 151 collectable_res = self.get_collectable_res() 152 if not collectable_res: 153 return None 154 155 jobs = JobList(self, self.job_ordering) 156 # iterate all building that provide one of the resources 157 for building in self.get_buildings_in_range(reslist=collectable_res): 158 # check if we can pickup here on principle 159 target_possible = self._target_possible_cache.get(building, None) 160 if target_possible is None: # not in cache, we have to check 161 target_possible = self.check_possible_job_target(building) 162 self._target_possible_cache[building] = target_possible 163 164 if target_possible: 165 # check for res here 166 reslist = (self.check_possible_job_target_for( 167 building, res) for res in collectable_res) 168 reslist = [i for i in reslist if i] 169 170 if reslist: # we can do something here 171 jobs.append(Job(building, reslist)) 172 173 # TODO: find out why order of self.get_buildings_in_range(..) and therefore order of jobs differs from client to client 174 # TODO: find out why WildAnimal.get_job(..) doesn't have this problem 175 # for MP-Games the jobs must have the same ordering to ensure get_best_possible_job(..) returns the same result 176 jobs.sort(key=lambda job: job.object.worldid) 177 178 return self.get_best_possible_job(jobs)
179
180 - def search_job(self):
181 self._clean_job_history_log() 182 super().search_job()
183
184 - def handle_no_possible_job(self):
185 super().handle_no_possible_job() 186 # only append a new element if it is different from the last one 187 if not self._job_history or abs(self._job_history[-1][1]) > 1e-9: 188 self._job_history.append((Scheduler().cur_tick, 0))
189
190 - def begin_current_job(self, job_location=None):
191 super().begin_current_job(job_location) 192 # Sum up the utilization for all res 193 utilization = 0.0 194 for entry in self.job.reslist: 195 max_amount = min(self.get_component(StorageComponent).inventory.get_limit(entry.res), 196 self.job.object.get_component(StorageComponent).inventory.get_limit(entry.res)) 197 utilization += entry.amount / float(max_amount) 198 199 # Divide by number of resources being transferred 200 utilization = utilization / len(self.job.reslist) 201 202 # Set job history 203 if not self._job_history or abs(self._job_history[-1][1] - utilization) > 1e-9: 204 self._job_history.append((Scheduler().cur_tick, utilization))
205
206 - def finish_working(self, collector_already_home=False):
207 """Called when collector has stayed at the target for a while. 208 Picks up the resources and sends collector home. 209 @param collector_already_home: whether collector has moved home before.""" 210 if not collector_already_home: 211 self.move_home(callback=self.reached_home) 212 super().finish_working()
213 214 # unused reroute code removed in 2aef7bba77536da333360566467d9a2f08d38cab 215
216 - def reached_home(self):
217 """Exchanges resources with home and calls end_job""" 218 self.log.debug("%s reached home", self) 219 for entry in self.job.reslist: 220 self.transfer_res_to_home(entry.res, entry.amount) 221 self.end_job()
222
223 - def get_collectable_res(self):
224 """Return all resources the collector can collect (depends on its home building)""" 225 # find needed res (only res that we have free room for) - Building function 226 return self.home_building.get_needed_resources()
227
228 - def get_buildings_in_range(self, reslist=None):
229 """Returns all buildings in range . 230 Overwrite in subclasses that need ranges around the pickup. 231 @param res: optional, only search for buildings that provide res""" 232 reach = RadiusRect(self.home_building.position, self.home_building.radius) 233 return self.home_building.island.get_providers_in_range(reach, reslist=reslist, 234 player=self.owner)
235
236 - def handle_path_home_blocked(self):
237 """Called when we get blocked while trying to move to the job location. """ 238 self.log.debug("%s: got blocked while moving home, teleporting home", self) 239 # make sure to get home, this prevents all movement problems by design 240 # at the expense of some jumping in very unusual corner cases 241 # NOTE: if this is seen as problem, self.resume_movement() could be tried before reverting to teleportation 242 self.teleport(self.home_building, callback=self.move_callbacks, destination_in_building=True)
243
244 - def move_home(self, callback=None, action='move_full'):
245 """Moves collector back to its home building""" 246 self.log.debug("%s move_home", self) 247 if self.home_building.position.contains(self.position): 248 # already home 249 self.stop() # make sure unit doesn't go anywhere in case a movement is going on 250 Scheduler().add_new_object(callback, self, run_in=0) 251 else: 252 # actually move home 253 try: 254 # reuse reversed path of path here (assumes all jobs started at home) 255 path = None if (self.job is None or self.job.path is None) else list(reversed(self.job.path)) 256 self.move(self.home_building, callback=callback, destination_in_building=True, 257 action=action, blocked_callback=self.handle_path_home_blocked, path=path) 258 self.state = self.states.moving_home 259 except MoveNotPossible: 260 # we are in trouble. 261 # the collector went somewhere, now there is no way for them to move home. 262 # this is an unsolved problem also in reality, so we are forced to use an unconventional solution. 263 self.teleport(self.home_building, callback=callback, destination_in_building=True)
264
265 - def cancel(self, continue_action=None):
266 """Cancels current job and moves back home""" 267 self.log.debug("%s cancel", self) 268 if continue_action is None: 269 continue_action = Callback(self.move_home, callback=self.end_job, action='move') 270 super().cancel(continue_action=continue_action)
271
273 return min(COLLECTORS.STATISTICAL_WINDOW, Scheduler().cur_tick - self._creation_tick)
274
275 - def get_utilization(self):
276 """ 277 Returns the utilization of the collector. 278 It is calculated by observing how full the inventory of the collector is or 279 how full it would be if it had reached the place where it picks up the resources. 280 """ 281 282 history_length = self.get_utilization_history_length() 283 if history_length <= 0: 284 return 0 285 286 current_tick = Scheduler().cur_tick 287 first_relevant_tick = current_tick - history_length 288 289 self._clean_job_history_log() 290 num_entries = len(self._job_history) 291 total_utilization = 0 292 for i in range(num_entries): 293 tick = self._job_history[i][0] 294 if tick >= current_tick: 295 break 296 297 next_tick = min(self._job_history[i + 1][0], current_tick) if i + 1 < num_entries else current_tick 298 relevant_ticks = next_tick - tick 299 if tick < first_relevant_tick: 300 # the beginning is not relevant 301 relevant_ticks -= first_relevant_tick - tick 302 total_utilization += relevant_ticks * self._job_history[i][1] 303 304 #assert -1e-7 < total_utilization / float(history_length) < 1 + 1e-7 305 306 return total_utilization / float(history_length)
307
308 - def _clean_job_history_log(self):
309 """ remove too old entries """ 310 first_relevant_tick = Scheduler().cur_tick - self.get_utilization_history_length() 311 while len(self._job_history) > 1 and self._job_history[1][0] < first_relevant_tick: 312 self._job_history.popleft()
313
314 - def level_upgrade(self, lvl):
315 """Upgrades collector to another tier""" 316 action_set = self.__class__.get_random_action_set(lvl, exact_level=True) 317 if action_set: 318 self._action_set_id = action_set 319 self.act(self._action, repeating=True)
320 321
322 -class StorageCollector(BuildingCollector):
323 """ Same as BuildingCollector, except that it moves on roads. 324 Used in storage facilities. 325 """ 326 pather_class = RoadPather 327 destination_always_in_building = True 328 job_ordering = JobList.order_by.for_storage_collector # type: ignore
329 330
331 -class FieldCollector(BuildingCollector):
332 """ Similar to the BuildingCollector but used on farms for example. 333 The main difference is that it uses a different way to sort it's jobs, to make for a nicer 334 look of farm using.""" 335 job_ordering = JobList.order_by.random # type: ignore
336 337
338 -class SettlerCollector(StorageCollector):
339 """Collector for settlers.""" 340 pass
341 342
343 -class FisherShipCollector(BuildingCollector):
344
345 - def __init__(self, *args, **kwargs):
346 if not args: 347 # We haven't preset a home_building, so search for one! 348 home_building = self.get_smallest_fisher(kwargs['session'], kwargs['owner']) 349 super().__init__(home_building=home_building, *args, **kwargs) 350 else: 351 super().__init__(*args, **kwargs)
352
353 - def get_smallest_fisher(self, session, owner):
354 """Returns the fisher with the least amount of boats""" 355 fishers = [] 356 for settlement in session.world.settlements: 357 if settlement.owner == owner: 358 fishers.extend(settlement.buildings_by_id[BUILDINGS.FISHER]) 359 smallest_fisher = fishers.pop() 360 for fisher in fishers: 361 if len(smallest_fisher.get_local_collectors()) > len(fisher.get_local_collectors()): 362 smallest_fisher = fisher 363 364 return smallest_fisher
365
366 - def get_buildings_in_range(self, reslist=None):
367 """Returns all buildings in range . 368 Overwrite in subclasses that need ranges around the pickup. 369 @param res: optional, only search for buildings that provide res""" 370 reach = RadiusRect(self.home_building.position, self.home_building.radius) 371 return self.session.world.get_providers_in_range(reach, reslist=reslist)
372 373
374 -class DisasterRecoveryCollector(StorageCollector):
375 """Collects disasters such as fire or pestilence."""
376 - def finish_working(self, collector_already_home=False):
377 super().finish_working(collector_already_home=collector_already_home) 378 building = self.job.object 379 if hasattr(building, "disaster"): # make sure that building hasn't recovered any other way 380 building.disaster.recover(building)
381
382 - def get_job(self):
383 if self.home_building is not None and \ 384 not self.session.world.disaster_manager.is_affected(self.home_building.settlement): 385 return None # not one disaster active, bail out 386 387 return super().get_job()
388