Source code for evennia.contrib.grid.xyzgrid.commands

"""

XYZ-aware commands

Just add the XYZGridCmdSet to the default character cmdset to override
the commands with XYZ-aware equivalents.

"""

from collections import namedtuple

from django.conf import settings

from evennia import CmdSet, InterruptCommand, default_cmds
from evennia.commands.default import building
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid
from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom
from evennia.utils import ansi
from evennia.utils.utils import class_from_module, delay, list_to_string

COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)


# temporary store of goto/path data when using the auto-stepper
PathData = namedtuple("PathData", ("target", "xymap", "directions", "step_sequence", "task"))


[docs]class CmdXYZTeleport(building.CmdTeleport): """ teleport object to another location Usage: tel/switch [<object> to||=] <target location> tel/switch [<object> to||=] (X,Y[,Z]) Examples: tel Limbo tel/quiet box = Limbo tel/tonone box tel (3, 3, the small cave) tel (4, 1) # on the same map tel/map Z|mapname Switches: quiet - don't echo leave/arrive messages to the source/target locations for the move. intoexit - if target is an exit, teleport INTO the exit object instead of to its destination tonone - if set, teleport the object to a None-location. If this switch is set, <target location> is ignored. Note that the only way to retrieve an object from a None location is by direct #dbref reference. A puppeted object cannot be moved to None. loc - teleport object to the target's location instead of its contents map - show coordinate map of given Zcoord/mapname. Teleports an object somewhere. If no object is given, you yourself are teleported to the target location. If (X,Y) or (X,Y,Z) coordinates are given, the target is a location on the XYZGrid. """ def _search_by_xyz(self, inp): inp = inp.strip("()") X, Y, *Z = inp.split(",", 2) if Z: # Z was specified Z = Z[0] else: # use current location's Z, if it exists try: xyz = self.caller.location.xyz except AttributeError: self.caller.msg( "Z-coordinate is also required since you are not currently " "in a room with a Z coordinate of its own." ) raise InterruptCommand else: Z = xyz[2] # search by coordinate X, Y, Z = str(X).strip(), str(Y).strip(), str(Z).strip() try: self.destination = XYZRoom.objects.get_xyz(xyz=(X, Y, Z)) except XYZRoom.DoesNotExist: self.caller.msg(f"Found no target XYZRoom at ({X},{Y},{Z}).") raise InterruptCommand
[docs] def parse(self): default_cmds.MuxCommand.parse(self) self.obj_to_teleport = self.caller self.destination = None if self.rhs: self.obj_to_teleport = self.caller.search(self.lhs, global_search=True) if not self.obj_to_teleport: self.caller.msg("Did not find object to teleport.") raise InterruptCommand if all(char in self.rhs for char in ("(", ")", ",")): # search by (X,Y) or (X,Y,Z) self._search_by_xyz(self.rhs) else: # fallback to regular search by name/alias self.destination = self.caller.search(self.rhs, global_search=True) elif self.lhs: if all(char in self.lhs for char in ("(", ")", ",")): self._search_by_xyz(self.lhs) else: self.destination = self.caller.search(self.lhs, global_search=True)
[docs]class CmdXYZOpen(building.CmdOpen): """ open a new exit from the current room Usage: open <new exit>[;alias;..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = <destination> open <new exit>[;alias;..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = (X,Y,Z) Handles the creation of exits. If a destination is given, the exit will point there. The destination can also be given as an (X,Y,Z) coordinate on the XYZGrid - this command is used to link non-grid rooms to the grid and vice-versa. The <return exit> argument sets up an exit at the destination leading back to the current room. Apart from (X,Y,Z) coordinate, destination name can be given both as a #dbref and a name, if that name is globally unique. Examples: open kitchen = Kitchen open north, south = Town Center open cave mouth;cave = (3, 4, the small cave) """
[docs] def parse(self): building.ObjManipCommand.parse(self) self.location = self.caller.location if not self.args or not self.rhs: self.caller.msg( "Usage: open <new exit>[;alias...][:typeclass]" "[,<return exit>[;alias..][:typeclass]]] " "= <destination or (X,Y,Z)>" ) raise InterruptCommand if not self.location: self.caller.msg("You cannot create an exit from a None-location.") raise InterruptCommand if all(char in self.rhs for char in ("(", ")", ",")): # search by (X,Y) or (X,Y,Z) inp = self.rhs.strip("()") X, Y, *Z = inp.split(",", 2) if not Z: self.caller.msg("A full (X,Y,Z) coordinate must be given for the destination.") raise InterruptCommand Z = Z[0] # search by coordinate X, Y, Z = str(X).strip(), str(Y).strip(), str(Z).strip() try: self.destination = XYZRoom.objects.get_xyz(xyz=(X, Y, Z)) except XYZRoom.DoesNotExist: self.caller.msg(f"Found no target XYZRoom at ({X},{Y},{Z}).") raise InterruptCommand else: # regular search query self.destination = self.caller.search(self.rhs, global_search=True) if not self.destination: raise InterruptCommand self.exit_name = self.lhs_objs[0]["name"] self.exit_aliases = self.lhs_objs[0]["aliases"] self.exit_typeclass = self.lhs_objs[0]["option"]
[docs]class CmdGoto(COMMAND_DEFAULT_CLASS): """ Go to a named location in this area via the shortest path. Usage: path <location> - find shortest path to target location (don't move) goto <location> - auto-move to target location, using shortest path path - show current target location and shortest path goto - abort current goto, otherwise show current path path clear - clear current path Finds the shortest route to a location in your current area and can then automatically walk you there. Builders can optionally specify a specific grid coordinate (X,Y) to go to. """ key = "goto" aliases = "path" help_category = "General" locks = "cmd:all()" # how quickly to step (seconds) auto_step_delay = 2 default_xyz_path_interrupt_msg = "Pathfinding interrupted here." def _search_by_xyz(self, inp, xyz_start): inp = inp.strip("()") X, Y = inp.split(",", 2) Z = xyz_start[2] # search by coordinate X, Y, Z = str(X).strip(), str(Y).strip(), str(Z).strip() try: return XYZRoom.objects.get_xyz(xyz=(X, Y, Z)) except XYZRoom.DoesNotExist: self.caller.msg(f"Could not find a room at ({X},{Y}) (Z={Z}).") return None def _search_by_key_and_alias(self, inp, xyz_start): Z = xyz_start[2] candidates = list(XYZRoom.objects.filter_xyz(xyz=("*", "*", Z))) return self.caller.search(inp, candidates=candidates) def _auto_step( self, caller, session, target=None, xymap=None, directions=None, step_sequence=None, step=True, ): path_data = caller.ndb.xy_path_data if target: # start/replace an old path if we provide the data for it if path_data and path_data.task and path_data.task.active(): # stop any old task in its tracks path_data.task.cancel() path_data = caller.ndb.xy_path_data = PathData( target=target, xymap=xymap, directions=directions, step_sequence=step_sequence, task=None, ) if step and path_data: step_sequence = path_data.step_sequence try: direction = path_data.directions.pop(0) current_node = path_data.step_sequence.pop(0) first_link = path_data.step_sequence.pop(0) except IndexError: caller.msg("Target reached.", session=session) caller.ndb.xy_path_data = None return # verfy our current location against the expected location expected_xyz = (current_node.X, current_node.Y, current_node.Z) location = caller.location try: xyz_start = location.xyz except AttributeError: caller.ndb.xy_path_data = None caller.msg("Goto aborted - outside of area.", session=session) return if xyz_start != expected_xyz: # we are not where we expected to be (maybe the user moved # manually) - we must recalculate the path to target caller.msg("Path changed - recalculating ('goto' to abort)", session=session) try: xyz_end = path_data.target.xyz except AttributeError: caller.ndb.xy_path_data = None caller.msg("Goto aborted - target outside of area.", session=session) return if xyz_start[2] != xyz_end[2]: # can't go to another map caller.ndb.xy_path_data = None caller.msg("Goto aborted - target outside of area.", session=session) return # recalculate path xy_start = xyz_start[:2] xy_end = xyz_end[:2] directions, step_sequence = path_data.xymap.get_shortest_path(xy_start, xy_end) # try again with this path, rebuilding the data try: direction = directions.pop(0) current_node = step_sequence.pop(0) first_link = step_sequence.pop(0) except IndexError: caller.msg("Target reached.", session=session) caller.ndb.xy_path_data = None return path_data = caller.ndb.xy_path_data = PathData( target=path_data.target, xymap=path_data.xymap, directions=directions, step_sequence=step_sequence, task=None, ) # the map can itself tell the stepper to stop the auto-step prematurely interrupt_node_or_link = None # pop any extra links up until the next node - these are # not useful when dealing with exits while step_sequence: if not interrupt_node_or_link and step_sequence[0].interrupt_path: interrupt_node_or_link = step_sequence[0] if hasattr(step_sequence[0], "node_index"): break step_sequence.pop(0) # the exit name does not need to be the same as the cardinal direction! exit_name, *_ = first_link.spawn_aliases.get( direction, current_node.direction_spawn_defaults.get(direction, ("unknown",)) ) exit_obj = caller.search(exit_name) if not exit_obj: # extra safety measure to avoid trying to walk over and over # if there's something wrong with the exit's name caller.msg(f"No exit '{exit_name}' found at current location. Aborting goto.") caller.ndb.xy_path_data = None return if interrupt_node_or_link: # premature stop of pathfind-step because of map node/link of interrupt type if hasattr(interrupt_node_or_link, "node_index"): message = exit_obj.destination.attributes.get( "xyz_path_interrupt_msg", default=self.default_xyz_path_interrupt_msg ) # we move into the node/room and then stop caller.execute_cmd(exit_name, session=session) else: # if the link is interrupted we don't cross it at all message = exit_obj.attributes.get( "xyz_path_interrupt_msg", default=self.default_xyz_path_interrupt_msg ) caller.msg(message) return # do the actual move - we use the command to allow for more obvious overrides caller.execute_cmd(exit_name, session=session) # namedtuples are unmutables, so we recreate and store # with the new task caller.ndb.xy_path_data = PathData( target=path_data.target, xymap=path_data.xymap, directions=path_data.directions, step_sequence=path_data.step_sequence, task=delay(self.auto_step_delay, self._auto_step, caller, session), )
[docs] def func(self): """ Implement command """ caller = self.caller goto_mode = self.cmdname == "goto" # check if we have an existing path path_data = caller.ndb.xy_path_data if not self.args: if path_data: target_name = path_data.target.get_display_name(caller) task = path_data.task if goto_mode: if task and task.active(): task.cancel() caller.msg(f"Aborted auto-walking to {target_name}.") return # goto/path-command will show current path current_path = list_to_string([f"|w{step}|n" for step in path_data.directions]) moving = "(moving)" if task and task.active() else "" caller.msg(f"Path to {target_name}{moving}: {current_path}") else: caller.msg("Usage: goto|path [<location>]") return if not goto_mode and self.args == "clear" and path_data: # in case there is a target location 'clear', this is only # used if path data already exists. caller.ndb.xy_path_data = None caller.msg("Cleared goto-path.") return # find target xyzgrid = get_xyzgrid() try: xyz_start = caller.location.xyz except AttributeError: self.caller.msg("Cannot path-find since the current location is not on the grid.") return allow_xyz_query = caller.locks.check_lockstring(caller, "perm(Builder)") if allow_xyz_query and all(char in self.args for char in ("(", ")", ",")): # search by (X,Y) target = self._search_by_xyz(self.args, xyz_start) if not target: return else: # search by normal key/alias target = self._search_by_key_and_alias(self.args, xyz_start) if not target: return try: xyz_end = target.xyz except AttributeError: self.caller.msg("Target location is not on the grid and cannot be auto-walked to.") return xymap = xyzgrid.get_map(xyz_start[2]) # we only need the xy coords once we have the map xy_start = xyz_start[:2] xy_end = xyz_end[:2] directions, step_sequence = xymap.get_shortest_path(xy_start, xy_end) caller.msg( f"There are {len(directions)} steps to {target.get_display_name(caller)}: " f"|w{list_to_string(directions, endsep='|n, and finally|w')}|n" ) # create data for display and start stepping if we used goto self._auto_step( caller, self.session, target=target, xymap=xymap, directions=directions, step_sequence=step_sequence, step=goto_mode, )
[docs]class CmdMap(COMMAND_DEFAULT_CLASS): """ Show a map of an area Usage: map [Zcoord] map list This is a builder-command. """ key = "map" locks = "cmd:perm(Builders)"
[docs] def func(self): """Implement command""" xyzgrid = get_xyzgrid() Z = None if not self.args: # show current area's map location = self.caller.location try: xyz = location.xyz except AttributeError: self.caller.msg("Your current location is not on the grid.") return Z = xyz[2] elif self.args.strip().lower() == "list": xymaps = "\n ".join(str(repr(xymap)) for xymap in xyzgrid.all_maps()) self.caller.msg(f"Maps (Z coords) on the grid:\n |w{xymaps}") return else: Z = self.args xymap = xyzgrid.get_map(Z) if not xymap: self.caller.msg( f"XYMap '{Z}' is not found on the grid. Try 'map list' to see " "available maps/Zcoords." ) return self.caller.msg(ansi.raw(xymap.mapstring))
[docs]class XYZGridCmdSet(CmdSet): """ Cmdset for easily adding the above cmds to the character cmdset. """ key = "xyzgrid_cmdset"
[docs] def at_cmdset_creation(self): self.add(CmdXYZTeleport()) self.add(CmdXYZOpen()) self.add(CmdGoto()) self.add(CmdMap())
# Optional fly/dive commands to move between maps (enable # full 3D-grid movements)
[docs]class CmdFlyAndDive(COMMAND_DEFAULT_CLASS): """ Fly or Dive up and down. Usage: fly dive Will fly up one room or dive down one room at your current position. If there is no room above/below you, your movement will fail. """ key = "fly or dive" aliases = ("fly", "dive")
[docs] def func(self): caller = self.caller action = self.cmdname try: xyz_start = caller.location.xyz except AttributeError: caller.msg(f"You cannot {action} here.") return try: zcoord = int(xyz_start[2]) except ValueError: caller.msg(f"You cannot {action} here.") return if action == "fly": diff = 1 direction = "upwards" from_direction = "below" error_message = "Can't fly here - you'd hit your head." elif action == "dive": diff = -1 direction = "downwards" from_direction = "above" error_message = "Can't dive here - you'd just fall flat on the ground." else: caller.msg("You must decide if you want to |wfly|n up or |wdive|n down.") return target_coord = (str(xyz_start[0]), str(xyz_start[1]), zcoord + diff) try: target = XYZRoom.objects.get_xyz(xyz=(target_coord)) except XYZRoom.DoesNotExist: # no available room above/below to fly/dive to caller.msg(error_message) return # action succeeds, we have a target. One could picture being able to # lock certain rooms from flight/dive, here we allow it as long as there # is a suitable room above/below. caller.location.msg_contents(f"$You() {action} {direction}.", from_obj=caller) caller.move_to(target, quiet=True) target.msg_contents( f"$You() {action} from {from_direction}.", from_obj=caller, exclude=[caller] )
[docs]class XYZGridFlyDiveCmdSet(CmdSet): """ Optional cmdset if you want the fly/dive commands to move in a 3D environment. """ key = "xyzgrid_flydive_cmdset"
[docs] def at_cmdset_creation(self): self.add(CmdFlyAndDive())