Trying a new approach. Introduced DefaultObject.get_object_typeclass() and cleaned up .create() hooks. Building commands now use the new logic.

This commit is contained in:
Andrew Bastien 2023-11-01 17:35:51 -04:00
parent 7746ff1663
commit 0da7f962c2
2 changed files with 178 additions and 150 deletions

View file

@ -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

View file

@ -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", "")