Finished launcher, testing example

This commit is contained in:
Griatch 2021-07-07 23:45:37 +02:00
parent 2af82b19e1
commit 1c6fffeff2
10 changed files with 504 additions and 170 deletions

View file

@ -16,6 +16,8 @@ Use `evennia xyzgrid help` for usage help.
"""
from os.path import join as pathjoin
from django.conf import settings
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid
_HELP_SHORT = """
@ -37,7 +39,7 @@ evennia xyzgrid init
First start of the grid. This will create the XYZGrid global script. No maps are loaded yet!
It's safe to run this command multiple times; the grid will only be initialized once.
evennia xyzgrid add path.to.xymap.module
evennia xyzgrid add path.to.xymap.module [,path, path,...]
Add one or more XYmaps (each a string-map representing one Z position along with prototypes
etc). The module will be parsed for
@ -45,11 +47,12 @@ evennia xyzgrid add path.to.xymap.module
- a XYMAP_DATA a dict
{"map": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict}
describing one single XYmap, or
- a XYMAP_LIST - a list of multiple dicts on the XYMAP_DATA form. This allows to load
- a XYMAP_DATA_LIST - a list of multiple dicts on the XYMAP_DATA form. This allows to load
multiple maps from the same module.
Note that adding a map does *not* build it. If maps are linked to one another, you should add
all linked maps before building, or you'll get errors when spawning the linking exits.
all linked maps before running 'build', or you'll get errors when creating transitional exits
between maps.
evennia xyzgrid build
@ -106,38 +109,77 @@ def _option_list(**suboptions):
print(str(xymap))
def _option_init(**suboptions):
def _option_init(*suboptions):
"""
Initialize a new grid. Will fail if a Grid already exists.
"""
grid = get_xyzgrid()
print(f"The grid is initalized as the Script 'XYZGrid'({grid.dbref})")
print(f"The grid is initalized as the Script '{grid.key}'({grid.dbref})")
def _option_add(**suboptions):
def _option_add(*suboptions):
"""
Add a new map to the grid.
Add one or more map to the grid. Supports `add path,path,path,...`
"""
grid = get_xyzgrid()
xymap_data_list = []
for path in suboptions:
xymap_data_list.expand(grid.maps_from_module(path))
grid.add_maps(*xymap_data_list)
def _option_build(**suboptions):
def _option_build(*suboptions):
"""
Build the grid or part of it.
"""
grid = get_xyzgrid()
def _option_initpath(**suboptions):
# override grid's logger to echo directly to console
def _log(self, msg):
print(msg)
grid.log = _log
if suboptions:
opts = ''.join(suboptions).strip('()')
# coordinate tuple
try:
x, y, z = (part.strip() for part in opts.split(","))
except ValueError:
print("Build coordinate must be given as (X, Y, Z) tuple, where '*' act "
"wild cards and Z is the mapname/z-coord of the map to load.")
return
else:
x, y, z = '*', '*', '*'
grid.spawn(xyz=(x, y, z))
def _option_initpath(*suboptions):
"""
Initialize the pathfinding matrices for grid or part of it.
"""
def _option_delete(**suboptions):
"""
Delete the grid or parts of it.
(Re)Initialize the pathfinding matrices for grid or part of it.
"""
grid = get_xyzgrid()
xymaps = grid.all_rooms()
nmaps = len(xymaps)
for inum, xymap in enumerate(grid.all_rooms()):
print(f"Rebuilding pathfinding matrix for xymap Z={xymap.Z} ({inum+1}/{nmaps}) ...")
xymap.calculate_path_matrix(force=True)
cachepath = pathjoin(settings.GAMEDIR, "server", ".cache")
print(f"... done. Data cached to {cachepath}.")
def _option_delete(*suboptions):
"""
Delete the grid or parts of it. Allows mapname,mapname, ...
"""
grid = get_xyzgrid()
if not suboptions:
repl = input("WARNING: This will delete the ENTIRE Grid and wipe all rooms/exits!"
"\nObjects/Chars inside deleted rooms will be moved to their home locations."
@ -146,16 +188,35 @@ def _option_delete(**suboptions):
print("Aborted.")
else:
print("Deleting grid ...")
grid = get_xyzgrid()
grid.delete()
else:
pass
zcoords = (part.strip() for part in suboptions)
err = False
for zcoord in zcoords:
if not grid.get_map(zcoord):
print(f"Mapname/zcoord {zcoord} is not a part of the grid.")
err = True
if err:
print("Valid mapnames/zcoords are\n:", "\n ".join(
xymap.Z for xymap in grid.all_rooms()))
return
repl = input("This will delete map(s) {', '.join(zcoords)} and wipe all corresponding "
"rooms/exits!"
"\nObjects/Chars inside deleted rooms will be moved to their home locations."
"\nThis can't be undone. Are you sure you want to continue? Y/[N]?")
if repl.lower() not in ('yes', 'y'):
print("Aborted.")
else:
print("Deleting selected xymaps ...")
grid.remove_map(*zcoords, remove_objects=True)
def xyzcommand(*args):
"""
Evennia launcher command. This is made available as `evennia xyzgrid` on the command line,
once `settings.EXTRA_LAUNCHER_COMMANDS` is updated.
once added to `settings.EXTRA_LAUNCHER_COMMANDS`.
"""
if not args:

View file

@ -1,68 +1,263 @@
MAP = r"""
"""
Example xymaps to use with the XYZgrid contrib. Build outside of the game using
the `evennia xyzgrid` launcher command.
First add the launcher extension in your mygame/server/conf/settings.py:
EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.xyzgrid.launchcmd.xyzcommand'
Then
evennia xyzgrid init
evennia xyzgrid add evennia.contrib.xyzgrid.map_example
evennia xyzgrid build
"""
from evennia.contrib.xyzgrid import map_legend
# default prototype parent. It's important that
# the typeclass inherits from the XYZRoom (or XYZExit)
# the map_legend.XYZROOM_PARENT and XYZEXIT_PARENTS can also
# be used as a shortcut.
PARENT = {
"key": "An empty room",
"prototype_key": "xyzmap_room_map1",
"typeclass": "evennia.contrib.xyzgrid.xyzroom.XYZRoom",
"desc": "An empty room."
}
# -------------------- map 1 - the large tree
# this exemplifies the various map symbols
# but is not heavily prototyped
MAP1 = r"""
1
+ 0 1 2 3 4 5 6 7 8 9 0
10 #-#-#-#-#
| | \
9 #---+---#-#-----I
\ | /
8 #-#-#-#-# #
9 #-------#-#-------I
\ /
8 #-#---# #-t
|\ |
7 #i#-#-#+#-----#-t
7 #i#-#b--#-t
| |
6 #i#-#---#-#-#-#-#
| |x|x|
5 o-#-#-# #-#-#
\ / |x|x|
4 o-o-#-# #-#-#
/ /
3 #-# / #
\ / d
2 o-o-#-# |
| | u
1 #-#-#># #
^ |
0 T-----#-# #-t
5 o-#---#
\ /
4 o-o-#-#
/ d
3 #-----+-------#
| d
2 | |
v u
1 #---#>#-#
/
0 T-#
+ 0 1 2 3 4 5 6 7 8 9 0
1
"""
# use default legend
LEGEND = {
class TransitionToCave(map_legend.MapTransitionMapNode):
"""
A transition from map2 to map1
"""
symbol = 'T'
target_map_xyz = (2, 3, 'small cave')
# extends the default legend
LEGEND_MAP1 = {
'T': TransitionToCave
}
PARENT = {
"key": "An empty dungeon room",
"prototype_key": "dungeon_doom_prot",
"typeclass": "evennia.contrib.xyzgrid.xyzrooms.XYZRoom",
"desc": "Air is cold and stale in this barren room."
}
# link coordinates to rooms
ROOMS = {
(1, 0): {
PROTOTYPES_MAP1 = {
# node/room prototypes
(3, 0): {
"key": "Dungeon Entrance",
"prototype_parent": PARENT,
"desc": "A dark entrance."
"desc": "To the west, a narrow opening leads into darkness."
},
(4, 0): {
"key": "Antechamber",
"prototype_parent": PARENT,
"desc": "A small antechamber",
(4, 1): {
"key": "Under the foilage of a giant tree",
"desc": "High above the branches of a giant tree blocs out the sunlight. A slide "
"leading down from the upper branches ends here."
},
(4, 4): {
"key": "The slide",
"desc": "A slide leads down to the ground from here. It looks like a one-way trip."
},
(6, 1): {
"key": "Thorny path",
"desc": "To the east is a pathway of thorns. If you get through, you don't think you'll be "
"able to get back here the same way."
},
(8, 1): {
"key": "By a large tree",
"desc": "You are standing at the root of a great tree."
},
(8, 3): {
"key": "At the top of the tree",
"desc": "You are at the top of the tree."
},
(3, 7): {
"key": "Dense foilage",
"desc": "The foilage to the east is extra dense. It will take forever to get through it."
},
(5, 7): {
"key": "On a huge branch",
"desc": "To the east is a glowing light, may be a teleporter."
},
(9, 8): {
"key": "On an enormous branch",
"desc": "To the east is a glowing light, may be a teleporter."
},
(10, 9): {
"key": "A gorgeous view",
"desc": "The view from here is breathtaking, showing the forest stretching far and wide."
},
# default rooms
('*', '*'): {
"key": "Among the branches of a giant tree",
"desc": "These branches are wide enough to easily walk on. There's green all around."
},
# directional prototypes
(3, 0, 'w'): {
"desc": "A dark passage into the underworld."
},
}
for prot in PROTOTYPES_MAP1.values():
prot['prototype_parent'] = PARENT
XYMAP_DATA_MAP1 = {
"zcoord": "the large tree",
"map": MAP1,
"legend": LEGEND_MAP1,
"prototypes": PROTOTYPES_MAP1
}
# ------------- map2 definitions - small cave
# this gives prototypes for every room
MAP2 = r"""
+ 0 1 2 3
3 #-#-#
|x|
2 #-#-#
| \
1 #---#
| /
0 T-#-#
+ 0 1 2 3
"""
# custom map node
class TransitionToLargeTree(map_legend.MapTransitionMapNode):
"""
A transition from map1 to map2
"""
symbol = 'T'
target_map_xyz = (3, 0, 'the large tree')
# this extends the default legend (that defines #,-+ etc)
LEGEND_MAP2 = {
"T": TransitionToLargeTree
}
# prototypes for specific locations
PROTOTYPES_MAP2 = {
# node/rooms prototype overrides
(1, 0): {
"key": "The entrance",
"desc": "This is the entrance to a small cave leading into the ground. "
"Light sifts in from the outside, while cavernous passages disappear "
"into darkness."
},
(2, 0): {
"key": "A gruesome sight.",
"desc": "Something was killed here recently. The smell is unbearable."
},
(1, 1): {
"key": "A dark pathway",
"desc": "The path splits three ways here. To the north a faint light can be seen."
},
(3, 2): {
"key": "Stagnant water",
"desc": "A pool of stagnant, black water dominates this small chamber. To the nortwest "
"a faint light can be seen."
},
(0, 2): {
"key": "A dark alcove",
"desc": "This alcove is empty."
},
(1, 2): {
"key": "South-west corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones."
},
(2, 2): {
"key": "South-east corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones."
},
(1, 3): {
"key": "North-west corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones."
},
(2, 3): {
"key": "North-east corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones. To the east is a dark passage."
},
(3, 3): {
"key": "Craggy crevice",
"desc": "This is the deepest part of the dungeon. The path shrinks away and there "
"is no way to continue deeper."
},
# default fallback for undefined nodes
('*', '*'): {
"key": "A dark room",
"desc": "A dark, but empty, room."
},
# directional prototypes
(1, 0, 'w'): {
"desc": "A narrow path to the fresh air of the outside world."
},
# directional fallbacks for unset directions
('*', '*', '*'): {
"desc": "A dark passage"
}
}
# this is required by the prototypes, but we add it all at once so we don't
# need to add it to every line above
for prot in PROTOTYPES_MAP2.values():
prot['prototype_parent'] = PARENT
MAP_DATA = {
"name": "Dungeon of Doom",
"map": MAP,
"legend": LEGEND,
"rooms": ROOMS,
XYMAP_DATA_MAP2 = {
"map": MAP2,
"zcoord": "the small cave",
"legend": LEGEND_MAP2,
"prototypes": PROTOTYPES_MAP2
}
XYMAP_LIST = [
MAP_DATA
# This is read by the parser
XYMAP_DATA_LIST = [
XYMAP_DATA_MAP1,
XYMAP_DATA_MAP2
]

View file

@ -79,8 +79,8 @@ class MapNode:
'sw': ('southwest', 'sw', 'south-west'),
'w': ('west', 'w'),
'nw': ('northwest', 'nw', 'north-west'),
'd' : ('down', 'd', 'do'),
'u' : ('up', 'u'),
'd': ('down', 'd', 'do'),
'u': ('up', 'u'),
}
def __init__(self, x, y, Z, node_index=0, xymap=None):
@ -261,6 +261,7 @@ class MapNode:
return
xyz = self.get_spawn_xyz()
print("xyz:", xyz, self.node_index)
try:
nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
@ -944,9 +945,10 @@ class SmartMapLink(MapLink):
directions[direction] = REVERSE_DIRECTIONS[direction]
else:
raise MapParserError(
f"must have exactly two connections - either "
f"two nodes or unambiguous link directions. Found neighbor(s) in directions "
f"{list(neighbors.keys())}.", self)
"must have exactly two connections - either directly to "
"two nodes or connecting directly to one node and with exactly one other "
f"link direction. The neighbor(s) in directions {list(neighbors.keys())} do "
"not fulfill these criteria.", self)
self.directions = directions
return self.directions.get(start_direction)
@ -1027,7 +1029,7 @@ class InvisibleSmartMapLink(SmartMapLink):
class BasicMapNode(MapNode):
"""Basic map Node"""
symbol = "#"
prototype = "xyz_room_prototype"
prototype = "xyz_room"
class MapTransitionMapNode(TransitionMapNode):
@ -1043,35 +1045,35 @@ class InterruptMapNode(MapNode):
symbol = "I"
display_symbol = "#"
interrupt_path = True
prototype = "xyz_room_prototype"
prototype = "xyz_room"
class NSMapLink(MapLink):
"""Two-way, North-South link"""
symbol = "|"
directions = {"n": "s", "s": "n"}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class EWMapLink(MapLink):
"""Two-way, East-West link"""
symbol = "-"
directions = {"e": "w", "w": "e"}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class NESWMapLink(MapLink):
"""Two-way, NorthWest-SouthWest link"""
symbol = "/"
directions = {"ne": "sw", "sw": "ne"}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class SENWMapLink(MapLink):
"""Two-way, SouthEast-NorthWest link"""
symbol = "\\"
directions = {"se": "nw", "nw": "se"}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class PlusMapLink(MapLink):
@ -1079,7 +1081,7 @@ class PlusMapLink(MapLink):
symbol = "+"
directions = {"s": "n", "n": "s",
"e": "w", "w": "e"}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class CrossMapLink(MapLink):
@ -1087,35 +1089,35 @@ class CrossMapLink(MapLink):
symbol = "x"
directions = {"ne": "sw", "sw": "ne",
"se": "nw", "nw": "se"}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class NSOneWayMapLink(MapLink):
"""One-way North-South link"""
symbol = "v"
directions = {"n": "s"}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class SNOneWayMapLink(MapLink):
"""One-way South-North link"""
symbol = "^"
directions = {"s": "n"}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class EWOneWayMapLink(MapLink):
"""One-way East-West link"""
symbol = "<"
directions = {"e": "w"}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class WEOneWayMapLink(MapLink):
"""One-way West-East link"""
symbol = ">"
directions = {"w": "e"}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class UpMapLink(SmartMapLink):
@ -1125,7 +1127,7 @@ class UpMapLink(SmartMapLink):
# all movement over this link is 'up', regardless of where on the xygrid we move.
direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol,
's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class DownMapLink(UpMapLink):
@ -1134,14 +1136,14 @@ class DownMapLink(UpMapLink):
# all movement over this link is 'down', regardless of where on the xygrid we move.
direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol,
's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class InterruptMapLink(InvisibleSmartMapLink):
"""A (still passable) link that causes the pathfinder to stop before crossing."""
symbol = "i"
interrupt_path = True
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class BlockedMapLink(InvisibleSmartMapLink):
@ -1154,7 +1156,7 @@ class BlockedMapLink(InvisibleSmartMapLink):
symbol = 'b'
weights = {'n': BIGVAL, 'ne': BIGVAL, 'e': BIGVAL, 'se': BIGVAL,
's': BIGVAL, 'sw': BIGVAL, 'w': BIGVAL, 'nw': BIGVAL}
prototype = "xyz_exit_prototype"
prototype = "xyz_exit"
class RouterMapLink(SmartRerouterMapLink):

View file

@ -1,25 +0,0 @@
"""
Maprunner
This is a stand-alone program for baking and preparing grid-maps.
## Baking
The Dijkstra algorithm is very powerful for pathfinding, but for very large grids it can be slow
to build the initial distance-matrix. As an example, for an extreme case of 10 000 nodes, all
connected along all 8 cardinal directions, there are so many possible combinations that it
takes about 25 seconds on medium hardware to build the matrix. 40 000 nodes takes about 9 minutes.
Once the matrix is built, pathfinding across the entire grid is a <0.1s operation however. So as
long as the grid doesn't change, it's a good idea to pre-build it. Pre-building like this is
often referred to as 'baking' the asset.
This program will build and run the Dijkstra on a given map and store the result as a
serialized binary file in the `mygame/server/.cache/ directory. If it exists, the Map
will load this file. If the map changed since it was saved, the file will be automatically
be rebuilt.
"""

View file

@ -1,35 +1,32 @@
"""
Prototypes for building the XYZ-grid into actual game-rooms.
Default prototypes for building the XYZ-grid into actual game-rooms.
Add this to mygame/conf/settings/settings.py:
PROTOTYPE_MODULES += ['evennia.contrib.xyzgrid.prototypes']
The prototypes can then be used in mapping prototypes as
{'prototype_parent': 'xyz_room', ...}
and/or
{'prototype_parent': 'xyz_exit', ...}
"""
# Note - the XYZRoom/exit parents track the XYZ coordinates automatically
# so we don't need to add custom tags to them here.
_ROOM_PARENT = {
'prototype_tags': ("xyzroom", ),
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZRoom'
}
_EXIT_PARENT = {
'prototype_tags': ("xyzexit", ),
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZExit'
}
# required by the prototype importer
PROTOTYPE_LIST = [
{
'prototype_key': 'xyz_room_prototype',
'prototype_parent': _ROOM_PARENT,
'key': "A non-descript room",
},{
'prototype_key': 'xyz_transition_room_prototype',
'prototype_parent': _ROOM_PARENT,
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZMapTransitionRoom',
},{
'prototype_key': 'xyz_exit_prototype',
'prototype_parent': _EXIT_PARENT,
'prototype_key': 'xyz_room',
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZRoom',
'prototype_tags': ("xyzroom", ),
'key': "A room",
'desc': "An empty room."
}, {
'prototype_key': 'xyz_exit',
'prototype_tags': ("xyzexit", ),
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZExit',
'desc': "An exit."
}
]

View file

@ -349,11 +349,11 @@ class _MapTest(TestCase):
map_data = {'map': MAP1, 'zcoord': "map1"}
map_display = MAP1_DISPLAY
def setUp(self):
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'])
self.map = self.grid.get_map(self.map_data['zcoord'])
def tearDown(self):
self.grid.delete()
@ -1155,7 +1155,7 @@ class TestXYZGrid(TestCase):
def test_str_output(self):
"""Check the display_map"""
xymap = self.grid.get(self.zcoord)
xymap = self.grid.get_map(self.zcoord)
stripped_map = "\n".join(line.rstrip() for line in str(xymap).split('\n'))
self.assertEqual(MAP1_DISPLAY, stripped_map)
@ -1215,7 +1215,7 @@ class TestXYZGridTransition(TestCase):
test shortest-path calculations throughout the grid.
"""
directions, _ = self.grid.get('map12a').get_shortest_path(startcoord, endcoord)
directions, _ = self.grid.get_map('map12a').get_shortest_path(startcoord, endcoord)
self.assertEqual(expected_directions, tuple(directions))
def test_spawn(self):
@ -1236,3 +1236,41 @@ class TestXYZGridTransition(TestCase):
# make sure exits traverse the maps
self.assertEqual(east_exit.db_destination, room2)
self.assertEqual(west_exit.db_destination, room1)
class TestBuildExampleGrid(TestCase):
"""
Test building the map_example
"""
def setUp(self):
# build and populate grid
self.grid, err = xyzgrid.XYZGrid.create("testgrid")
def tearDown(self):
self.grid.delete()
def test_build(self):
"""
Build the map example.
"""
mapdatas = self.grid.maps_from_module("evennia.contrib.xyzgrid.map_example")
self.assertEqual(len(mapdatas), 2)
self.grid.add_maps(*mapdatas)
self.grid.spawn()
# testing
room1a = xyzroom.XYZRoom.objects.get_xyz(xyz=(3, 0, 'the large tree'))
room1b = xyzroom.XYZRoom.objects.get_xyz(xyz=(10, 9, 'the large tree'))
room2a = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 0, 'small cave'))
room2b = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 3, 'small cave'))
self.assertEqual(room1a.key, "Dungeon Entrance")
self.assertTrue(room1a.desc.startswith("To the west"))
self.assertEqual(room1b.key, "A gorgeous view")
self.assertTrue(room1b.desc.startswith("The view from here is breathtaking."))
self.assertEqual(room2a.key, "The entrance")
self.assertTrue(room2a.desc.startswith("This is the entrance to"))
self.assertEqual(room2b.key, "North-west corner of the atrium")
self.assertTrue(room2b.desc.startswith("Sunlight sifts down"))

View file

@ -36,7 +36,7 @@ class MapError(RuntimeError):
prefix = ""
if node_or_link:
prefix = (f"{node_or_link.__class__.__name__} '{node_or_link.symbol}' "
f"at XY=({node_or_link.X:g},{node_or_link.Y:g}) ")
f"at XYZ=({node_or_link.X:g},{node_or_link.Y:g},{node_or_link.Z}) ")
self.node_or_link = node_or_link
self.message = f"{prefix}{error}"
super().__init__(self.message)

View file

@ -12,9 +12,7 @@ as up and down. These are indicated in code as 'n', 'ne', 'e', 'se', 's', 'sw',
```python
# in module passed to 'Map' class. It will either a dict
# MAP_DATA with keys 'map' and (optionally) 'legend', or
# the MAP/LEGEND variables directly.
# in module passed to 'Map' class
MAP = r'''
1
@ -47,10 +45,11 @@ as up and down. These are indicated in code as 'n', 'ne', 'e', 'se', 's', 'sw',
'''
LEGEND = {'#': xyzgrid.MapNode, '|': xyzgrid.NSMapLink,...}
# optional, for more control
MAP_DATA = {
# read by parser if XYMAP_DATA_LIST doesn't exist
XYMAP_DATA = {
"map": MAP,
"legend": LEGEND,
"zcoord": "City of Foo",
@ -62,6 +61,11 @@ as up and down. These are indicated in code as 'n', 'ne', 'e', 'se', 's', 'sw',
}
# will be parsed first, allows for multiple map-data dicts from one module
XYMAP_DATA_LIST = [
XYMAP_DATA
]
```
The two `+` signs in the upper/lower left corners are required and marks the edge of the map area.
@ -102,7 +106,7 @@ except ImportError as err:
f"{err}\nThe XYZgrid contrib requires "
"the SciPy package. Install with `pip install scipy'.")
from django.conf import settings
from evennia.utils.utils import variable_from_module, mod_import
from evennia.utils.utils import variable_from_module, mod_import, is_iter
from evennia.utils import logger
from evennia.prototypes import prototypes as protlib
@ -140,7 +144,6 @@ DEFAULT_LEGEND = {
}
# --------------------------------------------
# Map parser implementation
@ -309,13 +312,18 @@ class XYMap:
else:
# read from contents of module
mod = mod_import(map_module_or_dict)
mapdata = variable_from_module(mod, "MAP_DATA")
mapdata_list = variable_from_module(mod, "XYMAP_DATA_LIST")
if mapdata_list and self.Z:
# use the stored Z value to figure out which map data we want
mapping = {mapdata.get("zcoord") for mapdata in mapdata_list}
mapdata = mapping.get(self.Z, {})
if not mapdata:
# try to read mapdata directly from global variables
mapdata['zcoord'] = variable_from_module(mod, "ZCOORD", default=self.name)
mapdata['map'] = variable_from_module(mod, "MAP")
mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND)
mapdata['prototypes'] = variable_from_module(mod, "PROTOTYPES", default={})
mapdata = variable_from_module(mod, "XYMAP_DATA")
if not mapdata:
raise MapError("No valid XYMAP_DATA or XYMAP_DATA_LIST could be found from "
f"{map_module_or_dict}.")
# validate
if any(key for key in mapdata if key not in MAP_DATA_KEYS):
@ -330,10 +338,9 @@ class XYMap:
"`.display_symbol` property to change how it is "
"displayed.")
if 'map' not in mapdata or not mapdata['map']:
raise MapError("No map found. Add 'map' key to map-data (MAP_DATA) dict or "
"add variable MAP to a module passed into the parser.")
raise MapError("No map found. Add 'map' key to map-data dict.")
for key, prototype in mapdata.get('prototypes', {}).items():
if not is_iter(key) and (2 <= len(key) <= 3):
if not (is_iter(key) and (2 <= len(key) <= 3)):
raise MapError(f"Prototype override key {key} is malformed: It must be a "
"coordinate (X, Y) for nodes or (X, Y, direction) for links; "
"where direction is a supported direction string ('n', 'ne', etc).")
@ -491,10 +498,13 @@ class XYMap:
for node in node_index_map.values():
node_coord = (node.X, node.Y)
# load prototype from override, or use default
node.prototype = self.prototypes.get(node_coord, node.prototype)
node.prototype = self.prototypes.get(
node_coord, self.prototypes.get(('*', '*'), node.prototype))
# do the same for links (x, y, direction) coords
for direction, maplink in node.first_links.items():
maplink.prototype = self.prototypes.get(node_coord + (direction,), maplink.prototype)
maplink.prototype = self.prototypes.get(
node_coord + (direction,),
self.prototypes.get(('*', '*', '*'), maplink.prototype))
# store
self.display_map = display_map
@ -547,13 +557,16 @@ class XYMap:
points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist)
return list(set(points)), xmin, xmax, ymin, ymax
def calculate_path_matrix(self):
def calculate_path_matrix(self, force=False):
"""
Solve the pathfinding problem using Dijkstra's algorithm. This will try to
load the solution from disk if possible.
Args:
force (bool, optional): If the cache should always be rebuilt.
"""
if self.pathfinder_baked_filename and isfile(self.pathfinder_baked_filename):
if not force and self.pathfinder_baked_filename and isfile(self.pathfinder_baked_filename):
# check if the solution for this grid was already solved previously.
mapstr, dist_matrix, pathfinding_routes = "", None, None
@ -569,7 +582,6 @@ class XYMap:
# we can re-use the stored data!
self.dist_matrix = dist_matrix
self.pathfinding_routes = pathfinding_routes
return
# build a matrix representing the map graph, with 0s as impassable areas
@ -615,7 +627,7 @@ class XYMap:
wildcard = '*'
spawned = []
for node in self.node_index_map.values():
for node in sorted(self.node_index_map.values(), key=lambda n: (n.Y, n.X)):
if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)):
node.spawn()
spawned.append(node)
@ -643,7 +655,7 @@ class XYMap:
wildcard = '*'
if not nodes:
nodes = self.node_index_map.values()
nodes = sorted(self.node_index_map.values(), key=lambda n: (n.Z, n.Y, n.X))
for node in nodes:
if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)):

