Merge pull request #3177 from volundmush/player_character_refactor

Improved generation of Player Characters using charcreate
This commit is contained in:
Griatch 2023-11-23 18:28:24 +01:00 committed by GitHub
commit 86f2cb6a7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 121 additions and 60 deletions

View file

@ -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.
default_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,48 @@ 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, or
None if there are no limits.
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.
Returns:
int (optional): The number of character slots this account has, or None
if there are no limits.
"""
return settings.MAX_NR_CHARACTERS
def get_available_character_slots(self) -> typing.Optional[int]:
"""
Returns the number of character slots this account has available, or None if
there are no limits.
Returns:
int (optional): The number of open character slots this account has, or None
if there are no limits.
"""
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.
@ -797,7 +842,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
Args:
key (str, optional): If not given, use the same name as the account.
typeclass (str, optional): Typeclass to use for this character. If
not given, use settings.BASE_CHARACTER_TYPECLASS.
not given, use self.default_character_class.
permissions (list, optional): If not given, use the account's permissions.
ip (str, optional): The client IP creating this character. Will fall back to the
one stored for the account if not given.
@ -807,16 +852,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.default_character_typeclass)
Character = class_from_module(character_typeclass)
if "location" not in kwargs:
@ -832,12 +878,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):
@ -962,7 +1025,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# Auto-create a character to go with this account
character, errs = account.create_character(
typeclass=kwargs.get("character_typeclass")
typeclass=kwargs.get("character_typeclass", account.default_character_typeclass)
)
if errs:
errors.extend(errs)

View file

@ -352,6 +352,32 @@ class TestDefaultAccount(TestCase):
)
self.assertIsNone(obj.at_post_puppet.call_args)
@override_settings(MAX_NR_CHARACTERS=5)
def test_get_character_slots(self):
"Check get_character_slots method"
account = create.create_account(
f"TestAccount{randint(0, 999999)}",
email="test@test.com",
password="testpassword",
typeclass=DefaultAccount,
)
self.assertEqual(account.get_character_slots(), 5)
account.delete()
@override_settings(MAX_NR_CHARACTERS=5)
def test_get_available_character_slots(self):
"Check get_available_character_slots method"
account = create.create_account(
f"TestAccount{randint(0, 999999)}",
email="test@test.com",
password="testpassword",
typeclass=DefaultAccount,
)
self.assertEqual(account.get_available_character_slots(), 5)
account.delete()
class TestAccountPuppetDeletion(BaseEvenniaTest):
@override_settings(MULTISESSION_MODE=2)

View file

@ -146,53 +146,20 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
self.msg("Usage: charcreate <charname> [= 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):

View file

@ -9,6 +9,7 @@ This is the v1.0 develop version (for ref in doc building).
"""
import time
from collections import defaultdict
import typing
import inflect
from django.conf import settings
@ -2529,8 +2530,8 @@ class DefaultCharacter(DefaultObject):
# Normalize to latin characters and validate, if necessary, the supplied key
key = cls.normalize_name(key)
if not cls.validate_name(key):
errors.append(_("Invalid character name."))
if (val_err := cls.validate_name(key, account=account)):
errors.append(val_err)
return obj, errors
# Set the supplied key as the name of the intended object
@ -2548,8 +2549,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
@ -2603,17 +2605,20 @@ class DefaultCharacter(DefaultObject):
return latin_name
@classmethod
def validate_name(cls, name):
"""Validate the character name prior to creating. Overload this function to add custom validators
def validate_name(cls, name, account=None) -> typing.Optional[str]:
"""
Validate the character name prior to creating. Overload this function to add custom validators
Args:
name (str) : The name of the character
Kwargs:
account (DefaultAccount, optional) : The account creating the character.
Returns:
valid (bool) : True if character creation should continue; False if it should fail
error (str, optional) : A non-empty error message if there is a problem, otherwise False.
"""
return True # Default validator does not perform any operations
if account and cls.objects.filter_family(db_key__iexact=name):
return f"|rA character named '|w{name}|r' already exists.|n"
def basetype_setup(self):
"""