"""
Custom Evennia launcher command option for maintaining 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.grid.xyzgrid.launchcmd.xyzcommand'})
You should now be able to do
::
evennia xyzgrid <options>
Use `evennia xyzgrid help` for usage help.
"""
from os.path import join as pathjoin
from django.conf import settings
import evennia
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid
from evennia.utils import ansi
_HELP_SHORT = """
evennia xyzgrid help | list | init | add | spawn | initpath | delete [<options>]
Manages the XYZ grid. Use 'xyzgrid help <option>' for documentation.
"""
_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
spawn - spawn 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.
list <Z|mapname>
Display the given XYmap in more detail. Also 'show' works. Use quotes around
map-names with spaces.
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.
Example:
evennia xyzgrid init
"""
_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/grid/xyzgrid/example.py
for an example of how this looks.
Note that adding a map does *not* spawn it. If maps are linked to one another, you should
add all linked maps before running 'spawn', or you'll get errors when creating transitional
exits between maps.
Examples:
evennia xyzgrid add evennia.contrib.grid.xyzgrid.example
evennia xyzgrid add world.mymap1 world.mymap2 world.mymap3
"""
_HELP_SPAWN = """
spawn
spawns/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 spawn.
spawn "(X,Y,Z|mapname)"
spawns/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 spawn.
Examples:
evennia xyzgrid spawn - spawn all
evennia xyzgrid "(*, *, mymap1)" - spawn everything of map/zcoord mymap1
evennia xyzgrid "(12, 5, mymap1)" - spawn 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
respawn 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,
"spawn": _HELP_SPAWN,
"initpath": _HELP_INITPATH,
"delete": _HELP_DELETE,
}
evennia._init()
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.
"""
xyzgrid = get_xyzgrid()
# override grid's logger to echo directly to console
def _log(msg):
print(msg)
xyzgrid.log = _log
xymap_data = xyzgrid.grid
if not xymap_data:
if xyzgrid.db.map_data:
print("Grid could not load due to errors.")
else:
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("\n" + str(repr(xymap)) + ":\n")
print(ansi.parse_ansi(str(xymap)))
return
zcoord = " ".join(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 'spawn' 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 spawn."
)
print("\nDisplayed map (as appearing in-game):\n\n" + ansi.parse_ansi(str(xymap)))
print(
"\nRaw map string (including axes and invisible nodes/links):\n" + str(xymap.mapstring)
)
print(f"\nCustom map options: {xymap.options}\n")
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):
"""
Initialize a new grid. Will fail if a Grid already exists.
"""
grid = get_xyzgrid()
print(f"The grid is initalized as the Script '{grid.key}'({grid.dbref})")
def _option_add(*suboptions):
"""
Add one or more map to the grid. Supports `add path,path,path,...`
"""
grid = get_xyzgrid()
# override grid's logger to echo directly to console
def _log(msg):
print(msg)
grid.log = _log
xymap_data_list = []
for path in suboptions:
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)
try:
grid.reload()
except Exception as err:
print(err)
else:
print(f"Added (or readded) {len(xymap_data_list)} XYMaps to grid.")
def _option_spawn(*suboptions):
"""
spawn the grid or part of it.
"""
grid = get_xyzgrid()
# override grid's logger to echo directly to console
def _log(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(
"spawn 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 = "*", "*", "*"
if x == y == z == "*":
inp = input(
"This will (re)spawn 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 spawn ...")
grid.spawn(xyz=(x, y, z))
print(
"... spawn complete!\nIt's recommended to reload the server to refresh caches if this "
"modified an existing grid."
)
def _option_initpath(*suboptions):
"""
(Re)Initialize the pathfinding matrices for grid or part of it.
"""
grid = get_xyzgrid()
# override grid's logger to echo directly to console
def _log(msg):
print(msg)
grid.log = _log
xymaps = grid.all_maps()
nmaps = len(xymaps)
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.GAME_DIR, "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()
# override grid's logger to echo directly to console
def _log(msg):
print(msg)
grid.log = _log
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.")
return
print("Deleting grid ...")
grid.delete()
print(
"... done.\nPlease reload the server now; otherwise removed rooms may linger in cache."
)
return
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_maps()))
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.\nPlease reload the server to refresh room caches."
"\nAlso remember to remove any links from remaining maps pointing to deleted maps."
)
[docs]def xyzcommand(*args):
"""
Evennia launcher command. This is made available as `evennia xyzgrid` on the command line,
once added to `settings.EXTRA_LAUNCHER_COMMANDS`.
"""
if not args:
print(_HELP_SHORT.strip())
return
option, *suboptions = args
if option in ("help", "h"):
_option_help(*suboptions)
elif option in ("list", "show"):
_option_list(*suboptions)
elif option == "init":
_option_init(*suboptions)
elif option == "add":
_option_add(*suboptions)
elif option == "spawn":
_option_spawn(*suboptions)
elif option == "initpath":
_option_initpath(*suboptions)
elif option == "delete":
_option_delete(*suboptions)
else:
print(f"Unknown option '{option}'. Use 'evennia xyzgrid help' for valid arguments.")