From 0da7f962c2d15084b88409b1a40aaeafdaf89bde Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Wed, 1 Nov 2023 17:35:51 -0400 Subject: [PATCH] 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", "")