Cleaned up refactor to support more extension.

This commit is contained in:
Andrew Bastien 2023-11-25 02:32:45 -05:00
parent e2a7c54e24
commit 97c73d133d
6 changed files with 271 additions and 95 deletions

View file

@ -272,6 +272,12 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
"""
# Determines which order command sets begin to be assembled from.
# Accounts are usually second.
cmd_order = 50
cmd_order_error = 0
cmd_type = "account"
objects = AccountManager()
# Used by account.create_character() to choose default typeclass for characters.
@ -309,6 +315,20 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
def characters(self):
return CharactersHandler(self)
def get_command_objects(self) -> dict[str, "CommandObject"]:
"""
Overrideable method which returns a dictionary of all the kinds of CommandObjects
linked to this Account.
In all normal cases, that's just the account itself.
The cmdhandler uses this to determine available cmdsets when executing a command.
Returns:
dict[str, CommandObject]: The CommandObjects linked to this Account.
"""
return {"account": self}
def at_post_add_character(self, character: "DefaultCharacter"):
"""
Called after a character is added to this account's list of playable characters.
@ -1514,17 +1534,35 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
def at_cmdset_get(self, **kwargs):
"""
Called just *before* cmdsets on this account are requested by
the command handler. The cmdsets are available as
`self.cmdset`. If changes need to be done on the fly to the
Called just before cmdsets on this object are requested by the
command handler. If changes need to be done on the fly to the
cmdset before passing them on to the cmdhandler, this is the
place to do it. This is called also if the account currently
have no cmdsets. kwargs are usually not used unless the
cmdset is generated dynamically.
place to do it. This is called also if the object currently
have no cmdsets.
Keyword Args:
caller (obj): The object requesting the cmdsets.
current (cmdset): The current merged cmdset.
force_init (bool): If `True`, force a re-build of the cmdset. (seems unused)
**kwargs: Arbitrary input for overloads.
"""
pass
def get_cmdsets(self, caller, current, **kwargs):
"""
Called by the CommandHandler to get a list of cmdsets to merge.
Args:
caller (obj): The object requesting the cmdsets.
current (cmdset): The current merged cmdset.
**kwargs: Arbitrary input for overloads.
Returns:
tuple: A tuple of (current, cmdsets), which is probably self.cmdset.current and self.cmdset.cmdset_stack
"""
return self.cmdset.current, list(self.cmdset.cmdset_stack)
def at_first_login(self, **kwargs):
"""
Called the very first time this account logs into the game.

View file

@ -41,6 +41,7 @@ from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.task import deferLater
from evennia.commands.command import InterruptCommand
from evennia.commands.cmdset import CmdSet
from evennia.utils import logger, utils
from evennia.utils.utils import string_suggestions
@ -280,10 +281,37 @@ class ErrorReported(Exception):
# Helper function
def generate_command_objects(called_by, session=None):
command_objects = dict()
command_objects.update(called_by.get_command_objects())
if session and session is not called_by:
command_objects.update(session.get_command_objects())
command_objects_list = list(command_objects.values())
command_objects_list.sort(key=lambda x: getattr(x, "cmd_order", 0))
# sort the dictionary by priority. This can be done because Python now cares about dictionary insert order.
command_objects = {c.cmd_type: c for c in command_objects_list}
if not command_objects:
raise RuntimeError("cmdhandler: no command objects found.")
# the caller will be the one to receive messages and excert its permissions.
# we assign the caller with preference 'bottom up'
caller = command_objects_list[-1]
command_objects_list_error = sorted(
command_objects_list, key=lambda x: getattr(x, "cmd_order_error", 0)
)
# The error_to is the default recipient for errors. Tries to make sure an account
# does not get spammed for errors while preserving character mirroring.
error_to = command_objects_list_error[-1]
return command_objects, command_objects_list, command_objects_list_error, caller, error_to
@inlineCallbacks
def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string):
def get_and_merge_cmdsets(caller, command_objects, callertype, raw_string, report_to=None):
"""
Gather all relevant cmdsets and merge them.
@ -293,12 +321,11 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
when the user is not logged in, this will be a Session, when being OOC
it will be an Account and when puppeting an object this will (often) be
a Character Object. In the end it depends on where the cmdset is stored.
session (Session or None): The Session associated with caller, if any.
account (Account or None): The calling Account associated with caller, if any.
obj (Object or None): The Object associated with caller, if any.
command_objects (list): A list of sorted objects which provide cmdsets.
callertype (str): This identifies caller as either "account", "object" or "session"
to avoid having to do this check internally.
raw_string (str): The input string. This is only used for error reporting.
report_to (Object, optional): If given, this object will receive error messages
Returns:
cmdset (Deferred): This deferred fires with the merged cmdset
@ -366,78 +393,47 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
raise ErrorReported(raw_string)
@inlineCallbacks
def _get_cmdsets(obj):
def _get_cmdsets(obj, current):
"""
Helper method; Get cmdset while making sure to trigger all
hooks safely. Returns the stack and the valid options.
"""
try:
yield obj.at_cmdset_get()
yield obj.at_cmdset_get(caller=caller, current=current)
except Exception:
_msg_err(caller, _ERROR_CMDSETS)
raise ErrorReported(raw_string)
try:
returnValue((obj.cmdset.current, list(obj.cmdset.cmdset_stack)))
returnValue(obj.get_cmdsets(caller=caller, current=current))
except AttributeError:
returnValue(((None, None, None), []))
local_obj_cmdsets = []
if callertype == "session":
# we are calling the command from the session level
report_to = session
current, cmdsets = yield _get_cmdsets(session)
if account: # this automatically implies logged-in
pcurrent, account_cmdsets = yield _get_cmdsets(account)
cmdsets += account_cmdsets
current = current + pcurrent
if obj:
ocurrent, obj_cmdsets = yield _get_cmdsets(obj)
current = current + ocurrent
cmdsets += obj_cmdsets
current_cmdset = CmdSet()
object_cmdsets = list()
for cmdobj in command_objects:
current, cur_cmdsets = yield _get_cmdsets(cmdobj, current_cmdset)
if current:
current_cmdset = current_cmdset + current
if cur_cmdsets:
object_cmdsets += cur_cmdsets
match cmdobj.cmd_type:
case "object":
if not current.no_objs:
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj)
local_obj_cmdsets = yield _get_local_obj_cmdsets(cmdobj)
if current.no_exits:
# filter out all exits
local_obj_cmdsets = [
cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
]
cmdsets += local_obj_cmdsets
elif callertype == "account":
# we are calling the command from the account level
report_to = account
current, cmdsets = yield _get_cmdsets(account)
if obj:
ocurrent, obj_cmdsets = yield _get_cmdsets(obj)
current = current + ocurrent
cmdsets += obj_cmdsets
if not current.no_objs:
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj)
if current.no_exits:
# filter out all exits
local_obj_cmdsets = [
cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
]
cmdsets += local_obj_cmdsets
elif callertype == "object":
# we are calling the command from the object level
report_to = obj
current, cmdsets = yield _get_cmdsets(obj)
if not current.no_objs:
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj)
if current.no_exits:
# filter out all exits
local_obj_cmdsets = [
cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
]
cmdsets += yield local_obj_cmdsets
else:
raise Exception("get_and_merge_cmdsets: callertype %s is not valid." % callertype)
object_cmdsets += local_obj_cmdsets
# weed out all non-found sets
cmdsets = yield [cmdset for cmdset in cmdsets if cmdset and cmdset.key != "_EMPTY_CMDSET"]
cmdsets = yield [
cmdset for cmdset in object_cmdsets if cmdset and cmdset.key != "_EMPTY_CMDSET"
]
# report cmdset errors to user (these should already have been logged)
yield [
report_to.msg(cmdset.errmessage) for cmdset in cmdsets if cmdset.key == "_CMDSET_ERROR"
@ -552,7 +548,7 @@ def cmdhandler(
"""
@inlineCallbacks
def _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account):
def _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account, command_objects):
"""
Helper function: This initializes and runs the Command
instance once the parser has identified it as either a normal
@ -568,6 +564,7 @@ def cmdhandler(
cmdset (CmdSet): Command sert the command belongs to (if any)..
session (Session): Session of caller (if any).
account (Account): Account of caller (if any).
command_objects (dict): Dictionary of all command objects.
Returns:
deferred (Deferred): this will fire with the return of the
@ -586,6 +583,7 @@ def cmdhandler(
cmd.cmdstring = cmdname # deprecated
cmd.args = args
cmd.cmdset = cmdset
cmd.command_objects = command_objects.copy()
cmd.session = session
cmd.account = account
cmd.raw_string = unformatted_raw_string
@ -655,25 +653,15 @@ def cmdhandler(
finally:
_COMMAND_NESTING[called_by] -= 1
session, account, obj = session, None, None
if callertype == "session":
session = called_by
account = session.account
obj = session.puppet
elif callertype == "account":
account = called_by
if session:
obj = yield session.puppet
elif callertype == "object":
obj = called_by
else:
raise RuntimeError("cmdhandler: callertype %s is not valid." % callertype)
# the caller will be the one to receive messages and excert its permissions.
# we assign the caller with preference 'bottom up'
caller = obj or account or session
# The error_to is the default recipient for errors. Tries to make sure an account
# does not get spammed for errors while preserving character mirroring.
error_to = obj or session or account
(
command_objects,
command_objects_list,
command_objects_list_error,
caller,
error_to,
) = generate_command_objects(called_by, session=session)
account = command_objects.get("account", None)
try: # catch bugs in cmdhandler itself
try: # catch special-type commands
@ -691,7 +679,7 @@ def cmdhandler(
else:
# no explicit cmdobject given, figure it out
cmdset = yield get_and_merge_cmdsets(
caller, session, account, obj, callertype, raw_string
caller, command_objects_list, callertype, raw_string
)
if not cmdset:
# this is bad and shouldn't happen.
@ -764,7 +752,9 @@ def cmdhandler(
cmd = copy(cmd)
# A normal command.
ret = yield _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account)
ret = yield _run_command(
cmd, cmdname, args, raw_cmdname, cmdset, session, account, command_objects
)
returnValue(ret)
except ErrorReported as exc:
@ -780,7 +770,14 @@ def cmdhandler(
if syscmd:
ret = yield _run_command(
syscmd, syscmd.key, sysarg, unformatted_raw_string, cmdset, session, account
syscmd,
syscmd.key,
sysarg,
unformatted_raw_string,
cmdset,
session,
account,
command_objects,
)
returnValue(ret)
elif sysarg:

View file

@ -10,7 +10,7 @@ from django.db.models import Max, Min, Q
import evennia
from evennia import InterruptCommand
from evennia.commands.cmdhandler import get_and_merge_cmdsets
from evennia.commands.cmdhandler import get_and_merge_cmdsets, generate_command_objects
from evennia.locks.lockhandler import LockException
from evennia.objects.models import ObjectDB
from evennia.prototypes import menus as olc_menus
@ -3122,8 +3122,16 @@ class CmdExamine(ObjManipCommand):
def _get_cmdset_callback(current_cmdset):
self.msg(self.format_output(obj, current_cmdset).strip())
(
command_objects,
command_objects_list,
command_objects_list_error,
caller,
error_to,
) = generate_command_objects(obj, session=session)
get_and_merge_cmdsets(
obj, session, account, objct, mergemode, self.raw_string
obj, command_objects_list, mergemode, self.raw_string, error_to
).addCallback(_get_cmdset_callback)
else:

View file

@ -1020,8 +1020,16 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest):
a = self.cmdset_a
a.no_channels = True
self.set_cmdsets(self.session, a)
(
command_objects,
command_objects_list,
command_objects_list_error,
caller,
error_to,
) = cmdhandler.generate_command_objects(self.session)
deferred = cmdhandler.get_and_merge_cmdsets(
self.session, self.session, None, None, "session", ""
self.session, [self.session], "session", "", error_to
)
def _callback(cmdset):
@ -1036,8 +1044,16 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest):
a = self.cmdset_a
a.no_channels = True
self.set_cmdsets(self.account, a)
(
command_objects,
command_objects_list,
command_objects_list_error,
caller,
error_to,
) = cmdhandler.generate_command_objects(self.account)
deferred = cmdhandler.get_and_merge_cmdsets(
self.account, None, self.account, None, "account", ""
self.account, command_objects_list, "account", "", error_to
)
# get_and_merge_cmdsets converts to lower-case internally.
@ -1053,7 +1069,17 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest):
def test_from_object(self):
self.set_cmdsets(self.obj1, self.cmdset_a)
deferred = cmdhandler.get_and_merge_cmdsets(self.obj1, None, None, self.obj1, "object", "")
(
command_objects,
command_objects_list,
command_objects_list_error,
caller,
error_to,
) = cmdhandler.generate_command_objects(self.obj1)
deferred = cmdhandler.get_and_merge_cmdsets(
self.obj1, command_objects_list, "object", "", error_to
)
# get_and_merge_cmdsets converts to lower-case internally.
def _callback(cmdset):
@ -1069,8 +1095,16 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest):
a.no_exits = True
a.no_channels = True
self.set_cmdsets(self.obj1, a, b, c, d)
deferred = cmdhandler.get_and_merge_cmdsets(self.obj1, None, None, self.obj1, "object", "")
(
command_objects,
command_objects_list,
command_objects_list_error,
caller,
error_to,
) = cmdhandler.generate_command_objects(self.obj1)
deferred = cmdhandler.get_and_merge_cmdsets(
self.obj1, command_objects_list, "object", "", error_to
)
def _callback(cmdset):
self.assertTrue(cmdset.no_exits)
@ -1087,7 +1121,17 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest):
b.duplicates = True
d.duplicates = True
self.set_cmdsets(self.obj1, a, b, c, d)
deferred = cmdhandler.get_and_merge_cmdsets(self.obj1, None, None, self.obj1, "object", "")
(
command_objects,
command_objects_list,
command_objects_list_error,
caller,
error_to,
) = cmdhandler.generate_command_objects(self.obj1, session=None)
deferred = cmdhandler.get_and_merge_cmdsets(
self.obj1, command_objects_list, "object", "", error_to
)
def _callback(cmdset):
self.assertEqual(len(cmdset.commands), 9)

View file

@ -204,6 +204,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
"""
# Determines which order command sets begin to be assembled from.
# Objects are usually third.
cmd_order = 100
cmd_order_error = 100
cmd_type = "object"
# Used for sorting / filtering in inventories / room contents.
_content_types = ("object",)
@ -256,6 +262,24 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
"""
return self.sessions.count()
def get_command_objects(self) -> dict[str, "CommandObject"]:
"""
Overrideable method which returns a dictionary of all the kinds of CommandObjects
linked to this Object.
In all normal cases, that's the Object itself, and maybe an Account if the Object
is being puppeted.
The cmdhandler uses this to determine available cmdsets when executing a command.
Returns:
dict[str, CommandObject]: The CommandObjects linked to this Object.
"""
out = {"object": self}
if self.account:
out["account"] = self.account
return out
@property
def is_superuser(self):
"""
@ -1601,12 +1625,28 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
have no cmdsets.
Keyword Args:
caller (Session, Object or Account): The caller requesting
this cmdset.
caller (obj): The object requesting the cmdsets.
current (cmdset): The current merged cmdset.
force_init (bool): If `True`, force a re-build of the cmdset. (seems unused)
**kwargs: Arbitrary input for overloads.
"""
pass
def get_cmdsets(self, caller, current, **kwargs):
"""
Called by the CommandHandler to get a list of cmdsets to merge.
Args:
caller (obj): The object requesting the cmdsets.
current (cmdset): The current merged cmdset.
**kwargs: Arbitrary input for overloads.
Returns:
tuple: A tuple of (current, cmdsets), which is probably self.cmdset.current and self.cmdset.cmdset_stack
"""
return self.cmdset.current, list(self.cmdset.cmdset_stack)
def at_pre_puppet(self, account, session=None, **kwargs):
"""
Called just before an Account connects to this object to puppet

View file

@ -46,6 +46,12 @@ class ServerSession(_BASE_SESSION_CLASS):
"""
# Determines which order command sets begin to be assembled from.
# Sessions are usually first.
cmd_order = 0
cmd_order_error = 50
cmd_type = "session"
def __init__(self):
"""
Initiate to avoid AttributeErrors down the line
@ -64,6 +70,26 @@ class ServerSession(_BASE_SESSION_CLASS):
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set)
def get_command_objects(self) -> dict[str, "CommandObject"]:
"""
Overrideable method which returns a dictionary of all the kinds of CommandObjects
linked to this ServerSession.
In all normal cases, that's the Session itself, and possibly an account and puppeted
object.
The cmdhandler uses this to determine available cmdsets when executing a command.
Returns:
dict[str, CommandObject]: The CommandObjects linked to this Object.
"""
out = {"session": self}
if self.account:
out["account"] = self.account
if self.puppet:
out["puppet"] = self.puppet
return out
@property
def id(self):
return self.sessid
@ -376,12 +402,35 @@ class ServerSession(_BASE_SESSION_CLASS):
def at_cmdset_get(self, **kwargs):
"""
A dummy hook all objects with cmdsets need to have
Called just before cmdsets on this object are requested by the
command handler. If changes need to be done on the fly to the
cmdset before passing them on to the cmdhandler, this is the
place to do it. This is called also if the object currently
have no cmdsets.
Keyword Args:
caller (obj): The object requesting the cmdsets.
current (cmdset): The current merged cmdset.
force_init (bool): If `True`, force a re-build of the cmdset. (seems unused)
**kwargs: Arbitrary input for overloads.
"""
pass
def get_cmdsets(self, caller, current, **kwargs):
"""
Called by the CommandHandler to get a list of cmdsets to merge.
Args:
caller (obj): The object requesting the cmdsets.
current (cmdset): The current merged cmdset.
**kwargs: Arbitrary input for overloads.
Returns:
tuple: A tuple of (current, cmdsets), which is probably self.cmdset.current and self.cmdset.cmdset_stack
"""
return self.cmdset.current, list(self.cmdset.cmdset_stack)
# Mock db/ndb properties for allowing easy storage on the session
# (note that no databse is involved at all here. session.db.attr =
# value just saves a normal property in memory, just like ndb).