From 7746ff16631f9da7ec065c73710f72b75a3dda58 Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sat, 6 May 2023 03:21:48 -0400 Subject: [PATCH 1/4] Added DefaultObject.can_build_object() and DefaultObject.at_object_constructed(builder) hooks and inserted them into all building commands for improved building flexibility. --- evennia/commands/default/building.py | 21 +++++++++++++++++- evennia/objects/objects.py | 33 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 9f7c5fa627..b0ddb00e5f 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -605,6 +605,9 @@ class CmdCreate(ObjManipCommand): # create object (if not a valid typeclass, the default # object typeclass will automatically be used) lockstring = self.new_obj_lockstring.format(id=caller.id) + if (err := caller.can_build_object()): + caller.msg(err) + return obj = create.create_object( typeclass, name, @@ -616,6 +619,7 @@ class CmdCreate(ObjManipCommand): ) if not obj: continue + obj.at_object_constructed(caller) if aliases: string = ( f"You create a new {obj.typename}: {obj.name} (aliases: {', '.join(aliases)})." @@ -929,6 +933,9 @@ class CmdDig(ObjManipCommand): typeclass = settings.BASE_ROOM_TYPECLASS # create room + if (err := caller.can_build_object()): + caller.msg(err) + return new_room = create.create_object( typeclass, room["name"], aliases=room["aliases"], report_to=caller ) @@ -937,6 +944,7 @@ class CmdDig(ObjManipCommand): alias_string = "" if new_room.aliases.all(): alias_string = " (%s)" % ", ".join(new_room.aliases.all()) + new_room.at_object_constructed(caller) room_string = ( f"Created room {new_room}({new_room.dbref}){alias_string} of type {typeclass}." ) @@ -957,7 +965,9 @@ class CmdDig(ObjManipCommand): typeclass = to_exit["option"] if not typeclass: typeclass = settings.BASE_EXIT_TYPECLASS - + if (err := caller.can_build_object()): + caller.msg(err) + return new_to_exit = create.create_object( typeclass, to_exit["name"], @@ -974,6 +984,7 @@ class CmdDig(ObjManipCommand): f"\nCreated Exit from {location.name} to {new_room.name}:" f" {new_to_exit}({new_to_exit.dbref}){alias_string}." ) + new_to_exit.at_object_constructed(caller) # Create exit back from new room @@ -988,6 +999,9 @@ class CmdDig(ObjManipCommand): typeclass = back_exit["option"] if not typeclass: typeclass = settings.BASE_EXIT_TYPECLASS + if (err := caller.can_build_object()): + caller.msg(err) + return new_back_exit = create.create_object( typeclass, back_exit["name"], @@ -1004,6 +1018,7 @@ class CmdDig(ObjManipCommand): f"\nCreated Exit back from {new_room.name} to {location.name}:" f" {new_back_exit}({new_back_exit.dbref}){alias_string}." ) + new_back_exit.at_object_constructed(caller) caller.msg(f"{room_string}{exit_to_string}{exit_back_string}") if new_room and "teleport" in self.switches: caller.move_to(new_room, move_type="teleport") @@ -1477,6 +1492,9 @@ class CmdOpen(ObjManipCommand): lockstring = self.new_obj_lockstring.format(id=caller.id) if not typeclass: typeclass = settings.BASE_EXIT_TYPECLASS + if (err := caller.can_build_object()): + caller.msg(err) + return exit_obj = create.create_object( typeclass, key=exit_name, @@ -1497,6 +1515,7 @@ class CmdOpen(ObjManipCommand): f"Created new Exit '{exit_name}' from {location.name} to" f" {destination.name}{string}." ) + exit_obj.at_object_constructed(caller) else: string = f"Error: Exit '{exit.name}' not created." # emit results diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index c710e96498..b222ac53d3 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1598,6 +1598,38 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ pass + def can_build_object(self): + """ + This hook is called by the build command to determine if + self can build a new object. This is called before the + object is created. As it receives no arguments, it + can only be used to determine if self is allowed to build + anything in the current context. + + For instance, a room may want to limit its number of exits, + or the builder might have an enforced quota limit for rooms + they can add to their custom dungeon. + + Returns: + can_build (str or None): If self is allowed to build objects. + return a string as the error if there is a problem. If + this returns True, the build will abort. + """ + pass + + def at_object_constructed(self, builder): + """ + Called when the object is constructed by a builder. + This is used to implement custom logic for building, + such as quota tracking systems, auto-tagging of rooms, + creation of logical groups of rooms like Zones or + dungeons, etc. + + Args: + builder (Object): The object that constructed this object. + """ + pass + def at_pre_puppet(self, account, session=None, **kwargs): """ Called just before an Account connects to this object to puppet @@ -1664,6 +1696,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ pass + def at_server_reload(self): """ This hook is called whenever the server is shutting down for From 0da7f962c2d15084b88409b1a40aaeafdaf89bde Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Wed, 1 Nov 2023 17:35:51 -0400 Subject: [PATCH 2/4] Trying a new approach. Introduced DefaultObject.get_object_typeclass() and cleaned up .create() hooks. Building commands now use the new logic. --- evennia/commands/default/building.py | 126 +++++++++-------- evennia/objects/objects.py | 202 +++++++++++++++------------ 2 files changed, 178 insertions(+), 150 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index b0ddb00e5f..82b6ff480f 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -579,10 +579,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,26 +596,26 @@ 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) - if (err := caller.can_build_object()): - caller.msg(err) - return - obj = create.create_object( - typeclass, + obj_typeclass, errors = caller.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, - locks=lockstring, report_to=caller, + creator=caller ) + if errors: + self.msg(errors) if not obj: continue - obj.at_object_constructed(caller) + if aliases: string = ( f"You create a new {obj.typename}: {obj.name} (aliases: {', '.join(aliases)})." @@ -928,25 +924,27 @@ class CmdDig(ObjManipCommand): location = caller.location # Create the new room - typeclass = room["option"] - if not typeclass: - typeclass = settings.BASE_ROOM_TYPECLASS + room_typeclass, errors = caller.get_object_typeclass(obj_type="room", typeclass=room["option"], method="dig") + if errors: + self.msg("|rError creating room:|n %s" % errors) + if not room_typeclass: + return # create room - if (err := caller.can_build_object()): - caller.msg(err) - return - 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, creator=caller, method="dig" ) - 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()) - new_room.at_object_constructed(caller) + room_string = ( - f"Created room {new_room}({new_room.dbref}){alias_string} of type {typeclass}." + f"Created room {new_room}({new_room.dbref}){alias_string} of type {new_room}." ) # create exit to room @@ -962,21 +960,27 @@ 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 - if (err := caller.can_build_object()): - caller.msg(err) + exit_typeclass, errors = caller.get_object_typeclass(obj_type="exit", typeclass=to_exit["option"], + method="dig") + if errors: + self.msg("|rError creating exit:|n %s" % errors) + if not exit_typeclass: return - new_to_exit = create.create_object( - typeclass, + + new_to_exit, errors = exit_typeclass.create( to_exit["name"], - location, - aliases=to_exit["aliases"], - locks=lockstring, + location=location, destination=new_room, + aliases=to_exit["aliases"], report_to=caller, + creator=caller, + method="dig" ) + 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()) @@ -984,7 +988,6 @@ class CmdDig(ObjManipCommand): f"\nCreated Exit from {location.name} to {new_room.name}:" f" {new_to_exit}({new_to_exit.dbref}){alias_string}." ) - new_to_exit.at_object_constructed(caller) # Create exit back from new room @@ -996,21 +999,25 @@ 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 - if (err := caller.can_build_object()): - caller.msg(err) + exit_typeclass, errors = caller.get_object_typeclass(obj_type="exit", typeclass=back_exit["option"], + method="dig") + if errors: + self.msg("|rError creating exit:|n %s" % errors) + if not exit_typeclass: return - new_back_exit = create.create_object( - typeclass, + new_back_exit, errors = exit_typeclass.create( back_exit["name"], - new_room, - aliases=back_exit["aliases"], - locks=lockstring, + location=new_room, destination=location, + aliases=back_exit["aliases"], report_to=caller, + creator=caller, + method="dig" ) + 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()) @@ -1018,7 +1025,6 @@ class CmdDig(ObjManipCommand): f"\nCreated Exit back from {new_room.name} to {location.name}:" f" {new_back_exit}({new_back_exit.dbref}){alias_string}." ) - new_back_exit.at_object_constructed(caller) caller.msg(f"{room_string}{exit_to_string}{exit_back_string}") if new_room and "teleport" in self.switches: caller.move_to(new_room, move_type="teleport") @@ -1489,20 +1495,23 @@ 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 - if (err := caller.can_build_object()): - caller.msg(err) + exit_typeclass, errors = caller.get_object_typeclass(obj_type="exit", typeclass=typeclass, method="open") + if errors: + self.msg("|rError creating exit:|n %s" % errors) + if not exit_typeclass: return - exit_obj = create.create_object( - typeclass, - key=exit_name, + exit_obj, errors = exit_typeclass.create( + exit_name, location=location, aliases=exit_aliases, - locks=lockstring, report_to=caller, + creator=caller, + method="open" ) + 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 @@ -1515,7 +1524,6 @@ class CmdOpen(ObjManipCommand): f"Created new Exit '{exit_name}' from {location.name} to" f" {destination.name}{string}." ) - exit_obj.at_object_constructed(caller) else: string = f"Error: Exit '{exit.name}' not created." # emit results diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index b222ac53d3..494de20dfa 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 inflect @@ -222,6 +223,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): {footer} """ + default_typeclasses = { + "object": settings.BASE_OBJECT_TYPECLASS, + "character": settings.BASE_CHARACTER_TYPECLASS, + "room": settings.BASE_ROOM_TYPECLASS, + "exit": settings.BASE_EXIT_TYPECLASS, + } + # on-object properties @lazy_property @@ -264,9 +272,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ return ( - self.db_account - and self.db_account.is_superuser - and not self.db_account.attributes.get("_quell") + self.db_account + and self.db_account.is_superuser + and not self.db_account.attributes.get("_quell") ) def contents_get(self, exclude=None, content_type=None): @@ -312,22 +320,22 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # main methods def search( - self, - searchdata, - global_search=False, - use_nicks=True, - typeclass=None, - location=None, - attribute_name=None, - quiet=False, - exact=False, - candidates=None, - use_locks=True, - nofound_string=None, - multimatch_string=None, - use_dbref=None, - tags=None, - stacked=0, + self, + searchdata, + global_search=False, + use_nicks=True, + typeclass=None, + location=None, + attribute_name=None, + quiet=False, + exact=False, + candidates=None, + use_locks=True, + nofound_string=None, + multimatch_string=None, + use_dbref=None, + tags=None, + stacked=0, ): """ Returns an Object matching a search string/condition @@ -431,10 +439,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): ) if global_search or ( - is_string - and searchdata.startswith("#") - and len(searchdata) > 1 - and searchdata[1:].isdigit() + is_string + and searchdata.startswith("#") + and len(searchdata) > 1 + and searchdata[1:].isdigit() ): # only allow exact matching if searching the entire database # or unique #dbrefs @@ -669,13 +677,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): func(obj, **kwargs) def msg_contents( - self, - text=None, - exclude=None, - from_obj=None, - mapping=None, - raise_funcparse_errors=False, - **kwargs, + self, + text=None, + exclude=None, + from_obj=None, + mapping=None, + raise_funcparse_errors=False, + **kwargs, ): """ Emits a message to all objects inside this object. @@ -788,15 +796,15 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): receiver.msg(text=(outmessage, outkwargs), from_obj=from_obj, **kwargs) def move_to( - self, - destination, - quiet=False, - emit_to_obj=None, - use_destination=True, - to_none=False, - move_hooks=True, - move_type="move", - **kwargs, + self, + destination, + quiet=False, + emit_to_obj=None, + use_destination=True, + to_none=False, + move_hooks=True, + move_type="move", + **kwargs, ): """ Moves this object to a new location. @@ -887,7 +895,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # check if source location lets us go try: if source_location and not source_location.at_pre_object_leave( - self, destination, **kwargs + self, destination, **kwargs ): return False except Exception as err: @@ -896,7 +904,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # check if destination accepts us try: if destination and not destination.at_pre_object_receive( - self, source_location, **kwargs + self, source_location, **kwargs ): return False except Exception as err: @@ -1012,7 +1020,8 @@ 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, creator: "DefaultObject" = None, method: str = "create", + **kwargs): """ Creates a basic object with default parameters, unless otherwise specified or extended. @@ -1021,11 +1030,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. + creator (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. @@ -1173,7 +1185,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): return True def access( - self, accessing_obj, access_type="read", default=False, no_superuser_bypass=False, **kwargs + self, accessing_obj, access_type="read", default=False, no_superuser_bypass=False, **kwargs ): """ Determines if another object has permission to access this object @@ -1598,37 +1610,42 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ pass - def can_build_object(self): + def get_object_typeclass(self, obj_type: str = "object", typeclass: str = None, method: str = "create", **kwargs) -> tuple[ + typing.Optional["Builder"], list[str]]: """ - This hook is called by the build command to determine if - self can build a new object. This is called before the - object is created. As it receives no arguments, it - can only be used to determine if self is allowed to build - anything in the current context. + 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. - For instance, a room may want to limit its number of exits, - or the builder might have an enforced quota limit for rooms - they can add to their custom dungeon. - - Returns: - can_build (str or None): If self is allowed to build objects. - return a string as the error if there is a problem. If - this returns True, the build will abort. - """ - pass - - def at_object_constructed(self, builder): - """ - Called when the object is constructed by a builder. - This is used to implement custom logic for building, - such as quota tracking systems, auto-tagging of rooms, - creation of logical groups of rooms like Zones or - dungeons, etc. + 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: - builder (Object): The object that constructed this object. + 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 "create". Others are "dig", "open", + "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.) """ - pass + + 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) 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, [] def at_pre_puppet(self, account, session=None, **kwargs): """ @@ -1696,7 +1713,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ pass - def at_server_reload(self): """ This hook is called whenever the server is shutting down for @@ -2112,8 +2128,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): obj for obj in obj_list if obj != looker - and obj.access(looker, "view") - and obj.access(looker, "search", default=True) + and obj.access(looker, "view") + and obj.access(looker, "search", default=True) ] return { @@ -2370,13 +2386,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): at_before_say = at_pre_say def at_say( - self, - message, - msg_self=None, - msg_location=None, - receivers=None, - msg_receivers=None, - **kwargs, + self, + message, + msg_self=None, + msg_location=None, + receivers=None, + msg_receivers=None, + **kwargs, ): """ Display the actual say (or whisper) of self. @@ -2752,7 +2768,6 @@ class DefaultCharacter(DefaultObject): if not self.sessions.count(): # only remove this char from grid if no sessions control it anymore. if self.location: - def message(obj, from_obj): obj.msg( _("{name} has left the game{reason}.").format( @@ -2814,7 +2829,8 @@ class DefaultRoom(DefaultObject): ) @classmethod - def create(cls, key, account=None, **kwargs): + def create(cls, key: str, account: "DefaultAccount" = None, creator: DefaultObject = None, method: str = "create", + **kwargs): """ Creates a basic Room with default parameters, unless otherwise specified or extended. @@ -2823,13 +2839,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 + creator (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. @@ -3020,7 +3038,8 @@ 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, creator: DefaultObject = None, + method: str = "create", **kwargs) -> tuple[typing.Optional["DefaultExit"], list[str]]: """ Creates a basic Exit with default parameters, unless otherwise specified or extended. @@ -3030,13 +3049,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. + creator (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. @@ -3059,8 +3079,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", "") From 5278ecb730889bd51711eee9c1a823f9472370f1 Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sat, 4 Nov 2023 17:40:52 -0400 Subject: [PATCH 3/4] Moved the new hook to ObjManipCommand --- evennia/commands/default/building.py | 110 ++++++++++++---- evennia/objects/objects.py | 186 ++++++++++++--------------- 2 files changed, 168 insertions(+), 128 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 82b6ff480f..cd799bae7d 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) + 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.""" @@ -597,19 +650,16 @@ class CmdCreate(ObjManipCommand): name = objdef["name"] aliases = objdef["aliases"] - obj_typeclass, errors = caller.get_object_typeclass(obj_type="object", typeclass=objdef["option"]) + 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, - creator=caller + name, caller, home=caller, aliases=aliases, report_to=caller, caller=caller ) if errors: self.msg(errors) @@ -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,7 +976,9 @@ class CmdDig(ObjManipCommand): location = caller.location # Create the new room - room_typeclass, errors = caller.get_object_typeclass(obj_type="room", typeclass=room["option"], method="dig") + 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: @@ -932,7 +986,11 @@ class CmdDig(ObjManipCommand): # create room new_room, errors = room_typeclass.create( - room["name"], aliases=room["aliases"], report_to=caller, creator=caller, method="dig" + room["name"], + aliases=room["aliases"], + report_to=caller, + caller=caller, + method=self.method_type, ) if errors: self.msg("|rError creating room:|n %s" % errors) @@ -943,9 +1001,7 @@ class CmdDig(ObjManipCommand): 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 {new_room}." - ) + room_string = f"Created room {new_room}({new_room.dbref}){alias_string} of type {new_room}." # create exit to room @@ -960,8 +1016,9 @@ 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 - exit_typeclass, errors = caller.get_object_typeclass(obj_type="exit", typeclass=to_exit["option"], - method="dig") + 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: @@ -973,8 +1030,8 @@ class CmdDig(ObjManipCommand): destination=new_room, aliases=to_exit["aliases"], report_to=caller, - creator=caller, - method="dig" + caller=caller, + method=self.method_type, ) if errors: self.msg("|rError creating exit:|n %s" % errors) @@ -999,8 +1056,9 @@ class CmdDig(ObjManipCommand): elif not location: exit_back_string = "\nYou cannot create an exit back to a None-location." else: - exit_typeclass, errors = caller.get_object_typeclass(obj_type="exit", typeclass=back_exit["option"], - method="dig") + 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: @@ -1011,8 +1069,8 @@ class CmdDig(ObjManipCommand): destination=location, aliases=back_exit["aliases"], report_to=caller, - creator=caller, - method="dig" + caller=caller, + method=self.method_type, ) if errors: self.msg("|rError creating exit:|n %s" % errors) @@ -1063,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"), @@ -1449,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 @@ -1495,7 +1557,9 @@ class CmdOpen(ObjManipCommand): else: # exit does not exist before. Create a new one. - exit_typeclass, errors = caller.get_object_typeclass(obj_type="exit", typeclass=typeclass, method="open") + 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: @@ -1505,8 +1569,8 @@ class CmdOpen(ObjManipCommand): location=location, aliases=exit_aliases, report_to=caller, - creator=caller, - method="open" + caller=caller, + method=self.method_type, ) if errors: self.msg("|rError creating exit:|n %s" % errors) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 494de20dfa..d40d5e92e0 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -222,14 +222,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): {exits}{characters}{things} {footer} """ - - default_typeclasses = { - "object": settings.BASE_OBJECT_TYPECLASS, - "character": settings.BASE_CHARACTER_TYPECLASS, - "room": settings.BASE_ROOM_TYPECLASS, - "exit": settings.BASE_EXIT_TYPECLASS, - } - # on-object properties @lazy_property @@ -272,9 +264,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ return ( - self.db_account - and self.db_account.is_superuser - and not self.db_account.attributes.get("_quell") + self.db_account + and self.db_account.is_superuser + and not self.db_account.attributes.get("_quell") ) def contents_get(self, exclude=None, content_type=None): @@ -320,22 +312,22 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # main methods def search( - self, - searchdata, - global_search=False, - use_nicks=True, - typeclass=None, - location=None, - attribute_name=None, - quiet=False, - exact=False, - candidates=None, - use_locks=True, - nofound_string=None, - multimatch_string=None, - use_dbref=None, - tags=None, - stacked=0, + self, + searchdata, + global_search=False, + use_nicks=True, + typeclass=None, + location=None, + attribute_name=None, + quiet=False, + exact=False, + candidates=None, + use_locks=True, + nofound_string=None, + multimatch_string=None, + use_dbref=None, + tags=None, + stacked=0, ): """ Returns an Object matching a search string/condition @@ -439,10 +431,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): ) if global_search or ( - is_string - and searchdata.startswith("#") - and len(searchdata) > 1 - and searchdata[1:].isdigit() + is_string + and searchdata.startswith("#") + and len(searchdata) > 1 + and searchdata[1:].isdigit() ): # only allow exact matching if searching the entire database # or unique #dbrefs @@ -677,13 +669,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): func(obj, **kwargs) def msg_contents( - self, - text=None, - exclude=None, - from_obj=None, - mapping=None, - raise_funcparse_errors=False, - **kwargs, + self, + text=None, + exclude=None, + from_obj=None, + mapping=None, + raise_funcparse_errors=False, + **kwargs, ): """ Emits a message to all objects inside this object. @@ -796,15 +788,15 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): receiver.msg(text=(outmessage, outkwargs), from_obj=from_obj, **kwargs) def move_to( - self, - destination, - quiet=False, - emit_to_obj=None, - use_destination=True, - to_none=False, - move_hooks=True, - move_type="move", - **kwargs, + self, + destination, + quiet=False, + emit_to_obj=None, + use_destination=True, + to_none=False, + move_hooks=True, + move_type="move", + **kwargs, ): """ Moves this object to a new location. @@ -895,7 +887,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # check if source location lets us go try: if source_location and not source_location.at_pre_object_leave( - self, destination, **kwargs + self, destination, **kwargs ): return False except Exception as err: @@ -904,7 +896,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # check if destination accepts us try: if destination and not destination.at_pre_object_receive( - self, source_location, **kwargs + self, source_location, **kwargs ): return False except Exception as err: @@ -1020,8 +1012,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): obj.move_to(home, move_type="teleport") @classmethod - def create(cls, key: str, account: "DefaultAccount" = None, creator: "DefaultObject" = None, method: str = "create", - **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. @@ -1034,7 +1032,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Keyword Args: account (Account): Account to attribute this object to. - creator (DefaultObject): The object which is creating this one. + 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". @@ -1185,7 +1183,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): return True def access( - self, accessing_obj, access_type="read", default=False, no_superuser_bypass=False, **kwargs + self, accessing_obj, access_type="read", default=False, no_superuser_bypass=False, **kwargs ): """ Determines if another object has permission to access this object @@ -1610,43 +1608,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ pass - def get_object_typeclass(self, obj_type: str = "object", typeclass: str = None, method: str = "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 "create". Others are "dig", "open", - "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) 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, [] - def at_pre_puppet(self, account, session=None, **kwargs): """ Called just before an Account connects to this object to puppet @@ -2128,8 +2089,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): obj for obj in obj_list if obj != looker - and obj.access(looker, "view") - and obj.access(looker, "search", default=True) + and obj.access(looker, "view") + and obj.access(looker, "search", default=True) ] return { @@ -2386,13 +2347,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): at_before_say = at_pre_say def at_say( - self, - message, - msg_self=None, - msg_location=None, - receivers=None, - msg_receivers=None, - **kwargs, + self, + message, + msg_self=None, + msg_location=None, + receivers=None, + msg_receivers=None, + **kwargs, ): """ Display the actual say (or whisper) of self. @@ -2768,6 +2729,7 @@ class DefaultCharacter(DefaultObject): if not self.sessions.count(): # only remove this char from grid if no sessions control it anymore. if self.location: + def message(obj, from_obj): obj.msg( _("{name} has left the game{reason}.").format( @@ -2829,8 +2791,14 @@ class DefaultRoom(DefaultObject): ) @classmethod - def create(cls, key: str, account: "DefaultAccount" = None, creator: DefaultObject = None, method: str = "create", - **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. @@ -2844,7 +2812,7 @@ class DefaultRoom(DefaultObject): 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 - creator (DefaultObject): The object which is creating this one. + 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". @@ -3038,8 +3006,16 @@ class DefaultExit(DefaultObject): # Command hooks @classmethod - def create(cls, key: str, location: DefaultRoom = None, destination: DefaultRoom = None, account: "DefaultAccount" = None, creator: DefaultObject = None, - method: str = "create", **kwargs) -> tuple[typing.Optional["DefaultExit"], list[str]]: + 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. @@ -3053,7 +3029,7 @@ class DefaultExit(DefaultObject): Keyword Args: account (obj): Account to associate this Exit with. - creator (ObjectDB): the Object creating this Object. + 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. From 5b1139cf1107b60913cf07dc9cab45311a4aea92 Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sun, 19 Nov 2023 13:55:42 -0500 Subject: [PATCH 4/4] Fixing the tests. --- evennia/commands/default/building.py | 2 +- evennia/commands/default/tests.py | 9 ++--- .../batchprocessor/example_batch_cmds_test.ev | 36 +++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 evennia/contrib/tutorials/batchprocessor/example_batch_cmds_test.ev diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index cd799bae7d..fd18fb28ae 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -203,7 +203,7 @@ class ObjManipCommand(COMMAND_DEFAULT_CLASS): try: type_class = ( - class_from_module(found_typeclass) + class_from_module(found_typeclass, settings.TYPECLASS_PATHS) if isinstance(found_typeclass, str) else found_typeclass ) 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).