Package horizons :: Module session
[hide private]
[frames] | no frames]

Source Code for Module horizons.session

  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 json 
 23  import logging 
 24  import os 
 25  import os.path 
 26  import time 
 27  import traceback 
 28  from random import Random 
 29   
 30  import horizons.globals 
 31  import horizons.main 
 32  from horizons.ai.aiplayer import AIPlayer 
 33  from horizons.command.building import Tear 
 34  from horizons.command.unit import RemoveUnit 
 35  from horizons.component.ambientsoundcomponent import AmbientSoundComponent 
 36  from horizons.component.namedcomponent import NamedComponent 
 37  from horizons.component.selectablecomponent import SelectableBuildingComponent 
 38  from horizons.constants import GAME_SPEED 
 39  from horizons.entities import Entities 
 40  from horizons.extscheduler import ExtScheduler 
 41  from horizons.gui.ingamegui import IngameGui 
 42  from horizons.i18n import gettext as T 
 43  from horizons.messaging import LoadingProgress, MessageBus, SettingChanged, SpeedChanged 
 44  from horizons.messaging.queuingmessagebus import QueuingMessageBus 
 45  from horizons.savegamemanager import SavegameManager 
 46  from horizons.scenario import ScenarioEventHandler 
 47  from horizons.scheduler import Scheduler 
 48  from horizons.util.dbreader import DbReader 
 49  from horizons.util.living import LivingObject, livingProperty 
 50  from horizons.util.savegameaccessor import SavegameAccessor 
 51  from horizons.util.uhdbaccessor import read_savegame_template 
 52  from horizons.util.worldobject import WorldObject 
 53  from horizons.view import View 
 54  from horizons.world import World 
