def move(self, destination, callback=None, destination_in_building=False, action='move',
	         blocked_callback=None, path=None):
		"""Moves unit to destination
		@param destination: Point or Rect
		@param callback: a parameter supported by WeakMethodList. Gets called when unit arrives.
		@param action: action as string to use for movement
		@param blocked_callback: a parameter supported by WeakMethodList. Gets called when unit gets blocked.
		@param path: a precalculated path (return value of FindPath()())
		"""
		if not path:
			# calculate the path
			move_possible = self.path.calc_path(destination, destination_in_building)

			self.log.debug("%s: move to %s; possible: %s; is_moving: %s", self,
			               destination, move_possible, self.is_moving())

			if not move_possible:
				raise MoveNotPossible
		else:
			self.path.move_on_path(path, destination_in_building=destination_in_building)

		self.move_callbacks = WeakMethodList(callback)
		self.blocked_callbacks = WeakMethodList(blocked_callback)
		self._conditional_callbacks = {}
		self._setup_move(action)

		# start moving by regular ticking (only if next tick isn't scheduled)
		if not self.is_moving():
			self.__is_moving = True
			# start moving in 1 tick
			# this assures that a movement takes at least 1 tick, which is sometimes subtly
			# assumed e.g. in the collector code
			Scheduler().add_new_object(self._move_tick, self)
예제 #2
0
	def move(self, destination, callback=None, destination_in_building=False, action='move',
	         blocked_callback=None, path=None):
		"""Moves unit to destination
		@param destination: Point or Rect
		@param callback: a parameter supported by WeakMethodList. Gets called when unit arrives.
		@param action: action as string to use for movement
		@param blocked_callback: a parameter supported by WeakMethodList. Gets called when unit gets blocked.
		@param path: a precalculated path (return value of FindPath()())
		"""
		if not path:
			# calculate the path
			move_possible = self.path.calc_path(destination, destination_in_building)

			self.log.debug("%s: move to %s; possible: %s; is_moving: %s", self,
			               destination, move_possible, self.is_moving())

			if not move_possible:
				raise MoveNotPossible
		else:
			self.path.move_on_path(path, destination_in_building=destination_in_building)

		self.move_callbacks = WeakMethodList(callback)
		self.blocked_callbacks = WeakMethodList(blocked_callback)
		self._conditional_callbacks = {}
		self._setup_move(action)

		# start moving by regular ticking (only if next tick isn't scheduled)
		if not self.is_moving():
			self.__is_moving = True
			# start moving in 1 tick
			# this assures that a movement takes at least 1 tick, which is sometimes subtly
			# assumed e.g. in the collector code
			Scheduler().add_new_object(self._move_tick, self)
예제 #3
0
 def __init(self):
     self.__listeners = WeakMethodList()
     self.__remove_listeners = WeakMethodList()
     # number of event calls
     # if any event is triggered increase the number, after all callbacks are executed decrease it
     # if it reaches 0 it means that in the current object all event callbacks were executed
     self.__event_call_number = 0
     self.__hard_remove = True
예제 #4
0
	def __init(self):
		self.__listeners = WeakMethodList()
		self.__remove_listeners = WeakMethodList()
		# number of event calls
		# if any event is triggered increase the number, after all callbacks are executed decrease it
		# if it reaches 0 it means that in the current object all event callbacks were executed
		self.__event_call_number = 0
		self.__hard_remove = True
예제 #5
0
    def stop(self, callback=None):
        """Stops a unit with currently no possibility to continue the movement.
		The unit actually stops moving when current move (to the next coord) is finished.
		@param callback: a parameter supported by WeakMethodList. is executed immediately if unit isn't moving
		"""
        if not self.is_moving():
            WeakMethodList(callback).execute()
            return
        self.move_callbacks = WeakMethodList(callback)
        self.path.end_move()
예제 #6
0
	def __init(self, x, y):
		self.position = Point(x, y)
		self.last_position = Point(x, y)
		self._next_target = Point(x, y)

		self.move_callbacks = WeakMethodList()
		self.blocked_callbacks = WeakMethodList()
		self._conditional_callbacks = {}

		self.__is_moving = False

		self.path = self.pather_class(self, session=self.session)

		self._exact_model_coords = fife.ExactModelCoordinate() # save instance since construction is expensive (no other purpose)
		self._fife_location = None
예제 #7
0
	def __init(self, x, y):
		self.position = Point(x, y)
		self.last_position = Point(x, y)
		self._next_target = Point(x, y)

		self.move_callbacks = WeakMethodList()
		self.blocked_callbacks = WeakMethodList()
		self._conditional_callbacks = {}

		self.__is_moving = False

		self.path = self.pather_class(self, session=self.session)

		self._exact_model_coords1 = fife.ExactModelCoordinate() # save instance since construction is expensive (no other purpose)
		self._exact_model_coords2 = fife.ExactModelCoordinate() # save instance since construction is expensive (no other purpose)
		self._fife_location1 = None
		self._fife_location2 = None
	def stop(self, callback=None):
		"""Stops a unit with currently no possibility to continue the movement.
		The unit actually stops moving when current move (to the next coord) is finished.
		@param callback: a parameter supported by WeakMethodList. is executed immediately if unit isn't moving
		"""
		if not self.is_moving():
			WeakMethodList(callback).execute()
			return
		self.move_callbacks = WeakMethodList(callback)
		self.path.end_move()
