예제 #1
0
class Session(LivingObject):
    """The Session class represents the game's main ingame view and controls cameras and map loading.
	It is alive as long as a game is running.
	Many objects require a reference to this, which makes it a pseudo-global, from which we would
	like to move away in the long term. This is where we hope the components come into play, which
	you will encounter later.

	This is the most important class if you are going to hack on Unknown Horizons; it provides most of
	the important ingame variables.
	Here's a small list of commonly used attributes:

	* world - horizons.world instance of the currently running horizons. Stores players and islands,
		which store settlements, which store buildings, which have productions and collectors.
		Therefore, world deserves its name -- it contains the whole game state.
	* scheduler - horizons.scheduler instance. Used to execute timed events. Master of time in UH.
	* manager - horizons.manager instance. Used to execute commands (used to apply user interactions).
		There is a singleplayer and a multiplayer version. Our mp system works by the mp-manager not
		executing the commands directly, but sending them to all players, where they will be executed
		at the same tick.
	* view - horizons.view instance. Used to control the ingame camera.
	* ingame_gui - horizons.gui.ingame_gui instance. Used to control the ingame gui framework.
		(This is different from gui, which is the main menu and general session-independent gui)
	* selected_instances - Set that holds the currently selected instances (building, units).

	TUTORIAL:
	For further digging you should now be checking out the load() function.
	"""
    timer = livingProperty()
    manager = livingProperty()
    view = livingProperty()
    ingame_gui = livingProperty()
    scenario_eventhandler = livingProperty()

    log = logging.getLogger('session')

    def __init__(self, db, rng_seed=None, ingame_gui_class=IngameGui):
        super(Session, self).__init__()
        assert isinstance(db, horizons.util.uhdbaccessor.UhDbAccessor)
        self.log.debug("Initing session")
        self.db = db  # main db for game data (game.sql)
        # this saves how often the current game has been saved
        self.savecounter = 0
        self.is_alive = True
        self.paused_ticks_per_second = GAME_SPEED.TICKS_PER_SECOND

        self._clear_caches()

        #game
        self.random = self.create_rng(rng_seed)
        assert isinstance(self.random, Random)
        self.timer = self.create_timer()
        Scheduler.create_instance(self.timer)
        self.manager = self.create_manager()
        self.view = View()
        Entities.load(self.db)
        self.scenario_eventhandler = ScenarioEventHandler(
            self)  # dummy handler with no events

        #GUI
        self._ingame_gui_class = ingame_gui_class

        self.selected_instances = set()
        # List of sets that holds the player assigned unit groups.
        self.selection_groups = [set() for _unused in range(10)]

        self._old_autosave_interval = None

    def start(self):
        """Actually starts the game."""
        self.timer.activate()
        self.scenario_eventhandler.start()
        self.reset_autosave()
        SettingChanged.subscribe(self._on_setting_changed)

    def reset_autosave(self):
        """(Re-)Set up autosave. Called if autosave interval has been changed."""
        # get_uh_setting returns floats like 4.0 and 42.0 since slider stepping is 1.0.
        interval = int(
            horizons.globals.fife.get_uh_setting("AutosaveInterval"))
        if interval != self._old_autosave_interval:
            self._old_autosave_interval = interval
            ExtScheduler().rem_call(self, self.autosave)
            if interval != 0:  #autosave
                self.log.debug("Initing autosave every %s minutes", interval)
                ExtScheduler().add_new_object(self.autosave, self,
                                              interval * 60, -1)

    def _on_setting_changed(self, message):
        if message.setting_name == 'AutosaveInterval':
            self.reset_autosave()

    def create_manager(self):
        """Returns instance of command manager (currently MPManager or SPManager)"""
        raise NotImplementedError

    def create_rng(self, seed=None):
        """Returns a RNG (random number generator). Must support the python random.Random interface"""
        raise NotImplementedError

    def create_timer(self):
        """Returns a Timer instance."""
        raise NotImplementedError

    @classmethod
    def _clear_caches(cls):
        """Clear all data caches in global namespace related to a session"""
        WorldObject.reset()
        NamedComponent.reset()
        AIPlayer.clear_caches()
        SelectableBuildingComponent.reset()

    def end(self):
        self.log.debug("Ending session")
        self.is_alive = False

        # Has to be done here, cause the manager uses Scheduler!
        Scheduler().rem_all_classinst_calls(self)
        ExtScheduler().rem_all_classinst_calls(self)

        horizons.globals.fife.sound.end()

        # these will call end() if the attribute still exists by the LivingObject magic
        self.ingame_gui = None  # keep this before world

        if hasattr(self, 'world'):
            # must be called before the world ref is gone, but may not exist yet while loading
            self.world.end()
        self.world = None
        self.view = None
        self.manager = None
        self.timer = None
        self.scenario_eventhandler = None

        Scheduler().end()
        Scheduler.destroy_instance()

        self.selected_instances = None
        self.selection_groups = None

        self._clear_caches()

        # discard() in case loading failed and we did not yet subscribe
        SettingChanged.discard(self._on_setting_changed)
        MessageBus().reset()

    def quit(self):
        self.end()
        horizons.main.quit_session()

    def autosave(self):
        raise NotImplementedError

    def quicksave(self):
        raise NotImplementedError

    def quickload(self):
        raise NotImplementedError

    def save(self, savegame=None):
        raise NotImplementedError

    def load(self, options):
        """Loads a map. Key method for starting a game."""
        """
		TUTORIAL: Here you see how the vital game elements (and some random things that are also required)
		are initialized.
		"""
        if options.is_scenario:
            # game_identifier is a yaml file, that contains reference to actual map file
            self.scenario_eventhandler = ScenarioEventHandler(
                self, options.game_identifier)
            # scenario maps can be normal maps or scenario maps:
            map_filename = self.scenario_eventhandler.get_map_file()
            options.game_identifier = os.path.join(
                SavegameManager.scenario_maps_dir, map_filename)
            if not os.path.exists(options.game_identifier):
                options.game_identifier = os.path.join(
                    SavegameManager.maps_dir, map_filename)
            options.is_map = True

        self.log.debug("Session: Loading from %s", options.game_identifier)
        savegame_db = SavegameAccessor(options.game_identifier, options.is_map,
                                       options)  # Initialize new dbreader
        savegame_data = SavegameManager.get_metadata(savegame_db.db_path)
        self.view.resize_layers(savegame_db)

        # load how often the game has been saved (used to know the difference between
        # a loaded and a new game)
        self.savecounter = savegame_data.get('savecounter', 0)

        if savegame_data.get('rng_state', None):
            rng_state_list = json.loads(savegame_data['rng_state'])

            # json treats tuples as lists, but we need tuples here, so convert back
            def rec_list_to_tuple(x):
                if isinstance(x, list):
                    return tuple(rec_list_to_tuple(i) for i in x)
                else:
                    return x

            rng_state_tuple = rec_list_to_tuple(rng_state_list)
            # changing the rng is safe for mp, as all players have to have the same map
            self.random.setstate(rng_state_tuple)

        LoadingProgress.broadcast(self, 'session_create_world')
        self.world = World(
            self
        )  # Load horizons.world module (check horizons/world/__init__.py)
        self.world._init(savegame_db,
                         options.force_player_id,
                         disasters_enabled=options.disasters_enabled)
        self.view.load(savegame_db, self.world)  # load view
        if not self.is_game_loaded():
            options.init_new_world(self)
        else:
            # try to load scenario data
            self.scenario_eventhandler.load(savegame_db)
        self.manager.load(
            savegame_db
        )  # load the manager (there might be old scheduled ticks).
        LoadingProgress.broadcast(self, "session_index_fish")
        self.world.init_fish_indexer()  # now the fish should exist

        # load the old gui positions and stuff
        # Do this before loading selections, they need the minimap setup
        LoadingProgress.broadcast(self, "session_load_gui")
        self.ingame_gui = self._ingame_gui_class(self)
        self.ingame_gui.load(savegame_db)

        Scheduler().before_ticking()
        savegame_db.close()

        assert hasattr(self.world, "player"), 'Error: there is no human player'
        LoadingProgress.broadcast(self, "session_finish")
        """
		TUTORIAL:
		That's it. After that, we call start() to activate the timer, and we're live.
		From here on you should dig into the classes that are loaded above, especially the world class
		(horizons/world/__init__.py). It's where the magic happens and all buildings and units are loaded.
		"""

    def speed_set(self, ticks, suggestion=False):
        """Set game speed to ticks ticks per second"""
        old = self.timer.ticks_per_second
        self.timer.ticks_per_second = ticks
        self.view.map.setTimeMultiplier(
            float(ticks) / float(GAME_SPEED.TICKS_PER_SECOND))
        if old == 0 and self.timer.tick_next_time is None:  # back from paused state
            if self.paused_time_missing is None:
                # happens if e.g. a dialog pauses the game during startup on hotkeypress
                self.timer.tick_next_time = time.time()
            else:
                self.timer.tick_next_time = time.time() + (
                    self.paused_time_missing / ticks)
        elif ticks == 0 or self.timer.tick_next_time is None:
            # go into paused state or very early speed change (before any tick)
            if self.timer.tick_next_time is not None:
                self.paused_time_missing = (self.timer.tick_next_time -
                                            time.time()) * old
            else:
                self.paused_time_missing = None
            self.timer.tick_next_time = None
        else:
            """
			Under odd circumstances (anti-freeze protection just activated, game speed
			decremented multiple times within this frame) this can delay the next tick
			by minutes. Since the positive effects of the code aren't really observeable,
			this code is commented out and possibly will be removed.

			# correct the time until the next tick starts
			time_to_next_tick = self.timer.tick_next_time - time.time()
			if time_to_next_tick > 0: # only do this if we aren't late
				self.timer.tick_next_time += (time_to_next_tick * old / ticks)
			"""

        SpeedChanged.broadcast(self, old, ticks)

    def speed_up(self):
        if self.speed_is_paused():
            AmbientSoundComponent.play_special('error')
            return
        if self.timer.ticks_per_second in GAME_SPEED.TICK_RATES:
            i = GAME_SPEED.TICK_RATES.index(self.timer.ticks_per_second)
            if i + 1 < len(GAME_SPEED.TICK_RATES):
                self.speed_set(GAME_SPEED.TICK_RATES[i + 1])
        else:
            self.speed_set(GAME_SPEED.TICK_RATES[0])

    def speed_down(self):
        if self.speed_is_paused():
            AmbientSoundComponent.play_special('error')
            return
        if self.timer.ticks_per_second in GAME_SPEED.TICK_RATES:
            i = GAME_SPEED.TICK_RATES.index(self.timer.ticks_per_second)
            if i > 0:
                self.speed_set(GAME_SPEED.TICK_RATES[i - 1])
        else:
            self.speed_set(GAME_SPEED.TICK_RATES[0])

    _pause_stack = 0  # this saves the level of pausing

    # e.g. if two dialogs are displayed, that pause the game,
    # unpause needs to be called twice to unpause the game. cf. #876
    def speed_pause(self, suggestion=False):
        self.log.debug("Session: Pausing")
        self._pause_stack += 1
        if not self.speed_is_paused():
            self.paused_ticks_per_second = self.timer.ticks_per_second
            self.speed_set(0, suggestion)

    def speed_unpause(self, suggestion=False):
        self.log.debug("Session: Unpausing")
        if self.speed_is_paused():
            self._pause_stack -= 1
            if self._pause_stack == 0:
                self.speed_set(self.paused_ticks_per_second)

    def speed_toggle_pause(self, suggestion=False):
        if self.speed_is_paused():
            self.speed_unpause(suggestion)
        else:
            self.speed_pause(suggestion)

    def speed_is_paused(self):
        return (self.timer.ticks_per_second == 0)

    def is_game_loaded(self):
        """Checks if the current game is a new one, or a loaded one.
		@return: True if game is loaded, else False
		"""
        return (self.savecounter > 0)

    def remove_selected(self):
        self.log.debug('Removing %s', self.selected_instances)
        for instance in [inst for inst in self.selected_instances]:
            if instance.is_building:
                if instance.tearable and instance.owner is self.world.player:
                    self.log.debug('Attempting to remove building %s', inst)
                    Tear(instance).execute(self)
                    self.selected_instances.discard(instance)
                else:
                    self.log.debug('Unable to remove building %s', inst)
            elif instance.is_unit:
                if instance.owner is self.world.player:
                    self.log.debug('Attempting to remove unit %s', inst)
                    RemoveUnit(instance).execute(self)
                    self.selected_instances.discard(instance)
                else:
                    self.log.debug('Unable to remove unit %s', inst)
            else:
                self.log.error('Unable to remove unknown object %s', instance)

    def _do_save(self, savegame):
        """Actual save code.
		@param savegame: absolute path"""
        assert os.path.isabs(savegame)
        self.log.debug("Session: Saving to %s", savegame)
        try:
            if os.path.exists(savegame):
                os.unlink(savegame)
            self.savecounter += 1

            db = DbReader(savegame)
        except IOError as e:  # usually invalid filename
            headline = T("Failed to create savegame file")
            descr = T(
                "There has been an error while creating your savegame file.")
            advice = T(
                "This usually means that the savegame name contains unsupported special characters."
            )
            self.ingame_gui.open_error_popup(headline, descr, advice,
                                             unicode(e))
            # retry with new savegamename entered by the user
            # (this must not happen with quicksave/autosave)
            return self.save()
        except OSError as e:
            if e.errno != errno.EACCES:
                raise
            self.ingame_gui.open_error_popup(
                T("Access is denied"),
                T("The savegame file could be read-only or locked by another process."
                  ))
            return self.save()

        try:
            read_savegame_template(db)

            db("BEGIN")
            self.world.save(db)
            self.view.save(db)
            self.ingame_gui.save(db)
            self.scenario_eventhandler.save(db)

            # Store RNG state
            rng_state = json.dumps(self.random.getstate())
            SavegameManager.write_metadata(db, self.savecounter, rng_state)

            # Make sure everything gets written now
            db("COMMIT")
            db.close()
            return True
        except Exception:
            self.log.error("Save Exception:")
            traceback.print_exc()
            # remove invalid savegamefile (but close db connection before deleting)
            db.close()
            os.unlink(savegame)
            return False
