diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d81932f6..5b5f2c62b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -194,6 +194,15 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10 startup modes. Used for more generic overriding (volund) - New `search` lock type used to completely hide an object from being found by the `DefaultObject.search` (`caller.search`) method. (CloudKeeper) +- Change setting `MULTISESSION_MODE` to now only control sessions, not how many + characters can be puppeted simultaneously. New settings now control that. +- Add new setting `AUTO_CREATE_CHARACTER_WITH_ACCOUNT`, a boolean deciding if + the new account should also get a matching character (legacy MUD style). +- Add new setting `AUTO_PUPPET_ON_LOGIN`, boolean deciding if one should + automatically puppet the last/available character on connection (legacy MUD style) +- Add new setting `MAX_NR_SIMULTANEUS_PUPPETS` - how many puppets the account + can run at the same time. Used to limit multi-playing. +- Make setting `MAX_NR_CHARACTERS` interact better with the new settings above. ## Evennia 0.9.5 diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index dd89d907fa..f9a857e11c 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -45,6 +45,9 @@ _SESSIONS = None _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1)) _MULTISESSION_MODE = settings.MULTISESSION_MODE +_AUTO_CREATE_CHARACTER_WITH_ACCOUNT = settings.AUTO_CREATE_CHARACTER_WITH_ACCOUNT +_AUTO_PUPPET_ON_LOGIN = settings.AUTO_PUPPET_ON_LOGIN +_MAX_NR_SIMULTANEOUS_PUPPETS = settings.MAX_NR_SIMULTANEOUS_PUPPETS _MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS _CMDSET_ACCOUNT = settings.CMDSET_ACCOUNT _MUDINFO_CHANNEL = None @@ -338,7 +341,6 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): self.msg(_("|c{key}|R is already puppeted by another Account.").format(key=obj.key)) return - # do the puppeting if session.puppet: # cleanly unpuppet eventual previous object puppeted by this session self.unpuppet_object(session) @@ -346,6 +348,21 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): # was left with a lingering account/session reference from an unclean # server kill or similar + # check so we are not puppeting too much already + if _MAX_NR_SIMULTANEOUS_PUPPETS is not None: + already_puppeted = self.get_all_puppets() + if ( + not self.is_superuser + and not self.check_permstring("Developer") + and obj not in already_puppeted + and len(self.get_all_puppets()) >= _MAX_NR_SIMULTANEOUS_PUPPETS + ): + self.msg( + _(f"You cannot control any more puppets (max {_MAX_NR_SIMULTANEOUS_PUPPETS})") + ) + return + + # do the puppeting obj.at_pre_puppet(self, session=session) # do the connection @@ -452,7 +469,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ - ip = kwargs.get("ip", "").strip() + ip = kwargs.get("ip", "") + if isinstance(ip, (tuple, list)): + ip = ip[0] + ip = ip.strip() username = kwargs.get("username", "").lower().strip() # Check IP and/or name bans @@ -772,6 +792,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): typeclass = kwargs.get("typeclass", cls) ip = kwargs.get("ip", "") + if isinstance(ip, (tuple, list)): + ip = ip[0] + if ip and CREATION_THROTTLE.check(ip): errors.append( _("You are creating too many accounts. Please log into an existing account.") @@ -843,7 +866,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): errors.append(string) logger.log_err(string) - if account and settings.MULTISESSION_MODE < 2: + if account and _AUTO_CREATE_CHARACTER_WITH_ACCOUNT: # Auto-create a character to go with this account character, errs = account.create_character( @@ -1237,7 +1260,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ # set an (empty) attribute holding the characters this account has - lockstring = "attrread:perm(Admins);attredit:perm(Admins);" "attrcreate:perm(Admins);" + lockstring = "attrread:perm(Admins);attredit:perm(Admins);attrcreate:perm(Admins);" self.attributes.add("_playable_characters", [], lockstring=lockstring) self.attributes.add("_saved_protocol_flags", {}, lockstring=lockstring) @@ -1434,7 +1457,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): Notes: This is called *before* an eventual Character's `at_post_login` hook. By default it is used to set up - auto-puppeting based on `MULTISESSION_MODE`. + auto-puppeting based on `MULTISESSION_MODE` """ # if we have saved protocol flags on ourselves, load them here. @@ -1447,23 +1470,15 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): session.msg(logged_in={}) self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key)) - if _MULTISESSION_MODE == 0: - # in this mode we should have only one character available. We - # try to auto-connect to our last connected object, if any + if _AUTO_PUPPET_ON_LOGIN: + # in this mode we try to auto-connect to our last connected object, if any try: self.puppet_object(session, self.db._last_puppet) except RuntimeError: self.msg(_("The Character does not exist.")) return - elif _MULTISESSION_MODE == 1: - # in this mode all sessions connect to the same puppet. - try: - self.puppet_object(session, self.db._last_puppet) - except RuntimeError: - self.msg(_("The Character does not exist.")) - return - elif _MULTISESSION_MODE in (2, 3): - # In this mode we by default end up at a character selection + 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. @@ -1583,6 +1598,24 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ pass + ooc_appearance_template = """ +-------------------------------------------------------------------- +{header} + +{sessions} + + |whelp|n - more commands + |wpublic |n - talk on public channel + |wcharcreate [=description]|n - create new character + |wchardelete |n - delete a character + |wic |n - enter the game as character (|wooc|n to get back here) + |wic|n - enter the game as latest character controlled. + +{characters} +{footer} +-------------------------------------------------------------------- +""".strip() + def at_look(self, target=None, session=None, **kwargs): """ Called when this object executes a look. It allows to customize @@ -1590,7 +1623,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): Args: target (Object or list, optional): An object or a list - objects to inspect. + objects to inspect. This is normally a list of characters. session (Session, optional): The session doing this look. **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default). @@ -1607,94 +1640,75 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): return target.return_appearance(self) else: return f"{target} has no in-game appearance." - else: - # list of targets - make list to disconnect from db - characters = list(tar for tar in target if tar) if target else [] - sessions = self.sessions.all() - if not sessions: - # no sessions, nothing to report - return "" - is_su = self.is_superuser - # text shown when looking in the ooc area - result = [f"Account |g{self.key}|n (you are Out-of-Character)"] + # multiple targets - this is a list of characters + characters = list(tar for tar in target if tar) if target else [] + ncars = len(characters) + sessions = self.sessions.all() + nsess = len(sessions) - nsess = len(sessions) - result.append( - nsess == 1 - and "\n\n|wConnected session:|n" - or f"\n\n|wConnected sessions ({nsess}):|n" + if not nsess: + # no sessions, nothing to report + return "" + + # header text + txt_header = f"Account |g{self.name}|n (you are Out-of-Character)" + + # sessions + sess_strings = [] + for isess, sess in enumerate(sessions): + ip_addr = sess.address[0] if isinstance(sess.address, tuple) else sess.address + addr = f"{sess.protocol_key} ({ip_addr})" + sess_str = ( + f"|w* {isess + 1}|n" + if session and session.sessid == sess.sessid + else f" {isess + 1}" ) - for isess, sess in enumerate(sessions): - csessid = sess.sessid - addr = "%s (%s)" % ( - sess.protocol_key, - isinstance(sess.address, tuple) and str(sess.address[0]) or str(sess.address), - ) - result.append( - "\n %s %s" - % ( - session - and session.sessid == csessid - and "|w* %s|n" % (isess + 1) - or " %s" % (isess + 1), - addr, - ) - ) - result.append("\n\n |whelp|n - more commands") - result.append("\n |wpublic |n - talk on public channel") - charmax = _MAX_NR_CHARACTERS + sess_strings.append(f"{sess_str} {addr}") - if is_su or len(characters) < charmax: - if not characters: - result.append( - "\n\n You don't have any characters yet. See |whelp charcreate|n for " - "creating one." - ) + txt_sessions = "|wConnected session(s):|n\n" + "\n".join(sess_strings) + + if not characters: + txt_characters = "You don't have a character yet. Use |wcharcreate|n." + else: + max_chars = ( + "unlimited" + if self.is_superuser or _MAX_NR_CHARACTERS is None + else _MAX_NR_CHARACTERS + ) + + char_strings = [] + for char in characters: + csessions = char.sessions.all() + if csessions: + for sess in csessions: + # character is already puppeted + sid = sess in sessions and sessions.index(sess) + 1 + if sess and sid: + char_strings.append( + f" - |G{char.name}|n [{', '.join(char.permissions.all())}] " + f"(played by you in session {sid})" + ) + else: + char_strings.append( + f" - |R{char.name}|n [{', '.join(char.permissions.all())}] " + "(played by someone else)" + ) else: - result.append("\n |wcharcreate [=description]|n - create new character") - result.append( - "\n |wchardelete |n - delete a character (cannot be undone!)" - ) + # character is "free to puppet" + char_strings.append(f" - {char.name} [{', '.join(char.permissions.all())}]") - if characters: - string_s_ending = len(characters) > 1 and "s" or "" - result.append("\n |wic |n - enter the game (|wooc|n to get back here)") - if is_su: - result.append( - f"\n\nAvailable character{string_s_ending} ({len(characters)}/unlimited):" - ) - else: - result.append( - "\n\nAvailable character%s%s:" - % ( - string_s_ending, - charmax > 1 and " (%i/%i)" % (len(characters), charmax) or "", - ) - ) - - for char in characters: - csessions = char.sessions.all() - if csessions: - for sess in csessions: - # character is already puppeted - sid = sess in sessions and sessions.index(sess) + 1 - if sess and sid: - result.append( - f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] " - f"(played by you in session {sid})" - ) - else: - result.append( - f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] " - "(played by someone else)" - ) - else: - # character is "free to puppet" - result.append(f"\n - {char.key} [{', '.join(char.permissions.all())}]") - look_string = ("-" * 68) + "\n" + "".join(result) + "\n" + ("-" * 68) - return look_string + txt_characters = ( + f"Available character(s) ({ncars}/{max_chars}, |wic |n to play):|n\n" + + "\n".join(char_strings) + ) + return self.ooc_appearance_template.format( + header=txt_header, + sessions=txt_sessions, + characters=txt_characters, + footer="", + ) class DefaultGuest(DefaultAccount): @@ -1789,8 +1803,9 @@ class DefaultGuest(DefaultAccount): def at_post_login(self, session=None, **kwargs): """ - In theory, guests only have one character regardless of which - MULTISESSION_MODE we're in. They don't get a choice. + By default, Guests only have one character regardless of which + MAX_NR_CHARACTERS we use. They also always auto-puppet a matching + character and don't get a choice. Args: session (Session, optional): Session connecting. diff --git a/evennia/accounts/models.py b/evennia/accounts/models.py index c9cf774410..50926da14b 100644 --- a/evennia/accounts/models.py +++ b/evennia/accounts/models.py @@ -16,22 +16,16 @@ account info and OOC account configuration variables etc. """ from django.conf import settings -from django.db import models from django.contrib.auth.models import AbstractUser +from django.db import models from django.utils.encoding import smart_str - from evennia.accounts.manager import AccountDBManager +from evennia.server.signals import SIGNAL_ACCOUNT_POST_RENAME from evennia.typeclasses.models import TypedObject from evennia.utils.utils import make_iter -from evennia.server.signals import SIGNAL_ACCOUNT_POST_RENAME __all__ = ("AccountDB",) -# _ME = _("me") -# _SELF = _("self") - -_MULTISESSION_MODE = settings.MULTISESSION_MODE - _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ @@ -94,8 +88,10 @@ class AccountDB(TypedObject, AbstractUser): "cmdset", max_length=255, null=True, - help_text="optional python path to a cmdset class. If creating a Character, this will " - "default to settings.CMDSET_CHARACTER.", + help_text=( + "optional python path to a cmdset class. If creating a Character, this will " + "default to settings.CMDSET_CHARACTER." + ), ) # marks if this is a "virtual" bot account object db_is_bot = models.BooleanField( diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index b8c37df266..93a2eebf21 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -20,14 +20,15 @@ method. Otherwise all text will be returned to all connected sessions. """ import time from codecs import lookup as codecs_lookup + from django.conf import settings from evennia.server.sessionhandler import SESSIONS -from evennia.utils import utils, create, logger, search +from evennia.utils import create, logger, search, utils COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS) _MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS -_MULTISESSION_MODE = settings.MULTISESSION_MODE +_AUTO_PUPPET_ON_LOGIN = settings.AUTO_PUPPET_ON_LOGIN # limit symbol import for API __all__ = ( @@ -59,11 +60,6 @@ class MuxAccountLookCommand(COMMAND_DEFAULT_CLASS): super().parse() - if _MULTISESSION_MODE < 2: - # only one character allowed - not used in this mode - self.playable = None - return - playable = self.account.db._playable_characters if playable is not None: # clean up list if character object was deleted in between @@ -111,8 +107,14 @@ class CmdOOCLook(MuxAccountLookCommand): def func(self): """implement the ooc look command""" - if _MULTISESSION_MODE < 2: - # only one character allowed + if self.session.puppet: + # if we are puppeting, this is only reached in the case the that puppet + # has no look command on its own. + self.msg("You currently have no ability to look around.") + return + + if _AUTO_PUPPET_ON_LOGIN and _MAX_NR_CHARACTERS == 1 and self.playable: + # only one exists and is allowed - simplify self.msg("You are out-of-character (OOC).\nUse |wic|n to get back into the game.") return @@ -149,14 +151,16 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS): key = self.lhs desc = self.rhs - charmax = _MAX_NR_CHARACTERS - - if not account.is_superuser and ( - account.db._playable_characters and len(account.db._playable_characters) >= charmax - ): - plural = "" if charmax == 1 else "s" - self.msg(f"You may only create a maximum of {charmax} character{plural}.") - return + if _MAX_NR_CHARACTERS is not None: + 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 + ): + 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 typeclass = settings.BASE_CHARACTER_TYPECLASS @@ -177,8 +181,8 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS): ) # 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) + "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) if desc: @@ -228,7 +232,8 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS): return elif len(match) > 1: self.msg( - "Aborting - there are two characters with the same name. Ask an admin to delete the right one." + "Aborting - there are two characters with the same name. Ask an admin to delete the" + " right one." ) return else: # one match @@ -419,8 +424,8 @@ class CmdOOC(MuxAccountLookCommand): account.unpuppet_object(session) self.msg("\n|GYou go OOC.|n\n") - if _MULTISESSION_MODE < 2: - # only one character allowed + if _AUTO_PUPPET_ON_LOGIN and _MAX_NR_CHARACTERS == 1 and self.playable: + # only one character exists and is allowed - simplify self.msg("You are out-of-character (OOC).\nUse |wic|n to get back into the game.") return @@ -917,7 +922,10 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS): % (5 - ir, 5 - ig, 5 - ib, ir, ig, ib, "||[%i%i%i" % (ir, ig, ib)) ) table = self.table_format(table) - string = "Xterm256 colors (if not all hues show, your client might not report that it can handle xterm256):" + string = ( + "Xterm256 colors (if not all hues show, your client might not report that it can" + " handle xterm256):" + ) string += "\n" + "\n".join("".join(row) for row in table) table = [[], [], [], [], [], [], [], [], [], [], [], []] for ibatch in range(4): @@ -985,9 +993,7 @@ class CmdQuell(COMMAND_DEFAULT_CLASS): """Perform the command""" account = self.account permstr = ( - account.is_superuser - and " (superuser)" - or "(%s)" % (", ".join(account.permissions.all())) + account.is_superuser and " (superuser)" or "(%s)" % ", ".join(account.permissions.all()) ) if self.cmdstring in ("unquell", "unquell"): if not account.attributes.get("_quell"): diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 3d677e978e..5b92eda25e 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -12,41 +12,34 @@ main test suite started with """ import datetime +from unittest.mock import MagicMock, Mock, patch + from anything import Anything - -from parameterized import parameterized from django.conf import settings -from twisted.internet import task -from unittest.mock import patch, Mock, MagicMock - -from evennia import DefaultRoom, DefaultExit, ObjectDB -from evennia.commands.default.cmdset_character import CharacterCmdSet -from evennia.utils.test_resources import ( - BaseEvenniaTest, - BaseEvenniaCommandTest, - EvenniaCommandTest, -) # noqa -from evennia.commands.default import ( - help as help_module, - general, - system, - admin, - account, - building, - batchprocess, - comms, - unloggedin, - syscommands, +from django.test import override_settings +from evennia import ( + DefaultCharacter, + DefaultExit, + DefaultObject, + DefaultRoom, + ObjectDB, + search_object, ) -from evennia.commands.default.muxcommand import MuxCommand -from evennia.commands.command import Command, InterruptCommand from evennia.commands import cmdparser from evennia.commands.cmdset import CmdSet -from evennia.utils import utils, gametime, create -from evennia.server.sessionhandler import SESSIONS -from evennia import search_object -from evennia import DefaultObject, DefaultCharacter +from evennia.commands.command import Command, InterruptCommand +from evennia.commands.default import account, admin, batchprocess, building, comms, general +from evennia.commands.default import help as help_module +from evennia.commands.default import syscommands, system, unloggedin +from evennia.commands.default.cmdset_character import CharacterCmdSet +from evennia.commands.default.muxcommand import MuxCommand from evennia.prototypes import prototypes as protlib +from evennia.server.sessionhandler import SESSIONS +from evennia.utils import create, gametime, utils +from evennia.utils.test_resources import BaseEvenniaCommandTest # noqa +from evennia.utils.test_resources import BaseEvenniaTest, EvenniaCommandTest +from parameterized import parameterized +from twisted.internet import task # ------------------------------------------------------------ # Command testing @@ -232,19 +225,16 @@ class TestHelp(BaseEvenniaCommandTest): ), ( "test/extra/subsubtopic", # partial subsub-match - "Help for test/creating extra stuff/subsubtopic\n\n" "A subsubtopic text", + "Help for test/creating extra stuff/subsubtopic\n\nA subsubtopic text", ), ( "test/creating extra/subsub", # partial subsub-match - "Help for test/creating extra stuff/subsubtopic\n\n" "A subsubtopic text", + "Help for test/creating extra stuff/subsubtopic\n\nA subsubtopic text", ), - ("test/Something else", "Help for test/something else\n\n" "Something else"), # case + ("test/Something else", "Help for test/something else\n\nSomething else"), # case ( "test/More", # case - "Help for test/more\n\n" - "Another text\n\n" - "Subtopics:\n" - " test/more/second-more", + "Help for test/more\n\nAnother text\n\nSubtopics:\n test/more/second-more", ), ( "test/More/Second-more", @@ -264,11 +254,11 @@ class TestHelp(BaseEvenniaCommandTest): ), ( "test/more/second/more again", - "Help for test/more/second-more/more again\n\n" "Even more text.\n", + "Help for test/more/second-more/more again\n\nEven more text.\n", ), ( "test/more/second/third", - "Help for test/more/second-more/third more\n\n" "Third more text\n", + "Help for test/more/second-more/third more\n\nThird more text\n", ), ] ) @@ -520,7 +510,7 @@ class TestCmdTasks(BaseEvenniaCommandTest): def test_misformed_command(self): wanted_msg = ( - "Task command misformed.|Proper format tasks[/switch] " "[function name or task id]" + "Task command misformed.|Proper format tasks[/switch] [function name or task id]" ) self.call(system.CmdTasks(), f"/cancel", wanted_msg) @@ -557,18 +547,49 @@ class TestAdmin(BaseEvenniaCommandTest): class TestAccount(BaseEvenniaCommandTest): - def test_ooc_look(self): - if settings.MULTISESSION_MODE < 2: - self.call( - account.CmdOOCLook(), "", "You are out-of-character (OOC).", caller=self.account - ) - if settings.MULTISESSION_MODE == 2: - self.call( - account.CmdOOCLook(), - "", - "Account TestAccount (you are OutofCharacter)", - caller=self.account, - ) + """ + Test different account-specific modes + + """ + + @parameterized.expand( + # multisession-mode, auto-puppet, max_nr_characters + [ + (0, True, 1, "You are out-of-character"), + (1, True, 1, "You are out-of-character"), + (2, True, 1, "You are out-of-character"), + (3, True, 1, "You are out-of-character"), + (0, False, 1, "Account TestAccount"), + (1, False, 1, "Account TestAccount"), + (2, False, 1, "Account TestAccount"), + (3, False, 1, "Account TestAccount"), + (0, True, 2, "Account TestAccount"), + (1, True, 2, "Account TestAccount"), + (2, True, 2, "Account TestAccount"), + (3, True, 2, "Account TestAccount"), + (0, False, 2, "Account TestAccount"), + (1, False, 2, "Account TestAccount"), + (2, False, 2, "Account TestAccount"), + (3, False, 2, "Account TestAccount"), + ] + ) + def test_ooc_look(self, multisession_mode, auto_puppet, max_nr_chars, expected_result): + + self.account.db._playable_characters = [self.char1] + self.account.unpuppet_all() + + with self.settings(MULTISESSION=multisession_mode): + # we need to patch the module header instead of settings + with patch("evennia.commands.default.account._MAX_NR_CHARACTERS", new=max_nr_chars): + with patch( + "evennia.commands.default.account._AUTO_PUPPET_ON_LOGIN", new=auto_puppet + ): + self.call( + account.CmdOOCLook(), + "", + expected_result, + caller=self.account, + ) def test_ooc(self): self.call(account.CmdOOC(), "", "You go OOC.", caller=self.account) @@ -901,7 +922,8 @@ class TestBuilding(BaseEvenniaCommandTest): self.call( building.CmdSetAttribute(), "Obj/test2[+'three']", - "Attribute Obj/test2[+'three'] [category:None] does not exist. (Nested lookups attempted)", + "Attribute Obj/test2[+'three'] [category:None] does not exist. (Nested lookups" + " attempted)", ) self.call( building.CmdSetAttribute(), @@ -1091,7 +1113,8 @@ class TestBuilding(BaseEvenniaCommandTest): self.call( building.CmdSetAttribute(), "Obj/test4[0]['one']", - "Attribute Obj/test4[0]['one'] [category:None] does not exist. (Nested lookups attempted)", + "Attribute Obj/test4[0]['one'] [category:None] does not exist. (Nested lookups" + " attempted)", ) def test_split_nested_attr(self): @@ -1339,7 +1362,8 @@ class TestBuilding(BaseEvenniaCommandTest): self.call( building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultExit", - "Obj already has the typeclass 'evennia.objects.objects.DefaultExit'. Use /force to override.", + "Obj already has the typeclass 'evennia.objects.objects.DefaultExit'. Use /force to" + " override.", ) self.call( building.CmdTypeclass(), @@ -1355,9 +1379,9 @@ class TestBuilding(BaseEvenniaCommandTest): self.call( building.CmdTypeclass(), "Obj", - "Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n" - "Only the at_object_creation hook was run (update mode). Attributes set before swap were not removed\n" - "(use `swap` or `type/reset` to clear all).", + "Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\nOnly the" + " at_object_creation hook was run (update mode). Attributes set before swap were not" + " removed\n(use `swap` or `type/reset` to clear all).", cmdstring="update", ) self.call( @@ -1560,9 +1584,8 @@ class TestBuilding(BaseEvenniaCommandTest): self.call( building.CmdTeleport(), "Obj = Room2", - "Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2.".format( - oid, rid, rid2 - ), + "Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2." + .format(oid, rid, rid2), ) self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.") self.call( @@ -1598,7 +1621,7 @@ class TestBuilding(BaseEvenniaCommandTest): self.call( building.CmdTag(), "Obj", - "Tags on Obj: 'testtag', 'testtag2', " "'testtag2' (category: category1), 'testtag3'", + "Tags on Obj: 'testtag', 'testtag2', 'testtag2' (category: category1), 'testtag3'", ) self.call(building.CmdTag(), "/search NotFound", "No objects found with tag 'NotFound'.") @@ -1654,7 +1677,7 @@ class TestBuilding(BaseEvenniaCommandTest): self.call( building.CmdSpawn(), - "/save {'key':'Test Char', " "'typeclass':'evennia.objects.objects.DefaultCharacter'}", + "/save {'key':'Test Char', 'typeclass':'evennia.objects.objects.DefaultCharacter'}", "A prototype_key must be given, either as `prototype_key = ` or as " "a key 'prototype_key' inside the prototype structure.", ) @@ -1678,7 +1701,8 @@ class TestBuilding(BaseEvenniaCommandTest): self.call( building.CmdSpawn(), "{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', " - "'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, + "'key':'goblin', 'location':'%s'}" + % spawnLoc.dbref, "Spawned goblin", ) goblin = get_object(self, "goblin") @@ -1725,7 +1749,8 @@ class TestBuilding(BaseEvenniaCommandTest): # Location should be the specified location. self.call( building.CmdSpawn(), - "/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo', 'location':'%s'}" + "/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo'," + " 'location':'%s'}" % spawnLoc.dbref, "Spawned Ball", ) @@ -1785,8 +1810,8 @@ class TestBuilding(BaseEvenniaCommandTest): import evennia.commands.default.comms as cmd_comms # noqa -from evennia.utils.create import create_channel # noqa from evennia.comms.comms import DefaultChannel # noqa +from evennia.utils.create import create_channel # noqa @patch("evennia.commands.default.comms.CHANNEL_DEFAULT_TYPECLASS", DefaultChannel) @@ -1986,7 +2011,8 @@ class TestBatchProcess(BaseEvenniaCommandTest): self.call( batchprocess.CmdBatchCommands(), "batchprocessor.example_batch_cmds", - "Running Batch-command processor - Automatic mode for batchprocessor.example_batch_cmds", + "Running Batch-command processor - Automatic mode for" + " batchprocessor.example_batch_cmds", ) # we make sure to delete the button again here to stop the running reactor confirm = building.CmdDestroy.confirm @@ -2018,7 +2044,8 @@ class TestUnconnectedCommand(BaseEvenniaCommandTest): # instead of using SERVER_START_TIME (0), we use 86400 because Windows won't let us use anything lower gametime.SERVER_START_TIME = 86400 expected = ( - "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" + "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END" + " INFO" % ( settings.SERVERNAME, datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), diff --git a/evennia/commands/default/unloggedin.py b/evennia/commands/default/unloggedin.py index 7bd6cc7ffc..9a32611757 100644 --- a/evennia/commands/default/unloggedin.py +++ b/evennia/commands/default/unloggedin.py @@ -1,15 +1,16 @@ """ Commands that are available from the connect screen. + """ -import re import datetime +import re from codecs import lookup as codecs_lookup + from django.conf import settings +from evennia.commands.cmdhandler import CMD_LOGINSTART from evennia.comms.models import ChannelDB from evennia.server.sessionhandler import SESSIONS - -from evennia.utils import class_from_module, create, logger, utils, gametime -from evennia.commands.cmdhandler import CMD_LOGINSTART +from evennia.utils import class_from_module, create, gametime, logger, utils COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -25,7 +26,6 @@ __all__ = ( "CmdUnconnectedScreenreader", ) -MULTISESSION_MODE = settings.MULTISESSION_MODE CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE @@ -215,7 +215,7 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS): session.msg("Aborted. If your user name contains spaces, surround it by quotes.") return - # everything's ok. Create the new account account. + # everything's ok. Create the new player account. account, errors = Account.create( username=username, password=password, ip=address, session=session ) @@ -447,7 +447,8 @@ class CmdUnconnectedInfo(COMMAND_DEFAULT_CLASS): def func(self): self.caller.msg( - "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" + "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END" + " INFO" % ( settings.SERVERNAME, datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), @@ -468,8 +469,8 @@ def _create_account(session, accountname, password, permissions, typeclass=None, except Exception as e: session.msg( - "There was an error creating the Account:\n%s\n If this problem persists, contact an admin." - % e + "There was an error creating the Account:\n%s\n If this problem persists, contact an" + " admin." % e ) logger.log_trace() return False @@ -490,7 +491,7 @@ def _create_account(session, accountname, password, permissions, typeclass=None, 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 MULTISESSION_MODE < 2 situations. + This is meant for Guest and AUTO_CREATRE_CHARACTER_WITH_ACCOUNT=True situations. """ try: new_character = create.create_object( @@ -512,7 +513,7 @@ def _create_character(session, new_account, typeclass, home, permissions): 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 + "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/base_systems/email_login/email_login.py b/evennia/contrib/base_systems/email_login/email_login.py index 8ff83045fd..8afc5beb28 100644 --- a/evennia/contrib/base_systems/email_login/email_login.py +++ b/evennia/contrib/base_systems/email_login/email_login.py @@ -31,19 +31,14 @@ after this change. The login splashscreen is taken from strings in the module given by settings.CONNECTION_SCREEN_MODULE. """ -import re + from django.conf import settings from evennia.accounts.models import AccountDB -from evennia.objects.models import ObjectDB -from evennia.server.models import ServerConfig - -from evennia.commands.cmdset import CmdSet -from evennia.utils import logger, utils, ansi -from evennia.commands.default.muxcommand import MuxCommand from evennia.commands.cmdhandler import CMD_LOGINSTART -from evennia.commands.default import ( - unloggedin as default_unloggedin, -) # Used in CmdUnconnectedCreate +from evennia.commands.cmdset import CmdSet +from evennia.commands.default.muxcommand import MuxCommand +from evennia.server.models import ServerConfig +from evennia.utils import ansi, class_from_module, utils # limit symbol import for API __all__ = ( @@ -54,7 +49,6 @@ __all__ = ( "CmdUnconnectedHelp", ) -MULTISESSION_MODE = settings.MULTISESSION_MODE CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE CONNECTION_SCREEN = "" try: @@ -162,21 +156,24 @@ class CmdUnconnectedCreate(MuxCommand): # this means we have a multi_word accountname. pop from the back. password = self.arglist.pop() email = self.arglist.pop() - # what remains is the accountname. - accountname = " ".join(self.arglist) + # what remains is the username. + username = " ".join(self.arglist) else: - accountname, email, password = self.arglist + username, email, password = self.arglist - accountname = accountname.replace('"', "") # remove " - accountname = accountname.replace("'", "") - self.accountinfo = (accountname, email, password) + username = username.replace('"', "") # remove " + username = username.replace("'", "") + self.accountinfo = (username, email, password) def func(self): """Do checks and create account""" + Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) + address = self.session.address + session = self.caller try: - accountname, email, password = self.accountinfo + username, email, password = self.accountinfo except ValueError: string = '\n\r Usage (without <>): create "" ' session.msg(string) @@ -188,85 +185,41 @@ class CmdUnconnectedCreate(MuxCommand): # check so the email at least looks ok. session.msg("'%s' is not a valid e-mail address." % email) return - # sanity checks - if not re.findall(r"^[\w. @+\-']+$", accountname) or not (0 < len(accountname) <= 30): - # this echoes the restrictions made by django's auth - # module (except not allowing spaces, for convenience of - # logging in). - string = "\n\r Accountname can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_/' only." - session.msg(string) - return - # strip excessive spaces in accountname - accountname = re.sub(r"\s+", " ", accountname).strip() - if AccountDB.objects.filter(username__iexact=accountname): - # account already exists (we also ignore capitalization here) - session.msg("Sorry, there is already an account with the name '%s'." % accountname) - return - if AccountDB.objects.get_account_from_email(email): - # email already set on an account - session.msg("Sorry, there is already an account with that email address.") - return - # Reserve accountnames found in GUEST_LIST - if settings.GUEST_LIST and accountname.lower() in ( - guest.lower() for guest in settings.GUEST_LIST - ): - string = "\n\r That name is reserved. Please choose another Accountname." - session.msg(string) - return - if not re.findall(r"^[\w. @+\-']+$", password) or not (3 < len(password)): - string = ( - "\n\r Password should be longer than 3 characters. Letters, spaces, digits and @/./+/-/_/' only." - "\nFor best security, make it longer than 8 characters. You can also use a phrase of" - "\nmany words if you enclose the password in double quotes." - ) - session.msg(string) - return - # Check IP and/or name bans - bans = ServerConfig.objects.conf("server_bans") - if bans and ( - any(tup[0] == accountname.lower() for tup in bans) - or any(tup[2].match(session.address) for tup in bans if tup[2]) - ): - # this is a banned IP or name! - string = ( - "|rYou have been banned and cannot continue from here." - "\nIf you feel this ban is in error, please email an admin.|x" + # pre-normalize username so the user know what they get + non_normalized_username = username + username = Account.normalize_username(username) + if non_normalized_username != username: + session.msg( + "Note: your username was normalized to strip spaces and remove characters " + "that could be visually confusing." ) - session.msg(string) - session.sessionhandler.disconnect(session, "Good bye! Disconnecting.") + + # have the user verify their new account was what they intended + answer = yield ( + f"You want to create an account '{username}' with email '{email}' and password " + f"'{password}'.\nIs this what you intended? [Y]/N?" + ) + if answer.lower() in ("n", "no"): + session.msg("Aborted. If your user name contains spaces, surround it by quotes.") return # everything's ok. Create the new player account. - try: - permissions = settings.PERMISSION_ACCOUNT_DEFAULT - typeclass = settings.BASE_CHARACTER_TYPECLASS - new_account = default_unloggedin._create_account( - session, accountname, password, permissions, email=email - ) - if new_account: - if MULTISESSION_MODE < 2: - default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) - default_unloggedin._create_character( - session, new_account, typeclass, default_home, permissions - ) - # tell the caller everything went well. - string = "A new account '%s' was created. Welcome!" - if " " in accountname: - string += ( - "\n\nYou can now log in with the command 'connect \"%s\" '." - ) - else: - string += "\n\nYou can now log with the command 'connect %s '." - session.msg(string % (accountname, email)) - - except Exception: - # We are in the middle between logged in and -not, so we have - # to handle tracebacks ourselves at this point. If we don't, - # we won't see any errors at all. - session.msg("An error occurred. Please e-mail an admin if the problem persists.") - logger.log_trace() - raise + account, errors = Account.create( + username=username, email=email, password=password, ip=address, session=session + ) + if account: + # tell the caller everything went well. + string = "A new account '%s' was created. Welcome!" + if " " in username: + string += ( + "\n\nYou can now log in with the command 'connect \"%s\" '." + ) + else: + string += "\n\nYou can now log with the command 'connect %s '." + session.msg(string % (username, username)) + else: + session.msg("|R%s|n" % "\n".join(errors)) class CmdUnconnectedQuit(MuxCommand): diff --git a/evennia/contrib/base_systems/email_login/tests.py b/evennia/contrib/base_systems/email_login/tests.py index 20b203ee1c..a9d3dbcdad 100644 --- a/evennia/contrib/base_systems/email_login/tests.py +++ b/evennia/contrib/base_systems/email_login/tests.py @@ -4,6 +4,7 @@ Test email login. """ from evennia.commands.default.tests import BaseEvenniaCommandTest + from . import email_login @@ -13,17 +14,20 @@ class TestEmailLogin(BaseEvenniaCommandTest): email_login.CmdUnconnectedConnect(), "mytest@test.com test", "The email 'mytest@test.com' does not match any accounts.", + inputs=["Y"], ) self.call( email_login.CmdUnconnectedCreate(), '"mytest" mytest@test.com test11111', "A new account 'mytest' was created. Welcome!", + inputs=["Y"], ) self.call( email_login.CmdUnconnectedConnect(), "mytest@test.com test11111", "", caller=self.account.sessions.get()[0], + inputs=["Y"], ) def test_quit(self): diff --git a/evennia/server/deprecations.py b/evennia/server/deprecations.py index 06b0ec0817..a9f83dedeb 100644 --- a/evennia/server/deprecations.py +++ b/evennia/server/deprecations.py @@ -30,7 +30,7 @@ def check_errors(settings): raise DeprecationWarning(deprstring % ("CMDSET_OOC", "CMDSET_ACCOUNT")) if settings.WEBSERVER_ENABLED and not isinstance(settings.WEBSERVER_PORTS[0], tuple): raise DeprecationWarning( - "settings.WEBSERVER_PORTS must be on the form " "[(proxyport, serverport), ...]" + "settings.WEBSERVER_PORTS must be on the form [(proxyport, serverport), ...]" ) if hasattr(settings, "BASE_COMM_TYPECLASS"): raise DeprecationWarning(deprstring % ("BASE_COMM_TYPECLASS", "BASE_CHANNEL_TYPECLASS")) @@ -43,7 +43,7 @@ def check_errors(settings): "(see evennia/settings_default.py)." ) deprstring = ( - "settings.%s is now merged into settings.TYPECLASS_PATHS. " "Update your settings file." + "settings.%s is now merged into settings.TYPECLASS_PATHS. Update your settings file." ) if hasattr(settings, "OBJECT_TYPECLASS_PATHS"): raise DeprecationWarning(deprstring % "OBJECT_TYPECLASS_PATHS") @@ -147,6 +147,13 @@ def check_errors(settings): " 2. Rename your existing `static_overrides` folder to `static` instead." ) + if settings.MULTISESSION_MODE < 2 and settings.MAX_NR_SIMULTANEOUS_PUPPETS > 1: + raise DeprecationWarning( + f"settings.MULTISESSION_MODE={settings.MULTISESSION_MODE} is not compatible with " + f"settings.MAX_NR_SIMULTANEOUS_PUPPETS={settings.MAX_NR_SIMULTANEOUS_PUPPETS}. " + "To allow multiple simultaneous puppets, the multi-session mode must be higher than 1." + ) + def check_warnings(settings): """ diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index d116fef8cb..a69f8ed72f 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -13,22 +13,20 @@ There are two similar but separate stores of sessions: """ import time +from codecs import decode as codecs_decode from django.conf import settings -from evennia.commands.cmdhandler import CMD_LOGINSTART -from evennia.utils.logger import log_trace -from evennia.utils.utils import ( - is_iter, - make_iter, - delay, - callables_from_module, - class_from_module, -) -from evennia.server.portal import amp -from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT -from evennia.server.signals import SIGNAL_ACCOUNT_POST_FIRST_LOGIN, SIGNAL_ACCOUNT_POST_LAST_LOGOUT -from codecs import decode as codecs_decode from django.utils.translation import gettext as _ +from evennia.commands.cmdhandler import CMD_LOGINSTART +from evennia.server.portal import amp +from evennia.server.signals import ( + SIGNAL_ACCOUNT_POST_FIRST_LOGIN, + SIGNAL_ACCOUNT_POST_LAST_LOGOUT, + SIGNAL_ACCOUNT_POST_LOGIN, + SIGNAL_ACCOUNT_POST_LOGOUT, +) +from evennia.utils.logger import log_trace +from evennia.utils.utils import callables_from_module, class_from_module, delay, is_iter, make_iter _FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED _BROADCAST_SERVER_RESTART_MESSAGES = settings.BROADCAST_SERVER_RESTART_MESSAGES diff --git a/evennia/settings_default.py b/evennia/settings_default.py index abb7bf64d6..c0fcf6a5c0 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -249,7 +249,7 @@ EXTRA_LAUNCHER_COMMANDS = {} MAX_CHAR_LIMIT = 6000 # The warning to echo back to users if they enter a very large string MAX_CHAR_LIMIT_WARNING = ( - "You entered a string that was too long. " "Please break it up into multiple parts." + "You entered a string that was too long. Please break it up into multiple parts." ) # If this is true, errors and tracebacks from the engine will be # echoed as text in-game as well as to the log. This can speed up @@ -538,8 +538,6 @@ BASE_SCRIPT_TYPECLASS = "typeclasses.scripts.Script" # is Limbo (#2). DEFAULT_HOME = "#2" # The start position for new characters. Default is Limbo (#2). -# MULTISESSION_MODE = 0, 1 - used by default unloggedin create command -# MULTISESSION_MODE = 2, 3 - used by default character_create command START_LOCATION = "#2" # Lookups of Attributes, Tags, Nicks, Aliases can be aggressively # cached to avoid repeated database hits. This often gives noticeable @@ -709,21 +707,31 @@ GLOBAL_SCRIPTS = { ###################################################################### # Different Multisession modes allow a player (=account) to connect to the -# game simultaneously with multiple clients (=sessions). In modes 0,1 there is -# only one character created to the same name as the account at first login. -# In modes 2,3 no default character will be created and the MAX_NR_CHARACTERS -# value (below) defines how many characters the default char_create command -# allow per account. -# 0 - single session, one account, one character, when a new session is -# connected, the old one is disconnected -# 1 - multiple sessions, one account, one character, each session getting -# the same data -# 2 - multiple sessions, one account, many characters, one session per -# character (disconnects multiplets) -# 3 - like mode 2, except multiple sessions can puppet one character, each +# game simultaneously with multiple clients (=sessions). +# 0 - single session per account (if reconnecting, disconnect old session) +# 1 - multiple sessions per account, all sessions share output +# 2 - multiple sessions per account, one session allowed per puppet +# 3 - multiple sessions per account, multiple sessions per puppet (share output) # session getting the same data. MULTISESSION_MODE = 0 -# The maximum number of characters allowed by the default ooc char-creation command +# Whether we should create a character with the same name as the account when +# a new account is created. Together with AUTO_PUPPET_ON_LOGIN, this mimics +# a legacy MUD, where there is no difference between account and character. +AUTO_CREATE_CHARACTER_WITH_ACCOUNT = True +# Whether an account should auto-puppet the last puppeted puppet when logging in. This +# will only work if the session/puppet combination can be determined (usually +# MULTISESSION_MODE 0 or 1), otherwise, the player will end up OOC. Use +# MULTISESSION_MODE=0, AUTO_CREATE_CHARACTER_WITH_ACCOUNT=True and this value to +# mimic a legacy mud with minimal difference between Account and Character. Disable +# this and AUTO_PUPPET to get a chargen/character select screen on login. +AUTO_PUPPET_ON_LOGIN = True +# How many *different* characters an account can puppet *at the same time*. A value +# above 1 only makes a difference together with MULTISESSION_MODE > 1. +MAX_NR_SIMULTANEOUS_PUPPETS = 1 +# The maximum number of characters allowed by be created by the default ooc +# char-creation command. This can be seen as how big of a 'stable' of characters +# an account can have (not how many you can puppet at the same time). Set to +# None for no limit. MAX_NR_CHARACTERS = 1 # The access hierarchy, in climbing order. A higher permission in the # hierarchy includes access of all levels below it. Used by the perm()/pperm() diff --git a/evennia/utils/test_resources.py b/evennia/utils/test_resources.py index 53d054de37..cdd19046a7 100644 --- a/evennia/utils/test_resources.py +++ b/evennia/utils/test_resources.py @@ -22,26 +22,25 @@ Other: helper. Used by the command-test classes, but can be used for making a customt test class. """ -import sys import re +import sys import types -from twisted.internet.defer import Deferred + from django.conf import settings from django.test import TestCase, override_settings -from mock import Mock, patch, MagicMock -from evennia.objects.objects import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit +from evennia import settings_default from evennia.accounts.accounts import DefaultAccount +from evennia.commands.command import InterruptCommand +from evennia.commands.default.muxcommand import MuxCommand +from evennia.objects.objects import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom from evennia.scripts.scripts import DefaultScript from evennia.server.serversession import ServerSession from evennia.server.sessionhandler import SESSIONS -from evennia.utils import create +from evennia.utils import ansi, create from evennia.utils.idmapper.models import flush_cache from evennia.utils.utils import all_from_module, to_str -from evennia.utils import ansi -from evennia import settings_default -from evennia.commands.default.muxcommand import MuxCommand -from evennia.commands.command import InterruptCommand - +from mock import MagicMock, Mock, patch +from twisted.internet.defer import Deferred _RE_STRIP_EVMENU = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE) @@ -382,7 +381,8 @@ class EvenniaCommandTestMixin: inputs (list, optional): A list of strings to pass to functions that pause to take input from the user (normally using `@interactive` and `ret = yield(question)` or `evmenu.get_input`). Each element of the - list will be passed into the command as if the user wrote that at the prompt. + list will be passed into the command as if the user answered each prompt + in that order. raw_string (str, optional): Normally the `.raw_string` property is set as a combination of your `key/cmdname` and `input_args`. This allows direct control of what this is, for example for testing edge cases diff --git a/evennia/web/website/tests.py b/evennia/web/website/tests.py index 1ac5e1a9c5..bdf75fd494 100644 --- a/evennia/web/website/tests.py +++ b/evennia/web/website/tests.py @@ -1,11 +1,11 @@ from django.conf import settings -from django.utils.text import slugify from django.test import Client, override_settings from django.urls import reverse +from django.utils.text import slugify +from evennia.help import filehelp from evennia.utils import class_from_module from evennia.utils.create import create_help_entry from evennia.utils.test_resources import BaseEvenniaTest -from evennia.help import filehelp _FILE_HELP_ENTRIES = None @@ -216,7 +216,7 @@ class CharacterCreateView(EvenniaWebTest): url_name = "character-create" unauthenticated_response = 302 - @override_settings(MULTISESSION_MODE=0) + @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.db._playable_characters = [] @@ -237,10 +237,9 @@ class CharacterCreateView(EvenniaWebTest): % self.account.db._playable_characters, ) - @override_settings(MULTISESSION_MODE=2) - @override_settings(MAX_NR_CHARACTERS=10) + @override_settings(MAX_NR_CHARACTERS=5) def test_valid_access_multisession_2(self): - "Account1 should be able to create a new character" + "Account1 should be able to create multiple new characters" # Login account self.login() @@ -275,7 +274,8 @@ class CharacterPuppetView(EvenniaWebTest): response = self.client.get(reverse(self.url_name, kwargs=kwargs), follow=True) self.assertTrue( response.status_code >= 400, - "Invalid access should return a 4xx code-- either obj not found or permission denied! (Returned %s)" + "Invalid access should return a 4xx code-- either obj not found or permission denied!" + " (Returned %s)" % response.status_code, )