mirror of
https://github.com/evennia/evennia.git
synced 2026-03-24 00:36:30 +01:00
Refactoring of map spawner mechanism. Still not working correctly
This commit is contained in:
parent
90ad6c112c
commit
61ab313ee3
9 changed files with 647 additions and 313 deletions
|
|
@ -15,6 +15,7 @@ except ImportError as err:
|
|||
"the SciPy package. Install with `pip install scipy'.")
|
||||
|
||||
from evennia.prototypes import spawner
|
||||
from evennia.utils.utils import make_iter
|
||||
from .utils import MAPSCAN, REVERSE_DIRECTIONS, MapParserError, BIGVAL
|
||||
|
||||
NodeTypeclass = None
|
||||
|
|
@ -48,12 +49,9 @@ class MapNode:
|
|||
expected to be able to continue without some further in-game action not covered by the map
|
||||
(such as a guard or locked gate etc).
|
||||
- `prototype` (dict) - The default `prototype` dict to use for reproducing this map component
|
||||
on the game grid. This is used if not overridden specifically for this coordinate.
|
||||
- `deferred` (bool): A deferred node is used to indicate a link (currently) pointing to nowhere
|
||||
because the end node is not yet available - usually because that node is on another map
|
||||
and won't be available until the full grid has loaded. A deferred node doesn't need a symbol
|
||||
but is returned from links. Links pointing to deferred nodes will be re-parsed once the entire
|
||||
grid has been built, in order to correctly link maps together.
|
||||
on the game grid. This is used if not overridden specifically for this coordinate. If this
|
||||
is not given, nothing will be spawned for this coordinate (a 'virtual' node can be useful
|
||||
for various reasons, mostly map-transitions).
|
||||
|
||||
"""
|
||||
# symbol used to identify this link on the map
|
||||
|
|
@ -61,26 +59,38 @@ class MapNode:
|
|||
# if printing this node should show another symbol. If set
|
||||
# to the empty string, use `symbol`.
|
||||
display_symbol = None
|
||||
|
||||
# internal use. Set during generation, but is also used for identification of the node
|
||||
node_index = None
|
||||
|
||||
# this should always be left True and avoids inifinite loops during querying.
|
||||
multilink = True
|
||||
# this will interrupt a shortest-path step (useful for 'points' of interest, stop before
|
||||
# a door etc).
|
||||
interrupt_path = False
|
||||
# the prototype to use for mapping this to the grid.
|
||||
prototype = None
|
||||
|
||||
# internal use. Set during generation, but is also used for identification of the node
|
||||
node_index = None
|
||||
# this should always be left True for Nodes and avoids inifinite loops during querying.
|
||||
multilink = True
|
||||
# default values to use if the exit doesn't have a 'spawn_aliases' iterable
|
||||
direction_spawn_defaults = {
|
||||
'n': ('north', 'n'),
|
||||
'ne': ('northeast', 'ne', 'north-east'),
|
||||
'e': ('east',),
|
||||
'se': ('southeast', 'se', 'south-east'),
|
||||
's': ('south', 's'),
|
||||
'sw': ('southwest', 'sw', 'south-west'),
|
||||
'w': ('west', 'w'),
|
||||
'nw': ('northwest', 'nw', 'north-west'),
|
||||
'd' : ('down', 'd', 'do'),
|
||||
'u' : ('up', 'u'),
|
||||
}
|
||||
|
||||
def __init__(self, x, y, node_index=0, xymap=None):
|
||||
def __init__(self, x, y, Z, node_index=0, xymap=None):
|
||||
"""
|
||||
Initialize the mapnode.
|
||||
|
||||
Args:
|
||||
x (int): Coordinate on xygrid.
|
||||
y (int): Coordinate on xygrid.
|
||||
Z (int or str): Name/Z-pos of this map.
|
||||
node_index (int): This identifies this node with a running
|
||||
index number required for pathfinding. This is used
|
||||
internally and should not be set manually.
|
||||
|
|
@ -97,6 +107,7 @@ class MapNode:
|
|||
# XYgrid coordinate
|
||||
self.X = x // 2
|
||||
self.Y = y // 2
|
||||
self.Z = Z
|
||||
|
||||
self.node_index = node_index
|
||||
|
||||
|
|
@ -124,22 +135,21 @@ class MapNode:
|
|||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def build_links(self, xygrid):
|
||||
def build_links(self):
|
||||
"""
|
||||
This is called by the map parser when this node is encountered. It tells the node
|
||||
to scan in all directions and follow any found links to other nodes. Since there
|
||||
could be multiple steps to reach another node, the system will iterate down each
|
||||
path and store it once and for all.
|
||||
|
||||
Args:
|
||||
xygrid (dict): A 2d dict-of-dicts with x,y coordinates as keys and nodes as values.
|
||||
|
||||
Notes:
|
||||
This sets up all data needed for later use of this node in pathfinding and
|
||||
other operations. The method can't run immediately when the node is created
|
||||
since a complete parsed xygrid is required.
|
||||
|
||||
"""
|
||||
xygrid = self.xymap.xygrid
|
||||
|
||||
# we must use the xygrid coordinates
|
||||
x, y = self.x, self.y
|
||||
|
||||
|
|
@ -153,7 +163,7 @@ class MapNode:
|
|||
|
||||
# just because there is a link here, doesn't mean it has a
|
||||
# connection in this direction. If so, the `end_node` will be None.
|
||||
end_node, weight, steps = link.traverse(REVERSE_DIRECTIONS[direction], xygrid)
|
||||
end_node, weight, steps = link.traverse(REVERSE_DIRECTIONS[direction])
|
||||
|
||||
if end_node:
|
||||
# the link could be followed to an end node!
|
||||
|
|
@ -207,14 +217,10 @@ class MapNode:
|
|||
link_graph[node_index] = weight
|
||||
return link_graph
|
||||
|
||||
def get_display_symbol(self, xygrid, xymap=None, **kwargs):
|
||||
def get_display_symbol(self):
|
||||
"""
|
||||
Hook to override for customizing how the display_symbol is determined.
|
||||
|
||||
Args:
|
||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||
xymap (XYMap): Main Map object.
|
||||
|
||||
Returns:
|
||||
str: The display-symbol to use. This must visually be a single character
|
||||
but could have color markers, use a unicode font etc.
|
||||
|
|
@ -225,8 +231,21 @@ class MapNode:
|
|||
"""
|
||||
return self.symbol if self.display_symbol is None else self.display_symbol
|
||||
|
||||
def sync_node_to_grid(self):
|
||||
def get_spawn_coords(self):
|
||||
"""
|
||||
This should return the XYZ-coordinates for spawning this node. This normally
|
||||
the XYZ of the current map, but for traversal-nodes, it can also be the location
|
||||
on another map.
|
||||
|
||||
Returns:
|
||||
tuple: The (X, Y, Z) coords to spawn this node at.
|
||||
"""
|
||||
return self.X, self.Y, self.Z
|
||||
|
||||
def spawn(self):
|
||||
"""
|
||||
Build an actual in-game room from this node.
|
||||
|
||||
This should be called as part of the node-sync step of the map sync. The reason is
|
||||
that the exits (next step) requires all nodes to exist before they can link up
|
||||
to their destinations.
|
||||
|
|
@ -234,77 +253,120 @@ class MapNode:
|
|||
"""
|
||||
global NodeTypeclass
|
||||
if not NodeTypeclass:
|
||||
from .room import XYZRoom as NodeTypeclass
|
||||
from .xyzroom import XYZRoom as NodeTypeclass
|
||||
|
||||
coord = (self.X, self.Y, self.xymap.name)
|
||||
if not self.prototype:
|
||||
# no prototype means we can't spawn anything -
|
||||
# a 'virtual' node.
|
||||
return
|
||||
|
||||
coord = self.get_spawn_coords()
|
||||
|
||||
try:
|
||||
nodeobj = NodeTypeclass.objects.get_xyz(coord=coord)
|
||||
except NodeTypeclass.DoesNotExist:
|
||||
# create a new entity with proper coordinates etc
|
||||
nodeobj = NodeTypeclass.create(
|
||||
nodeobj, err = NodeTypeclass.create(
|
||||
self.prototype.get('key', 'An Empty room'),
|
||||
coord=coord
|
||||
)
|
||||
if err:
|
||||
raise RuntimeError(err)
|
||||
# apply prototype to node. This will not override the XYZ tags since
|
||||
# these are not in the prototype and exact=False
|
||||
spawner.batch_update_objects_with_prototype(
|
||||
self.prototype, objects=[nodeobj], exact=False)
|
||||
|
||||
def sync_links_to_grid(self):
|
||||
def spawn_links(self, only_directions=None):
|
||||
"""
|
||||
Build actual in-game exits based on the links out of this room.
|
||||
|
||||
Args:
|
||||
only_directions (list, optional): If given, this should be a list of supported
|
||||
directions (n, ne, etc). Only links in these directions will be spawned
|
||||
for this node.
|
||||
|
||||
This should be called after all `sync_node_to_grid` operations have finished across
|
||||
the entire XYZgrid. This creates/syncs all exits to their locations and destinations.
|
||||
|
||||
"""
|
||||
coord = (self.X, self.Y, self.xymap.name)
|
||||
coord = (self.X, self.Y, self.Z)
|
||||
|
||||
global ExitTypeclass
|
||||
if not ExitTypeclass:
|
||||
from .room import XYZExit as ExitTypeclass
|
||||
from .xyzroom import XYZExit as ExitTypeclass
|
||||
|
||||
maplinks = {}
|
||||
for direction, link in self.first_links.items():
|
||||
key, *aliases = (
|
||||
make_iter(link.spawn_aliases)
|
||||
if link.spawn_aliases
|
||||
else self.direction_spawn_defaults.get(direction, ('unknown',))
|
||||
)
|
||||
maplinks[key.lower()] = (key, aliases, direction, link)
|
||||
|
||||
maplinks = self.first_links
|
||||
# we need to search for exits in all directions since some
|
||||
# may have been removed since last sync
|
||||
linkobjs = {exi.db_key: exi for exi in ExitTypeclass.filter_xyz(coord=coord)}
|
||||
linkobjs = {exi.db_key.lower(): exi
|
||||
for exi in ExitTypeclass.objects.filter_xyz(coord=coord)}
|
||||
|
||||
# figure out if the topology changed between grid and map (will always
|
||||
# build all exits first run)
|
||||
differing_directions = set(maplinks.keys()).symmetric_difference(set(linkobjs.keys()))
|
||||
for direction in differing_directions:
|
||||
if direction in linkobjs:
|
||||
# an exit without a maplink - delete the exit
|
||||
linkobjs.pop(direction).delete()
|
||||
else:
|
||||
# a maplink without an exit - create the exit
|
||||
differing_keys = set(maplinks.keys()).symmetric_difference(set(linkobjs.keys()))
|
||||
for differing_key in differing_keys:
|
||||
|
||||
link = maplinks[direction]
|
||||
if differing_key not in maplinks:
|
||||
# an exit without a maplink - delete the exit-object
|
||||
linkobjs.pop(differing_key).delete()
|
||||
else:
|
||||
# missing in linkobjs - create a new exit
|
||||
key, aliases, direction, link = maplinks[differing_key]
|
||||
exitnode = self.links[direction]
|
||||
|
||||
linkobjs[direction] = ExitTypeclass.create(
|
||||
link.prototype.get('key', direction),
|
||||
# either get name from the prototype or use our custom set
|
||||
key,
|
||||
coord=coord,
|
||||
destination_coord=(exitnode.X, exitnode.Y, exitnode.xymap.name)
|
||||
destination_coord=exitnode.get_spawn_coords(),
|
||||
aliases=aliases,
|
||||
)
|
||||
|
||||
# apply prototypes to catch any changes
|
||||
for direction, linkobj in linkobjs:
|
||||
spawner.batch_update_objects_with_prototype(
|
||||
maplinks[direction].prototype, objects=[linkobj], exact=False)
|
||||
|
||||
def unspawn(self):
|
||||
"""
|
||||
Remove all spawned objects related to this node and all links.
|
||||
|
||||
"""
|
||||
global NodeTypeclass
|
||||
if not NodeTypeclass:
|
||||
from .room import XYZRoom as NodeTypeclass
|
||||
|
||||
try:
|
||||
nodeobj = NodeTypeclass.objects.get_xyz(coord=coord)
|
||||
except NodeTypeclass.DoesNotExist:
|
||||
# no object exists
|
||||
pass
|
||||
else:
|
||||
nodeobj.delete()
|
||||
|
||||
|
||||
class TransitionMapNode(MapNode):
|
||||
"""
|
||||
Entering this node teleports the user to another Map (this is completely handled by the
|
||||
prototyped Room class). This teleportation is not understood by the pathfinder, so why it will
|
||||
be possible to pathfind to this node, it really represents a map transition. Only a single link
|
||||
must ever be connected to this node.
|
||||
This node acts as an end-node for a link that actually leads to a specific node on another
|
||||
map. It is not actually represented by a separate room in-game.
|
||||
|
||||
This teleportation is not understood by the pathfinder, so why it will be possible to pathfind
|
||||
to this node, it really represents a map transition. Only a single link must ever be connected
|
||||
to this node.
|
||||
|
||||
Properties:
|
||||
- `linked_map_name` (str) - the map you will move to when entering this node.
|
||||
- `linked_coords` (tuple) - the XY coordinates *on the linked* map this node
|
||||
will teleport to. This must be another node that is not a TransitionMapNode.
|
||||
Note that for the trip to be two-way, a similar set up must be created from the
|
||||
other map.
|
||||
- `target_map_coord` (tuple) - the (X, Y, Z) coordinate of a node on the other map to teleport
|
||||
to when moving to this node. This should not be another TransitionMapNode (see below for
|
||||
how to make a two-way link).
|
||||
|
||||
Examples:
|
||||
::
|
||||
|
|
@ -312,18 +374,26 @@ class TransitionMapNode(MapNode):
|
|||
map1 map2
|
||||
|
||||
#-T #- - one-way transition from map1 -> map2.
|
||||
#-T T-# - two-way. Both ExternalMapNodes links to the coords of the
|
||||
`#` (NOT the `T`) on the other map!
|
||||
#-T T-# - two-way. Both TransitionMapNodes links to the coords of the
|
||||
actual rooms (`#`) on the other map (NOT to the `T`s)!
|
||||
|
||||
"""
|
||||
symbol = 'T'
|
||||
display_symbol = ' '
|
||||
linked_map_name = ""
|
||||
linked_map_coords = None
|
||||
# X,Y,Z coordinates of target node (not a transitionalmapnode)
|
||||
taget_map_coord = (None, None, None)
|
||||
|
||||
def build_links(self, xygrid):
|
||||
def get_spawn_coords(self):
|
||||
"""
|
||||
Make sure to return the coord of the *target* - this will be used when building
|
||||
the exit to this node (since the prototype is None, this node itself will not be built).
|
||||
|
||||
"""
|
||||
return self.target_map_coord
|
||||
|
||||
def build_links(self):
|
||||
"""Check so we don't have too many links"""
|
||||
super().build_links(xygrid)
|
||||
super().build_links()
|
||||
if len(self.links) > 1:
|
||||
raise MapParserError("may have at most one link connecting to it.", self)
|
||||
|
||||
|
|
@ -349,9 +419,7 @@ class MapLink:
|
|||
- `display_symbol` (str or None) - This is what is used to visualize this node later. This
|
||||
symbol must still only have a visual size of 1, but you could e.g. use some fancy unicode
|
||||
character (be aware of encodings to different clients though) or, commonly, add color
|
||||
tags around it. For further customization, the `.get_display_symbol` method receives
|
||||
the full grid and can return a dynamically determined display symbol. If `None`, the
|
||||
`symbol` is used.
|
||||
tags around it. For further customization, the `.get_display_symbol` can be used.
|
||||
- `default_weight` (int) - Each link direction covered by this link can have its seprate weight,
|
||||
this is used if none is specified in a particular direction. This value must be >= 1,
|
||||
and can be higher than 1 if a link should be less favored.
|
||||
|
|
@ -380,10 +448,9 @@ class MapLink:
|
|||
on the game grid. This is only relevant for the *first* link out of a Node (the continuation
|
||||
of the link is only used to determine its destination). This can be overridden on a
|
||||
per-direction basis.
|
||||
- `requires_grid` (bool): If set, it indicates this component requires the full grid (multiple
|
||||
maps to be available before it can be processed. This is usually only needed for
|
||||
inter-map traversal links where the other map must already be ready. Note that this is
|
||||
*only* relevant for the *first* link out of a node.
|
||||
- `spawn_aliases` (list): A list of [key, alias, alias, ...] for the node to use when spawning
|
||||
exits from this link. If not given, a sane set of defaults (n=north etc) will be used. This
|
||||
is required if you use any custom directions outside of the cardinal directions + up/down.
|
||||
|
||||
"""
|
||||
# symbol for identifying this link on the map
|
||||
|
|
@ -421,15 +488,19 @@ class MapLink:
|
|||
interrupt_path = False
|
||||
# prototype for the first link out of a node.
|
||||
prototype = None
|
||||
# used for spawning, if the exit prototype doesn't contain an explicit key.
|
||||
# if neither that nor this is not given, the central node's direction_aliases will be used.
|
||||
# the first element of this list is the key, the others are the aliases.
|
||||
spawn_aliases = []
|
||||
|
||||
|
||||
def __init__(self, x, y, xymap=None):
|
||||
def __init__(self, x, y, Z, xymap=None):
|
||||
"""
|
||||
Initialize the link.
|
||||
|
||||
Args:
|
||||
x (int): The xygrid x coordinate
|
||||
y (int): The xygrid y coordinate.
|
||||
X (int or str): The name/Z-coord of this map we are on.
|
||||
xymap (XYMap, optional): The map object this sits on.
|
||||
|
||||
"""
|
||||
|
|
@ -440,6 +511,7 @@ class MapLink:
|
|||
|
||||
self.X = x / 2
|
||||
self.Y = y / 2
|
||||
self.Z = Z
|
||||
|
||||
def __str__(self):
|
||||
return f"<LinkNode '{self.symbol}' XY=({self.X:g},{self.Y:g})>"
|
||||
|
|
@ -447,14 +519,13 @@ class MapLink:
|
|||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def traverse(self, start_direction, xygrid, _weight=0, _linklen=1, _steps=None):
|
||||
def traverse(self, start_direction, _weight=0, _linklen=1, _steps=None):
|
||||
"""
|
||||
Recursively traverse the links out of this LinkNode.
|
||||
|
||||
Args:
|
||||
start_direction (str): The direction (n, ne etc) from which
|
||||
this traversal originates for this link.
|
||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||
Kwargs:
|
||||
_weight (int): Internal use.
|
||||
_linklen (int): Internal use.
|
||||
|
|
@ -469,7 +540,9 @@ class MapLink:
|
|||
MapParserError: If a link lead to nowhere.
|
||||
|
||||
"""
|
||||
end_direction = self.get_direction(start_direction, xygrid)
|
||||
xygrid = self.xymap.xygrid
|
||||
|
||||
end_direction = self.get_direction(start_direction)
|
||||
if not end_direction:
|
||||
if _steps is None:
|
||||
# is perfectly okay to not be linking back on the first step (to a node)
|
||||
|
|
@ -486,19 +559,13 @@ class MapLink:
|
|||
next_target = xygrid[end_x][end_y]
|
||||
except KeyError:
|
||||
# check if we have some special action up our sleeve
|
||||
next_target = self.at_empty_target(start_direction, end_direction, xygrid)
|
||||
next_target = self.at_empty_target(start_direction, end_direction)
|
||||
|
||||
if not next_target:
|
||||
raise MapParserError(
|
||||
f"points to empty space in the direction {end_direction}!", self)
|
||||
|
||||
if ((hasattr(next_target, "deferred") and next_target.deferred)
|
||||
or (next_target.xymap.name != self.xymap.name)):
|
||||
# this target is either deferred until grid exists, or sits on another map. Immediately
|
||||
# exit the traversal and set a high weight.
|
||||
return (next_target, BIGVAL, [self])
|
||||
|
||||
_weight += self.get_weight(start_direction, xygrid, _weight)
|
||||
_weight += self.get_weight(start_direction, _weight)
|
||||
if _steps is None:
|
||||
_steps = []
|
||||
_steps.append(self)
|
||||
|
|
@ -515,15 +582,14 @@ class MapLink:
|
|||
# we hit another link. Progress recursively.
|
||||
return next_target.traverse(
|
||||
REVERSE_DIRECTIONS.get(end_direction, end_direction),
|
||||
xygrid, _weight=_weight, _linklen=_linklen + 1, _steps=_steps)
|
||||
_weight=_weight, _linklen=_linklen + 1, _steps=_steps)
|
||||
|
||||
def get_linked_neighbors(self, xygrid, directions=None):
|
||||
def get_linked_neighbors(self, directions=None):
|
||||
"""
|
||||
A helper to get all directions to which there appears to be a
|
||||
visual link/node. This does not trace the length of the link and check weights etc.
|
||||
|
||||
Args:
|
||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||
directions (list, optional): Only scan in these directions.
|
||||
|
||||
Returns:
|
||||
|
|
@ -533,6 +599,7 @@ class MapLink:
|
|||
if not directions:
|
||||
directions = REVERSE_DIRECTIONS.keys()
|
||||
|
||||
xygrid = self.xymap.xygrid
|
||||
links = {}
|
||||
for direction in directions:
|
||||
dx, dy = MAPSCAN[direction]
|
||||
|
|
@ -542,11 +609,11 @@ class MapLink:
|
|||
# a map node or a link connecting in our direction
|
||||
node_or_link = xygrid[end_x][end_y]
|
||||
if (node_or_link.multilink
|
||||
or node_or_link.get_direction(direction, xygrid)):
|
||||
or node_or_link.get_direction(direction)):
|
||||
links[direction] = node_or_link
|
||||
return links
|
||||
|
||||
def at_empty_target(self, start_direction, end_direction, xygrid):
|
||||
def at_empty_target(self, start_direction, end_direction):
|
||||
"""
|
||||
This is called by `.traverse` when it finds this link pointing to nowhere.
|
||||
|
||||
|
|
@ -554,7 +621,6 @@ class MapLink:
|
|||
start_direction (str): The direction (n, ne etc) from which
|
||||
this traversal originates for this link.
|
||||
end_direction (str): The direction found from `get_direction` earlier.
|
||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||
|
||||
Returns:
|
||||
MapNode, MapLink or None: The next target to go to from here. `None` if this
|
||||
|
|
@ -567,14 +633,13 @@ class MapLink:
|
|||
"""
|
||||
return None
|
||||
|
||||
def get_direction(self, start_direction, xygrid, **kwargs):
|
||||
def get_direction(self, start_direction, **kwargs):
|
||||
"""
|
||||
Hook to override for customizing how the directions are
|
||||
determined.
|
||||
|
||||
Args:
|
||||
start_direction (str): The starting direction (n, ne etc).
|
||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||
|
||||
Returns:
|
||||
str: The 'out' direction side of the link - where the link
|
||||
|
|
@ -588,13 +653,12 @@ class MapLink:
|
|||
"""
|
||||
return self.directions.get(start_direction)
|
||||
|
||||
def get_weight(self, start_direction, xygrid, current_weight, **kwargs):
|
||||
def get_weight(self, start_direction, current_weight, **kwargs):
|
||||
"""
|
||||
Hook to override for customizing how the weights are determined.
|
||||
|
||||
Args:
|
||||
start_direction (str): The starting direction (n, ne etc).
|
||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||
current_weight (int): This can have an existing value if
|
||||
we are progressing down a multi-step path.
|
||||
|
||||
|
|
@ -604,15 +668,11 @@ class MapLink:
|
|||
"""
|
||||
return self.weights.get(start_direction, self.default_weight)
|
||||
|
||||
def get_display_symbol(self, xygrid, xymap=None, **kwargs):
|
||||
def get_display_symbol(self):
|
||||
"""
|
||||
Hook to override for customizing how the display_symbol is determined.
|
||||
This is called after all other hooks, at map visualization.
|
||||
|
||||
Args:
|
||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||
xymap (XYMap): The map object this sits on.
|
||||
|
||||
Returns:
|
||||
str: The display-symbol to use. This must visually be a single character
|
||||
but could have color markers, use a unicode font etc.
|
||||
|
|
@ -657,7 +717,7 @@ class SmartRerouterMapLink(MapLink):
|
|||
"""
|
||||
multilink = True
|
||||
|
||||
def get_direction(self, start_direction, xygrid):
|
||||
def get_direction(self, start_direction):
|
||||
"""
|
||||
Dynamically determine the direction based on a source direction and grid topology.
|
||||
|
||||
|
|
@ -665,7 +725,7 @@ class SmartRerouterMapLink(MapLink):
|
|||
# get all visually connected links
|
||||
if not self.directions:
|
||||
directions = {}
|
||||
unhandled_links = list(self.get_linked_neighbors(xygrid).keys())
|
||||
unhandled_links = list(self.get_linked_neighbors().keys())
|
||||
|
||||
# get all straight lines (n-s, sw-ne etc) we can trace through
|
||||
# the dynamic link and remove them from the unhandled_links list
|
||||
|
|
@ -726,7 +786,7 @@ class TeleporterMapLink(MapLink):
|
|||
super().__init__(*args, **kwargs)
|
||||
self.paired_teleporter = None
|
||||
|
||||
def at_empty_target(self, start_direction, end_direction, xygrid):
|
||||
def at_empty_target(self, start_direction, end_direction):
|
||||
"""
|
||||
Called during traversal, when finding an unknown direction out of the link (same as
|
||||
targeting a link at an empty spot on the grid). This will also search for
|
||||
|
|
@ -735,7 +795,6 @@ class TeleporterMapLink(MapLink):
|
|||
Args:
|
||||
start_direction (str): The direction (n, ne etc) from which this traversal originates
|
||||
for this link.
|
||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||
|
||||
Returns:
|
||||
TeleporterMapLink: The paired teleporter.
|
||||
|
|
@ -746,6 +805,7 @@ class TeleporterMapLink(MapLink):
|
|||
'pointing to an empty space' error we'd get if returning `None`.
|
||||
|
||||
"""
|
||||
xygrid = self.xymap.xygrid
|
||||
if not self.paired_teleporter:
|
||||
# scan for another teleporter
|
||||
symbol = self.symbol
|
||||
|
|
@ -769,13 +829,13 @@ class TeleporterMapLink(MapLink):
|
|||
|
||||
return self.paired_teleporter
|
||||
|
||||
def get_direction(self, start_direction, xygrid):
|
||||
def get_direction(self, start_direction):
|
||||
"""
|
||||
Figure out the connected link and paired teleport.
|
||||
|
||||
"""
|
||||
if not self.directions:
|
||||
neighbors = self.get_linked_neighbors(xygrid)
|
||||
neighbors = self.get_linked_neighbors()
|
||||
|
||||
if len(neighbors) != 1:
|
||||
raise MapParserError("must have exactly one link connected to it.", self)
|
||||
|
|
@ -846,7 +906,7 @@ class SmartMapLink(MapLink):
|
|||
"""
|
||||
multilink = True
|
||||
|
||||
def get_direction(self, start_direction, xygrid):
|
||||
def get_direction(self, start_direction):
|
||||
"""
|
||||
Figure out the direction from a specific source direction based on grid topology.
|
||||
|
||||
|
|
@ -854,7 +914,7 @@ class SmartMapLink(MapLink):
|
|||
# get all visually connected links
|
||||
if not self.directions:
|
||||
directions = {}
|
||||
neighbors = self.get_linked_neighbors(xygrid)
|
||||
neighbors = self.get_linked_neighbors()
|
||||
nodes = [direction for direction, neighbor in neighbors.items()
|
||||
if hasattr(neighbor, 'node_index')]
|
||||
|
||||
|
|
@ -914,7 +974,7 @@ class InvisibleSmartMapLink(SmartMapLink):
|
|||
(('ne', 'sw'), ('sw', 'ne')): '/',
|
||||
}
|
||||
|
||||
def get_display_symbol(self, xygrid, xymap=None, **kwargs):
|
||||
def get_display_symbol(self):
|
||||
"""
|
||||
The SmartMapLink already calculated the directions before this, so we
|
||||
just need to figure out what to replace this with in order to make this 'invisible'
|
||||
|
|
@ -924,7 +984,7 @@ class InvisibleSmartMapLink(SmartMapLink):
|
|||
|
||||
"""
|
||||
if not hasattr(self, "_cached_display_symbol"):
|
||||
legend = xymap.legend
|
||||
legend = self.xymap.legend
|
||||
default_symbol = (
|
||||
self.symbol if self.display_symbol is None else self.display_symbol)
|
||||
self._cached_display_symbol = default_symbol
|
||||
|
|
@ -939,8 +999,8 @@ class InvisibleSmartMapLink(SmartMapLink):
|
|||
if node_or_link_class:
|
||||
# initiate class in the current location and run get_display_symbol
|
||||
# to get what it would show.
|
||||
self._cached_display_symbol = node_or_link_class(
|
||||
self.x, self.y).get_display_symbol(xygrid, xymap=xymap, **kwargs)
|
||||
self._cached_display_symbol = (
|
||||
node_or_link_class(self.x, self.y, self.Z).get_display_symbol())
|
||||
return self._cached_display_symbol
|
||||
|
||||
|
||||
|
|
@ -950,15 +1010,15 @@ class InvisibleSmartMapLink(SmartMapLink):
|
|||
class BasicMapNode(MapNode):
|
||||
"""Basic map Node"""
|
||||
symbol = "#"
|
||||
prototype = "xyz_room_prototype"
|
||||
|
||||
|
||||
class MapTransitionMapNode(TransitionMapNode):
|
||||
"""Teleports entering players to other map"""
|
||||
"""Transition-target to other map"""
|
||||
symbol = "T"
|
||||
display_symbol = " "
|
||||
interrupt_path = True
|
||||
linked_map_name = ""
|
||||
linked_map_coords = None
|
||||
target_map_coords = (0, 0, 'unset') # must be changed
|
||||
prototype = None # important!
|
||||
|
||||
|
||||
class InterruptMapNode(MapNode):
|
||||
|
|
@ -966,30 +1026,35 @@ class InterruptMapNode(MapNode):
|
|||
symbol = "I"
|
||||
display_symbol = "#"
|
||||
interrupt_path = True
|
||||
prototype = "xyz_room_prototype"
|
||||
|
||||
|
||||
class NSMapLink(MapLink):
|
||||
"""Two-way, North-South link"""
|
||||
symbol = "|"
|
||||
directions = {"n": "s", "s": "n"}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class EWMapLink(MapLink):
|
||||
"""Two-way, East-West link"""
|
||||
symbol = "-"
|
||||
directions = {"e": "w", "w": "e"}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class NESWMapLink(MapLink):
|
||||
"""Two-way, NorthWest-SouthWest link"""
|
||||
symbol = "/"
|
||||
directions = {"ne": "sw", "sw": "ne"}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class SENWMapLink(MapLink):
|
||||
"""Two-way, SouthEast-NorthWest link"""
|
||||
symbol = "\\"
|
||||
directions = {"se": "nw", "nw": "se"}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class PlusMapLink(MapLink):
|
||||
|
|
@ -997,6 +1062,7 @@ class PlusMapLink(MapLink):
|
|||
symbol = "+"
|
||||
directions = {"s": "n", "n": "s",
|
||||
"e": "w", "w": "e"}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class CrossMapLink(MapLink):
|
||||
|
|
@ -1004,30 +1070,35 @@ class CrossMapLink(MapLink):
|
|||
symbol = "x"
|
||||
directions = {"ne": "sw", "sw": "ne",
|
||||
"se": "nw", "nw": "se"}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class NSOneWayMapLink(MapLink):
|
||||
"""One-way North-South link"""
|
||||
symbol = "v"
|
||||
directions = {"n": "s"}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class SNOneWayMapLink(MapLink):
|
||||
"""One-way South-North link"""
|
||||
symbol = "^"
|
||||
directions = {"s": "n"}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class EWOneWayMapLink(MapLink):
|
||||
"""One-way East-West link"""
|
||||
symbol = "<"
|
||||
directions = {"e": "w"}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class WEOneWayMapLink(MapLink):
|
||||
"""One-way West-East link"""
|
||||
symbol = ">"
|
||||
directions = {"w": "e"}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class UpMapLink(SmartMapLink):
|
||||
|
|
@ -1037,6 +1108,7 @@ class UpMapLink(SmartMapLink):
|
|||
# all movement over this link is 'up', regardless of where on the xygrid we move.
|
||||
direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol,
|
||||
's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class DownMapLink(UpMapLink):
|
||||
|
|
@ -1045,12 +1117,14 @@ class DownMapLink(UpMapLink):
|
|||
# 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,
|
||||
's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class InterruptMapLink(InvisibleSmartMapLink):
|
||||
"""A (still passable) link that causes the pathfinder to stop before crossing."""
|
||||
symbol = "i"
|
||||
interrupt_path = True
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class BlockedMapLink(InvisibleSmartMapLink):
|
||||
|
|
@ -1063,6 +1137,7 @@ class BlockedMapLink(InvisibleSmartMapLink):
|
|||
symbol = 'b'
|
||||
weights = {'n': BIGVAL, 'ne': BIGVAL, 'e': BIGVAL, 'se': BIGVAL,
|
||||
's': BIGVAL, 'sw': BIGVAL, 'w': BIGVAL, 'nw': BIGVAL}
|
||||
prototype = "xyz_exit_prototype"
|
||||
|
||||
|
||||
class RouterMapLink(SmartRerouterMapLink):
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from time import time
|
|||
from random import randint
|
||||
from unittest import TestCase
|
||||
from parameterized import parameterized
|
||||
from django.test import override_settings
|
||||
from . import xymap, xyzgrid, map_legend
|
||||
|
||||
|
||||
|
|
@ -346,7 +347,7 @@ class TestMap1(TestCase):
|
|||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP1}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP1}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -425,13 +426,14 @@ class TestMap1(TestCase):
|
|||
mapstr = self.map.get_visual_range(coord, dist=dist, mode='nodes', character='@')
|
||||
self.assertEqual(expected, mapstr)
|
||||
|
||||
|
||||
class TestMap2(TestCase):
|
||||
"""
|
||||
Test with Map2 - a bigger map with multi-step links
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP2}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP2}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -542,7 +544,7 @@ class TestMap3(TestCase):
|
|||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP3}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP3}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -592,7 +594,7 @@ class TestMap4(TestCase):
|
|||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP4}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP4}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -623,7 +625,7 @@ class TestMap5(TestCase):
|
|||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP5}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP5}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -652,7 +654,7 @@ class TestMap6(TestCase):
|
|||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP6}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP6}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -685,7 +687,7 @@ class TestMap7(TestCase):
|
|||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP7}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP7}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -714,7 +716,7 @@ class TestMap8(TestCase):
|
|||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP8}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP8}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -781,7 +783,7 @@ class TestMap9(TestCase):
|
|||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP9}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP9}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -811,7 +813,7 @@ class TestMap10(TestCase):
|
|||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP10}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP10}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -860,7 +862,7 @@ class TestMap11(TestCase):
|
|||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.map = xymap.XYMap({"map": MAP11}, name="testmap")
|
||||
self.map = xymap.XYMap({"map": MAP11}, Z="testmap")
|
||||
self.map.parse()
|
||||
|
||||
def test_str_output(self):
|
||||
|
|
@ -951,7 +953,7 @@ class TestMapStressTest(TestCase):
|
|||
grid = self._get_grid(Xmax, Ymax)
|
||||
# print(f"\n\n{grid}\n")
|
||||
t0 = time()
|
||||
mapobj = xymap.XYMap({'map': grid}, name="testmap")
|
||||
mapobj = xymap.XYMap({'map': grid}, Z="testmap")
|
||||
mapobj.parse()
|
||||
t1 = time()
|
||||
self.assertLess(t1 - t0, max_time, f"Map creation of ({Xmax}x{Ymax}) grid slower "
|
||||
|
|
@ -968,7 +970,7 @@ class TestMapStressTest(TestCase):
|
|||
"""
|
||||
Xmax, Ymax = gridsize
|
||||
grid = self._get_grid(Xmax, Ymax)
|
||||
mapobj = xymap.XYMap({'map': grid}, name="testmap")
|
||||
mapobj = xymap.XYMap({'map': grid}, Z="testmap")
|
||||
mapobj.parse()
|
||||
|
||||
t0 = time()
|
||||
|
|
@ -1001,7 +1003,7 @@ class TestMapStressTest(TestCase):
|
|||
"""
|
||||
Xmax, Ymax = gridsize
|
||||
grid = self._get_grid(Xmax, Ymax)
|
||||
mapobj = xymap.XYMap({'map': grid}, name="testmap")
|
||||
mapobj = xymap.XYMap({'map': grid}, Z="testmap")
|
||||
mapobj.parse()
|
||||
|
||||
t0 = time()
|
||||
|
|
@ -1026,20 +1028,49 @@ class TestMapStressTest(TestCase):
|
|||
f"slower than expected {max_time}s.")
|
||||
|
||||
|
||||
class TestXYZGrid(TestCase):
|
||||
"""
|
||||
Test base grid class with a single map, including spawning objects.
|
||||
"""
|
||||
|
||||
zcoord = "map1"
|
||||
|
||||
def setUp(self):
|
||||
self.grid, err = xyzgrid.XYZGrid.create("testgrid")
|
||||
|
||||
self.map_data1 = {
|
||||
"map": MAP1,
|
||||
"zcoord": self.zcoord
|
||||
}
|
||||
|
||||
self.grid.add_maps(self.map_data1)
|
||||
|
||||
def tearDown(self):
|
||||
self.grid.delete()
|
||||
|
||||
def test_str_output(self):
|
||||
"""Check the display_map"""
|
||||
xymap = self.grid.get(self.zcoord)
|
||||
stripped_map = "\n".join(line.rstrip() for line in str(xymap).split('\n'))
|
||||
self.assertEqual(MAP1_DISPLAY, stripped_map)
|
||||
|
||||
def test_spawn(self):
|
||||
"""Spawn objects for the grid"""
|
||||
self.grid.spawn()
|
||||
|
||||
|
||||
# map transitions
|
||||
class Map12aTransition(map_legend.MapTransitionMapNode):
|
||||
symbol = "T"
|
||||
linked_map_name = "map12b"
|
||||
linked_map_coords = (1, 0)
|
||||
target_map_coords = (1, 0, "map12b")
|
||||
|
||||
|
||||
class Map12bTransition(map_legend.MapTransitionMapNode):
|
||||
symbol = "T"
|
||||
linked_map_name= "map12a"
|
||||
linked_map_coords = (0, 1)
|
||||
target_map_coords = (0, 1, "map12a")
|
||||
|
||||
|
||||
class TestXYZGrid(TestCase):
|
||||
class TestXYZGridTransition(TestCase):
|
||||
"""
|
||||
Test the XYZGrid class and transitions between maps.
|
||||
|
||||
|
|
@ -1049,12 +1080,12 @@ class TestXYZGrid(TestCase):
|
|||
|
||||
self.map_data12a = {
|
||||
"map": MAP12a,
|
||||
"name": "map12a",
|
||||
"zcoord": "map12a",
|
||||
"legend": {"T": Map12aTransition}
|
||||
}
|
||||
self.map_data12b = {
|
||||
"map": MAP12b,
|
||||
"name": "map12b",
|
||||
"zcoord": "map12b",
|
||||
"legend": {"T": Map12bTransition}
|
||||
|
||||
}
|
||||
|
|
@ -1076,9 +1107,9 @@ class TestXYZGrid(TestCase):
|
|||
directions, _ = self.grid.get('map12a').get_shortest_path(startcoord, endcoord)
|
||||
self.assertEqual(expected_directions, tuple(directions))
|
||||
|
||||
def test_transition(self):
|
||||
def test_spawn(self):
|
||||
"""
|
||||
Test transition.
|
||||
Spawn the two maps into actual objects.
|
||||
|
||||
"""
|
||||
|
||||
self.grid.spawn()
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ as up and down. These are indicated in code as 'n', 'ne', 'e', 'se', 's', 'sw',
|
|||
MAP_DATA = {
|
||||
"map": MAP,
|
||||
"legend": LEGEND,
|
||||
"name": "City of Foo",
|
||||
"zcoord": "City of Foo",
|
||||
"prototypes": {
|
||||
(0,1): { ... },
|
||||
(1,3): { ... },
|
||||
|
|
@ -104,12 +104,17 @@ except ImportError as err:
|
|||
from django.conf import settings
|
||||
from evennia.utils.utils import variable_from_module, mod_import
|
||||
from evennia.utils import logger
|
||||
from evennia.prototypes import prototypes as protlib
|
||||
|
||||
from .utils import MapError, MapParserError, BIGVAL
|
||||
from . import map_legend
|
||||
|
||||
_CACHE_DIR = settings.CACHE_DIR
|
||||
_LOADED_PROTOTYPES = None
|
||||
|
||||
MAP_DATA_KEYS = [
|
||||
"zcoord", "map", "legend", "prototypes"
|
||||
]
|
||||
|
||||
# these are all symbols used for x,y coordinate spots
|
||||
DEFAULT_LEGEND = {
|
||||
|
|
@ -134,6 +139,8 @@ DEFAULT_LEGEND = {
|
|||
't': map_legend.TeleporterMapLink,
|
||||
}
|
||||
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# Map parser implementation
|
||||
|
||||
|
|
@ -184,7 +191,7 @@ class XYMap:
|
|||
# we normally only accept one single character for the legend key
|
||||
legend_key_exceptions = ("\\")
|
||||
|
||||
def __init__(self, map_module_or_dict, name="map", grid=None):
|
||||
def __init__(self, map_module_or_dict, Z="map", xyzgrid=None):
|
||||
"""
|
||||
Initialize the map parser by feeding it the map.
|
||||
|
||||
|
|
@ -192,31 +199,41 @@ class XYMap:
|
|||
map_module_or_dict (str, module or dict): Path or module pointing to a map. If a dict,
|
||||
this should be a dict with a MAP_DATA key 'map' and optionally a 'legend'
|
||||
dicts to specify the map structure.
|
||||
name (str, optional): Unique identifier for this map. Needed if the game uses
|
||||
more than one map. Used when referencing this map during map transitions,
|
||||
baking of pathfinding matrices etc. This will be overridden by any 'name' given
|
||||
in the MAP_DATA itself.
|
||||
grid (.xyzgrid.XYZgrid): A top-level grid this map is a part of.
|
||||
Z (int or str, optional): Name or Z-coord for for this map. Needed if the game uses
|
||||
more than one map. If not given, it can also be embedded in the
|
||||
`map_module_or_dict`. Used when referencing this map during map transitions,
|
||||
baking of pathfinding matrices etc.
|
||||
xyzgrid (.xyzgrid.XYZgrid): A top-level grid this map is a part of.
|
||||
|
||||
Notes:
|
||||
The map deals with two sets of coorinate systems:
|
||||
Interally, the map deals with two sets of coordinate systems:
|
||||
- grid-coordinates x,y are the character positions in the map string.
|
||||
- world-coordinates X,Y are the in-world coordinates of nodes/rooms.
|
||||
There are fewer of these since they ignore the 'link' spaces between
|
||||
the nodes in the grid, so
|
||||
the nodes in the grid, s
|
||||
|
||||
X = x // 2
|
||||
Y = y // 2
|
||||
|
||||
- The Z-coordinate, if given, is only used when transitioning between maps
|
||||
on the supplied `grid`.
|
||||
|
||||
"""
|
||||
self.name = name
|
||||
global _LOADED_PROTOTYPES
|
||||
if not _LOADED_PROTOTYPES:
|
||||
# inject default prototypes, but don't override prototype-keys loaded from
|
||||
# settings, if they exist (that means the user wants to replace the defaults)
|
||||
protlib.load_module_prototypes("evennia.contrib.xyzgrid.prototypes", override=False)
|
||||
_LOADED_PROTOTYPES = True
|
||||
|
||||
self.Z = Z
|
||||
self.xyzgrid = xyzgrid
|
||||
|
||||
self.mapstring = ""
|
||||
|
||||
# store so we can reload
|
||||
self.map_module_or_dict = map_module_or_dict
|
||||
|
||||
self.grid = grid
|
||||
self.prototypes = None
|
||||
|
||||
# transitional mapping
|
||||
|
|
@ -237,10 +254,10 @@ class XYMap:
|
|||
self.pathfinding_routes = None
|
||||
|
||||
self.pathfinder_baked_filename = None
|
||||
if name:
|
||||
if Z:
|
||||
if not isdir(_CACHE_DIR):
|
||||
mkdir(_CACHE_DIR)
|
||||
self.pathfinder_baked_filename = pathjoin(_CACHE_DIR, f"{name}.P")
|
||||
self.pathfinder_baked_filename = pathjoin(_CACHE_DIR, f"{Z}.P")
|
||||
|
||||
# load data and parse it
|
||||
self.reload()
|
||||
|
|
@ -257,6 +274,88 @@ class XYMap:
|
|||
def __repr__(self):
|
||||
return f"<Map {self.max_X + 1}x{self.max_Y + 1}, {len(self.node_index_map)} nodes>"
|
||||
|
||||
def reload(self, map_module_or_dict=None):
|
||||
"""
|
||||
(Re)Load a map.
|
||||
|
||||
Args:
|
||||
map_module_or_dict (str, module or dict, optional): See description for the variable
|
||||
in the class' `__init__` function. If given, replace the already loaded
|
||||
map with a new one. If not given, the existing one given on class creation
|
||||
will be reloaded.
|
||||
parse (bool, optional): If set, auto-run `.parse()` on the newly loaded data.
|
||||
|
||||
Notes:
|
||||
This will both (re)load the data and parse it into a new map structure, replacing any
|
||||
existing one. The valid mapstructure is:
|
||||
::
|
||||
|
||||
{
|
||||
"map": <str>,
|
||||
"zcoord": <int or str>, # optional
|
||||
"legend": <dict>, # optional
|
||||
"prototypes": <dict> # optional
|
||||
}
|
||||
|
||||
"""
|
||||
if not map_module_or_dict:
|
||||
map_module_or_dict = self.map_module_or_dict
|
||||
|
||||
mapdata = {}
|
||||
if isinstance(map_module_or_dict, dict):
|
||||
# map-=structure provided directly
|
||||
mapdata = map_module_or_dict
|
||||
else:
|
||||
# read from contents of module
|
||||
mod = mod_import(map_module_or_dict)
|
||||
mapdata = variable_from_module(mod, "MAP_DATA")
|
||||
if not mapdata:
|
||||
# try to read mapdata directly from global variables
|
||||
mapdata['zcoord'] = variable_from_module(mod, "ZCOORD", default=self.name)
|
||||
mapdata['map'] = variable_from_module(mod, "MAP")
|
||||
mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND)
|
||||
mapdata['prototypes'] = variable_from_module(mod, "PROTOTYPES", default={})
|
||||
|
||||
# validate
|
||||
if any(key for key in mapdata if key not in MAP_DATA_KEYS):
|
||||
raise MapError(f"Mapdata has keys {list(mapdata)}, but only "
|
||||
f"keys {MAP_DATA_KEYS} are allowed.")
|
||||
|
||||
for key in mapdata.get('legend', DEFAULT_LEGEND):
|
||||
if not key or len(key) > 1:
|
||||
if key not in self.legend_key_exceptions:
|
||||
raise MapError(f"Map-legend key '{key}' is invalid: All keys must "
|
||||
"be exactly one character long. Use the node/link's "
|
||||
"`.display_symbol` property to change how it is "
|
||||
"displayed.")
|
||||
if 'map' not in mapdata or not mapdata['map']:
|
||||
raise MapError("No map found. Add 'map' key to map-data (MAP_DATA) dict or "
|
||||
"add variable MAP to a module passed into the parser.")
|
||||
for key, prototype in mapdata.get('prototypes', {}).items():
|
||||
if not is_iter(key) and (2 <= len(key) <= 3):
|
||||
raise MapError(f"Prototype override key {key} is malformed: It must be a "
|
||||
"coordinate (X, Y) for nodes or (X, Y, direction) for links; "
|
||||
"where direction is a supported direction string ('n', 'ne', etc).")
|
||||
|
||||
# store/update result
|
||||
self.Z = mapdata.get('zcoord', self.Z)
|
||||
self.mapstring = mapdata['map']
|
||||
self.prototypes = mapdata.get('prototypes', {})
|
||||
|
||||
# merge the custom legend onto the default legend to allow easily
|
||||
# overriding only parts of it
|
||||
self.legend = {**DEFAULT_LEGEND, **map_module_or_dict.get("legend", DEFAULT_LEGEND)}
|
||||
|
||||
# initialize any prototypes on the legend entities
|
||||
for char, node_or_link_class in self.legend.items():
|
||||
prototype = node_or_link_class.prototype
|
||||
if not prototype or isinstance(prototype, dict):
|
||||
# nothing more to do
|
||||
continue
|
||||
# we need to load the prototype dict onto each for ease of access
|
||||
proto = protlib.search_prototype(prototype, require_single=True)[0]
|
||||
node_or_link_class.prototype = proto
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
Parses the numerical grid from the string. The first pass means parsing out
|
||||
|
|
@ -359,26 +458,33 @@ class XYMap:
|
|||
node_index += 1
|
||||
|
||||
xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[node_index] = \
|
||||
mapnode_or_link_class(node_index=node_index, x=ix, y=iy, xymap=self)
|
||||
mapnode_or_link_class(x=ix, y=iy, Z=self.Z,
|
||||
node_index=node_index, xymap=self)
|
||||
|
||||
else:
|
||||
# we have a link at this xygrid position (this is ok everywhere)
|
||||
xygrid[ix][iy] = mapnode_or_link_class(ix, iy, xymap=self)
|
||||
xygrid[ix][iy] = mapnode_or_link_class(x=ix, y=iy, Z=self.Z, xymap=self)
|
||||
|
||||
# store the symbol mapping for transition lookups
|
||||
symbol_map[char].append(xygrid[ix][iy])
|
||||
|
||||
# second pass - link all nodes of the map except the inter-map traversals.
|
||||
# store before building links
|
||||
self.max_x, self.max_y = max_x, max_y
|
||||
self.max_X, self.max_Y = max_X, max_Y
|
||||
self.xygrid = xygrid
|
||||
self.XYgrid = XYgrid
|
||||
self.node_index_map = node_index_map
|
||||
self.symbol_map = symbol_map
|
||||
|
||||
# build all links except the transitional links
|
||||
# build all links
|
||||
for node in node_index_map.values():
|
||||
node.build_links(xygrid)
|
||||
node.build_links()
|
||||
|
||||
# build display map
|
||||
display_map = [[" "] * (max_x + 1) for _ in range(max_y + 1)]
|
||||
for ix, ydct in xygrid.items():
|
||||
for iy, node_or_link in ydct.items():
|
||||
display_map[iy][ix] = node_or_link.get_display_symbol(xygrid, xymap=self)
|
||||
display_map[iy][ix] = node_or_link.get_display_symbol()
|
||||
|
||||
# validate and make sure all nodes/links have prototypes
|
||||
for node in node_index_map.values():
|
||||
|
|
@ -389,16 +495,7 @@ class XYMap:
|
|||
for direction, maplink in node.first_links.items():
|
||||
maplink.prototype = self.prototypes.get(node_coord + (direction,), maplink.prototype)
|
||||
|
||||
# store results
|
||||
self.max_x, self.max_y = max_x, max_y
|
||||
self.xygrid = xygrid
|
||||
|
||||
self.max_X, self.max_Y = max_X, max_Y
|
||||
self.XYgrid = XYgrid
|
||||
|
||||
self.node_index_map = node_index_map
|
||||
self.symbol_map = symbol_map
|
||||
|
||||
# store
|
||||
self.display_map = display_map
|
||||
|
||||
def _get_topology_around_coord(self, coord, dist=2):
|
||||
|
|
@ -494,60 +591,59 @@ class XYMap:
|
|||
pickle.dump((self.mapstring, self.dist_matrix, self.pathfinding_routes),
|
||||
fil, protocol=4)
|
||||
|
||||
def reload(self, map_module_or_dict=None):
|
||||
def spawn_nodes(self, coord=(None, None)):
|
||||
"""
|
||||
(Re)Load a map.
|
||||
Convert the nodes of this XYMap into actual in-world rooms by spawning their
|
||||
related prototypes in the correct coordinate positions. This must be done *first*
|
||||
before spawning links (with `spawn_links` because exits require the target destination
|
||||
to exist. It's also possible to only spawn a subset of the map
|
||||
|
||||
Args:
|
||||
map_module_or_dict (str, module or dict, optional): See description for the variable
|
||||
in the class' `__init__` function. If given, replace the already loaded
|
||||
map with a new one. If not given, the existing one given on class creation
|
||||
will be reloaded.
|
||||
parse (bool, optional): If set, auto-run `.parse()` on the newly loaded data.
|
||||
coord (tuple, optional): An (X,Y) coordinate of node(s). `None` acts as a wildcard.
|
||||
|
||||
Notes:
|
||||
This will both (re)load the data and parse it into a new map structure, replacing any
|
||||
existing one.
|
||||
Examples:
|
||||
- `coord=(1, 3) - spawn (1,3) coordinate only.
|
||||
- `coord=(None, 1) - spawn all nodes in the first row of the map only.
|
||||
- `coord=(None, None)` - spawn all nodes
|
||||
|
||||
Returns:
|
||||
list: A list of nodes that were spawned.
|
||||
|
||||
"""
|
||||
if not map_module_or_dict:
|
||||
map_module_or_dict = self.map_module_or_dict
|
||||
x, y = coord
|
||||
|
||||
mapdata = {}
|
||||
if isinstance(map_module_or_dict, dict):
|
||||
mapdata = map_module_or_dict
|
||||
else:
|
||||
mod = mod_import(map_module_or_dict)
|
||||
mapdata = variable_from_module(mod, "MAP_DATA")
|
||||
if not mapdata:
|
||||
# try to read mapdata directly from global variables
|
||||
mapdata['name'] = variable_from_module(mod, "NAME", default=self.name)
|
||||
mapdata['map'] = variable_from_module(mod, "MAP")
|
||||
mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND)
|
||||
mapdata['rooms'] = variable_from_module(mod, "ROOMS")
|
||||
mapdata['prototypes'] = variable_from_module(mod, "PROTOTYPES", default={})
|
||||
spawned = []
|
||||
for node in self.node_index_map.values():
|
||||
if (x is None or x == node.X) and (y is None or y == node.Y):
|
||||
node.spawn()
|
||||
spawned.append(node)
|
||||
return spawned
|
||||
|
||||
# validate
|
||||
for key in mapdata.get('legend', DEFAULT_LEGEND):
|
||||
if not key or len(key) > 1:
|
||||
if key not in self.legend_key_exceptions:
|
||||
raise MapError(f"Map-legend key '{key}' is invalid: All keys must "
|
||||
"be exactly one character long. Use the node/link's "
|
||||
"`.display_symbol` property to change how it is "
|
||||
"displayed.")
|
||||
if 'map' not in mapdata or not mapdata['map']:
|
||||
raise MapError("No map found. Add 'map' key to map-data (MAP_DATA) dict or "
|
||||
"add variable MAP to a module passed into the parser.")
|
||||
def spawn_links(self, coord=(None, None), nodes=None, only_directions=None):
|
||||
"""
|
||||
Convert links of this XYMap into actual in-game exits by spawning their related
|
||||
prototypes. It's possible to only spawn a specic exit by specifying the node and
|
||||
|
||||
self.room_prototypes = mapdata.get('rooms')
|
||||
Args:
|
||||
coord (tuple, optional): An (X,Y) coordinate of node(s). `None` acts as a wildcard.
|
||||
nodes (list, optional): If given, only consider links out of these nodes. This also
|
||||
affects `coords`, so that if there are no nodes of given coords in `nodes`, no
|
||||
links will be spawned at all.
|
||||
directions (list, optional): A list of cardinal directions ('n', 'ne' etc). If given,
|
||||
sync only the exit in the given directions (`coords` limits which links out of which
|
||||
nodes should be considered). `None` acts as a wildcard.
|
||||
Examples:
|
||||
- `coord=(1, 3 )`, `direction='ne'` - sync only the north-eastern exit
|
||||
out of the (1, 3) node.
|
||||
|
||||
# store/update result
|
||||
self.name = mapdata.get('name', self.name)
|
||||
self.mapstring = mapdata['map']
|
||||
self.prototypes = mapdata.get('prototypes', {})
|
||||
# merge the custom legend onto the default legend to allow easily
|
||||
# overriding only parts of it
|
||||
self.legend = {**DEFAULT_LEGEND, **map_module_or_dict.get("legend", DEFAULT_LEGEND)}
|
||||
"""
|
||||
x, y = coord
|
||||
if not nodes:
|
||||
nodes = self.node_index_map.values()
|
||||
|
||||
for node in nodes:
|
||||
if (x is None or x == node.X) and (y is None or y == node.Y):
|
||||
node.spawn_links(only_directions=only_directions)
|
||||
|
||||
def get_node_from_coord(self, coords):
|
||||
"""
|
||||
|
|
@ -775,7 +871,7 @@ class XYMap:
|
|||
|
||||
def _default_callable(node):
|
||||
return target_path_style.format(
|
||||
display_symbol=node.get_display_symbol(self.xygrid))
|
||||
display_symbol=node.get_display_symbol())
|
||||
|
||||
if callable(target_path_style):
|
||||
_target_path_style = target_path_style
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import itertools
|
|||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.utils import logger
|
||||
from .xymap import XYMap
|
||||
from .xyzroom import XYZRoom, XYZExit
|
||||
|
||||
|
||||
class XYZGrid(DefaultScript):
|
||||
|
|
@ -54,12 +55,13 @@ class XYZGrid(DefaultScript):
|
|||
nmaps = 0
|
||||
# generate all Maps - this will also initialize their components
|
||||
# and bake any pathfinding paths (or load from disk-cache)
|
||||
for mapname, mapdata in self.db.map_data.items():
|
||||
logger.log_info(f"[grid] Loading map '{mapname}'...")
|
||||
xymap = XYMap(dict(mapdata), name=mapname, grid=self)
|
||||
for zcoord, mapdata in self.db.map_data.items():
|
||||
|
||||
logger.log_info(f"[grid] Loading map '{zcoord}'...")
|
||||
xymap = XYMap(dict(mapdata), Z=zcoord, xyzgrid=self)
|
||||
xymap.parse()
|
||||
xymap.calculate_path_matrix()
|
||||
self.ndb.grid[mapname] = xymap
|
||||
self.ndb.grid[zcoord] = xymap
|
||||
nmaps += 1
|
||||
|
||||
# store
|
||||
|
|
@ -82,43 +84,44 @@ class XYZGrid(DefaultScript):
|
|||
`{"map": <mapstr>, "legend": <legenddict>, "name": <name>,
|
||||
"prototypes": <dict-of-dicts>}`. The `prototypes are
|
||||
coordinate-specific overrides for nodes/links on the map, keyed with their
|
||||
(X,Y) coordinate.
|
||||
(X,Y) coordinate within that map.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If mapdata is malformed.
|
||||
|
||||
Notes:
|
||||
This will assume that all added maps produce a complete set (that is, they are correctly
|
||||
and completely linked together with each other and/or with existing maps). So
|
||||
this will automatically trigger `.reload()` to rebuild the grid.
|
||||
After this, you need to run `.sync_to_grid` to make the new map actually
|
||||
available in-game.
|
||||
|
||||
"""
|
||||
for mapdata in mapdatas:
|
||||
name = mapdata.get('name')
|
||||
if not name:
|
||||
raise RuntimeError("XYZGrid.add_map data must contain 'name'.")
|
||||
zcoord = mapdata.get('zcoord')
|
||||
if not zcoord:
|
||||
raise RuntimeError("XYZGrid.add_map data must contain 'zcoord'.")
|
||||
|
||||
self.db.map_data[name] = mapdata
|
||||
self.db.map_data[zcoord] = mapdata
|
||||
|
||||
def remove_map(self, mapname, remove_objects=False):
|
||||
def remove_map(self, *zcoords, remove_objects=True):
|
||||
"""
|
||||
Remove a map from the grid.
|
||||
Remove an XYmap from the grid.
|
||||
|
||||
Args:
|
||||
mapname (str): The map to remove.
|
||||
*zoords (str): The zcoords/XYmaps to remove.
|
||||
remove_objects (bool, optional): If the synced database objects (rooms/exits) should
|
||||
be removed alongside this map.
|
||||
"""
|
||||
if mapname in self.db.map_data:
|
||||
self.db.map_data.pop(zcoord)
|
||||
self.reload()
|
||||
for zcoord in zcoords:
|
||||
if zcoord in self.db.map_data:
|
||||
self.db.map_data.pop(zcoord)
|
||||
if remove_objects:
|
||||
# this should also remove all exits automatically
|
||||
XYZRoom.objects.filter_xyz(coord=(None, None, zcoord)).delete()
|
||||
self.reload()
|
||||
|
||||
if remove_objects:
|
||||
pass
|
||||
def delete(self):
|
||||
"""
|
||||
Clear the entire grid, including database entities.
|
||||
|
||||
def sync_to_grid(self, coord=(None, None, None), direction=None):
|
||||
"""
|
||||
self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True)
|
||||
|
||||
def spawn(self, coord=(None, None, None), only_directions=None):
|
||||
"""
|
||||
Create/recreate/update the in-game grid based on the stored Maps or for a specific Map
|
||||
or coordinate.
|
||||
|
|
@ -126,8 +129,8 @@ class XYZGrid(DefaultScript):
|
|||
Args:
|
||||
coord (tuple, optional): An (X,Y,Z) coordinate, where Z is the name of the map. `None`
|
||||
acts as a wildcard.
|
||||
direction (str, optional): A cardinal direction ('n', 'ne' etc). If given, sync only the
|
||||
exit in the given direction. `None` acts as a wildcard.
|
||||
only_directions (list, optional): A list of cardinal directions ('n', 'ne' etc).
|
||||
If given, spawn exits only the given direction. `None` acts as a wildcard.
|
||||
|
||||
Examples:
|
||||
- `coord=(1, 3, 'foo')` - sync a specific element of map 'foo' only.
|
||||
|
|
@ -147,14 +150,12 @@ class XYZGrid(DefaultScript):
|
|||
else:
|
||||
raise RuntimeError(f"The 'z' coordinate/name '{z}' is not found on the grid.")
|
||||
|
||||
# first syncing pass (only nodes/rooms)
|
||||
synced = []
|
||||
for xymap in xymaps:
|
||||
for node in xymap.node_index_map.values():
|
||||
if (x is None or x == node.X) and (y is None or y == node.Y):
|
||||
node.sync_node_to_grid()
|
||||
synced.append(node)
|
||||
# second pass (links/exits)
|
||||
for node in synced:
|
||||
node.sync_links_to_grid()
|
||||
# first build all nodes/rooms
|
||||
for zcoord, xymap in xymaps.items():
|
||||
logger.log_info(f"[grid] spawning/updating nodes for {zcoord} ...")
|
||||
xymap.spawn_nodes(coord=(x, y))
|
||||
|
||||
# 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(coord=(x, y), only_directions=only_directions)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ used as stand-alone XYZ-coordinate-aware rooms.
|
|||
from django.db.models import Q
|
||||
from evennia.objects.objects import DefaultRoom, DefaultExit
|
||||
from evennia.objects.manager import ObjectManager
|
||||
from evennia.utils.utils import inherits_from
|
||||
|
||||
# name of all tag categories. Note that the Z-coordinate is
|
||||
# the `map_name` of the XYZgrid
|
||||
|
|
@ -185,6 +186,22 @@ class XYZRoom(DefaultRoom):
|
|||
# makes the `room.objects.filter_xymap` available
|
||||
objects = XYZManager()
|
||||
|
||||
def __str__(self):
|
||||
return repr(self)
|
||||
|
||||
def __repr__(self):
|
||||
x, y, z = self.xyzcoords
|
||||
return f"<XYZRoom '{self.db_key}', XYZ=({x},{y},{z})>"
|
||||
|
||||
@property
|
||||
def xyzcoords(self):
|
||||
if not hasattr(self, "_xyzcoords"):
|
||||
x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False)
|
||||
y = self.tags.get(category=MAP_Y_TAG_CATEGORY, return_list=False)
|
||||
z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False)
|
||||
self._xyzcoords = (x, y, z)
|
||||
return self._xyzcoords
|
||||
|
||||
@classmethod
|
||||
def create(cls, key, account=None, coord=(0, 0, 'map'), **kwargs):
|
||||
"""
|
||||
|
|
@ -215,7 +232,7 @@ class XYZRoom(DefaultRoom):
|
|||
return None, [f"XYRroom.create got `coord={coord}` - needs a valid (X,Y,Z) "
|
||||
"coordinate of ints/strings."]
|
||||
|
||||
existing_query = cls.objects.filter_xy(x=x, y=y, z=z)
|
||||
existing_query = cls.objects.filter_xyz(coord=(x, y, z))
|
||||
if existing_query.exists():
|
||||
existing_room = existing_query.first()
|
||||
return None, [f"XYRoom XYZ={coord} already exists "
|
||||
|
|
@ -227,7 +244,7 @@ class XYZRoom(DefaultRoom):
|
|||
(str(z), MAP_Z_TAG_CATEGORY),
|
||||
)
|
||||
|
||||
return DefaultRoom.create(key, account=account, tags=tags, **kwargs)
|
||||
return DefaultRoom.create(key, account=account, tags=tags, typeclass=cls, **kwargs)
|
||||
|
||||
|
||||
class XYZExit(DefaultExit):
|
||||
|
|
@ -238,6 +255,32 @@ class XYZExit(DefaultExit):
|
|||
|
||||
objects = XYZExitManager()
|
||||
|
||||
def __str__(self):
|
||||
return repr(self)
|
||||
|
||||
def __repr__(self):
|
||||
x, y, z = self.xyzcoords
|
||||
xd, yd, zd = self.xyzdestcoords
|
||||
return f"<XYZExit '{self.db_key}', XYZ=({x},{y},{z})->({xd},{yd},{zd})>"
|
||||
|
||||
@property
|
||||
def xyzcoords(self):
|
||||
if not hasattr(self, "_xyzcoords"):
|
||||
x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False)
|
||||
y = self.tags.get(category=MAP_Y_TAG_CATEGORY, return_list=False)
|
||||
z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False)
|
||||
self._xyzcoords = (x, y, z)
|
||||
return self._xyzcoords
|
||||
|
||||
@property
|
||||
def xyzdestcoords(self):
|
||||
if not hasattr(self, "_xyzdestcoords"):
|
||||
xd = self.tags.get(category=MAP_XDEST_TAG_CATEGORY, return_list=False)
|
||||
yd = self.tags.get(category=MAP_YDEST_TAG_CATEGORY, return_list=False)
|
||||
zd = self.tags.get(category=MAP_ZDEST_TAG_CATEGORY, return_list=False)
|
||||
self._xyzdestcoords = (xd, yd, zd)
|
||||
return self._xyzdestcoords
|
||||
|
||||
@classmethod
|
||||
def create(cls, key, account=None, coord=(0, 0, 'map'), destination_coord=(0, 0, 'map'),
|
||||
location=None, destination=None, **kwargs):
|
||||
|
|
@ -246,8 +289,7 @@ class XYZExit(DefaultExit):
|
|||
|
||||
Args:
|
||||
key (str): New name of object to create.
|
||||
account (Account, optional): Any Account to tie to this entity (usually not used for
|
||||
rooms).
|
||||
account (Account, optional): Any Account to tie to this entity (unused for exits).
|
||||
coords (tuple or None, optional): A 3D coordinate (X, Y, Z) for this room's location
|
||||
on a map grid. Each element can theoretically be either `int` or `str`, but for the
|
||||
XYZgrid contrib, the X, Y are always integers while the `Z` coordinate is used for
|
||||
|
|
@ -255,15 +297,17 @@ class XYZExit(DefaultExit):
|
|||
`location`. destination_coord (tuple or None, optional): Works as `coords`, but for
|
||||
destination of
|
||||
the exit. Set to `None` if using the `destination` kwarg to point to room directly.
|
||||
destination_coord (tuple, optional): The XYZ coordinate of the place the exit
|
||||
leads to. Will be ignored if `destination` is given directly.
|
||||
location (Object, optional): Only used if `coord` is not given. This can be used
|
||||
to place this exit in any room, including non-XYRoom type rooms.
|
||||
destination (Object, optional): Only used if `destination_coord` is not given. This can
|
||||
destination (Object, optional): If given, overrides `destination_coord`. This can
|
||||
be any room (including non-XYRooms) and is not checked for XY coordinates.
|
||||
**kwargs: Will be passed into the normal `DefaultRoom.create` method.
|
||||
|
||||
Returns:
|
||||
room (Object): A newly created Room of the given typeclass.
|
||||
errors (list): A list of errors in string form, if any.
|
||||
tuple: A tuple `(exit, errors)`, where the errors is a list containing all found
|
||||
errors (in which case the returned exit will be `None`).
|
||||
|
||||
"""
|
||||
tags = []
|
||||
|
|
@ -274,22 +318,28 @@ class XYZExit(DefaultExit):
|
|||
return None, ["XYExit.create need either a `coord` or a `location`."]
|
||||
source = location
|
||||
else:
|
||||
source = cls.objects.get_xyz(x=x, y=y, z=z)
|
||||
print("rooms:", XYZRoom.objects.all().count(), XYZRoom.objects.all())
|
||||
print("exits:", XYZExit.objects.all().count(), XYZExit.objects.all())
|
||||
source = XYZRoom.objects.get_xyz(coord=(x, y, z))
|
||||
tags.extend(((str(x), MAP_X_TAG_CATEGORY),
|
||||
(str(y), MAP_Y_TAG_CATEGORY),
|
||||
(str(z), MAP_Z_TAG_CATEGORY)))
|
||||
try:
|
||||
xdest, ydest, zdest = destination_coord
|
||||
except ValueError:
|
||||
if not destination:
|
||||
return None, ["XYExit.create need either a `destination_coord` or a `destination`."]
|
||||
if destination:
|
||||
dest = destination
|
||||
else:
|
||||
dest = cls.objects.get_xyz(x=xdest, y=ydest, z=zdest)
|
||||
tags.extend(((str(xdest), MAP_XDEST_TAG_CATEGORY),
|
||||
(str(ydest), MAP_YDEST_TAG_CATEGORY),
|
||||
(str(zdest), MAP_ZDEST_TAG_CATEGORY)))
|
||||
try:
|
||||
xdest, ydest, zdest = destination_coord
|
||||
except ValueError:
|
||||
if not destination:
|
||||
return None, ["XYExit.create need either a `destination_coord` or "
|
||||
"a `destination`."]
|
||||
dest = destination
|
||||
else:
|
||||
dest = XYZRoom.objects.get_xyz(coord=(xdest, ydest, zdest))
|
||||
tags.extend(((str(xdest), MAP_XDEST_TAG_CATEGORY),
|
||||
(str(ydest), MAP_YDEST_TAG_CATEGORY),
|
||||
(str(zdest), MAP_ZDEST_TAG_CATEGORY)))
|
||||
|
||||
return DefaultExit.create(
|
||||
key, source, dest, account=account,
|
||||
location=location, destination=destination, tags=tags, **kwargs)
|
||||
location=location, tags=tags, typeclass=cls, **kwargs)
|
||||
|
|
@ -816,6 +816,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
use_destination=True,
|
||||
to_none=False,
|
||||
move_hooks=True,
|
||||
alternative_source=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
|
|
@ -837,6 +838,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
move_hooks (bool): If False, turn off the calling of move-related hooks
|
||||
(at_before/after_move etc) with quiet=True, this is as quiet a move
|
||||
as can be done.
|
||||
alternative_source (Object, optional): Normally, the current `self.location` is
|
||||
assumed the 'source' of the move. This allows for replacing this
|
||||
with a custom source (for example to create a teleporter room that
|
||||
retains the original source when moving to another place).
|
||||
|
||||
Keyword Args:
|
||||
Passed on to announce_move_to and announce_move_from hooks.
|
||||
|
|
@ -861,7 +866,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
7. `self.at_after_move(source_location)`
|
||||
|
||||
"""
|
||||
|
||||
def logerr(string="", err=None):
|
||||
"""Simple log helper method"""
|
||||
logger.log_trace()
|
||||
|
|
@ -872,6 +876,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
if not emit_to_obj:
|
||||
emit_to_obj = self
|
||||
|
||||
source_location = alternative_source or self.location
|
||||
|
||||
if not destination:
|
||||
if to_none:
|
||||
# immediately move to None. There can be no hooks called since
|
||||
|
|
@ -887,15 +893,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
# Before the move, call eventual pre-commands.
|
||||
if move_hooks:
|
||||
try:
|
||||
if not self.at_before_move(destination, **kwargs):
|
||||
if not source_location.at_before_move(destination, **kwargs):
|
||||
return False
|
||||
except Exception as err:
|
||||
logerr(errtxt.format(err="at_before_move()"), err)
|
||||
return False
|
||||
|
||||
# Save the old location
|
||||
source_location = self.location
|
||||
|
||||
# Call hook on source location
|
||||
if move_hooks and source_location:
|
||||
try:
|
||||
|
|
@ -2446,7 +2449,7 @@ class DefaultRoom(DefaultObject):
|
|||
if not locks and account:
|
||||
locks = cls.lockstring.format(**{"id": account.id})
|
||||
elif not locks and not account:
|
||||
locks = cls.lockstring(**{"id": obj.id})
|
||||
locks = cls.lockstring.format(**{"id": obj.id})
|
||||
|
||||
obj.locks.add(locks)
|
||||
|
||||
|
|
@ -2461,6 +2464,7 @@ class DefaultRoom(DefaultObject):
|
|||
obj.db.desc = description if description else _("This is a room.")
|
||||
|
||||
except Exception as e:
|
||||
raise
|
||||
errors.append("An error occurred while creating this '%s' object." % key)
|
||||
logger.log_err(e)
|
||||
|
||||
|
|
@ -2667,7 +2671,7 @@ class DefaultExit(DefaultObject):
|
|||
obj.db.desc = description if description else _("This is an exit.")
|
||||
|
||||
except Exception as e:
|
||||
errors.append("An error occurred while creating this '%s' object." % key)
|
||||
errors.append("An error occurred while creating this '%s' object (%s)." % key)
|
||||
logger.log_err(e)
|
||||
|
||||
return obj, errors
|
||||
|
|
|
|||
|
|
@ -144,19 +144,45 @@ def homogenize_prototype(prototype, custom_keys=None):
|
|||
return homogenized
|
||||
|
||||
|
||||
# module-based prototypes
|
||||
# module/dict-based prototypes
|
||||
|
||||
def load_module_prototypes():
|
||||
def load_module_prototypes(*mod_or_prototypes, override=True):
|
||||
"""
|
||||
This is called by `evennia.__init__` as Evennia initializes. It's important
|
||||
to do this late so as to not interfere with evennia initialization.
|
||||
Load module prototypes. Also prototype-dicts passed directly to this function are considered
|
||||
'module' prototypes (they are impossible to change) but will have a module of None.
|
||||
|
||||
Args:
|
||||
*mod_or_prototypes (module or dict): Each arg should be a separate module or
|
||||
prototype-dict to load. If none are given, `settings.PROTOTYPE_MODULES` will be used.
|
||||
override (bool, optional): If prototypes should override existing ones already loaded.
|
||||
Disabling this can allow for injecting prototypes into the system dynamically while
|
||||
still allowing same prototype-keys to be overridden from settings (even though settings
|
||||
is usually loaded before dynamic loading).
|
||||
|
||||
Note:
|
||||
This is called (without arguments) by `evennia.__init__` as Evennia initializes. It's
|
||||
important to do this late so as to not interfere with evennia initialization. But it can
|
||||
also be used later to add more prototypes to the library on the fly. This is requried
|
||||
before a module-based prototype can be accessed by prototype-key.
|
||||
|
||||
"""
|
||||
for mod in settings.PROTOTYPE_MODULES:
|
||||
# to remove a default prototype, override it with an empty dict.
|
||||
# internally we store as (key, desc, locks, tags, prototype_dict)
|
||||
global _MODULE_PROTOTYPE_MODULES, _MODULE_PROTOTYPES
|
||||
|
||||
def _prototypes_from_module(mod):
|
||||
"""
|
||||
Load prototypes from a module, first by looking for a global list PROTOTYPE_LIST (a list of
|
||||
dict-prototypes), and if not found, assuming all global-level dicts in the module are
|
||||
prototypes.
|
||||
|
||||
Args:
|
||||
mod (module): The module to load from.evennia
|
||||
|
||||
Returns:
|
||||
list: A list of tuples `(prototype_key, prototype-dict)` where the prototype
|
||||
has been homogenized.
|
||||
|
||||
"""
|
||||
prots = []
|
||||
|
||||
prototype_list = variable_from_module(mod, "PROTOTYPE_LIST")
|
||||
if prototype_list:
|
||||
# found mod.PROTOTYPE_LIST - this should be a list of valid
|
||||
|
|
@ -179,27 +205,74 @@ def load_module_prototypes():
|
|||
if "prototype_key" not in prot:
|
||||
prot["prototype_key"] = variable_name.lower()
|
||||
prots.append((prot["prototype_key"], homogenize_prototype(prot)))
|
||||
return prots
|
||||
|
||||
# assign module path to each prototype_key for easy reference
|
||||
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots})
|
||||
# make sure the prototype contains all meta info
|
||||
def _cleanup_prototype(prototype_key, prototype, mod=None):
|
||||
"""
|
||||
We need to handle externally determined prototype-keys and to make sure
|
||||
the prototype contains all needed meta information.
|
||||
|
||||
Args:
|
||||
prototype_key (str): The determined name of the prototype.
|
||||
prototype (dict): The prototype itself.
|
||||
mod (module, optional): The module the prototype was loaded from, if any.
|
||||
|
||||
Returns:
|
||||
dict: The cleaned up prototype.
|
||||
|
||||
"""
|
||||
actual_prot_key = prototype.get("prototype_key", prototype_key).lower()
|
||||
prototype.update(
|
||||
{
|
||||
"prototype_key": actual_prot_key,
|
||||
"prototype_desc": (
|
||||
prototype["prototype_desc"] if "prototype_desc" in prototype else (mod or "N/A")),
|
||||
"prototype_locks": (
|
||||
prototype["prototype_locks"]
|
||||
if "prototype_locks" in prototype
|
||||
else "use:all();edit:false()"
|
||||
),
|
||||
"prototype_tags": list(
|
||||
set(list(make_iter(prototype.get("prototype_tags", []))) + ["module"])
|
||||
),
|
||||
}
|
||||
)
|
||||
return prototype
|
||||
|
||||
if not mod_or_prototypes:
|
||||
# in principle this means PROTOTYPE_MODULES could also contain prototypes, but that is
|
||||
# rarely useful ...
|
||||
mod_or_prototypes = settings.PROTOTYPE_MODULES
|
||||
|
||||
for mod_or_dict in mod_or_prototypes:
|
||||
|
||||
if isinstance(mod_or_dict, dict):
|
||||
# a single prototype; we must make sure it has its key
|
||||
prototype_key = mod_or_dict.get('prototype_key')
|
||||
if not prototype_key:
|
||||
raise ValidationError(f"The prototype {mod_or_prototype} does not contain a 'prototype_key'")
|
||||
prots = [(prototype_key, mod_or_dict)]
|
||||
mod = None
|
||||
else:
|
||||
# a module (or path to module). This can contain many prototypes; they can be keyed by
|
||||
# variable-name too
|
||||
prots = _prototypes_from_module(mod_or_dict)
|
||||
mod = repr(mod_or_dict)
|
||||
|
||||
# store all found prototypes
|
||||
for prototype_key, prot in prots:
|
||||
actual_prot_key = prot.get("prototype_key", prototype_key).lower()
|
||||
prot.update(
|
||||
{
|
||||
"prototype_key": actual_prot_key,
|
||||
"prototype_desc": prot["prototype_desc"] if "prototype_desc" in prot else mod,
|
||||
"prototype_locks": (
|
||||
prot["prototype_locks"]
|
||||
if "prototype_locks" in prot
|
||||
else "use:all();edit:false()"
|
||||
),
|
||||
"prototype_tags": list(
|
||||
set(list(make_iter(prot.get("prototype_tags", []))) + ["module"])
|
||||
),
|
||||
}
|
||||
)
|
||||
_MODULE_PROTOTYPES[actual_prot_key] = prot
|
||||
prototype = _cleanup_prototype(prototype_key, prot, mod=mod)
|
||||
# the key can change since in-proto key is given prio over variable-name-based keys
|
||||
actual_prototype_key = prototype['prototype_key']
|
||||
|
||||
if actual_prototype_key in _MODULE_PROTOTYPES and not override:
|
||||
# don't override - useful to still let settings replace dynamic inserts
|
||||
continue
|
||||
|
||||
# make sure the prototype contains all meta info
|
||||
_MODULE_PROTOTYPES[actual_prototype_key] = prototype
|
||||
# track module path for display purposes
|
||||
_MODULE_PROTOTYPE_MODULES[actual_prototype_key.lower()] = mod
|
||||
|
||||
|
||||
# Db-based prototypes
|
||||
|
|
@ -266,11 +339,12 @@ def save_prototype(prototype):
|
|||
|
||||
# we can't edit a prototype defined in a module
|
||||
if prototype_key in _MODULE_PROTOTYPES:
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
|
||||
raise PermissionError(
|
||||
_("{protkey} is a read-only prototype " "(defined as code in {module}).").format(
|
||||
protkey=prototype_key, module=mod)
|
||||
)
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key)
|
||||
if mod:
|
||||
err = _("{protkey} is a read-only prototype (defined as code in {module}).")
|
||||
else:
|
||||
err = _("{protkey} is a read-only prototype (passed directly as a dict).")
|
||||
raise PermissionError(err.format(protkey=prototype_key, module=mod))
|
||||
|
||||
# make sure meta properties are included with defaults
|
||||
in_prototype["prototype_desc"] = in_prototype.get(
|
||||
|
|
@ -334,11 +408,12 @@ def delete_prototype(prototype_key, caller=None):
|
|||
|
||||
"""
|
||||
if prototype_key in _MODULE_PROTOTYPES:
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key.lower(), "N/A")
|
||||
raise PermissionError(
|
||||
_("{protkey} is a read-only prototype " "(defined as code in {module}).").format(
|
||||
protkey=prototype_key, module=mod)
|
||||
)
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key)
|
||||
if mod:
|
||||
err = _("{protkey} is a read-only prototype (defined as code in {module}).")
|
||||
else:
|
||||
err = _("{protkey} is a read-only prototype (passed directly as a dict).")
|
||||
raise PermissionError(err.format(protkey=prototype_key, module=mod))
|
||||
|
||||
stored_prototype = DbPrototype.objects.filter(db_key__iexact=prototype_key)
|
||||
|
||||
|
|
@ -452,7 +527,7 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators
|
|||
ndbprots = db_matches.count()
|
||||
if nmodules + ndbprots != 1:
|
||||
raise KeyError(_(
|
||||
"Found {num} matching prototypes {module_prototypes}.").format(
|
||||
"Found {num} matching prototypes among {module_prototypes}.").format(
|
||||
num=nmodules + ndbprots,
|
||||
module_prototypes=module_prototypes)
|
||||
)
|
||||
|
|
@ -906,10 +981,12 @@ def check_permission(prototype_key, action, default=True):
|
|||
"""
|
||||
if action == "edit":
|
||||
if prototype_key in _MODULE_PROTOTYPES:
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
|
||||
logger.log_err(
|
||||
"{} is a read-only prototype " "(defined as code in {}).".format(prototype_key, mod)
|
||||
)
|
||||
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key)
|
||||
if mod:
|
||||
err = _("{protkey} is a read-only prototype (defined as code in {module}).")
|
||||
else:
|
||||
err = _("{protkey} is a read-only prototype (passed directly as a dict).")
|
||||
logger.log_err(err.format(protkey=prototype_key, module=mod))
|
||||
return False
|
||||
|
||||
prototype = search_prototype(key=prototype_key)
|
||||
|
|
|
|||
|
|
@ -797,7 +797,7 @@ class TypeclassManager(TypedObjectManager):
|
|||
all_subclasses.extend(self._get_subclasses(subclass))
|
||||
return all_subclasses
|
||||
|
||||
def get_family(self, **kwargs):
|
||||
def get_family(self, *args, **kwargs):
|
||||
"""
|
||||
Variation of get that not only returns the current typeclass
|
||||
but also all subclasses of that typeclass.
|
||||
|
|
@ -817,7 +817,7 @@ class TypeclassManager(TypedObjectManager):
|
|||
"%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model)
|
||||
]
|
||||
kwargs.update({"db_typeclass_path__in": paths})
|
||||
return super().get(**kwargs)
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def filter_family(self, *args, **kwargs):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ callable must be a module-global function on the form
|
|||
|
||||
>: start
|
||||
|
||||
# node abort
|
||||
## node abort
|
||||
|
||||
This exits the menu since there is no `## options` section.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue