Merge pull request #3180 from volundmush/dig_hook

Building Command Hook Improvements
This commit is contained in:
Griatch 2023-11-23 18:36:25 +01:00 committed by GitHub
commit 80a17cab87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 230 additions and 73 deletions

View file

@ -2,6 +2,7 @@
Building and world design commands Building and world design commands
""" """
import re import re
import typing
from django.conf import settings from django.conf import settings
from django.core.paginator import Paginator 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 # OBS - this is just a parent - it's not intended to actually be
# included in a commandset on its own! # 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): def parse(self):
""" """
We need to expand the default parsing to get all 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.lhs_objattr = obj_attrs[0]
self.rhs_objattr = obj_attrs[1] 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): class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
""" """
@ -194,6 +245,8 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
locks = "cmd:perm(setobjalias) or perm(Builder)" locks = "cmd:perm(setobjalias) or perm(Builder)"
help_category = "Building" help_category = "Building"
method_type = "cmd_create"
def func(self): def func(self):
"""Set the aliases.""" """Set the aliases."""
@ -579,10 +632,6 @@ class CmdCreate(ObjManipCommand):
locks = "cmd:perm(create) or perm(Builder)" locks = "cmd:perm(create) or perm(Builder)"
help_category = "Building" 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): def func(self):
""" """
Creates the object. Creates the object.
@ -600,22 +649,23 @@ class CmdCreate(ObjManipCommand):
string = "" string = ""
name = objdef["name"] name = objdef["name"]
aliases = objdef["aliases"] aliases = objdef["aliases"]
typeclass = objdef["option"]
# create object (if not a valid typeclass, the default obj_typeclass, errors = self.get_object_typeclass(
# object typeclass will automatically be used) obj_type="object", typeclass=objdef["option"]
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,
) )
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: if not obj:
continue continue
if aliases: if aliases:
string = ( string = (
f"You create a new {obj.typename}: {obj.name} (aliases: {', '.join(aliases)})." 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)" locks = "cmd:perm(dig) or perm(Builder)"
help_category = "Building" help_category = "Building"
method_type = "cmd_dig"
# lockstring of newly created rooms, for easy overloading. # lockstring of newly created rooms, for easy overloading.
# Will be formatted with the {id} of the creating object. # Will be formatted with the {id} of the creating object.
new_room_lockstring = ( new_room_lockstring = (
@ -924,22 +976,32 @@ class CmdDig(ObjManipCommand):
location = caller.location location = caller.location
# Create the new room # Create the new room
typeclass = room["option"] room_typeclass, errors = self.get_object_typeclass(
if not typeclass: obj_type="room", typeclass=room["option"], method=self.method_type
typeclass = settings.BASE_ROOM_TYPECLASS )
if errors:
self.msg("|rError creating room:|n %s" % errors)
if not room_typeclass:
return
# create room # create room
new_room = create.create_object( new_room, errors = room_typeclass.create(
typeclass, room["name"], aliases=room["aliases"], report_to=caller room["name"],
aliases=room["aliases"],
report_to=caller,
caller=caller,
method=self.method_type,
) )
lockstring = self.new_room_lockstring.format(id=caller.id) if errors:
new_room.locks.add(lockstring) self.msg("|rError creating room:|n %s" % errors)
if not new_room:
return
alias_string = "" alias_string = ""
if new_room.aliases.all(): if new_room.aliases.all():
alias_string = " (%s)" % ", ".join(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 # create exit to room
@ -954,19 +1016,28 @@ class CmdDig(ObjManipCommand):
exit_to_string = "\nYou cannot create an exit from a None-location." exit_to_string = "\nYou cannot create an exit from a None-location."
else: else:
# Build the exit to the new room from the current one # Build the exit to the new room from the current one
typeclass = to_exit["option"] exit_typeclass, errors = self.get_object_typeclass(
if not typeclass: obj_type="exit", typeclass=to_exit["option"], method=self.method_type
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,
) )
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 = "" alias_string = ""
if new_to_exit.aliases.all(): if new_to_exit.aliases.all():
alias_string = " (%s)" % ", ".join(new_to_exit.aliases.all()) alias_string = " (%s)" % ", ".join(new_to_exit.aliases.all())
@ -985,18 +1056,26 @@ class CmdDig(ObjManipCommand):
elif not location: elif not location:
exit_back_string = "\nYou cannot create an exit back to a None-location." exit_back_string = "\nYou cannot create an exit back to a None-location."
else: else:
typeclass = back_exit["option"] exit_typeclass, errors = self.get_object_typeclass(
if not typeclass: obj_type="exit", typeclass=back_exit["option"], method=self.method_type
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,
) )
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 = "" alias_string = ""
if new_back_exit.aliases.all(): if new_back_exit.aliases.all():
alias_string = " (%s)" % ", ".join(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)" locks = "cmd: perm(tunnel) or perm(Builder)"
help_category = "Building" help_category = "Building"
method_type = "cmd_tunnel"
# store the direction, full name and its opposite # store the direction, full name and its opposite
directions = { directions = {
"n": ("north", "s"), "n": ("north", "s"),
@ -1428,6 +1509,8 @@ class CmdOpen(ObjManipCommand):
locks = "cmd:perm(open) or perm(Builder)" locks = "cmd:perm(open) or perm(Builder)"
help_category = "Building" help_category = "Building"
method_type = "cmd_open"
new_obj_lockstring = "control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)" 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 # a custom member method to chug out exits and do checks
@ -1474,17 +1557,25 @@ class CmdOpen(ObjManipCommand):
else: else:
# exit does not exist before. Create a new one. # exit does not exist before. Create a new one.
lockstring = self.new_obj_lockstring.format(id=caller.id) exit_typeclass, errors = self.get_object_typeclass(
if not typeclass: obj_type="exit", typeclass=typeclass, method=self.method_type
typeclass = settings.BASE_EXIT_TYPECLASS )
exit_obj = create.create_object( if errors:
typeclass, self.msg("|rError creating exit:|n %s" % errors)
key=exit_name, if not exit_typeclass:
return
exit_obj, errors = exit_typeclass.create(
exit_name,
location=location, location=location,
aliases=exit_aliases, aliases=exit_aliases,
locks=lockstring,
report_to=caller, 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: if exit_obj:
# storing a destination is what makes it an exit! # storing a destination is what makes it an exit!
exit_obj.destination = destination exit_obj.destination = destination

View file

@ -725,16 +725,17 @@ class TestAccount(BaseEvenniaCommandTest):
class TestBuilding(BaseEvenniaCommandTest): class TestBuilding(BaseEvenniaCommandTest):
def test_create(self): def test_create(self):
name = settings.BASE_OBJECT_TYPECLASS.rsplit(".", 1)[1] typeclass = settings.BASE_OBJECT_TYPECLASS
name = typeclass.rsplit(".", 1)[1]
self.call( self.call(
building.CmdCreate(), 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, "You create a new %s: TestObj1." % name,
) )
self.call(building.CmdCreate(), "", "Usage: ") self.call(building.CmdCreate(), "", "Usage: ")
self.call( self.call(
building.CmdCreate(), building.CmdCreate(),
"TestObj1;foo;bar", f"TestObj1;foo;bar:{typeclass}",
"You create a new %s: TestObj1 (aliases: foo, bar)." % name, "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 # cannot test batchcode here, it must run inside the server process
self.call( self.call(
batchprocess.CmdBatchCommands(), batchprocess.CmdBatchCommands(),
"batchprocessor.example_batch_cmds", "batchprocessor.example_batch_cmds_test",
"Running Batch-command processor - Automatic mode for" "Running Batch-command processor - Automatic mode for"
" batchprocessor.example_batch_cmds", " batchprocessor.example_batch_cmds",
) )

View file

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

View file

@ -8,6 +8,7 @@ This is the v1.0 develop version (for ref in doc building).
""" """
import time import time
import typing
from collections import defaultdict from collections import defaultdict
import typing import typing
@ -222,7 +223,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
{exits}{characters}{things} {exits}{characters}{things}
{footer} {footer}
""" """
# on-object properties # on-object properties
@lazy_property @lazy_property
@ -1013,7 +1013,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
obj.move_to(home, move_type="teleport") obj.move_to(home, move_type="teleport")
@classmethod @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 Creates a basic object with default parameters, unless otherwise
specified or extended. specified or extended.
@ -1022,11 +1029,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
Args: Args:
key (str): Name of the new object. key (str): Name of the new object.
account (Account): Account to attribute this object to.
Keyword Args: 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. description (str): Brief description for this object.
ip (str): IP address of creator (for object auditing). ip (str): IP address of creator (for object auditing).
method (str): The method of creation. Defaults to "create".
Returns: Returns:
object (Object): A newly created object of the given typeclass. object (Object): A newly created object of the given typeclass.
@ -2786,7 +2796,14 @@ class DefaultRoom(DefaultObject):
) )
@classmethod @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 Creates a basic Room with default parameters, unless otherwise
specified or extended. specified or extended.
@ -2795,13 +2812,15 @@ class DefaultRoom(DefaultObject):
Args: Args:
key (str): Name of the new Room. 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: 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. description (str): Brief description for this object.
ip (str): IP address of creator (for object auditing). ip (str): IP address of creator (for object auditing).
method (str): The method used to create the room. Defaults to "create".
Returns: Returns:
room (Object): A newly created Room of the given typeclass. room (Object): A newly created Room of the given typeclass.
@ -2992,7 +3011,16 @@ class DefaultExit(DefaultObject):
# Command hooks # Command hooks
@classmethod @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 Creates a basic Exit with default parameters, unless otherwise
specified or extended. specified or extended.
@ -3002,13 +3030,14 @@ class DefaultExit(DefaultObject):
Args: Args:
key (str): Name of the new Exit, as it should appear from the key (str): Name of the new Exit, as it should appear from the
source room. source room.
account (obj): Account to associate this Exit with. location (Room): The room to create this exit in.
source (Room): The room to create this exit in.
dest (Room): The room to which this exit should go.
Keyword Args: Keyword Args:
account (obj): Account to associate this Exit with.
caller (ObjectDB): the Object creating this Object.
description (str): Brief description for this object. description (str): Brief description for this object.
ip (str): IP address of creator (for object auditing). ip (str): IP address of creator (for object auditing).
destination (Room): The room to which this exit should go.
Returns: Returns:
exit (Object): A newly created Room of the given typeclass. 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) kwargs["report_to"] = kwargs.pop("report_to", account)
# Set to/from rooms # Set to/from rooms
kwargs["location"] = source kwargs["location"] = location
kwargs["destination"] = dest kwargs["destination"] = destination
description = kwargs.pop("description", "") description = kwargs.pop("description", "")