Add xyzgrid support commands

This commit is contained in:
Griatch 2021-07-08 21:40:35 +02:00
parent 1c6fffeff2
commit 578e6d63e1
9 changed files with 345 additions and 100 deletions

View file

@ -4,6 +4,7 @@ Building and world design commands
import re
from django.conf import settings
from django.db.models import Q, Min, Max
from evennia import InterruptCommand
from evennia.objects.models import ObjectDB
from evennia.locks.lockhandler import LockException
from evennia.commands.cmdhandler import get_and_merge_cmdsets
@ -1487,40 +1488,33 @@ class CmdOpen(ObjManipCommand):
caller.msg(string)
return exit_obj
def parse(self):
super().parse()
self.location = self.caller.location
if not self.args or not self.rhs:
self.caller.msg("Usage: open <new exit>[;alias...][:typeclass]"
"[,<return exit>[;alias..][:typeclass]]] "
"= <destination>")
raise InterruptCommand
if not self.location:
self.caller.msg("You cannot create an exit from a None-location.")
raise InterruptCommand
self.destination = self.caller.search(self.rhs, global_search=True)
if not self.destination:
raise InterruptCommand
self.exit_name = self.lhs_objs[0]["name"]
self.exit_aliases = self.lhs_objs[0]["aliases"]
self.exit_typeclass = self.lhs_objs[0]["option"]
def func(self):
"""
This is where the processing starts.
Uses the ObjManipCommand.parser() for pre-processing
as well as the self.create_exit() method.
"""
caller = self.caller
if not self.args or not self.rhs:
string = "Usage: open <new exit>[;alias...][:typeclass][,<return exit>[;alias..][:typeclass]]] "
string += "= <destination>"
caller.msg(string)
return
# We must have a location to open an exit
location = caller.location
if not location:
caller.msg("You cannot create an exit from a None-location.")
return
# obtain needed info from cmdline
exit_name = self.lhs_objs[0]["name"]
exit_aliases = self.lhs_objs[0]["aliases"]
exit_typeclass = self.lhs_objs[0]["option"]
dest_name = self.rhs
# first, check so the destination exists.
destination = caller.search(dest_name, global_search=True)
if not destination:
return
# Create exit
ok = self.create_exit(exit_name, location, destination, exit_aliases, exit_typeclass)
ok = self.create_exit(self.exit_name, self.location, self.destination,
self.exit_aliases, self.exit_typeclass)
if not ok:
# an error; the exit was not created, so we quit.
return
@ -1529,9 +1523,8 @@ class CmdOpen(ObjManipCommand):
back_exit_name = self.lhs_objs[1]["name"]
back_exit_aliases = self.lhs_objs[1]["aliases"]
back_exit_typeclass = self.lhs_objs[1]["option"]
self.create_exit(
back_exit_name, destination, location, back_exit_aliases, back_exit_typeclass
)
self.create_exit(back_exit_name, self.destination, self.location, back_exit_aliases,
back_exit_typeclass)
def _convert_from_string(cmd, strobj):
@ -2981,28 +2974,31 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
locks = "cmd:perm(teleport) or perm(Builder)"
help_category = "Building"
def parse(self):
"""
Breaking out searching here to make this easier to override.
"""
super().parse()
self.obj_to_teleport = self.caller
self.destination = None
if self.lhs:
self.obj_to_teleport = self.caller.search(self.lhs, global_search=True)
if not self.obj_to_teleport:
self.caller.msg("Did not find object to teleport.")
raise InterruptCommand
if self.rhs:
self.destination = self.caller.search(self.rhs, global_search=True)
def func(self):
"""Performs the teleport"""
caller = self.caller
args = self.args
lhs, rhs = self.lhs, self.rhs
switches = self.switches
obj_to_teleport = self.obj_to_teleport
destination = self.destination
# setting switches
tel_quietly = "quiet" in switches
to_none = "tonone" in switches
to_loc = "loc" in switches
if to_none:
if "tonone" in self.switches:
# teleporting to None
if not args:
obj_to_teleport = caller
else:
obj_to_teleport = caller.search(lhs, global_search=True)
if not obj_to_teleport:
caller.msg("Did not find object to teleport.")
return
if obj_to_teleport.has_account:
caller.msg(
"Cannot teleport a puppeted object "
@ -3011,53 +3007,44 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
)
return
caller.msg("Teleported %s -> None-location." % obj_to_teleport)
if obj_to_teleport.location and not tel_quietly:
if obj_to_teleport.location and "quiet" not in self.switches:
obj_to_teleport.location.msg_contents(
"%s teleported %s into nothingness." % (caller, obj_to_teleport), exclude=caller
)
obj_to_teleport.location = None
return
# not teleporting to None location
if not args and not to_none:
caller.msg("Usage: teleport[/switches] [<obj> =] <target_loc>||home")
return
if rhs:
obj_to_teleport = caller.search(lhs, global_search=True)
destination = caller.search(rhs, global_search=True)
else:
obj_to_teleport = caller
destination = caller.search(lhs, global_search=True)
if not obj_to_teleport:
caller.msg("Did not find object to teleport.")
if not self.args:
caller.msg("Usage: teleport[/switches] [<obj> =] <target or (X,Y,Z)>||home")
return
if not destination:
caller.msg("Destination not found.")
return
if to_loc:
if "loc" in self.switches:
destination = destination.location
if not destination:
caller.msg("Destination has no location.")
return
if obj_to_teleport == destination:
caller.msg("You can't teleport an object inside of itself!")
return
if obj_to_teleport == destination.location:
caller.msg("You can't teleport an object inside something it holds!")
return
if obj_to_teleport.location and obj_to_teleport.location == destination:
caller.msg("%s is already at %s." % (obj_to_teleport, destination))
return
use_destination = True
if "intoexit" in self.switches:
use_destination = False
# try the teleport
if obj_to_teleport.move_to(
destination, quiet=tel_quietly, emit_to_obj=caller, use_destination=use_destination
):
destination, quiet="quiet" in self.switches,
emit_to_obj=caller, use_destination="intoexit" not in self.switches):
if obj_to_teleport == caller:
caller.msg("Teleported to %s." % destination)
else:

