From 2bf96f7c7f778256928c05b9070f9f0c5021492f Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sun, 10 Sep 2023 12:23:56 -0400 Subject: [PATCH] Added CharactersHandler to account and altered all calls of add/remove characters to use it. --- evennia/accounts/accounts.py | 120 +++++++++++++----- evennia/accounts/tests.py | 23 ++-- evennia/commands/default/account.py | 4 +- evennia/commands/default/tests.py | 8 +- .../character_creator/character_creator.py | 2 +- .../contrib/rpg/character_creator/tests.py | 2 +- .../contrib/tutorials/evadventure/chargen.py | 2 +- evennia/objects/objects.py | 4 +- evennia/web/admin/objects.py | 2 +- evennia/web/website/tests.py | 6 +- evennia/web/website/views/help.py | 2 +- 11 files changed, 112 insertions(+), 63 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index bc14b5bf0..3f11882b0 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -120,6 +120,80 @@ class AccountSessionHandler(object): return len(self.get()) +class CharactersHandler: + """ + A simple Handler that lives on DefaultAccount as .characters via @lazy_property used to + wrap access to .db._playable_characters. + """ + + def __init__(self, owner: "DefaultAccount"): + """ + Create the CharactersHandler. + + Args: + owner: The Account that owns this handler. + """ + self.owner = owner + self._ensure_playable_characters() + self._clean() + + def _ensure_playable_characters(self): + if self.owner.db._playable_characters is None: + self.owner.db._playable_characters = [] + + def _clean(self): + # Remove all instances of None from the list. + self.owner.db._playable_characters = [x for x in self.owner.db._playable_characters if x] + + def add(self, character: "DefaultCharacter"): + """ + Add a character to this account's list of playable characters. + + Args: + character (DefaultCharacter): The character to add. + """ + self._clean() + if character not in self.owner.db._playable_characters: + self.owner.db._playable_characters.append(character) + self.owner.at_post_add_character(character) + + def remove(self, character: "DefaultCharacter"): + """ + Remove a character from this account's list of playable characters. + + Args: + character (DefaultCharacter): The character to remove. + """ + self._clean() + if character in self.owner.db._playable_characters: + self.owner.db._playable_characters.remove(character) + self.owner.at_post_remove_character(character) + + def all(self) -> list["DefaultCharacter"]: + """ + Get all playable characters. + + Returns: + list[DefaultCharacter]: All playable characters. + """ + self._clean() + return list(self.owner.db._playable_characters) + + def count(self) -> int: + """ + Get the number of playable characters. + + Returns: + int: The number of playable characters. + """ + return len(self.all()) + + __len__ = count + + def __iter__(self): + return iter(self.all()) + + class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ This is the base Typeclass for all Accounts. Accounts represent @@ -219,56 +293,32 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): load_kwargs={"category": "option"}, ) - # Do not make this a lazy property; the web UI will not refresh it! - @property + @lazy_property def characters(self): - # Get playable characters list - objs = self.db._playable_characters or [] + return CharactersHandler(self) - # Rebuild the list if legacy code left null values after deletion - try: - if None in objs: - objs = [x for x in self.db._playable_characters if x] - self.db._playable_characters = objs - except Exception as e: - logger.log_trace(e) - logger.log_err(e) - - return objs - - def add_character_to_playable_list(self, character: "DefaultCharacter"): - """ - Add a character to this account's list of playable characters. - """ - if character not in self.db._playable_characters: - self.db._playable_characters.append(character) - self.at_post_add_character_to_playable_list(character) - - def at_post_add_character_to_playable_list(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. Use it to easily implement custom logic when a character is added to an account. + + Args: + character (DefaultCharacter): The character that was added. """ pass - def remove_character_from_playable_list(self, character): - """ - Remove a character from this account's list of playable characters. - """ - if character in self.db._playable_characters: - self.db._playable_characters.remove(character) - self.at_post_remove_character_from_playable_list(character) - - def at_post_remove_character_from_playable_list(self, character): + def at_post_remove_character(self, character: "DefaultAccount"): """ Called after a character is removed from this account's list of playable characters. Use it to easily implement custom logic when a character is removed from an account. + + Args: + character (DefaultCharacter): The character that was removed. """ pass - def uses_screenreader(self, session=None): """ Shortcut to determine if a session uses a screenreader. If no session given, @@ -776,7 +826,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): ) if character: # Update playable character list - self.add_character_to_playable_list(character) + self.characters.add(character) # We need to set this to have @ic auto-connect to this character self.db._last_puppet = character diff --git a/evennia/accounts/tests.py b/evennia/accounts/tests.py index c35609589..18b4025d9 100644 --- a/evennia/accounts/tests.py +++ b/evennia/accounts/tests.py @@ -105,14 +105,14 @@ class TestDefaultGuest(BaseEvenniaTest): def test_at_server_shutdown(self): account, errors = DefaultGuest.create(ip=self.ip) self.char1.delete = MagicMock() - account.add_character_to_playable_list(self.char1) + account.characters.add(self.char1) account.at_server_shutdown() self.char1.delete.assert_called() def test_at_post_disconnect(self): account, errors = DefaultGuest.create(ip=self.ip) self.char1.delete = MagicMock() - account.add_character_to_playable_list(self.char1) + account.characters.add(self.char1) account.at_post_disconnect() self.char1.delete.assert_called() @@ -362,7 +362,7 @@ class TestAccountPuppetDeletion(BaseEvenniaTest): ) # Add char1 to account's playable characters - self.account.add_character_to_playable_list(self.char1) + self.account.characters.add(self.char1) self.assertTrue(self.account.characters, "Char was not added to account.") # See what happens when we delete char1. @@ -383,20 +383,19 @@ class TestDefaultAccountEv(BaseEvenniaTest): def test_characters_property(self): "test existence of None in _playable_characters Attr" self.account.db._playable_characters = [self.char1, None] - chars = self.account.characters - self.assertEqual(chars, [self.char1]) + self.assertEqual(self.account.characters.all(), [self.char1]) self.assertEqual(self.account.db._playable_characters, [self.char1]) def test_add_character_to_playable_list(self): - self.assertEqual(self.account.characters, []) - self.account.add_character_to_playable_list(self.char1) - self.assertEqual(self.account.characters, [self.char1]) + self.assertEqual(self.account.characters.all(), []) + self.account.characters.add(self.char1) + self.assertEqual(self.account.characters.all(), [self.char1]) def test_remove_character_from_playable_list(self): - self.account.add_character_to_playable_list(self.char1) - self.assertEqual(self.account.characters, [self.char1]) - self.account.remove_character_from_playable_list(self.char1) - self.assertEqual(self.account.characters, []) + self.account.characters.add(self.char1) + self.assertEqual(self.account.characters.all(), [self.char1]) + self.account.characters.remove(self.char1) + self.assertEqual(self.account.characters.all(), []) def test_puppet_success(self): self.account.msg = MagicMock() diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index 571d9f7ee..5e38dea46 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -179,7 +179,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS): "puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer);delete:id(%i) or" " perm(Admin)" % (new_character.id, account.id, account.id) ) - account.add_character_to_playable_list(new_character) + account.characters.add(new_character) if desc: new_character.db.desc = desc elif not new_character.db.desc: @@ -238,7 +238,7 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS): # only take action delobj = caller.ndb._char_to_delete key = delobj.key - caller.remove_character_from_playable_list(delobj) + caller.characters.remove(delobj) delobj.delete() self.msg(f"Character '{key}' was permanently deleted.") logger.log_sec( diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 0e25737e8..774bd8333 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -589,7 +589,7 @@ class TestAccount(BaseEvenniaCommandTest): ] ) def test_ooc_look(self, multisession_mode, auto_puppet, max_nr_chars, expected_result): - self.account.add_character_to_playable_list(self.char1) + self.account.characters.add(self.char1) self.account.unpuppet_all() with self.settings(MULTISESSION=multisession_mode): @@ -609,14 +609,14 @@ class TestAccount(BaseEvenniaCommandTest): self.call(account.CmdOOC(), "", "You go OOC.", caller=self.account) def test_ic(self): - self.account.add_character_to_playable_list(self.char1) + self.account.characters.add(self.char1) self.account.unpuppet_object(self.session) self.call( account.CmdIC(), "Char", "You become Char.", caller=self.account, receiver=self.char1 ) def test_ic__other_object(self): - self.account.add_character_to_playable_list(self.obj1) + self.account.characters.add(self.obj1) self.account.unpuppet_object(self.session) self.call( account.CmdIC(), "Obj", "You become Obj.", caller=self.account, receiver=self.obj1 @@ -670,7 +670,7 @@ class TestAccount(BaseEvenniaCommandTest): # whether permissions are being checked # Add char to account playable characters - self.account.add_character_to_playable_list(self.char1) + self.account.characters.add(self.char1) # Try deleting as Developer self.call( diff --git a/evennia/contrib/rpg/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py index b6c0ae51e..8ebc49435 100644 --- a/evennia/contrib/rpg/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -90,7 +90,7 @@ class ContribCmdCharCreate(MuxAccountCommand): ) # initalize the new character to the beginning of the chargen menu new_character.db.chargen_step = "menunode_welcome" - account.add_character_to_playable_list(new_character) + account.characters.add(new_character) # set the menu node to start at to the character's last saved step startnode = new_character.db.chargen_step diff --git a/evennia/contrib/rpg/character_creator/tests.py b/evennia/contrib/rpg/character_creator/tests.py index e6aec7fd9..5cdaaa0ec 100644 --- a/evennia/contrib/rpg/character_creator/tests.py +++ b/evennia/contrib/rpg/character_creator/tests.py @@ -17,7 +17,7 @@ class TestCharacterCreator(BaseEvenniaCommandTest): self.account.swap_typeclass(character_creator.ContribChargenAccount) def test_ooc_look(self): - self.account.add_character_to_playable_list(self.char1) + self.account.characters.add(self.char1) self.account.unpuppet_all() self.char1.db.chargen_step = "start" diff --git a/evennia/contrib/tutorials/evadventure/chargen.py b/evennia/contrib/tutorials/evadventure/chargen.py index fed7b1c76..ab440d2f4 100644 --- a/evennia/contrib/tutorials/evadventure/chargen.py +++ b/evennia/contrib/tutorials/evadventure/chargen.py @@ -316,7 +316,7 @@ def node_apply_character(caller, raw_string, **kwargs): """ tmp_character = kwargs["tmp_character"] new_character = tmp_character.apply(caller) - caller.add_character_to_playable_list(new_character) + caller.characters.add(new_character) text = "Character created!" diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 4b6605efa..f8f98b552 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1149,7 +1149,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # sever the connection (important!) if self.account: # Remove the object from playable characters list - self.account.remove_character_from_playable_list(self) + self.account.characters.remove(self) for session in self.sessions.all(): self.account.unpuppet_object(session) @@ -2559,7 +2559,7 @@ class DefaultCharacter(DefaultObject): obj.db.creator_ip = ip if account: obj.db.creator_id = account.id - account.add_character_to_playable_list(obj) + account.characters.add(obj) # Add locks if not locks and account: diff --git a/evennia/web/admin/objects.py b/evennia/web/admin/objects.py index da73a4931..48e5fe99e 100644 --- a/evennia/web/admin/objects.py +++ b/evennia/web/admin/objects.py @@ -319,7 +319,7 @@ class ObjectAdmin(admin.ModelAdmin): if account: account.db._last_puppet = obj - account.add_character_to_playable_list(obj) + account.characters.add(obj) if not obj.access(account, "puppet"): lock = obj.locks.get("puppet") lock += f" or pid({account.id})" diff --git a/evennia/web/website/tests.py b/evennia/web/website/tests.py index e5b857109..230d4442f 100644 --- a/evennia/web/website/tests.py +++ b/evennia/web/website/tests.py @@ -35,8 +35,8 @@ class EvenniaWebTest(BaseEvenniaTest): super().setUp() # Add chars to account rosters - self.account.add_character_to_playable_list(self.char1) - self.account2.add_character_to_playable_list(self.char2) + self.account.characters.add(self.char1) + self.account2.characters.add(self.char2) for account in (self.account, self.account2): # Demote accounts to Player permissions @@ -220,7 +220,7 @@ class CharacterCreateView(EvenniaWebTest): @override_settings(MAX_NR_CHARACTERS=1) def test_valid_access_multisession_0(self): "Account1 with no characters should be able to create a new one" - self.account.remove_character_from_playable_list(self.char1) + self.account.characters.remove(self.char1) # Login account self.login() diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index c416f4e73..98ef5cbcb 100644 --- a/evennia/web/website/views/help.py +++ b/evennia/web/website/views/help.py @@ -102,7 +102,7 @@ def collect_topics(account): cmd_help_topics = [] if not str(account) == "AnonymousUser": # create list of account and account's puppets - puppets = account.characters + [account] + puppets = account.characters.all() + [account] # add the account's and puppets' commands to cmd_help_topics list for puppet in puppets: for cmdset in puppet.cmdset.get():