Test build/spawn launche command

This commit is contained in:
Griatch 2021-07-10 23:59:36 +02:00
parent dad9575a70
commit 8c9f7e58ef
6 changed files with 289 additions and 125 deletions

View file

@ -22,77 +22,157 @@ from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid
_HELP_SHORT = """
evennia xyzgrid help|list|init|add|build|initpath|delete [<options>]
Manages the XYZ grid. Use 'xyzgrid help' for documentation.
Manages the XYZ grid. Use 'xyzgrid help <option>' for documentation.
"""
_HELP_LONG = """
evennia xyzgrid list
_HELP_HELP = """
evennia xyzgrid <command> [<options>]
Manages the XYZ grid.
help <command> - get help about each command:
list - show list
init - initialize grid (only one time)
add - add new maps to grid
build - build added maps into actual db-rooms/exits
initpath - (re)creates pathfinder matrices
delete - delete part or all of grid
"""
_HELP_LIST = """
list
Lists the map grid structure and any loaded maps.
evennia xyzgrid list Z|mapname
list <Z|mapname>
Display the given XYmap in more detail. Also 'show' works.
Display the given XYmap in more detail. Also 'show' works. Use quotes around
map-names with spaces.
evennia xyzgrid init
Examples:
evennia xyzgrid list
evennia xyzgrid list mymap
evennia xyzgrid list "the small cave"
"""
_HELP_INIT = """
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 [,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
- a XYMAP_DATA a dict
{"map": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict}
describing one single XYmap, or
- 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 running 'build', or you'll get errors when creating transitional exits
between maps.
evennia xyzgrid build
Builds/updates the entire database grid based on the added maps. For a new grid, this will spawn
all new rooms/exits (and may take a good while!). For updating, rooms may be removed/spawned if
a map changed since the last build.
evennia xyzgrid build (X,Y,Z|mapname)
Builds/updates only a part of the grid. This should usually only be used if the full grid has
already been built once - otherwise inter-map transitions may fail! Z is the name/z-coordinate
of the map. Use '*' as a wildcard. For example (*, *, mymap) will only update map `mymap` and
(12, 6, mymap) will only update position (12, 6) on the map 'mymap'.
evennia xyzgrid initpath
Recreates the pathfinder matrices for the entire grid. These are used for all shortest-path
calculations. The result will be cached to disk (in mygame/server/.cache/). If not run, each map
will run this automatically first time it's used. Running this will always force to rebuild the
cache.
evennia xyzgrid initpath Z|mapname
recreate the pathfinder matrix for a specific map only. Z is the name/z-coordinate of the map.
evennia xyzgrid delete Z|mapname
Remove a previously added XYmap with the name/z-coordinate Z. E.g. 'remove mymap'. If the map
was built, this will also wipe all its spawned rooms/exits. You will be asked to confirm before
continuing with this operation.
evennia xyzgrid delete
WARNING: This will delete the entire xyz-grid (all maps), and *all* rooms/exits built to match
it (they serve no purpose without the grid). You will be asked to confirm before continuing with
this operation.
Example:
evennia xyzgrid init
"""
def _option_list(**suboptions):
_HELP_ADD = """
add <path.to.xymap.module> [<path> <path>,...]
Add path(s) to one or more modules containing XYMap definitions. The module will be parsed
for
- a XYMAP_DATA - a dict on this form:
{"map": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict}
describing one single XYmap, or
- a XYMAP_DATA_LIST - a list of multiple dicts on the XYMAP_DATA form. This allows for
embedding multiple maps in the same module. See evennia/contrib/xyzgrid/map_example.py
for an example of how this looks.
Note that adding a map does *not* build it. If maps are linked to one another, you should
add all linked maps before running 'build', or you'll get errors when creating transitional
exits between maps.
Examples:
evennia xyzgrid add evennia.contrib.xyzgrid.map_example
evennia xyzgrid add world.mymap1 world.mymap2 world.mymap3
"""
_HELP_BUILD = """
build
Builds/updates the entire database grid based on the added maps. For a new grid, this will
spawn all new rooms/exits (and may take a good while!). For updating, rooms may be
removed/spawned if a map changed since the last build.
build "(X,Y,Z|mapname)"
Builds/updates only a part of the grid. Remember the quotes around the coordinate (this
is mostly because shells don't like them)! Use '*' as a wild card for XY coordinates.
This should usually only be used if the full grid has already been built once - otherwise
inter-map transitions may fail! Z is the name/z-coordinate of the map to build.
Examples:
evennia xyzgrid build - build all
evennia xyzgrid "(*, *, mymap1)" - build everything of map/zcoord mymap1
evennia xyzgrid "(12, 5, mymap1)" - build only coordinate (12, 5) on map/zcoord mymap1
"""
_HELP_INITPATH = """
initpath
Recreates the pathfinder matrices for the entire grid. These are used for all shortest-path
calculations. The result will be cached to disk (in mygame/server/.cache/). If not run, each
map will run this automatically first time it's used. Running this will always force to
rebuild the cache.
initpath Z|mapname
recreate the pathfinder matrix for a specific map only. Z is the name/z-coordinate of the
map. If the map name has spaces in it, use quotes.
Examples:
evennia xyzgrid initpath
evennia xyzgrid initpath mymap1
evennia xyzgrid initpath "the small cave"
"""
_HELP_DELETE = """
delete
WARNING: This will delete the entire xyz-grid (all maps), and *all* rooms/exits built to
match it (they serve no purpose without the grid). You will be asked to confirm before
continuing with this operation.
delete Z|mapname
Remove a previously added XYmap with the name/z-coordinate Z. If the map was built, this
will also wipe all its spawned rooms/exits. You will be asked to confirm before continuing
with this operation. Use quotes if the Z/mapname contains spaces.
Examples:
evennia xyzgrid delete
evennia xyzgrid delete mymap1
evennia xyzgrid delete "the small cave"
"""
_TOPICS_MAP = {
"list": _HELP_LIST,
"init": _HELP_INIT,
"add": _HELP_ADD,
"build": _HELP_BUILD,
"initpath": _HELP_INITPATH,
"delete": _HELP_DELETE
}
def _option_help(*suboptions):
"""
Show help <command> aid.
"""
if not suboptions:
topic = _HELP_HELP
else:
topic = _TOPICS_MAP.get(suboptions[0], _HELP_HELP)
print(topic.strip())
def _option_list(*suboptions):
"""
List/view grid.
@ -106,7 +186,43 @@ def _option_list(**suboptions):
if not suboptions:
print("XYMaps stored in grid:")
for zcoord, xymap in sorted(xymap_data.items(), key=lambda tup: tup[0]):
print("\n" + str(repr(xymap)) + ":\n")
print(str(xymap))
return
for zcoord in suboptions:
xymap = xyzgrid.get_map(zcoord)
if not xymap:
print(f"No XYMap with Z='{zcoord}' was found on grid.")
else:
nrooms = xyzgrid.get_room(('*', '*', zcoord)).count()
nnodes = len(xymap.node_index_map)
print("\n" + str(repr(xymap)) + ":\n")
checkwarning = True
if not nrooms:
print(f"{nrooms} / {nnodes} rooms are spawned.")
checkwarning = False
elif nrooms < nnodes:
print(f"{nrooms} / {nnodes} rooms are spawned\n"
"Note: Transitional nodes are *not* spawned (they just point \n"
"to another map), so the 'missing room(s)' may just be from such nodes.")
elif nrooms > nnodes:
print(f"{nrooms} / {nnodes} rooms are spawned\n"
"Note: Maybe some rooms were removed from map. Run 'build' to re-sync.")
else:
print(f"{nrooms} / {nnodes} rooms are spawned\n")
if checkwarning:
print("Note: This check is not complete; it does not consider changed map "
"topology\nlike relocated nodes/rooms and new/removed links/exits - this "
"is calculated only during a build.")
print("\nDisplayed map (as appearing in-game):\n\n" + str(xymap))
print("\nRaw map string (including axes and invisible nodes/links):\n"
+ str(xymap.mapstring))
legend = []
for key, node_or_link in xymap.legend.items():
legend.append(f"{key} - {node_or_link.__doc__.strip()}")
print("Legend (all elements may not be present on map):\n " + "\n ".join(legend))
def _option_init(*suboptions):
@ -126,8 +242,15 @@ def _option_add(*suboptions):
grid = get_xyzgrid()
xymap_data_list = []
for path in suboptions:
xymap_data_list.expand(grid.maps_from_module(path))
maps = grid.maps_from_module(path)
if not maps:
print(f"No maps found with the path {path}.\nSeparate multiple paths with spaces. ")
return
mapnames = "\n ".join(f"'{m['zcoord']}'" for m in maps)
print(f" XYMaps from {path}:\n {mapnames}")
xymap_data_list.extend(maps)
grid.add_maps(*xymap_data_list)
print(f"Added (or readded) {len(xymap_data_list)} XYMaps to grid.")
def _option_build(*suboptions):
@ -138,7 +261,7 @@ def _option_build(*suboptions):
grid = get_xyzgrid()
# override grid's logger to echo directly to console
def _log(self, msg):
def _log(msg):
print(msg)
grid.log = _log
@ -154,7 +277,20 @@ def _option_build(*suboptions):
else:
x, y, z = '*', '*', '*'
if x == y == z == '*':
inp = input("This will (re)build the entire grid. If it was built before, it may spawn \n"
"new rooms or delete rooms that no longer matches the grid.\nDo you want to "
"continue? [Y]/N? ")
else:
inp = input("This will spawn/delete objects in the database matching grid coordinates \n"
f"({x},{y},{z}) (where '*' is a wildcard).\nDo you want to continue? [Y]/N? ")
if inp.lower() in ('no', 'n'):
print("Aborted.")
return
print("Starting build ...")
grid.spawn(xyz=(x, y, z))
print("... build complete!")
def _option_initpath(*suboptions):
@ -163,13 +299,13 @@ def _option_initpath(*suboptions):
"""
grid = get_xyzgrid()
xymaps = grid.all_rooms()
xymaps = grid.all_maps()
nmaps = len(xymaps)
for inum, xymap in enumerate(grid.all_rooms()):
print(f"Rebuilding pathfinding matrix for xymap Z={xymap.Z} ({inum+1}/{nmaps}) ...")
for inum, xymap in enumerate(xymaps):
print(f"(Re)building pathfinding matrix for xymap Z={xymap.Z} ({inum+1}/{nmaps}) ...")
xymap.calculate_path_matrix(force=True)
cachepath = pathjoin(settings.GAMEDIR, "server", ".cache")
cachepath = pathjoin(settings.GAME_DIR, "server", ".cache")
print(f"... done. Data cached to {cachepath}.")
@ -186,31 +322,33 @@ def _option_delete(*suboptions):
"\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 grid ...")
grid.delete()
else:
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 ...")
print("Deleting grid ...")
grid.delete()
print("... done.")
return
grid.remove_map(*zcoords, remove_objects=True)
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\n"
"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.")
return
print("Deleting selected xymaps ...")
grid.remove_map(*zcoords, remove_objects=True)
print("... done. Remember to remove any links from remaining maps pointing to deleted maps.")
def xyzcommand(*args):
@ -226,8 +364,7 @@ def xyzcommand(*args):
option, *suboptions = args
if option in ('help', 'h'):
print(f"{_HELP_SHORT.strip()}\n{_HELP_LONG.rstrip()}")
_option_help(*suboptions)
if option in ('list', 'show'):
_option_list(*suboptions)
elif option == 'init':

View file

@ -67,7 +67,8 @@ MAP1 = r"""
class TransitionToCave(map_legend.MapTransitionMapNode):
"""
A transition from map2 to map1
A transition from 'the large tree' to 'the small cave' map. This node is never spawned
into a room but only acts as a target for finding the exit's destination.
"""
symbol = 'T'
@ -169,7 +170,8 @@ MAP2 = r"""
# custom map node
class TransitionToLargeTree(map_legend.MapTransitionMapNode):
"""
A transition from map1 to map2
A transition from 'the small cave' to 'the large tree' map. This node is never spawned
into a room by only acts as a target for finding the exit's destination.
"""
symbol = 'T'

View file

@ -810,7 +810,7 @@ class SmartRerouterMapLink(MapLink):
return self.directions.get(start_direction)
class TeleporterMapLink(MapLink):
class SmartTeleporterMapLink(MapLink):
"""
The teleport link works by connecting to nowhere - and will then continue
on another teleport link with the same symbol elsewhere on the map. The teleport
@ -1066,26 +1066,24 @@ class InvisibleSmartMapLink(SmartMapLink):
# Default nodes and link classes
class BasicMapNode(MapNode):
"""Basic map Node"""
"""A map node/room"""
symbol = "#"
prototype = "xyz_room"
class MapTransitionMapNode(TransitionMapNode):
"""Transition-target to other map"""
symbol = "T"
display_symbol = " "
prototype = None # important to leave None!
target_map_xyz = (None, None, None) # must be set manually
class InterruptMapNode(MapNode):
"""A point of interest, where pathfinder will stop"""
"""A point of interest node/room. The pathfinder will always stop here if passing through."""
symbol = "I"
display_symbol = "#"
interrupt_path = True
prototype = "xyz_room"
class MapTransitionMapNode(TransitionMapNode):
"""Transition-target node to other map. This is not actually spawned in-game."""
symbol = "T"
display_symbol = " "
prototype = None # important to leave None!
target_map_xyz = (None, None, None) # must be set manually
class NSMapLink(MapLink):
"""Two-way, North-South link"""
@ -1160,7 +1158,7 @@ class WEOneWayMapLink(MapLink):
class UpMapLink(SmartMapLink):
"""Up direction. Note that this still uses the xygrid!"""
"""Up direction. Note that this stays on the same z-coord so it's a 'fake' up."""
symbol = 'u'
# all movement over this link is 'up', regardless of where on the xygrid we move.
@ -1170,7 +1168,7 @@ class UpMapLink(SmartMapLink):
class DownMapLink(UpMapLink):
"""Works exactly like `UpMapLink` but for the 'down' direction."""
"""Down direction. Note that this stays on the same z-coord, so it's a 'fake' down."""
symbol = 'd'
# 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,
@ -1179,7 +1177,7 @@ class DownMapLink(UpMapLink):
class InterruptMapLink(InvisibleSmartMapLink):
"""A (still passable) link that causes the pathfinder to stop before crossing."""
"""A (still passable) link. Pathfinder will always abort before crossing this link."""
symbol = "i"
interrupt_path = True
prototype = "xyz_exit"
@ -1187,9 +1185,9 @@ class InterruptMapLink(InvisibleSmartMapLink):
class BlockedMapLink(InvisibleSmartMapLink):
"""
A high-weight (but still passable) link that causes the shortest-path algorithm to consider this
a blocked path. The block will not show up in the map display, paths will just never use this
link.
Causes the shortest-path algorithm to consider this a blocked path. The block will not show up
in the map display (and exit can be traversed normally), pathfinder will just not include this
link in any paths.
"""
symbol = 'b'
@ -1199,5 +1197,13 @@ class BlockedMapLink(InvisibleSmartMapLink):
class RouterMapLink(SmartRerouterMapLink):
"""Connects multiple links to build knees, pass-throughs etc."""
"""A link that connects other links to build 'knees', pass-throughs etc."""
symbol = "o"
class TeleporterMapLink(SmartTeleporterMapLink):
"""
Teleporter links. Must appear in pairs on the same xy map. To make it one-way, add additional
one-way link out of the teleporter on one side.
"""
symbol = 't'

View file

@ -233,6 +233,7 @@ class XYMap:
self.xyzgrid = xyzgrid
self.mapstring = ""
self.raw_mapstring = ""
# store so we can reload
self.map_module_or_dict = map_module_or_dict

View file

@ -59,23 +59,24 @@ class XYZGrid(DefaultScript):
Get all xymaps stored in the grid.
Returns:
dict: All initialized xymaps stored with this grid.
list: All initialized xymaps stored with this grid.
"""
return self.grid
return list(self.grid.values())
def log(self, msg):
logger.log_info(f"|grid| {msg}")
def get_room(xyz, **kwargs):
def get_room(self, xyz, **kwargs):
"""
Get room object from XYZ coordinate.
Get one or more room objects from XYZ coordinate.
Args:
xyz (tuple): X,Y,Z coordinate of room to fetch.
xyz (tuple): X,Y,Z coordinate of room to fetch. '*' acts
as wild cards.
Returns:
XYZRoom: The found room.
Queryset: A queryset of XYZRoom(s) found.
Raises:
XYZRoom.DoesNotExist: If room is not found.
@ -84,20 +85,30 @@ class XYZGrid(DefaultScript):
This assumes the room was previously built.
"""
return XYZRoom.objects.get_xyz(xyz=xyz, **kwargs)
return XYZRoom.objects.filter_xyz(xyz=xyz, **kwargs)
def get_exit(xyz, name='north', **kwargs):
def get_exit(self, xyz, name='north', **kwargs):
"""
Get exit object at coordinate.
Get one or more exit object at coordinate.
Args:
xyz (tuple): X,Y,Z coordinate of the room the
exit leads out of.
exit leads out of. '*' acts as a wildcard.
name (str): The full name of the exit, e.g. 'north' or 'northwest'.
The '*' acts as a wild card.
Returns:
Queryset: A queryset of XYZExit(s) found.
"""
kwargs['db_key'] = name
return XYZExit.objects.get_xyz_exit(xyz=xyz, **kwargs)
return XYZExit.objects.filter_xyz_exit(xyz=xyz, **kwargs)
def build_diff(zcoord):
"""
Find out which rooms are built/not built and if some should be deleted. This
"""
def reload(self):
"""
@ -226,12 +237,12 @@ class XYZGrid(DefaultScript):
# first build all nodes/rooms
for zcoord, xymap in xymaps.items():
self.log(f"spawning/updating nodes for {zcoord} ...")
self.log(f"spawning/updating nodes for Z='{zcoord}' ...")
xymap.spawn_nodes(xy=(x, y))
# next build all links between nodes (including between maps)
for zcoord, xymap in xymaps.items():
self.log(f"spawning/updating links for {zcoord} ...")
self.log(f"spawning/updating links for Z='{zcoord}' ...")
xymap.spawn_links(xy=(x, y), directions=directions)

View file

@ -119,7 +119,12 @@ class ContentsHandler:
obj (Object): object to remove
"""
self._pkcache.remove(obj.pk)
try:
self._pkcache.remove(obj.pk)
except KeyError:
# not in pk cache, but can happen deletions happens
# remotely from out-of-thread.
pass
for ctype in obj._content_types:
if obj.pk in self._typecache[ctype]:
self._typecache[ctype].remove(obj.pk)
@ -340,9 +345,11 @@ class ObjectDB(TypedObject):
except RuntimeError:
errmsg = "Error: %s.location = %s creates a location loop." % (self.key, location)
raise RuntimeError(errmsg)
except Exception as e:
errmsg = "Error (%s): %s is not a valid location." % (str(e), location)
raise RuntimeError(errmsg)
except Exception:
# raising here gives more info for now
raise
# errmsg = "Error (%s): %s is not a valid location." % (str(e), location)
# raise RuntimeError(errmsg)
return
def __location_del(self):