예제 #2
0
class IngameGui(LivingObject):
    """Class handling all the ingame gui events.
	Assumes that only 1 instance is used (class variables)"""

    message_widget = livingProperty()
    minimap = livingProperty()
    keylistener = livingProperty()

    def __init__(self, session):
        super(IngameGui, self).__init__()
        self.session = session
        assert isinstance(self.session, horizons.session.Session)
        self.settlement = None
        self._old_menu = None

        self.cursor = None
        self.coordinates_tooltip = None

        self.keylistener = IngameKeyListener(self.session)

        self.cityinfo = CityInfo(self)
        LastActivePlayerSettlementManager.create_instance(self.session)

        self.message_widget = MessageWidget(self.session)

        # Windows
        self.windows = WindowManager()
        self.show_popup = self.windows.show_popup
        self.show_error_popup = self.windows.show_error_popup

        self.logbook = LogBook(self.session, self.windows)
        self.players_overview = PlayersOverview(self.session)
        self.players_settlements = PlayersSettlements(self.session)
        self.players_ships = PlayersShips(self.session)

        self.chat_dialog = ChatDialog(self.windows, self.session)
        self.change_name_dialog = ChangeNameDialog(self.windows, self.session)
        self.pausemenu = PauseMenu(self.session,
                                   self,
                                   self.windows,
                                   in_editor_mode=False)
        self.help_dialog = HelpDialog(self.windows, session=self.session)

        # Icon manager
        self.status_icon_manager = StatusIconManager(
            renderer=self.session.view.renderer['GenericRenderer'],
            layer=self.session.view.layers[LAYERS.OBJECTS])
        self.production_finished_icon_manager = ProductionFinishedIconManager(
            renderer=self.session.view.renderer['GenericRenderer'],
            layer=self.session.view.layers[LAYERS.OBJECTS])

        # 'minimap' is the guichan gui around the actual minimap, which is saved
        # in self.minimap
        self.mainhud = load_uh_widget('minimap.xml')
        self.mainhud.position_technique = "right:top"

        icon = self.mainhud.findChild(name="minimap")
        self.minimap = Minimap(
            icon,
            targetrenderer=horizons.globals.fife.targetrenderer,
            imagemanager=horizons.globals.fife.imagemanager,
            session=self.session,
            view=self.session.view)

        def speed_up():
            SpeedUpCommand().execute(self.session)

        def speed_down():
            SpeedDownCommand().execute(self.session)

        self.mainhud.mapEvents({
            'zoomIn':
            self.session.view.zoom_in,
            'zoomOut':
            self.session.view.zoom_out,
            'rotateRight':
            Callback.ChainedCallbacks(self.session.view.rotate_right,
                                      self.minimap.rotate_right),
            'rotateLeft':
            Callback.ChainedCallbacks(self.session.view.rotate_left,
                                      self.minimap.rotate_left),
            'speedUp':
            speed_up,
            'speedDown':
            speed_down,
            'destroy_tool':
            self.toggle_destroy_tool,
            'build':
            self.show_build_menu,
            'diplomacyButton':
            self.show_diplomacy_menu,
            'gameMenuButton':
            self.toggle_pause,
            'logbook':
            lambda: self.windows.toggle(self.logbook)
        })
        self.mainhud.show()

        self.resource_overview = ResourceOverviewBar(self.session)

        # Register for messages
        SpeedChanged.subscribe(self._on_speed_changed)
        NewDisaster.subscribe(self._on_new_disaster)
        NewSettlement.subscribe(self._on_new_settlement)
        PlayerLevelUpgrade.subscribe(self._on_player_level_upgrade)
        MineEmpty.subscribe(self._on_mine_empty)
        self.session.view.add_change_listener(self._update_zoom)

        self._display_speed(self.session.timer.ticks_per_second)

    def end(self):
        # unsubscribe early, to avoid messages coming in while we're shutting down
        SpeedChanged.unsubscribe(self._on_speed_changed)
        NewDisaster.unsubscribe(self._on_new_disaster)
        NewSettlement.unsubscribe(self._on_new_settlement)
        PlayerLevelUpgrade.unsubscribe(self._on_player_level_upgrade)
        MineEmpty.unsubscribe(self._on_mine_empty)
        self.session.view.remove_change_listener(self._update_zoom)

        self.mainhud.mapEvents({
            'zoomIn': None,
            'zoomOut': None,
            'rotateRight': None,
            'rotateLeft': None,
            'destroy_tool': None,
            'build': None,
            'diplomacyButton': None,
            'gameMenuButton': None
        })
        self.mainhud.hide()
        self.mainhud = None

        self.windows.close_all()
        self.message_widget = None
        self.minimap = None
        self.resource_overview.end()
        self.resource_overview = None
        self.keylistener = None
        self.cityinfo.end()
        self.cityinfo = None
        self.hide_menu()

        if self.cursor:
            self.cursor.remove()
            self.cursor.end()
            self.cursor = None

        LastActivePlayerSettlementManager().remove()
        LastActivePlayerSettlementManager.destroy_instance()

        self.production_finished_icon_manager.end()
        self.production_finished_icon_manager = None
        self.status_icon_manager.end()
        self.status_icon_manager = None

        super(IngameGui, self).end()

    def show_select_savegame(self, mode):
        window = SelectSavegameDialog(mode, self.windows)
        return self.windows.show(window)

    def toggle_pause(self):
        self.windows.toggle(self.pausemenu)

    def toggle_help(self):
        self.windows.toggle(self.help_dialog)

    def minimap_to_front(self):
        """Make sure the full right top gui is visible and not covered by some dialog"""
        self.mainhud.hide()
        self.mainhud.show()

    def show_diplomacy_menu(self):
        # check if the menu is already shown
        if getattr(self.get_cur_menu(), 'name', None) == "diplomacy_widget":
            self.hide_menu()
            return

        if not DiplomacyTab.is_useable(self.session.world):
            self.windows.show_popup(
                _("No diplomacy possible"),
                _("Cannot do diplomacy as there are no other players."))
            return

        tab = DiplomacyTab(self, self.session.world)
        self.show_menu(tab)

    def show_multi_select_tab(self, instances):
        tab = TabWidget(self,
                        tabs=[SelectMultiTab(instances)],
                        name='select_multi')
        self.show_menu(tab)

    def show_build_menu(self, update=False):
        """
		@param update: set when build possibilities change (e.g. after inhabitant tier upgrade)
		"""
        # check if build menu is already shown
        if hasattr(self.get_cur_menu(), 'name') and self.get_cur_menu(
        ).name == "build_menu_tab_widget":
            self.hide_menu()

            if not update:  # this was only a toggle call, don't reshow
                return

        self.set_cursor()  # set default cursor for build menu
        self.deselect_all()

        if not any(settlement.owner.is_local_player
                   for settlement in self.session.world.settlements):
            # player has not built any settlements yet. Accessing the build menu at such a point
            # indicates a mistake in the mental model of the user. Display a hint.
            tab = TabWidget(
                self, tabs=[TabInterface(widget="buildtab_no_settlement.xml")])
        else:
            btabs = BuildTab.create_tabs(self.session, self._build)
            tab = TabWidget(self,
                            tabs=btabs,
                            name="build_menu_tab_widget",
                            active_tab=BuildTab.last_active_build_tab)
        self.show_menu(tab)

    def deselect_all(self):
        for instance in self.session.selected_instances:
            instance.get_component(SelectableComponent).deselect()
        self.session.selected_instances.clear()

    def _build(self, building_id, unit=None):
        """Calls the games buildingtool class for the building_id.
		@param building_id: int with the building id that is to be built.
		@param unit: weakref to the unit, that builds (e.g. ship for warehouse)"""
        self.hide_menu()
        self.deselect_all()
        cls = Entities.buildings[building_id]
        if hasattr(cls, 'show_build_menu'):
            cls.show_build_menu()
        self.set_cursor('building', cls, None if unit is None else unit())

    def toggle_road_tool(self):
        if not isinstance(self.cursor, mousetools.BuildingTool
                          ) or self.cursor._class.id != BUILDINGS.TRAIL:
            self._build(BUILDINGS.TRAIL)
        else:
            self.set_cursor()

    def get_cur_menu(self):
        """Returns menu that is currently displayed"""
        return self._old_menu

    def show_menu(self, menu):
        """Shows a menu
		@param menu: str with the guiname or pychan object.
		"""
        if self._old_menu is not None:
            if hasattr(self._old_menu, "remove_remove_listener"):
                self._old_menu.remove_remove_listener(
                    Callback(self.show_menu, None))
            self._old_menu.hide()

        self._old_menu = menu
        if self._old_menu is not None:
            if hasattr(self._old_menu, "add_remove_listener"):
                self._old_menu.add_remove_listener(
                    Callback(self.show_menu, None))
            self._old_menu.show()
            self.minimap_to_front()

        TabWidgetChanged.broadcast(self)

    def hide_menu(self):
        self.show_menu(None)

    def save(self, db):
        self.message_widget.save(db)
        self.logbook.save(db)
        self.resource_overview.save(db)
        LastActivePlayerSettlementManager().save(db)

    def load(self, db):
        self.message_widget.load(db)
        self.logbook.load(db)
        self.resource_overview.load(db)

        if self.session.is_game_loaded():
            LastActivePlayerSettlementManager().load(db)
            cur_settlement = LastActivePlayerSettlementManager(
            ).get_current_settlement()
            self.cityinfo.set_settlement(cur_settlement)

        self.minimap.draw()  # update minimap to new world

        self.current_cursor = 'default'
        self.cursor = mousetools.SelectionTool(self.session)
        # Set cursor correctly, menus might need to be opened.
        # Open menus later; they may need unit data not yet inited
        self.cursor.apply_select()

        if not self.session.is_game_loaded():
            # Fire a message for new world creation
            self.session.ingame_gui.message_widget.add('NEW_WORLD')

        # Show message when the relationship between players changed
        def notify_change(caller, old_state, new_state, a, b):
            player1 = u"%s" % a.name
            player2 = u"%s" % b.name

            data = {'player1': player1, 'player2': player2}

            string_id = 'DIPLOMACY_STATUS_{old}_{new}'.format(
                old=old_state.upper(), new=new_state.upper())
            self.message_widget.add(string_id=string_id, message_dict=data)

        self.session.world.diplomacy.add_diplomacy_status_changed_listener(
            notify_change)

    def show_change_name_dialog(self, instance):
        """Shows a dialog where the user can change the name of an object."""
        self.windows.show(self.change_name_dialog, instance=instance)

    def on_escape(self):
        if self.windows.visible:
            self.windows.on_escape()
        elif hasattr(self.cursor, 'on_escape'):
            self.cursor.on_escape()
        else:
            self.toggle_pause()

        return True

    def on_return(self):
        if self.windows.visible:
            self.windows.on_return()

        return True

    def _on_speed_changed(self, message):
        self._display_speed(message.new)

    def _display_speed(self, tps):
        text = u''
        up_icon = self.mainhud.findChild(name='speedUp')
        down_icon = self.mainhud.findChild(name='speedDown')
        if tps == 0:  # pause
            text = u'0x'
            up_icon.set_inactive()
            down_icon.set_inactive()
        else:
            if tps != GAME_SPEED.TICKS_PER_SECOND:
                text = unicode("%1gx" %
                               (tps * 1.0 / GAME_SPEED.TICKS_PER_SECOND))
                #%1g: displays 0.5x, but 2x instead of 2.0x
            index = GAME_SPEED.TICK_RATES.index(tps)
            if index + 1 >= len(GAME_SPEED.TICK_RATES):
                up_icon.set_inactive()
            else:
                up_icon.set_active()
            if index > 0:
                down_icon.set_active()
            else:
                down_icon.set_inactive()

        wdg = self.mainhud.findChild(name="speed_text")
        wdg.text = text
        wdg.resizeToContent()
        self.mainhud.show()

    def on_key_press(self, action, evt):
        """Handle a key press in-game.

		Returns True if the key was acted upon.
		"""
        _Actions = KeyConfig._Actions
        keyval = evt.getKey().getValue()

        if action == _Actions.ESCAPE:
            return self.on_escape()
        elif keyval == fife.Key.ENTER:
            return self.on_return()

        if action == _Actions.GRID:
            gridrenderer = self.session.view.renderer['GridRenderer']
            gridrenderer.setEnabled(not gridrenderer.isEnabled())
        elif action == _Actions.COORD_TOOLTIP:
            self.coordinates_tooltip.toggle()
        elif action == _Actions.DESTROY_TOOL:
            self.toggle_destroy_tool()
        elif action == _Actions.REMOVE_SELECTED:
            self.session.remove_selected()
        elif action == _Actions.ROAD_TOOL:
            self.toggle_road_tool()
        elif action == _Actions.SPEED_UP:
            SpeedUpCommand().execute(self.session)
        elif action == _Actions.SPEED_DOWN:
            SpeedDownCommand().execute(self.session)
        elif action == _Actions.PAUSE:
            TogglePauseCommand().execute(self.session)
        elif action == _Actions.PLAYERS_OVERVIEW:
            self.logbook.toggle_stats_visibility(widget='players')
        elif action == _Actions.SETTLEMENTS_OVERVIEW:
            self.logbook.toggle_stats_visibility(widget='settlements')
        elif action == _Actions.SHIPS_OVERVIEW:
            self.logbook.toggle_stats_visibility(widget='ships')
        elif action == _Actions.LOGBOOK:
            self.windows.toggle(self.logbook)
        elif action == _Actions.DEBUG and VERSION.IS_DEV_VERSION:
            import pdb
            pdb.set_trace()
        elif action == _Actions.BUILD_TOOL:
            self.show_build_menu()
        elif action == _Actions.ROTATE_RIGHT:
            if hasattr(self.cursor, "rotate_right"):
                # used in e.g. build preview to rotate building instead of map
                self.cursor.rotate_right()
            else:
                self.session.view.rotate_right()
                self.minimap.rotate_right()
        elif action == _Actions.ROTATE_LEFT:
            if hasattr(self.cursor, "rotate_left"):
                self.cursor.rotate_left()
            else:
                self.session.view.rotate_left()
                self.minimap.rotate_left()
        elif action == _Actions.CHAT:
            self.windows.show(self.chat_dialog)
        elif action == _Actions.TRANSLUCENCY:
            self.session.world.toggle_translucency()
        elif action == _Actions.TILE_OWNER_HIGHLIGHT:
            self.session.world.toggle_owner_highlight()
        elif keyval in (fife.Key.NUM_0, fife.Key.NUM_1, fife.Key.NUM_2,
                        fife.Key.NUM_3, fife.Key.NUM_4, fife.Key.NUM_5,
                        fife.Key.NUM_6, fife.Key.NUM_7, fife.Key.NUM_8,
                        fife.Key.NUM_9):
            num = int(keyval - fife.Key.NUM_0)
            if evt.isControlPressed():
                # create new group (only consider units owned by the player)
                self.session.selection_groups[num] = \
                    set(filter(lambda unit : unit.owner.is_local_player,
                               self.session.selected_instances))
                # drop units of the new group from all other groups
                for group in self.session.selection_groups:
                    if group is not self.session.selection_groups[num]:
                        group -= self.session.selection_groups[num]
            else:
                # deselect
                # we need to make sure to have a cursor capable of selection (for apply_select())
                # this handles deselection implicitly in the destructor
                self.set_cursor('selection')

                # apply new selection
                for instance in self.session.selection_groups[num]:
                    instance.get_component(SelectableComponent).select(
                        reset_cam=True)
                # assign copy since it will be randomly changed, the unit should only be changed on ctrl-events
                self.session.selected_instances = self.session.selection_groups[
                    num].copy()
                # show menu depending on the entities selected
                if self.session.selected_instances:
                    self.cursor.apply_select()
                else:
                    # nothing is selected here, we need to hide the menu since apply_select doesn't handle that case
                    self.show_menu(None)
        elif action == _Actions.QUICKSAVE:
            self.session.quicksave(
            )  # load is only handled by the MainListener
        elif action == _Actions.PIPETTE:
            # copy mode: pipette tool
            self.toggle_cursor('pipette')
        elif action == _Actions.HEALTH_BAR:
            # shows health bar of every instance with an health component
            self.session.world.toggle_health_for_all_health_instances()
        elif action == _Actions.SHOW_SELECTED:
            if self.session.selected_instances:
                # scroll to first one, we can never guarantee to display all selected units
                instance = iter(self.session.selected_instances).next()
                self.session.view.center(*instance.position.center.to_tuple())
                for instance in self.session.selected_instances:
                    if hasattr(instance,
                               "path") and instance.owner.is_local_player:
                        self.minimap.show_unit_path(instance)
        elif action == _Actions.HELP:
            self.toggle_help()
        else:
            return False

        return True

    def toggle_cursor(self, which, *args, **kwargs):
        """Alternate between the cursor which and default.
		args and kwargs are used to construct which."""
        if self.current_cursor == which:
            self.set_cursor()
        else:
            self.set_cursor(which, *args, **kwargs)

    def set_cursor(self, which='default', *args, **kwargs):
        """Sets the mousetool (i.e. cursor).
		This is done here for encapsulation and control over destructors.
		Further arguments are passed to the mouse tool constructor."""
        self.cursor.remove()
        self.current_cursor = which
        klass = {
            'default': mousetools.SelectionTool,
            'selection': mousetools.SelectionTool,
            'tearing': mousetools.TearingTool,
            'pipette': mousetools.PipetteTool,
            'attacking': mousetools.AttackingTool,
            'building': mousetools.BuildingTool,
        }[which]
        self.cursor = klass(self.session, *args, **kwargs)

    def toggle_destroy_tool(self):
        """Initiate the destroy tool"""
        self.toggle_cursor('tearing')

    def _update_zoom(self):
        """Enable/disable zoom buttons"""
        zoom = self.session.view.get_zoom()
        in_icon = self.mainhud.findChild(name='zoomIn')
        out_icon = self.mainhud.findChild(name='zoomOut')
        if zoom == VIEW.ZOOM_MIN:
            out_icon.set_inactive()
        else:
            out_icon.set_active()
        if zoom == VIEW.ZOOM_MAX:
            in_icon.set_inactive()
        else:
            in_icon.set_active()

    def _on_new_disaster(self, message):
        """Called when a building is 'infected' with a disaster."""
        if message.building.owner.is_local_player and len(
                message.disaster._affected_buildings) == 1:
            pos = message.building.position.center
            self.message_widget.add(
                point=pos, string_id=message.disaster_class.NOTIFICATION_TYPE)

    def _on_new_settlement(self, message):
        player = message.settlement.owner
        self.message_widget.add(string_id='NEW_SETTLEMENT',
                                point=message.warehouse_position,
                                message_dict={'player': player.name},
                                play_sound=player.is_local_player)

    def _on_player_level_upgrade(self, message):
        """Called when a player's population reaches a new level."""
        if not message.sender.is_local_player:
            return

        # show notification
        self.message_widget.add(point=message.building.position.center,
                                string_id='SETTLER_LEVEL_UP',
                                message_dict={'level': message.level + 1})

        # update build menu to show new buildings
        menu = self.get_cur_menu()
        if hasattr(menu, "name") and menu.name == "build_menu_tab_widget":
            self.show_build_menu(update=True)

        # TODO: Use a better measure then first tab
        # Quite fragile, makes sure the tablist in the mainsquare menu is updated
        if hasattr(menu, '_tabs') and isinstance(menu._tabs[0],
                                                 MainSquareOverviewTab):
            instance = list(self.session.selected_instances)[0]
            instance.get_component(SelectableComponent).show_menu(
                jump_to_tabclass=type(menu.current_tab))

    def _on_mine_empty(self, message):
        self.message_widget.add(point=message.mine.position.center,
                                string_id='MINE_EMPTY')
