Fix remaining map contrib issues

This commit is contained in:
Griatch 2021-07-18 23:28:28 +02:00
parent 25781b27d7
commit a3995f5b67
8 changed files with 165 additions and 59 deletions

View file

@ -191,6 +191,7 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
# how quickly to step (seconds)
auto_step_delay = 2
default_xyz_path_interrupt_msg = "Pathfinding interrupted here."
def _search_by_xyz(self, inp, xyz_start):
inp = inp.strip("()")
@ -286,10 +287,14 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
step_sequence=step_sequence,
task=None
)
# the map can itself tell the stepper to stop the auto-step prematurely
interrupt_node_or_link = None
# pop any extra links up until the next node - these are
# not useful when dealing with exits
while step_sequence:
if not interrupt_node_or_link and step_sequence[0].interrupt_path:
interrupt_node_or_link = step_sequence[0]
if hasattr(step_sequence[0], "node_index"):
break
step_sequence.pop(0)
@ -298,13 +303,28 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
exit_name, *_ = first_link.spawn_aliases.get(
direction, current_node.direction_spawn_defaults.get(direction, ('unknown', )))
if not caller.search(exit_name):
exit_obj = caller.search(exit_name)
if not exit_obj:
# extra safety measure to avoid trying to walk over and over
# if there's something wrong with the exit's name
caller.msg(f"No exit '{exit_name}' found at current location. Aborting goto.")
caller.ndb.xy_path_data = None
return
if interrupt_node_or_link:
# premature stop of pathfind-step because of map node/link of interrupt type
if hasattr(interrupt_node_or_link, "node_index"):
message = exit_obj.destination.attributes.get(
"xyz_path_interrupt_msg", default=self.default_xyz_path_interrupt_msg)
# we move into the node/room and then stop
caller.execute_cmd(exit_name, session=session)
else:
# if the link is interrupted we don't cross it at all
message = exit_obj.attributes.get(
"xyz_path_interrupt_msg", default=self.default_xyz_path_interrupt_msg)
caller.msg(message)
return
# do the actual move - we use the command to allow for more obvious overrides
caller.execute_cmd(exit_name, session=session)

View file