View file

@ -0,0 +1,187 @@
"""
XYZ-aware commands
Just add the XYZGridCmdSet to the default character cmdset to override
the commands with XYZ-aware equivalents.
"""
from evennia import InterruptCommand
from evennia import MuxCommand, CmdSet
from evennia.commands.default import building, general
from evennia.contrib.xyzgrid.xyzroom import XYZRoom
from evennia.utils.utils import inherits_from
class CmdXYZLook(general.CmdLook):
character = '@'
visual_range = 2
map_mode = 'nodes' # or 'scan'
def func(self):
"""
Add xymap display before the normal look command.
"""
location = self.caller.location
if inherits_from(location, XYZRoom):
xyz = location.xyz
xymap = location.xyzgrid.get_map(xyz[2])
map_display = xymap.get_visual_range(
(xyz[0], xyz[1]),
dist=self.visual_range,
mode=self.map_mode)
maxw = min(xymap.max_x, self.client_width())
sep = "~" * maxw
map_display = f"|x{sep}|n\n{map_display}\n|x{sep}"
self.msg(map_display, {"type", "xymap"}, options=None)
# now run the normal look command
super().func()
class CmdXYZTeleport(building.CmdTeleport):
"""
teleport object to another location
Usage:
tel/switch [<object> to||=] <target location>
tel/switch [<object> to||=] (X,Y[,Z])
Examples:
tel Limbo
tel/quiet box = Limbo
tel/tonone box
tel (3, 3, the small cave)
tel (4, 1) # on the same map
Switches:
quiet - don't echo leave/arrive messages to the source/target
locations for the move.
intoexit - if target is an exit, teleport INTO
the exit object instead of to its destination
tonone - if set, teleport the object to a None-location. If this
switch is set, <target location> is ignored.
Note that the only way to retrieve
an object from a None location is by direct #dbref
reference. A puppeted object cannot be moved to None.
loc - teleport object to the target's location instead of its contents
Teleports an object somewhere. If no object is given, you yourself are
teleported to the target location. If (X,Y) or (X,Y,Z) coordinates
are given, the target is a location on the XYZGrid.
"""
def parse(self):
MuxCommand.parse(self)
self.obj_to_teleport = self.caller
self.destination = None
rhs = self.rhs
if self.lhs:
self.obj_to_teleport = self.caller.search(self.lhs, global_search=True)
if not self.obj_to_teleport:
self.caller.msg("Did not find object to teleport.")
raise InterruptCommand
if rhs:
if all(char in rhs for char in ("(", ")", ",")):
# search by (X,Y) or (X,Y,Z)
X, Y, *Z = rhs.split(",", 2)
if Z:
# Z was specified
Z = Z[0]
else:
# use current location's Z, if it exists
try:
xyz = self.caller.xyz
except AttributeError:
self.caller.msg("Z-coordinate is also required since you are not currently "
"in a room with a Z coordinate of its own.")
raise InterruptCommand
else:
Z = xyz[2]
# search by coordinate
X, Y, Z = str(X).strip(), str(Y).strip(), str(Z).strip()
try:
self.obj_to_teleport = XYZRoom.objects.get_xyz(xyz=(X, Y, Z))
except XYZRoom.DoesNotExist:
self.caller.msg("Found no target XYZRoom at ({X},{Y},{Y}).")
raise InterruptCommand
else:
# regular search
self.destination = self.caller.search(rhs, global_search=True)
class CmdXYZOpen(building.CmdOpen):
"""
open a new exit from the current room
Usage:
open <new exit>[;alias;..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = <destination>
open <new exit>[;alias;..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = (X,Y,Z)
Handles the creation of exits. If a destination is given, the exit
will point there. The destination can also be given as an (X,Y,Z) coordinate on the
XYZGrid - this command is used to link non-grid rooms to the grid and vice-versa.
The <return exit> argument sets up an exit at the destination leading back to the current room.
Apart from (X,Y,Z) coordinate, destination name can be given both as a #dbref and a name, if
that name is globally unique.
Examples:
open kitchen = Kitchen
open north, south = Town Center
open cave mouth;cave = (3, 4, the small cave)
"""
def parse(self):
building.ObjManipCommand.parse(self)
self.location = self.caller.location
if not self.args or not self.rhs:
self.caller.msg("Usage: open <new exit>[;alias...][:typeclass]"
"[,<return exit>[;alias..][:typeclass]]] "
"= <destination>")
raise InterruptCommand
if not self.location:
self.caller.msg("You cannot create an exit from a None-location.")
raise InterruptCommand
if all(char in self.rhs for char in ("(", ")", ",")):
# search by (X,Y) or (X,Y,Z)
X, Y, *Z = self.rhs.split(",", 2)
if not Z:
self.caller.msg("A full (X,Y,Z) coordinate must be given for the destination.")
raise InterruptCommand
Z = Z[0]
# search by coordinate
X, Y, Z = str(X).strip(), str(Y).strip(), str(Z).strip()
try:
self.destination = XYZRoom.objects.get_xyz(xyz=(X, Y, Z))
except XYZRoom.DoesNotExist:
self.caller.msg("Found no target XYZRoom at ({X},{Y},{Y}).")
raise InterruptCommand
else:
# regular search query
self.destination = self.caller.search(self.rhs, global_search=True)
if not self.destination:
raise InterruptCommand
self.exit_name = self.lhs_objs[0]["name"]
self.exit_aliases = self.lhs_objs[0]["aliases"]
self.exit_typeclass = self.lhs_objs[0]["option"]
class XYZGridCmdSet(CmdSet):
"""
Cmdset for easily adding the above cmds to the character cmdset.
"""
key = "xyzgrid_cmdset"
def at_cmdset_creation(self):
self.add(CmdXYZLook())
self.add(CmdXYZTeleport())
self.add(CmdXYZOpen())

