Merge branch 'develop' into contrib/evadventure

This commit is contained in:
Griatch 2022-09-17 13:23:13 +02:00
commit f05add7c5b
13 changed files with 381 additions and 357 deletions

View file

@ -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) startup modes. Used for more generic overriding (volund)
- New `search` lock type used to completely hide an object from being found by - New `search` lock type used to completely hide an object from being found by
the `DefaultObject.search` (`caller.search`) method. (CloudKeeper) 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 ## Evennia 0.9.5

View file

@ -45,6 +45,9 @@ _SESSIONS = None
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1)) _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
_MULTISESSION_MODE = settings.MULTISESSION_MODE _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 _MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS
_CMDSET_ACCOUNT = settings.CMDSET_ACCOUNT _CMDSET_ACCOUNT = settings.CMDSET_ACCOUNT
_MUDINFO_CHANNEL = None _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)) self.msg(_("|c{key}|R is already puppeted by another Account.").format(key=obj.key))
return return
# do the puppeting
if session.puppet: if session.puppet:
# cleanly unpuppet eventual previous object puppeted by this session # cleanly unpuppet eventual previous object puppeted by this session
self.unpuppet_object(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 # was left with a lingering account/session reference from an unclean
# server kill or similar # 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) obj.at_pre_puppet(self, session=session)
# do the connection # 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() username = kwargs.get("username", "").lower().strip()
# Check IP and/or name bans # Check IP and/or name bans
@ -772,6 +792,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
typeclass = kwargs.get("typeclass", cls) typeclass = kwargs.get("typeclass", cls)
ip = kwargs.get("ip", "") ip = kwargs.get("ip", "")
if isinstance(ip, (tuple, list)):
ip = ip[0]
if ip and CREATION_THROTTLE.check(ip): if ip and CREATION_THROTTLE.check(ip):
errors.append( errors.append(
_("You are creating too many accounts. Please log into an existing account.") _("You are creating too many accounts. Please log into an existing account.")
@ -843,7 +866,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
errors.append(string) errors.append(string)
logger.log_err(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 # Auto-create a character to go with this account
character, errs = account.create_character( character, errs = account.create_character(
@ -1237,7 +1260,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
""" """
# set an (empty) attribute holding the characters this account has # 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("_playable_characters", [], lockstring=lockstring)
self.attributes.add("_saved_protocol_flags", {}, lockstring=lockstring) self.attributes.add("_saved_protocol_flags", {}, lockstring=lockstring)
@ -1434,7 +1457,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
Notes: Notes:
This is called *before* an eventual Character's This is called *before* an eventual Character's
`at_post_login` hook. By default it is used to set up `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. # if we have saved protocol flags on ourselves, load them here.
@ -1447,23 +1470,15 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
session.msg(logged_in={}) session.msg(logged_in={})
self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key)) self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
if _MULTISESSION_MODE == 0: if _AUTO_PUPPET_ON_LOGIN:
# in this mode we should have only one character available. We # in this mode we try to auto-connect to our last connected object, if any
# try to auto-connect to our last connected object, if any
try: try:
self.puppet_object(session, self.db._last_puppet) self.puppet_object(session, self.db._last_puppet)
except RuntimeError: except RuntimeError:
self.msg(_("The Character does not exist.")) self.msg(_("The Character does not exist."))
return return
elif _MULTISESSION_MODE == 1: else:
# in this mode all sessions connect to the same puppet. # In this mode we don't auto-connect but by default end up at a character selection
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
# screen. We execute look on the account. # screen. We execute look on the account.
# we make sure to clean up the _playable_characters list in case # we make sure to clean up the _playable_characters list in case
# any was deleted in the interim. # any was deleted in the interim.
@ -1583,6 +1598,24 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
""" """
pass pass
ooc_appearance_template = """
--------------------------------------------------------------------
{header}
{sessions}
|whelp|n - more commands
|wpublic <text>|n - talk on public channel
|wcharcreate <name> [=description]|n - create new character
|wchardelete <name>|n - delete a character
|wic <name>|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): def at_look(self, target=None, session=None, **kwargs):
""" """
Called when this object executes a look. It allows to customize Called when this object executes a look. It allows to customize
@ -1590,7 +1623,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
Args: Args:
target (Object or list, optional): An object or a list 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. session (Session, optional): The session doing this look.
**kwargs (dict): Arbitrary, optional arguments for users **kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default). overriding the call (unused by default).
@ -1607,94 +1640,75 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
return target.return_appearance(self) return target.return_appearance(self)
else: else:
return f"{target} has no in-game appearance." 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 # multiple targets - this is a list of characters
result = [f"Account |g{self.key}|n (you are Out-of-Character)"] 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) if not nsess:
result.append( # no sessions, nothing to report
nsess == 1 return ""
and "\n\n|wConnected session:|n"
or f"\n\n|wConnected sessions ({nsess}):|n" # 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 <Text>|n - talk on public channel")
charmax = _MAX_NR_CHARACTERS sess_strings.append(f"{sess_str} {addr}")
if is_su or len(characters) < charmax: txt_sessions = "|wConnected session(s):|n\n" + "\n".join(sess_strings)
if not characters:
result.append( if not characters:
"\n\n You don't have any characters yet. See |whelp charcreate|n for " txt_characters = "You don't have a character yet. Use |wcharcreate|n."
"creating one." 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: else:
result.append("\n |wcharcreate <name> [=description]|n - create new character") # character is "free to puppet"
result.append( char_strings.append(f" - {char.name} [{', '.join(char.permissions.all())}]")
"\n |wchardelete <name>|n - delete a character (cannot be undone!)"
)
if characters: txt_characters = (
string_s_ending = len(characters) > 1 and "s" or "" f"Available character(s) ({ncars}/{max_chars}, |wic <name>|n to play):|n\n"
result.append("\n |wic <character>|n - enter the game (|wooc|n to get back here)") + "\n".join(char_strings)
if is_su: )
result.append( return self.ooc_appearance_template.format(
f"\n\nAvailable character{string_s_ending} ({len(characters)}/unlimited):" header=txt_header,
) sessions=txt_sessions,
else: characters=txt_characters,
result.append( footer="",
"\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
class DefaultGuest(DefaultAccount): class DefaultGuest(DefaultAccount):
@ -1789,8 +1803,9 @@ class DefaultGuest(DefaultAccount):
def at_post_login(self, session=None, **kwargs): def at_post_login(self, session=None, **kwargs):
""" """
In theory, guests only have one character regardless of which By default, Guests only have one character regardless of which
MULTISESSION_MODE we're in. They don't get a choice. MAX_NR_CHARACTERS we use. They also always auto-puppet a matching
character and don't get a choice.
Args: Args:
session (Session, optional): Session connecting. session (Session, optional): Session connecting.

View file

@ -16,22 +16,16 @@ account info and OOC account configuration variables etc.
""" """
from django.conf import settings from django.conf import settings
from django.db import models
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from evennia.accounts.manager import AccountDBManager from evennia.accounts.manager import AccountDBManager
from evennia.server.signals import SIGNAL_ACCOUNT_POST_RENAME
from evennia.typeclasses.models import TypedObject from evennia.typeclasses.models import TypedObject
from evennia.utils.utils import make_iter from evennia.utils.utils import make_iter
from evennia.server.signals import SIGNAL_ACCOUNT_POST_RENAME
__all__ = ("AccountDB",) __all__ = ("AccountDB",)
# _ME = _("me")
# _SELF = _("self")
_MULTISESSION_MODE = settings.MULTISESSION_MODE
_GA = object.__getattribute__ _GA = object.__getattribute__
_SA = object.__setattr__ _SA = object.__setattr__
_DA = object.__delattr__ _DA = object.__delattr__
@ -94,8 +88,10 @@ class AccountDB(TypedObject, AbstractUser):
"cmdset", "cmdset",
max_length=255, max_length=255,
null=True, null=True,
help_text="optional python path to a cmdset class. If creating a Character, this will " help_text=(
"default to settings.CMDSET_CHARACTER.", "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 # marks if this is a "virtual" bot account object
db_is_bot = models.BooleanField( db_is_bot = models.BooleanField(

View file

@ -20,14 +20,15 @@ method. Otherwise all text will be returned to all connected sessions.
""" """
import time import time
from codecs import lookup as codecs_lookup from codecs import lookup as codecs_lookup
from django.conf import settings from django.conf import settings
from evennia.server.sessionhandler import SESSIONS 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) COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
_MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS _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 # limit symbol import for API
__all__ = ( __all__ = (
@ -59,11 +60,6 @@ class MuxAccountLookCommand(COMMAND_DEFAULT_CLASS):
super().parse() 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 playable = self.account.db._playable_characters
if playable is not None: if playable is not None:
# clean up list if character object was deleted in between # clean up list if character object was deleted in between
@ -111,8 +107,14 @@ class CmdOOCLook(MuxAccountLookCommand):
def func(self): def func(self):
"""implement the ooc look command""" """implement the ooc look command"""
if _MULTISESSION_MODE < 2: if self.session.puppet:
# only one character allowed # 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.") self.msg("You are out-of-character (OOC).\nUse |wic|n to get back into the game.")
return return
@ -149,14 +151,16 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
key = self.lhs key = self.lhs
desc = self.rhs desc = self.rhs
charmax = _MAX_NR_CHARACTERS if _MAX_NR_CHARACTERS is not None:
if (
if not account.is_superuser and ( not account.is_superuser
account.db._playable_characters and len(account.db._playable_characters) >= charmax and not account.check_permstring("Developer")
): and account.db._playable_characters
plural = "" if charmax == 1 else "s" and len(account.db._playable_characters) >= _MAX_NR_CHARACTERS
self.msg(f"You may only create a maximum of {charmax} character{plural}.") ):
return 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 from evennia.objects.models import ObjectDB
typeclass = settings.BASE_CHARACTER_TYPECLASS typeclass = settings.BASE_CHARACTER_TYPECLASS
@ -177,8 +181,8 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
) )
# only allow creator (and developers) to puppet this char # only allow creator (and developers) to puppet this char
new_character.locks.add( new_character.locks.add(
"puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer);delete:id(%i) or perm(Admin)" "puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer);delete:id(%i) or"
% (new_character.id, account.id, account.id) " perm(Admin)" % (new_character.id, account.id, account.id)
) )
account.db._playable_characters.append(new_character) account.db._playable_characters.append(new_character)
if desc: if desc:
@ -228,7 +232,8 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
return return
elif len(match) > 1: elif len(match) > 1:
self.msg( 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 return
else: # one match else: # one match
@ -419,8 +424,8 @@ class CmdOOC(MuxAccountLookCommand):
account.unpuppet_object(session) account.unpuppet_object(session)
self.msg("\n|GYou go OOC.|n\n") self.msg("\n|GYou go OOC.|n\n")
if _MULTISESSION_MODE < 2: if _AUTO_PUPPET_ON_LOGIN and _MAX_NR_CHARACTERS == 1 and self.playable:
# only one character allowed # 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.") self.msg("You are out-of-character (OOC).\nUse |wic|n to get back into the game.")
return 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)) % (5 - ir, 5 - ig, 5 - ib, ir, ig, ib, "||[%i%i%i" % (ir, ig, ib))
) )
table = self.table_format(table) 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) string += "\n" + "\n".join("".join(row) for row in table)
table = [[], [], [], [], [], [], [], [], [], [], [], []] table = [[], [], [], [], [], [], [], [], [], [], [], []]
for ibatch in range(4): for ibatch in range(4):
@ -985,9 +993,7 @@ class CmdQuell(COMMAND_DEFAULT_CLASS):
"""Perform the command""" """Perform the command"""
account = self.account account = self.account
permstr = ( permstr = (
account.is_superuser account.is_superuser and " (superuser)" or "(%s)" % ", ".join(account.permissions.all())
and " (superuser)"
or "(%s)" % (", ".join(account.permissions.all()))
) )
if self.cmdstring in ("unquell", "unquell"): if self.cmdstring in ("unquell", "unquell"):
if not account.attributes.get("_quell"): if not account.attributes.get("_quell"):

View file

@ -12,41 +12,34 @@ main test suite started with
""" """
import datetime import datetime
from unittest.mock import MagicMock, Mock, patch
from anything import Anything from anything import Anything
from parameterized import parameterized
from django.conf import settings from django.conf import settings
from twisted.internet import task from django.test import override_settings
from unittest.mock import patch, Mock, MagicMock from evennia import (
DefaultCharacter,
from evennia import DefaultRoom, DefaultExit, ObjectDB DefaultExit,
from evennia.commands.default.cmdset_character import CharacterCmdSet DefaultObject,
from evennia.utils.test_resources import ( DefaultRoom,
BaseEvenniaTest, ObjectDB,
BaseEvenniaCommandTest, search_object,
EvenniaCommandTest,
) # noqa
from evennia.commands.default import (
help as help_module,
general,
system,
admin,
account,
building,
batchprocess,
comms,
unloggedin,
syscommands,
) )
from evennia.commands.default.muxcommand import MuxCommand
from evennia.commands.command import Command, InterruptCommand
from evennia.commands import cmdparser from evennia.commands import cmdparser
from evennia.commands.cmdset import CmdSet from evennia.commands.cmdset import CmdSet
from evennia.utils import utils, gametime, create from evennia.commands.command import Command, InterruptCommand
from evennia.server.sessionhandler import SESSIONS from evennia.commands.default import account, admin, batchprocess, building, comms, general
from evennia import search_object from evennia.commands.default import help as help_module
from evennia import DefaultObject, DefaultCharacter 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.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 # Command testing
@ -232,19 +225,16 @@ class TestHelp(BaseEvenniaCommandTest):
), ),
( (
"test/extra/subsubtopic", # partial subsub-match "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 "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 "test/More", # case
"Help for test/more\n\n" "Help for test/more\n\nAnother text\n\nSubtopics:\n test/more/second-more",
"Another text\n\n"
"Subtopics:\n"
" test/more/second-more",
), ),
( (
"test/More/Second-more", "test/More/Second-more",
@ -264,11 +254,11 @@ class TestHelp(BaseEvenniaCommandTest):
), ),
( (
"test/more/second/more again", "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", "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): def test_misformed_command(self):
wanted_msg = ( 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) self.call(system.CmdTasks(), f"/cancel", wanted_msg)
@ -557,18 +547,49 @@ class TestAdmin(BaseEvenniaCommandTest):
class TestAccount(BaseEvenniaCommandTest): class TestAccount(BaseEvenniaCommandTest):
def test_ooc_look(self): """
if settings.MULTISESSION_MODE < 2: Test different account-specific modes
self.call(
account.CmdOOCLook(), "", "You are out-of-character (OOC).", caller=self.account """
)
if settings.MULTISESSION_MODE == 2: @parameterized.expand(
self.call( # multisession-mode, auto-puppet, max_nr_characters
account.CmdOOCLook(), [
"", (0, True, 1, "You are out-of-character"),
"Account TestAccount (you are OutofCharacter)", (1, True, 1, "You are out-of-character"),
caller=self.account, (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): def test_ooc(self):
self.call(account.CmdOOC(), "", "You go OOC.", caller=self.account) self.call(account.CmdOOC(), "", "You go OOC.", caller=self.account)
@ -901,7 +922,8 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2[+'three']", "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( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
@ -1091,7 +1113,8 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test4[0]['one']", "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): def test_split_nested_attr(self):
@ -1339,7 +1362,8 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdTypeclass(), building.CmdTypeclass(),
"Obj = evennia.objects.objects.DefaultExit", "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( self.call(
building.CmdTypeclass(), building.CmdTypeclass(),
@ -1355,9 +1379,9 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdTypeclass(), building.CmdTypeclass(),
"Obj", "Obj",
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n" "Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\nOnly the"
"Only the at_object_creation hook was run (update mode). Attributes set before swap were not removed\n" " at_object_creation hook was run (update mode). Attributes set before swap were not"
"(use `swap` or `type/reset` to clear all).", " removed\n(use `swap` or `type/reset` to clear all).",
cmdstring="update", cmdstring="update",
) )
self.call( self.call(
@ -1560,9 +1584,8 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdTeleport(), building.CmdTeleport(),
"Obj = Room2", "Obj = Room2",
"Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2.".format( "Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2."
oid, rid, rid2 .format(oid, rid, rid2),
),
) )
self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.") self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.")
self.call( self.call(
@ -1598,7 +1621,7 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdTag(), building.CmdTag(),
"Obj", "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'.") self.call(building.CmdTag(), "/search NotFound", "No objects found with tag 'NotFound'.")
@ -1654,7 +1677,7 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdSpawn(), 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 = <prototype>` or as " "A prototype_key must be given, either as `prototype_key = <prototype>` or as "
"a key 'prototype_key' inside the prototype structure.", "a key 'prototype_key' inside the prototype structure.",
) )
@ -1678,7 +1701,8 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdSpawn(), building.CmdSpawn(),
"{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', " "{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "'key':'goblin', 'location':'%s'}"
% spawnLoc.dbref,
"Spawned goblin", "Spawned goblin",
) )
goblin = get_object(self, "goblin") goblin = get_object(self, "goblin")
@ -1725,7 +1749,8 @@ class TestBuilding(BaseEvenniaCommandTest):
# Location should be the specified location. # Location should be the specified location.
self.call( self.call(
building.CmdSpawn(), 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, % spawnLoc.dbref,
"Spawned Ball", "Spawned Ball",
) )
@ -1785,8 +1810,8 @@ class TestBuilding(BaseEvenniaCommandTest):
import evennia.commands.default.comms as cmd_comms # noqa 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.comms.comms import DefaultChannel # noqa
from evennia.utils.create import create_channel # noqa
@patch("evennia.commands.default.comms.CHANNEL_DEFAULT_TYPECLASS", DefaultChannel) @patch("evennia.commands.default.comms.CHANNEL_DEFAULT_TYPECLASS", DefaultChannel)
@ -1986,7 +2011,8 @@ class TestBatchProcess(BaseEvenniaCommandTest):
self.call( self.call(
batchprocess.CmdBatchCommands(), batchprocess.CmdBatchCommands(),
"batchprocessor.example_batch_cmds", "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 # we make sure to delete the button again here to stop the running reactor
confirm = building.CmdDestroy.confirm 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 # instead of using SERVER_START_TIME (0), we use 86400 because Windows won't let us use anything lower
gametime.SERVER_START_TIME = 86400 gametime.SERVER_START_TIME = 86400
expected = ( 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, settings.SERVERNAME,
datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),

View file

@ -1,15 +1,16 @@
""" """
Commands that are available from the connect screen. Commands that are available from the connect screen.
""" """
import re
import datetime import datetime
import re
from codecs import lookup as codecs_lookup from codecs import lookup as codecs_lookup
from django.conf import settings from django.conf import settings
from evennia.commands.cmdhandler import CMD_LOGINSTART
from evennia.comms.models import ChannelDB from evennia.comms.models import ChannelDB
from evennia.server.sessionhandler import SESSIONS from evennia.server.sessionhandler import SESSIONS
from evennia.utils import class_from_module, create, gametime, logger, utils
from evennia.utils import class_from_module, create, logger, utils, gametime
from evennia.commands.cmdhandler import CMD_LOGINSTART
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -25,7 +26,6 @@ __all__ = (
"CmdUnconnectedScreenreader", "CmdUnconnectedScreenreader",
) )
MULTISESSION_MODE = settings.MULTISESSION_MODE
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE 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.") session.msg("Aborted. If your user name contains spaces, surround it by quotes.")
return return
# everything's ok. Create the new account account. # everything's ok. Create the new player account.
account, errors = Account.create( account, errors = Account.create(
username=username, password=password, ip=address, session=session username=username, password=password, ip=address, session=session
) )
@ -447,7 +447,8 @@ class CmdUnconnectedInfo(COMMAND_DEFAULT_CLASS):
def func(self): def func(self):
self.caller.msg( 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, settings.SERVERNAME,
datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), 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: except Exception as e:
session.msg( session.msg(
"There was an error creating the Account:\n%s\n If this problem persists, contact an admin." "There was an error creating the Account:\n%s\n If this problem persists, contact an"
% e " admin." % e
) )
logger.log_trace() logger.log_trace()
return False return False
@ -490,7 +491,7 @@ def _create_account(session, accountname, password, permissions, typeclass=None,
def _create_character(session, new_account, typeclass, home, permissions): def _create_character(session, new_account, typeclass, home, permissions):
""" """
Helper function, creates a character based on an account's name. 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: try:
new_character = create.create_object( 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 new_account.db._last_puppet = new_character
except Exception as e: except Exception as e:
session.msg( session.msg(
"There was an error creating the Character:\n%s\n If this problem persists, contact an admin." "There was an error creating the Character:\n%s\n If this problem persists, contact an"
% e " admin." % e
) )
logger.log_trace() logger.log_trace()

View file

@ -31,19 +31,14 @@ after this change. The login splashscreen is taken from strings in
the module given by settings.CONNECTION_SCREEN_MODULE. the module given by settings.CONNECTION_SCREEN_MODULE.
""" """
import re
from django.conf import settings from django.conf import settings
from evennia.accounts.models import AccountDB 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.cmdhandler import CMD_LOGINSTART
from evennia.commands.default import ( from evennia.commands.cmdset import CmdSet
unloggedin as default_unloggedin, from evennia.commands.default.muxcommand import MuxCommand
) # Used in CmdUnconnectedCreate from evennia.server.models import ServerConfig
from evennia.utils import ansi, class_from_module, utils
# limit symbol import for API # limit symbol import for API
__all__ = ( __all__ = (
@ -54,7 +49,6 @@ __all__ = (
"CmdUnconnectedHelp", "CmdUnconnectedHelp",
) )
MULTISESSION_MODE = settings.MULTISESSION_MODE
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
CONNECTION_SCREEN = "" CONNECTION_SCREEN = ""
try: try:
@ -162,21 +156,24 @@ class CmdUnconnectedCreate(MuxCommand):
# this means we have a multi_word accountname. pop from the back. # this means we have a multi_word accountname. pop from the back.
password = self.arglist.pop() password = self.arglist.pop()
email = self.arglist.pop() email = self.arglist.pop()
# what remains is the accountname. # what remains is the username.
accountname = " ".join(self.arglist) username = " ".join(self.arglist)
else: else:
accountname, email, password = self.arglist username, email, password = self.arglist
accountname = accountname.replace('"', "") # remove " username = username.replace('"', "") # remove "
accountname = accountname.replace("'", "") username = username.replace("'", "")
self.accountinfo = (accountname, email, password) self.accountinfo = (username, email, password)
def func(self): def func(self):
"""Do checks and create account""" """Do checks and create account"""
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
address = self.session.address
session = self.caller session = self.caller
try: try:
accountname, email, password = self.accountinfo username, email, password = self.accountinfo
except ValueError: except ValueError:
string = '\n\r Usage (without <>): create "<accountname>" <email> <password>' string = '\n\r Usage (without <>): create "<accountname>" <email> <password>'
session.msg(string) session.msg(string)
@ -188,85 +185,41 @@ class CmdUnconnectedCreate(MuxCommand):
# check so the email at least looks ok. # check so the email at least looks ok.
session.msg("'%s' is not a valid e-mail address." % email) session.msg("'%s' is not a valid e-mail address." % email)
return 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 # pre-normalize username so the user know what they get
bans = ServerConfig.objects.conf("server_bans") non_normalized_username = username
if bans and ( username = Account.normalize_username(username)
any(tup[0] == accountname.lower() for tup in bans) if non_normalized_username != username:
or any(tup[2].match(session.address) for tup in bans if tup[2]) session.msg(
): "Note: your username was normalized to strip spaces and remove characters "
# this is a banned IP or name! "that could be visually confusing."
string = (
"|rYou have been banned and cannot continue from here."
"\nIf you feel this ban is in error, please email an admin.|x"
) )
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 return
# everything's ok. Create the new player account. # everything's ok. Create the new player account.
try: account, errors = Account.create(
permissions = settings.PERMISSION_ACCOUNT_DEFAULT username=username, email=email, password=password, ip=address, session=session
typeclass = settings.BASE_CHARACTER_TYPECLASS )
new_account = default_unloggedin._create_account( if account:
session, accountname, password, permissions, email=email # tell the caller everything went well.
) string = "A new account '%s' was created. Welcome!"
if new_account: if " " in username:
if MULTISESSION_MODE < 2: string += (
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) "\n\nYou can now log in with the command 'connect \"%s\" <your password>'."
default_unloggedin._create_character( )
session, new_account, typeclass, default_home, permissions else:
) string += "\n\nYou can now log with the command 'connect %s <your password>'."
# tell the caller everything went well. session.msg(string % (username, username))
string = "A new account '%s' was created. Welcome!" else:
if " " in accountname: session.msg("|R%s|n" % "\n".join(errors))
string += (
"\n\nYou can now log in with the command 'connect \"%s\" <your password>'."
)
else:
string += "\n\nYou can now log with the command 'connect %s <your password>'."
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
class CmdUnconnectedQuit(MuxCommand): class CmdUnconnectedQuit(MuxCommand):

View file

@ -4,6 +4,7 @@ Test email login.
""" """
from evennia.commands.default.tests import BaseEvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import email_login from . import email_login
@ -13,17 +14,20 @@ class TestEmailLogin(BaseEvenniaCommandTest):
email_login.CmdUnconnectedConnect(), email_login.CmdUnconnectedConnect(),
"mytest@test.com test", "mytest@test.com test",
"The email 'mytest@test.com' does not match any accounts.", "The email 'mytest@test.com' does not match any accounts.",
inputs=["Y"],
) )
self.call( self.call(
email_login.CmdUnconnectedCreate(), email_login.CmdUnconnectedCreate(),
'"mytest" mytest@test.com test11111', '"mytest" mytest@test.com test11111',
"A new account 'mytest' was created. Welcome!", "A new account 'mytest' was created. Welcome!",
inputs=["Y"],
) )
self.call( self.call(
email_login.CmdUnconnectedConnect(), email_login.CmdUnconnectedConnect(),
"mytest@test.com test11111", "mytest@test.com test11111",
"", "",
caller=self.account.sessions.get()[0], caller=self.account.sessions.get()[0],
inputs=["Y"],
) )
def test_quit(self): def test_quit(self):

View file

@ -30,7 +30,7 @@ def check_errors(settings):
raise DeprecationWarning(deprstring % ("CMDSET_OOC", "CMDSET_ACCOUNT")) raise DeprecationWarning(deprstring % ("CMDSET_OOC", "CMDSET_ACCOUNT"))
if settings.WEBSERVER_ENABLED and not isinstance(settings.WEBSERVER_PORTS[0], tuple): if settings.WEBSERVER_ENABLED and not isinstance(settings.WEBSERVER_PORTS[0], tuple):
raise DeprecationWarning( 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"): if hasattr(settings, "BASE_COMM_TYPECLASS"):
raise DeprecationWarning(deprstring % ("BASE_COMM_TYPECLASS", "BASE_CHANNEL_TYPECLASS")) raise DeprecationWarning(deprstring % ("BASE_COMM_TYPECLASS", "BASE_CHANNEL_TYPECLASS"))
@ -43,7 +43,7 @@ def check_errors(settings):
"(see evennia/settings_default.py)." "(see evennia/settings_default.py)."
) )
deprstring = ( 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"): if hasattr(settings, "OBJECT_TYPECLASS_PATHS"):
raise DeprecationWarning(deprstring % "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." " 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): def check_warnings(settings):
""" """

View file

@ -13,22 +13,20 @@ There are two similar but separate stores of sessions:
""" """
import time import time
from codecs import decode as codecs_decode
from django.conf import settings 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 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 _FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED
_BROADCAST_SERVER_RESTART_MESSAGES = settings.BROADCAST_SERVER_RESTART_MESSAGES _BROADCAST_SERVER_RESTART_MESSAGES = settings.BROADCAST_SERVER_RESTART_MESSAGES

View file

@ -249,7 +249,7 @@ EXTRA_LAUNCHER_COMMANDS = {}
MAX_CHAR_LIMIT = 6000 MAX_CHAR_LIMIT = 6000
# The warning to echo back to users if they enter a very large string # The warning to echo back to users if they enter a very large string
MAX_CHAR_LIMIT_WARNING = ( 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 # 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 # 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). # is Limbo (#2).
DEFAULT_HOME = "#2" DEFAULT_HOME = "#2"
# The start position for new characters. Default is Limbo (#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" START_LOCATION = "#2"
# Lookups of Attributes, Tags, Nicks, Aliases can be aggressively # Lookups of Attributes, Tags, Nicks, Aliases can be aggressively
# cached to avoid repeated database hits. This often gives noticeable # 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 # Different Multisession modes allow a player (=account) to connect to the
# game simultaneously with multiple clients (=sessions). In modes 0,1 there is # game simultaneously with multiple clients (=sessions).
# only one character created to the same name as the account at first login. # 0 - single session per account (if reconnecting, disconnect old session)
# In modes 2,3 no default character will be created and the MAX_NR_CHARACTERS # 1 - multiple sessions per account, all sessions share output
# value (below) defines how many characters the default char_create command # 2 - multiple sessions per account, one session allowed per puppet
# allow per account. # 3 - multiple sessions per account, multiple sessions per puppet (share output)
# 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
# session getting the same data. # session getting the same data.
MULTISESSION_MODE = 0 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 MAX_NR_CHARACTERS = 1
# The access hierarchy, in climbing order. A higher permission in the # The access hierarchy, in climbing order. A higher permission in the
# hierarchy includes access of all levels below it. Used by the perm()/pperm() # hierarchy includes access of all levels below it. Used by the perm()/pperm()

View file

@ -22,26 +22,25 @@ Other:
helper. Used by the command-test classes, but can be used for making a customt test class. helper. Used by the command-test classes, but can be used for making a customt test class.
""" """
import sys
import re import re
import sys
import types import types
from twisted.internet.defer import Deferred
from django.conf import settings from django.conf import settings
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from mock import Mock, patch, MagicMock from evennia import settings_default
from evennia.objects.objects import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
from evennia.accounts.accounts import DefaultAccount 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.scripts.scripts import DefaultScript
from evennia.server.serversession import ServerSession from evennia.server.serversession import ServerSession
from evennia.server.sessionhandler import SESSIONS 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.idmapper.models import flush_cache
from evennia.utils.utils import all_from_module, to_str from evennia.utils.utils import all_from_module, to_str
from evennia.utils import ansi from mock import MagicMock, Mock, patch
from evennia import settings_default from twisted.internet.defer import Deferred
from evennia.commands.default.muxcommand import MuxCommand
from evennia.commands.command import InterruptCommand
_RE_STRIP_EVMENU = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE) _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 inputs (list, optional): A list of strings to pass to functions that pause to
take input from the user (normally using `@interactive` and take input from the user (normally using `@interactive` and
`ret = yield(question)` or `evmenu.get_input`). Each element of the `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 raw_string (str, optional): Normally the `.raw_string` property is set as
a combination of your `key/cmdname` and `input_args`. This allows a combination of your `key/cmdname` and `input_args`. This allows
direct control of what this is, for example for testing edge cases direct control of what this is, for example for testing edge cases

View file

@ -1,11 +1,11 @@
from django.conf import settings from django.conf import settings
from django.utils.text import slugify
from django.test import Client, override_settings from django.test import Client, override_settings
from django.urls import reverse 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 import class_from_module
from evennia.utils.create import create_help_entry from evennia.utils.create import create_help_entry
from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.test_resources import BaseEvenniaTest
from evennia.help import filehelp
_FILE_HELP_ENTRIES = None _FILE_HELP_ENTRIES = None
@ -216,7 +216,7 @@ class CharacterCreateView(EvenniaWebTest):
url_name = "character-create" url_name = "character-create"
unauthenticated_response = 302 unauthenticated_response = 302
@override_settings(MULTISESSION_MODE=0) @override_settings(MAX_NR_CHARACTERS=1)
def test_valid_access_multisession_0(self): def test_valid_access_multisession_0(self):
"Account1 with no characters should be able to create a new one" "Account1 with no characters should be able to create a new one"
self.account.db._playable_characters = [] self.account.db._playable_characters = []
@ -237,10 +237,9 @@ class CharacterCreateView(EvenniaWebTest):
% self.account.db._playable_characters, % self.account.db._playable_characters,
) )
@override_settings(MULTISESSION_MODE=2) @override_settings(MAX_NR_CHARACTERS=5)
@override_settings(MAX_NR_CHARACTERS=10)
def test_valid_access_multisession_2(self): 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 # Login account
self.login() self.login()
@ -275,7 +274,8 @@ class CharacterPuppetView(EvenniaWebTest):
response = self.client.get(reverse(self.url_name, kwargs=kwargs), follow=True) response = self.client.get(reverse(self.url_name, kwargs=kwargs), follow=True)
self.assertTrue( self.assertTrue(
response.status_code >= 400, 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, % response.status_code,
) )