@ -178,7 +178,14 @@ 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:
print("The XYZgrid is currently empty. Use 'add' to add paths to your map data.")
@ -220,6 +227,7 @@ def _option_list(*suboptions):
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()}")
@ -241,6 +249,12 @@ def _option_add(*suboptions):
"""
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)
@ -291,7 +305,8 @@ def _option_build(*suboptions):
print("Starting build ...")
grid.spawn(xyz=(x, y, z))
print("... build complete!")
print("... build complete!\nIt's recommended to reload the server to refresh caches if this "
"modified an existing grid.")
def _option_initpath(*suboptions):
@ -300,6 +315,12 @@ def _option_initpath(*suboptions):
"""
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):
@ -317,6 +338,12 @@ def _option_delete(*suboptions):
"""
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."
@ -326,7 +353,8 @@ def _option_delete(*suboptions):
return
print("Deleting grid ...")
grid.delete()
print("... done.")
print("... done.\nPlease reload the server now; otherwise "
"removed rooms may linger in cache.")
return
zcoords = (part.strip() for part in suboptions)
@ -349,7 +377,8 @@ def _option_delete(*suboptions):
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.")
print("... done.\nPlease reload the server to refresh room caches."
"\nAlso remember to remove any links from remaining maps pointing to deleted maps.")
def xyzcommand(*args):

View file

@ -45,13 +45,13 @@ MAP1 = r"""
8 #-------#-#-------I
\ /
7 #-#---# #-t
7 #-#---# t-#
|\ |
6 #i#-#b--#-t
| |
5 o-#---#
\ /
4 o-o-#-#
4 o---#-#
/ d
3 #-----+-------#
| d
@ -59,7 +59,7 @@ MAP1 = r"""
v u
1 #---#>#-#
/
0 T-#
0 #-T
+ 0 1 2 3 4 5 6 7 8 9 0
1
@ -87,11 +87,11 @@ PROTOTYPES_MAP1 = {
# node/room prototypes
(3, 0): {
"key": "Dungeon Entrance",
"desc": "To the west, a narrow opening leads into darkness."
"desc": "To the east, a narrow opening leads into darkness."
},
(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 "
"desc": "High above the branches of a giant tree blocks out the sunlight. A slide "
"leading down from the upper branches ends here."
},
(4, 4): {
@ -117,11 +117,11 @@ PROTOTYPES_MAP1 = {
},
(5, 6): {
"key": "On a huge branch",
"desc": "To the east is a glowing light, may be a teleporter."
"desc": "To the east is a glowing light, may be a teleporter to a higher branch."
},
(9, 7): {
"key": "On an enormous branch",
"desc": "To the east is a glowing light, may be a teleporter."
"desc": "To the west is a glowing light. It may be a teleporter to a lower branch."
},
(10, 8): {
"key": "A gorgeous view",

View file

@ -15,6 +15,7 @@ except ImportError as err:
"the SciPy package. Install with `pip install scipy'.")
import uuid
from collections import defaultdict
from evennia.prototypes import spawner
from evennia.utils.utils import make_iter
from .utils import MAPSCAN, REVERSE_DIRECTIONS, MapParserError, BIGVAL
@ -141,7 +142,7 @@ class MapNode:
def log(self, msg):
"""log messages using the xygrid parent"""
self.xymap.xyzgrid.log(msg)
self.xymap.log(msg)
def generate_prototype_key(self):
"""
@ -301,17 +302,19 @@ class MapNode:
xyz = self.get_spawn_xyz()
self.log(f" spawning/updating room at xyz={xyz}")
try:
nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
except NodeTypeclass.DoesNotExist:
# create a new entity with proper coordinates etc
self.log(f" spawning room at xyz={xyz}")
nodeobj, err = NodeTypeclass.create(
self.prototype.get('key', 'An empty room'),
xyz=xyz
)
if err:
raise RuntimeError(err)
else:
self.log(f" updating existing room (if changed) at xyz={xyz}")
if not self.prototype.get('prototype_key'):
# make sure there is a prototype_key in prototype
@ -356,6 +359,17 @@ class MapNode:
link.prototype['prototype_key'] = self.generate_prototype_key()
maplinks[key.lower()] = (key, aliases, direction, link)
# if xyz == (8, 1, 'the large tree'):
# from evennia import set_trace;set_trace()
# remove duplicates
linkobjs = defaultdict(list)
for exitobj in ExitTypeclass.objects.filter_xyz(xyz=xyz):
linkobjs[exitobj.key].append(exitobj)
for exitkey, exitobjs in linkobjs.items():
for exitobj in exitobjs[1:]:
self.log(f" deleting duplicate {exitkey}")
exitobj.delete()
# we need to search for exits in all directions since some
# may have been removed since last sync
linkobjs = {exi.db_key.lower(): exi
@ -365,10 +379,11 @@ class MapNode:
# build all exits first run)
differing_keys = set(maplinks.keys()).symmetric_difference(set(linkobjs.keys()))
for differing_key in differing_keys:
# from evennia import set_trace;set_trace()
if differing_key not in maplinks:
# an exit without a maplink - delete the exit-object
self.log(f" deleting exit at xyz={xyz}, direction={direction}")
self.log(f" deleting exit at xyz={xyz}, direction={differing_key}")
linkobjs.pop(differing_key).delete()
else:
@ -388,7 +403,7 @@ class MapNode:
if err:
raise RuntimeError(err)
linkobjs[key.lower()] = exi
self.log(f" spawning/updating exit xyz={xyz}, direction={key}")
self.log(f" spawning/updating exit xyz={xyz}, direction={key}")
# apply prototypes to catch any changes
for key, linkobj in linkobjs.items():

View file

@ -115,9 +115,10 @@ from . import map_legend
_CACHE_DIR = settings.CACHE_DIR
_LOADED_PROTOTYPES = None
_XYZROOMCLASS = None
MAP_DATA_KEYS = [
"zcoord", "map", "legend", "prototypes", "options"
"zcoord", "map", "legend", "prototypes", "options", "module_path"
]
# these are all symbols used for x,y coordinate spots
@ -280,6 +281,12 @@ class XYMap:
return (f"<XYMap(Z={self.Z}), {self.max_X + 1}x{self.max_Y + 1}, "
f"{len(self.node_index_map)} nodes>")
def log(self, msg):
if self.xyzgrid:
self.xyzgrid.log(msg)
else:
logger.log_info(msg)
def reload(self, map_module_or_dict=None):
"""
(Re)Load a map.
@ -629,10 +636,23 @@ class XYMap:
list: A list of nodes that were spawned.
"""
global _XYZROOMCLASS
if not _XYZROOMCLASS:
from evennia.contrib.xyzgrid.xyzroom import XYZRoom as _XYZROOMCLASS
x, y = xy
wildcard = '*'
spawned = []
# find existing nodes, in case some rooms need to be removed
map_coords = ((node.X, node.Y) for node in
sorted(self.node_index_map.values(), key=lambda n: (n.Y, n.X)))
for existing_room in _XYZROOMCLASS.objects.filter_xyz(xyz=(x, y, self.Z)):
roomX, roomY, _ = existing_room.xyz
if (roomX, roomY) not in map_coords:
self.log(f" deleting room at {existing_room.xyz} (not found on map).")
existing_room.delete()
# (re)build nodes (will not build already existing rooms)
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()
@ -758,15 +778,6 @@ class XYMap:
directions.append(shortest_route_to[0])
path.extend(shortest_route_to[1][::-1] + [nextnode])
if any(1 for step in shortest_route_to[1] if step.interrupt_path):
# detected an interrupt in linkage - discard what we have so far
directions = []
path = [nextnode]
if nextnode.interrupt_path and nextnode is not startnode:
directions = []
path = [nextnode]
# we have the path - reverse to get the correct order
directions = directions[::-1]
path = path[::-1]

View file

@ -111,51 +111,77 @@ class XYZGrid(DefaultScript):
"""
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.
"""
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():
self.log(f"Loading map '{zcoord}'...")
xymap = XYMap(dict(mapdata), Z=zcoord, xyzgrid=self)
xymap.parse()
xymap.calculate_path_matrix()
self.ndb.grid[zcoord] = xymap
nmaps += 1
# store
self.log(f"Loaded and linked {nmaps} map(s).")
self.ndb.loaded = True
def maps_from_module(self, module):
def maps_from_module(self, module_path):
"""
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
module_path (module_path): A 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")
map_data_list = variable_from_module(module_path, "XYMAP_DATA_LIST")
if not map_data_list:
map_data_list = variable_from_module(module, "XYMAP_DATA")
map_data_list = variable_from_module(module_path, "XYMAP_DATA")
if map_data_list:
map_data_list = make_iter(map_data_list)
# inject the python path in the map data
for mapdata in map_data_list:
mapdata['module_path'] = module_path
return map_data_list
def reload(self):
"""
Reload and rebuild the grid. This is done on a server reload.
"""
self.log("(Re)loading grid ...")
self.ndb.grid = {}
nmaps = 0
loaded_mapdata = {}
changed = []
# generate all Maps - this will also initialize their components
# and bake any pathfinding paths (or load from disk-cache)
for zcoord, old_mapdata in self.db.map_data.items():
self.log(f"Loading map '{zcoord}'...")
# we reload the map from module
new_mapdata = loaded_mapdata.get(zcoord)
if not new_mapdata:
if 'module_path' in old_mapdata:
for mapdata in self.maps_from_module(old_mapdata['module_path']):
loaded_mapdata[mapdata['zcoord']] = mapdata
else:
# nowhere to reload from - use what we have
loaded_mapdata[zcoord] = old_mapdata
new_mapdata = loaded_mapdata.get(zcoord)
if new_mapdata != old_mapdata:
self.log(f" XYMap data for Z='{zcoord}' has changed.")
changed.append(zcoord)
xymap = XYMap(dict(new_mapdata), Z=zcoord, xyzgrid=self)
xymap.parse()
xymap.calculate_path_matrix()
self.ndb.grid[zcoord] = xymap
nmaps += 1
# re-store changed data
for zcoord in changed:
self.db.map_data[zcoord] = loaded_mapdata['zcoord']
# store
self.log(f"Loaded and linked {nmaps} map(s).")
self.ndb.loaded = True
def add_maps(self, *mapdatas):
"""
Add map or maps to the grid.
@ -163,9 +189,10 @@ class XYZGrid(DefaultScript):
Args:
*mapdatas (dict): Each argument is a dict structure
`{"map": <mapstr>, "legend": <legenddict>, "name": <name>,
"prototypes": <dict-of-dicts>}`. The `prototypes are
"prototypes": <dict-of-dicts>, "module_path": <str>}`. The `prototypes are
coordinate-specific overrides for nodes/links on the map, keyed with their
(X,Y) coordinate within that map.
(X,Y) coordinate within that map. The `module_path` is injected automatically
by self.maps_from_module.
Raises:
RuntimeError: If mapdata is malformed.

View file

@ -484,7 +484,7 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators
if key:
if key in mod_matches:
# exact match
module_prototypes = [mod_matches[key]]
module_prototypes = [mod_matches[key].copy()]
allow_fuzzy = False
else:
# fuzzy matching
@ -494,7 +494,9 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators
if key in prototype_key
]
else:
module_prototypes = [match for match in mod_matches.values()]
# note - we return a copy of the prototype dict, otherwise using this with e.g.
# prototype_from_object will modify the base prototype for every object
module_prototypes = [match.copy() for match in mod_matches.values()]
# search db-stored prototypes
@ -1053,7 +1055,8 @@ def value_to_obj(value, force=True):
stype = type(value)
if is_iter(value):
if stype == dict:
return {value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.iter()}
return {value_to_obj_or_any(key): value_to_obj_or_any(val)
for key, val in value.items()}
else:
return stype([value_to_obj_or_any(val) for val in value])
return dbid_to_obj(value, ObjectDB)

View file

@ -154,7 +154,8 @@ from evennia.prototypes.prototypes import (
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks")
_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags",
"prototype_locks", "prototype_parent")
_PROTOTYPE_ROOT_NAMES = (
"typeclass",
"key",