예제 #3
0
class IngameGui(LivingObject):
    minimap = livingProperty()
    keylistener = livingProperty()
    message_widget = livingProperty()

    def __init__(self, session):
        self.session = session

        self.cursor = None
        self.coordinates_tooltip = None
        self.keylistener = IngameKeyListener(self.session)
        # used by NavigationTool
        LastActivePlayerSettlementManager.create_instance(self.session)

        # Mocks needed to act like the real IngameGui
        self.show_menu = Dummy
        self.hide_menu = Dummy
        # a logbook Dummy is necessary for message_widget to work
        self.logbook = Dummy()

        self.mainhud = load_uh_widget('minimap.xml')
        self.mainhud.position_technique = "right+0:top+0"

        icon = self.mainhud.findChild(name="minimap")
        self.minimap = Minimap(
            icon,
            targetrenderer=horizons.globals.fife.targetrenderer,
            imagemanager=horizons.globals.fife.imagemanager,
            session=self.session,
            view=self.session.view)

        self.mainhud.mapEvents({
            'zoomIn':
            self.session.view.zoom_in,
            'zoomOut':
            self.session.view.zoom_out,
            'rotateRight':
            Callback.ChainedCallbacks(self.session.view.rotate_right,
                                      self.minimap.rotate_right),
            'rotateLeft':
            Callback.ChainedCallbacks(self.session.view.rotate_left,
                                      self.minimap.rotate_left),
            'gameMenuButton':
            self.toggle_pause,
        })

        self.mainhud.show()
        ZoomChanged.subscribe(self._update_zoom)

        # Hide unnecessary buttons in hud
        for widget in ("build", "speedUp", "speedDown", "destroy_tool",
                       "diplomacyButton", "logbook"):
            self.mainhud.findChild(name=widget).hide()

        self.windows = WindowManager()
        self.message_widget = MessageWidget(self.session)
        self.pausemenu = PauseMenu(self.session,
                                   self,
                                   self.windows,
                                   in_editor_mode=True)
        self.help_dialog = HelpDialog(self.windows)

    def end(self):
        self.mainhud.mapEvents({
            'zoomIn': None,
            'zoomOut': None,
            'rotateRight': None,
            'rotateLeft': None,
            'gameMenuButton': None
        })
        self.mainhud.hide()
        self.mainhud = None
        self._settings_tab.hide()
        self._settings_tab = None

        self.windows.close_all()
        self.minimap = None
        self.keylistener = None
        LastActivePlayerSettlementManager().remove()
        LastActivePlayerSettlementManager.destroy_instance()
        ZoomChanged.unsubscribe(self._update_zoom)

        if self.cursor:
            self.cursor.remove()
            self.cursor.end()
            self.cursor = None

        super(IngameGui, self).end()

    def handle_selection_group(self, num, ctrl_pressed):
        # Someday, maybe cool stuff will be possible here.
        # That day is not today, I'm afraid.
        pass

    def toggle_pause(self):
        self.windows.toggle(self.pausemenu)

    def toggle_help(self):
        self.windows.toggle(self.help_dialog)

    def load(self, savegame):
        self.minimap.draw()

        self.cursor = SelectionTool(self.session)

    def setup(self):
        """Called after the world editor was initialized."""
        self._settings_tab = TabWidget(
            self, tabs=[SettingsTab(self.session.world_editor, self)])
        self._settings_tab.show()

    def minimap_to_front(self):
        """Make sure the full right top gui is visible and not covered by some dialog"""
        self.mainhud.hide()
        self.mainhud.show()

    def show_save_map_dialog(self):
        """Shows a dialog where the user can set the name of the saved map."""
        window = SelectSavegameDialog('editor-save', self.windows)
        savegamename = self.windows.open(window)
        if savegamename is None:
            return False  # user aborted dialog
        success = self.session.save(savegamename)
        if success:
            self.message_widget.add('SAVED_GAME')

    def on_escape(self):
        pass

    def on_key_press(self, action, evt):
        _Actions = KeyConfig._Actions
        if action == _Actions.QUICKSAVE:
            self.session.quicksave()
        if action == _Actions.ESCAPE:
            if self.windows.visible:
                self.windows.on_escape()
            elif hasattr(self.cursor, 'on_escape'):
                self.cursor.on_escape()
            else:
                self.toggle_pause()
        elif action == _Actions.HELP:
            self.toggle_help()
        else:
            return False
        return True

    def set_cursor(self, which='default', *args, **kwargs):
        """Sets the mousetool (i.e. cursor).
		This is done here for encapsulation and control over destructors.
		Further arguments are passed to the mouse tool constructor.
		"""
        self.cursor.remove()
        klass = {'default': SelectionTool, 'tile_layer': TileLayingTool}[which]
        self.cursor = klass(self.session, *args, **kwargs)

    def _update_zoom(self, message):
        """Enable/disable zoom buttons"""
        in_icon = self.mainhud.findChild(name='zoomIn')
        out_icon = self.mainhud.findChild(name='zoomOut')
        if message.zoom == VIEW.ZOOM_MIN:
            out_icon.set_inactive()
        else:
            out_icon.set_active()
        if message.zoom == VIEW.ZOOM_MAX:
            in_icon.set_inactive()
        else:
            in_icon.set_active()
