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

Source Code for Module horizons.world.production.production

  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  from collections import defaultdict, deque 
 24   
 25  from horizons.constants import PRODUCTION 
 26  from horizons.scheduler import Scheduler 
 27  from horizons.util.changelistener import ChangeListener, metaChangeListenerDecorator 
 28  from horizons.world.production.productionline import ProductionLine 
29 30 31 @metaChangeListenerDecorator("production_finished") 32 -class Production(ChangeListener):
33 """Class for production to be used by ResourceHandler. 34 Controls production and starts it by watching the assigned building's inventory, 35 which is virtually the only "interface" to the building. 36 This ensures independence and encapsulation from the building code. 37 38 A Production is active by default, but you can pause it. 39 40 Before we can start production, we check certain assertions, i.e. if we have all the 41 resources, that the production takes and if there is enough space to store the produced goods. 42 43 It has basic states, which are useful for e.g. setting animation. Changes in state 44 can be observed via ChangeListener interface.""" 45 log = logging.getLogger('world.production') 46 47 # optimization: 48 # the special resource gold is only stored in the player's inventory. 49 # If productions want to use it, they will observer every change of it, which results in 50 # a lot calls. Therefore, this is not done by default but only for few subclasses that actually need it. 51 uses_gold = False 52 53 keep_original_prod_line = False 54 55 ## INIT/DESTRUCT
56 - def __init__(self, inventory, owner_inventory, prod_id, prod_data, 57 start_finished=False, load=False, **kwargs):
58 """ 59 @param inventory: interface to the world, take res from here and put output back there 60 @param owner_inventory: same as inventory, but for gold. Usually the players'. 61 @param prod_id: int id of the production line 62 @param prod_data: ? 63 @param start_finished: Whether to start at the final state of a production 64 @param load: set to True if this production is supposed to load a saved production 65 """ 66 super().__init__(**kwargs) 67 # this has grown to be a bit weird compared to other init/loads 68 # __init__ is always called before load, therefore load just overwrites some of the values here 69 self._state_history = deque() 70 self.prod_id = prod_id 71 self.prod_data = prod_data 72 self.__start_finished = start_finished 73 self.inventory = inventory 74 self.owner_inventory = owner_inventory 75 76 self._pause_remaining_ticks = 0 # only used in pause() 77 self._pause_old_state = None # only used in pause() 78 79 self._creation_tick = Scheduler().cur_tick 80 81 assert isinstance(prod_id, int) 82 self._prod_line = ProductionLine(id=prod_id, data=prod_data) 83 84 if self.__class__.keep_original_prod_line: # used by unit productions 85 self.original_prod_line = self._prod_line.get_original_copy() 86 87 if not load: 88 # init production to start right away 89 90 if self.__start_finished: 91 # finish the production 92 self._give_produced_res() 93 94 self._state = PRODUCTION.STATES.waiting_for_res 95 self._add_listeners(check_now=True)
96
97 - def save(self, db, owner_id):
98 """owner_id: worldid of the owner of the producer object that owns this production""" 99 self._clean_state_history() 100 current_tick = Scheduler().cur_tick 101 translated_creation_tick = self._creation_tick - current_tick + 1 # pre-translate the tick number for the loading process 102 103 remaining_ticks = 0 104 if self._state == PRODUCTION.STATES.paused: 105 remaining_ticks = self._pause_remaining_ticks 106 elif self._state == PRODUCTION.STATES.producing: 107 remaining_ticks = Scheduler().get_remaining_ticks(self, self._get_producing_callback()) 108 # use a number > 0 for ticks 109 remaining_ticks = remaining_ticks or 0 110 if remaining_ticks < 1: 111 remaining_ticks = 1 112 db('INSERT INTO production(rowid, state, prod_line_id, remaining_ticks, \ 113 _pause_old_state, creation_tick, owner) VALUES(?, ?, ?, ?, ?, ?, ?)', 114 None, self._state.index, self._prod_line.id, remaining_ticks, 115 None if self._pause_old_state is None else self._pause_old_state.index, 116 translated_creation_tick, owner_id) 117 118 # save state history 119 for tick, state in self._state_history: 120 # pre-translate the tick number for the loading process 121 translated_tick = tick - current_tick + 1 122 db("INSERT INTO production_state_history(production, tick, state, object_id) VALUES(?, ?, ?, ?)", 123 self.prod_id, translated_tick, state, owner_id)
124
125 - def load(self, db, worldid):
126 # NOTE: __init__ must have been called with load=True 127 # worldid is the world id of the producer component instance calling this 128 super().load(db, worldid) 129 130 db_data = db.get_production_by_id_and_owner(self.prod_id, worldid) 131 self._creation_tick = db_data[5] 132 self._state = PRODUCTION.STATES[db_data[0]] 133 self._pause_old_state = None if db_data[4] is None else PRODUCTION.STATES[db_data[4]] 134 if self._state == PRODUCTION.STATES.paused: 135 self._pause_remaining_ticks = db_data[3] 136 elif self._state == PRODUCTION.STATES.producing: 137 Scheduler().add_new_object(self._get_producing_callback(), self, db_data[3]) 138 elif self._state == PRODUCTION.STATES.waiting_for_res or \ 139 self._state == PRODUCTION.STATES.inventory_full: 140 # no need to call now, this just restores the state before 141 # saving, where it hasn't triggered yet, therefore it won't now 142 self._add_listeners() 143 144 self._state_history = db.get_production_state_history(worldid, self.prod_id)
145
146 - def remove(self):
147 self._remove_listeners() 148 Scheduler().rem_all_classinst_calls(self) 149 super().remove()
150 151 ## INTERFACE METHODS
152 - def get_production_line_id(self):
153 """Returns id of production line""" 154 return self._prod_line.id
155
156 - def get_consumed_resources(self):
157 """Res that are consumed here. Returns dict {res:amount}. Interface for _prod_line.""" 158 return self._prod_line.consumed_res
159
160 - def get_produced_resources(self):
161 """Res that are produced here. Returns dict {res:amount}. Interface for _prod_line.""" 162 return self._prod_line.produced_res
163
164 - def get_production_time(self):
165 return self._prod_line.time
166
167 - def get_produced_units(self):
168 """@return dict of produced units {unit_id: amount}""" 169 return self._prod_line.unit_production
170
171 - def changes_animation(self):
172 """Returns whether the production should change the animation""" 173 return self._prod_line.changes_animation
174
175 - def get_state(self):
176 """Returns the Production's current state""" 177 return self._state
178
179 - def get_animating_state(self):
180 """Returns the production's current state, 181 but only if it effects the animation, else None""" 182 if self._prod_line.changes_animation: 183 return self._state 184 else: 185 return None
186
187 - def toggle_pause(self):
188 if self.is_paused(): 189 self.pause() 190 else: 191 self.pause(pause=False)
192
193 - def is_paused(self):
194 return self._state == PRODUCTION.STATES.paused
195
196 - def pause(self, pause=True):
197 self.log.debug("Production pause: %s", pause) 198 if not pause: # do unpause 199 # switch state 200 self._state = self._pause_old_state 201 self._pause_old_state = None 202 203 # apply state 204 if self._state in (PRODUCTION.STATES.waiting_for_res, 205 PRODUCTION.STATES.inventory_full, 206 PRODUCTION.STATES.done): 207 # just restore watching 208 self._add_listeners(check_now=True) 209 210 elif self._state == PRODUCTION.STATES.producing: 211 # restore scheduler call 212 Scheduler().add_new_object(self._get_producing_callback(), self, 213 self._pause_remaining_ticks) 214 else: 215 assert False, 'Unhandled production state: {}'.format(self._pause_old_state) 216 else: # do pause 217 # switch state 218 self._pause_old_state = self._state 219 self._state = PRODUCTION.STATES.paused 220 221 if self._pause_old_state in (PRODUCTION.STATES.waiting_for_res, 222 PRODUCTION.STATES.inventory_full, 223 PRODUCTION.STATES.done): 224 self._remove_listeners() 225 elif self._pause_old_state == PRODUCTION.STATES.producing: 226 # save when production finishes and remove that call 227 self._pause_remaining_ticks = \ 228 Scheduler().get_remaining_ticks(self, self._get_producing_callback()) 229 Scheduler().rem_call(self, self._get_producing_callback()) 230 else: 231 assert False, 'Unhandled production state: {}'.format(self._pause_old_state) 232 233 self._changed()
234
235 - def finish_production_now(self):
236 """Makes the production finish now""" 237 if self._state != PRODUCTION.STATES.producing: 238 return 239 Scheduler().rem_call(self, self._get_producing_callback()) 240 self._finished_producing()
241
242 - def alter_production_time(self, modifier):
243 """@see ProductionLine.alter_production_time""" 244 try: 245 self._prod_line.alter_production_time(modifier) 246 except AttributeError: # production line doesn't have this alter method 247 pass
248
249 - def get_state_history_times(self, ignore_pause):
250 """ 251 Returns the part of time 0 <= x <= 1 the production has been in a state during the last history_length ticks. 252 """ 253 self._clean_state_history() 254 result = defaultdict(int) 255 current_tick = Scheduler().cur_tick 256 pause_state = PRODUCTION.STATES.paused.index 257 first_relevant_tick = self._get_first_relevant_tick(ignore_pause) 258 num_entries = len(self._state_history) 259 260 for i in range(num_entries): 261 if ignore_pause and self._state_history[i][1] == pause_state: 262 continue 263 tick = self._state_history[i][0] 264 if tick >= current_tick: 265 break 266 267 next_tick = min(self._state_history[i + 1][0], current_tick) if i + 1 < num_entries else current_tick 268 if next_tick <= first_relevant_tick: 269 continue 270 relevant_ticks = next_tick - tick 271 if tick < first_relevant_tick: 272 # the beginning is not relevant 273 relevant_ticks -= first_relevant_tick - tick 274 result[self._state_history[i][1]] += relevant_ticks 275 276 total_length = sum(result.values()) 277 if total_length == 0: 278 return result 279 for key in result: 280 result[key] /= float(total_length) 281 return result
282
283 - def get_age(self):
284 return Scheduler().cur_tick - self._creation_tick
285
287 """Returns all produced res for whose there is no space""" 288 l = [] 289 for res, amount in self._prod_line.produced_res.items(): 290 if self.inventory.get_free_space_for(res) < amount: 291 l.append(res) 292 return l
293 294 ## PROTECTED METHODS
295 - def _get_producing_callback(self):
296 """Returns the callback used during the process of producing (state: producing)""" 297 return self._finished_producing
298
299 - def _get_first_relevant_tick(self, ignore_pause):
300 """ 301 Returns the first tick that is relevant for production utilization calculation 302 @param ignore_pause: whether to ignore the time spent in the pause state 303 """ 304 305 current_tick = Scheduler().cur_tick 306 state_hist_len = min(PRODUCTION.STATISTICAL_WINDOW, current_tick - self._creation_tick) 307 308 first_relevant_tick = current_tick - state_hist_len 309 if not ignore_pause: 310 return first_relevant_tick 311 312 # ignore paused time 313 pause_state = PRODUCTION.STATES.paused.index 314 for i in range(len(self._state_history) - 1, -1, -1): 315 if self._state_history[i][1] != pause_state: 316 continue 317 tick = self._state_history[i][0] 318 next_tick = self._state_history[i + 1][0] if i + 1 < len(self._state_history) else current_tick 319 if next_tick <= first_relevant_tick: 320 break 321 first_relevant_tick -= next_tick - tick 322 return max(self._creation_tick, first_relevant_tick)
323
324 - def _clean_state_history(self):
325 """ remove the part of the state history that is too old to matter """ 326 first_relevant_tick = self._get_first_relevant_tick(True) 327 while len(self._state_history) > 1 and self._state_history[1][0] < first_relevant_tick: 328 self._state_history.popleft()
329
330 - def _changed(self):
331 super()._changed() 332 if not self._prod_line.save_statistics: 333 return 334 335 state = self._state.index 336 current_tick = Scheduler().cur_tick 337 338 if self._state_history and self._state_history[-1][0] == current_tick: 339 self._state_history.pop() # make sure no two events are on the same tick 340 if not self._state_history or self._state_history[-1][1] != state: 341 self._state_history.append((current_tick, state)) 342 343 self._clean_state_history()
344
345 - def _check_inventory(self):
346 """Called when assigned building's inventory changed in some way""" 347 check_space = self._check_for_space_for_produced_res() 348 if not check_space: 349 # can't produce, no space in our inventory 350 self._state = PRODUCTION.STATES.inventory_full 351 self._changed() 352 elif self._check_available_res(): 353 # we have space in our inventory and needed res are available 354 # stop listening for res 355 self._remove_listeners() 356 self._start_production() 357 else: 358 # we have space in our inventory, but needed res are missing 359 self._state = PRODUCTION.STATES.waiting_for_res 360 self._changed()
361
362 - def _start_production(self):
363 """Actually start production. Sets self to producing state""" 364 self._state = PRODUCTION.STATES.producing 365 self._produce() 366 self._changed()
367
368 - def _produce(self):
369 """Called when there are enough res in the inventory for starting production""" 370 self.log.debug("%s _produce", self) 371 assert self._check_available_res() 372 assert self._check_for_space_for_produced_res() 373 # take the res we need 374 self._remove_res_to_expend() 375 # call finished in some time 376 time = Scheduler().get_ticks(self._prod_line.time) 377 Scheduler().add_new_object(self._get_producing_callback(), self, time) 378 self.log.debug("%s _produce Adding callback in %d time", self, time)
379
380 - def _finished_producing(self, continue_producing=True, **kwargs):
381 """Called when the production finishes.""" 382 self.log.debug("%s finished", self) 383 self._give_produced_res() 384 self.on_production_finished() 385 if continue_producing: 386 self._state = PRODUCTION.STATES.waiting_for_res 387 self._add_listeners(check_now=True)
388
389 - def _add_listeners(self, check_now=False):
390 """Listen for changes in the inventory from now on.""" 391 # don't set call_listener_now to True here, adding/removing changelisteners wouldn't be atomic any more 392 self.inventory.add_change_listener(self._check_inventory) 393 if self.__class__.uses_gold: 394 self.owner_inventory.add_change_listener(self._check_inventory) 395 396 if check_now: # only check now after adding everything 397 self._check_inventory()
398
399 - def _remove_listeners(self):
400 # depending on state, a check_inventory listener might be active 401 self.inventory.discard_change_listener(self._check_inventory) 402 if self.__class__.uses_gold: 403 self.owner_inventory.discard_change_listener(self._check_inventory)
404
405 - def _give_produced_res(self):
406 """Put produces goods to the inventory""" 407 for res, amount in self._prod_line.produced_res.items(): 408 self.inventory.alter(res, amount)
409
410 - def _check_available_res(self):
411 """Checks if all required resources are there. 412 @return: bool, True if we can start production 413 """ 414 for res, amount in self._prod_line.consumed_res.items(): 415 if self.inventory[res] < (-amount): # consumed res have negative sign 416 return False 417 return True
418
419 - def _remove_res_to_expend(self):
420 """Removes the resources from the inventory, that production takes.""" 421 for res, amount in self._prod_line.consumed_res.items(): 422 remnant = self.inventory.alter(res, amount) 423 assert remnant == 0
424
426 """Checks if there is enough space in the inventory for the res, we want to produce. 427 @return bool, True if everything can fit.""" 428 for res, amount in self._prod_line.produced_res.items(): 429 if self.inventory.get_free_space_for(res) < amount: 430 return False 431 return True
432
433 - def __str__(self): # debug
434 if hasattr(self, "_state"): 435 return 'Production(state={};prodline={})'.format(self._state, self._prod_line) 436 else: 437 return "UninitializedProduction()"
438
439 440 -class ChangingProduction(Production):
441 """Same as Production, but can changes properties of the production line""" 442
443 - def save(self, db, owner_id):
444 super().save(db, owner_id) 445 self._prod_line.save(db, owner_id)
446
447 - def load(self, db, worldid):
448 super().load(db, worldid) 449 self._prod_line.load(db, worldid)
450
451 452 -class SettlerProduction(ChangingProduction):
453 """For settlers, production behaves different: 454 They produce happiness from the goods they get. They get happy immediately when the get 455 the resource (i.e. they produce at production start). 456 It needs to be a changing production since the time can be altered"""
457 - def _give_produced_res(self):
458 pass # don't give any resources, when they actually should be given
459
460 - def _remove_res_to_expend(self):
461 super()._remove_res_to_expend() 462 # give the resources when taking away the consumed goods at prod start 463 super()._give_produced_res()
464
465 466 -class SingleUseProduction(Production):
467 """This Production just produces one time, and then finishes. 468 Notification of the finishing is done via production_finished listeners. 469 Use case: Settler getting upgrade material""" 470 471 # TODO: it seems that these kinds of productions are never removed (for settlers and unit productions) 472
473 - def __init__(self, inventory, owner_inventory, prod_id, prod_data, **kwargs):
474 super().__init__(inventory=inventory, owner_inventory=owner_inventory, 475 prod_id=prod_id, prod_data=prod_data, **kwargs)
476
477 - def _finished_producing(self, **kwargs):
478 super()._finished_producing(continue_producing=False, **kwargs) 479 self._state = PRODUCTION.STATES.done
480