From cc820beb985dcd0c9bca1ecc450a5374cf002f2e Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 4 Jul 2021 18:03:07 +0200 Subject: [PATCH] First working function of map-spawning --- evennia/contrib/xyzgrid/map_legend.py | 67 +++--- evennia/contrib/xyzgrid/tests.py | 55 ++++- evennia/contrib/xyzgrid/xymap.py | 84 ++++---- evennia/contrib/xyzgrid/xyzgrid.py | 38 ++-- evennia/contrib/xyzgrid/xyzroom.py | 291 +++++++++++++++----------- 5 files changed, 327 insertions(+), 208 deletions(-) diff --git a/evennia/contrib/xyzgrid/map_legend.py b/evennia/contrib/xyzgrid/map_legend.py index 0ab08816a0..fd51957187 100644 --- a/evennia/contrib/xyzgrid/map_legend.py +++ b/evennia/contrib/xyzgrid/map_legend.py @@ -73,7 +73,7 @@ class MapNode: direction_spawn_defaults = { 'n': ('north', 'n'), 'ne': ('northeast', 'ne', 'north-east'), - 'e': ('east',), + 'e': ('east', 'e'), 'se': ('southeast', 'se', 'south-east'), 's': ('south', 's'), 'sw': ('southwest', 'sw', 'south-west'), @@ -231,7 +231,7 @@ class MapNode: """ return self.symbol if self.display_symbol is None else self.display_symbol - def get_spawn_coords(self): + def get_spawn_xyz(self): """ This should return the XYZ-coordinates for spawning this node. This normally the XYZ of the current map, but for traversal-nodes, it can also be the location @@ -260,15 +260,15 @@ class MapNode: # a 'virtual' node. return - coord = self.get_spawn_coords() + xyz = self.get_spawn_xyz() try: - nodeobj = NodeTypeclass.objects.get_xyz(coord=coord) + nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz) except NodeTypeclass.DoesNotExist: # create a new entity with proper coordinates etc nodeobj, err = NodeTypeclass.create( self.prototype.get('key', 'An Empty room'), - coord=coord + xyz=xyz ) if err: raise RuntimeError(err) @@ -277,12 +277,12 @@ class MapNode: spawner.batch_update_objects_with_prototype( self.prototype, objects=[nodeobj], exact=False) - def spawn_links(self, only_directions=None): + def spawn_links(self, directions=None): """ Build actual in-game exits based on the links out of this room. Args: - only_directions (list, optional): If given, this should be a list of supported + directions (list, optional): If given, this should be a list of supported directions (n, ne, etc). Only links in these directions will be spawned for this node. @@ -290,7 +290,12 @@ class MapNode: the entire XYZgrid. This creates/syncs all exits to their locations and destinations. """ - coord = (self.X, self.Y, self.Z) + if not self.prototype: + # no exits to spawn out of a 'virtual' node. + return + + xyz = (self.X, self.Y, self.Z) + direction_limits = directions global ExitTypeclass if not ExitTypeclass: @@ -308,7 +313,7 @@ class MapNode: # we need to search for exits in all directions since some # may have been removed since last sync linkobjs = {exi.db_key.lower(): exi - for exi in ExitTypeclass.objects.filter_xyz(coord=coord)} + for exi in ExitTypeclass.objects.filter_xyz(xyz=xyz)} # figure out if the topology changed between grid and map (will always # build all exits first run) @@ -321,20 +326,25 @@ class MapNode: else: # missing in linkobjs - create a new exit key, aliases, direction, link = maplinks[differing_key] - exitnode = self.links[direction] - linkobjs[direction] = ExitTypeclass.create( - # either get name from the prototype or use our custom set + if direction_limits and direction not in direction_limits: + continue + + exitnode = self.links[direction] + exi, err = ExitTypeclass.create( key, - coord=coord, - destination_coord=exitnode.get_spawn_coords(), + xyz=xyz, + xyz_destination=exitnode.get_spawn_xyz(), aliases=aliases, ) + if err: + raise RuntimeError(err) + linkobjs[key.lower()] = exi # apply prototypes to catch any changes - for direction, linkobj in linkobjs: + for key, linkobj in linkobjs.items(): spawner.batch_update_objects_with_prototype( - maplinks[direction].prototype, objects=[linkobj], exact=False) + maplinks[key.lower()][3].prototype, objects=[linkobj], exact=False) def unspawn(self): """ @@ -345,8 +355,10 @@ class MapNode: if not NodeTypeclass: from .room import XYZRoom as NodeTypeclass + xyz = (self.X, self.Y, self.Z) + try: - nodeobj = NodeTypeclass.objects.get_xyz(coord=coord) + nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz) except NodeTypeclass.DoesNotExist: # no object exists pass @@ -364,7 +376,7 @@ class TransitionMapNode(MapNode): to this node. Properties: - - `target_map_coord` (tuple) - the (X, Y, Z) coordinate of a node on the other map to teleport + - `target_map_xyz` (tuple) - the (X, Y, Z) coordinate of a node on the other map to teleport to when moving to this node. This should not be another TransitionMapNode (see below for how to make a two-way link). @@ -380,16 +392,21 @@ class TransitionMapNode(MapNode): """ symbol = 'T' display_symbol = ' ' - # X,Y,Z coordinates of target node (not a transitionalmapnode) - taget_map_coord = (None, None, None) + # X,Y,Z coordinates of target node + taget_map_xyz = (None, None, None) - def get_spawn_coords(self): + def get_spawn_xyz(self): """ Make sure to return the coord of the *target* - this will be used when building the exit to this node (since the prototype is None, this node itself will not be built). """ - return self.target_map_coord + if any(True for coord in self.target_map_xyz if coord in (None, 'unset')): + raise MapParserError(f"(Z={self.xymap.Z}) has not defined its " + "`.target_map_xyz` property. It must point " + "to another valid xymap (Z coordinate).", self) + + return self.target_map_xyz def build_links(self): """Check so we don't have too many links""" @@ -449,7 +466,7 @@ class MapLink: of the link is only used to determine its destination). This can be overridden on a per-direction basis. - `spawn_aliases` (list): A list of [key, alias, alias, ...] for the node to use when spawning - exits from this link. If not given, a sane set of defaults (n=north etc) will be used. This + exits from this link. If not given, a sane set of defaults ((north, n) etc) will be used. This is required if you use any custom directions outside of the cardinal directions + up/down. """ @@ -1017,8 +1034,8 @@ class MapTransitionMapNode(TransitionMapNode): """Transition-target to other map""" symbol = "T" display_symbol = " " - target_map_coords = (0, 0, 'unset') # must be changed - prototype = None # important! + prototype = None # important to leave None! + target_map_xyz = (None, None, None) # must be set manually class InterruptMapNode(MapNode): diff --git a/evennia/contrib/xyzgrid/tests.py b/evennia/contrib/xyzgrid/tests.py index 7e716568f1..87ef64cef6 100644 --- a/evennia/contrib/xyzgrid/tests.py +++ b/evennia/contrib/xyzgrid/tests.py @@ -9,7 +9,7 @@ from random import randint from unittest import TestCase from parameterized import parameterized from django.test import override_settings -from . import xymap, xyzgrid, map_legend +from . import xymap, xyzgrid, map_legend, xyzroom MAP1 = """ @@ -341,14 +341,36 @@ MAP12b = r""" """ -class TestMap1(TestCase): +class _MapTest(TestCase): + """ + Parent for map tests + + """ + map_data = { + 'map': MAP1, + 'zcoord': "map1", + + } + map_display = MAP1_DISPLAY + + def setUp(self): + """Set up grid and map""" + self.grid, err = xyzgrid.XYZGrid.create("testgrid") + self.grid.add_maps(self.map_data) + self.map = self.grid.get(self.map_data['zcoord']) + + def tearDown(self): + self.grid.delete() + + +class TestMap1(_MapTest): """ Test the Map class with a simple 4-node map """ - def setUp(self): - self.map = xymap.XYMap({"map": MAP1}, Z="testmap") - self.map.parse() + # def setUp(self): + # self.map = xymap.XYMap({"map": MAP1}, Z="testmap") + # self.map.parse() def test_str_output(self): """Check the display_map""" @@ -1057,17 +1079,23 @@ class TestXYZGrid(TestCase): def test_spawn(self): """Spawn objects for the grid""" self.grid.spawn() + # import sys + # sys.stderr.write("\nrooms: " + repr(xyzroom.XYZRoom.objects.all())) + # sys.stderr.write("\n\nexits: " + repr(xyzroom.XYZExit.objects.all()) + "\n") + + self.assertEqual(xyzroom.XYZRoom.objects.all().count(), 4) + self.assertEqual(xyzroom.XYZExit.objects.all().count(), 8) # map transitions class Map12aTransition(map_legend.MapTransitionMapNode): symbol = "T" - target_map_coords = (1, 0, "map12b") + target_map_xyz = (1, 0, "map12b") class Map12bTransition(map_legend.MapTransitionMapNode): symbol = "T" - target_map_coords = (0, 1, "map12a") + target_map_xyz = (0, 1, "map12a") class TestXYZGridTransition(TestCase): @@ -1112,4 +1140,17 @@ class TestXYZGridTransition(TestCase): Spawn the two maps into actual objects. """ + # from evennia import set_trace;set_trace() self.grid.spawn() + + self.assertEqual(xyzroom.XYZRoom.objects.all().count(), 6) + self.assertEqual(xyzroom.XYZExit.objects.all().count(), 10) + + room1 = xyzroom.XYZRoom.objects.get_xyz(xyz=(0, 1, 'map12a')) + room2 = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 0, 'map12b')) + east_exit = [exi for exi in room1.exits if exi.db_key == 'east'][0] + west_exit = [exi for exi in room2.exits if exi.db_key == 'west'][0] + + # make sure exits traverse the maps + self.assertEqual(east_exit.db_destination, room2) + self.assertEqual(west_exit.db_destination, room1) diff --git a/evennia/contrib/xyzgrid/xymap.py b/evennia/contrib/xyzgrid/xymap.py index bfb2ab5eed..4f4266af7d 100644 --- a/evennia/contrib/xyzgrid/xymap.py +++ b/evennia/contrib/xyzgrid/xymap.py @@ -272,7 +272,8 @@ class XYMap: return "\n".join("".join(line) for line in self.display_map[::-1]) def __repr__(self): - return f"" + return (f"") def reload(self, map_module_or_dict=None): """ @@ -498,20 +499,20 @@ class XYMap: # store self.display_map = display_map - def _get_topology_around_coord(self, coord, dist=2): + def _get_topology_around_coord(self, xy, dist=2): """ Get all links and nodes up to a certain distance from an XY coordinate. Args: - coord (tuple), the X,Y coordinate of the center point. + xy (tuple), the X,Y coordinate of the center point. dist (int): How many nodes away from center point to find paths for. Returns: - tuple: A tuple of 5 elements `(coords, xmin, xmax, ymin, ymax)`, where the + tuple: A tuple of 5 elements `(xy_coords, xmin, xmax, ymin, ymax)`, where the first element is a list of xy-coordinates (on xygrid) for all linked nodes within range. This is meant to be used with the xygrid for extracting a subset for display purposes. The others are the minimum size of the rectangle - surrounding the area containing `coords`. + surrounding the area containing `xy_coords`. Notes: This performs a depth-first pass down the the given dist. @@ -542,7 +543,7 @@ class XYMap: return points, xmin, xmax, ymin, ymax - center_node = self.get_node_from_coord(coord) + center_node = self.get_node_from_coord(xy) points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist) return list(set(points)), xmin, xmax, ymin, ymax @@ -591,7 +592,7 @@ class XYMap: pickle.dump((self.mapstring, self.dist_matrix, self.pathfinding_routes), fil, protocol=4) - def spawn_nodes(self, coord=(None, None)): + def spawn_nodes(self, xy=('*', '*')): """ Convert the nodes of this XYMap into actual in-world rooms by spawning their related prototypes in the correct coordinate positions. This must be done *first* @@ -599,58 +600,61 @@ class XYMap: to exist. It's also possible to only spawn a subset of the map Args: - coord (tuple, optional): An (X,Y) coordinate of node(s). `None` acts as a wildcard. + xy (tuple, optional): An (X,Y) coordinate of node(s). `'*'` acts as a wildcard. Examples: - - `coord=(1, 3) - spawn (1,3) coordinate only. - - `coord=(None, 1) - spawn all nodes in the first row of the map only. - - `coord=(None, None)` - spawn all nodes + - `xy=(1, 3) - spawn (1,3) coordinate only. + - `xy=('*', 1) - spawn all nodes in the first row of the map only. + - `xy=('*', '*')` - spawn all nodes Returns: list: A list of nodes that were spawned. """ - x, y = coord - + x, y = xy + wildcard = '*' spawned = [] + for node in self.node_index_map.values(): - if (x is None or x == node.X) and (y is None or y == node.Y): + if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)): node.spawn() spawned.append(node) return spawned - def spawn_links(self, coord=(None, None), nodes=None, only_directions=None): + def spawn_links(self, xy=('*', '*'), nodes=None, directions=None): """ Convert links of this XYMap into actual in-game exits by spawning their related prototypes. It's possible to only spawn a specic exit by specifying the node and Args: - coord (tuple, optional): An (X,Y) coordinate of node(s). `None` acts as a wildcard. + xy (tuple, optional): An (X,Y) coordinate of node(s). `'*'` acts as a wildcard. nodes (list, optional): If given, only consider links out of these nodes. This also - affects `coords`, so that if there are no nodes of given coords in `nodes`, no + affects `xy`, so that if there are no nodes of given coords in `nodes`, no links will be spawned at all. directions (list, optional): A list of cardinal directions ('n', 'ne' etc). If given, - sync only the exit in the given directions (`coords` limits which links out of which - nodes should be considered). `None` acts as a wildcard. + sync only the exit in the given directions (`xy` limits which links out of which + nodes should be considered). If unset, there are no limits to directions. Examples: - - `coord=(1, 3 )`, `direction='ne'` - sync only the north-eastern exit + - `xy=(1, 3 )`, `direction='ne'` - sync only the north-eastern exit out of the (1, 3) node. """ - x, y = coord + x, y = xy + wildcard = '*' + if not nodes: nodes = self.node_index_map.values() for node in nodes: - if (x is None or x == node.X) and (y is None or y == node.Y): - node.spawn_links(only_directions=only_directions) + if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)): + node.spawn_links(directions=directions) - def get_node_from_coord(self, coords): + def get_node_from_coord(self, xy): """ Get a MapNode from a coordinate. Args: - coords (tuple): X,Y coordinates on XYgrid. + xy (tuple): X,Y coordinate on XYgrid. Returns: MapNode: The node found at the given coordinates. Returns @@ -664,12 +668,12 @@ class XYMap: if not self.XYgrid: self.parse() - iX, iY = coords + iX, iY = xy if not ((0 <= iX <= self.max_X) and (0 <= iY <= self.max_Y)): - raise MapError(f"get_node_from_coord got coordinate {coords} which is " + raise MapError(f"get_node_from_coord got coordinate {xy} which is " f"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y}).") try: - return self.XYgrid[coords[0]][coords[1]] + return self.XYgrid[iX][iY] except KeyError: return None @@ -686,14 +690,14 @@ class XYMap: """ return self.symbol_map.get(symbol, []) - def get_shortest_path(self, startcoord, endcoord): + def get_shortest_path(self, start_xy, end_xy): """ Get the shortest route between two points on the grid. Args: - startcoord (tuple): A starting (X,Y) coordinate on the XYgrid (in-game coordinate) for + start_xy (tuple): A starting (X,Y) coordinate on the XYgrid (in-game coordinate) for where we start from. - endcoord (tuple or MapNode): The end (X,Y) coordinate on the XYgrid (in-game coordinate) + end_xy (tuple or MapNode): The end (X,Y) coordinate on the XYgrid (in-game coordinate) we want to find the shortest route to. Returns: @@ -702,8 +706,8 @@ class XYMap: the full path including the start- and end-node. """ - startnode = self.get_node_from_coord(startcoord) - endnode = self.get_node_from_coord(endcoord) + startnode = self.get_node_from_coord(start_xy) + endnode = self.get_node_from_coord(end_xy) if not (startnode and endnode): # no node at given coordinate. No path is possible. @@ -751,7 +755,7 @@ class XYMap: return directions, path - def get_visual_range(self, coord, dist=2, mode='nodes', + def get_visual_range(self, xy, dist=2, mode='nodes', character='@', target=None, target_path_style="|y{display_symbol}|n", max_size=None, @@ -761,7 +765,7 @@ class XYMap: of nodes or grid points in every direction. Args: - coord (tuple): (X,Y) in-world coordinate location. If this is not the location + xy (tuple): (X,Y) in-world coordinate location. If this is not the location of a node on the grid, the `character` or the empty-space symbol (by default an empty space) will be shown. dist (int, optional): Number of gridpoints distance to show. Which @@ -771,7 +775,7 @@ class XYMap: number of xy grid points in all directions and doesn't care about if visible nodes are reachable or not. If 'nodes', distance measure how many linked nodes away from the center coordinate to display. - character (str, optional): Place this symbol at the `coord` position + character (str, optional): Place this symbol at the `xy` position of the displayed map. The center node' symbol is shown if this is falsy. target (tuple, optional): A target XY coordinate to go to. The path to this (or the beginning of said path, if outside of visual range) will be @@ -823,7 +827,7 @@ class XYMap: # @-# """ - iX, iY = coord + iX, iY = xy # convert inputs to xygrid width, height = self.max_x + 1, self.max_y + 1 ix, iy = max(0, min(iX * 2, width)), max(0, min(iY * 2, height)) @@ -835,14 +839,14 @@ class XYMap: gridmap = self.display_map ixc, iyc = ix, iy - elif dist is None or dist <= 0 or not self.get_node_from_coord(coord): + elif dist is None or dist <= 0 or not self.get_node_from_coord(xy): # There is no node at these coordinates. Show # nothing but ourselves or emptiness return character if character else self.empty_symbol elif mode == 'nodes': # dist measures only full, reachable nodes. - points, xmin, xmax, ymin, ymax = self._get_topology_around_coord(coord, dist=dist) + points, xmin, xmax, ymin, ymax = self._get_topology_around_coord(xy, dist=dist) ixc, iyc = ix - xmin, iy - ymin # note - override width/height here since our grid is @@ -878,7 +882,7 @@ class XYMap: else: _target_path_style = _default_callable - _, path = self.get_shortest_path(coord, target) + _, path = self.get_shortest_path(xy, target) maxstep = dist if mode == 'nodes' else dist / 2 nsteps = 0 diff --git a/evennia/contrib/xyzgrid/xyzgrid.py b/evennia/contrib/xyzgrid/xyzgrid.py index ec9508ed18..176672f534 100644 --- a/evennia/contrib/xyzgrid/xyzgrid.py +++ b/evennia/contrib/xyzgrid/xyzgrid.py @@ -106,12 +106,15 @@ class XYZGrid(DefaultScript): remove_objects (bool, optional): If the synced database objects (rooms/exits) should be removed alongside this map. """ + # from evennia import set_trace;set_trace() for zcoord in zcoords: if zcoord in self.db.map_data: self.db.map_data.pop(zcoord) if remove_objects: - # this should also remove all exits automatically - XYZRoom.objects.filter_xyz(coord=(None, None, zcoord)).delete() + # we can't batch-delete because we want to run the .delete + # method that also wipes exits and moves content to save locations + for xyzroom in XYZRoom.objects.filter_xyz(xyz=('*', '*', zcoord)): + xyzroom.delete() self.reload() def delete(self): @@ -121,41 +124,42 @@ class XYZGrid(DefaultScript): """ self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True) - def spawn(self, coord=(None, None, None), only_directions=None): + def spawn(self, xyz=('*', '*', '*'), directions=None): """ Create/recreate/update the in-game grid based on the stored Maps or for a specific Map or coordinate. Args: - coord (tuple, optional): An (X,Y,Z) coordinate, where Z is the name of the map. `None` + xyz (tuple, optional): An (X,Y,Z) coordinate, where Z is the name of the map. `'*'` acts as a wildcard. - only_directions (list, optional): A list of cardinal directions ('n', 'ne' etc). - If given, spawn exits only the given direction. `None` acts as a wildcard. + directions (list, optional): A list of cardinal directions ('n', 'ne' etc). + Spawn exits only the given direction. If unset, all needed directions are spawned. Examples: - - `coord=(1, 3, 'foo')` - sync a specific element of map 'foo' only. - - `coord=(None, None, 'foo') - sync all elements of map 'foo' - - `coord=(1, 3, None) - sync all (1,3) coordinates on all maps (rarely useful) - - `coord=(None, None, None)` - sync all maps. - - `coord=(1, 3, 'foo')`, `direction='ne'` - sync only the north-eastern exit + - `xyz=('*', '*', '*')` (default) - spawn/update all maps. + - `xyz=(1, 3, 'foo')` - sync a specific element of map 'foo' only. + - `xyz=('*', '*', 'foo') - sync all elements of map 'foo' + - `xyz=(1, 3, '*') - sync all (1,3) coordinates on all maps (rarely useful) + - `xyz=(1, 3, 'foo')`, `direction='ne'` - sync only the north-eastern exit out of the specific node on map 'foo'. """ - x, y, z = coord + x, y, z = xyz + wildcard = '*' - if z is None: + if z == wildcard: xymaps = self.grid - elif z in self.ndb.grid: - xymaps = [self.grid[z]] + elif self.ndb.grid and z in self.ndb.grid: + xymaps = {z: self.grid[z]} else: raise RuntimeError(f"The 'z' coordinate/name '{z}' is not found on the grid.") # first build all nodes/rooms for zcoord, xymap in xymaps.items(): logger.log_info(f"[grid] spawning/updating nodes for {zcoord} ...") - xymap.spawn_nodes(coord=(x, y)) + xymap.spawn_nodes(xy=(x, y)) # next build all links between nodes (including between maps) for zcoord, xymap in xymaps.items(): logger.log_info(f"[grid] spawning/updating links for {zcoord} ...") - xymap.spawn_links(coord=(x, y), only_directions=only_directions) + xymap.spawn_links(xy=(x, y), directions=directions) diff --git a/evennia/contrib/xyzgrid/xyzroom.py b/evennia/contrib/xyzgrid/xyzroom.py index 667faec3a4..d9dd3984a7 100644 --- a/evennia/contrib/xyzgrid/xyzroom.py +++ b/evennia/contrib/xyzgrid/xyzroom.py @@ -30,16 +30,15 @@ class XYZManager(ObjectManager): efficiently querying the room in the database based on XY coordinates. """ - def filter_xyz(self, coord=(None, None, 'map'), **kwargs): + def filter_xyz(self, xyz=('*', '*', '*'), **kwargs): """ - Filter queryset based on map as well as x- or y-coordinate, or both. The map-name is - required but not the coordinates - if only one coordinate is given, multiple rooms may be - returned from the same coordinate row/column. If both coordinates are omitted (set to - `None`), then all rooms of a given map is returned. + Filter queryset based on XYZ position on the grid. The Z-position is the name of the XYMap + Set a coordinate to `'*'` to act as a wildcard (setting all coords to `*` will thus find + *all* XYZ rooms). This will also find children of XYZRooms on the given coordinates. Kwargs: - coord (tuple, optional): A tuple (X, Y, Z) where each element is either - an `int`, `str` or `None`. `None` acts as a wild card. Note that + xyz (tuple, optional): A coordinate tuple (X, Y, Z) where each element is either + an `int` or `str`. The character `'*'` acts as a wild card. Note that the `Z`-coordinate is the name of the map (case-sensitive) in the XYZgrid contrib. **kwargs: All other kwargs are passed on to the query. @@ -48,25 +47,34 @@ class XYZManager(ObjectManager): with further filtering. """ - x, y, z = coord + x, y, z = xyz + wildcard = '*' - return self.filter_family( - (Q() if x is None else Q(db_tags__db_key=str(x), - db_tags__db_category=MAP_X_TAG_CATEGORY)), - (Q() if y is None else Q(db_tags__db_key=str(y), - db_tags__db_category=MAP_Y_TAG_CATEGORY)), - (Q() if z is None else Q(db_tags__db_key=str(z), - db_tags__db_category=MAP_Z_TAG_CATEGORY)), - **kwargs + + + return ( + self + .filter_family(**kwargs) + .filter( + Q() if x == wildcard + else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)) + .filter( + Q() if y == wildcard + else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)) + .filter( + Q() if z == wildcard + else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)) ) - def get_xyz(self, coord=(0, 0, 'map'), **kwargs): + def get_xyz(self, xyz=(0, 0, 'map'), **kwargs): """ - Always return a single matched entity directly. + Always return a single matched entity directly. This accepts no `*`-wildcards. + This will also find children of XYZRooms on the given coordinates. Kwargs: - coord (tuple): A tuple of `int` or `str` (not `None`). The `Z`-coordinate - acts as the name (case-sensitive) of the map in the XYZgrid contrib. + xyz (tuple): A coordinate tuple of `int` or `str` (not `'*'`, no wildcards are + allowed in get). The `Z`-coordinate acts as the name (case-sensitive) of the map in + the XYZgrid contrib. **kwargs: All other kwargs are passed on to the query. Returns: @@ -78,13 +86,27 @@ class XYZManager(ObjectManager): possible with a unique combination of x,y,z). """ - x, y, z = coord - return self.get_family( - Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY), - Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY), - Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY), - **kwargs - ) + x, y, z = xyz + + # mimic get_family + paths = [self.model.path] + [ + "%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model) + ] + kwargs["db_typeclass_path__in"] = paths + + try: + return ( + self + .filter(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY) + .filter(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY) + .filter(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY) + .get(**kwargs) + ) + except self.model.DoesNotExist: + inp = (f"xyz=({x},{y},{z}), " + + ",".join(f"{key}={val}" for key, val in kwargs.items())) + raise self.model.DoesNotExist(f"{self.model.__name__} " + f"matching query {inp} does not exist.") class XYZExitManager(XYZManager): @@ -94,17 +116,19 @@ class XYZExitManager(XYZManager): """ - def filter_xyz_exit(self, coord=(None, None, 'map'), - destination_coord=(None, None, 'map'), **kwargs): + def filter_xyz_exit(self, xyz=('*', '*', '*'), + xyz_destination=('*', '*', '*'), **kwargs): """ Used by exits (objects with a source and -destination property). - Find all exits out of a source or to a particular destination. + Find all exits out of a source or to a particular destination. This will also find + children of XYZExit on the given coords.. Kwargs: - coord (tuple, optional): A tuple (X, Y, Z) for the source location. Each - element is either an `int`, `str` or `None`. `None` acts as a wild card. Note that + xyz (tuple, optional): A coordinate (X, Y, Z) for the source location. Each + element is either an `int` or `str`. The character `'*'` is used as a wildcard - + so setting all coordinates to the wildcard will return *all* XYZExits. the `Z`-coordinate is the name of the map (case-sensitive) in the XYZgrid contrib. - destination_coord (tuple, optional): Same as the `coord` but for the destination of the + xyz_destination (tuple, optional): Same as `xyz` but for the destination of the exit. **kwargs: All other kwargs are passed on to the query. @@ -113,42 +137,52 @@ class XYZExitManager(XYZManager): with further filtering. Notes: - Depending on what coordinates are set to `None`, this can be used to + Depending on what coordinates are set to `*`, this can be used to e.g. find all exits in a room, or leading to a room or even to rooms in a particular X/Y row/column. - In the XYZgrid, `z != zdest` means a _transit_ between different maps. + In the XYZgrid, `z_source != z_destination` means a _transit_ between different maps. """ - x, y, z = coord - xdest, ydest, zdest = destination_coord + x, y, z = xyz + xdest, ydest, zdest = xyz_destination + wildcard = '*' - return self.filter_family( - (Q() if x is None else Q(db_tags__db_key=str(x), - db_tags__db_category=MAP_X_TAG_CATEGORY)), - (Q() if y is None else Q(db_tags__db_key=str(y), - db_tags__db_category=MAP_Y_TAG_CATEGORY)), - (Q() if z is None else Q(db_tags__db_key=str(z), - db_tags__db_category=MAP_Z_TAG_CATEGORY)), - (Q() if xdest is None else Q(db_tags__db_key=str(xdest), - db_tags__db_category=MAP_XDEST_TAG_CATEGORY)), - (Q() if ydest is None else Q(db_tags__db_key=str(ydest), - db_tags__db_category=MAP_YDEST_TAG_CATEGORY)), - (Q() if zdest is None else Q(db_tags__db_key=str(zdest), - db_tags__db_category=MAP_ZDEST_TAG_CATEGORY)), + return ( + self + .filter_family(**kwargs) + .filter( + Q() if x == wildcard + else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)) + .filter( + Q() if y == wildcard + else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)) + .filter( + Q() if z == wildcard + else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)) + .filter( + Q() if xdest == wildcard + else Q(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY)) + .filter( + Q() if ydest == wildcard + else Q(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY)) + .filter( + Q() if zdest == wildcard + else Q(db_tags__db_key=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY)) ) - def get_xyz_exit(self, coord=(0, 0, 'map'), destination_coord=(0, 0, 'map'), **kwargs): + def get_xyz_exit(self, xyz=(0, 0, 'map'), xyz_destination=(0, 0, 'map'), **kwargs): """ Used by exits (objects with a source and -destination property). Get a single exit. All source/destination coordinates (as well as the map's name) are required. + This will also find children of XYZExits on the given coords. Kwargs: - coord (tuple, optional): A tuple (X, Y, Z) for the source location. Each - element is either an `int` or `str` (not `None`). + xyz (tuple, optional): A coordinate (X, Y, Z) for the source location. Each + element is either an `int` or `str` (not `*`, no wildcards are allowed for get). the `Z`-coordinate is the name of the map (case-sensitive) in the XYZgrid contrib. - destination_coord (tuple, optional): Same as the `coord` but for the destination of the - exit. + xyz_destination_coord (tuple, optional): Same as the `xyz` but for the destination of + the exit. **kwargs: All other kwargs are passed on to the query. Returns: @@ -157,29 +191,41 @@ class XYZExitManager(XYZManager): Raises: DoesNotExist: If no matching query was found. MultipleObjectsReturned: If more than one match was found (which should not - possible with a unique combination of x,y,x). + be possible with a unique combination of x,y,x). Notes: All coordinates are required. """ - x, y, z = coord - xdest, ydest, zdest = destination_coord + x, y, z = xyz + xdest, ydest, zdest = xyz_destination + # mimic get_family + paths = [self.model.path] + [ + "%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model) + ] + kwargs["db_typeclass_path__in"] = paths - return self.get_family( - Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY), - Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY), - Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY), - Q(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY), - Q(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY), - Q(db_tags__db_key=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY), - **kwargs - ) + try: + return ( + self + .filter(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY) + .filter(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY) + .filter(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY) + .filter(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY) + .filter(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY) + .filter(db_tags__db_key=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY) + .get(**kwargs) + ) + except self.model.DoesNotExist: + inp = (f"xyz=({x},{y},{z}),xyz_destination=({xdest},{ydest},{zdest})," + + ",".join(f"{key}={val}" for key, val in kwargs.items())) + raise self.model.DoesNotExist(f"{self.model.__name__} " + f"matching query {inp} does not exist.") class XYZRoom(DefaultRoom): """ - A game location aware of its XY-coordinate and map. + A game location aware of its XYZ-position. """ @@ -190,28 +236,32 @@ class XYZRoom(DefaultRoom): return repr(self) def __repr__(self): - x, y, z = self.xyzcoords + x, y, z = self.xyz return f"" @property - def xyzcoords(self): - if not hasattr(self, "_xyzcoords"): + def xyz(self): + if not hasattr(self, "_xyz"): x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False) y = self.tags.get(category=MAP_Y_TAG_CATEGORY, return_list=False) z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False) - self._xyzcoords = (x, y, z) - return self._xyzcoords + if x is None or y is None or z is None: + # don't cache unfinished coordinate + return (x, y, z) + # cache result + self._xyz = (x, y, z) + return self._xyz @classmethod - def create(cls, key, account=None, coord=(0, 0, 'map'), **kwargs): + def create(cls, key, account=None, xyz=(0, 0, 'map'), **kwargs): """ - Creation method aware of coordinates. + Creation method aware of XYZ coordinates. Args: key (str): New name of object to create. account (Account, optional): Any Account to tie to this entity (usually not used for rooms). - coords (tuple, optional): A 3D coordinate (X, Y, Z) for this room's location on a + xyz (tuple, optional): A 3D coordinate (X, Y, Z) for this room's location on a map grid. Each element can theoretically be either `int` or `str`, but for the XYZgrid, the X, Y are always integers while the `Z` coordinate is used for the map's name. @@ -227,15 +277,15 @@ class XYZRoom(DefaultRoom): """ try: - x, y, z = coord + x, y, z = xyz except ValueError: - return None, [f"XYRroom.create got `coord={coord}` - needs a valid (X,Y,Z) " + return None, [f"XYRroom.create got `xyz={xyz}` - needs a valid (X,Y,Z) " "coordinate of ints/strings."] - existing_query = cls.objects.filter_xyz(coord=(x, y, z)) + existing_query = cls.objects.filter_xyz(xyz=(x, y, z)) if existing_query.exists(): existing_room = existing_query.first() - return None, [f"XYRoom XYZ={coord} already exists " + return None, [f"XYRoom XYZ=({x},{y},{z}) already exists " f"(existing room is named '{existing_room.db_key}')!"] tags = ( @@ -249,7 +299,7 @@ class XYZRoom(DefaultRoom): class XYZExit(DefaultExit): """ - An exit that is aware of the XY coordinate system. + An exit that is aware of the XYZ coordinate system. """ @@ -259,30 +309,38 @@ class XYZExit(DefaultExit): return repr(self) def __repr__(self): - x, y, z = self.xyzcoords - xd, yd, zd = self.xyzdestcoords + x, y, z = self.xyz + xd, yd, zd = self.xyz_destination return f"({xd},{yd},{zd})>" @property - def xyzcoords(self): - if not hasattr(self, "_xyzcoords"): + def xyz(self): + if not hasattr(self, "_xyz"): x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False) y = self.tags.get(category=MAP_Y_TAG_CATEGORY, return_list=False) z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False) - self._xyzcoords = (x, y, z) - return self._xyzcoords + if x is None or y is None or z is None: + # don't cache yet unfinished coordinate + return (x, y, z) + # cache result + self._xyz = (x, y, z) + return self._xyz @property - def xyzdestcoords(self): - if not hasattr(self, "_xyzdestcoords"): + def xyz_destination(self): + if not hasattr(self, "_xyz_destination"): xd = self.tags.get(category=MAP_XDEST_TAG_CATEGORY, return_list=False) yd = self.tags.get(category=MAP_YDEST_TAG_CATEGORY, return_list=False) zd = self.tags.get(category=MAP_ZDEST_TAG_CATEGORY, return_list=False) - self._xyzdestcoords = (xd, yd, zd) - return self._xyzdestcoords + if xd is None or yd is None or zd is None: + # don't cache unfinished coordinate + return (xd, yd, zd) + # cache result + self._xyz_destination = (xd, yd, zd) + return self._xyz_destination @classmethod - def create(cls, key, account=None, coord=(0, 0, 'map'), destination_coord=(0, 0, 'map'), + def create(cls, key, account=None, xyz=(0, 0, 'map'), xyz_destination=(0, 0, 'map'), location=None, destination=None, **kwargs): """ Creation method aware of coordinates. @@ -290,19 +348,17 @@ class XYZExit(DefaultExit): Args: key (str): New name of object to create. account (Account, optional): Any Account to tie to this entity (unused for exits). - coords (tuple or None, optional): A 3D coordinate (X, Y, Z) for this room's location + xyz (tuple or None, optional): A 3D coordinate (X, Y, Z) for this room's location on a map grid. Each element can theoretically be either `int` or `str`, but for the XYZgrid contrib, the X, Y are always integers while the `Z` coordinate is used for the map's name. Set to `None` if instead using a direct room reference with - `location`. destination_coord (tuple or None, optional): Works as `coords`, but for - destination of - the exit. Set to `None` if using the `destination` kwarg to point to room directly. - destination_coord (tuple, optional): The XYZ coordinate of the place the exit + `location`. + xyz_destination (tuple, optional): The XYZ coordinate of the place the exit leads to. Will be ignored if `destination` is given directly. - location (Object, optional): Only used if `coord` is not given. This can be used + location (Object, optional): If given, overrides `xyz` coordinate. This can be used to place this exit in any room, including non-XYRoom type rooms. - destination (Object, optional): If given, overrides `destination_coord`. This can - be any room (including non-XYRooms) and is not checked for XY coordinates. + destination (Object, optional): If given, overrides `xyz_destination`. This can + be any room (including non-XYRooms) and is not checked for XYZ coordinates. **kwargs: Will be passed into the normal `DefaultRoom.create` method. Returns: @@ -311,35 +367,32 @@ class XYZExit(DefaultExit): """ tags = [] - try: - x, y, z = coord - except ValueError: - if not location: - return None, ["XYExit.create need either a `coord` or a `location`."] + if location: source = location else: - print("rooms:", XYZRoom.objects.all().count(), XYZRoom.objects.all()) - print("exits:", XYZExit.objects.all().count(), XYZExit.objects.all()) - source = XYZRoom.objects.get_xyz(coord=(x, y, z)) - tags.extend(((str(x), MAP_X_TAG_CATEGORY), - (str(y), MAP_Y_TAG_CATEGORY), - (str(z), MAP_Z_TAG_CATEGORY))) + try: + x, y, z = xyz + except ValueError: + return None, ["XYExit.create need either `xyz=(X,Y,Z)` coordinate or a `location`."] + else: + source = XYZRoom.objects.get_xyz(xyz=(x, y, z)) + tags.extend(((str(x), MAP_X_TAG_CATEGORY), + (str(y), MAP_Y_TAG_CATEGORY), + (str(z), MAP_Z_TAG_CATEGORY))) if destination: dest = destination else: try: - xdest, ydest, zdest = destination_coord + xdest, ydest, zdest = xyz_destination except ValueError: - if not destination: - return None, ["XYExit.create need either a `destination_coord` or " - "a `destination`."] - dest = destination + return None, ["XYExit.create need either `xyz_destination=(X,Y,Z)` coordinate " + "or a `destination`."] else: - dest = XYZRoom.objects.get_xyz(coord=(xdest, ydest, zdest)) + dest = XYZRoom.objects.get_xyz(xyz=(xdest, ydest, zdest)) tags.extend(((str(xdest), MAP_XDEST_TAG_CATEGORY), (str(ydest), MAP_YDEST_TAG_CATEGORY), (str(zdest), MAP_ZDEST_TAG_CATEGORY))) return DefaultExit.create( - key, source, dest, account=account, - location=location, tags=tags, typeclass=cls, **kwargs) + key, source, dest, + account=account, tags=tags, typeclass=cls, **kwargs)