예제 #4
0
class IngameGui(LivingObject):
    """Class handling all the ingame gui events.
	Assumes that only 1 instance is used (class variables)

	@type session: horizons.session.Session
	@param session: instance of session the world belongs to.
	"""

    message_widget = livingProperty()
    minimap = livingProperty()
    keylistener = livingProperty()

    def __init__(self, session):
        super().__init__()
        self.session = session
        assert isinstance(self.session, horizons.session.Session)
        self.settlement = None
        self._old_menu = None

        self.cursor = None
        self.coordinates_tooltip = None

        self.keylistener = IngameKeyListener(self.session)

        self.cityinfo = CityInfo(self)
        LastActivePlayerSettlementManager.create_instance(self.session)

        self.message_widget = MessageWidget(self.session)

        # Windows
        self.windows = WindowManager()
        self.open_popup = self.windows.open_popup
        self.open_error_popup = self.windows.open_error_popup

        self.logbook = LogBook(self.session, self.windows)
        self.players_overview = PlayersOverview(self.session)
        self.players_settlements = PlayersSettlements(self.session)
        self.players_ships = PlayersShips(self.session)

        self.chat_dialog = ChatDialog(self.windows, self.session)
        self.change_name_dialog = ChangeNameDialog(self.windows, self.session)
        self.pausemenu = PauseMenu(self.session,
                                   self,
                                   self.windows,
                                   in_editor_mode=False)
        self.help_dialog = HelpDialog(self.windows)

        # Icon manager
        self.status_icon_manager = StatusIconManager(
            renderer=self.session.view.renderer['GenericRenderer'],
            layer=self.session.view.layers[LAYERS.OBJECTS])
        self.production_finished_icon_manager = ProductionFinishedIconManager(
            renderer=self.session.view.renderer['GenericRenderer'],
            layer=self.session.view.layers[LAYERS.OBJECTS])

        # 'minimap' is the guichan gui around the actual minimap, which is saved
        # in self.minimap
        self.mainhud = load_uh_widget('minimap.xml')
        self.mainhud.position_technique = "right:top"

        icon = self.mainhud.findChild(name="minimap")
        self.minimap = Minimap(
            icon,
            targetrenderer=horizons.globals.fife.targetrenderer,
            imagemanager=horizons.globals.fife.imagemanager,
            session=self.session,
            view=self.session.view)

        def speed_up():
            SpeedUpCommand().execute(self.session)

        def speed_down():
            SpeedDownCommand().execute(self.session)

        self.mainhud.mapEvents({
            'zoomIn':
            self.session.view.zoom_in,
            'zoomOut':
            self.session.view.zoom_out,
            'rotateRight':
            Callback.ChainedCallbacks(self.session.view.rotate_right,
                                      self.minimap.update_rotation),
            'rotateLeft':
            Callback.ChainedCallbacks(self.session.view.rotate_left,
                                      self.minimap.update_rotation),
            'speedUp':
            speed_up,
            'speedDown':
            speed_down,
            'destroy_tool':
            self.toggle_destroy_tool,
            'build':
            self.show_build_menu,
            'diplomacyButton':
            self.show_diplomacy_menu,
            'gameMenuButton':
            self.toggle_pause,
            'logbook':
            lambda: self.windows.toggle(self.logbook)
        })
        self.mainhud.show()

        self._replace_hotkeys_in_widgets()

        self.resource_overview = ResourceOverviewBar(self.session)

        # Register for messages
        SpeedChanged.subscribe(self._on_speed_changed)
        NewDisaster.subscribe(self._on_new_disaster)
        NewSettlement.subscribe(self._on_new_settlement)
        PlayerLevelUpgrade.subscribe(self._on_player_level_upgrade)
        MineEmpty.subscribe(self._on_mine_empty)
        ZoomChanged.subscribe(self._update_zoom)
        GuiAction.subscribe(self._on_gui_click_action)
        GuiHover.subscribe(self._on_gui_hover_action)
        GuiCancelAction.subscribe(self._on_gui_cancel_action)
        # NOTE: This has to be called after the text is replaced!
        LanguageChanged.subscribe(self._on_language_changed)

        self._display_speed(self.session.timer.ticks_per_second)

    def end(self):
        # unsubscribe early, to avoid messages coming in while we're shutting down
        SpeedChanged.unsubscribe(self._on_speed_changed)
        NewDisaster.unsubscribe(self._on_new_disaster)
        NewSettlement.unsubscribe(self._on_new_settlement)
        PlayerLevelUpgrade.unsubscribe(self._on_player_level_upgrade)
        MineEmpty.unsubscribe(self._on_mine_empty)
        ZoomChanged.unsubscribe(self._update_zoom)
        GuiAction.unsubscribe(self._on_gui_click_action)
        GuiHover.unsubscribe(self._on_gui_hover_action)
        GuiCancelAction.unsubscribe(self._on_gui_cancel_action)

        self.mainhud.mapEvents({
            'zoomIn': None,
            'zoomOut': None,
            'rotateRight': None,
            'rotateLeft': None,
            'destroy_tool': None,
            'build': None,
            'diplomacyButton': None,
            'gameMenuButton': None
        })
        self.mainhud.hide()
        self.mainhud = None

        self.windows.close_all()
        self.message_widget = None
        self.minimap = None
        self.resource_overview.end()
        self.resource_overview = None
        self.keylistener = None
        self.cityinfo.end()
        self.cityinfo = None
        self.hide_menu()

        if self.cursor:
            self.cursor.remove()
            self.cursor.end()
            self.cursor = None

        LastActivePlayerSettlementManager().remove()
        LastActivePlayerSettlementManager.destroy_instance()

        self.production_finished_icon_manager.end()
        self.production_finished_icon_manager = None
        self.status_icon_manager.end()
        self.status_icon_manager = None

        super().end()

    def show_select_savegame(self, mode):
        window = SelectSavegameDialog(mode, self.windows)
        return self.windows.open(window)

    def toggle_pause(self):
        self.set_cursor('default')
        self.windows.toggle(self.pausemenu)

    def toggle_help(self):
        self.windows.toggle(self.help_dialog)

    def minimap_to_front(self):
        """Make sure the full right top gui is visible and not covered by some dialog"""
        self.mainhud.hide()
        self.mainhud.show()

    def show_diplomacy_menu(self):
        # check if the menu is already shown
        if getattr(self.get_cur_menu(), 'name', None) == "diplomacy_widget":
            self.hide_menu()
            return

        if not DiplomacyTab.is_useable(self.session.world):
            self.windows.open_popup(
                T("No diplomacy possible"),
                T("Cannot do diplomacy as there are no other players."))
            return

        tab = DiplomacyTab(self, self.session.world)
        self.show_menu(tab)

    def show_multi_select_tab(self, instances):
        tab = TabWidget(self,
                        tabs=[SelectMultiTab(instances)],
                        name='select_multi')
        self.show_menu(tab)

    def show_build_menu(self, update=False):
        """
		@param update: set when build possibilities change (e.g. after inhabitant tier upgrade)
		"""
        # check if build menu is already shown
        if hasattr(self.get_cur_menu(), 'name') and self.get_cur_menu(
        ).name == "build_menu_tab_widget":
            self.hide_menu()

            if not update:  # this was only a toggle call, don't reshow
                return

        self.set_cursor()  # set default cursor for build menu
        self.deselect_all()

        if not any(settlement.owner.is_local_player
                   for settlement in self.session.world.settlements):
            # player has not built any settlements yet. Accessing the build menu at such a point
            # indicates a mistake in the mental model of the user. Display a hint.
            tab = TabWidget(
                self, tabs=[TabInterface(widget="buildtab_no_settlement.xml")])
        else:
            btabs = BuildTab.create_tabs(self.session, self._build)
            tab = TabWidget(self,
                            tabs=btabs,
                            name="build_menu_tab_widget",
                            active_tab=BuildTab.last_active_build_tab)
        self.show_menu(tab)

    def deselect_all(self):
        for instance in self.session.selected_instances:
            instance.get_component(SelectableComponent).deselect()
        self.session.selected_instances.clear()

    def _build(self, building_id, unit=None):
        """Calls the games buildingtool class for the building_id.
		@param building_id: int with the building id that is to be built.
		@param unit: weakref to the unit, that builds (e.g. ship for warehouse)"""
        self.hide_menu()
        self.deselect_all()
        cls = Entities.buildings[building_id]
        if hasattr(cls, 'show_build_menu'):
            cls.show_build_menu()
        self.set_cursor('building', cls, None if unit is None else unit())

    def toggle_road_tool(self):
        if not isinstance(self.cursor, mousetools.BuildingTool
                          ) or self.cursor._class.id != BUILDINGS.TRAIL:
            self._build(BUILDINGS.TRAIL)
        else:
            self.set_cursor()

    def get_cur_menu(self):
        """Returns menu that is currently displayed"""
        return self._old_menu

    def show_menu(self, menu):
        """Shows a menu
		@param menu: str with the guiname or pychan object.
		"""
        if self._old_menu is not None:
            if hasattr(self._old_menu, "remove_remove_listener"):
                self._old_menu.remove_remove_listener(
                    Callback(self.show_menu, None))
            self._old_menu.hide()

        self._old_menu = menu
        if self._old_menu is not None:
            if hasattr(self._old_menu, "add_remove_listener"):
                self._old_menu.add_remove_listener(
                    Callback(self.show_menu, None))
            self._old_menu.show()
            self.minimap_to_front()

        TabWidgetChanged.broadcast(self)

    def hide_menu(self):
        self.show_menu(None)

    def save(self, db):
        self.message_widget.save(db)
        self.logbook.save(db)
        self.resource_overview.save(db)
        LastActivePlayerSettlementManager().save(db)
        self.save_selection(db)

    def save_selection(self, db):
        # Store instances that are selected right now.
        for instance in self.session.selected_instances:
            db("INSERT INTO selected (`group`, id) VALUES (NULL, ?)",
               instance.worldid)

        # If a single instance is selected, also store the currently displayed tab.
        # (Else, upon restoring, we display a multi-selection tab.)
        tabname = None
        if len(self.session.selected_instances) == 1:
            tabclass = self.get_cur_menu().current_tab
            tabname = tabclass.__class__.__name__
        db("INSERT INTO metadata (name, value) VALUES (?, ?)", 'selected_tab',
           tabname)

        # Store user defined unit selection groups (Ctrl+number)
        for (number, group) in enumerate(self.session.selection_groups):
            for instance in group:
                db("INSERT INTO selected (`group`, id) VALUES (?, ?)", number,
                   instance.worldid)

    def load(self, db):
        self.message_widget.load(db)
        self.logbook.load(db)
        self.resource_overview.load(db)

        if self.session.is_game_loaded():
            LastActivePlayerSettlementManager().load(db)
            cur_settlement = LastActivePlayerSettlementManager(
            ).get_current_settlement()
            self.cityinfo.set_settlement(cur_settlement)

        self.minimap.draw()  # update minimap to new world

        self.current_cursor = 'default'
        self.cursor = mousetools.SelectionTool(self.session)
        # Set cursor correctly, menus might need to be opened.
        # Open menus later; they may need unit data not yet inited
        self.cursor.apply_select()

        self.load_selection(db)

        if not self.session.is_game_loaded():
            # Fire a message for new world creation
            self.message_widget.add('NEW_WORLD')

        # Show message when the relationship between players changed
        def notify_change(caller, old_state, new_state, a, b):
            player1 = "{0!s}".format(a.name)
            player2 = "{0!s}".format(b.name)

            data = {'player1': player1, 'player2': player2}

            string_id = 'DIPLOMACY_STATUS_{old}_{new}'.format(
                old=old_state.upper(), new=new_state.upper())
            self.message_widget.add(string_id=string_id, message_dict=data)

        self.session.world.diplomacy.add_diplomacy_status_changed_listener(
            notify_change)

    def load_selection(self, db):
        # Re-select old selected instance
        for (instance_id,
             ) in db("SELECT id FROM selected WHERE `group` IS NULL"):
            obj = WorldObject.get_object_by_id(instance_id)
            self.session.selected_instances.add(obj)
            obj.get_component(SelectableComponent).select()

        # Re-show old tab (if there was one) or multiselection
        if len(self.session.selected_instances) == 1:
            tabname = db("SELECT value FROM metadata WHERE name = ?",
                         'selected_tab')[0][0]
            # This can still be None due to old savegames not storing the information
            tabclass = None if tabname is None else resolve_tab(tabname)
            obj.get_component(SelectableComponent).show_menu(
                jump_to_tabclass=tabclass)
        elif self.session.selected_instances:
            self.show_multi_select_tab(self.session.selected_instances)

        # Load user defined unit selection groups (Ctrl+number)
        for (num, group) in enumerate(self.session.selection_groups):
            for (instance_id, ) in db(
                    "SELECT id FROM selected WHERE `group` = ?", num):
                obj = WorldObject.get_object_by_id(instance_id)
                group.add(obj)

    def show_change_name_dialog(self, instance):
        """Shows a dialog where the user can change the name of an object."""
        self.windows.open(self.change_name_dialog, instance=instance)

    def on_escape(self):
        if self.windows.visible:
            self.windows.on_escape()
        elif hasattr(self.cursor, 'on_escape'):
            self.cursor.on_escape()
        else:
            self.toggle_pause()

        return True

    def on_return(self):
        if self.windows.visible:
            self.windows.on_return()

        return True

    def _on_speed_changed(self, message):
        self._display_speed(message.new)

    def _display_speed(self, tps):
        text = ''
        up_icon = self.mainhud.findChild(name='speedUp')
        down_icon = self.mainhud.findChild(name='speedDown')
        if tps == 0:  # pause
            text = '0x'
            up_icon.set_inactive()
            down_icon.set_inactive()
        else:
            if tps != GAME_SPEED.TICKS_PER_SECOND:
                text = "{0:1g}x".format(tps * 1.0 /
                                        GAME_SPEED.TICKS_PER_SECOND)
                #%1g: displays 0.5x, but 2x instead of 2.0x
            index = GAME_SPEED.TICK_RATES.index(tps)
            if index + 1 >= len(GAME_SPEED.TICK_RATES):
                up_icon.set_inactive()
            else:
                up_icon.set_active()
            if index > 0:
                down_icon.set_active()
            else:
                down_icon.set_inactive()

        wdg = self.mainhud.findChild(name="speed_text")
        wdg.text = text
        wdg.resizeToContent()
        self.mainhud.show()

    def on_key_press(self, action, evt):
        """Handle a key press in-game.

		Returns True if the key was acted upon.
		"""
        _Actions = KeyConfig._Actions
        keyval = evt.getKey().getValue()

        if action == _Actions.ESCAPE:
            return self.on_escape()
        elif keyval == fife.Key.ENTER:
            return self.on_return()

        if action == _Actions.GRID:
            gridrenderer = self.session.view.renderer['GridRenderer']
            gridrenderer.setEnabled(not gridrenderer.isEnabled())
        elif action == _Actions.COORD_TOOLTIP:
            self.coordinates_tooltip.toggle()
        elif action == _Actions.DESTROY_TOOL:
            self.toggle_destroy_tool()
        elif action == _Actions.REMOVE_SELECTED:
            message = T("Are you sure you want to delete these objects?")
            if self.windows.open_popup(T("Delete"),
                                       message,
                                       show_cancel_button=True):
                self.session.remove_selected()
            else:
                self.deselect_all()
        elif action == _Actions.ROAD_TOOL:
            self.toggle_road_tool()
        elif action == _Actions.SPEED_UP:
            SpeedUpCommand().execute(self.session)
        elif action == _Actions.SPEED_DOWN:
            SpeedDownCommand().execute(self.session)
        elif action == _Actions.ZOOM_IN:
            self.session.view.zoom_in()
        elif action == _Actions.ZOOM_OUT:
            self.session.view.zoom_out()
        elif action == _Actions.PAUSE:
            TogglePauseCommand().execute(self.session)
        elif action == _Actions.PLAYERS_OVERVIEW:
            self.logbook.toggle_stats_visibility(widget='players')
        elif action == _Actions.SETTLEMENTS_OVERVIEW:
            self.logbook.toggle_stats_visibility(widget='settlements')
        elif action == _Actions.SHIPS_OVERVIEW:
            self.logbook.toggle_stats_visibility(widget='ships')
        elif action == _Actions.LOGBOOK:
            self.windows.toggle(self.logbook)
        elif action == _Actions.DEBUG and VERSION.IS_DEV_VERSION:
            import pdb
            pdb.set_trace()
        elif action == _Actions.BUILD_TOOL:
            self.show_build_menu()
        elif action == _Actions.ROTATE_RIGHT:
            if hasattr(self.cursor, "rotate_right"):
                # used in e.g. build preview to rotate building instead of map
                self.cursor.rotate_right()
            else:
                self.session.view.rotate_right()
                self.minimap.update_rotation()
        elif action == _Actions.ROTATE_LEFT:
            if hasattr(self.cursor, "rotate_left"):
                self.cursor.rotate_left()
            else:
                self.session.view.rotate_left()
                self.minimap.update_rotation()
        elif action == _Actions.CHAT:
            self.windows.open(self.chat_dialog)
        elif action == _Actions.TRANSLUCENCY:
            self.session.world.toggle_translucency()
        elif action == _Actions.TILE_OWNER_HIGHLIGHT:
            self.session.world.toggle_owner_highlight()
        elif fife.Key.NUM_0 <= keyval <= fife.Key.NUM_9:
            num = int(keyval - fife.Key.NUM_0)
            self.handle_selection_group(num, evt.isControlPressed())
        elif action == _Actions.QUICKSAVE:
            self.session.quicksave()
        # Quickload is only handled by the MainListener.
        elif action == _Actions.PIPETTE:
            # Mode that allows copying buildings.
            self.toggle_cursor('pipette')
        elif action == _Actions.HEALTH_BAR:
            # Show health bar of every instance with a health component.
            self.session.world.toggle_health_for_all_health_instances()
        elif action == _Actions.SHOW_SELECTED:
            if self.session.selected_instances:
                # Scroll to first one, we can never guarantee to display all selected units.
                instance = next(iter(self.session.selected_instances))
                self.session.view.center(*instance.position.center.to_tuple())
                for instance in self.session.selected_instances:
                    if hasattr(instance,
                               "path") and instance.owner.is_local_player:
                        self.minimap.show_unit_path(instance)
        elif action == _Actions.HELP:
            self.toggle_help()
        else:
            return False

        return True

    def handle_selection_group(self, num, ctrl_pressed):
        """Select existing or assign new unit selection group.

		Ctrl+number creates or overwrites the group of number `num`
		with the currently selected units.
		Pressing the associated key selects a group and centers the
		camera around these units.
		"""
        if ctrl_pressed:
            # Only consider units owned by the player.
            units = {
                u
                for u in self.session.selected_instances
                if u.owner.is_local_player
            }
            self.session.selection_groups[num] = units
            # Drop units of the new group from all other groups.
            for group in self.session.selection_groups:
                current_group = self.session.selection_groups[num]
                if group != current_group:
                    group -= current_group
        else:
            # We need to make sure to have a cursor capable of selection
            # for apply_select() to work.
            # This handles deselection implicitly in the destructor.
            self.set_cursor('selection')
            # Apply new selection.
            for instance in self.session.selection_groups[num]:
                instance.get_component(SelectableComponent).select(
                    reset_cam=True)
            # Assign copy since it will be randomly changed in selection code.
            # The unit group itself should only be changed on Ctrl events.
            self.session.selected_instances = self.session.selection_groups[
                num].copy()
            # Show correct tabs depending on what's selected.
            if self.session.selected_instances:
                self.cursor.apply_select()
            else:
                # Nothing is selected here. Hide the menu since apply_select
                # doesn't handle that case.
                self.show_menu(None)

    def toggle_cursor(self, which):
        """Alternate between the cursor *which* and the default cursor."""
        if which == self.current_cursor:
            self.set_cursor()
        else:
            self.set_cursor(which)

    def set_cursor(self, which='default', *args, **kwargs):
        """Sets the mousetool (i.e. cursor).
		This is done here for encapsulation and control over destructors.
		Further arguments are passed to the mouse tool constructor."""
        self.cursor.remove()
        self.current_cursor = which
        klass = {
            'default': mousetools.SelectionTool,
            'selection': mousetools.SelectionTool,
            'tearing': mousetools.TearingTool,
            'pipette': mousetools.PipetteTool,
            'attacking': mousetools.AttackingTool,
            'building': mousetools.BuildingTool,
        }[which]
        self.cursor = klass(self.session, *args, **kwargs)

    def toggle_destroy_tool(self):
        """Initiate the destroy tool"""
        self.toggle_cursor('tearing')

    def _update_zoom(self, message):
        """Enable/disable zoom buttons"""
        in_icon = self.mainhud.findChild(name='zoomIn')
        out_icon = self.mainhud.findChild(name='zoomOut')
        if message.zoom == VIEW.ZOOM_MIN:
            out_icon.set_inactive()
        else:
            out_icon.set_active()
        if message.zoom == VIEW.ZOOM_MAX:
            in_icon.set_inactive()
        else:
            in_icon.set_active()

    def _on_new_disaster(self, message):
        """Called when a building is 'infected' with a disaster."""
        if message.building.owner.is_local_player and len(
                message.disaster._affected_buildings) == 1:
            pos = message.building.position.center
            self.message_widget.add(
                point=pos, string_id=message.disaster_class.NOTIFICATION_TYPE)

    def _on_new_settlement(self, message):
        player = message.settlement.owner
        self.message_widget.add(string_id='NEW_SETTLEMENT',
                                point=message.warehouse_position,
                                message_dict={'player': player.name},
                                play_sound=player.is_local_player)

    def _on_player_level_upgrade(self, message):
        """Called when a player's population reaches a new level."""
        if not message.sender.is_local_player:
            return

        # show notification
        self.message_widget.add(point=message.building.position.center,
                                string_id='SETTLER_LEVEL_UP',
                                message_dict={'level': message.level + 1})

        # update build menu to show new buildings
        menu = self.get_cur_menu()
        if hasattr(menu, "name") and menu.name == "build_menu_tab_widget":
            self.show_build_menu(update=True)

    def _on_mine_empty(self, message):
        self.message_widget.add(point=message.mine.position.center,
                                string_id='MINE_EMPTY')

    def _on_gui_click_action(self, msg):
        """Make a sound when a button is clicked"""
        AmbientSoundComponent.play_special('click', gain=10)

    def _on_gui_cancel_action(self, msg):
        """Make a sound when a cancelButton is clicked"""
        AmbientSoundComponent.play_special('success', gain=10)

    def _on_gui_hover_action(self, msg):
        """Make a sound when the mouse hovers over a button"""
        AmbientSoundComponent.play_special('refresh', position=None, gain=1)

    def _replace_hotkeys_in_widgets(self):
        """Replaces the `{key}` in the (translated) widget helptext with the actual hotkey"""
        hotkey_replacements = {
            'rotateRight': 'ROTATE_RIGHT',
            'rotateLeft': 'ROTATE_LEFT',
            'speedUp': 'SPEED_UP',
            'speedDown': 'SPEED_DOWN',
            'destroy_tool': 'DESTROY_TOOL',
            'build': 'BUILD_TOOL',
            'gameMenuButton': 'ESCAPE',
            'logbook': 'LOGBOOK',
        }
        for (widgetname, action) in hotkey_replacements.items():
            widget = self.mainhud.findChild(name=widgetname)
            keys = horizons.globals.fife.get_keys_for_action(action)
            # No `.upper()` here: "Pause" looks better than "PAUSE".
            keyname = HOTKEYS.DISPLAY_KEY.get(keys[0], keys[0].capitalize())
            widget.helptext = widget.helptext.format(key=keyname)

    def _on_language_changed(self, msg):
        """Replace the hotkeys after translation.

		NOTE: This should be called _after_ the texts are replaced. This
		currently relies on import order with `horizons.gui`.
		"""
        self._replace_hotkeys_in_widgets()