예제 #9
0
class MovingObject(ComponentHolder, ConcreteObject):
    """This class provides moving functionality and is to be inherited by Unit.
	Its purpose is to provide a cleaner division of the code.

	It provides:
	*attributes:
	- position, last_position: Point
	- path: Pather

	*moving methods:
	- move
	- stop
	- add_move_callback

	*getters/checkers:
	- check_move
	- get_move_target
	- is_moving
	"""
    movable = True

    log = logging.getLogger("world.units")

    pather_class = None  # overwrite this with a descendant of AbstractPather

    def __init__(self, x, y, **kwargs):
        super(MovingObject, self).__init__(x=x, y=y, **kwargs)
        self.__init(x, y)

    def __init(self, x, y):
        self.position = Point(x, y)
        self.last_position = Point(x, y)
        self._next_target = Point(x, y)

        self.move_callbacks = WeakMethodList()
        self.blocked_callbacks = WeakMethodList()
        self._conditional_callbacks = {}

        self.__is_moving = False

        self.path = self.pather_class(self, session=self.session)

        self._exact_model_coords1 = fife.ExactModelCoordinate(
        )  # save instance since construction is expensive (no other purpose)
        self._exact_model_coords2 = fife.ExactModelCoordinate(
        )  # save instance since construction is expensive (no other purpose)
        self._fife_location1 = None
        self._fife_location2 = None

    def check_move(self, destination):
        """Tries to find a path to destination
		@param destination: destination supported by pathfinding
		@return: object that can be used in boolean expressions (the path in case there is one)
		"""
        return self.path.calc_path(destination, check_only=True)

    def is_moving(self):
        """Returns whether unit is currently moving"""
        return self.__is_moving

    def stop(self, callback=None):
        """Stops a unit with currently no possibility to continue the movement.
		The unit actually stops moving when current move (to the next coord) is finished.
		@param callback: a parameter supported by WeakMethodList. is executed immediately if unit isn't moving
		"""
        if not self.is_moving():
            WeakMethodList(callback).execute()
            return
        self.move_callbacks = WeakMethodList(callback)
        self.path.end_move()

    def _setup_move(self, action='move'):
        """Executes necessary steps to begin a movement. Currently only the action is set."""
        # try a number of actions and use first existent one
        for action_iter in (action, 'move', self._action):
            if self.has_action(action_iter):
                self._move_action = action_iter
                return
        # this case shouldn't happen, but no other action might be available (e.g. ships)
        self._move_action = 'idle'

    def move(self,
             destination,
             callback=None,
             destination_in_building=False,
             action='move',
             blocked_callback=None,
             path=None):
        """Moves unit to destination
		@param destination: Point or Rect
		@param callback: a parameter supported by WeakMethodList. Gets called when unit arrives.
		@param action: action as string to use for movement
		@param blocked_callback: a parameter supported by WeakMethodList. Gets called when unit gets blocked.
		@param path: a precalculated path (return value of FindPath()())
		"""
        if not path:
            # calculate the path
            move_possible = self.path.calc_path(destination,
                                                destination_in_building)

            self.log.debug("%s: move to %s; possible: %s; is_moving: %s", self,
                           destination, move_possible, self.is_moving())

            if not move_possible:
                raise MoveNotPossible
        else:
            self.path.move_on_path(
                path, destination_in_building=destination_in_building)

        self.move_callbacks = WeakMethodList(callback)
        self.blocked_callbacks = WeakMethodList(blocked_callback)
        self._conditional_callbacks = {}
        self._setup_move(action)

        # start moving by regular ticking (only if next tick isn't scheduled)
        if not self.is_moving():
            self.__is_moving = True
            # start moving in 1 tick
            # this assures that a movement takes at least 1 tick, which is sometimes subtly
            # assumed e.g. in the collector code
            Scheduler().add_new_object(self._move_tick, self)

    def _movement_finished(self):
        self.log.debug("%s: movement finished. calling callbacks %s", self,
                       self.move_callbacks)
        self._next_target = self.position
        self.__is_moving = False
        self.move_callbacks.execute()

    @decorators.make_constants()
    def _move_tick(self, resume=False):
        """Called by the scheduler, moves the unit one step for this tick.
		"""
        assert self._next_target is not None

        if self._fife_location1 is None:
            # this data structure is needed multiple times, only create once
            self._fife_location1 = fife.Location(
                self._instance.getLocationRef().getLayer())
            self._fife_location2 = fife.Location(
                self._instance.getLocationRef().getLayer())

        if resume:
            self.__is_moving = True
        else:
            #self.log.debug("%s move tick from %s to %s", self, self.last_position, self._next_target)
            self.last_position = self.position
            self.position = self._next_target
            self._changed()

        # try to get next step, handle a blocked path
        while self._next_target == self.position:
            try:
                self._next_target = self.path.get_next_step()
            except PathBlockedError:
                # if we are trying to resume and it isn't possible then we need to raise it again
                if resume:
                    raise

                self.log.debug("path is blocked")
                self.log.debug("owner: %s", self.owner)
                self.__is_moving = False
                self._next_target = self.position
                if self.blocked_callbacks:
                    self.log.debug(
                        'PATH FOR UNIT %s is blocked. Calling blocked_callback',
                        self)
                    self.blocked_callbacks.execute()
                else:
                    # generic solution: retry in 2 secs
                    self.log.debug(
                        'PATH FOR UNIT %s is blocked. Retry in 2 secs', self)
                    # technically, the ship doesn't move, but it is in the process of moving,
                    # as it will continue soon in general. Needed in border cases for add_move_callback
                    self.__is_moving = True
                    Scheduler().add_new_object(self._move_tick, self,
                                               GAME_SPEED.TICKS_PER_SECOND * 2)
                self.log.debug("Unit %s: path is blocked, no way around", self)
                return

        if self._next_target is None:
            self._movement_finished()
            return
        else:
            self.__is_moving = True

        #setup movement
        move_time = self.get_unit_velocity()
        UnitClass.ensure_action_loaded(
            self._action_set_id, self._move_action)  # lazy load move action

        self._exact_model_coords1.set(self.position.x, self.position.y, 0)
        self._fife_location1.setExactLayerCoordinates(
            self._exact_model_coords1)
        self._exact_model_coords2.set(self._next_target.x, self._next_target.y,
                                      0)
        self._fife_location2.setExactLayerCoordinates(
            self._exact_model_coords2)
        self._route = fife.Route(self._fife_location1, self._fife_location2)
        # TODO/HACK the *5 provides slightly less flickery behavior of the moving
        # objects. This should be fixed properly by using the fife pathfinder for
        # the entire route and task
        location_list = fife.LocationList([self._fife_location2] * 5)
        # It exists for FIFE 0.3.4 compat. See #1993.
        if Fife.getVersion() == (0, 3, 4):
            location_list.thisown = 0
            self._route.thisown = 0
        self._route.setPath(location_list)

        self.act(self._move_action)
        diagonal = self._next_target.x != self.position.x and self._next_target.y != self.position.y
        speed = float(self.session.timer.get_ticks(1)) / move_time[0]
        action = self._instance.getCurrentAction().getId()
        self._instance.follow(action, self._route, speed)

        #self.log.debug("%s registering move tick in %s ticks", self, move_time[int(diagonal)])
        Scheduler().add_new_object(self._move_tick, self,
                                   move_time[int(diagonal)])

        # check if a conditional callback becomes true
        for cond in self._conditional_callbacks.keys(
        ):  # iterate of copy of keys to be able to delete
            if cond():
                # start callback when this function is done
                Scheduler().add_new_object(self._conditional_callbacks[cond],
                                           self)
                del self._conditional_callbacks[cond]

    def teleport(self,
                 destination,
                 callback=None,
                 destination_in_building=False):
        """Like move, but nearly instantaneous"""
        if hasattr(destination, "position"):
            destination_coord = destination.position.center.to_tuple()
        else:
            destination_coord = destination
        self.move(destination,
                  callback=callback,
                  destination_in_building=destination_in_building,
                  path=[destination_coord])

    def add_move_callback(self, callback):
        """Registers callback to be executed when movement of unit finishes.
		This has no effect if the unit isn't moving."""
        if self.is_moving():
            self.move_callbacks.append(callback)

    def add_blocked_callback(self, blocked_callback):
        """Registers callback to be executed when movement of the unit gets blocked."""
        self.blocked_callbacks.append(blocked_callback)

    def add_conditional_callback(self, condition, callback):
        """Adds a callback, that gets called, if, at any time of the movement, the condition becomes
		True. The condition is checked every move_tick. After calling the callback, it is removed."""
        assert callable(condition)
        assert callable(callback)
        self._conditional_callbacks[condition] = callback

    def get_unit_velocity(self):
        """Returns the number of ticks that it takes to do a straight (i.e. vertical or horizontal)
		or diagonal movement as a tuple in this order.
		@return: (int, int)
		"""
        tile = self.session.world.get_tile(self.position)
        if self.id in tile.velocity:
            return tile.velocity[self.id]
        else:
            return (12, 17)  # standard values

    def get_move_target(self):
        return self.path.get_move_target()

    def save(self, db):
        super(MovingObject, self).save(db)
        # NOTE: _move_action is currently not yet saved and neither is blocked_callback.
        self.path.save(db, self.worldid)

    def load(self, db, worldid):
        super(MovingObject, self).load(db, worldid)
        x, y = db("SELECT x, y FROM unit WHERE rowid = ?", worldid)[0]
        self.__init(x, y)
        path_loaded = self.path.load(db, worldid)
        if path_loaded:
            self.__is_moving = True
            self._setup_move()
            Scheduler().add_new_object(self._move_tick, self, run_in=0)
