mirror of
https://github.com/evennia/evennia.git
synced 2026-03-26 17:56:32 +01:00
Fix remaining map contrib issues
This commit is contained in:
parent
25781b27d7
commit
a3995f5b67
8 changed files with 165 additions and 59 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue