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() objects = AccountManager()
# Used by account.create_character() to choose default typeclass for characters. # Used by account.create_character() to choose default typeclass for characters.
@ -309,6 +315,20 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
def characters(self): def characters(self):
return CharactersHandler(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"): def at_post_add_character(self, character: "DefaultCharacter"):
""" """
Called after a character is added to this account's list of playable characters. 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): def at_cmdset_get(self, **kwargs):
""" """
Called just *before* cmdsets on this account are requested by Called just before cmdsets on this object are requested by the
the command handler. The cmdsets are available as command handler. If changes need to be done on the fly to the
`self.cmdset`. If changes need to be done on the fly to the
cmdset before passing them on to the cmdhandler, this is the cmdset before passing them on to the cmdhandler, this is the
place to do it. This is called also if the account currently place to do it. This is called also if the object currently
have no cmdsets. kwargs are usually not used unless the have no cmdsets.
cmdset is generated dynamically.
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 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): def at_first_login(self, **kwargs):
""" """
Called the very first time this account logs into the game. 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 twisted.internet.task import deferLater
from evennia.commands.command import InterruptCommand from evennia.commands.command import InterruptCommand
from evennia.commands.cmdset import CmdSet
from evennia.utils import logger, utils from evennia.utils import logger, utils
from evennia.utils.utils import string_suggestions from evennia.utils.utils import string_suggestions
@ -280,10 +281,37 @@ class ErrorReported(Exception):
# Helper function # 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 @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. 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 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 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. 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. command_objects (list): A list of sorted objects which provide cmdsets.
account (Account or None): The calling Account associated with caller, if any.
obj (Object or None): The Object associated with caller, if any.
callertype (str): This identifies caller as either "account", "object" or "session" callertype (str): This identifies caller as either "account", "object" or "session"
to avoid having to do this check internally. to avoid having to do this check internally.
raw_string (str): The input string. This is only used for error reporting. 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: Returns:
cmdset (Deferred): This deferred fires with the merged cmdset 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) raise ErrorReported(raw_string)
@inlineCallbacks @inlineCallbacks
def _get_cmdsets(obj): def _get_cmdsets(obj, current):
""" """
Helper method; Get cmdset while making sure to trigger all Helper method; Get cmdset while making sure to trigger all
hooks safely. Returns the stack and the valid options. hooks safely. Returns the stack and the valid options.
""" """
try: try:
yield obj.at_cmdset_get() yield obj.at_cmdset_get(caller=caller, current=current)
except Exception: except Exception:
_msg_err(caller, _ERROR_CMDSETS) _msg_err(caller, _ERROR_CMDSETS)
raise ErrorReported(raw_string) raise ErrorReported(raw_string)
try: try:
returnValue((obj.cmdset.current, list(obj.cmdset.cmdset_stack))) returnValue(obj.get_cmdsets(caller=caller, current=current))
except AttributeError: except AttributeError:
returnValue(((None, None, None), [])) returnValue(((None, None, None), []))
local_obj_cmdsets = [] local_obj_cmdsets = []
if callertype == "session":
# we are calling the command from the session level current_cmdset = CmdSet()
report_to = session object_cmdsets = list()
current, cmdsets = yield _get_cmdsets(session) for cmdobj in command_objects:
if account: # this automatically implies logged-in current, cur_cmdsets = yield _get_cmdsets(cmdobj, current_cmdset)
pcurrent, account_cmdsets = yield _get_cmdsets(account) if current:
cmdsets += account_cmdsets current_cmdset = current_cmdset + current
current = current + pcurrent if cur_cmdsets:
if obj: object_cmdsets += cur_cmdsets
ocurrent, obj_cmdsets = yield _get_cmdsets(obj) match cmdobj.cmd_type:
current = current + ocurrent case "object":
cmdsets += obj_cmdsets
if not current.no_objs: 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: if current.no_exits:
# filter out all exits # filter out all exits
local_obj_cmdsets = [ local_obj_cmdsets = [
cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet" cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
] ]
cmdsets += local_obj_cmdsets object_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)
# weed out all non-found sets # 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) # report cmdset errors to user (these should already have been logged)
yield [ yield [
report_to.msg(cmdset.errmessage) for cmdset in cmdsets if cmdset.key == "_CMDSET_ERROR" report_to.msg(cmdset.errmessage) for cmdset in cmdsets if cmdset.key == "_CMDSET_ERROR"
@ -552,7 +548,7 @@ def cmdhandler(
""" """
@inlineCallbacks @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 Helper function: This initializes and runs the Command
instance once the parser has identified it as either a normal 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).. cmdset (CmdSet): Command sert the command belongs to (if any)..
session (Session): Session of caller (if any). session (Session): Session of caller (if any).
account (Account): Account of caller (if any). account (Account): Account of caller (if any).
command_objects (dict): Dictionary of all command objects.
Returns: Returns:
deferred (Deferred): this will fire with the return of the deferred (Deferred): this will fire with the return of the
@ -586,6 +583,7 @@ def cmdhandler(
cmd.cmdstring = cmdname # deprecated cmd.cmdstring = cmdname # deprecated
cmd.args = args cmd.args = args
cmd.cmdset = cmdset cmd.cmdset = cmdset
cmd.command_objects = command_objects.copy()
cmd.session = session cmd.session = session
cmd.account = account cmd.account = account
cmd.raw_string = unformatted_raw_string cmd.raw_string = unformatted_raw_string
@ -655,25 +653,15 @@ def cmdhandler(
finally: finally:
_COMMAND_NESTING[called_by] -= 1 _COMMAND_NESTING[called_by] -= 1
session, account, obj = session, None, None (
if callertype == "session": command_objects,
session = called_by command_objects_list,
account = session.account command_objects_list_error,
obj = session.puppet caller,
elif callertype == "account": error_to,
account = called_by ) = generate_command_objects(called_by, session=session)
if session:
obj = yield session.puppet account = command_objects.get("account", None)
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
try: # catch bugs in cmdhandler itself try: # catch bugs in cmdhandler itself
try: # catch special-type commands try: # catch special-type commands
@ -691,7 +679,7 @@ def cmdhandler(
else: else:
# no explicit cmdobject given, figure it out # no explicit cmdobject given, figure it out
cmdset = yield get_and_merge_cmdsets( cmdset = yield get_and_merge_cmdsets(
caller, session, account, obj, callertype, raw_string caller, command_objects_list, callertype, raw_string
) )
if not cmdset: if not cmdset:
# this is bad and shouldn't happen. # this is bad and shouldn't happen.
@ -764,7 +752,9 @@ def cmdhandler(
cmd = copy(cmd) cmd = copy(cmd)
# A normal command. # 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) returnValue(ret)
except ErrorReported as exc: except ErrorReported as exc:
@ -780,7 +770,14 @@ def cmdhandler(
if syscmd: if syscmd:
ret = yield _run_command( 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) returnValue(ret)
elif sysarg: elif sysarg:

View file

@ -10,7 +10,7 @@ from django.db.models import Max, Min, Q
import evennia import evennia
from evennia import InterruptCommand 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.locks.lockhandler import LockException
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.prototypes import menus as olc_menus from evennia.prototypes import menus as olc_menus
@ -3122,8 +3122,16 @@ class CmdExamine(ObjManipCommand):
def _get_cmdset_callback(current_cmdset): def _get_cmdset_callback(current_cmdset):
self.msg(self.format_output(obj, current_cmdset).strip()) 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( 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) ).addCallback(_get_cmdset_callback)
else: else:

View file

@ -1020,8 +1020,16 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest):
a = self.cmdset_a a = self.cmdset_a
a.no_channels = True a.no_channels = True
self.set_cmdsets(self.session, a) 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( deferred = cmdhandler.get_and_merge_cmdsets(
self.session, self.session, None, None, "session", "" self.session, [self.session], "session", "", error_to
) )
def _callback(cmdset): def _callback(cmdset):
@ -1036,8 +1044,16 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest):
a = self.cmdset_a a = self.cmdset_a
a.no_channels = True a.no_channels = True
self.set_cmdsets(self.account, a) 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( 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. # get_and_merge_cmdsets converts to lower-case internally.
@ -1053,7 +1069,17 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest):
def test_from_object(self): def test_from_object(self):
self.set_cmdsets(self.obj1, self.cmdset_a) 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. # get_and_merge_cmdsets converts to lower-case internally.
def _callback(cmdset): def _callback(cmdset):
@ -1069,8 +1095,16 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest):
a.no_exits = True a.no_exits = True
a.no_channels = True a.no_channels = True
self.set_cmdsets(self.obj1, a, b, c, d) 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): def _callback(cmdset):
self.assertTrue(cmdset.no_exits) self.assertTrue(cmdset.no_exits)
@ -1087,7 +1121,17 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest):
b.duplicates = True b.duplicates = True
d.duplicates = True d.duplicates = True
self.set_cmdsets(self.obj1, a, b, c, d) 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): def _callback(cmdset):
self.assertEqual(len(cmdset.commands), 9) 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. # Used for sorting / filtering in inventories / room contents.
_content_types = ("object",) _content_types = ("object",)
@ -256,6 +262,24 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
""" """
return self.sessions.count() 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 @property
def is_superuser(self): def is_superuser(self):
""" """
@ -1601,12 +1625,28 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
have no cmdsets. have no cmdsets.
Keyword Args: Keyword Args:
caller (Session, Object or Account): The caller requesting caller (obj): The object requesting the cmdsets.
this cmdset. 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 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): def at_pre_puppet(self, account, session=None, **kwargs):
""" """
Called just before an Account connects to this object to puppet 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): def __init__(self):
""" """
Initiate to avoid AttributeErrors down the line 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) 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 @property
def id(self): def id(self):
return self.sessid return self.sessid
@ -376,12 +402,35 @@ class ServerSession(_BASE_SESSION_CLASS):
def at_cmdset_get(self, **kwargs): 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 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 # Mock db/ndb properties for allowing easy storage on the session
# (note that no databse is involved at all here. session.db.attr = # (note that no databse is involved at all here. session.db.attr =
# value just saves a normal property in memory, just like ndb). # value just saves a normal property in memory, just like ndb).