diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 6964f337a3..dff960ee43 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -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 [;alias...][:typeclass]" + "[,[;alias..][:typeclass]]] " + "= ") + 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 [;alias...][:typeclass][,[;alias..][:typeclass]]] " - string += "= " - 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] [ =] ||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] [ =] ||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: diff --git a/evennia/contrib/xyzgrid/commands.py b/evennia/contrib/xyzgrid/commands.py new file mode 100644 index 0000000000..a8d765de85 --- /dev/null +++ b/evennia/contrib/xyzgrid/commands.py @@ -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 [ to||=] + tel/switch [ 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, 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 [;alias;..][:typeclass] [,[;alias;..][:typeclass]]] = + open [;alias;..][:typeclass] [,[;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 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 [;alias...][:typeclass]" + "[,[;alias..][:typeclass]]] " + "= ") + 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()) diff --git a/evennia/contrib/xyzgrid/map_example.py b/evennia/contrib/xyzgrid/map_example.py index 8feb5f753b..2a7193951a 100644 --- a/evennia/contrib/xyzgrid/map_example.py +++ b/evennia/contrib/xyzgrid/map_example.py @@ -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""" diff --git a/evennia/contrib/xyzgrid/map_legend.py b/evennia/contrib/xyzgrid/map_legend.py index c6fd3294a4..44c111e6e5 100644 --- a/evennia/contrib/xyzgrid/map_legend.py +++ b/evennia/contrib/xyzgrid/map_legend.py @@ -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. diff --git a/evennia/contrib/xyzgrid/tests.py b/evennia/contrib/xyzgrid/tests.py index e58a883253..71c9c33aea 100644 --- a/evennia/contrib/xyzgrid/tests.py +++ b/evennia/contrib/xyzgrid/tests.py @@ -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")) diff --git a/evennia/contrib/xyzgrid/xymap.py b/evennia/contrib/xyzgrid/xymap.py index d371876788..04fb9626ea 100644 --- a/evennia/contrib/xyzgrid/xymap.py +++ b/evennia/contrib/xyzgrid/xymap.py @@ -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 diff --git a/evennia/contrib/xyzgrid/xyzroom.py b/evennia/contrib/xyzgrid/xyzroom.py index 2b9188ca7d..a0648ada15 100644 --- a/evennia/contrib/xyzgrid/xyzroom.py +++ b/evennia/contrib/xyzgrid/xyzroom.py @@ -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"" + @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"({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"): diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index cad32c7258..6e56ad6c63 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -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 ()) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index bde49cf1b0..2f4d0ad640 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -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