예제 #5
0
class IngameGui(LivingObject):
    """Class handling all the ingame gui events.
	Assumes that only 1 instance is used (class variables)"""

    gui = livingProperty()
    tabwidgets = livingProperty()
    message_widget = livingProperty()
    minimap = livingProperty()

    styles = {
        'city_info': 'resource_bar',
        'change_name': 'book',
        'save_map': 'book',
        'chat': 'book',
    }

    def __init__(self, session, gui):
        super(IngameGui, self).__init__()
        self.session = session
        assert isinstance(self.session, horizons.session.Session)
        self.main_gui = gui
        self.main_widget = None
        self.tabwidgets = {}
        self.settlement = None
        self.resource_source = None
        self.resources_needed, self.resources_usable = {}, {}
        self._old_menu = None

        self.widgets = LazyWidgetsDict(self.styles, center_widgets=False)

        self.cityinfo = self.widgets['city_info']
        self.cityinfo.child_finder = PychanChildFinder(self.cityinfo)

        self.logbook = LogBook(self.session)
        self.message_widget = MessageWidget(self.session)
        self.players_overview = PlayersOverview(self.session)
        self.players_settlements = PlayersSettlements(self.session)
        self.players_ships = PlayersShips(self.session)

        # self.widgets['minimap'] is the guichan gui around the actual minimap,
        # which is saved in self.minimap
        minimap = self.widgets['minimap']
        minimap.position_technique = "right+0:top+0"

        icon = minimap.findChild(name="minimap")
        self.minimap = Minimap(
            icon,
            targetrenderer=horizons.globals.fife.targetrenderer,
            imagemanager=horizons.globals.fife.imagemanager,
            session=self.session,
            view=self.session.view)

        def speed_up():
            SpeedUpCommand().execute(self.session)

        def speed_down():
            SpeedDownCommand().execute(self.session)

        minimap.mapEvents({
            'zoomIn':
            self.session.view.zoom_in,
            'zoomOut':
            self.session.view.zoom_out,
            'rotateRight':
            Callback.ChainedCallbacks(self.session.view.rotate_right,
                                      self.minimap.rotate_right),
            'rotateLeft':
            Callback.ChainedCallbacks(self.session.view.rotate_left,
                                      self.minimap.rotate_left),
            'speedUp':
            speed_up,
            'speedDown':
            speed_down,
            'destroy_tool':
            self.session.toggle_destroy_tool,
            'build':
            self.show_build_menu,
            'diplomacyButton':
            self.show_diplomacy_menu,
            'gameMenuButton':
            self.main_gui.toggle_pause,
            'logbook':
            self.logbook.toggle_visibility
        })
        minimap.show()
        #minimap.position_technique = "right+15:top+153"

        self.widgets['tooltip'].hide()

        self.resource_overview = ResourceOverviewBar(self.session)
        ResourceBarResize.subscribe(self._on_resourcebar_resize)

        # Register for messages
        SettlerUpdate.subscribe(self._on_settler_level_change)
        SettlerInhabitantsChanged.subscribe(self._on_settler_inhabitant_change)
        HoverSettlementChanged.subscribe(self._cityinfo_set)

    def _on_resourcebar_resize(self, message):
        self._update_cityinfo_position()

    def end(self):
        self.widgets['minimap'].mapEvents({
            'zoomIn': None,
            'zoomOut': None,
            'rotateRight': None,
            'rotateLeft': None,
            'destroy_tool': None,
            'build': None,
            'diplomacyButton': None,
            'gameMenuButton': None
        })

        for w in self.widgets.itervalues():
            if w.parent is None:
                w.hide()
        self.message_widget = None
        self.tabwidgets = None
        self.minimap = None
        self.resource_overview.end()
        self.resource_overview = None
        self.hide_menu()
        SettlerUpdate.unsubscribe(self._on_settler_level_change)
        ResourceBarResize.unsubscribe(self._on_resourcebar_resize)
        HoverSettlementChanged.unsubscribe(self._cityinfo_set)
        SettlerInhabitantsChanged.unsubscribe(
            self._on_settler_inhabitant_change)

        super(IngameGui, self).end()

    def _cityinfo_set(self, message):
        """Sets the city name at top center of screen.

		Show/Hide is handled automatically
		To hide cityname, set name to ''
		@param message: HoverSettlementChanged message
		"""
        settlement = message.settlement
        old_was_player_settlement = False
        if self.settlement is not None:
            self.settlement.remove_change_listener(self.update_settlement)
            old_was_player_settlement = self.settlement.owner.is_local_player

        # save reference to new "current" settlement in self.settlement
        self.settlement = settlement

        if settlement is None:  # we want to hide the widget now (but perhaps delayed).
            if old_was_player_settlement:
                # After scrolling away from settlement, leave name on screen for some
                # seconds. Players can still click on it to rename the settlement now.
                ExtScheduler().add_new_object(self.cityinfo.hide,
                                              self,
                                              run_in=GUI.CITYINFO_UPDATE_DELAY)
                #TODO 'click to rename' tooltip of cityinfo can stay visible in
                # certain cases if cityinfo gets hidden in tooltip delay buffer.
            else:
                # hovered settlement of other player, simply hide the widget
                self.cityinfo.hide()

        else:  # do not hide if settlement is hovered and a hide was previously scheduled
            ExtScheduler().rem_call(self, self.cityinfo.hide)

            self.update_settlement()  # calls show()
            settlement.add_change_listener(self.update_settlement)

    def _on_settler_inhabitant_change(self, message):
        assert isinstance(message, SettlerInhabitantsChanged)
        foundlabel = self.cityinfo.child_finder('city_inhabitants')
        old_amount = int(foundlabel.text) if foundlabel.text else 0
        foundlabel.text = u' {amount:>4d}'.format(amount=old_amount +
                                                  message.change)
        foundlabel.resizeToContent()

    def update_settlement(self):
        city_name_label = self.cityinfo.child_finder('city_name')
        if self.settlement.owner.is_local_player:  # allow name changes
            # Update settlement on the resource overview to make sure it
            # is setup correctly for the coming calculations
            self.resource_overview.set_inventory_instance(self.settlement)
            cb = Callback(self.show_change_name_dialog, self.settlement)
            helptext = _("Click to change the name of your settlement")
            city_name_label.enable_cursor_change_on_hover()
        else:  # no name changes
            cb = lambda: AmbientSoundComponent.play_special('error')
            helptext = u""
            city_name_label.disable_cursor_change_on_hover()
        self.cityinfo.mapEvents({'city_name': cb})
        city_name_label.helptext = helptext

        foundlabel = self.cityinfo.child_finder('owner_emblem')
        foundlabel.image = 'content/gui/images/tabwidget/emblems/emblem_%s.png' % (
            self.settlement.owner.color.name)
        foundlabel.helptext = self.settlement.owner.name

        foundlabel = self.cityinfo.child_finder('city_name')
        foundlabel.text = self.settlement.get_component(
            SettlementNameComponent).name
        foundlabel.resizeToContent()

        foundlabel = self.cityinfo.child_finder('city_inhabitants')
        foundlabel.text = u' {amount:>4d}'.format(
            amount=self.settlement.inhabitants)
        foundlabel.resizeToContent()

        self._update_cityinfo_position()

    def _update_cityinfo_position(self):
        """ Places cityinfo widget depending on resource bar dimensions.

		For a normal-sized resource bar and reasonably large resolution:
		* determine resource bar length (includes gold)
		* determine empty horizontal space between resbar end and minimap start
		* display cityinfo centered in that area if it is sufficiently large

		If too close to the minimap (cityinfo larger than length of this empty space)
		move cityinfo centered to very upper screen edge. Looks bad, works usually.
		In this case, the resbar is redrawn to put the cityinfo "behind" it visually.
		"""
        width = horizons.globals.fife.engine_settings.getScreenWidth()
        resbar = self.resource_overview.get_size()
        is_foreign = (self.settlement.owner != self.session.world.player)
        blocked = self.cityinfo.size[0] + int(1.5 * self.minimap.get_size()[1])
        # minimap[1] returns width! Use 1.5*width because of the GUI around it

        if is_foreign:  # other player, no resbar exists
            self.cityinfo.pos = ('center', 'top')
            xoff = 0
            yoff = 19
        elif blocked < width < resbar[
                0] + blocked:  # large resbar / small resolution
            self.cityinfo.pos = ('center', 'top')
            xoff = 0
            yoff = 0  # upper screen edge
        else:
            self.cityinfo.pos = ('left', 'top')
            xoff = resbar[0] + (width - blocked - resbar[0]) // 2
            yoff = 24

        self.cityinfo.offset = (xoff, yoff)
        self.cityinfo.position_technique = "{pos[0]}{off[0]:+d}:{pos[1]}{off[1]:+d}".format(
            pos=self.cityinfo.pos, off=self.cityinfo.offset)
        self.cityinfo.hide()
        self.cityinfo.show()

    def minimap_to_front(self):
        """Make sure the full right top gui is visible and not covered by some dialog"""
        self.widgets['minimap'].hide()
        self.widgets['minimap'].show()

    def show_diplomacy_menu(self):
        # check if the menu is already shown
        if getattr(self.get_cur_menu(), 'name', None) == "diplomacy_widget":
            self.hide_menu()
            return

        if not DiplomacyTab.is_useable(self.session.world):
            self.main_gui.show_popup(
                _("No diplomacy possible"),
                _("Cannot do diplomacy as there are no other players."))
            return

        tab = DiplomacyTab(self, self.session.world)
        self.show_menu(tab)

    def show_multi_select_tab(self):
        tab = TabWidget(self,
                        tabs=[SelectMultiTab(self.session)],
                        name='select_multi')
        self.show_menu(tab)

    def show_build_menu(self, update=False):
        """
		@param update: set when build possiblities change (e.g. after settler upgrade)
		"""
        # check if build menu is already shown
        if hasattr(self.get_cur_menu(), 'name') and self.get_cur_menu(
        ).name == "build_menu_tab_widget":
            self.hide_menu()

            if not update:  # this was only a toggle call, don't reshow
                return

        self.session.set_cursor()  # set default cursor for build menu
        self.deselect_all()

        if not any(settlement.owner.is_local_player
                   for settlement in self.session.world.settlements):
            # player has not built any settlements yet. Accessing the build menu at such a point
            # indicates a mistake in the mental model of the user. Display a hint.
            tab = TabWidget(
                self, tabs=[TabInterface(widget="buildtab_no_settlement.xml")])
        else:
            btabs = BuildTab.create_tabs(self.session, self._build)
            tab = TabWidget(self,
                            tabs=btabs,
                            name="build_menu_tab_widget",
                            active_tab=BuildTab.last_active_build_tab)
        self.show_menu(tab)

    def deselect_all(self):
        for instance in self.session.selected_instances:
            instance.get_component(SelectableComponent).deselect()
        self.session.selected_instances.clear()

    def _build(self, building_id, unit=None):
        """Calls the games buildingtool class for the building_id.
		@param building_id: int with the building id that is to be built.
		@param unit: weakref to the unit, that builds (e.g. ship for warehouse)"""
        self.hide_menu()
        self.deselect_all()
        cls = Entities.buildings[building_id]
        if hasattr(cls, 'show_build_menu'):
            cls.show_build_menu()
        self.session.set_cursor('building', cls,
                                None if unit is None else unit())

    def toggle_road_tool(self):
        if not isinstance(
                self.session.cursor, BuildingTool
        ) or self.session.cursor._class.id != BUILDINGS.TRAIL:
            self._build(BUILDINGS.TRAIL)
        else:
            self.session.set_cursor()

    def _get_menu_object(self, menu):
        """Returns pychan object if menu is a string, else returns menu
		@param menu: str with the guiname or pychan object.
		"""
        if isinstance(menu, str):
            menu = self.widgets[menu]
        return menu

    def get_cur_menu(self):
        """Returns menu that is currently displayed"""
        return self._old_menu

    def show_menu(self, menu):
        """Shows a menu
		@param menu: str with the guiname or pychan object.
		"""
        if self._old_menu is not None:
            if hasattr(self._old_menu, "remove_remove_listener"):
                self._old_menu.remove_remove_listener(
                    Callback(self.show_menu, None))
            self._old_menu.hide()

        self._old_menu = self._get_menu_object(menu)
        if self._old_menu is not None:
            if hasattr(self._old_menu, "add_remove_listener"):
                self._old_menu.add_remove_listener(
                    Callback(self.show_menu, None))
            self._old_menu.show()
            self.minimap_to_front()

        TabWidgetChanged.broadcast(self)

    def hide_menu(self):
        self.show_menu(None)

    def toggle_menu(self, menu):
        """Shows a menu or hides it if it is already displayed.
		@param menu: parameter supported by show_menu().
		"""
        if self.get_cur_menu() == self._get_menu_object(menu):
            self.hide_menu()
        else:
            self.show_menu(menu)

    def save(self, db):
        self.message_widget.save(db)
        self.logbook.save(db)
        self.resource_overview.save(db)

    def load(self, db):
        self.message_widget.load(db)
        self.logbook.load(db)
        self.resource_overview.load(db)

        cur_settlement = LastActivePlayerSettlementManager(
        ).get_current_settlement()
        self._cityinfo_set(HoverSettlementChanged(self, cur_settlement))

        self.minimap.draw()  # update minimap to new world

    def show_change_name_dialog(self, instance):
        """Shows a dialog where the user can change the name of a NamedComponant.
		The game gets paused while the dialog is executed."""
        events = {
            OkButton.DEFAULT_NAME: Callback(self.change_name, instance),
            CancelButton.DEFAULT_NAME: self._hide_change_name_dialog
        }
        self.main_gui.on_escape = self._hide_change_name_dialog
        changename = self.widgets['change_name']
        oldname = changename.findChild(name='old_name')
        oldname.text = instance.get_component(SettlementNameComponent).name
        newname = changename.findChild(name='new_name')
        changename.mapEvents(events)
        newname.capture(Callback(self.change_name, instance))

        def forward_escape(event):
            # the textfield will eat everything, even control events
            if event.getKey().getValue() == fife.Key.ESCAPE:
                self.main_gui.on_escape()

        newname.capture(forward_escape, "keyPressed")

        changename.show()
        newname.requestFocus()

    def _hide_change_name_dialog(self):
        """Escapes the change_name dialog"""
        self.main_gui.on_escape = self.main_gui.toggle_pause
        self.widgets['change_name'].hide()

    def change_name(self, instance):
        """Applies the change_name dialogs input and hides it.
		If the new name has length 0 or only contains blanks, the old name is kept.
		"""
        new_name = self.widgets['change_name'].collectData('new_name')
        self.widgets['change_name'].findChild(name='new_name').text = u''
        if not new_name or not new_name.isspace():
            # different namedcomponent classes share the name
            RenameObject(instance.get_component_by_name(NamedComponent.NAME),
                         new_name).execute(self.session)
        self._hide_change_name_dialog()

    def show_save_map_dialog(self):
        """Shows a dialog where the user can set the name of the saved map."""
        events = {
            OkButton.DEFAULT_NAME: self.save_map,
            CancelButton.DEFAULT_NAME: self._hide_save_map_dialog
        }
        self.main_gui.on_escape = self._hide_save_map_dialog
        dialog = self.widgets['save_map']
        name = dialog.findChild(name='map_name')
        name.text = u''
        dialog.mapEvents(events)
        name.capture(Callback(self.save_map))
        dialog.show()
        name.requestFocus()

    def _hide_save_map_dialog(self):
        """Closes the map saving dialog."""
        self.main_gui.on_escape = self.main_gui.toggle_pause
        self.widgets['save_map'].hide()

    def save_map(self):
        """Saves the map and hides the dialog."""
        name = self.widgets['save_map'].collectData('map_name')
        if re.match('^[a-zA-Z0-9_-]+$', name):
            self.session.save_map(name)
            self._hide_save_map_dialog()
        else:
            #xgettext:python-format
            message = _(
                'Valid map names are in the following form: {expression}'
            ).format(expression='[a-zA-Z0-9_-]+')
            #xgettext:python-format
            advice = _('Try a name that only contains letters and numbers.')
            self.session.gui.show_error_popup(_('Error'), message, advice)

    def on_escape(self):
        if self.main_widget:
            self.main_widget.hide()
        else:
            return False
        return True

    def on_switch_main_widget(self, widget):
        """The main widget has been switched to the given one (possibly None)."""
        if self.main_widget and self.main_widget != widget:  # close the old one if it exists
            old_main_widget = self.main_widget
            self.main_widget = None
            old_main_widget.hide()
        self.main_widget = widget

    def display_game_speed(self, text):
        """
		@param text: unicode string to display as speed value
		"""
        wdg = self.widgets['minimap'].findChild(name="speed_text")
        wdg.text = text
        wdg.resizeToContent()
        self.widgets['minimap'].show()

    def _on_settler_level_change(self, message):
        """Gets called when the player changes"""
        if message.sender.owner.is_local_player:
            menu = self.get_cur_menu()
            if hasattr(menu, "name") and menu.name == "build_menu_tab_widget":
                # player changed and build menu is currently displayed
                self.show_build_menu(update=True)

            # TODO: Use a better measure then first tab
            # Quite fragile, makes sure the tablist in the mainsquare menu is updated
            if hasattr(menu, '_tabs') and isinstance(menu._tabs[0],
                                                     MainSquareOverviewTab):
                instance = list(self.session.selected_instances)[0]
                instance.get_component(SelectableComponent).show_menu(
                    jump_to_tabclass=type(menu.current_tab))

    def show_chat_dialog(self):
        """Show a dialog where the user can enter a chat message"""
        events = {
            OkButton.DEFAULT_NAME: self._do_chat,
            CancelButton.DEFAULT_NAME: self._hide_chat_dialog
        }
        self.main_gui.on_escape = self._hide_chat_dialog

        self.widgets['chat'].mapEvents(events)

        def forward_escape(event):
            # the textfield will eat everything, even control events
            if event.getKey().getValue() == fife.Key.ESCAPE:
                self.main_gui.on_escape()

        self.widgets['chat'].findChild(name='msg').capture(
            forward_escape, "keyPressed")
        self.widgets['chat'].findChild(name='msg').capture(self._do_chat)
        self.widgets['chat'].show()
        self.widgets['chat'].findChild(name="msg").requestFocus()

    def _hide_chat_dialog(self):
        """Escapes the chat dialog"""
        self.main_gui.on_escape = self.main_gui.toggle_pause
        self.widgets['chat'].hide()

    def _do_chat(self):
        """Actually initiates chatting and hides the dialog"""
        msg = self.widgets['chat'].findChild(name='msg').text
        Chat(msg).execute(self.session)
        self.widgets['chat'].findChild(name='msg').text = u''
        self._hide_chat_dialog()
