From 97c73d133d1c2179b89d84c121e7a49785a6f45d Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sat, 25 Nov 2023 02:32:45 -0500 Subject: [PATCH 1/2] Cleaned up refactor to support more extension. --- evennia/accounts/accounts.py | 50 +++++++-- evennia/commands/cmdhandler.py | 151 +++++++++++++-------------- evennia/commands/default/building.py | 12 ++- evennia/commands/tests.py | 56 ++++++++-- evennia/objects/objects.py | 44 +++++++- evennia/server/serversession.py | 53 +++++++++- 6 files changed, 271 insertions(+), 95 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 1acf45a2f3..167fc83515 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -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. diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 78e400f7da..c0df779615 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -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: diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index da6a05e67e..2a2dd6e367 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -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: diff --git a/evennia/commands/tests.py b/evennia/commands/tests.py index dcd11b9865..bd23f94dda 100644 --- a/evennia/commands/tests.py +++ b/evennia/commands/tests.py @@ -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) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 67ee70b9c8..8b3c55dba6 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -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 diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index 2836609c57..50777ad61e 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -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). From 1cfee2352b8aafc24880a6b65be7a0a44da7a566 Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sat, 2 Dec 2023 19:56:48 -0500 Subject: [PATCH 2/2] Renaming things to be more Evennia'ish. --- evennia/accounts/accounts.py | 19 +++--- evennia/commands/cmdhandler.py | 94 +++++++++++++++------------- evennia/commands/default/building.py | 4 +- evennia/commands/tests.py | 10 +-- evennia/objects/objects.py | 20 +++--- evennia/server/serversession.py | 19 +++--- 6 files changed, 84 insertions(+), 82 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 167fc83515..6df9f80244 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -274,9 +274,9 @@ 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" + cmdset_provider_order = 50 + cmdset_provider_error_order = 0 + cmdset_provider_type = "account" objects = AccountManager() @@ -315,17 +315,16 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): def characters(self): return CharactersHandler(self) - def get_command_objects(self) -> dict[str, "CommandObject"]: + def get_cmdset_providers(self) -> dict[str, "CmdSetProvider"]: """ - Overrideable method which returns a dictionary of all the kinds of CommandObjects - linked to this Account. + Overrideable method which returns a dictionary of every kind of object which + has a cmdsethandler linked to this Account, and should participate in cmdset + merging. - In all normal cases, that's just the account itself. - - The cmdhandler uses this to determine available cmdsets when executing a command. + Accounts have no way of being aware of anything besides themselves, unfortunately. Returns: - dict[str, CommandObject]: The CommandObjects linked to this Account. + dict[str, CmdSetProvider]: The CmdSetProviders linked to this Object. """ return {"account": self} diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index c0df779615..261ff3108e 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -151,9 +151,13 @@ _GET_INPUT = None # helper functions +def err_helper(raw_string, cmdid=None): + if cmdid is not None: + return raw_string, {"cmdid": cmdid} + return raw_string -def _msg_err(receiver, stringtuple): +def _msg_err(receiver, stringtuple, cmdid=None): """ Helper function for returning an error to the caller. @@ -169,19 +173,16 @@ def _msg_err(receiver, stringtuple): tracestring = format_exc() logger.log_trace() if _IN_GAME_ERRORS: - receiver.msg( - string.format( - traceback=tracestring, errmsg=stringtuple[0].strip(), timestamp=timestamp - ).strip() - ) + out = string.format( + traceback=tracestring, errmsg=stringtuple[0].strip(), timestamp=timestamp + ).strip() else: - receiver.msg( - string.format( - traceback=tracestring.splitlines()[-1], - errmsg=stringtuple[1].strip(), - timestamp=timestamp, - ).strip() - ) + out = string.format( + traceback=tracestring.splitlines()[-1], + errmsg=stringtuple[1].strip(), + timestamp=timestamp, + ).strip() + receiver.msg(err_helper(out, cmdid=cmdid)) def _process_input(caller, prompt, result, cmd, generator): @@ -281,37 +282,39 @@ class ErrorReported(Exception): # Helper function -def generate_command_objects(called_by, session=None): - command_objects = dict() - command_objects.update(called_by.get_command_objects()) +def generate_cmdset_providers(called_by, session=None): + cmdset_providers = dict() + cmdset_providers.update(called_by.get_cmdset_providers()) if session and session is not called_by: - command_objects.update(session.get_command_objects()) + cmdset_providers.update(session.get_cmdset_providers()) - command_objects_list = list(command_objects.values()) - command_objects_list.sort(key=lambda x: getattr(x, "cmd_order", 0)) + cmdset_providers_list = list(cmdset_providers.values()) + cmdset_providers_list.sort(key=lambda x: getattr(x, "cmdset_provider_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} + cmdset_providers = {c.cmdset_provider_type: c for c in cmdset_providers_list} - if not command_objects: + if not cmdset_providers: 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] + caller = cmdset_providers_list[-1] - command_objects_list_error = sorted( - command_objects_list, key=lambda x: getattr(x, "cmd_order_error", 0) + cmdset_providers_errors_list = sorted( + cmdset_providers_list, key=lambda x: getattr(x, "cmdset_provider_error_order", 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] + error_to = cmdset_providers_errors_list[-1] - return command_objects, command_objects_list, command_objects_list_error, caller, error_to + return cmdset_providers, cmdset_providers_list, cmdset_providers_errors_list, caller, error_to @inlineCallbacks -def get_and_merge_cmdsets(caller, command_objects, callertype, raw_string, report_to=None): +def get_and_merge_cmdsets( + caller, cmdset_providers, callertype, raw_string, report_to=None, cmdid=None +): """ Gather all relevant cmdsets and merge them. @@ -321,7 +324,7 @@ def get_and_merge_cmdsets(caller, command_objects, callertype, raw_string, repor 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. - command_objects (list): A list of sorted objects which provide cmdsets. + cmdset_providers (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. @@ -413,13 +416,13 @@ def get_and_merge_cmdsets(caller, command_objects, callertype, raw_string, repor current_cmdset = CmdSet() object_cmdsets = list() - for cmdobj in command_objects: + for cmdobj in cmdset_providers: 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: + match cmdobj.cmdset_provider_type: case "object": if not current.no_objs: local_obj_cmdsets = yield _get_local_obj_cmdsets(cmdobj) @@ -436,7 +439,9 @@ def get_and_merge_cmdsets(caller, command_objects, callertype, raw_string, repor ] # 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" + report_to.msg(err_helper(cmdset.errmessage, cmdid=cmdid)) + for cmdset in cmdsets + if cmdset.key == "_CMDSET_ERROR" ] if cmdsets: @@ -546,9 +551,10 @@ def cmdhandler( default Evennia. """ + cmdid = kwargs.get("cmdid", None) @inlineCallbacks - def _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account, command_objects): + def _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account, cmdset_providers): """ Helper function: This initializes and runs the Command instance once the parser has identified it as either a normal @@ -564,7 +570,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. + cmdset_providers (dict): Dictionary of all cmdset-providing objects. Returns: deferred (Deferred): this will fire with the return of the @@ -583,7 +589,7 @@ def cmdhandler( cmd.cmdstring = cmdname # deprecated cmd.args = args cmd.cmdset = cmdset - cmd.command_objects = command_objects.copy() + cmd.cmdset_providers = cmdset_providers.copy() cmd.session = session cmd.account = account cmd.raw_string = unformatted_raw_string @@ -654,14 +660,14 @@ def cmdhandler( _COMMAND_NESTING[called_by] -= 1 ( - command_objects, - command_objects_list, - command_objects_list_error, + cmdset_providers, + cmdset_providers_list, + cmdset_providers_list_error, caller, error_to, - ) = generate_command_objects(called_by, session=session) + ) = generate_cmdset_providers(called_by, session=session) - account = command_objects.get("account", None) + account = cmdset_providers.get("account", None) try: # catch bugs in cmdhandler itself try: # catch special-type commands @@ -679,7 +685,7 @@ def cmdhandler( else: # no explicit cmdobject given, figure it out cmdset = yield get_and_merge_cmdsets( - caller, command_objects_list, callertype, raw_string + caller, cmdset_providers_list, callertype, raw_string, cmdid=cmdid ) if not cmdset: # this is bad and shouldn't happen. @@ -753,7 +759,7 @@ def cmdhandler( # A normal command. ret = yield _run_command( - cmd, cmdname, args, raw_cmdname, cmdset, session, account, command_objects + cmd, cmdname, args, raw_cmdname, cmdset, session, account, cmdset_providers ) returnValue(ret) @@ -777,17 +783,17 @@ def cmdhandler( cmdset, session, account, - command_objects, + cmdset_providers, ) returnValue(ret) elif sysarg: # return system arg - error_to.msg(exc.sysarg) + error_to.msg(err_helper(exc.sysarg, cmdid=cmdid)) except NoCmdSets: # Critical error. logger.log_err("No cmdsets found: %s" % caller) - error_to.msg(_ERROR_NOCMDSETS) + error_to.msg(err_helper(_ERROR_NOCMDSETS, cmdid=cmdid)) except Exception: # We should not end up here. If we do, it's a programming bug. diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 2a2dd6e367..50d3d20966 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -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, generate_command_objects +from evennia.commands.cmdhandler import get_and_merge_cmdsets, generate_cmdset_providers from evennia.locks.lockhandler import LockException from evennia.objects.models import ObjectDB from evennia.prototypes import menus as olc_menus @@ -3128,7 +3128,7 @@ class CmdExamine(ObjManipCommand): command_objects_list_error, caller, error_to, - ) = generate_command_objects(obj, session=session) + ) = generate_cmdset_providers(obj, session=session) get_and_merge_cmdsets( obj, command_objects_list, mergemode, self.raw_string, error_to diff --git a/evennia/commands/tests.py b/evennia/commands/tests.py index bd23f94dda..10eb5143a7 100644 --- a/evennia/commands/tests.py +++ b/evennia/commands/tests.py @@ -1026,7 +1026,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest): command_objects_list_error, caller, error_to, - ) = cmdhandler.generate_command_objects(self.session) + ) = cmdhandler.generate_cmdset_providers(self.session) deferred = cmdhandler.get_and_merge_cmdsets( self.session, [self.session], "session", "", error_to @@ -1050,7 +1050,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest): command_objects_list_error, caller, error_to, - ) = cmdhandler.generate_command_objects(self.account) + ) = cmdhandler.generate_cmdset_providers(self.account) deferred = cmdhandler.get_and_merge_cmdsets( self.account, command_objects_list, "account", "", error_to @@ -1075,7 +1075,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest): command_objects_list_error, caller, error_to, - ) = cmdhandler.generate_command_objects(self.obj1) + ) = cmdhandler.generate_cmdset_providers(self.obj1) deferred = cmdhandler.get_and_merge_cmdsets( self.obj1, command_objects_list, "object", "", error_to @@ -1101,7 +1101,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest): command_objects_list_error, caller, error_to, - ) = cmdhandler.generate_command_objects(self.obj1) + ) = cmdhandler.generate_cmdset_providers(self.obj1) deferred = cmdhandler.get_and_merge_cmdsets( self.obj1, command_objects_list, "object", "", error_to ) @@ -1127,7 +1127,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, BaseEvenniaTest): command_objects_list_error, caller, error_to, - ) = cmdhandler.generate_command_objects(self.obj1, session=None) + ) = cmdhandler.generate_cmdset_providers(self.obj1, session=None) deferred = cmdhandler.get_and_merge_cmdsets( self.obj1, command_objects_list, "object", "", error_to diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 8b3c55dba6..d0af66fece 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -206,9 +206,9 @@ 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" + cmdset_provider_order = 100 + cmdset_provider_error_order = 100 + cmdset_provider_type = "object" # Used for sorting / filtering in inventories / room contents. _content_types = ("object",) @@ -262,18 +262,16 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ return self.sessions.count() - def get_command_objects(self) -> dict[str, "CommandObject"]: + def get_cmdset_providers(self) -> dict[str, "CmdSetProvider"]: """ - Overrideable method which returns a dictionary of all the kinds of CommandObjects - linked to this Object. + Overrideable method which returns a dictionary of every kind of object which + has a cmdsethandler linked to this Object, and should participate in cmdset + merging. - 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. + Objects might be aware of an Account. Otherwise, just themselves, by default. Returns: - dict[str, CommandObject]: The CommandObjects linked to this Object. + dict[str, CmdSetProvider]: The CmdSetProviders linked to this Object. """ out = {"object": self} if self.account: diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index 50777ad61e..09488303b1 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -48,9 +48,9 @@ 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" + cmdset_provider_order = 0 + cmdset_provider_error_order = 50 + cmdset_provider_type = "session" def __init__(self): """ @@ -70,24 +70,23 @@ class ServerSession(_BASE_SESSION_CLASS): cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set) - def get_command_objects(self) -> dict[str, "CommandObject"]: + def get_cmdset_providers(self) -> dict[str, "CmdSetProvider"]: """ - Overrideable method which returns a dictionary of all the kinds of CommandObjects - linked to this ServerSession. + Overrideable method which returns a dictionary of every kind of object which + has a cmdsethandler linked to this ServerSession, and should participate in cmdset + merging. 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. + dict[str, CmdSetProvider]: The CmdSetProviders linked to this Object. """ out = {"session": self} if self.account: out["account"] = self.account if self.puppet: - out["puppet"] = self.puppet + out["object"] = self.puppet return out @property