55 56 57 -class Session(LivingObject):
58 """The Session class represents the game's main ingame view and controls cameras and map loading. 59 It is alive as long as a game is running. 60 Many objects require a reference to this, which makes it a pseudo-global, from which we would 61 like to move away in the long term. This is where we hope the components come into play, which 62 you will encounter later. 63 64 This is the most important class if you are going to hack on Unknown Horizons; it provides most of 65 the important ingame variables. 66 Here's a small list of commonly used attributes: 67 68 * world - horizons.world instance of the currently running horizons. Stores players and islands, 69 which store settlements, which store buildings, which have productions and collectors. 70 Therefore, world deserves its name -- it contains the whole game state. 71 * scheduler - horizons.scheduler instance. Used to execute timed events. Master of time in UH. 72 * manager - horizons.manager instance. Used to execute commands (used to apply user interactions). 73 There is a singleplayer and a multiplayer version. Our mp system works by the mp-manager not 74 executing the commands directly, but sending them to all players, where they will be executed 75 at the same tick. 76 * view - horizons.view instance. Used to control the ingame camera. 77 * ingame_gui - horizons.gui.ingame_gui instance. Used to control the ingame gui framework. 78 (This is different from gui, which is the main menu and general session-independent gui) 79 * selected_instances - Set that holds the currently selected instances (building, units). 80 81 TUTORIAL: 82 For further digging you should now be checking out the load() function. 83 """ 84 timer = livingProperty() 85 manager = livingProperty() 86 view = livingProperty() 87 ingame_gui = livingProperty() 88 scenario_eventhandler = livingProperty() 89 90 log = logging.getLogger('session') 91
92 - def __init__(self, db, rng_seed=None, ingame_gui_class=IngameGui):
93 super().__init__() 94 assert isinstance(db, horizons.util.uhdbaccessor.UhDbAccessor) 95 self.log.debug("Initing session") 96 self.db = db # main db for game data (game.sql) 97 # this saves how often the current game has been saved 98 self.savecounter = 0 99 self.is_alive = True 100 self.paused_ticks_per_second = GAME_SPEED.TICKS_PER_SECOND 101 102 self._clear_caches() 103 104 #game 105 self.random = self.create_rng(rng_seed) 106 assert isinstance(self.random, Random) 107 self.timer = self.create_timer() 108 Scheduler.create_instance(self.timer) 109 self.manager = self.create_manager() 110 self.view = View() 111 Entities.load(self.db) 112 self.scenario_eventhandler = ScenarioEventHandler(self) # dummy handler with no events 113 114 #GUI 115 self._ingame_gui_class = ingame_gui_class 116 117 self.selected_instances = set() 118 # List of sets that holds the player assigned unit groups. 119 self.selection_groups = [set() for _unused in range(10)] 120 121 self._old_autosave_interval = None
122
123 - def start(self):
124 """Actually starts the game.""" 125 self.timer.activate() 126 self.scenario_eventhandler.start() 127 self.reset_autosave() 128 SettingChanged.subscribe(self._on_setting_changed)
129
130 - def reset_autosave(self):
131 """(Re-)Set up autosave. Called if autosave interval has been changed.""" 132 # get_uh_setting returns floats like 4.0 and 42.0 since slider stepping is 1.0. 133 interval = int(horizons.globals.fife.get_uh_setting("AutosaveInterval")) 134 if interval != self._old_autosave_interval: 135 self._old_autosave_interval = interval 136 ExtScheduler().rem_call(self, self.autosave) 137 if interval != 0: #autosave 138 self.log.debug("Initing autosave every %s minutes", interval) 139 ExtScheduler().add_new_object(self.autosave, self, interval * 60, -1)
140
141 - def _on_setting_changed(self, message):
142 if message.setting_name == 'AutosaveInterval': 143 self.reset_autosave()
144
145 - def create_manager(self):
146 """Returns instance of command manager (currently MPManager or SPManager)""" 147 raise NotImplementedError
148
149 - def create_rng(self, seed=None):
150 """Returns a RNG (random number generator). Must support the python random.Random interface""" 151 raise NotImplementedError
152
153 - def create_timer(self):
154 """Returns a Timer instance.""" 155 raise NotImplementedError
156 157 @classmethod
158 - def _clear_caches(cls):
159 """Clear all data caches in global namespace related to a session""" 160 WorldObject.reset() 161 NamedComponent.reset() 162 AIPlayer.clear_caches() 163 SelectableBuildingComponent.reset()
164
165 - def end(self):
166 self.log.debug("Ending session") 167 self.is_alive = False 168 169 # Has to be done here, cause the manager uses Scheduler! 170 Scheduler().rem_all_classinst_calls(self) 171 ExtScheduler().rem_all_classinst_calls(self) 172 173 horizons.globals.fife.sound.end() 174 175 # these will call end() if the attribute still exists by the LivingObject magic 176 self.ingame_gui = None # keep this before world 177 178 if hasattr(self, 'world'): 179 # must be called before the world ref is gone, but may not exist yet while loading 180 self.world.end() 181 self.world = None 182 self.view = None 183 self.manager = None 184 self.timer = None 185 self.scenario_eventhandler = None 186 187 Scheduler().end() 188 Scheduler.destroy_instance() 189 190 self.selected_instances = None 191 self.selection_groups = None 192 193 self._clear_caches() 194 195 # discard() in case loading failed and we did not yet subscribe 196 SettingChanged.discard(self._on_setting_changed) 197 MessageBus().reset() 198 QueuingMessageBus().reset()
199
200 - def quit(self):
201 self.end() 202 horizons.main.quit_session()
203
204 - def autosave(self):
205 raise NotImplementedError
206
207 - def quicksave(self):
208 raise NotImplementedError
209
210 - def quickload(self):
211 raise NotImplementedError
212
213 - def save(self, savegame=None):
214 raise NotImplementedError
215
216 - def load(self, options):
217 """Loads a map. Key method for starting a game.""" 218 """ 219 TUTORIAL: Here you see how the vital game elements (and some random things that are also required) 220 are initialized. 221 """ 222 if options.is_scenario: 223 # game_identifier is a yaml file, that contains reference to actual map file 224 self.scenario_eventhandler = ScenarioEventHandler(self, options.game_identifier) 225 # scenario maps can be normal maps or scenario maps: 226 map_filename = self.scenario_eventhandler.get_map_file() 227 options.game_identifier = os.path.join(SavegameManager.scenario_maps_dir, map_filename) 228 if not os.path.exists(options.game_identifier): 229 options.game_identifier = os.path.join(SavegameManager.maps_dir, map_filename) 230 options.is_map = True 231 232 self.log.debug("Session: Loading from %s", options.game_identifier) 233 savegame_db = SavegameAccessor(options.game_identifier, options.is_map, options) # Initialize new dbreader 234 savegame_data = SavegameManager.get_metadata(savegame_db.db_path) 235 self.view.resize_layers(savegame_db) 236 237 # load how often the game has been saved (used to know the difference between 238 # a loaded and a new game) 239 self.savecounter = savegame_data.get('savecounter', 0) 240 241 if savegame_data.get('rng_state', None): 242 rng_state_list = json.loads(savegame_data['rng_state']) 243 244 # json treats tuples as lists, but we need tuples here, so convert back 245 def rec_list_to_tuple(x): 246 if isinstance(x, list): 247 return tuple(rec_list_to_tuple(i) for i in x) 248 else: 249 return x
250 rng_state_tuple = rec_list_to_tuple(rng_state_list) 251 # changing the rng is safe for mp, as all players have to have the same map 252 self.random.setstate(rng_state_tuple) 253 254 LoadingProgress.broadcast(self, 'session_create_world') 255 self.world = World(self) # Load horizons.world module (check horizons/world/__init__.py) 256 self.world._init(savegame_db, options.force_player_id, disasters_enabled=options.disasters_enabled) 257 self.view.load(savegame_db, self.world) # load view 258 if not self.is_game_loaded(): 259 options.init_new_world(self) 260 else: 261 # try to load scenario data 262 self.scenario_eventhandler.load(savegame_db) 263 self.manager.load(savegame_db) # load the manager (there might be old scheduled ticks). 264 LoadingProgress.broadcast(self, "session_index_fish") 265 self.world.init_fish_indexer() # now the fish should exist 266 267 # load the old gui positions and stuff 268 # Do this before loading selections, they need the minimap setup 269 LoadingProgress.broadcast(self, "session_load_gui") 270 self.ingame_gui = self._ingame_gui_class(self) 271 self.ingame_gui.load(savegame_db) 272 273 Scheduler().before_ticking() 274 savegame_db.close() 275 276 assert hasattr(self.world, "player"), 'Error: there is no human player' 277 LoadingProgress.broadcast(self, "session_finish") 278 """ 279 TUTORIAL: 280 That's it. After that, we call start() to activate the timer, and we're live. 281 From here on you should dig into the classes that are loaded above, especially the world class 282 (horizons/world/__init__.py). It's where the magic happens and all buildings and units are loaded. 283 """
284
285 - def speed_set(self, ticks, suggestion=False):
286 """Set game speed to ticks ticks per second""" 287 old = self.timer.ticks_per_second 288 self.timer.ticks_per_second = ticks 289 self.view.map.setTimeMultiplier(float(ticks) / float(GAME_SPEED.TICKS_PER_SECOND)) 290 if old == 0 and self.timer.tick_next_time is None: # back from paused state 291 if self.paused_time_missing is None: 292 # happens if e.g. a dialog pauses the game during startup on hotkeypress 293 self.timer.tick_next_time = time.time() 294 else: 295 self.timer.tick_next_time = time.time() + (self.paused_time_missing / ticks) 296 elif ticks == 0 or self.timer.tick_next_time is None: 297 # go into paused state or very early speed change (before any tick) 298 if self.timer.tick_next_time is not None: 299 self.paused_time_missing = (self.timer.tick_next_time - time.time()) * old 300 else: 301 self.paused_time_missing = None 302 self.timer.tick_next_time = None 303 else: 304 """ 305 Under odd circumstances (anti-freeze protection just activated, game speed 306 decremented multiple times within this frame) this can delay the next tick 307 by minutes. Since the positive effects of the code aren't really observeable, 308 this code is commented out and possibly will be removed. 309 310 # correct the time until the next tick starts 311 time_to_next_tick = self.timer.tick_next_time - time.time() 312 if time_to_next_tick > 0: # only do this if we aren't late 313 self.timer.tick_next_time += (time_to_next_tick * old / ticks) 314 """ 315 316 SpeedChanged.broadcast(self, old, ticks)
317
318 - def speed_up(self):
319 if self.speed_is_paused(): 320 AmbientSoundComponent.play_special('error') 321 return 322 if self.timer.ticks_per_second in GAME_SPEED.TICK_RATES: 323 i = GAME_SPEED.TICK_RATES.index(self.timer.ticks_per_second) 324 if i + 1 < len(GAME_SPEED.TICK_RATES): 325 self.speed_set(GAME_SPEED.TICK_RATES[i + 1]) 326 else: 327 self.speed_set(GAME_SPEED.TICK_RATES[0])
328
329 - def speed_down(self):
330 if self.speed_is_paused(): 331 AmbientSoundComponent.play_special('error') 332 return 333 if self.timer.ticks_per_second in GAME_SPEED.TICK_RATES: 334 i = GAME_SPEED.TICK_RATES.index(self.timer.ticks_per_second) 335 if i > 0: 336 self.speed_set(GAME_SPEED.TICK_RATES[i - 1]) 337 else: 338 self.speed_set(GAME_SPEED.TICK_RATES[0])
339 340 _pause_stack = 0 # this saves the level of pausing 341 # e.g. if two dialogs are displayed, that pause the game, 342 # unpause needs to be called twice to unpause the game. cf. #876 343
344 - def speed_pause(self, suggestion=False):
345 self.log.debug("Session: Pausing") 346 self._pause_stack += 1 347 if not self.speed_is_paused(): 348 self.paused_ticks_per_second = self.timer.ticks_per_second 349 self.speed_set(0, suggestion)
350
351 - def speed_unpause(self, suggestion=False):
352 self.log.debug("Session: Unpausing") 353 if self.speed_is_paused(): 354 self._pause_stack -= 1 355 if self._pause_stack == 0: 356 self.speed_set(self.paused_ticks_per_second)
357
358 - def speed_toggle_pause(self, suggestion=False):
359 if self.speed_is_paused(): 360 self.speed_unpause(suggestion) 361 else: 362 self.speed_pause(suggestion)
363
364 - def speed_is_paused(self):
365 return (self.timer.ticks_per_second == 0)
366
367 - def is_game_loaded(self):
368 """Checks if the current game is a new one, or a loaded one. 369 @return: True if game is loaded, else False 370 """ 371 return (self.savecounter > 0)
372
373 - def remove_selected(self):
374 self.log.debug('Removing %s', self.selected_instances) 375 for instance in [inst for inst in self.selected_instances]: 376 if instance.is_building: 377 if instance.tearable and instance.owner is self.world.player: 378 self.log.debug('Attempting to remove building %s', instance) 379 Tear(instance).execute(self) 380 self.selected_instances.discard(instance) 381 else: 382 self.log.debug('Unable to remove building %s', instance) 383 elif instance.is_unit: 384 if instance.owner is self.world.player: 385 self.log.debug('Attempting to remove unit %s', instance) 386 RemoveUnit(instance).execute(self) 387 self.selected_instances.discard(instance) 388 else: 389 self.log.debug('Unable to remove unit %s', instance) 390 else: 391 self.log.error('Unable to remove unknown object %s', instance)
392
393 - def _do_save(self, savegame):
394 """Actual save code. 395 @param savegame: absolute path""" 396 assert os.path.isabs(savegame) 397 self.log.debug("Session: Saving to %s", savegame) 398 try: 399 if os.path.exists(savegame): 400 os.unlink(savegame) 401 self.savecounter += 1 402 403 db = DbReader(savegame) 404 except IOError as e: # usually invalid filename 405 headline = T("Failed to create savegame file") 406 descr = T("There has been an error while creating your savegame file.") 407 advice = T("This usually means that the savegame name contains unsupported special characters.") 408 self.ingame_gui.open_error_popup(headline, descr, advice, str(e)) 409 # retry with new savegamename entered by the user 410 # (this must not happen with quicksave/autosave) 411 return self.save() 412 except PermissionError: 413 self.ingame_gui.open_error_popup( 414 T("Access is denied"), 415 T("The savegame file could be read-only or locked by another process.") 416 ) 417 return self.save() 418 419 try: 420 read_savegame_template(db) 421 422 db("BEGIN") 423 self.world.save(db) 424 self.view.save(db) 425 self.ingame_gui.save(db) 426 self.scenario_eventhandler.save(db) 427 428 # Store RNG state 429 rng_state = json.dumps(self.random.getstate()) 430 SavegameManager.write_metadata(db, self.savecounter, rng_state) 431 432 # Make sure everything gets written now 433 db("COMMIT") 434 db.close() 435 return True 436 except Exception: 437 self.log.error("Save Exception:") 438 traceback.print_exc() 439 # remove invalid savegamefile (but close db connection before deleting) 440 db.close() 441 os.unlink(savegame) 442 return False
443