View file

@ -18,8 +18,9 @@ The grid has three main functions:
"""
from evennia.scripts.scripts import DefaultScript
from evennia.utils import logger
from evennia.utils.utils import variable_from_module, make_iter
from .xymap import XYMap
from .xyzroom import XYZRoom
from .xyzroom import XYZRoom, XYZExit
class XYZGrid(DefaultScript):
@ -40,7 +41,7 @@ class XYZGrid(DefaultScript):
self.reload()
return self.ndb.grid
def get(self, zcoord):
def get_map(self, zcoord):
"""
Get a specific xymap.
@ -53,7 +54,7 @@ class XYZGrid(DefaultScript):
"""
return self.grid.get(zcoord)
def all(self):
def all_maps(self):
"""
Get all xymaps stored in the grid.
@ -63,20 +64,55 @@ class XYZGrid(DefaultScript):
"""
return self.grid
def log(self, msg):
logger.log_info(f"|grid| {msg}")
def get_room(xyz, **kwargs):
"""
Get room object from XYZ coordinate.
Args:
xyz (tuple): X,Y,Z coordinate of room to fetch.
Returns:
XYZRoom: The found room.
Raises:
XYZRoom.DoesNotExist: If room is not found.
Notes:
This assumes the room was previously built.
"""
return XYZRoom.objects.get_xyz(xyz=xyz, **kwargs)
def get_exit(xyz, name='north', **kwargs):
"""
Get exit object at coordinate.
Args:
xyz (tuple): X,Y,Z coordinate of the room the
exit leads out of.
name (str): The full name of the exit, e.g. 'north' or 'northwest'.
"""
kwargs['db_key'] = name
return XYZExit.objects.get_xyz_exit(xyz=xyz, **kwargs)
def reload(self):
"""
Reload and rebuild the grid. This is done on a server reload and is also necessary if adding
a new map since this may introduce new between-map traversals.
"""
logger.log_info("[grid] (Re)loading grid ...")
self.log("(Re)loading grid ...")
self.ndb.grid = {}
nmaps = 0
# generate all Maps - this will also initialize their components
# and bake any pathfinding paths (or load from disk-cache)
for zcoord, mapdata in self.db.map_data.items():
logger.log_info(f"[grid] Loading map '{zcoord}'...")
self.log(f"Loading map '{zcoord}'...")
xymap = XYMap(dict(mapdata), Z=zcoord, xyzgrid=self)
xymap.parse()
xymap.calculate_path_matrix()
@ -84,7 +120,7 @@ class XYZGrid(DefaultScript):
nmaps += 1
# store
logger.log_info(f"[grid] Loaded and linked {nmaps} map(s).")
self.log(f"Loaded and linked {nmaps} map(s).")
def at_init(self):
"""
@ -94,6 +130,27 @@ class XYZGrid(DefaultScript):
"""
self.reload()
def maps_from_module(self, module):
"""
Load map data from module. The loader will look for a dict XYMAP_DATA or a list of
XYMAP_DATA_LIST (a list of XYMAP_DATA dicts). Each XYMAP_DATA dict should contain
`{"xymap": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict}`.
Args:
module (module or str): A module or python-path to a module containing
map data as either `XYMAP_DATA` or `XYMAP_DATA_LIST` variables.
Returns:
list: List of zero, one or more xy-map data dicts loaded from the module.
"""
map_data_list = variable_from_module(module, "XYMAP_DATA_LIST")
if not map_data_list:
map_data_list = variable_from_module(module, "XYMAP_DATA")
if map_data_list:
map_data_list = make_iter(map_data_list)
return map_data_list
def add_maps(self, *mapdatas):
"""
Add map or maps to the grid.
@ -176,12 +233,12 @@ class XYZGrid(DefaultScript):
# first build all nodes/rooms
for zcoord, xymap in xymaps.items():
logger.log_info(f"[grid] spawning/updating nodes for {zcoord} ...")
self.log(f"spawning/updating nodes for {zcoord} ...")
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} ...")
self.log(f"spawning/updating links for {zcoord} ...")
xymap.spawn_links(xy=(x, y), directions=directions)

View file

@ -10,7 +10,6 @@ used as stand-alone XYZ-coordinate-aware rooms.
from django.db.models import Q
from evennia.objects.objects import DefaultRoom, DefaultExit
from evennia.objects.manager import ObjectManager
from evennia.utils.utils import inherits_from
# name of all tag categories. Note that the Z-coordinate is
# the `map_name` of the XYZgrid
@ -50,8 +49,6 @@ class XYZManager(ObjectManager):
x, y, z = xyz
wildcard = '*'
return (
self
.filter_family(**kwargs)