diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 17f17ad9b8..0c0006f83e 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -12,16 +12,16 @@ instead for most things). """ import re import time +import typing from random import getrandbits +import evennia from django.conf import settings from django.contrib.auth import authenticate, password_validation from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ - -import evennia from evennia.accounts.manager import AccountManager from evennia.accounts.models import AccountDB from evennia.commands.cmdsethandler import CmdSetHandler @@ -272,6 +272,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): objects = AccountManager() + # Used by account.create_character() to choose default typeclass for characters. + player_character_typeclass = settings.BASE_CHARACTER_TYPECLASS + # properties @lazy_property def cmdset(self): @@ -315,7 +318,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ pass - def at_post_remove_character(self, character: "DefaultAccount"): + def at_post_remove_character(self, character: "DefaultCharacter"): """ Called after a character is removed from this account's list of playable characters. @@ -790,6 +793,40 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): logger.log_sec(f"Password successfully changed for {self}.") self.at_password_change() + def get_character_slots(self) -> typing.Optional[int]: + """ + Returns the number of character slots this account has. + + By default, that's settings.MAX_NR_CHARACTERS but this makes it easy to override. + Maybe for your game, players can be rewarded with more slots, somehow. + + If it returns None, then there are no limits on character slots. + """ + return settings.MAX_NR_CHARACTERS + + def get_available_character_slots(self) -> typing.Optional[int]: + """ + Returns the number of character slots this account has available. + """ + if (slots := self.get_character_slots()) is None: + return None + return max(0, slots - len(self.characters)) + + def check_available_slots(self, **kwargs) -> typing.Optional[str]: + """ + Helper method used to determine if an account can create additional characters using + the character slot system. + + Returns: + str (optional): An error message regarding the status of slots. If present, this + will halt character creation. If not, character creation can proceed. + """ + if (slots := self.get_available_character_slots()) is not None: + if slots <= 0: + if not (self.is_superuser or self.check_permstring("Developer")): + plural = "" if (max_slots := self.get_character_slots()) == 1 else "s" + return f"You may only have a maximum of {max_slots} character{plural}." + def create_character(self, *args, **kwargs): """ Create a character linked to this account. @@ -807,16 +844,17 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): list or None: A list of errors, or None. """ + # check character slot usage. + if (slot_check := self.check_available_slots()): + return None, [slot_check] + # parse inputs character_key = kwargs.pop("key", self.key) character_ip = kwargs.pop("ip", self.db.creator_ip) character_permissions = kwargs.pop("permissions", self.permissions) # Load the appropriate Character class - character_typeclass = kwargs.pop("typeclass", None) - character_typeclass = ( - character_typeclass if character_typeclass else settings.BASE_CHARACTER_TYPECLASS - ) + character_typeclass = kwargs.pop("typeclass", self.player_character_typeclass) Character = class_from_module(character_typeclass) if "location" not in kwargs: @@ -832,12 +870,29 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): **kwargs, ) if character: - # Update playable character list + self.at_post_create_character(character, ip=character_ip) + + return character, errs + + def at_post_create_character(self, character, **kwargs): + """ + An overloadable hook method that allows for further customization of newly created characters. + """ + if character not in self.characters: self.characters.add(character) - # We need to set this to have @ic auto-connect to this character + # We need to set this to have @ic auto-connect to this character + if len(self.characters) == 1: self.db._last_puppet = character - return character, errs + + character.locks.add( + f"puppet:id({character.id}) or pid({self.id}) or perm(Developer) or pperm(Developer);delete:id({self.id}) or" + " perm(Admin)" + ) + + logger.log_sec( + f"Character Created: {character} (Caller: {self}, IP: {kwargs.get('ip', None)})." + ) @classmethod def create(cls, *args, **kwargs): @@ -960,10 +1015,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): if account and _AUTO_CREATE_CHARACTER_WITH_ACCOUNT: # Auto-create a character to go with this account - - character, errs = account.create_character( - typeclass=kwargs.get("character_typeclass") - ) + call_kwargs = {} + if "character_typeclass" in kwargs: + call_kwargs["character_typeclass"] = kwargs["character_typeclass"] + character, errs = account.create_character(**call_kwargs) if errs: errors.extend(errs) diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index acbd071ddf..67231a657a 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -146,53 +146,20 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS): self.msg("Usage: charcreate [= description]") return key = self.lhs - desc = self.rhs + description = self.rhs or "This is a character." - if _MAX_NR_CHARACTERS is not None: - if ( - not account.is_superuser - and not account.check_permstring("Developer") - 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}.") - return - from evennia.objects.models import ObjectDB + new_character, errors = self.account.create_character(key=key, description=description, ip=self.session.address) - typeclass = settings.BASE_CHARACTER_TYPECLASS - - if ObjectDB.objects.filter(db_typeclass_path=typeclass, db_key__iexact=key): - # check if this Character already exists. Note that we are only - # searching the base character typeclass here, not any child - # classes. - self.msg(f"|rA character named '|w{key}|r' already exists.|n") + if errors: + self.msg(errors) + if not new_character: return - # create the character - start_location = ObjectDB.objects.get_id(settings.START_LOCATION) - default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) - permissions = settings.PERMISSION_ACCOUNT_DEFAULT - new_character = create.create_object( - typeclass, key=key, location=start_location, home=default_home, permissions=permissions - ) - # only allow creator (and developers) to puppet this char - new_character.locks.add( - "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.characters.add(new_character) - if desc: - new_character.db.desc = desc - elif not new_character.db.desc: - new_character.db.desc = "This is a character." self.msg( f"Created new character {new_character.key}. Use |wic {new_character.key}|n to enter" " the game as this character." ) - logger.log_sec( - f"Character Created: {new_character} (Caller: {account}, IP: {self.session.address})." - ) + class CmdCharDelete(COMMAND_DEFAULT_CLASS): diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index c710e96498..503167f9da 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -2548,8 +2548,9 @@ class DefaultCharacter(DefaultObject): try: # Check to make sure account does not have too many chars if account: - if len(account.characters) >= settings.MAX_NR_CHARACTERS: - errors.append(_("There are too many characters associated with this account.")) + avail = account.check_available_slots() + if avail: + errors.append(avail) return obj, errors # Create the Character @@ -2604,7 +2605,8 @@ class DefaultCharacter(DefaultObject): @classmethod def validate_name(cls, name): - """Validate the character name prior to creating. Overload this function to add custom validators + """ + Validate the character name prior to creating. Overload this function to add custom validators Args: name (str) : The name of the character