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 __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 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 __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 __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
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)
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
def clear_change_listeners(self): """Removes all change listeners""" self.__listeners = WeakMethodList()
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
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)
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)