diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index eb7e8b5d74..d0608adf71 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -236,6 +236,39 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): return objs + def add_character(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(character) + + 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. + """ + pass + + def remove_character(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(character) + + def at_post_remove_character(self, character): + """ + 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. + """ + pass + + def uses_screenreader(self, session=None): """ Shortcut to determine if a session uses a screenreader. If no session given, @@ -743,8 +776,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): ) if character: # Update playable character list - if character not in self.characters: - self.db._playable_characters.append(character) + self.add_character(character) # We need to set this to have @ic auto-connect to this character self.db._last_puppet = character @@ -1483,11 +1515,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): else: # In this mode we don't auto-connect but by default end up at a character selection # screen. We execute look on the account. - # we make sure to clean up the _playable_characters list in case - # any was deleted in the interim. - self.db._playable_characters = [char for char in self.db._playable_characters if char] self.msg( - self.at_look(target=self.db._playable_characters, session=session), session=session + self.at_look(target=self.characters, session=session), session=session ) def at_failed_login(self, session, **kwargs): @@ -1825,11 +1854,8 @@ class DefaultGuest(DefaultAccount): be on the safe side. """ super().at_server_shutdown() - characters = self.db._playable_characters - if characters: - for character in characters: - if character: - character.delete() + for character in self.characters: + character.delete() def at_post_disconnect(self, **kwargs): """ @@ -1841,8 +1867,6 @@ class DefaultGuest(DefaultAccount): """ super().at_post_disconnect() - characters = self.db._playable_characters - for character in characters: - if character: - character.delete() + for character in self.characters: + character.delete() self.delete() diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index eb0b07495e..5e3f2f4182 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -60,12 +60,7 @@ class MuxAccountLookCommand(COMMAND_DEFAULT_CLASS): super().parse() - playable = self.account.db._playable_characters - if playable is not None: - # clean up list if character object was deleted in between - if None in playable: - playable = [character for character in playable if character] - self.account.db._playable_characters = playable + playable = self.account.characters # store playable property if self.args: self.playable = dict((utils.to_str(char.key.lower()), char) for char in playable).get( @@ -155,8 +150,8 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS): if ( not account.is_superuser and not account.check_permstring("Developer") - and account.db._playable_characters - and len(account.db._playable_characters) >= _MAX_NR_CHARACTERS + and account.characters + and len(account.characters) >= _MAX_NR_CHARACTERS ): plural = "" if _MAX_NR_CHARACTERS == 1 else "s" self.msg(f"You may only have a maximum of {_MAX_NR_CHARACTERS} character{plural}.") @@ -184,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.db._playable_characters.append(new_character) + account.add_character(new_character) if desc: new_character.db.desc = desc elif not new_character.db.desc: @@ -223,7 +218,7 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS): # use the playable_characters list to search match = [ char - for char in utils.make_iter(account.db._playable_characters) + for char in utils.make_iter(account.characters) if char.key.lower() == self.args.lower() ] if not match: @@ -243,9 +238,7 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS): # only take action delobj = caller.ndb._char_to_delete key = delobj.key - caller.db._playable_characters = [ - pc for pc in caller.db._playable_characters if pc != delobj - ] + caller.remove_character(delobj) delobj.delete() self.msg(f"Character '{key}' was permanently deleted.") logger.log_sec( @@ -314,13 +307,13 @@ class CmdIC(COMMAND_DEFAULT_CLASS): else: # argument given - if account.db._playable_characters: + if (playables := account.characters): # look at the playable_characters list first character_candidates.extend( utils.make_iter( account.search( self.args, - candidates=account.db._playable_characters, + candidates=playables, search_object=True, quiet=True, ) diff --git a/evennia/commands/default/unloggedin.py b/evennia/commands/default/unloggedin.py index 7bc58dc115..0b0179b3dd 100644 --- a/evennia/commands/default/unloggedin.py +++ b/evennia/commands/default/unloggedin.py @@ -465,3 +465,64 @@ class CmdUnconnectedInfo(COMMAND_DEFAULT_CLASS): utils.get_evennia_version(), ) ) + + +def _create_account(session, accountname, password, permissions, typeclass=None, email=None): + """ + Helper function, creates an account of the specified typeclass. + """ + try: + new_account = create.create_account( + accountname, email, password, permissions=permissions, typeclass=typeclass + ) + + except Exception as e: + session.msg( + "There was an error creating the Account:\n%s\n If this problem persists, contact an" + " admin." % e + ) + logger.log_trace() + return False + + # This needs to be set so the engine knows this account is + # logging in for the first time. (so it knows to call the right + # hooks during login later) + new_account.db.FIRST_LOGIN = True + + # join the new account to the public channel + pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"]) + if not pchannel or not pchannel.connect(new_account): + string = "New account '%s' could not connect to public channel!" % new_account.key + logger.log_err(string) + return new_account + + +def _create_character(session, new_account, typeclass, home, permissions): + """ + Helper function, creates a character based on an account's name. + This is meant for Guest and AUTO_CREATRE_CHARACTER_WITH_ACCOUNT=True situations. + """ + try: + new_character = create.create_object( + typeclass, key=new_account.key, home=home, permissions=permissions + ) + # set playable character list + new_account.add_character(new_character) + + # allow only the character itself and the account to puppet this character (and Developers). + new_character.locks.add( + "puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)" + % (new_character.id, new_account.id) + ) + + # If no description is set, set a default description + if not new_character.db.desc: + new_character.db.desc = "This is a character." + # We need to set this to have ic auto-connect to this character + new_account.db._last_puppet = new_character + except Exception as e: + session.msg( + "There was an error creating the Character:\n%s\n If this problem persists, contact an" + " admin." % e + ) + logger.log_trace() diff --git a/evennia/contrib/rpg/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py index 3547698235..d70a6be0a5 100644 --- a/evennia/contrib/rpg/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -54,7 +54,7 @@ class ContribCmdCharCreate(MuxAccountCommand): session = self.session # only one character should be in progress at a time, so we check for WIPs first - in_progress = [chara for chara in account.db._playable_characters if chara.db.chargen_step] + in_progress = [chara for chara in account.characters if chara.db.chargen_step] if len(in_progress): # we're continuing chargen for a WIP character @@ -64,7 +64,7 @@ class ContribCmdCharCreate(MuxAccountCommand): charmax = settings.MAX_NR_CHARACTERS if not account.is_superuser and ( - account.db._playable_characters and len(account.db._playable_characters) >= charmax + account.characters and len(account.characters) >= charmax ): plural = "" if charmax == 1 else "s" self.msg(f"You may only create a maximum of {charmax} character{plural}.") @@ -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.db._playable_characters.append(new_character) + account.add_character(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/tutorials/evadventure/chargen.py b/evennia/contrib/tutorials/evadventure/chargen.py index 26f3df6464..2236ddd7f1 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.db._playable_characters.append(new_character) + caller.add_character(new_character) text = "Character created!" diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index ffd9e21655..5e22cc03b0 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1149,10 +1149,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # sever the connection (important!) if self.account: # Remove the object from playable characters list - if self in self.account.db._playable_characters: - self.account.db._playable_characters = [ - x for x in self.account.db._playable_characters if x != self - ] + self.account.remove_character(self) for session in self.sessions.all(): self.account.unpuppet_object(session) @@ -2562,8 +2559,7 @@ class DefaultCharacter(DefaultObject): obj.db.creator_ip = ip if account: obj.db.creator_id = account.id - if obj not in account.characters: - account.db._playable_characters.append(obj) + account.add_character(obj) # Add locks if not locks and account: diff --git a/evennia/server/initial_setup.py b/evennia/server/initial_setup.py index ffec79ab17..cfd79fea0a 100644 --- a/evennia/server/initial_setup.py +++ b/evennia/server/initial_setup.py @@ -98,16 +98,15 @@ def create_objects(): # Create the in-game god-character for account #1 and set # it to exist in Limbo. - character_typeclass = settings.BASE_CHARACTER_TYPECLASS try: superuser_character = ObjectDB.objects.get(id=1) except ObjectDB.DoesNotExist: - superuser_character = create.create_object( - character_typeclass, key=superuser.username, nohome=True + superuser_character, errors = superuser.create_character( + key=superuser.username, nohome=True, description=_("This is User #1.") ) + if errors: + raise Exception(str(errors)) - superuser_character.db_typeclass_path = character_typeclass - superuser_character.db.desc = _("This is User #1.") superuser_character.locks.add( "examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()" ) @@ -118,11 +117,6 @@ def create_objects(): superuser.attributes.add("_first_login", True) superuser.attributes.add("_last_puppet", superuser_character) - try: - superuser.db._playable_characters.append(superuser_character) - except AttributeError: - superuser.db_playable_characters = [superuser_character] - room_typeclass = settings.BASE_ROOM_TYPECLASS try: limbo_obj = ObjectDB.objects.get(id=2) diff --git a/evennia/server/server.py b/evennia/server/server.py index 8407ff1189..c5bd6da958 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -647,9 +647,8 @@ class Evennia: for guest in AccountDB.objects.all().filter( db_typeclass_path=settings.BASE_GUEST_TYPECLASS ): - for character in guest.db._playable_characters: - if character: - character.delete() + for character in guest.characters: + character.delete() guest.delete() for mod in SERVER_STARTSTOP_MODULES: if hasattr(mod, "at_server_cold_start"): diff --git a/evennia/web/admin/objects.py b/evennia/web/admin/objects.py index 1545add8e5..a705fa4589 100644 --- a/evennia/web/admin/objects.py +++ b/evennia/web/admin/objects.py @@ -310,7 +310,7 @@ class ObjectAdmin(admin.ModelAdmin): This will: - Set account.db._last_puppet to this object - - Add object to account.db._playable_characters + - Add object to account.characters - Change object locks to allow puppeting by account """ @@ -319,10 +319,7 @@ class ObjectAdmin(admin.ModelAdmin): if account: account.db._last_puppet = obj - if not account.db._playable_characters: - account.db._playable_characters = [] - if obj not in account.db._playable_characters: - account.db._playable_characters.append(obj) + account.add_character(obj) if not obj.access(account, "puppet"): lock = obj.locks.get("puppet") lock += f" or pid({account.id})" @@ -331,7 +328,7 @@ class ObjectAdmin(admin.ModelAdmin): request, "Did the following (where possible): " f"Set Account.db._last_puppet = {obj}, " - f"Added {obj} to Account.db._playable_characters list, " + f"Added {obj} to Account.characters list, " f"Added 'puppet:pid({account.id})' lock to {obj}.", ) else: diff --git a/evennia/web/website/views/help.py b/evennia/web/website/views/help.py index 677328d57c..c416f4e73f 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.db._playable_characters + [account] + puppets = account.characters + [account] # add the account's and puppets' commands to cmd_help_topics list for puppet in puppets: for cmdset in puppet.cmdset.get():