diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 9f7c5fa627..fd18fb28ae 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2,6 +2,7 @@ Building and world design commands """ import re +import typing from django.conf import settings from django.core.paginator import Paginator @@ -110,6 +111,14 @@ class ObjManipCommand(COMMAND_DEFAULT_CLASS): # OBS - this is just a parent - it's not intended to actually be # included in a commandset on its own! + # used by get_object_typeclass as defaults. + default_typeclasses = { + "object": settings.BASE_OBJECT_TYPECLASS, + "character": settings.BASE_CHARACTER_TYPECLASS, + "room": settings.BASE_ROOM_TYPECLASS, + "exit": settings.BASE_EXIT_TYPECLASS, + } + def parse(self): """ We need to expand the default parsing to get all @@ -164,6 +173,48 @@ class ObjManipCommand(COMMAND_DEFAULT_CLASS): self.lhs_objattr = obj_attrs[0] self.rhs_objattr = obj_attrs[1] + def get_object_typeclass( + self, obj_type: str = "object", typeclass: str = None, method: str = "cmd_create", **kwargs + ) -> tuple[typing.Optional["Builder"], list[str]]: + """ + This hook is called by build commands to determine which typeclass to use for a specific purpose. For instance, + when using dig, the system can use this to autodetect which kind of Room typeclass to use based on where the + builder is currently located. + + Note: Although intended to be used with typeclasses, as long as this hook returns a class with a create method, + which accepts the same API as DefaultObject.create(), build commands and other places should take it. + + Args: + obj_type (str, optional): The type of object that is being created. Defaults to "object". Evennia provides + "room", "exit", and "character" by default, but this can be extended. + typeclass (str, optional): The typeclass that was requested by the player. Defaults to None. + Can also be an actual class. + method (str, optional): The method that is calling this hook. Defaults to "cmd_create". + Others are "cmd_dig", "cmd_open", "cmd_tunnel", etc. + + Returns: + results_tuple (tuple[Optional[Builder], list[str]]): A tuple containing the typeclass to use and a list of + errors. (which might be empty.) + """ + + found_typeclass = typeclass or self.default_typeclasses.get(obj_type, None) + if not found_typeclass: + return None, [f"No typeclass found for object type '{obj_type}'."] + + try: + type_class = ( + class_from_module(found_typeclass, settings.TYPECLASS_PATHS) + if isinstance(found_typeclass, str) + else found_typeclass + ) + except ImportError: + return None, [f"Typeclass '{found_typeclass}' could not be imported."] + + if not hasattr(type_class, "create"): + return None, [f"Typeclass '{found_typeclass}' is not creatable."] + + return type_class, [] + class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): """ @@ -194,6 +245,8 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS): locks = "cmd:perm(setobjalias) or perm(Builder)" help_category = "Building" + method_type = "cmd_create" + def func(self): """Set the aliases.""" @@ -579,10 +632,6 @@ class CmdCreate(ObjManipCommand): locks = "cmd:perm(create) or perm(Builder)" help_category = "Building" - # lockstring of newly created objects, for easy overloading. - # Will be formatted with the {id} of the creating object. - new_obj_lockstring = "control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)" - def func(self): """ Creates the object. @@ -600,22 +649,23 @@ class CmdCreate(ObjManipCommand): string = "" name = objdef["name"] aliases = objdef["aliases"] - typeclass = objdef["option"] - # create object (if not a valid typeclass, the default - # object typeclass will automatically be used) - lockstring = self.new_obj_lockstring.format(id=caller.id) - obj = create.create_object( - typeclass, - name, - caller, - home=caller, - aliases=aliases, - locks=lockstring, - report_to=caller, + obj_typeclass, errors = self.get_object_typeclass( + obj_type="object", typeclass=objdef["option"] ) + if errors: + self.msg(errors) + if not obj_typeclass: + continue + + obj, errors = obj_typeclass.create( + name, caller, home=caller, aliases=aliases, report_to=caller, caller=caller + ) + if errors: + self.msg(errors) if not obj: continue + if aliases: string = ( f"You create a new {obj.typename}: {obj.name} (aliases: {', '.join(aliases)})." @@ -896,6 +946,8 @@ class CmdDig(ObjManipCommand): locks = "cmd:perm(dig) or perm(Builder)" help_category = "Building" + method_type = "cmd_dig" + # lockstring of newly created rooms, for easy overloading. # Will be formatted with the {id} of the creating object. new_room_lockstring = ( @@ -924,22 +976,32 @@ class CmdDig(ObjManipCommand): location = caller.location # Create the new room - typeclass = room["option"] - if not typeclass: - typeclass = settings.BASE_ROOM_TYPECLASS + room_typeclass, errors = self.get_object_typeclass( + obj_type="room", typeclass=room["option"], method=self.method_type + ) + if errors: + self.msg("|rError creating room:|n %s" % errors) + if not room_typeclass: + return # create room - new_room = create.create_object( - typeclass, room["name"], aliases=room["aliases"], report_to=caller + new_room, errors = room_typeclass.create( + room["name"], + aliases=room["aliases"], + report_to=caller, + caller=caller, + method=self.method_type, ) - lockstring = self.new_room_lockstring.format(id=caller.id) - new_room.locks.add(lockstring) + if errors: + self.msg("|rError creating room:|n %s" % errors) + if not new_room: + return + alias_string = "" if new_room.aliases.all(): alias_string = " (%s)" % ", ".join(new_room.aliases.all()) - room_string = ( - f"Created room {new_room}({new_room.dbref}){alias_string} of type {typeclass}." - ) + + room_string = f"Created room {new_room}({new_room.dbref}){alias_string} of type {new_room}." # create exit to room @@ -954,19 +1016,28 @@ class CmdDig(ObjManipCommand): exit_to_string = "\nYou cannot create an exit from a None-location." else: # Build the exit to the new room from the current one - typeclass = to_exit["option"] - if not typeclass: - typeclass = settings.BASE_EXIT_TYPECLASS - - new_to_exit = create.create_object( - typeclass, - to_exit["name"], - location, - aliases=to_exit["aliases"], - locks=lockstring, - destination=new_room, - report_to=caller, + exit_typeclass, errors = self.get_object_typeclass( + obj_type="exit", typeclass=to_exit["option"], method=self.method_type ) + if errors: + self.msg("|rError creating exit:|n %s" % errors) + if not exit_typeclass: + return + + new_to_exit, errors = exit_typeclass.create( + to_exit["name"], + location=location, + destination=new_room, + aliases=to_exit["aliases"], + report_to=caller, + caller=caller, + method=self.method_type, + ) + if errors: + self.msg("|rError creating exit:|n %s" % errors) + if not new_to_exit: + return + alias_string = "" if new_to_exit.aliases.all(): alias_string = " (%s)" % ", ".join(new_to_exit.aliases.all()) @@ -985,18 +1056,26 @@ class CmdDig(ObjManipCommand): elif not location: exit_back_string = "\nYou cannot create an exit back to a None-location." else: - typeclass = back_exit["option"] - if not typeclass: - typeclass = settings.BASE_EXIT_TYPECLASS - new_back_exit = create.create_object( - typeclass, - back_exit["name"], - new_room, - aliases=back_exit["aliases"], - locks=lockstring, - destination=location, - report_to=caller, + exit_typeclass, errors = self.get_object_typeclass( + obj_type="exit", typeclass=back_exit["option"], method=self.method_type ) + if errors: + self.msg("|rError creating exit:|n %s" % errors) + if not exit_typeclass: + return + new_back_exit, errors = exit_typeclass.create( + back_exit["name"], + location=new_room, + destination=location, + aliases=back_exit["aliases"], + report_to=caller, + caller=caller, + method=self.method_type, + ) + if errors: + self.msg("|rError creating exit:|n %s" % errors) + if not new_back_exit: + return alias_string = "" if new_back_exit.aliases.all(): alias_string = " (%s)" % ", ".join(new_back_exit.aliases.all()) @@ -1042,6 +1121,8 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS): locks = "cmd: perm(tunnel) or perm(Builder)" help_category = "Building" + method_type = "cmd_tunnel" + # store the direction, full name and its opposite directions = { "n": ("north", "s"), @@ -1428,6 +1509,8 @@ class CmdOpen(ObjManipCommand): locks = "cmd:perm(open) or perm(Builder)" help_category = "Building" + method_type = "cmd_open" + new_obj_lockstring = "control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)" # a custom member method to chug out exits and do checks @@ -1474,17 +1557,25 @@ class CmdOpen(ObjManipCommand): else: # exit does not exist before. Create a new one. - lockstring = self.new_obj_lockstring.format(id=caller.id) - if not typeclass: - typeclass = settings.BASE_EXIT_TYPECLASS - exit_obj = create.create_object( - typeclass, - key=exit_name, + exit_typeclass, errors = self.get_object_typeclass( + obj_type="exit", typeclass=typeclass, method=self.method_type + ) + if errors: + self.msg("|rError creating exit:|n %s" % errors) + if not exit_typeclass: + return + exit_obj, errors = exit_typeclass.create( + exit_name, location=location, aliases=exit_aliases, - locks=lockstring, report_to=caller, + caller=caller, + method=self.method_type, ) + if errors: + self.msg("|rError creating exit:|n %s" % errors) + if not exit_obj: + return if exit_obj: # storing a destination is what makes it an exit! exit_obj.destination = destination diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 340c5271a6..353bfe6ea7 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -725,16 +725,17 @@ class TestAccount(BaseEvenniaCommandTest): class TestBuilding(BaseEvenniaCommandTest): def test_create(self): - name = settings.BASE_OBJECT_TYPECLASS.rsplit(".", 1)[1] + typeclass = settings.BASE_OBJECT_TYPECLASS + name = typeclass.rsplit(".", 1)[1] self.call( building.CmdCreate(), - "/d TestObj1", # /d switch is abbreviated form of /drop + f"/d TestObj1:{typeclass}", # /d switch is abbreviated form of /drop "You create a new %s: TestObj1." % name, ) self.call(building.CmdCreate(), "", "Usage: ") self.call( building.CmdCreate(), - "TestObj1;foo;bar", + f"TestObj1;foo;bar:{typeclass}", "You create a new %s: TestObj1 (aliases: foo, bar)." % name, ) @@ -2082,7 +2083,7 @@ class TestBatchProcess(BaseEvenniaCommandTest): # cannot test batchcode here, it must run inside the server process self.call( batchprocess.CmdBatchCommands(), - "batchprocessor.example_batch_cmds", + "batchprocessor.example_batch_cmds_test", "Running Batch-command processor - Automatic mode for" " batchprocessor.example_batch_cmds", ) diff --git a/evennia/contrib/tutorials/batchprocessor/example_batch_cmds_test.ev b/evennia/contrib/tutorials/batchprocessor/example_batch_cmds_test.ev new file mode 100644 index 0000000000..5f3bbe346d --- /dev/null +++ b/evennia/contrib/tutorials/batchprocessor/example_batch_cmds_test.ev @@ -0,0 +1,36 @@ +# +# This is an example batch build file for Evennia. +# +# This version is stripped down to work better with the test system. +# It avoids teleporting the button. For the full version look at the +# other example_batch_cmds.ev file. + +# This creates a red button + +create/drop button:red_button.RedButton + +# This comment ends input for @create +# Next command: + +set button/desc = + This is a large red button. Now and then + it flashes in an evil, yet strangely tantalizing way. + + A big sign sits next to it. It says: + + +----------- + + Press me! + +----------- + + + ... It really begs to be pressed, doesn't it? You +know you want to! + +# This ends the @set command. Note that line breaks and extra spaces +# in the argument are not considered. A completely empty line +# translates to a \n newline in the command; two empty lines will thus +# create a new paragraph. (note that few commands support it though, you +# mainly want to use it for descriptions). diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index b117cb613e..94f602be4b 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -8,6 +8,7 @@ This is the v1.0 develop version (for ref in doc building). """ import time +import typing from collections import defaultdict import typing @@ -222,7 +223,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): {exits}{characters}{things} {footer} """ - # on-object properties @lazy_property @@ -1013,7 +1013,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): obj.move_to(home, move_type="teleport") @classmethod - def create(cls, key, account=None, **kwargs): + def create( + cls, + key: str, + account: "DefaultAccount" = None, + caller: "DefaultObject" = None, + method: str = "create", + **kwargs, + ): """ Creates a basic object with default parameters, unless otherwise specified or extended. @@ -1022,11 +1029,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: key (str): Name of the new object. - account (Account): Account to attribute this object to. + Keyword Args: + account (Account): Account to attribute this object to. + caller (DefaultObject): The object which is creating this one. description (str): Brief description for this object. ip (str): IP address of creator (for object auditing). + method (str): The method of creation. Defaults to "create". Returns: object (Object): A newly created object of the given typeclass. @@ -2786,7 +2796,14 @@ class DefaultRoom(DefaultObject): ) @classmethod - def create(cls, key, account=None, **kwargs): + def create( + cls, + key: str, + account: "DefaultAccount" = None, + caller: DefaultObject = None, + method: str = "create", + **kwargs, + ): """ Creates a basic Room with default parameters, unless otherwise specified or extended. @@ -2795,13 +2812,15 @@ class DefaultRoom(DefaultObject): Args: key (str): Name of the new Room. - account (obj, optional): Account to associate this Room with. If - given, it will be given specific control/edit permissions to this - object (along with normal Admin perms). If not given, default Keyword Args: + account (DefaultAccount, optional): Account to associate this Room with. If + given, it will be given specific control/edit permissions to this + object (along with normal Admin perms). If not given, default + caller (DefaultObject): The object which is creating this one. description (str): Brief description for this object. ip (str): IP address of creator (for object auditing). + method (str): The method used to create the room. Defaults to "create". Returns: room (Object): A newly created Room of the given typeclass. @@ -2992,7 +3011,16 @@ class DefaultExit(DefaultObject): # Command hooks @classmethod - def create(cls, key, source, dest, account=None, **kwargs): + def create( + cls, + key: str, + location: DefaultRoom = None, + destination: DefaultRoom = None, + account: "DefaultAccount" = None, + caller: DefaultObject = None, + method: str = "create", + **kwargs, + ) -> tuple[typing.Optional["DefaultExit"], list[str]]: """ Creates a basic Exit with default parameters, unless otherwise specified or extended. @@ -3002,13 +3030,14 @@ class DefaultExit(DefaultObject): Args: key (str): Name of the new Exit, as it should appear from the source room. - account (obj): Account to associate this Exit with. - source (Room): The room to create this exit in. - dest (Room): The room to which this exit should go. + location (Room): The room to create this exit in. Keyword Args: + account (obj): Account to associate this Exit with. + caller (ObjectDB): the Object creating this Object. description (str): Brief description for this object. ip (str): IP address of creator (for object auditing). + destination (Room): The room to which this exit should go. Returns: exit (Object): A newly created Room of the given typeclass. @@ -3031,8 +3060,8 @@ class DefaultExit(DefaultObject): kwargs["report_to"] = kwargs.pop("report_to", account) # Set to/from rooms - kwargs["location"] = source - kwargs["destination"] = dest + kwargs["location"] = location + kwargs["destination"] = destination description = kwargs.pop("description", "")