예제 #6
0
class Session(LivingObject):
    """The Session class represents the game's main ingame view and controls cameras and map loading.
	It is alive as long as a game is running.
	Many objects require a reference to this, which makes it a pseudo-global, from which we would
	like to move away in the long term. This is where we hope the components come into play, which
	you will encounter later.

	This is the most important class if you are going to hack on Unknown Horizons; it provides most of
	the important ingame variables.
	Here's a small list of commonly used attributes:

	* world - horizons.world instance of the currently running horizons. Stores players and islands,
		which store settlements, which store buildings, which have productions and collectors.
		Therefore, world deserves its name -- it contains the whole game state.
	* scheduler - horizons.scheduler instance. Used to execute timed events. Master of time in UH.
	* manager - horizons.manager instance. Used to execute commands (used to apply user interactions).
		There is a singleplayer and a multiplayer version. Our mp system works by the mp-manager not
		executing the commands directly, but sending them to all players, where they will be executed
		at the same tick.
	* view - horizons.view instance. Used to control the ingame camera.
	* ingame_gui - horizons.gui.ingame_gui instance. Used to control the ingame gui framework.
		(This is different from gui, which is the main menu and general session-independent gui)
	* cursor - horizons.gui.{navigation/cursor/selection/building}tool instance. Used to handle
			   mouse events.
	* selected_instances - Set that holds the currently selected instances (building, units).

	TUTORIAL:
	For further digging you should now be checking out the load() function.
	"""
    timer = livingProperty()
    manager = livingProperty()
    view = livingProperty()
    ingame_gui = livingProperty()
    keylistener = livingProperty()
    scenario_eventhandler = livingProperty()

    log = logging.getLogger('session')

    def __init__(self, gui, db, rng_seed=None):
        super(Session, self).__init__()
        assert isinstance(db, horizons.util.uhdbaccessor.UhDbAccessor)
        self.log.debug("Initing session")
        self.gui = gui  # main gui, not ingame gui
        self.db = db  # main db for game data (game.sql)
        # this saves how often the current game has been saved
        self.savecounter = 0
        self.is_alive = True

        self._clear_caches()

        #game
        self.random = self.create_rng(rng_seed)
        assert isinstance(self.random, Random)
        self.timer = self.create_timer()
        Scheduler.create_instance(self.timer)
        self.manager = self.create_manager()
        self.view = View(self)
        Entities.load(self.db)
        self.scenario_eventhandler = ScenarioEventHandler(
            self)  # dummy handler with no events

        #GUI
        self.gui.session = self
        self.ingame_gui = IngameGui(self, self.gui)
        self.keylistener = IngameKeyListener(self)
        self.coordinates_tooltip = None
        self.display_speed()
        LastActivePlayerSettlementManager.create_instance(self)

        self.status_icon_manager = StatusIconManager(
            renderer=self.view.renderer['GenericRenderer'],
            layer=self.view.layers[LAYERS.OBJECTS])
        self.production_finished_icon_manager = None
        self.create_production_finished_icon_manager()

        self.selected_instances = set()
        self.selection_groups = [
            set() for _ in range(10)
        ]  # List of sets that holds the player assigned unit groups.

        self._old_autosave_interval = None

    def in_editor_mode(self):
        return False

    def create_production_finished_icon_manager(self):
        """ Checks the settings if we should display resrouce icons.
		If True: Create the ProductionFinishedIconManager
		If False and a manager is currently running: End it
		"""
        show_resource_icons = bool(
            horizons.globals.fife.get_uh_setting("ShowResourceIcons"))
        if show_resource_icons:
            self.production_finished_icon_manager = ProductionFinishedIconManager(
                renderer=self.view.renderer['GenericRenderer'],
                layer=self.view.layers[LAYERS.OBJECTS])
        else:
            self.end_production_finished_icon_manager()

    def start(self):
        """Actually starts the game."""
        self.timer.activate()
        self.reset_autosave()
        AutosaveIntervalChanged.subscribe(self._on_autosave_interval_changed)

    def reset_autosave(self):
        """(Re-)Set up autosave. Called if autosave interval has been changed."""
        # get_uh_setting returns floats like 4.0 and 42.0 since slider stepping is 1.0.
        interval = int(
            horizons.globals.fife.get_uh_setting("AutosaveInterval"))
        if interval != self._old_autosave_interval:
            self._old_autosave_interval = interval
            ExtScheduler().rem_call(self, self.autosave)
            if interval != 0:  #autosave
                self.log.debug("Initing autosave every %s minutes", interval)
                ExtScheduler().add_new_object(self.autosave, self,
                                              interval * 60, -1)

    def _on_autosave_interval_changed(self, message):
        self.reset_autosave()

    def create_manager(self):
        """Returns instance of command manager (currently MPManager or SPManager)"""
        raise NotImplementedError

    def create_rng(self, seed=None):
        """Returns a RNG (random number generator). Must support the python random.Random interface"""
        raise NotImplementedError

    def create_timer(self):
        """Returns a Timer instance."""
        raise NotImplementedError

    @classmethod
    def _clear_caches(cls):
        """Clear all data caches in global namespace related to a session"""
        WorldObject.reset()
        NamedComponent.reset()
        AIPlayer.clear_caches()
        SelectableBuildingComponent.reset()

    def end_production_finished_icon_manager(self):
        if self.production_finished_icon_manager is not None:
            self.production_finished_icon_manager.end()
            self.production_finished_icon_manager = None

    def end(self):
        self.log.debug("Ending session")
        self.is_alive = False

        self.gui.session = None

        # Has to be done here, cause the manager uses Scheduler!
        self.end_production_finished_icon_manager()
        Scheduler().rem_all_classinst_calls(self)
        ExtScheduler().rem_all_classinst_calls(self)

        if horizons.globals.fife.get_fife_setting("PlaySounds"):
            for emitter in horizons.globals.fife.sound.emitter['ambient'][:]:
                emitter.stop()
                horizons.globals.fife.sound.emitter['ambient'].remove(emitter)
            horizons.globals.fife.sound.emitter['effects'].stop()
            horizons.globals.fife.sound.emitter['speech'].stop()
        if hasattr(self, "cursor"):  # the line below would crash uglily on ^C
            self.cursor.remove()

        if hasattr(self, 'cursor') and self.cursor is not None:
            self.cursor.end()
        # these will call end() if the attribute still exists by the LivingObject magic
        self.ingame_gui = None  # keep this before world

        LastActivePlayerSettlementManager().remove()  # keep after ingame_gui
        LastActivePlayerSettlementManager.destroy_instance()

        self.cursor = None
        self.world.end()  # must be called before the world ref is gone
        self.world = None
        self.keylistener = None
        self.view = None
        self.manager = None
        self.timer = None
        self.scenario_eventhandler = None

        Scheduler().end()
        Scheduler.destroy_instance()

        self.selected_instances = None
        self.selection_groups = None

        self.status_icon_manager.end()
        self.status_icon_manager = None

        horizons.main._modules.session = None
        self._clear_caches()

        # subscriptions shouldn't survive listeners (except the main Gui)
        self.gui.unsubscribe()
        AutosaveIntervalChanged.unsubscribe(self._on_autosave_interval_changed)
        MessageBus().reset()
        self.gui.subscribe()

    def toggle_cursor(self, which, *args, **kwargs):
        """Alternate between the cursor which and default.
		args and kwargs are used to construct which."""
        if self.current_cursor == which:
            self.set_cursor()
        else:
            self.set_cursor(which, *args, **kwargs)

    def set_cursor(self, which='default', *args, **kwargs):
        """Sets the mousetool (i.e. cursor).
		This is done here for encapsulation and control over destructors.
		Further arguments are passed to the mouse tool constructor."""
        self.cursor.remove()
        self.current_cursor = which
        klass = {
            'default': SelectionTool,
            'selection': SelectionTool,
            'tearing': TearingTool,
            'pipette': PipetteTool,
            'attacking': AttackingTool,
            'building': BuildingTool,
            'tile_layer': TileLayingTool
        }[which]
        self.cursor = klass(self, *args, **kwargs)

    def toggle_destroy_tool(self):
        """Initiate the destroy tool"""
        self.toggle_cursor('tearing')

    def autosave(self):
        raise NotImplementedError

    def quicksave(self):
        raise NotImplementedError

    def quickload(self):
        raise NotImplementedError

    def save(self, savegame=None):
        raise NotImplementedError

    def load(self, options):
        """Loads a map. Key method for starting a game."""
        """
		TUTORIAL: Here you see how the vital game elements (and some random things that are also required)
		are initialised.
		"""
        if options.is_scenario:
            # game_identifier is a yaml file, that contains reference to actual map file
            self.scenario_eventhandler = ScenarioEventHandler(
                self, options.game_identifier)
            # scenario maps can be normal maps or scenario maps:
            map_filename = self.scenario_eventhandler.get_map_file()
            options.game_identifier = os.path.join(
                SavegameManager.scenario_maps_dir, map_filename)
            if not os.path.exists(options.game_identifier):
                options.game_identifier = os.path.join(
                    SavegameManager.maps_dir, map_filename)
            options.is_map = True

        self.log.debug("Session: Loading from %s", options.game_identifier)
        savegame_db = SavegameAccessor(
            options.game_identifier, options.is_map)  # Initialize new dbreader
        savegame_data = SavegameManager.get_metadata(savegame_db.db_path)
        self.view.resize_layers(savegame_db)

        # load how often the game has been saved (used to know the difference between
        # a loaded and a new game)
        self.savecounter = savegame_data.get('savecounter', 0)

        if savegame_data.get('rng_state', None):
            rng_state_list = json.loads(savegame_data['rng_state'])

            # json treats tuples as lists, but we need tuples here, so convert back
            def rec_list_to_tuple(x):
                if isinstance(x, list):
                    return tuple(rec_list_to_tuple(i) for i in x)
                else:
                    return x

            rng_state_tuple = rec_list_to_tuple(rng_state_list)
            # changing the rng is safe for mp, as all players have to have the same map
            self.random.setstate(rng_state_tuple)

        self.world = World(
            self
        )  # Load horizons.world module (check horizons/world/__init__.py)
        self.world._init(savegame_db,
                         options.force_player_id,
                         disasters_enabled=options.disasters_enabled)
        self.view.load(savegame_db)  # load view
        if not self.is_game_loaded():
            options.init_new_world(self)
        else:
            # try to load scenario data
            self.scenario_eventhandler.load(savegame_db)
        self.manager.load(
            savegame_db
        )  # load the manager (there might me old scheduled ticks).
        self.world.init_fish_indexer()  # now the fish should exist
        if self.is_game_loaded():
            LastActivePlayerSettlementManager().load(
                savegame_db)  # before ingamegui
        self.ingame_gui.load(
            savegame_db)  # load the old gui positions and stuff

        for instance_id in savegame_db(
                "SELECT id FROM selected WHERE `group` IS NULL"
        ):  # Set old selected instance
            obj = WorldObject.get_object_by_id(instance_id[0])
            self.selected_instances.add(obj)
            obj.get_component(SelectableComponent).select()
        for group in xrange(len(
                self.selection_groups)):  # load user defined unit groups
            for instance_id in savegame_db(
                    "SELECT id FROM selected WHERE `group` = ?", group):
                self.selection_groups[group].add(
                    WorldObject.get_object_by_id(instance_id[0]))

        # cursor has to be inited last, else player interacts with a not inited world with it.
        self.current_cursor = 'default'
        self.cursor = SelectionTool(self)
        # Set cursor correctly, menus might need to be opened.
        # Open menus later; they may need unit data not yet inited
        self.cursor.apply_select()

        Scheduler().before_ticking()
        savegame_db.close()

        assert hasattr(self.world, "player"), 'Error: there is no human player'
        """
		TUTORIAL:
		That's it. After that, we call start() to activate the timer, and we're live.
		From here on you should dig into the classes that are loaded above, especially the world class
		(horizons/world/__init__.py). It's where the magic happens and all buildings and units are loaded.
		"""

    def speed_set(self, ticks, suggestion=False):
        """Set game speed to ticks ticks per second"""
        old = self.timer.ticks_per_second
        self.timer.ticks_per_second = ticks
        self.view.map.setTimeMultiplier(
            float(ticks) / float(GAME_SPEED.TICKS_PER_SECOND))
        if old == 0 and self.timer.tick_next_time is None:  # back from paused state
            if self.paused_time_missing is None:
                # happens if e.g. a dialog pauses the game during startup on hotkeypress
                self.timer.tick_next_time = time.time()
            else:
                self.timer.tick_next_time = time.time() + (
                    self.paused_time_missing / ticks)
        elif ticks == 0 or self.timer.tick_next_time is None:
            # go into paused state or very early speed change (before any tick)
            if self.timer.tick_next_time is not None:
                self.paused_time_missing = (self.timer.tick_next_time -
                                            time.time()) * old
            else:
                self.paused_time_missing = None
            self.timer.tick_next_time = None
        else:
            """
			Under odd circumstances (anti-freeze protection just activated, game speed
			decremented multiple times within this frame) this can delay the next tick
			by minutes. Since the positive effects of the code aren't really observeable,
			this code is commented out and possibly will be removed.

			# correct the time until the next tick starts
			time_to_next_tick = self.timer.tick_next_time - time.time()
			if time_to_next_tick > 0: # only do this if we aren't late
				self.timer.tick_next_time += (time_to_next_tick * old / ticks)
			"""
        self.display_speed()

    def display_speed(self):
        text = u''
        up_icon = self.ingame_gui.widgets['minimap'].findChild(name='speedUp')
        down_icon = self.ingame_gui.widgets['minimap'].findChild(
            name='speedDown')
        tps = self.timer.ticks_per_second
        if tps == 0:  # pause
            text = u'0x'
            up_icon.set_inactive()
            down_icon.set_inactive()
        else:
            if tps != GAME_SPEED.TICKS_PER_SECOND:
                text = unicode("%1gx" %
                               (tps * 1.0 / GAME_SPEED.TICKS_PER_SECOND))
                #%1g: displays 0.5x, but 2x instead of 2.0x
            index = GAME_SPEED.TICK_RATES.index(tps)
            if index + 1 >= len(GAME_SPEED.TICK_RATES):
                up_icon.set_inactive()
            else:
                up_icon.set_active()
            if index > 0:
                down_icon.set_active()
            else:
                down_icon.set_inactive()
        self.ingame_gui.display_game_speed(text)

    def speed_up(self):
        if self.speed_is_paused():
            AmbientSoundComponent.play_special('error')
            return
        if self.timer.ticks_per_second in GAME_SPEED.TICK_RATES:
            i = GAME_SPEED.TICK_RATES.index(self.timer.ticks_per_second)
            if i + 1 < len(GAME_SPEED.TICK_RATES):
                self.speed_set(GAME_SPEED.TICK_RATES[i + 1])
        else:
            self.speed_set(GAME_SPEED.TICK_RATES[0])

    def speed_down(self):
        if self.speed_is_paused():
            AmbientSoundComponent.play_special('error')
            return
        if self.timer.ticks_per_second in GAME_SPEED.TICK_RATES:
            i = GAME_SPEED.TICK_RATES.index(self.timer.ticks_per_second)
            if i > 0:
                self.speed_set(GAME_SPEED.TICK_RATES[i - 1])
        else:
            self.speed_set(GAME_SPEED.TICK_RATES[0])

    _pause_stack = 0  # this saves the level of pausing

    # e.g. if two dialogs are displayed, that pause the game,
    # unpause needs to be called twice to unpause the game. cf. #876
    def speed_pause(self, suggestion=False):
        self.log.debug("Session: Pausing")
        self._pause_stack += 1
        if not self.speed_is_paused():
            self.paused_ticks_per_second = self.timer.ticks_per_second
            self.speed_set(0, suggestion)

    def speed_unpause(self, suggestion=False):
        self.log.debug("Session: Unpausing")
        if self.speed_is_paused():
            self._pause_stack -= 1
            if self._pause_stack == 0:
                self.speed_set(self.paused_ticks_per_second)

                # check if resource icons should be displayed (possible changes in settings)
                self.create_production_finished_icon_manager()

    def speed_toggle_pause(self, suggestion=False):
        if self.speed_is_paused():
            self.speed_unpause(suggestion)
        else:
            self.speed_pause(suggestion)

    def speed_is_paused(self):
        return (self.timer.ticks_per_second == 0)

    def is_game_loaded(self):
        """Checks if the current game is a new one, or a loaded one.
		@return: True if game is loaded, else False
		"""
        return (self.savecounter > 0)

    def remove_selected(self):
        self.log.debug('Removing %s', self.selected_instances)
        for instance in [inst for inst in self.selected_instances]:
            if instance.is_building:
                if instance.tearable and instance.owner is self.world.player:
                    self.log.debug('Attempting to remove building %s', inst)
                    Tear(instance).execute(self)
                    self.selected_instances.discard(instance)
                else:
                    self.log.debug('Unable to remove building %s', inst)
            elif instance.is_unit:
                if instance.owner is self.world.player:
                    self.log.debug('Attempting to remove unit %s', inst)
                    RemoveUnit(instance).execute(self)
                    self.selected_instances.discard(instance)
                else:
                    self.log.debug('Unable to remove unit %s', inst)
            else:
                self.log.error('Unable to remove unknown object %s', instance)

    def _do_save(self, savegame):
        """Actual save code.
		@param savegame: absolute path"""
        assert os.path.isabs(savegame)
        self.log.debug("Session: Saving to %s", savegame)
        try:
            if os.path.exists(savegame):
                os.unlink(savegame)
            self.savecounter += 1

            db = DbReader(savegame)
        except IOError as e:  # usually invalid filename
            headline = _("Failed to create savegame file")
            descr = _(
                "There has been an error while creating your savegame file.")
            advice = _(
                "This usually means that the savegame name contains unsupported special characters."
            )
            self.gui.show_error_popup(headline, descr, advice, unicode(e))
            return self.save(
            )  # retry with new savegamename entered by the user
            # this must not happen with quicksave/autosave
        except OSError as e:
            if e.errno == errno.EACCES:
                self.gui.show_error_popup(
                    _("Access is denied"),
                    _("The savegame file could be read-only or locked by another process."
                      ))
                return self.save()
            raise

        try:
            read_savegame_template(db)

            db("BEGIN")
            self.world.save(db)
            #self.manager.save(db)
            self.view.save(db)
            self.ingame_gui.save(db)
            self.scenario_eventhandler.save(db)
            LastActivePlayerSettlementManager().save(db)

            for instance in self.selected_instances:
                db("INSERT INTO selected(`group`, id) VALUES(NULL, ?)",
                   instance.worldid)
            for group in xrange(len(self.selection_groups)):
                for instance in self.selection_groups[group]:
                    db("INSERT INTO selected(`group`, id) VALUES(?, ?)", group,
                       instance.worldid)

            rng_state = json.dumps(self.random.getstate())
            SavegameManager.write_metadata(db, self.savecounter, rng_state)
            # make sure everything gets written now
            db("COMMIT")
            db.close()
            return True
        except:
            print "Save Exception"
            traceback.print_exc()
            db.close()  # close db before delete
            os.unlink(savegame)  # remove invalid savegamefile
            return False