diff --git a/evennia/contrib/xyzgrid/launchcmd.py b/evennia/contrib/xyzgrid/launchcmd.py new file mode 100644 index 0000000000..098c8d0bca --- /dev/null +++ b/evennia/contrib/xyzgrid/launchcmd.py @@ -0,0 +1,181 @@ +""" +Custom Evennia launcher command option for building/rebuilding the grid in a separate process than +the main server (since this can be slow). + +To use, add to the settings: +:: + + EXTRA_LAUNCHER_COMMANDS.update({'xyzgrid': 'evennia.contrib.xyzgrid.launchcmd.xyzcommand'}) + +You should now be able to do +:: + + evennia xyzgrid + +Use `evennia xyzgrid help` for usage help. + +""" + +from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid + +_HELP_SHORT = """ +evennia xyzgrid help|list|init|add|build|initpath|delete [] +Manages the XYZ grid. Use 'xyzgrid help' for documentation. +""" + +_HELP_LONG = """ +evennia xyzgrid list + + Lists the map grid structure and any loaded maps. + +evennia xyzgrid list Z|mapname + + Display the given XYmap in more detail. Also 'show' works. + +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 + + 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_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. + +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. + +""" + +def _option_list(**suboptions): + """ + List/view grid. + + """ + xyzgrid = get_xyzgrid() + xymap_data = xyzgrid.grid + if not xymap_data: + print("The XYZgrid is currently empty. Use 'add' to add paths to your map data.") + return + + if not suboptions: + print("XYMaps stored in grid:") + for zcoord, xymap in sorted(xymap_data.items(), key=lambda tup: tup[0]): + print(str(xymap)) + + +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})") + +def _option_add(**suboptions): + """ + Add a new map to the grid. + + """ + +def _option_build(**suboptions): + """ + Build the grid or part of it. + + """ + +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. + + """ + + 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." + "\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 = get_xyzgrid() + grid.delete() + else: + pass + + +def xyzcommand(*args): + """ + Evennia launcher command. This is made available as `evennia xyzgrid` on the command line, + once `settings.EXTRA_LAUNCHER_COMMANDS` is updated. + + """ + if not args: + print(_HELP_SHORT.strip()) + return + + option, *suboptions = args + + if option in ('help', 'h'): + print(f"{_HELP_SHORT.strip()}\n{_HELP_LONG.rstrip()}") + + if option in ('list', 'show'): + _option_list(*suboptions) + elif option == 'init': + _option_init(*suboptions) + elif option == 'add': + _option_add(*suboptions) + elif option == 'build': + _option_build(*suboptions) + elif option == 'initpath': + _option_initpath(*suboptions) + elif option == 'delete': + _option_delete(*suboptions) diff --git a/evennia/contrib/xyzgrid/map_example.py b/evennia/contrib/xyzgrid/map_example.py index 92accb2766..67b4955add 100644 --- a/evennia/contrib/xyzgrid/map_example.py +++ b/evennia/contrib/xyzgrid/map_example.py @@ -62,3 +62,7 @@ MAP_DATA = { "legend": LEGEND, "rooms": ROOMS, } + +XYMAP_LIST = [ + MAP_DATA +] diff --git a/evennia/contrib/xyzgrid/prototypes.py b/evennia/contrib/xyzgrid/prototypes.py new file mode 100644 index 0000000000..ba742483a0 --- /dev/null +++ b/evennia/contrib/xyzgrid/prototypes.py @@ -0,0 +1,35 @@ +""" +Prototypes for building the XYZ-grid into actual game-rooms. + +Add this to mygame/conf/settings/settings.py: + + PROTOTYPE_MODULES += ['evennia.contrib.xyzgrid.prototypes'] + +""" + +# 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' +} + +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, + } +] diff --git a/evennia/contrib/xyzgrid/xyzgrid.py b/evennia/contrib/xyzgrid/xyzgrid.py index 176672f534..e94219b989 100644 --- a/evennia/contrib/xyzgrid/xyzgrid.py +++ b/evennia/contrib/xyzgrid/xyzgrid.py @@ -16,11 +16,10 @@ The grid has three main functions: """ -import itertools from evennia.scripts.scripts import DefaultScript from evennia.utils import logger from .xymap import XYMap -from .xyzroom import XYZRoom, XYZExit +from .xyzroom import XYZRoom class XYZGrid(DefaultScript): @@ -41,8 +40,28 @@ class XYZGrid(DefaultScript): self.reload() return self.ndb.grid - def get(self, mapname, default=None): - return self.grid.get(mapname, default) + def get(self, zcoord): + """ + Get a specific xymap. + + Args: + zcoord (str): The name/zcoord of the xymap. + + Returns: + XYMap: Or None if no map was found. + + """ + return self.grid.get(zcoord) + + def all(self): + """ + Get all xymaps stored in the grid. + + Returns: + dict: All initialized xymaps stored with this grid. + + """ + return self.grid def reload(self): """ @@ -119,10 +138,11 @@ class XYZGrid(DefaultScript): def delete(self): """ - Clear the entire grid, including database entities. + Clear the entire grid, including database entities, then the grid too. """ self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True) + super().delete() def spawn(self, xyz=('*', '*', '*'), directions=None): """ @@ -161,5 +181,24 @@ class XYZGrid(DefaultScript): # next build all links between nodes (including between maps) for zcoord, xymap in xymaps.items(): - logger.log_info(f"[grid] spawning/updating links for {zcoord} ...") - xymap.spawn_links(xy=(x, y), directions=directions) + logger.log_info(f"[grid] spawning/updating links for {zcoord} ...") + xymap.spawn_links(xy=(x, y), directions=directions) + + +def get_xyzgrid(): + """ + Helper for getting the grid. This will create the XYZGrid global script if it didn't + previously exist. + + """ + xyzgrid = XYZGrid.objects.all() + if not xyzgrid: + # create a new one + xyzgrid, err = XYZGrid.create("XYZGrid") + if err: + raise RuntimeError(err) + return xyzgrid + elif len(xyzgrid) > 1: + ("Warning: More than one XYZGrid instances were found. This is an error and " + "only the first one will be used. Delete the other one(s) manually.") + return xyzgrid[0]