예제 #10
0
class ChangeListener(object):
    """Trivial ChangeListener.
	The object that changes and the object that listens have to inherit from this class.
	An object calls _changed every time something has changed, obviously.
	This function calls every Callback, that has been registered to listen for a change.
	NOTE: ChangeListeners aren't saved, they have to be reregistered on load
	NOTE: RemoveListeners must not access the object, as it is in progress of being destroyed.
	"""

    log = logging.getLogger('changelistener')

    def __init__(self, *args, **kwargs):
        super(ChangeListener, self).__init__()
        self.__init()

    def __init(self):
        self.__listeners = WeakMethodList()
        self.__remove_listeners = WeakMethodList()
        # number of event calls
        # if any event is triggered increase the number, after all callbacks are executed decrease it
        # if it reaches 0 it means that in the current object all event callbacks were executed
        self.__event_call_number = 0
        self.__hard_remove = True

    def __remove_listener(self, listener_list, listener):
        # check if the listener should be hard removed
        # if so switch it in the list to None
        try:
            if self.__hard_remove:
                listener_list.remove(listener)
            else:
                listener_list[listener_list.index(listener)] = None
        except ValueError as e:  # nicer error:
            raise ValueError(
                str(e) + "\nTried to remove: " + str(listener) + "\nat " +
                str(self) + "\nList: " + str([str(i) for i in listener_list]))

    def __call_listeners(self, listener_list):
        # instead of removing from list, switch the listener in position to None
        # this way, iteration won't be affected while listeners may modify the list
        self.__hard_remove = False
        # increase the event call number
        self.__event_call_number += 1
        for listener in listener_list:
            if listener:
                try:
                    listener()
                except ReferenceError as e:
                    # listener object is dead, don't crash since it doesn't need updates now anyway
                    self.log.warning('The dead are listening to %s: %s', self,
                                     e)
                    traceback.print_stack()

        self.__event_call_number -= 1

        if self.__event_call_number == 0:
            self.__hard_remove = True
            listener_list[:] = [l for l in listener_list if l]

    ## Normal change listener
    def add_change_listener(self,
                            listener,
                            call_listener_now=False,
                            no_duplicates=False):
        assert callable(listener)
        if not no_duplicates or listener not in self.__listeners:
            self.__listeners.append(listener)
        if call_listener_now:  # also call if duplicate is added
            listener()

    def remove_change_listener(self, listener):
        self.__remove_listener(self.__listeners, listener)

    def has_change_listener(self, listener):
        return (listener in self.__listeners)

    def discard_change_listener(self, listener):
        """Remove listener if it's there"""
        if self.has_change_listener(listener):
            self.remove_change_listener(listener)

    def clear_change_listeners(self):
        """Removes all change listeners"""
        self.__listeners = WeakMethodList()

    def _changed(self):
        """Calls every listener when an object changed"""
        self.__call_listeners(self.__listeners)

    ## Removal change listener
    def add_remove_listener(self, listener, no_duplicates=False):
        """A listener that listens for removal of the object"""
        assert callable(listener)
        if no_duplicates and listener in self.__remove_listeners:
            return  # don't allow duplicate entries
        self.__remove_listeners.append(listener)

    def remove_remove_listener(self, listener):
        self.__remove_listener(self.__remove_listeners, listener)

    def has_remove_listener(self, listener):
        return (listener in self.__remove_listeners)

    def discard_remove_listener(self, listener):
        if self.has_remove_listener(listener):
            self.remove_remove_listener(listener)

    def load(self, db, world_id):
        self.__init()

    def remove(self):
        self.__call_listeners(self.__remove_listeners)
        self.end()

    def end(self):
        self.__listeners = None
        self.__remove_listeners = None
