Package horizons :: Package world :: Package production :: Module producer
[hide private]
[frames] | no frames]

Source Code for Module horizons.world.production.producer

  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 logging 
 23   
 24  from horizons.command.unit import CreateUnit 
 25  from horizons.component import Component 
 26  from horizons.component.ambientsoundcomponent import AmbientSoundComponent 
 27  from horizons.component.namedcomponent import NamedComponent 
 28  from horizons.component.storagecomponent import StorageComponent 
 29  from horizons.constants import PRODUCTION 
 30  from horizons.messaging import AddStatusIcon, MineEmpty, RemoveStatusIcon 
 31  from horizons.scheduler import Scheduler 
 32  from horizons.util.changelistener import metaChangeListenerDecorator 
 33  from horizons.util.python.callback import Callback 
 34  from horizons.util.shapes import Circle, Point 
 35  from horizons.world.production.production import Production, SingleUseProduction 
 36  from horizons.world.production.productionline import ProductionLine 
 37  from horizons.world.production.unitproduction import UnitProduction 
 38  from horizons.world.production.utilization import FieldUtilization, FullUtilization, Utilization 
 39  from horizons.world.status import DecommissionedStatus, InventoryFullStatus, ProductivityLowStatus 
40 41 42 @metaChangeListenerDecorator("production_finished") 43 @metaChangeListenerDecorator("activity_changed") 44 -class Producer(Component):
45 """Class for objects, that produce something. 46 @param auto_init: bool. If True, the producer automatically adds one 47 production for each production_line. 48 """ 49 log = logging.getLogger("world.production") 50 51 NAME = "producer" 52 DEPENDENCIES = [StorageComponent] 53 54 utilization_mapping = { 55 'FieldUtilization': FieldUtilization, 56 'FullUtilization': FullUtilization 57 } 58 59 produces_resource = True 60 production_class = Production 61 62 # INIT
63 - def __init__(self, auto_init=True, start_finished=False, productionlines=None, 64 utilization_calculator=None, is_mine_for=None, settler_upgrade_lines=None, 65 **kwargs):
66 """ 67 @param productionline: yaml-dict for prod line data. Must not be changed since it is cached. 68 @param utilization_calculator: one of utilization_mapping 69 @param settler_upgrade_lines: data for settler upgrades. can one day be generalized to other upgrades 70 """ 71 if productionlines is None: 72 productionlines = {} 73 super().__init__(**kwargs) 74 self.__auto_init = auto_init 75 self.__start_finished = start_finished 76 self.production_lines = productionlines 77 assert utilization_calculator is not None 78 self.__utilization = utilization_calculator 79 80 if settler_upgrade_lines: 81 from horizons.world.building.settler import SettlerUpgradeData 82 self.settler_upgrade_lines = SettlerUpgradeData(self, settler_upgrade_lines) 83 84 self.production_lines = self.production_lines.copy() 85 self.production_lines.update(self.settler_upgrade_lines.get_production_lines()) 86 else: 87 self.settler_upgrade_lines = None
88
89 - def __init(self):
90 # we store productions in 2 dicts, one for the active ones, and one for the inactive ones. 91 # the inactive ones won't get considered for needed_resources and such. 92 # the production_line id is the key in the dict (=> a building must not have two identical 93 # production lines) 94 self._productions = {} 95 self._inactive_productions = {} 96 # Store whether or not the producer is active 97 self.__active = True 98 # Store whether or not the utilization level is currently ok 99 self.__utilization_ok = True 100 # Track if the producer is being removed 101 self.__removal_started = False 102 103 # BIG FAT NOTE: this has to be executed for all players for mp 104 # even if this building has no status icons 105 # TODO: think about whether this is enough gui-related so it belongs to the ExtScheduler 106 # also check its performance when moving 107 interval = Scheduler().get_ticks(3) 108 run_in = self.session.random.randint(1, interval) # don't update all at once 109 if self.instance.has_status_icon: 110 Scheduler().add_new_object(self.update_capacity_utilization, self, run_in=run_in, 111 loops=-1, loop_interval=interval)
112
113 - def initialize(self):
114 self.__init() 115 # add production lines as specified in db. 116 if self.__auto_init: 117 for prod_line, attributes in self.production_lines.items(): 118 if 'enabled_by_default' in attributes and not attributes['enabled_by_default']: 119 continue # It's set to False, don't add 120 prod = self.create_production(prod_line) 121 self.add_production(prod) 122 # For newly built producers we set the utilization to full for the first 123 # few seconds, this avoids the low productivity icon being shown every 124 # time a new producer is built 125 temp_util = self.__utilization 126 self.__utilization = FullUtilization() 127 Scheduler().add_new_object(Callback(self.__set_utilization, temp_util), self, Scheduler().get_ticks(15))
128
129 - def get_production_lines_by_level(self, level):
130 prod_lines = [] 131 for key, data in self.production_lines.items(): 132 if 'level' in data and level in data['level']: 133 prod_lines.append(key) 134 return prod_lines
135
136 - def create_production(self, id, load=False):
137 """ 138 @param id: production line id 139 @param load: whether the production is used for loading. 140 """ 141 data = self.production_lines[id] 142 production_class = self.production_class 143 owner_inventory = self.instance._get_owner_inventory() 144 145 # not really fancy way of selecting special production class 146 if self.settler_upgrade_lines: 147 if id == self.settler_upgrade_lines.get_production_line_id(self.instance.level + 1): 148 production_class = SingleUseProduction 149 150 return production_class(inventory=self.instance.get_component(StorageComponent).inventory, 151 owner_inventory=owner_inventory, prod_id=id, prod_data=data, 152 load=load, start_finished=self.__start_finished)
153
154 - def create_production_line(self, id):
155 """Creates a production line instance, this is meant only for data transfer and READONLY use! 156 If you want to use production lines for anything else, go the proper way of the production class.""" 157 assert id in self.production_lines 158 data = self.production_lines[id] 159 return ProductionLine(id, data)
160
161 - def add_production_by_id(self, production_line_id):
162 """Convenience method. 163 @param production_line_id: Production line from db 164 """ 165 production = self.create_production(production_line_id) 166 self.add_production(production) 167 return production
168
170 """Called by the scheduler to update the utilization regularly""" 171 if not self.capacity_utilization_below(ProductivityLowStatus.threshold) is not self.__utilization_ok: 172 self.__utilization_ok = not self.__utilization_ok 173 if self.__utilization_ok: 174 RemoveStatusIcon.broadcast(self, self.instance, ProductivityLowStatus) 175 else: 176 self._add_status_icon(ProductivityLowStatus(self.instance))
177 178 @property
179 - def capacity_utilization(self):
180 return self.__utilization.capacity_utilization(self)
181
182 - def capacity_utilization_below(self, limit):
183 return self.__utilization.capacity_utilization_below(limit, self)
184
185 - def load(self, db, worldid):
186 # Call this before super, because we have to make sure this is called before the 187 # ConcreteObject's callback which is added during loading 188 Scheduler().add_new_object(self._on_production_change, self, run_in=0) 189 super().load(db, worldid) 190 # load all productions 191 self.__init() 192 for line_id in db.get_production_lines_by_owner(worldid): 193 production = self.create_production(line_id, load=True) 194 assert isinstance(production, Production) 195 production.load(db, worldid) 196 self.add_production(production) 197 198 self._update_decommissioned_icon()
199
200 - def save(self, db):
201 super().save(db) 202 for production in self.get_productions(): 203 production.save(db, self.instance.worldid)
204 205 # INTERFACE
206 - def add_production(self, production):
207 assert isinstance(production, Production) 208 self.log.debug('%s: added production line %s', self, production.get_production_line_id()) 209 if production.is_paused(): 210 self.log.debug('%s: added production line %s is paused', self, production.get_production_line_id()) 211 self._inactive_productions[production.get_production_line_id()] = production 212 else: 213 self.log.debug('%s: added production line %s is active', self, production.get_production_line_id()) 214 self._productions[production.get_production_line_id()] = production 215 production.add_production_finished_listener(self._production_finished) 216 # This would be called multiple times during init, just add it later this tick. 217 # It also ensures that the changelistener would stick, we used to readd 218 # the listener in load(), which was explained by this comment: 219 # Listener has been removed in the productions.load(), because the 220 # changelistener's load is called 221 Scheduler().add_new_object( 222 Callback(production.add_change_listener, self._on_production_change, call_listener_now=True), self, run_in=0 223 ) 224 self.instance._changed()
225
226 - def _production_finished(self, production):
227 """Gets called when a production finishes. Intercepts call, adds info 228 and forwards it""" 229 produced_resources = production.get_produced_resources() 230 self.on_production_finished(produced_resources)
231
232 - def finish_production_now(self):
233 """Cheat, makes current production finish right now (and produce the resources). 234 Useful to make trees fully grown at game start.""" 235 for production in self._productions.values(): 236 production.finish_production_now()
237
238 - def has_production_line(self, prod_line_id):
239 """Checks if this instance has a production with a certain production line id""" 240 return bool(self._get_production(prod_line_id))
241
242 - def remove_production(self, production):
243 """Removes a production instance. 244 @param production: Production instance""" 245 production.remove() # production "destructor" 246 if self.is_active(production): 247 del self._productions[production.get_production_line_id()] 248 # update decommissioned icon after removing production 249 self._update_decommissioned_icon() 250 251 self.instance._changed() 252 self.on_activity_changed(self.is_active()) 253 254 else: 255 del self._inactive_productions[production.get_production_line_id()]
256
257 - def remove_production_by_id(self, prod_line_id):
258 """ 259 Convenience method. Assumes, that this production line id has been added to this instance. 260 @param prod_line_id: production line id to remove 261 """ 262 self.remove_production(self._get_production(prod_line_id))
263
264 - def alter_production_time(self, modifier, prod_line_id=None):
265 """Multiplies the original production time of all production lines by modifier 266 @param modifier: a numeric value 267 @param prod_line_id: id of production line to alter. None means every production line""" 268 if prod_line_id is None: 269 for production in self.get_productions(): 270 production.alter_production_time(modifier) 271 else: 272 self._get_production(prod_line_id).alter_production_time(modifier)
273
274 - def remove(self):
275 self.__removal_started = True 276 Scheduler().rem_all_classinst_calls(self) 277 for production in self.get_productions(): 278 self.remove_production(production) 279 # call super() after removing all productions since it removes the instance (make it invalid) 280 # which can be needed by changelisteners' actions (e.g. in remove_production method) 281 super().remove() 282 assert not self.get_productions(), 'Failed to remove {} '.format(self.get_productions())
283 284 # PROTECTED METHODS
285 - def _get_current_state(self):
286 """Returns the current state of the producer. It is the most important 287 state of all productions combined. Check the PRODUCTION.STATES constant 288 for list of states and their importance.""" 289 current_state = PRODUCTION.STATES.none 290 for production in self.get_productions(): 291 state = production.get_animating_state() 292 if state is not None and current_state < state: 293 current_state = state 294 return current_state
295
296 - def get_productions(self):
297 """Returns all productions, inactive and active ones, as list""" 298 return list(self._productions.values()) + list(self._inactive_productions.values())
299
300 - def get_production_lines(self):
301 """Returns all production lines that have been added. 302 @return: a list of prodline ids""" 303 return list(self._productions.keys()) + list(self._inactive_productions.keys())
304
305 - def _get_production(self, prod_line_id):
306 """Returns a production of this producer by a production line id. 307 @return: instance of Production or None""" 308 if prod_line_id in self._productions: 309 return self._productions[prod_line_id] 310 elif prod_line_id in self._inactive_productions: 311 return self._inactive_productions[prod_line_id] 312 else: 313 return None
314
315 - def is_active(self, production=None):
316 """Checks if a production, or the at least one production if production is None, is active""" 317 if production is None: 318 for production in self.get_productions(): 319 if not production.is_paused(): 320 return True 321 return False 322 else: 323 assert production.get_production_line_id() in self._productions or \ 324 production.get_production_line_id() in self._inactive_productions 325 return not production.is_paused()
326
327 - def set_active(self, production=None, active=True):
328 """Pause or unpause a production (aka set it active/inactive). 329 see also: is_active, toggle_active 330 @param production: instance of Production. if None, we do it to all productions. 331 @param active: whether to set it active or inactive""" 332 if production is None: 333 # set all 334 for production in self.get_productions(): 335 self.set_active(production, active) 336 else: 337 line_id = production.get_production_line_id() 338 if active: 339 if not self.is_active(production): 340 self.log.debug("ResHandler %s: reactivating production %s", self.instance.worldid, line_id) 341 self._productions[line_id] = production 342 del self._inactive_productions[line_id] 343 production.pause(pause=False) 344 else: 345 if self.is_active(production): 346 self.log.debug("ResHandler %s: deactivating production %s", self.instance.worldid, line_id) 347 self._inactive_productions[line_id] = production 348 del self._productions[line_id] 349 production.pause() 350 self._update_decommissioned_icon() 351 352 self.instance._changed() 353 self.on_activity_changed(self.is_active())
354
355 - def _add_status_icon(self, icon):
356 if not self.__removal_started: 357 AddStatusIcon.broadcast(self, icon)
358
360 """Add or remove decommissioned icon.""" 361 if not self.instance.has_status_icon: 362 return 363 364 if self.is_active() is not self.__active: 365 self.__active = not self.__active 366 if self.__active: 367 RemoveStatusIcon.broadcast(self, self.instance, DecommissionedStatus) 368 else: 369 self._add_status_icon(DecommissionedStatus(self.instance))
370
371 - def toggle_active(self, production=None):
372 if production is None: 373 for production in self.get_productions(): 374 self.toggle_active(production) 375 else: 376 active = self.is_active(production) 377 self.set_active(production, active=not active)
378
379 - def _on_production_change(self):
380 """Makes the instance act according to the producers 381 current state""" 382 state = self._get_current_state() 383 new_action = 'idle' 384 if state is PRODUCTION.STATES.producing: 385 new_action = "work" 386 elif state is PRODUCTION.STATES.inventory_full: 387 new_action = "idle_full" 388 389 # don't force restarts as not to disturb sequences such as tree growth 390 self.instance.act(new_action, repeating=True, force_restart=False) 391 392 if self.instance.has_status_icon: 393 full = state is PRODUCTION.STATES.inventory_full 394 if full and not hasattr(self, "_producer_status_icon"): 395 affected_res = set() # find them: 396 for prod in self.get_productions(): 397 affected_res = affected_res.union(prod.get_unstorable_produced_res()) 398 self._producer_status_icon = InventoryFullStatus(self.instance, affected_res) 399 self._add_status_icon(self._producer_status_icon) 400 401 if not full and hasattr(self, "_producer_status_icon"): 402 RemoveStatusIcon.broadcast(self, self.instance, InventoryFullStatus) 403 del self._producer_status_icon
404
405 - def get_status_icons(self):
410
411 - def __str__(self):
412 return 'Producer(owner: ' + str(self.instance) + ')'
413
414 - def get_production_progress(self):
415 """Returns the current progress of the active production.""" 416 for production in self._productions.values(): 417 # Always return first production 418 return production.progress 419 for production in self._inactive_productions.values(): 420 # try inactive ones, if no active ones are found 421 # this makes e.g. the boatbuilder's progress bar constant when you pause it 422 return production.progress 423 return 0 # No production available
424
425 - def __set_utilization(self, utilization):
426 self.__utilization = utilization
427 428 @classmethod
429 - def get_instance(cls, arguments=None):
430 arguments = arguments and arguments.copy() or {} 431 432 utilization = None 433 if 'utilization' in arguments: 434 if arguments['utilization'] in cls.utilization_mapping: 435 utilization = cls.utilization_mapping[arguments['utilization']]() 436 del arguments['utilization'] 437 else: 438 utilization = Utilization() 439 440 if arguments.get('is_mine_for'): 441 # this is more of an aspect than an actual subclass, but python doesn't allow 442 # fast aspect-oriented programming 443 cls = MineProducer 444 445 return cls(utilization_calculator=utilization, **arguments)
446
447 448 -class MineProducer(Producer):
449 """Normal producer that can irrecoverably run out of resources and handles this case"""
450 - def set_active(self, production=None, active=True):
451 super().set_active(production, active) 452 # check if the user set it to waiting_for_res (which doesn't do anything) 453 if active and self._get_current_state() == PRODUCTION.STATES.waiting_for_res: 454 super().set_active(production, active=False) 455 AmbientSoundComponent.play_special('error')
456
457 - def _on_production_change(self):
458 super()._on_production_change() 459 if self._get_current_state() == PRODUCTION.STATES.waiting_for_res: 460 # this is never going to change, the building is useless now. 461 if self.is_active(): 462 self.set_active(active=False) 463 MineEmpty.broadcast(self, self.instance)
464
465 466 -class QueueProducer(Producer):
467 """The QueueProducer stores all productions in a queue and runs them one 468 by one. """ 469 470 production_class = SingleUseProduction 471
472 - def __init__(self, **kwargs):
473 super().__init__(auto_init=False, **kwargs) 474 self.__init()
475
476 - def __init(self):
477 self.production_queue = [] # queue of production line ids
478
479 - def save(self, db):
480 super().save(db) 481 for i in enumerate(self.production_queue): 482 position, prod_line_id = i 483 db("INSERT INTO production_queue (object, position, production_line_id) VALUES(?, ?, ?)", 484 self.instance.worldid, position, prod_line_id)
485
486 - def load(self, db, worldid):
487 super().load(db, worldid) 488 self.__init() 489 for (prod_line_id,) in db("SELECT production_line_id FROM production_queue WHERE object = ? ORDER by position", worldid): 490 self.production_queue.append(prod_line_id)
491
492 - def add_production_by_id(self, production_line_id):
493 """Convenience method. 494 @param production_line_id: Production line from db 495 """ 496 self.production_queue.append(production_line_id) 497 if not self.is_active(): 498 # Remove all calls to start_next_production 499 # These might still be scheduled if the last production finished 500 # in the same tick as this one is being added in 501 Scheduler().rem_call(self, self.start_next_production) 502 503 self.start_next_production()
504
506 # See if we can start the next production, this only works if the current 507 # production is done 508 state = self._get_current_state() 509 return len(self.production_queue) > 0 and \ 510 (state is PRODUCTION.STATES.done or 511 state is PRODUCTION.STATES.none)
512
513 - def on_queue_element_finished(self, production):
514 """Callback used for the SingleUseProduction""" 515 self.remove_production(production) 516 Scheduler().add_new_object(self.start_next_production, self)
517
518 - def start_next_production(self):
519 """Starts the next production that is in the queue, if there is one.""" 520 if self.check_next_production_startable(): 521 self._productions.clear() # Make sure we only have one production active 522 production_line_id = self.production_queue.pop(0) 523 prod = self.create_production(production_line_id) 524 prod.add_production_finished_listener(self.on_queue_element_finished) 525 self.add_production(prod) 526 self.set_active(production=prod, active=True) 527 else: 528 self.set_active(active=False)
529
530 - def cancel_all_productions(self):
531 self.production_queue = [] 532 self.cancel_current_production()
533
535 """Cancels the current production and proceeds to the next one, if there is one""" 536 # Remove current productions, lose all progress and resources 537 for production in self._productions.copy().values(): 538 self.remove_production(production) 539 for production in self._inactive_productions.copy().values(): 540 self.remove_production(production) 541 if self.production_queue: 542 self.start_next_production() 543 else: 544 self.set_active(active=False)
545
546 - def remove_from_queue(self, index):
547 """Remove the index'th element from the queue. First element is 0""" 548 self.production_queue.pop(index) 549 self.instance._changed()
550
551 552 -class ShipProducer(QueueProducer):
553 """Uses queues to produce naval units""" 554 555 produces_resource = False 556 production_class = UnitProduction 557
559 """Returns a list unit type ids that are going to be produced. 560 Does not include the currently produced unit. List is in order.""" 561 queue = [] 562 for prod_line_id in self.production_queue: 563 prod_line = self.create_production_line(prod_line_id) 564 units = list(prod_line.unit_production.keys()) 565 if len(units) > 1: 566 print('WARNING: unit production system has been designed for 1 type per order') 567 queue.append(units[0]) 568 return queue
569
570 - def on_queue_element_finished(self, production):
573
574 - def __create_unit(self):
575 """Create the produced unit now.""" 576 productions = list(self._productions.values()) 577 for production in productions: 578 assert isinstance(production, UnitProduction) 579 self.on_production_finished(production.get_produced_units()) 580 for unit, amount in production.get_produced_units().items(): 581 for i in range(amount): 582 self._place_unit(unit)
583
584 - def _place_unit(self, unit):
585 radius = 1 586 found_tile = False 587 # search for free water tile, and increase search radius if none is found 588 while not found_tile: 589 for coord in Circle(self.instance.position.center, radius).tuple_iter(): 590 point = Point(coord[0], coord[1]) 591 if self.instance.island.get_tile(point) is not None: 592 continue 593 tile = self.session.world.get_tile(point) 594 if tile is not None and tile.is_water and coord not in self.session.world.ship_map: 595 # execute bypassing the manager, it's simulated on every machine 596 u = CreateUnit(self.instance.owner.worldid, unit, point.x, point.y)(issuer=self.instance.owner) 597 # Fire a message indicating that the ship has been created 598 name = u.get_component(NamedComponent).name 599 self.session.ingame_gui.message_widget.add(string_id='NEW_SHIP', point=point, 600 message_dict={'name': name}) 601 found_tile = True 602 break 603 radius += 1
604
605 606 -class GroundUnitProducer(ShipProducer):
607 """Uses queues to produce groundunits""" 608 609 produces_resource = False 610
611 - def _place_unit(self, unit):
612 radius = 1 613 found_tile = False 614 while not found_tile: 615 # search for a free tile around the building 616 for tile in self.instance.island.get_surrounding_tiles(self.instance.position.center, radius): 617 point = Point(tile.x, tile.y) 618 if not (tile.is_water or tile.blocked) and (tile.x, tile.y) not in self.session.world.ground_unit_map: 619 u = CreateUnit(self.instance.owner.worldid, unit, tile.x, tile.y)(issuer=self.instance.owner) 620 # Fire a message indicating that the ship has been created 621 name = u.get_component(NamedComponent).name 622 self.session.ingame_gui.message_widget.add(string_id='NEW_SOLDIER', point=point, 623 message_dict={'name': name}) 624 found_tile = True 625 break 626 radius += 1
627