View file

@ -31,7 +31,10 @@ PARENT = {
"desc": "An empty room."
}
# -------------------- map 1 - the large tree
# ---------------------------------------- map1
# The large tree
#
# this exemplifies the various map symbols
# but is not heavily prototyped
@ -39,11 +42,11 @@ MAP1 = r"""
1
+ 0 1 2 3 4 5 6 7 8 9 0
9 #-------#-#-------I
\ /
8 #-#---# #-t
8 #-------#-#-------I
\ /
7 #-#---# #-t
|\ |
7 #i#-#b--#-t
6 #i#-#b--#-t
| |
5 o-#---#
\ /
@ -68,7 +71,7 @@ class TransitionToCave(map_legend.MapTransitionMapNode):
"""
symbol = 'T'
target_map_xyz = (2, 3, 'small cave')
target_map_xyz = (1, 0, 'the small cave')
# extends the default legend
@ -110,15 +113,15 @@ PROTOTYPES_MAP1 = {
"key": "Dense foilage",
"desc": "The foilage to the east is extra dense. It will take forever to get through it."
},
(5, 7): {
(5, 6): {
"key": "On a huge branch",
"desc": "To the east is a glowing light, may be a teleporter."
},
(9, 8): {
(9, 7): {
"key": "On an enormous branch",
"desc": "To the east is a glowing light, may be a teleporter."
},
(10, 9): {
(10, 8): {
"key": "A gorgeous view",
"desc": "The view from here is breathtaking, showing the forest stretching far and wide."
},
@ -144,7 +147,8 @@ XYMAP_DATA_MAP1 = {
"prototypes": PROTOTYPES_MAP1
}
# ------------- map2 definitions - small cave
# -------------------------------------- map2
# The small cave
# this gives prototypes for every room
MAP2 = r"""

View file

@ -14,6 +14,7 @@ except ImportError as err:
f"{err}\nThe XYZgrid contrib requires "
"the SciPy package. Install with `pip install scipy'.")
import uuid
from evennia.prototypes import spawner
from evennia.utils.utils import make_iter
from .utils import MAPSCAN, REVERSE_DIRECTIONS, MapParserError, BIGVAL
@ -22,6 +23,9 @@ NodeTypeclass = None
ExitTypeclass = None
UUID_XYZ_NAMESPACE = uuid.uuid5(uuid.UUID(int=0), "xyzgrid")
# Nodes/Links
class MapNode:
@ -135,6 +139,18 @@ class MapNode:
def __repr__(self):
return str(self)
def log(self, msg):
"""log messages using the xygrid parent"""
self.xymap.xyzgrid.log(msg)
def generate_prototype_key(self):
"""
Generate a deterministic prototype key to allow for users to apply prototypes without
needing a separate new name for every one.
"""
return str(uuid.uuid5(UUID_XYZ_NAMESPACE, str((self.X, self.Y, self.Z))))
def build_links(self):
"""
This is called by the map parser when this node is encountered. It tells the node
@ -261,20 +277,26 @@ class MapNode:
return
xyz = self.get_spawn_xyz()
print("xyz:", xyz, self.node_index)
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
nodeobj, err = NodeTypeclass.create(
self.prototype.get('key', 'An Empty room'),
self.prototype.get('key', 'An empty room'),
xyz=xyz
)
if err:
raise RuntimeError(err)
if not self.prototype.get('prototype_key'):
# make sure there is a prototype_key in prototype
self.prototype['prototype_key'] = self.generate_prototype_key()
# 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)
@ -309,6 +331,9 @@ class MapNode:
if link.spawn_aliases
else self.direction_spawn_defaults.get(direction, ('unknown',))
)
if not link.prototype.get('prototype_key'):
# generate a deterministic prototype_key if it doesn't exist
link.prototype['prototype_key'] = self.generate_prototype_key()
maplinks[key.lower()] = (key, aliases, direction, link)
# we need to search for exits in all directions since some
@ -323,6 +348,8 @@ class MapNode:
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}")
linkobjs.pop(differing_key).delete()
else:
# missing in linkobjs - create a new exit
@ -341,6 +368,7 @@ class MapNode:
if err:
raise RuntimeError(err)
linkobjs[key.lower()] = exi
self.log(f" spawning/updating exit xyz={xyz}, direction={direction}")
# apply prototypes to catch any changes
for key, linkobj in linkobjs.items():
@ -537,6 +565,17 @@ class MapLink:
def __repr__(self):
return str(self)
def generate_prototype_key(self, *args):
"""
Generate a deterministic prototype key to allow for users to apply prototypes without
needing a separate new name for every one.
Args:
*args (str): These are used to further seed the key.
"""
return str(uuid.uuid5(UUID_XYZ_NAMESPACE, str((self.X, self.Y, self.Z, *args))))
def traverse(self, start_direction, _weight=0, _linklen=1, _steps=None):
"""
Recursively traverse the links out of this LinkNode.

View file

@ -355,8 +355,15 @@ class _MapTest(TestCase):
self.grid.add_maps(self.map_data)
self.map = self.grid.get_map(self.map_data['zcoord'])
# output to console
# def _log(msg):
# print(msg)
# self.grid.log = _log
def tearDown(self):
self.grid.delete()
xyzroom.XYZRoom.objects.all().delete()
xyzroom.XYZExit.objects.all().delete()
class TestMap1(_MapTest):
@ -1055,7 +1062,6 @@ class TestMapStressTest(TestCase):
"""
Xmax, Ymax = gridsize
grid = self._get_grid(Xmax, Ymax)
# print(f"\n\n{grid}\n")
t0 = time()
mapobj = xymap.XYMap({'map': grid}, Z="testmap")
mapobj.parse()
@ -1239,13 +1245,17 @@ class TestXYZGridTransition(TestCase):
class TestBuildExampleGrid(TestCase):
"""
Test building the map_example
Test building the map_example (this takes about 30s)
"""
def setUp(self):
# build and populate grid
self.grid, err = xyzgrid.XYZGrid.create("testgrid")
def _log(msg):
print(msg)
self.grid.log = _log
def tearDown(self):
self.grid.delete()
@ -1262,15 +1272,15 @@ class TestBuildExampleGrid(TestCase):
# testing
room1a = xyzroom.XYZRoom.objects.get_xyz(xyz=(3, 0, 'the large tree'))
room1b = xyzroom.XYZRoom.objects.get_xyz(xyz=(10, 9, 'the large tree'))
room2a = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 0, 'small cave'))
room2b = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 3, 'small cave'))
room1b = xyzroom.XYZRoom.objects.get_xyz(xyz=(10, 8, 'the large tree'))
room2a = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 0, 'the small cave'))
room2b = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 3, 'the small cave'))
self.assertEqual(room1a.key, "Dungeon Entrance")
self.assertTrue(room1a.desc.startswith("To the west"))
self.assertTrue(room1a.db.desc.startswith("To the west"))
self.assertEqual(room1b.key, "A gorgeous view")
self.assertTrue(room1b.desc.startswith("The view from here is breathtaking."))
self.assertTrue(room1b.db.desc.startswith("The view from here is breathtaking,"))
self.assertEqual(room2a.key, "The entrance")
self.assertTrue(room2a.desc.startswith("This is the entrance to"))
self.assertTrue(room2a.db.desc.startswith("This is the entrance to"))
self.assertEqual(room2b.key, "North-west corner of the atrium")
self.assertTrue(room2b.desc.startswith("Sunlight sifts down"))
self.assertTrue(room2b.db.desc.startswith("Sunlight sifts down"))