예제 #11
0
 def clear_change_listeners(self):
     """Removes all change listeners"""
     self.__listeners = WeakMethodList()
예제 #12
0
class ChangeListener(object):
	"""Trivial ChangeListener.
	The object that changes and the object that listens have to inherit from this class.
	An object calls _changed everytime something has changed, obviously.
	This function calls every Callback, that has been registered to listen for a change.
	NOTE: ChangeListeners aren't saved, they have to be reregistered on load
	NOTE: RemoveListeners must not access the object, as it is in progress of being destroyed.
	"""
	def __init__(self, *args, **kwargs):
		super(ChangeListener, self).__init__()
		self.__init()

	def __init(self):
		self.__listeners = WeakMethodList()
		self.__remove_listeners = WeakMethodList()
		# number of event calls
		# if any event is triggered increase the number, after all callbacks are executed decrease it
		# if it reaches 0 it means that in the current object all event callbacks were executed
		self.__event_call_number = 0
		self.__hard_remove = True

	def __remove_listener(self, listener_list, listener):
		# check if the listener should be hard removed
		# if so switch it in the list to None
		try:
			if self.__hard_remove:
				listener_list.remove(listener)
			else:
				listener_list[listener_list.index(listener)] = None
		except ValueError as e: # nicer error:
			raise ValueError(str(e)+
			                 "\nTried to remove: "+str(listener)+"\nat "+str(self)+
			                 "\nList: "+str([str(i) for i in listener_list]))

	def __call_listeners(self, listener_list):
		# instead of removing from list, switch the listener in position to None
		# this way, iteration won't be affected while listeners may modify the list
		self.__hard_remove = False
		# increase the event call number
		self.__event_call_number += 1
		for listener in listener_list:
			if listener:
				try:
					listener()
				except ReferenceError as e:
					# listener object is dead, don't crash since it doesn't need updates now anyway
					print 'Warning: the dead are listening to', self, ': ', e
					traceback.print_stack()

		self.__event_call_number -= 1

		if self.__event_call_number == 0:
			self.__hard_remove = True
			listener_list[:] = [ l for l in listener_list if l ]

	## Normal change listener
	def add_change_listener(self, listener, call_listener_now=False, no_duplicates=False):
		assert callable(listener)
		if not no_duplicates or listener not in self.__listeners:
			self.__listeners.append(listener)
		if call_listener_now: # also call if duplicate is adde
			listener()

	def remove_change_listener(self, listener):
		self.__remove_listener(self.__listeners, listener)

	def has_change_listener(self, listener):
		return (listener in self.__listeners)

	def discard_change_listener(self, listener):
		"""Remove listener if it's there"""
		if self.has_change_listener(listener):
			self.remove_change_listener(listener)

	def clear_change_listeners(self):
		"""Removes all change listeners"""
		self.__listeners = WeakMethodList()

	def _changed(self):
		"""Calls every listener when an object changed"""
		self.__call_listeners(self.__listeners)

	## Removal change listener
	def add_remove_listener(self, listener, no_duplicates=False):
		"""A listener that listens for removal of the object"""
		assert callable(listener)
		if no_duplicates and listener in self.__remove_listeners:
			return # don't allow duplicate entries
		self.__remove_listeners.append(listener)

	def remove_remove_listener(self, listener):
		self.__remove_listener(self.__remove_listeners, listener)

	def has_remove_listener(self, listener):
		return (listener in self.__remove_listeners)

	def discard_remove_listener(self, listener):
		if self.has_remove_listener(listener):
			self.remove_remove_listener(listener)

	def load(self, db, world_id):
		self.__init()

	def remove(self):
		self.__call_listeners(self.__remove_listeners)
		self.end()

	def end(self):
		self.__listeners = None
		self.__remove_listeners = None
