First working function of map-spawning

This commit is contained in:
Griatch 2021-07-04 18:03:07 +02:00
parent 61ab313ee3
commit 93e20d05b2
5 changed files with 327 additions and 208 deletions

View file

@ -73,7 +73,7 @@ class MapNode:
direction_spawn_defaults = { direction_spawn_defaults = {
'n': ('north', 'n'), 'n': ('north', 'n'),
'ne': ('northeast', 'ne', 'north-east'), 'ne': ('northeast', 'ne', 'north-east'),
'e': ('east',), 'e': ('east', 'e'),
'se': ('southeast', 'se', 'south-east'), 'se': ('southeast', 'se', 'south-east'),
's': ('south', 's'), 's': ('south', 's'),
'sw': ('southwest', 'sw', 'south-west'), 'sw': ('southwest', 'sw', 'south-west'),
@ -231,7 +231,7 @@ class MapNode:
""" """
return self.symbol if self.display_symbol is None else self.display_symbol 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 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 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. # a 'virtual' node.
return return
coord = self.get_spawn_coords() xyz = self.get_spawn_xyz()
try: try:
nodeobj = NodeTypeclass.objects.get_xyz(coord=coord) nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
except NodeTypeclass.DoesNotExist: except NodeTypeclass.DoesNotExist:
# create a new entity with proper coordinates etc # create a new entity with proper coordinates etc
nodeobj, err = NodeTypeclass.create( nodeobj, err = NodeTypeclass.create(
self.prototype.get('key', 'An Empty room'), self.prototype.get('key', 'An Empty room'),
coord=coord xyz=xyz
) )
if err: if err:
raise RuntimeError(err) raise RuntimeError(err)
@ -277,12 +277,12 @@ class MapNode:
spawner.batch_update_objects_with_prototype( spawner.batch_update_objects_with_prototype(
self.prototype, objects=[nodeobj], exact=False) 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. Build actual in-game exits based on the links out of this room.
Args: 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 directions (n, ne, etc). Only links in these directions will be spawned
for this node. for this node.
@ -290,7 +290,12 @@ class MapNode:
the entire XYZgrid. This creates/syncs all exits to their locations and destinations. 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 global ExitTypeclass
if not ExitTypeclass: if not ExitTypeclass:
@ -308,7 +313,7 @@ class MapNode:
# we need to search for exits in all directions since some # we need to search for exits in all directions since some
# may have been removed since last sync # may have been removed since last sync
linkobjs = {exi.db_key.lower(): exi 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 # figure out if the topology changed between grid and map (will always
# build all exits first run) # build all exits first run)
@ -321,20 +326,25 @@ class MapNode:
else: else:
# missing in linkobjs - create a new exit # missing in linkobjs - create a new exit
key, aliases, direction, link = maplinks[differing_key] key, aliases, direction, link = maplinks[differing_key]
exitnode = self.links[direction]
linkobjs[direction] = ExitTypeclass.create( if direction_limits and direction not in direction_limits:
# either get name from the prototype or use our custom set continue
exitnode = self.links[direction]
exi, err = ExitTypeclass.create(
key, key,
coord=coord, xyz=xyz,
destination_coord=exitnode.get_spawn_coords(), xyz_destination=exitnode.get_spawn_xyz(),
aliases=aliases, aliases=aliases,
) )
if err:
raise RuntimeError(err)
linkobjs[key.lower()] = exi
# apply prototypes to catch any changes # apply prototypes to catch any changes
for direction, linkobj in linkobjs: for key, linkobj in linkobjs.items():
spawner.batch_update_objects_with_prototype( 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): def unspawn(self):
""" """
@ -345,8 +355,10 @@ class MapNode:
if not NodeTypeclass: if not NodeTypeclass:
from .room import XYZRoom as NodeTypeclass from .room import XYZRoom as NodeTypeclass
xyz = (self.X, self.Y, self.Z)
try: try:
nodeobj = NodeTypeclass.objects.get_xyz(coord=coord) nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
except NodeTypeclass.DoesNotExist: except NodeTypeclass.DoesNotExist:
# no object exists # no object exists
pass pass
@ -364,7 +376,7 @@ class TransitionMapNode(MapNode):
to this node. to this node.
Properties: 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 to when moving to this node. This should not be another TransitionMapNode (see below for
how to make a two-way link). how to make a two-way link).
@ -380,16 +392,21 @@ class TransitionMapNode(MapNode):
""" """
symbol = 'T' symbol = 'T'
display_symbol = ' ' display_symbol = ' '
# X,Y,Z coordinates of target node (not a transitionalmapnode) # X,Y,Z coordinates of target node
taget_map_coord = (None, None, None) 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 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). 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): def build_links(self):
"""Check so we don't have too many links""" """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 of the link is only used to determine its destination). This can be overridden on a
per-direction basis. per-direction basis.
- `spawn_aliases` (list): A list of [key, alias, alias, ...] for the node to use when spawning - `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. 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""" """Transition-target to other map"""
symbol = "T" symbol = "T"
display_symbol = " " display_symbol = " "
target_map_coords = (0, 0, 'unset') # must be changed prototype = None # important to leave None!
prototype = None # important! target_map_xyz = (None, None, None) # must be set manually
class InterruptMapNode(MapNode): class InterruptMapNode(MapNode):

View file

@ -9,7 +9,7 @@ from random import randint
from unittest import TestCase from unittest import TestCase
from parameterized import parameterized from parameterized import parameterized
from django.test import override_settings from django.test import override_settings
from . import xymap, xyzgrid, map_legend from . import xymap, xyzgrid, map_legend, xyzroom
MAP1 = """ 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 Test the Map class with a simple 4-node map
""" """
def setUp(self): # def setUp(self):
self.map = xymap.XYMap({"map": MAP1}, Z="testmap") # self.map = xymap.XYMap({"map": MAP1}, Z="testmap")
self.map.parse() # self.map.parse()
def test_str_output(self): def test_str_output(self):
"""Check the display_map""" """Check the display_map"""
@ -1057,17 +1079,23 @@ class TestXYZGrid(TestCase):
def test_spawn(self): def test_spawn(self):
"""Spawn objects for the grid""" """Spawn objects for the grid"""
self.grid.spawn() 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 # map transitions
class Map12aTransition(map_legend.MapTransitionMapNode): class Map12aTransition(map_legend.MapTransitionMapNode):
symbol = "T" symbol = "T"
target_map_coords = (1, 0, "map12b") target_map_xyz = (1, 0, "map12b")
class Map12bTransition(map_legend.MapTransitionMapNode): class Map12bTransition(map_legend.MapTransitionMapNode):
symbol = "T" symbol = "T"
target_map_coords = (0, 1, "map12a") target_map_xyz = (0, 1, "map12a")
class TestXYZGridTransition(TestCase): class TestXYZGridTransition(TestCase):
@ -1112,4 +1140,17 @@ class TestXYZGridTransition(TestCase):
Spawn the two maps into actual objects. Spawn the two maps into actual objects.
""" """
# from evennia import set_trace;set_trace()
self.grid.spawn() 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)

View file

@ -272,7 +272,8 @@ class XYMap:
return "\n".join("".join(line) for line in self.display_map[::-1]) return "\n".join("".join(line) for line in self.display_map[::-1])
def __repr__(self): def __repr__(self):
return f"<Map {self.max_X + 1}x{self.max_Y + 1}, {len(self.node_index_map)} nodes>" return (f"<XYMap(Z={self.Z}), {self.max_X + 1}x{self.max_Y + 1}, "
f"{len(self.node_index_map)} nodes>")
def reload(self, map_module_or_dict=None): def reload(self, map_module_or_dict=None):
""" """
@ -498,20 +499,20 @@ class XYMap:
# store # store
self.display_map = display_map 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. Get all links and nodes up to a certain distance from an XY coordinate.
Args: 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. dist (int): How many nodes away from center point to find paths for.
Returns: 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 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 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 for display purposes. The others are the minimum size of the rectangle
surrounding the area containing `coords`. surrounding the area containing `xy_coords`.
Notes: Notes:
This performs a depth-first pass down the the given dist. This performs a depth-first pass down the the given dist.
@ -542,7 +543,7 @@ class XYMap:
return points, xmin, xmax, ymin, ymax 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) points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist)
return list(set(points)), xmin, xmax, ymin, ymax return list(set(points)), xmin, xmax, ymin, ymax
@ -591,7 +592,7 @@ class XYMap:
pickle.dump((self.mapstring, self.dist_matrix, self.pathfinding_routes), pickle.dump((self.mapstring, self.dist_matrix, self.pathfinding_routes),
fil, protocol=4) 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 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* 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 to exist. It's also possible to only spawn a subset of the map
Args: 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: Examples:
- `coord=(1, 3) - spawn (1,3) coordinate only. - `xy=(1, 3) - spawn (1,3) coordinate only.
- `coord=(None, 1) - spawn all nodes in the first row of the map only. - `xy=('*', 1) - spawn all nodes in the first row of the map only.
- `coord=(None, None)` - spawn all nodes - `xy=('*', '*')` - spawn all nodes
Returns: Returns:
list: A list of nodes that were spawned. list: A list of nodes that were spawned.
""" """
x, y = coord x, y = xy
wildcard = '*'
spawned = [] spawned = []
for node in self.node_index_map.values(): 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() node.spawn()
spawned.append(node) spawned.append(node)
return spawned 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 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 prototypes. It's possible to only spawn a specic exit by specifying the node and
Args: 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 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. links will be spawned at all.
directions (list, optional): A list of cardinal directions ('n', 'ne' etc). If given, 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 sync only the exit in the given directions (`xy` limits which links out of which
nodes should be considered). `None` acts as a wildcard. nodes should be considered). If unset, there are no limits to directions.
Examples: 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. out of the (1, 3) node.
""" """
x, y = coord x, y = xy
wildcard = '*'
if not nodes: if not nodes:
nodes = self.node_index_map.values() nodes = self.node_index_map.values()
for node in nodes: for node in nodes:
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_links(only_directions=only_directions) 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. Get a MapNode from a coordinate.
Args: Args:
coords (tuple): X,Y coordinates on XYgrid. xy (tuple): X,Y coordinate on XYgrid.
Returns: Returns:
MapNode: The node found at the given coordinates. Returns MapNode: The node found at the given coordinates. Returns
@ -664,12 +668,12 @@ class XYMap:
if not self.XYgrid: if not self.XYgrid:
self.parse() self.parse()
iX, iY = coords iX, iY = xy
if not ((0 <= iX <= self.max_X) and (0 <= iY <= self.max_Y)): 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}).") f"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y}).")
try: try:
return self.XYgrid[coords[0]][coords[1]] return self.XYgrid[iX][iY]
except KeyError: except KeyError:
return None return None
@ -686,14 +690,14 @@ class XYMap:
""" """
return self.symbol_map.get(symbol, []) 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. Get the shortest route between two points on the grid.
Args: 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. 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. we want to find the shortest route to.
Returns: Returns:
@ -702,8 +706,8 @@ class XYMap:
the full path including the start- and end-node. the full path including the start- and end-node.
""" """
startnode = self.get_node_from_coord(startcoord) startnode = self.get_node_from_coord(start_xy)
endnode = self.get_node_from_coord(endcoord) endnode = self.get_node_from_coord(end_xy)
if not (startnode and endnode): if not (startnode and endnode):
# no node at given coordinate. No path is possible. # no node at given coordinate. No path is possible.
@ -751,7 +755,7 @@ class XYMap:
return directions, path return directions, path
def get_visual_range(self, coord, dist=2, mode='nodes', def get_visual_range(self, xy, dist=2, mode='nodes',
character='@', character='@',
target=None, target_path_style="|y{display_symbol}|n", target=None, target_path_style="|y{display_symbol}|n",
max_size=None, max_size=None,
@ -761,7 +765,7 @@ class XYMap:
of nodes or grid points in every direction. of nodes or grid points in every direction.
Args: 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 of a node on the grid, the `character` or the empty-space symbol (by default
an empty space) will be shown. an empty space) will be shown.
dist (int, optional): Number of gridpoints distance to show. Which 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 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 nodes are reachable or not. If 'nodes', distance measure how many linked nodes
away from the center coordinate to display. 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. 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 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 (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 # convert inputs to xygrid
width, height = self.max_x + 1, self.max_y + 1 width, height = self.max_x + 1, self.max_y + 1
ix, iy = max(0, min(iX * 2, width)), max(0, min(iY * 2, height)) ix, iy = max(0, min(iX * 2, width)), max(0, min(iY * 2, height))
@ -835,14 +839,14 @@ class XYMap:
gridmap = self.display_map gridmap = self.display_map
ixc, iyc = ix, iy 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 # There is no node at these coordinates. Show
# nothing but ourselves or emptiness # nothing but ourselves or emptiness
return character if character else self.empty_symbol return character if character else self.empty_symbol
elif mode == 'nodes': elif mode == 'nodes':
# dist measures only full, reachable 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 ixc, iyc = ix - xmin, iy - ymin
# note - override width/height here since our grid is # note - override width/height here since our grid is
@ -878,7 +882,7 @@ class XYMap:
else: else:
_target_path_style = _default_callable _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 maxstep = dist if mode == 'nodes' else dist / 2
nsteps = 0 nsteps = 0

View file

@ -106,12 +106,15 @@ class XYZGrid(DefaultScript):
remove_objects (bool, optional): If the synced database objects (rooms/exits) should remove_objects (bool, optional): If the synced database objects (rooms/exits) should
be removed alongside this map. be removed alongside this map.
""" """
# from evennia import set_trace;set_trace()
for zcoord in zcoords: for zcoord in zcoords:
if zcoord in self.db.map_data: if zcoord in self.db.map_data:
self.db.map_data.pop(zcoord) self.db.map_data.pop(zcoord)
if remove_objects: if remove_objects:
# this should also remove all exits automatically # we can't batch-delete because we want to run the .delete
XYZRoom.objects.filter_xyz(coord=(None, None, zcoord)).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() self.reload()
def delete(self): def delete(self):
@ -121,41 +124,42 @@ class XYZGrid(DefaultScript):
""" """
self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True) 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 Create/recreate/update the in-game grid based on the stored Maps or for a specific Map
or coordinate. or coordinate.
Args: 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. acts as a wildcard.
only_directions (list, optional): A list of cardinal directions ('n', 'ne' etc). directions (list, optional): A list of cardinal directions ('n', 'ne' etc).
If given, spawn exits only the given direction. `None` acts as a wildcard. Spawn exits only the given direction. If unset, all needed directions are spawned.
Examples: Examples:
- `coord=(1, 3, 'foo')` - sync a specific element of map 'foo' only. - `xyz=('*', '*', '*')` (default) - spawn/update all maps.
- `coord=(None, None, 'foo') - sync all elements of map 'foo' - `xyz=(1, 3, 'foo')` - sync a specific element of map 'foo' only.
- `coord=(1, 3, None) - sync all (1,3) coordinates on all maps (rarely useful) - `xyz=('*', '*', 'foo') - sync all elements of map 'foo'
- `coord=(None, None, None)` - sync all maps. - `xyz=(1, 3, '*') - sync all (1,3) coordinates on all maps (rarely useful)
- `coord=(1, 3, 'foo')`, `direction='ne'` - sync only the north-eastern exit - `xyz=(1, 3, 'foo')`, `direction='ne'` - sync only the north-eastern exit
out of the specific node on map 'foo'. 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 xymaps = self.grid
elif z in self.ndb.grid: elif self.ndb.grid and z in self.ndb.grid:
xymaps = [self.grid[z]] xymaps = {z: self.grid[z]}
else: else:
raise RuntimeError(f"The 'z' coordinate/name '{z}' is not found on the grid.") raise RuntimeError(f"The 'z' coordinate/name '{z}' is not found on the grid.")
# first build all nodes/rooms # first build all nodes/rooms
for zcoord, xymap in xymaps.items(): for zcoord, xymap in xymaps.items():
logger.log_info(f"[grid] spawning/updating nodes for {zcoord} ...") 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) # next build all links between nodes (including between maps)
for zcoord, xymap in xymaps.items(): for zcoord, xymap in xymaps.items():
logger.log_info(f"[grid] spawning/updating links for {zcoord} ...") 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)

View file

@ -30,16 +30,15 @@ class XYZManager(ObjectManager):
efficiently querying the room in the database based on XY coordinates. 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 Filter queryset based on XYZ position on the grid. The Z-position is the name of the XYMap
required but not the coordinates - if only one coordinate is given, multiple rooms may be Set a coordinate to `'*'` to act as a wildcard (setting all coords to `*` will thus find
returned from the same coordinate row/column. If both coordinates are omitted (set to *all* XYZ rooms). This will also find children of XYZRooms on the given coordinates.
`None`), then all rooms of a given map is returned.
Kwargs: Kwargs:
coord (tuple, optional): A tuple (X, Y, Z) where each element is either xyz (tuple, optional): A coordinate tuple (X, Y, Z) where each element is either
an `int`, `str` or `None`. `None` acts as a wild card. Note that 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. 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. **kwargs: All other kwargs are passed on to the query.
@ -48,25 +47,34 @@ class XYZManager(ObjectManager):
with further filtering. 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)), return (
(Q() if y is None else Q(db_tags__db_key=str(y), self
db_tags__db_category=MAP_Y_TAG_CATEGORY)), .filter_family(**kwargs)
(Q() if z is None else Q(db_tags__db_key=str(z), .filter(
db_tags__db_category=MAP_Z_TAG_CATEGORY)), Q() if x == wildcard
**kwargs 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: Kwargs:
coord (tuple): A tuple of `int` or `str` (not `None`). The `Z`-coordinate xyz (tuple): A coordinate tuple of `int` or `str` (not `'*'`, no wildcards are
acts as the name (case-sensitive) of the map in the XYZgrid contrib. 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. **kwargs: All other kwargs are passed on to the query.
Returns: Returns:
@ -78,13 +86,27 @@ class XYZManager(ObjectManager):
possible with a unique combination of x,y,z). possible with a unique combination of x,y,z).
""" """
x, y, z = coord x, y, z = xyz
return self.get_family(
Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY), # mimic get_family
Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY), paths = [self.model.path] + [
Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY), "%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model)
**kwargs ]
) 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): class XYZExitManager(XYZManager):
@ -94,17 +116,19 @@ class XYZExitManager(XYZManager):
""" """
def filter_xyz_exit(self, coord=(None, None, 'map'), def filter_xyz_exit(self, xyz=('*', '*', '*'),
destination_coord=(None, None, 'map'), **kwargs): xyz_destination=('*', '*', '*'), **kwargs):
""" """
Used by exits (objects with a source and -destination property). 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: Kwargs:
coord (tuple, optional): A tuple (X, Y, Z) for the source location. Each xyz (tuple, optional): A coordinate (X, Y, Z) for the source location. Each
element is either an `int`, `str` or `None`. `None` acts as a wild card. Note that 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. 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. exit.
**kwargs: All other kwargs are passed on to the query. **kwargs: All other kwargs are passed on to the query.
@ -113,42 +137,52 @@ class XYZExitManager(XYZManager):
with further filtering. with further filtering.
Notes: 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 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 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 x, y, z = xyz
xdest, ydest, zdest = destination_coord xdest, ydest, zdest = xyz_destination
wildcard = '*'
return self.filter_family( return (
(Q() if x is None else Q(db_tags__db_key=str(x), self
db_tags__db_category=MAP_X_TAG_CATEGORY)), .filter_family(**kwargs)
(Q() if y is None else Q(db_tags__db_key=str(y), .filter(
db_tags__db_category=MAP_Y_TAG_CATEGORY)), Q() if x == wildcard
(Q() if z is None else Q(db_tags__db_key=str(z), else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY))
db_tags__db_category=MAP_Z_TAG_CATEGORY)), .filter(
(Q() if xdest is None else Q(db_tags__db_key=str(xdest), Q() if y == wildcard
db_tags__db_category=MAP_XDEST_TAG_CATEGORY)), else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY))
(Q() if ydest is None else Q(db_tags__db_key=str(ydest), .filter(
db_tags__db_category=MAP_YDEST_TAG_CATEGORY)), Q() if z == wildcard
(Q() if zdest is None else Q(db_tags__db_key=str(zdest), else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY))
db_tags__db_category=MAP_ZDEST_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 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. 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: Kwargs:
coord (tuple, optional): A tuple (X, Y, Z) for the source location. Each xyz (tuple, optional): A coordinate (X, Y, Z) for the source location. Each
element is either an `int` or `str` (not `None`). 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. 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_coord (tuple, optional): Same as the `xyz` but for the destination of
exit. the exit.
**kwargs: All other kwargs are passed on to the query. **kwargs: All other kwargs are passed on to the query.
Returns: Returns:
@ -157,29 +191,41 @@ class XYZExitManager(XYZManager):
Raises: Raises:
DoesNotExist: If no matching query was found. DoesNotExist: If no matching query was found.
MultipleObjectsReturned: If more than one match was found (which should not 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: Notes:
All coordinates are required. All coordinates are required.
""" """
x, y, z = coord x, y, z = xyz
xdest, ydest, zdest = destination_coord 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( try:
Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY), return (
Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY), self
Q(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)
Q(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY), .filter(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
Q(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY), .filter(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
Q(db_tags__db_key=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY), .filter(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY)
**kwargs .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): 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) return repr(self)
def __repr__(self): def __repr__(self):
x, y, z = self.xyzcoords x, y, z = self.xyz
return f"<XYZRoom '{self.db_key}', XYZ=({x},{y},{z})>" return f"<XYZRoom '{self.db_key}', XYZ=({x},{y},{z})>"
@property @property
def xyzcoords(self): def xyz(self):
if not hasattr(self, "_xyzcoords"): if not hasattr(self, "_xyz"):
x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False) x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False)
y = self.tags.get(category=MAP_Y_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) z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False)
self._xyzcoords = (x, y, z) if x is None or y is None or z is None:
return self._xyzcoords # don't cache unfinished coordinate
return (x, y, z)
# cache result
self._xyz = (x, y, z)
return self._xyz
@classmethod @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: Args:
key (str): New name of object to create. key (str): New name of object to create.
account (Account, optional): Any Account to tie to this entity (usually not used for account (Account, optional): Any Account to tie to this entity (usually not used for
rooms). 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 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 XYZgrid, the X, Y are always integers while the `Z` coordinate is used for the
map's name. map's name.
@ -227,15 +277,15 @@ class XYZRoom(DefaultRoom):
""" """
try: try:
x, y, z = coord x, y, z = xyz
except ValueError: 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."] "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(): if existing_query.exists():
existing_room = existing_query.first() 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}')!"] f"(existing room is named '{existing_room.db_key}')!"]
tags = ( tags = (
@ -249,7 +299,7 @@ class XYZRoom(DefaultRoom):
class XYZExit(DefaultExit): 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) return repr(self)
def __repr__(self): def __repr__(self):
x, y, z = self.xyzcoords x, y, z = self.xyz
xd, yd, zd = self.xyzdestcoords xd, yd, zd = self.xyz_destination
return f"<XYZExit '{self.db_key}', XYZ=({x},{y},{z})->({xd},{yd},{zd})>" return f"<XYZExit '{self.db_key}', XYZ=({x},{y},{z})->({xd},{yd},{zd})>"
@property @property
def xyzcoords(self): def xyz(self):
if not hasattr(self, "_xyzcoords"): if not hasattr(self, "_xyz"):
x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False) x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False)
y = self.tags.get(category=MAP_Y_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) z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False)
self._xyzcoords = (x, y, z) if x is None or y is None or z is None:
return self._xyzcoords # don't cache yet unfinished coordinate
return (x, y, z)
# cache result
self._xyz = (x, y, z)
return self._xyz
@property @property
def xyzdestcoords(self): def xyz_destination(self):
if not hasattr(self, "_xyzdestcoords"): if not hasattr(self, "_xyz_destination"):
xd = self.tags.get(category=MAP_XDEST_TAG_CATEGORY, return_list=False) xd = self.tags.get(category=MAP_XDEST_TAG_CATEGORY, return_list=False)
yd = self.tags.get(category=MAP_YDEST_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) zd = self.tags.get(category=MAP_ZDEST_TAG_CATEGORY, return_list=False)
self._xyzdestcoords = (xd, yd, zd) if xd is None or yd is None or zd is None:
return self._xyzdestcoords # don't cache unfinished coordinate
return (xd, yd, zd)
# cache result
self._xyz_destination = (xd, yd, zd)
return self._xyz_destination
@classmethod @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): location=None, destination=None, **kwargs):
""" """
Creation method aware of coordinates. Creation method aware of coordinates.
@ -290,19 +348,17 @@ class XYZExit(DefaultExit):
Args: Args:
key (str): New name of object to create. key (str): New name of object to create.
account (Account, optional): Any Account to tie to this entity (unused for exits). 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 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 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 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 `location`.
destination of xyz_destination (tuple, optional): The XYZ coordinate of the place the exit
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
leads to. Will be ignored if `destination` is given directly. 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. to place this exit in any room, including non-XYRoom type rooms.
destination (Object, optional): If given, overrides `destination_coord`. This can destination (Object, optional): If given, overrides `xyz_destination`. This can
be any room (including non-XYRooms) and is not checked for XY coordinates. be any room (including non-XYRooms) and is not checked for XYZ coordinates.
**kwargs: Will be passed into the normal `DefaultRoom.create` method. **kwargs: Will be passed into the normal `DefaultRoom.create` method.
Returns: Returns:
@ -311,35 +367,32 @@ class XYZExit(DefaultExit):
""" """
tags = [] tags = []
try: if location:
x, y, z = coord
except ValueError:
if not location:
return None, ["XYExit.create need either a `coord` or a `location`."]
source = location source = location
else: else:
print("rooms:", XYZRoom.objects.all().count(), XYZRoom.objects.all()) try:
print("exits:", XYZExit.objects.all().count(), XYZExit.objects.all()) x, y, z = xyz
source = XYZRoom.objects.get_xyz(coord=(x, y, z)) except ValueError:
tags.extend(((str(x), MAP_X_TAG_CATEGORY), return None, ["XYExit.create need either `xyz=(X,Y,Z)` coordinate or a `location`."]
(str(y), MAP_Y_TAG_CATEGORY), else:
(str(z), MAP_Z_TAG_CATEGORY))) 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: if destination:
dest = destination dest = destination
else: else:
try: try:
xdest, ydest, zdest = destination_coord xdest, ydest, zdest = xyz_destination
except ValueError: except ValueError:
if not destination: return None, ["XYExit.create need either `xyz_destination=(X,Y,Z)` coordinate "
return None, ["XYExit.create need either a `destination_coord` or " "or a `destination`."]
"a `destination`."]
dest = destination
else: 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), tags.extend(((str(xdest), MAP_XDEST_TAG_CATEGORY),
(str(ydest), MAP_YDEST_TAG_CATEGORY), (str(ydest), MAP_YDEST_TAG_CATEGORY),
(str(zdest), MAP_ZDEST_TAG_CATEGORY))) (str(zdest), MAP_ZDEST_TAG_CATEGORY)))
return DefaultExit.create( return DefaultExit.create(
key, source, dest, account=account, key, source, dest,
location=location, tags=tags, typeclass=cls, **kwargs) account=account, tags=tags, typeclass=cls, **kwargs)