View file

@ -494,17 +494,19 @@ class XYMap:
for iy, node_or_link in ydct.items():
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():
node_coord = (node.X, node.Y)
# load prototype from override, or use default
node.prototype = self.prototypes.get(
node_coord, self.prototypes.get(('*', '*'), node.prototype))
# do the same for links (x, y, direction) coords
for direction, maplink in node.first_links.items():
maplink.prototype = self.prototypes.get(
node_coord + (direction,),
self.prototypes.get(('*', '*', '*'), maplink.prototype))
# override node-prototypes, ignore if no prototype
# is defined (some nodes should not be spawned)
if node.prototype:
node_coord = (node.X, node.Y)
# load prototype from override, or use default
node.prototype = self.prototypes.get(
node_coord, self.prototypes.get(('*', '*'), node.prototype))
# do the same for links (x, y, direction) coords
for direction, maplink in node.first_links.items():
maplink.prototype = self.prototypes.get(
node_coord + (direction,),
self.prototypes.get(('*', '*', '*'), maplink.prototype))
# store
self.display_map = display_map

View file

@ -21,6 +21,8 @@ MAP_XDEST_TAG_CATEGORY = "exit_dest_x_coordinate"
MAP_YDEST_TAG_CATEGORY = "exit_dest_y_coordinate"
MAP_ZDEST_TAG_CATEGORY = "exit_dest_z_coordinate"
GET_XYZGRID = None
class XYZManager(ObjectManager):
"""
@ -236,6 +238,13 @@ class XYZRoom(DefaultRoom):
x, y, z = self.xyz
return f"<XYZRoom '{self.db_key}', XYZ=({x},{y},{z})>"
@property
def xyzgrid(self):
global GET_XYZGRID
if not GET_XYZGRID:
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
return GET_XYZGRID()
@property
def xyz(self):
if not hasattr(self, "_xyz"):
@ -310,6 +319,13 @@ class XYZExit(DefaultExit):
xd, yd, zd = self.xyz_destination
return f"<XYZExit '{self.db_key}', XYZ=({x},{y},{z})->({xd},{yd},{zd})>"
@property
def xyzgrid(self):
global GET_XYZGRID
if not GET_XYZGRID:
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
return GET_XYZGRID()
@property
def xyz(self):
if not hasattr(self, "_xyz"):

View file

@ -92,8 +92,8 @@ def homogenize_prototype(prototype, custom_keys=None):
homogenizations like adding missing prototype_keys and setting a default typeclass.
"""
if not prototype or not isinstance(prototype, dict):
return {}
if not prototype or isinstance(prototype, str):
return prototype
reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())

View file

@ -228,7 +228,7 @@ COMMAND_RATE_WARNING = "You entered commands too fast. Wait a moment and try aga
# custom, extra commands to add to the `evennia` launcher. This is a dict
# of {'cmdname': 'path.to.callable', ...}, where the callable will be passed
# any extra args given on the command line. For example `evennia cmdname foo bar`.
CUSTOM_LAUNCHER_COMMANDS = {}
EXTRA_LAUNCHER_COMMANDS = {}
# Determine how large of a string can be sent to the server in number
# of characters. If they attempt to enter a string over this character