예제 #13
0
	def clear_change_listeners(self):
		"""Removes all change listeners"""
		self.__listeners = WeakMethodList()
class MovingObject(ComponentHolder, ConcreteObject):
	"""This class provides moving functionality and is to be inherited by Unit.
	Its purpose is to provide a cleaner division of the code.

	It provides:
	*attributes:
	- position, last_position: Point
	- path: Pather

	*moving methods:
	- move
	- stop
	- add_move_callback

	*getters/checkers:
	- check_move
	- get_move_target
	- is_moving
	"""
	movable = True

	log = logging.getLogger("world.units")

	# overwrite this with a descendant of AbstractPather
	pather_class = None # type: Type[AbstractPather]

	def __init__(self, x, y, **kwargs):
		super().__init__(x=x, y=y, **kwargs)
		self.__init(x, y)

	def __init(self, x, y):
		self.position = Point(x, y)
		self.last_position = Point(x, y)
		self._next_target = Point(x, y)

		self.move_callbacks = WeakMethodList()
		self.blocked_callbacks = WeakMethodList()
		self._conditional_callbacks = {}

		self.__is_moving = False

		self.path = self.pather_class(self, session=self.session)

		self._exact_model_coords1 = fife.ExactModelCoordinate() # save instance since construction is expensive (no other purpose)
		self._exact_model_coords2 = fife.ExactModelCoordinate() # save instance since construction is expensive (no other purpose)
		self._fife_location1 = None
		self._fife_location2 = None

	def check_move(self, destination):
		"""Tries to find a path to destination
		@param destination: destination supported by pathfinding
		@return: object that can be used in boolean expressions (the path in case there is one)
		"""
		return self.path.calc_path(destination, check_only=True)

	def is_moving(self):
		"""Returns whether unit is currently moving"""
		return self.__is_moving

	def stop(self, callback=None):
		"""Stops a unit with currently no possibility to continue the movement.
		The unit actually stops moving when current move (to the next coord) is finished.
		@param callback: a parameter supported by WeakMethodList. is executed immediately if unit isn't moving
		"""
		if not self.is_moving():
			WeakMethodList(callback).execute()
			return
		self.move_callbacks = WeakMethodList(callback)
		self.path.end_move()

	def _setup_move(self, action='move'):
		"""Executes necessary steps to begin a movement. Currently only the action is set."""
		# try a number of actions and use first existent one
		for action_iter in (action, 'move', self._action):
			if self.has_action(action_iter):
				self._move_action = action_iter
				return
		# this case shouldn't happen, but no other action might be available (e.g. ships)
		self._move_action = 'idle'

	def move(self, destination, callback=None, destination_in_building=False, action='move',
	         blocked_callback=None, path=None):
		"""Moves unit to destination
		@param destination: Point or Rect
		@param callback: a parameter supported by WeakMethodList. Gets called when unit arrives.
		@param action: action as string to use for movement
		@param blocked_callback: a parameter supported by WeakMethodList. Gets called when unit gets blocked.
		@param path: a precalculated path (return value of FindPath()())
		"""
		if not path:
			# calculate the path
			move_possible = self.path.calc_path(destination, destination_in_building)

			self.log.debug("%s: move to %s; possible: %s; is_moving: %s", self,
			               destination, move_possible, self.is_moving())

			if not move_possible:
				raise MoveNotPossible
		else:
			self.path.move_on_path(path, destination_in_building=destination_in_building)

		self.move_callbacks = WeakMethodList(callback)
		self.blocked_callbacks = WeakMethodList(blocked_callback)
		self._conditional_callbacks = {}
		self._setup_move(action)

		# start moving by regular ticking (only if next tick isn't scheduled)
		if not self.is_moving():
			self.__is_moving = True
			# start moving in 1 tick
			# this assures that a movement takes at least 1 tick, which is sometimes subtly
			# assumed e.g. in the collector code
			Scheduler().add_new_object(self._move_tick, self)

	def _movement_finished(self):
		self.log.debug("%s: movement finished. calling callbacks %s", self, self.move_callbacks)
		self._next_target = self.position
		self.__is_moving = False
		self.move_callbacks.execute()

	def _move_tick(self, resume=False):
		"""Called by the scheduler, moves the unit one step for this tick.
		"""
		assert self._next_target is not None

		if self._fife_location1 is None:
			# this data structure is needed multiple times, only create once
			self._fife_location1 = fife.Location(self._instance.getLocationRef().getLayer())
			self._fife_location2 = fife.Location(self._instance.getLocationRef().getLayer())

		if resume:
			self.__is_moving = True
		else:
			#self.log.debug("%s move tick from %s to %s", self, self.last_position, self._next_target)
			self.last_position = self.position
			self.position = self._next_target
			self._changed()

		# try to get next step, handle a blocked path
		while self._next_target == self.position:
			try:
				self._next_target = self.path.get_next_step()
			except PathBlockedError:
				# if we are trying to resume and it isn't possible then we need to raise it again
				if resume:
					raise

				self.log.debug("path is blocked")
				self.log.debug("owner: %s", self.owner)
				self.__is_moving = False
				self._next_target = self.position
				if self.blocked_callbacks:
					self.log.debug('PATH FOR UNIT %s is blocked. Calling blocked_callback', self)
					self.blocked_callbacks.execute()
				else:
					# generic solution: retry in 2 secs
					self.log.debug('PATH FOR UNIT %s is blocked. Retry in 2 secs', self)
					# technically, the ship doesn't move, but it is in the process of moving,
					# as it will continue soon in general. Needed in border cases for add_move_callback
					self.__is_moving = True
					Scheduler().add_new_object(self._move_tick, self,
					                           GAME_SPEED.TICKS_PER_SECOND * 2)
				self.log.debug("Unit %s: path is blocked, no way around", self)
				return

		if self._next_target is None:
			self._movement_finished()
			return
		else:
			self.__is_moving = True

		# HACK: 2 different pathfinding systems are being used here and they
		# might not always match.
		# If the graphical location is too far away from the actual location,
		# then forcefully synchronize the locations.
		# This fixes the symptoms from issue #2859
		# https://github.com/unknown-horizons/unknown-horizons/issues/2859
		pos = fife.ExactModelCoordinate(self.position.x, self.position.y, 0)
		fpos = self.fife_instance.getLocationRef().getExactLayerCoordinates()
		if (pos - fpos).length() > 1.5:
			self.fife_instance.getLocationRef().setExactLayerCoordinates(pos)

		#setup movement
		move_time = self.get_unit_velocity()
		UnitClass.ensure_action_loaded(self._action_set_id, self._move_action) # lazy load move action

		self._exact_model_coords1.set(self.position.x, self.position.y, 0)
		self._fife_location1.setExactLayerCoordinates(self._exact_model_coords1)
		self._exact_model_coords2.set(self._next_target.x, self._next_target.y, 0)
		self._fife_location2.setExactLayerCoordinates(self._exact_model_coords2)
		self._route = fife.Route(self._fife_location1, self._fife_location2)
		# TODO/HACK the *5 provides slightly less flickery behavior of the moving
		# objects. This should be fixed properly by using the fife pathfinder for
		# the entire route and task
		location_list = fife.LocationList([self._fife_location2] * 5)
		self._route.setPath(location_list)

		self.act(self._move_action)
		diagonal = self._next_target.x != self.position.x and self._next_target.y != self.position.y
		speed = float(self.session.timer.get_ticks(1)) / move_time[0]
		action = self._instance.getCurrentAction().getId()
		self._instance.follow(action, self._route, speed)

		#self.log.debug("%s registering move tick in %s ticks", self, move_time[int(diagonal)])
		Scheduler().add_new_object(self._move_tick, self, move_time[int(diagonal)])

		# check if a conditional callback becomes true
		for cond in list(self._conditional_callbacks.keys()): # iterate of copy of keys to be able to delete
			if cond():
				# start callback when this function is done
				Scheduler().add_new_object(self._conditional_callbacks[cond], self)
				del self._conditional_callbacks[cond]

	def teleport(self, destination, callback=None, destination_in_building=False):
		"""Like move, but nearly instantaneous"""
		if hasattr(destination, "position"):
			destination_coord = destination.position.center.to_tuple()
		else:
			destination_coord = destination
		self.move(destination, callback=callback, destination_in_building=destination_in_building, path=[destination_coord])

	def add_move_callback(self, callback):
		"""Registers callback to be executed when movement of unit finishes.
		This has no effect if the unit isn't moving."""
		if self.is_moving():
			self.move_callbacks.append(callback)

	def add_blocked_callback(self, blocked_callback):
		"""Registers callback to be executed when movement of the unit gets blocked."""
		self.blocked_callbacks.append(blocked_callback)

	def add_conditional_callback(self, condition, callback):
		"""Adds a callback, that gets called, if, at any time of the movement, the condition becomes
		True. The condition is checked every move_tick. After calling the callback, it is removed."""
		assert callable(condition)
		assert callable(callback)
		self._conditional_callbacks[condition] = callback

	def get_unit_velocity(self):
		"""Returns the number of ticks that it takes to do a straight (i.e. vertical or horizontal)
		or diagonal movement as a tuple in this order.
		@return: (int, int)
		"""
		tile = self.session.world.get_tile(self.position)
		if self.id in tile.velocity:
			return tile.velocity[self.id]
		else:
			return (12, 17) # standard values

	def get_move_target(self):
		return self.path.get_move_target()

	def save(self, db):
		super().save(db)
		# NOTE: _move_action is currently not yet saved and neither is blocked_callback.
		self.path.save(db, self.worldid)

	def load(self, db, worldid):
		super().load(db, worldid)
		x, y = db("SELECT x, y FROM unit WHERE rowid = ?", worldid)[0]
		self.__init(x, y)
		path_loaded = self.path.load(db, worldid)
		if path_loaded:
			self.__is_moving = True
			self._setup_move()
			Scheduler().add_new_object(self._move_tick, self, run_in=0)
예제 #15
0
class MovingObject(ComponentHolder, ConcreteObject):
	"""This class provides moving functionality and is to be inherited by Unit.
	Its purpose is to provide a cleaner division of the code.

	It provides:
	*attributes:
	- position, last_position: Point
	- path: Pather

	*moving methods:
	- move
	- stop
	- add_move_callback

	*getters/checkers:
	- check_move
	- get_move_target
	- is_moving
	"""
	movable = True

	log = logging.getLogger("world.units")

	pather_class = None # overwrite this with a descendant of AbstractPather

	def __init__(self, x, y, **kwargs):
		super(MovingObject, self).__init__(x=x, y=y, **kwargs)
		self.__init(x, y)

	def __init(self, x, y):
		self.position = Point(x, y)
		self.last_position = Point(x, y)
		self._next_target = Point(x, y)

		self.move_callbacks = WeakMethodList()
		self.blocked_callbacks = WeakMethodList()
		self._conditional_callbacks = {}

		self.__is_moving = False

		self.path = self.pather_class(self, session=self.session)

		self._exact_model_coords = fife.ExactModelCoordinate() # save instance since construction is expensive (no other purpose)
		self._fife_location = None

	def check_move(self, destination):
		"""Tries to find a path to destination
		@param destination: destination supported by pathfinding
		@return: object that can be used in boolean expressions (the path in case there is one)
		"""
		return self.path.calc_path(destination, check_only = True)

	def is_moving(self):
		"""Returns whether unit is currently moving"""
		return self.__is_moving

	def stop(self, callback=None):
		"""Stops a unit with currently no possibility to continue the movement.
		The unit actually stops moving when current move (to the next coord) is finished.
		@param callback: a parameter supported by WeakMethodList. is executed immediately if unit isn't moving
		"""
		if not self.is_moving():
			WeakMethodList(callback).execute()
			return
		self.move_callbacks = WeakMethodList(callback)
		self.path.end_move()

	def _setup_move(self, action='move'):
		"""Executes necessary steps to begin a movement. Currently only the action is set."""
		# try a number of actions and use first existent one
		for action_iter in (action, 'move', self._action):
			if self.has_action(action_iter):
				self._move_action = action_iter
				return
		# this case shouldn't happen, but no other action might be available (e.g. ships)
		self._move_action = 'idle'

	def move(self, destination, callback=None, destination_in_building=False, action='move',
	         blocked_callback=None, path=None):
		"""Moves unit to destination
		@param destination: Point or Rect
		@param callback: a parameter supported by WeakMethodList. Gets called when unit arrives.
		@param action: action as string to use for movement
		@param blocked_callback: a parameter supported by WeakMethodList. Gets called when unit gets blocked.
		@param path: a precalculated path (return value of FindPath()())
		"""
		if not path:
			# calculate the path
			move_possible = self.path.calc_path(destination, destination_in_building)

			self.log.debug("%s: move to %s; possible: %s; is_moving: %s", self,
			               destination, move_possible, self.is_moving())

			if not move_possible:
				raise MoveNotPossible
		else:
			self.path.move_on_path(path, destination_in_building=destination_in_building)

		self.move_callbacks = WeakMethodList(callback)
		self.blocked_callbacks = WeakMethodList(blocked_callback)
		self._conditional_callbacks = {}
		self._setup_move(action)

		# start moving by regular ticking (only if next tick isn't scheduled)
		if not self.is_moving():
			self.__is_moving = True
			# start moving in 1 tick
			# this assures that a movement takes at least 1 tick, which is sometimes subtly
			# assumed e.g. in the collector code
			Scheduler().add_new_object(self._move_tick, self)

	def _movement_finished(self):
		self.log.debug("%s: movement finished. calling callbacks %s", self, self.move_callbacks)
		self._next_target = self.position
		self.__is_moving = False
		self.move_callbacks.execute()

	@decorators.make_constants()
	def _move_tick(self, resume=False):
		"""Called by the scheduler, moves the unit one step for this tick.
		"""
		assert self._next_target is not None

		if self._fife_location is None:
			# this data structure is needed multiple times, only create once
			self._fife_location = fife.Location(self._instance.getLocationRef().getLayer())

		if resume:
			self.__is_moving = True
		else:
			#self.log.debug("%s move tick from %s to %s", self, self.last_position, self._next_target)
			self.last_position = self.position
			self.position = self._next_target
			self._exact_model_coords.set(self.position.x, self.position.y, 0)
			self._fife_location.setExactLayerCoordinates(self._exact_model_coords)
			# it's safe to use location here (thisown is 0, set by swig, and setLocation uses reference)
			self._instance.setLocation(self._fife_location)
			self._changed()

		# try to get next step, handle a blocked path
		while self._next_target == self.position:
			try:
				self._next_target = self.path.get_next_step()
			except PathBlockedError:
				# if we are trying to resume and it isn't possible then we need to raise it again
				if resume:
					raise

				self.log.debug("path is blocked")
				self.log.debug("owner: %s", self.owner)
				self.__is_moving = False
				self._next_target = self.position
				if self.blocked_callbacks:
					self.log.debug('PATH FOR UNIT %s is blocked. Calling blocked_callback', self)
					self.blocked_callbacks.execute()
					"""
					# TODO: This is supposed to delegate control over the behaviour of the unit to the owner.
					#       It is currently not used in a meaningful manner and possibly will be removed,
					#       as blocked_callback solves this problem more elegantly.
					#       Also, this sometimes triggers for collectors, who are supposed to use the
					#       generic solution. Only uncomment this code if this problem is fixed, else
					#       collectors will get stuck.
				elif self.owner is not None and hasattr(self.owner, "notify_unit_path_blocked"):
					self.log.debug('PATH FOR UNIT %s is blocked. Delegating to owner %s', self, self.owner)
					self.owner.notify_unit_path_blocked(self)
					"""
				else:
					# generic solution: retry in 2 secs
					self.log.debug('PATH FOR UNIT %s is blocked. Retry in 2 secs', self)
					# technically, the ship doesn't move, but it is in the process of moving,
					# as it will continue soon in general. Needed in border cases for add_move_callback
					self.__is_moving = True
					Scheduler().add_new_object(self._move_tick, self,
					                           GAME_SPEED.TICKS_PER_SECOND * 2)
				self.log.debug("Unit %s: path is blocked, no way around", self)
				return

		if self._next_target is None:
			self._movement_finished()
			return
		else:
			self.__is_moving = True

		#setup movement

		# WORK IN PROGRESS
		move_time = self.get_unit_velocity()

		#location = fife.Location(self._instance.getLocation().getLayer())
		self._exact_model_coords.set(self._next_target.x, self._next_target.y, 0)
		self._fife_location.setExactLayerCoordinates(self._exact_model_coords)

		UnitClass.ensure_action_loaded(self._action_set_id, self._move_action) # lazy load move action

		# it's safe to use location here (thisown is 0, set by swig, and setLocation uses reference)
		self._instance.move(self._move_action+"_"+str(self._action_set_id), self._fife_location,
												float(self.session.timer.get_ticks(1)) / move_time[0])
		# coords per sec

		diagonal = self._next_target.x != self.position.x and self._next_target.y != self.position.y
		#self.log.debug("%s registering move tick in %s ticks", self, move_time[int(diagonal)])
		Scheduler().add_new_object(self._move_tick, self, move_time[int(diagonal)])

		# check if a conditional callback becomes true
		for cond in self._conditional_callbacks.keys(): # iterate of copy of keys to be able to delete
			if cond():
				# start callback when this function is done
				Scheduler().add_new_object(self._conditional_callbacks[cond], self)
				del self._conditional_callbacks[cond]

	def teleport(self, destination, callback=None, destination_in_building=False):
		"""Like move, but nearly instantaneous"""
		if hasattr(destination, "position"):
			destination_coord = destination.position.center.to_tuple()
		else:
			destination_coord = destination
		self.move(destination, callback=callback, destination_in_building=destination_in_building, path=[destination_coord])

	def add_move_callback(self, callback):
		"""Registers callback to be executed when movement of unit finishes.
		This has no effect if the unit isn't moving."""
		if self.is_moving():
			self.move_callbacks.append(callback)

	def add_blocked_callback(self, blocked_callback):
		"""Registers callback to be executed when movement of the unit gets blocked."""
		self.blocked_callbacks.append(blocked_callback)

	def add_conditional_callback(self, condition, callback):
		"""Adds a callback, that gets called, if, at any time of the movement, the condition becomes
		True. The condition is checked every move_tick. After calling the callback, it is removed."""
		assert callable(condition)
		assert callable(callback)
		self._conditional_callbacks[condition] = callback

	def get_unit_velocity(self):
		"""Returns the number of ticks that it takes to do a straight (i.e. vertical or horizontal)
		or diagonal movement as a tuple in this order.
		@return: (int, int)
		"""
		tile = self.session.world.get_tile(self.position)
		if self.id in tile.velocity:
			return tile.velocity[self.id]
		else:
			return (12, 17) # standard values

	def get_move_target(self):
		return self.path.get_move_target()

	def save(self, db):
		super(MovingObject, self).save(db)
		# NOTE: _move_action is currently not yet saved and neither is blocked_callback.
		self.path.save(db, self.worldid)

	def load(self, db, worldid):
		super(MovingObject, self).load(db, worldid)
		x, y = db("SELECT x, y FROM unit WHERE rowid = ?", worldid)[0]
		self.__init(x, y)
		path_loaded = self.path.load(db, worldid)
		if path_loaded:
			self.__is_moving = True
			self._setup_move()
			Scheduler().add_new_object(self._move_tick, self, run_in=0)