Format code with black. Add makefile to run fmt/tests

This commit is contained in:
Griatch 2019-09-28 18:18:11 +02:00
parent 50d0292291
commit 21fe2c0e2c
299 changed files with 19037 additions and 11611 deletions

View file

@ -29,6 +29,7 @@ without arguments starts a full interactive Python console.
- Remove `pillow` requirement (install especially if using imagefield)
- Add Simplified Korean translation (user aceamro)
- Show warning on `start -l` if settings contains values unsafe for production.
- Make code auto-formatted with Black.
## Evennia 0.9 (2018-2019)

View file

@ -1,12 +1,39 @@
default: install
# This is used with `make <option>` and is used for running various
# administration operations on the code.
BLACK_FORMAT_CONFIGS = --target-version py37 --line-length 100
TEST_GAME_DIR = .test_game_dir
TESTS?=evennia
default:
@echo " Usage: "
@echo " make install - install evennia (recommended to activate virtualenv first)"
@echo " make fmt/format - run the black autoformatter on the source code"
@echo " make lint - run black in --check mode"
@echo " make test - run evennia test suite with all default values."
@echo " make tests=evennia.path test - run only specific test or tests."
@echo " make testp - run test suite using multiple cores."
install:
python setup.py develop
pip install -e .
fmt:
format:
black $(BLACK_FORMAT_CONFIGS) evennia
fmt: format
lint:
black --check $(BLACK_FORMAT_CONFIGS) evennia
test:
evennia --init $(TEST_GAME_DIR);\
cd $(TEST_GAME_DIR);\
evennia migrate;\
evennia test --keepdb $(TESTS);\
testp:
evennia --init $(TEST_GAME_DIR);\
cd $(TEST_GAME_DIR);\
evennia migrate;\
evennia test --keepdb --parallel 4 $(tests);\

View file

@ -136,14 +136,16 @@ def _create_version():
version = "Unknown"
root = os.path.dirname(os.path.abspath(__file__))
try:
with open(os.path.join(root, "VERSION.txt"), 'r') as f:
with open(os.path.join(root, "VERSION.txt"), "r") as f:
version = f.read().strip()
except IOError as err:
print(err)
try:
rev = check_output(
"git rev-parse --short HEAD",
shell=True, cwd=root, stderr=STDOUT).strip().decode()
rev = (
check_output("git rev-parse --short HEAD", shell=True, cwd=root, stderr=STDOUT)
.strip()
.decode()
)
version = "%s (rev %s)" % (version, rev)
except (IOError, CalledProcessError, OSError):
# ignore if we cannot get to git
@ -259,9 +261,10 @@ def _init():
def _help(self):
"Returns list of contents"
names = [name for name in self.__class__.__dict__ if not name.startswith('_')]
names += [name for name in self.__dict__ if not name.startswith('_')]
names = [name for name in self.__class__.__dict__ if not name.startswith("_")]
names += [name for name in self.__dict__ if not name.startswith("_")]
print(self.__doc__ + "-" * 60 + "\n" + ", ".join(names))
help = property(_help)
class DBmanagers(_EvContainer):
@ -281,6 +284,7 @@ def _init():
attributes - Attributes.objects
"""
from .help.models import HelpEntry
from .accounts.models import AccountDB
from .scripts.models import ScriptDB
@ -302,7 +306,7 @@ def _init():
tags = Tag.objects
# remove these so they are not visible as properties
del HelpEntry, AccountDB, ScriptDB, Msg, ChannelDB
#del ExternalChannelConnection
# del ExternalChannelConnection
del ObjectDB, ServerConfig, Tag, Attribute
managers = DBmanagers()
@ -325,15 +329,26 @@ def _init():
def __init__(self):
"populate the object with commands"
def add_cmds(module):
"helper method for populating this object with cmds"
from evennia.utils import utils
cmdlist = utils.variable_from_module(module, module.__all__)
self.__dict__.update(dict([(c.__name__, c) for c in cmdlist]))
from .commands.default import (admin, batchprocess,
building, comms, general,
account, help, system, unloggedin)
from .commands.default import (
admin,
batchprocess,
building,
comms,
general,
account,
help,
system,
unloggedin,
)
add_cmds(admin)
add_cmds(building)
add_cmds(batchprocess)
@ -367,19 +382,23 @@ def _init():
access the properties on the imported syscmdkeys object.
"""
from .commands import cmdhandler
CMD_NOINPUT = cmdhandler.CMD_NOINPUT
CMD_NOMATCH = cmdhandler.CMD_NOMATCH
CMD_MULTIMATCH = cmdhandler.CMD_MULTIMATCH
CMD_CHANNEL = cmdhandler.CMD_CHANNEL
CMD_LOGINSTART = cmdhandler.CMD_LOGINSTART
del cmdhandler
syscmdkeys = SystemCmds()
del SystemCmds
del _EvContainer
# typeclases
from .utils.utils import class_from_module
BASE_ACCOUNT_TYPECLASS = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
BASE_OBJECT_TYPECLASS = class_from_module(settings.BASE_OBJECT_TYPECLASS)
BASE_CHARACTER_TYPECLASS = class_from_module(settings.BASE_CHARACTER_TYPECLASS)
@ -422,20 +441,22 @@ def set_trace(term_size=(140, 40), debugger="auto"):
"""
import sys
dbg = None
if debugger in ('auto', 'pudb'):
if debugger in ("auto", "pudb"):
try:
from pudb import debugger
dbg = debugger.Debugger(stdout=sys.__stdout__,
term_size=term_size)
dbg = debugger.Debugger(stdout=sys.__stdout__, term_size=term_size)
except ImportError:
if debugger == 'pudb':
if debugger == "pudb":
raise
pass
if not dbg:
import pdb
dbg = pdb.Pdb(stdout=sys.__stdout__)
try:

View file

@ -26,11 +26,12 @@ from evennia.commands import cmdhandler
from evennia.server.models import ServerConfig
from evennia.server.throttle import Throttle
from evennia.utils import class_from_module, create, logger
from evennia.utils.utils import (lazy_property, to_str,
make_iter, is_iter,
variable_from_module)
from evennia.server.signals import (SIGNAL_ACCOUNT_POST_CREATE, SIGNAL_OBJECT_POST_PUPPET,
SIGNAL_OBJECT_POST_UNPUPPET)
from evennia.utils.utils import lazy_property, to_str, make_iter, is_iter, variable_from_module
from evennia.server.signals import (
SIGNAL_ACCOUNT_POST_CREATE,
SIGNAL_OBJECT_POST_PUPPET,
SIGNAL_OBJECT_POST_UNPUPPET,
)
from evennia.typeclasses.attributes import NickHandler
from evennia.scripts.scripthandler import ScriptHandler
from evennia.commands.cmdsethandler import CmdSetHandler
@ -43,7 +44,7 @@ __all__ = ("DefaultAccount",)
_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
_MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS
_CMDSET_ACCOUNT = settings.CMDSET_ACCOUNT
@ -202,12 +203,14 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
@lazy_property
def options(self):
return OptionHandler(self,
options_dict=settings.OPTIONS_ACCOUNT_DEFAULT,
savefunc=self.attributes.add,
loadfunc=self.attributes.get,
save_kwargs={"category": 'option'},
load_kwargs={"category": 'option'})
return OptionHandler(
self,
options_dict=settings.OPTIONS_ACCOUNT_DEFAULT,
savefunc=self.attributes.add,
loadfunc=self.attributes.get,
save_kwargs={"category": "option"},
load_kwargs={"category": "option"},
)
# Do not make this a lazy property; the web UI will not refresh it!
@property
@ -266,7 +269,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# already puppeting this object
self.msg("You are already puppeting this object.")
return
if not obj.access(self, 'puppet'):
if not obj.access(self, "puppet"):
# no access
self.msg(f"You don't have permission to puppet '{obj.key}'.")
return
@ -389,6 +392,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if _MULTISESSION_MODE in (0, 1):
return puppets and puppets[0] or None
return puppets
character = property(__get_single_puppet)
puppet = property(__get_single_puppet)
@ -407,21 +411,23 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
"""
ip = kwargs.get('ip', '').strip()
username = kwargs.get('username', '').lower().strip()
ip = kwargs.get("ip", "").strip()
username = kwargs.get("username", "").lower().strip()
# Check IP and/or name bans
bans = ServerConfig.objects.conf("server_bans")
if bans and (any(tup[0] == username for tup in bans if username) or
any(tup[2].match(ip) for tup in bans if ip and tup[2])):
if bans and (
any(tup[0] == username for tup in bans if username)
or any(tup[2].match(ip) for tup in bans if ip and tup[2])
):
return True
return False
@classmethod
def get_username_validators(
cls, validator_config=getattr(
settings, 'AUTH_USERNAME_VALIDATORS', [])):
cls, validator_config=getattr(settings, "AUTH_USERNAME_VALIDATORS", [])
):
"""
Retrieves and instantiates validators for usernames.
@ -436,16 +442,18 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
objs = []
for validator in validator_config:
try:
klass = import_string(validator['NAME'])
klass = import_string(validator["NAME"])
except ImportError:
msg = (f"The module in NAME could not be imported: {validator['NAME']}. "
"Check your AUTH_USERNAME_VALIDATORS setting.")
msg = (
f"The module in NAME could not be imported: {validator['NAME']}. "
"Check your AUTH_USERNAME_VALIDATORS setting."
)
raise ImproperlyConfigured(msg)
objs.append(klass(**validator.get('OPTIONS', {})))
objs.append(klass(**validator.get("OPTIONS", {})))
return objs
@classmethod
def authenticate(cls, username, password, ip='', **kwargs):
def authenticate(cls, username, password, ip="", **kwargs):
"""
Checks the given username/password against the database to see if the
credentials are valid.
@ -480,7 +488,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# See if authentication is currently being throttled
if ip and LOGIN_THROTTLE.check(ip):
errors.append('Too many login failures; please try again in a few minutes.')
errors.append("Too many login failures; please try again in a few minutes.")
# With throttle active, do not log continued hits-- it is a
# waste of storage and can be abused to make your logs harder to
@ -491,27 +499,29 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
banned = cls.is_banned(username=username, ip=ip)
if banned:
# this is a banned IP or name!
errors.append("|rYou have been banned and cannot continue from here."
"\nIf you feel this ban is in error, please email an admin.|x")
logger.log_sec(f'Authentication Denied (Banned): {username} (IP: {ip}).')
LOGIN_THROTTLE.update(ip, 'Too many sightings of banned artifact.')
errors.append(
"|rYou have been banned and cannot continue from here."
"\nIf you feel this ban is in error, please email an admin.|x"
)
logger.log_sec(f"Authentication Denied (Banned): {username} (IP: {ip}).")
LOGIN_THROTTLE.update(ip, "Too many sightings of banned artifact.")
return None, errors
# Authenticate and get Account object
account = authenticate(username=username, password=password)
if not account:
# User-facing message
errors.append('Username and/or password is incorrect.')
errors.append("Username and/or password is incorrect.")
# Log auth failures while throttle is inactive
logger.log_sec(f'Authentication Failure: {username} (IP: {ip}).')
logger.log_sec(f"Authentication Failure: {username} (IP: {ip}).")
# Update throttle
if ip:
LOGIN_THROTTLE.update(ip, 'Too many authentication failures.')
LOGIN_THROTTLE.update(ip, "Too many authentication failures.")
# Try to call post-failure hook
session = kwargs.get('session', None)
session = kwargs.get("session", None)
if session:
account = AccountDB.objects.get_account_from_name(username)
if account:
@ -520,7 +530,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
return None, errors
# Account successfully authenticated
logger.log_sec(f'Authentication Success: {account} (IP: {ip}).')
logger.log_sec(f"Authentication Success: {account} (IP: {ip}).")
return account, errors
@classmethod
@ -659,17 +669,19 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
account = None
errors = []
username = kwargs.get('username')
password = kwargs.get('password')
email = kwargs.get('email', '').strip()
guest = kwargs.get('guest', False)
username = kwargs.get("username")
password = kwargs.get("password")
email = kwargs.get("email", "").strip()
guest = kwargs.get("guest", False)
permissions = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT)
typeclass = kwargs.get('typeclass', cls)
permissions = kwargs.get("permissions", settings.PERMISSION_ACCOUNT_DEFAULT)
typeclass = kwargs.get("typeclass", cls)
ip = kwargs.get('ip', '')
ip = kwargs.get("ip", "")
if ip and CREATION_THROTTLE.check(ip):
errors.append("You are creating too many accounts. Please log into an existing account.")
errors.append(
"You are creating too many accounts. Please log into an existing account."
)
return None, errors
# Normalize username
@ -696,19 +708,25 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
banned = cls.is_banned(username=username, ip=ip)
if banned:
# 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"
string = (
"|rYou have been banned and cannot continue from here."
"\nIf you feel this ban is in error, please email an admin.|x"
)
errors.append(string)
return None, errors
# everything's ok. Create the new account.
try:
try:
account = create.create_account(username, email, password, permissions=permissions, typeclass=typeclass)
logger.log_sec(f'Account Created: {account} (IP: {ip}).')
account = create.create_account(
username, email, password, permissions=permissions, typeclass=typeclass
)
logger.log_sec(f"Account Created: {account} (IP: {ip}).")
except Exception as e:
errors.append("There was an error creating the Account. If this problem persists, contact an admin.")
errors.append(
"There was an error creating the Account. If this problem persists, contact an admin."
)
logger.log_trace()
return None, errors
@ -730,14 +748,20 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if account and settings.MULTISESSION_MODE < 2:
# Load the appropriate Character class
character_typeclass = kwargs.get('character_typeclass', settings.BASE_CHARACTER_TYPECLASS)
character_home = kwargs.get('home')
character_typeclass = kwargs.get(
"character_typeclass", settings.BASE_CHARACTER_TYPECLASS
)
character_home = kwargs.get("home")
Character = class_from_module(character_typeclass)
# Create the character
character, errs = Character.create(
account.key, account, ip=ip, typeclass=character_typeclass,
permissions=permissions, home=character_home
account.key,
account,
ip=ip,
typeclass=character_typeclass,
permissions=permissions,
home=character_home,
)
errors.extend(errs)
@ -758,7 +782,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# Update the throttle to indicate a new account was created from this IP
if ip and not guest:
CREATION_THROTTLE.update(ip, 'Too many accounts being created.')
CREATION_THROTTLE.update(ip, "Too many accounts being created.")
SIGNAL_ACCOUNT_POST_CREATE.send(sender=account, ip=ip)
return account, errors
@ -786,6 +810,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
self.nicks.clear()
self.aliases.clear()
super().delete(*args, **kwargs)
# methods inherited from database model
def msg(self, text=None, from_obj=None, session=None, options=None, **kwargs):
@ -826,7 +851,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
kwargs["options"] = options
if text is not None:
kwargs['text'] = to_str(text)
kwargs["text"] = to_str(text)
# session relay
sessions = make_iter(session) if session else self.sessions.all()
@ -853,17 +878,29 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
commands at run-time.
"""
raw_string = self.nicks.nickreplace(raw_string, categories=("inputline", "channel"), include_account=False)
raw_string = self.nicks.nickreplace(
raw_string, categories=("inputline", "channel"), include_account=False
)
if not session and _MULTISESSION_MODE in (0, 1):
# for these modes we use the first/only session
sessions = self.sessions.get()
session = sessions[0] if sessions else None
return cmdhandler.cmdhandler(self, raw_string,
callertype="account", session=session, **kwargs)
return cmdhandler.cmdhandler(
self, raw_string, callertype="account", session=session, **kwargs
)
def search(self, searchdata, return_puppet=False, search_object=False,
typeclass=None, nofound_string=None, multimatch_string=None, use_nicks=True, **kwargs):
def search(
self,
searchdata,
return_puppet=False,
search_object=False,
typeclass=None,
nofound_string=None,
multimatch_string=None,
use_nicks=True,
**kwargs,
):
"""
This is similar to `DefaultObject.search` but defaults to searching
for Accounts only.
@ -900,17 +937,25 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# handle me, self and *me, *self
if isinstance(searchdata, str):
# handle wrapping of common terms
if searchdata.lower() in ("me", "*me", "self", "*self",):
if searchdata.lower() in ("me", "*me", "self", "*self"):
return self
if search_object:
matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass, use_nicks=use_nicks)
matches = ObjectDB.objects.object_search(
searchdata, typeclass=typeclass, use_nicks=use_nicks
)
else:
searchdata = self.nicks.nickreplace(searchdata, categories=("account", ), include_account=False)
searchdata = self.nicks.nickreplace(
searchdata, categories=("account",), include_account=False
)
matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass)
matches = _AT_SEARCH_RESULT(matches, self, query=searchdata,
nofound_string=nofound_string,
multimatch_string=multimatch_string)
matches = _AT_SEARCH_RESULT(
matches,
self,
query=searchdata,
nofound_string=nofound_string,
multimatch_string=multimatch_string,
)
if matches and return_puppet:
try:
return matches.puppet
@ -918,7 +963,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
return None
return matches
def access(self, accessing_obj, access_type='read', default=False, no_superuser_bypass=False, **kwargs):
def access(
self, accessing_obj, access_type="read", default=False, no_superuser_bypass=False, **kwargs
):
"""
Determines if another object has permission to access this
object in whatever way.
@ -938,8 +985,12 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
result (bool): Result of access check.
"""
result = super().access(accessing_obj, access_type=access_type,
default=default, no_superuser_bypass=no_superuser_bypass)
result = super().access(
accessing_obj,
access_type=access_type,
default=default,
no_superuser_bypass=no_superuser_bypass,
)
self.at_access(result, accessing_obj, access_type, **kwargs)
return result
@ -974,9 +1025,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
"""
# A basic security setup
lockstring = "examine:perm(Admin);edit:perm(Admin);" \
"delete:perm(Admin);boot:perm(Admin);msg:all();" \
"noidletimeout:perm(Builder) or perm(noidletimeout)"
lockstring = (
"examine:perm(Admin);edit:perm(Admin);"
"delete:perm(Admin);boot:perm(Admin);msg:all();"
"noidletimeout:perm(Builder) or perm(noidletimeout)"
)
self.locks.add(lockstring)
# The ooc account cmdset
@ -991,8 +1044,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)
@ -1147,13 +1199,13 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
global _MUDINFO_CHANNEL
if not _MUDINFO_CHANNEL:
try:
_MUDINFO_CHANNEL = ChannelDB.objects.filter(
db_key=settings.CHANNEL_MUDINFO["key"])[0]
_MUDINFO_CHANNEL = ChannelDB.objects.filter(db_key=settings.CHANNEL_MUDINFO["key"])[
0
]
except Exception:
logger.log_trace()
now = timezone.now()
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month,
now.day, now.hour, now.minute)
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute)
if _MUDINFO_CHANNEL:
_MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")
else:
@ -1206,8 +1258,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# we make sure to clean up the _playable_characters list in case
# any was deleted in the interim.
self.db._playable_characters = [char for char in self.db._playable_characters if char]
self.msg(self.at_look(target=self.db._playable_characters,
session=session), session=session)
self.msg(
self.at_look(target=self.db._playable_characters, session=session), session=session
)
def at_failed_login(self, session, **kwargs):
"""
@ -1355,19 +1408,27 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
result = [f"Account |g{self.key}|n (you are Out-of-Character)"]
nsess = len(sessions)
result.append(nsess == 1 and
"\n\n|wConnected session:|n" or
f"\n\n|wConnected sessions ({nsess}):|n")
result.append(
nsess == 1
and "\n\n|wConnected session:|n"
or f"\n\n|wConnected sessions ({nsess}):|n"
)
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))
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 |wooc <Text>|n - talk on public channel")
@ -1375,19 +1436,30 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
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.")
result.append(
"\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one."
)
else:
result.append("\n |w@charcreate <name> [=description]|n - create new character")
result.append("\n |w@chardelete <name>|n - delete a character (cannot be undone!)")
result.append(
"\n |w@chardelete <name>|n - delete a character (cannot be undone!)"
)
if characters:
string_s_ending = len(characters) > 1 and "s" or ""
result.append("\n |w@ic <character>|n - enter the game (|w@ooc|n to get back here)")
if is_su:
result.append(f"\n\nAvailable character{string_s_ending} ({len(characters)}/unlimited):")
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 ""))
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()
@ -1396,9 +1468,13 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
# 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())}] (played by you in session {sid})")
result.append(
f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] (played by you in session {sid})"
)
else:
result.append(f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] (played by someone 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())}]")
@ -1437,11 +1513,11 @@ class DefaultGuest(DefaultAccount):
errors = []
account = None
username = None
ip = kwargs.get('ip', '').strip()
ip = kwargs.get("ip", "").strip()
# check if guests are enabled.
if not settings.GUEST_ENABLED:
errors.append('Guest accounts are not enabled on this server.')
errors.append("Guest accounts are not enabled on this server.")
return None, errors
try:
@ -1453,7 +1529,7 @@ class DefaultGuest(DefaultAccount):
if not username:
errors.append("All guest accounts are in use. Please try again later.")
if ip:
LOGIN_THROTTLE.update(ip, 'Too many requests for Guest access.')
LOGIN_THROTTLE.update(ip, "Too many requests for Guest access.")
return None, errors
else:
# build a new account with the found guest username

View file

@ -18,34 +18,34 @@ class AccountDBChangeForm(UserChangeForm):
Modify the accountdb class.
"""
class Meta(object):
model = AccountDB
fields = '__all__'
fields = "__all__"
username = forms.RegexField(
label="Username",
max_length=30,
regex=r'^[\w. @+-]+$',
widget=forms.TextInput(
attrs={'size': '30'}),
regex=r"^[\w. @+-]+$",
widget=forms.TextInput(attrs={"size": "30"}),
error_messages={
'invalid': "This value may contain only letters, spaces, numbers "
"and @/./+/-/_ characters."},
help_text="30 characters or fewer. Letters, spaces, digits and "
"@/./+/-/_ only.")
"invalid": "This value may contain only letters, spaces, numbers "
"and @/./+/-/_ characters."
},
help_text="30 characters or fewer. Letters, spaces, digits and " "@/./+/-/_ only.",
)
def clean_username(self):
"""
Clean the username and check its existence.
"""
username = self.cleaned_data['username']
username = self.cleaned_data["username"]
if username.upper() == self.instance.username.upper():
return username
elif AccountDB.objects.filter(username__iexact=username):
raise forms.ValidationError('An account with that name '
'already exists.')
return self.cleaned_data['username']
raise forms.ValidationError("An account with that name " "already exists.")
return self.cleaned_data["username"]
class AccountDBCreationForm(UserCreationForm):
@ -55,28 +55,27 @@ class AccountDBCreationForm(UserCreationForm):
class Meta(object):
model = AccountDB
fields = '__all__'
fields = "__all__"
username = forms.RegexField(
label="Username",
max_length=30,
regex=r'^[\w. @+-]+$',
widget=forms.TextInput(
attrs={'size': '30'}),
regex=r"^[\w. @+-]+$",
widget=forms.TextInput(attrs={"size": "30"}),
error_messages={
'invalid': "This value may contain only letters, spaces, numbers "
"and @/./+/-/_ characters."},
help_text="30 characters or fewer. Letters, spaces, digits and "
"@/./+/-/_ only.")
"invalid": "This value may contain only letters, spaces, numbers "
"and @/./+/-/_ characters."
},
help_text="30 characters or fewer. Letters, spaces, digits and " "@/./+/-/_ only.",
)
def clean_username(self):
"""
Cleanup username.
"""
username = self.cleaned_data['username']
username = self.cleaned_data["username"]
if AccountDB.objects.filter(username__iexact=username):
raise forms.ValidationError('An account with that name already '
'exists.')
raise forms.ValidationError("An account with that name already " "exists.")
return username
@ -85,61 +84,66 @@ class AccountForm(forms.ModelForm):
Defines how to display Accounts
"""
class Meta(object):
model = AccountDB
fields = '__all__'
fields = "__all__"
db_key = forms.RegexField(
label="Username",
initial="AccountDummy",
max_length=30,
regex=r'^[\w. @+-]+$',
regex=r"^[\w. @+-]+$",
required=False,
widget=forms.TextInput(attrs={'size': '30'}),
widget=forms.TextInput(attrs={"size": "30"}),
error_messages={
'invalid': "This value may contain only letters, spaces, numbers"
" and @/./+/-/_ characters."},
"invalid": "This value may contain only letters, spaces, numbers"
" and @/./+/-/_ characters."
},
help_text="This should be the same as the connected Account's key "
"name. 30 characters or fewer. Letters, spaces, digits and "
"@/./+/-/_ only.")
"name. 30 characters or fewer. Letters, spaces, digits and "
"@/./+/-/_ only.",
)
db_typeclass_path = forms.CharField(
label="Typeclass",
initial=settings.BASE_ACCOUNT_TYPECLASS,
widget=forms.TextInput(
attrs={'size': '78'}),
widget=forms.TextInput(attrs={"size": "78"}),
help_text="Required. Defines what 'type' of entity this is. This "
"variable holds a Python path to a module with a valid "
"Evennia Typeclass. Defaults to "
"settings.BASE_ACCOUNT_TYPECLASS.")
"variable holds a Python path to a module with a valid "
"Evennia Typeclass. Defaults to "
"settings.BASE_ACCOUNT_TYPECLASS.",
)
db_permissions = forms.CharField(
label="Permissions",
initial=settings.PERMISSION_ACCOUNT_DEFAULT,
required=False,
widget=forms.TextInput(
attrs={'size': '78'}),
widget=forms.TextInput(attrs={"size": "78"}),
help_text="In-game permissions. A comma-separated list of text "
"strings checked by certain locks. They are often used for "
"hierarchies, such as letting an Account have permission "
"'Admin', 'Builder' etc. An Account permission can be "
"overloaded by the permissions of a controlled Character. "
"Normal accounts use 'Accounts' by default.")
"strings checked by certain locks. They are often used for "
"hierarchies, such as letting an Account have permission "
"'Admin', 'Builder' etc. An Account permission can be "
"overloaded by the permissions of a controlled Character. "
"Normal accounts use 'Accounts' by default.",
)
db_lock_storage = forms.CharField(
label="Locks",
widget=forms.Textarea(attrs={'cols': '100', 'rows': '2'}),
widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}),
required=False,
help_text="In-game lock definition string. If not given, defaults "
"will be used. This string should be on the form "
"<i>type:lockfunction(args);type2:lockfunction2(args);...")
"will be used. This string should be on the form "
"<i>type:lockfunction(args);type2:lockfunction2(args);...",
)
db_cmdset_storage = forms.CharField(
label="cmdset",
initial=settings.CMDSET_ACCOUNT,
widget=forms.TextInput(attrs={'size': '78'}),
widget=forms.TextInput(attrs={"size": "78"}),
required=False,
help_text="python path to account cmdset class (set in "
"settings.CMDSET_ACCOUNT by default)")
"settings.CMDSET_ACCOUNT by default)",
)
class AccountInline(admin.StackedInline):
@ -147,19 +151,29 @@ class AccountInline(admin.StackedInline):
Inline creation of Account
"""
model = AccountDB
template = "admin/accounts/stacked.html"
form = AccountForm
fieldsets = (
("In-game Permissions and Locks",
{'fields': ('db_lock_storage',),
#{'fields': ('db_permissions', 'db_lock_storage'),
'description': "<i>These are permissions/locks for in-game use. "
"They are unrelated to website access rights.</i>"}),
("In-game Account data",
{'fields': ('db_typeclass_path', 'db_cmdset_storage'),
'description': "<i>These fields define in-game-specific properties "
"for the Account object in-game.</i>"}))
(
"In-game Permissions and Locks",
{
"fields": ("db_lock_storage",),
# {'fields': ('db_permissions', 'db_lock_storage'),
"description": "<i>These are permissions/locks for in-game use. "
"They are unrelated to website access rights.</i>",
},
),
(
"In-game Account data",
{
"fields": ("db_typeclass_path", "db_cmdset_storage"),
"description": "<i>These fields define in-game-specific properties "
"for the Account object in-game.</i>",
},
),
)
extra = 1
max_num = 1
@ -170,6 +184,7 @@ class AccountTagInline(TagInline):
Inline Account Tags.
"""
model = AccountDB.db_tags.through
related_field = "accountdb"
@ -179,6 +194,7 @@ class AccountAttributeInline(AttributeInline):
Inline Account Attributes.
"""
model = AccountDB.db_attributes.through
related_field = "accountdb"
@ -189,30 +205,43 @@ class AccountDBAdmin(BaseUserAdmin):
"""
list_display = ('username', 'email', 'is_staff', 'is_superuser')
list_display = ("username", "email", "is_staff", "is_superuser")
form = AccountDBChangeForm
add_form = AccountDBCreationForm
inlines = [AccountTagInline, AccountAttributeInline]
fieldsets = (
(None, {'fields': ('username', 'password', 'email')}),
('Website profile', {
'fields': ('first_name', 'last_name'),
'description': "<i>These are not used "
"in the default system.</i>"}),
('Website dates', {
'fields': ('last_login', 'date_joined'),
'description': '<i>Relevant only to the website.</i>'}),
('Website Permissions', {
'fields': ('is_active', 'is_staff', 'is_superuser',
'user_permissions', 'groups'),
'description': "<i>These are permissions/permission groups for "
"accessing the admin site. They are unrelated to "
"in-game access rights.</i>"}),
('Game Options', {
'fields': ('db_typeclass_path', 'db_cmdset_storage',
'db_lock_storage'),
'description': '<i>These are attributes that are more relevant '
'to gameplay.</i>'}))
(None, {"fields": ("username", "password", "email")}),
(
"Website profile",
{
"fields": ("first_name", "last_name"),
"description": "<i>These are not used " "in the default system.</i>",
},
),
(
"Website dates",
{
"fields": ("last_login", "date_joined"),
"description": "<i>Relevant only to the website.</i>",
},
),
(
"Website Permissions",
{
"fields": ("is_active", "is_staff", "is_superuser", "user_permissions", "groups"),
"description": "<i>These are permissions/permission groups for "
"accessing the admin site. They are unrelated to "
"in-game access rights.</i>",
},
),
(
"Game Options",
{
"fields": ("db_typeclass_path", "db_cmdset_storage", "db_lock_storage"),
"description": "<i>These are attributes that are more relevant " "to gameplay.</i>",
},
),
)
# ('Game Options', {'fields': (
# 'db_typeclass_path', 'db_cmdset_storage',
# 'db_permissions', 'db_lock_storage'),
@ -220,10 +249,15 @@ class AccountDBAdmin(BaseUserAdmin):
# 'more relevant to gameplay.</i>'}))
add_fieldsets = (
(None,
{'fields': ('username', 'password1', 'password2', 'email'),
'description': "<i>These account details are shared by the admin "
"system and the game.</i>"},),)
(
None,
{
"fields": ("username", "password1", "password2", "email"),
"description": "<i>These account details are shared by the admin "
"system and the game.</i>",
},
),
)
def save_model(self, request, obj, form, change):
"""
@ -246,6 +280,7 @@ class AccountDBAdmin(BaseUserAdmin):
def response_add(self, request, obj, post_url_continue=None):
from django.http import HttpResponseRedirect
from django.urls import reverse
return HttpResponseRedirect(reverse("admin:accounts_accountdb_change", args=[obj.id]))

View file

@ -23,6 +23,7 @@ _SESSIONS = None
# Bot helper utilities
class BotStarter(DefaultScript):
"""
This non-repeating script has the
@ -80,6 +81,7 @@ class BotStarter(DefaultScript):
"""
self.db.started = False
#
# Bot base class
@ -100,8 +102,10 @@ class Bot(DefaultAccount):
# the text encoding to use.
self.db.encoding = "utf-8"
# A basic security setup (also avoid idle disconnects)
lockstring = "examine:perm(Admin);edit:perm(Admin);delete:perm(Admin);" \
"boot:perm(Admin);msg:false();noidletimeout:true()"
lockstring = (
"examine:perm(Admin);edit:perm(Admin);delete:perm(Admin);"
"boot:perm(Admin);msg:false();noidletimeout:true()"
)
self.locks.add(lockstring)
# set the basics of being a bot
script_key = str(self.key)
@ -143,16 +147,25 @@ class Bot(DefaultAccount):
# IRC
class IRCBot(Bot):
"""
Bot for handling IRC connections.
"""
# override this on a child class to use custom factory
factory_path = "evennia.server.portal.irc.IRCBotFactory"
def start(self, ev_channel=None, irc_botname=None, irc_channel=None,
irc_network=None, irc_port=None, irc_ssl=None):
def start(
self,
ev_channel=None,
irc_botname=None,
irc_channel=None,
irc_network=None,
irc_port=None,
irc_ssl=None,
):
"""
Start by telling the portal to start a new session.
@ -202,12 +215,14 @@ class IRCBot(Bot):
# instruct the server and portal to create a new session with
# the stored configuration
configdict = {"uid": self.dbid,
"botname": self.db.irc_botname,
"channel": self.db.irc_channel,
"network": self.db.irc_network,
"port": self.db.irc_port,
"ssl": self.db.irc_ssl}
configdict = {
"uid": self.dbid,
"botname": self.db.irc_botname,
"channel": self.db.irc_channel,
"network": self.db.irc_network,
"port": self.db.irc_port,
"ssl": self.db.irc_ssl,
}
_SESSIONS.start_bot_session(self.factory_path, configdict)
def at_msg_send(self, **kwargs):
@ -275,8 +290,11 @@ class IRCBot(Bot):
# cache channel lookup
self.ndb.ev_channel = self.db.ev_channel
if ("from_channel" in options and text and
self.ndb.ev_channel.dbid == options["from_channel"]):
if (
"from_channel" in options
and text
and self.ndb.ev_channel.dbid == options["from_channel"]
):
if not from_obj or from_obj != [self]:
super().msg(channel=text)
@ -338,9 +356,14 @@ class IRCBot(Bot):
delta_cmd = t0 - sess.cmd_last_visible
delta_conn = t0 - session.conn_time
account = sess.get_account()
whos.append("%s (%s/%s)" % (utils.crop("|w%s|n" % account.name, width=25),
utils.time_format(delta_conn, 0),
utils.time_format(delta_cmd, 1)))
whos.append(
"%s (%s/%s)"
% (
utils.crop("|w%s|n" % account.name, width=25),
utils.time_format(delta_conn, 0),
utils.time_format(delta_cmd, 1),
)
)
text = f"Who list (online/idle): {', '.join(sorted(whos, key=lambda w: w.lower()))}"
elif txt.lower().startswith("about"):
# some bot info
@ -364,6 +387,7 @@ class IRCBot(Bot):
if self.ndb.ev_channel:
self.ndb.ev_channel.msg(text, senders=self)
#
# RSS
@ -410,9 +434,7 @@ class RSSBot(Bot):
self.db.rss_rate = rss_rate
# instruct the server and portal to create a new session with
# the stored configuration
configdict = {"uid": self.dbid,
"url": self.db.rss_url,
"rate": self.db.rss_rate}
configdict = {"uid": self.dbid, "url": self.db.rss_url, "rate": self.db.rss_rate}
_SESSIONS.start_bot_session("evennia.server.portal.rss.RSSBotFactory", configdict)
def execute_cmd(self, txt=None, session=None, **kwargs):
@ -437,12 +459,14 @@ class RSSBot(Bot):
# Grapevine bot
class GrapevineBot(Bot):
"""
g Grapevine (https://grapevine.haus) relayer. The channel to connect to is the first
name in the settings.GRAPEVINE_CHANNELS list.
"""
factory_path = "evennia.server.portal.grapevine.RestartingWebsocketServerFactory"
def start(self, ev_channel=None, grapevine_channel=None):
@ -472,8 +496,7 @@ class GrapevineBot(Bot):
self.db.grapevine_channel = grapevine_channel
# these will be made available as properties on the protocol factory
configdict = {"uid": self.dbid,
"grapevine_channel": self.db.grapevine_channel}
configdict = {"uid": self.dbid, "grapevine_channel": self.db.grapevine_channel}
_SESSIONS.start_bot_session(self.factory_path, configdict)
@ -501,8 +524,11 @@ class GrapevineBot(Bot):
# cache channel lookup
self.ndb.ev_channel = self.db.ev_channel
if ("from_channel" in options and text and
self.ndb.ev_channel.dbid == options["from_channel"]):
if (
"from_channel" in options
and text
and self.ndb.ev_channel.dbid == options["from_channel"]
):
if not from_obj or from_obj != [self]:
# send outputfunc channel(msg, chan, sender)
@ -511,11 +537,25 @@ class GrapevineBot(Bot):
# prefix since we pass that explicitly anyway
prefix, text = text.split(":", 1)
super().msg(channel=(text.strip(), self.db.grapevine_channel,
", ".join(obj.key for obj in from_obj), {}))
super().msg(
channel=(
text.strip(),
self.db.grapevine_channel,
", ".join(obj.key for obj in from_obj),
{},
)
)
def execute_cmd(self, txt=None, session=None, event=None, grapevine_channel=None,
sender=None, game=None, **kwargs):
def execute_cmd(
self,
txt=None,
session=None,
event=None,
grapevine_channel=None,
sender=None,
game=None,
**kwargs,
):
"""
Take incoming data from protocol and send it to connected channel. This is
triggered by the bot_data_in Inputfunc.

View file

@ -5,7 +5,8 @@ The managers for the custom Account object and permissions.
import datetime
from django.utils import timezone
from django.contrib.auth.models import UserManager
from evennia.typeclasses.managers import (TypedObjectManager, TypeclassManager)
from evennia.typeclasses.managers import TypedObjectManager, TypeclassManager
__all__ = ("AccountManager",)
@ -13,6 +14,7 @@ __all__ = ("AccountManager",)
# Account Manager
#
class AccountDBManager(TypedObjectManager, UserManager):
"""
This AccountManager implements methods for searching
@ -90,8 +92,7 @@ class AccountDBManager(TypedObjectManager, UserManager):
end_date = timezone.now()
tdelta = datetime.timedelta(days)
start_date = end_date - tdelta
return self.filter(last_login__range=(
start_date, end_date)).order_by('-last_login')
return self.filter(last_login__range=(start_date, end_date)).order_by("-last_login")
def get_account_from_email(self, uemail):
"""
@ -176,7 +177,8 @@ class AccountDBManager(TypedObjectManager, UserManager):
# try alias match
matches = self.filter(
db_tags__db_tagtype__iexact="alias",
**{"db_tags__db_key__iexact" if exact else "db_tags__db_key__icontains": ostring})
**{"db_tags__db_key__iexact" if exact else "db_tags__db_key__icontains": ostring},
)
return matches
# back-compatibility alias

View file

@ -8,42 +8,168 @@ import django.core.validators
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
('typeclasses', '0001_initial'),
]
dependencies = [("auth", "0001_initial"), ("typeclasses", "0001_initial")]
operations = [
migrations.CreateModel(
name='AccountDB',
name="AccountDB",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, max_length=30, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')])),
('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
('email', models.EmailField(max_length=75, verbose_name='email address', blank=True)),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('db_key', models.CharField(max_length=255, verbose_name='key', db_index=True)),
('db_typeclass_path', models.CharField(help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.", max_length=255, null=True, verbose_name='typeclass')),
('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
('db_lock_storage', models.TextField(help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.", verbose_name='locks', blank=True)),
('db_is_connected', models.BooleanField(default=False, help_text='If account is connected to game or not', verbose_name='is_connected')),
('db_cmdset_storage', models.CharField(help_text='optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.', max_length=255, null=True, verbose_name='cmdset')),
('db_is_bot', models.BooleanField(default=False, help_text='Used to identify irc/rss bots', verbose_name='is_bot')),
('db_attributes', models.ManyToManyField(help_text='attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute', null=True)),
('db_tags', models.ManyToManyField(help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag', null=True)),
('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')),
('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')),
(
"id",
models.AutoField(
verbose_name="ID", serialize=False, auto_created=True, primary_key=True
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
help_text="Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.",
unique=True,
max_length=30,
verbose_name="username",
validators=[
django.core.validators.RegexValidator(
"^[\\w.@+-]+$", "Enter a valid username.", "invalid"
)
],
),
),
(
"first_name",
models.CharField(max_length=30, verbose_name="first name", blank=True),
),
(
"last_name",
models.CharField(max_length=30, verbose_name="last name", blank=True),
),
(
"email",
models.EmailField(max_length=75, verbose_name="email address", blank=True),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
("db_key", models.CharField(max_length=255, verbose_name="key", db_index=True)),
(
"db_typeclass_path",
models.CharField(
help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.",
max_length=255,
null=True,
verbose_name="typeclass",
),
),
(
"db_date_created",
models.DateTimeField(auto_now_add=True, verbose_name="creation date"),
),
(
"db_lock_storage",
models.TextField(
help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.",
verbose_name="locks",
blank=True,
),
),
(
"db_is_connected",
models.BooleanField(
default=False,
help_text="If account is connected to game or not",
verbose_name="is_connected",
),
),
(
"db_cmdset_storage",
models.CharField(
help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.",
max_length=255,
null=True,
verbose_name="cmdset",
),
),
(
"db_is_bot",
models.BooleanField(
default=False,
help_text="Used to identify irc/rss bots",
verbose_name="is_bot",
),
),
(
"db_attributes",
models.ManyToManyField(
help_text="attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).",
to="typeclasses.Attribute",
null=True,
),
),
(
"db_tags",
models.ManyToManyField(
help_text="tags on this object. Tags are simple string markers to identify, group and alias objects.",
to="typeclasses.Tag",
null=True,
),
),
(
"groups",
models.ManyToManyField(
related_query_name="user",
related_name="user_set",
to="auth.Group",
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of his/her group.",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
related_query_name="user",
related_name="user_set",
to="auth.Permission",
blank=True,
help_text="Specific permissions for this user.",
verbose_name="user permissions",
),
),
],
options={
'verbose_name': 'Account',
'verbose_name_plural': 'Accounts',
},
options={"verbose_name": "Account", "verbose_name_plural": "Accounts"},
bases=(models.Model,),
),
)
]

View file

@ -13,10 +13,6 @@ def convert_defaults(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
dependencies = [("accounts", "0001_initial")]
operations = [
migrations.RunPython(convert_defaults),
]
operations = [migrations.RunPython(convert_defaults)]

View file

@ -6,31 +6,17 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_move_defaults'),
]
dependencies = [("accounts", "0002_move_defaults")]
operations = [
migrations.CreateModel(
name='DefaultAccount',
fields=[
],
options={
'proxy': True,
},
bases=('accounts.accountdb',),
name="DefaultAccount", fields=[], options={"proxy": True}, bases=("accounts.accountdb",)
),
migrations.CreateModel(
name='DefaultGuest',
fields=[
],
options={
'proxy': True,
},
bases=('accounts.defaultaccount',),
),
migrations.AlterModelOptions(
name='accountdb',
options={'verbose_name': 'Account'},
name="DefaultGuest",
fields=[],
options={"proxy": True},
bases=("accounts.defaultaccount",),
),
migrations.AlterModelOptions(name="accountdb", options={"verbose_name": "Account"}),
]

View file

@ -8,41 +8,52 @@ import django.core.validators
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_auto_20150209_2234'),
]
dependencies = [("accounts", "0003_auto_20150209_2234")]
operations = [
migrations.DeleteModel(
name='DefaultGuest',
),
migrations.DeleteModel(
name='DefaultAccount',
),
migrations.DeleteModel(name="DefaultGuest"),
migrations.DeleteModel(name="DefaultAccount"),
migrations.AlterModelManagers(
name='accountdb',
managers=[
('objects', evennia.accounts.manager.AccountDBManager()),
],
name="accountdb", managers=[("objects", evennia.accounts.manager.AccountDBManager())]
),
migrations.AlterField(
model_name='accountdb',
name='email',
field=models.EmailField(max_length=254, verbose_name='email address', blank=True),
model_name="accountdb",
name="email",
field=models.EmailField(max_length=254, verbose_name="email address", blank=True),
),
migrations.AlterField(
model_name='accountdb',
name='groups',
field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups'),
model_name="accountdb",
name="groups",
field=models.ManyToManyField(
related_query_name="user",
related_name="user_set",
to="auth.Group",
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
verbose_name="groups",
),
),
migrations.AlterField(
model_name='accountdb',
name='last_login',
field=models.DateTimeField(null=True, verbose_name='last login', blank=True),
model_name="accountdb",
name="last_login",
field=models.DateTimeField(null=True, verbose_name="last login", blank=True),
),
migrations.AlterField(
model_name='accountdb',
name='username',
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username'),
model_name="accountdb",
name="username",
field=models.CharField(
error_messages={"unique": "A user with that username already exists."},
max_length=30,
validators=[
django.core.validators.RegexValidator(
"^[\\w.@+-]+$",
"Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.",
"invalid",
)
],
help_text="Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.",
unique=True,
verbose_name="username",
),
),
]

View file

@ -8,14 +8,24 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_auto_20150403_2339'),
]
dependencies = [("accounts", "0004_auto_20150403_2339")]
operations = [
migrations.AlterField(
model_name='accountdb',
name='username',
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username'),
),
model_name="accountdb",
name="username",
field=models.CharField(
error_messages={"unique": "A user with that username already exists."},
help_text="Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=30,
unique=True,
validators=[
django.core.validators.RegexValidator(
"^[\\w.@+-]+$",
"Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.",
)
],
verbose_name="username",
),
)
]

View file

@ -8,24 +8,35 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0005_auto_20160905_0902'),
]
dependencies = [("accounts", "0005_auto_20160905_0902")]
operations = [
migrations.AlterField(
model_name='accountdb',
name='db_attributes',
field=models.ManyToManyField(help_text='attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'),
model_name="accountdb",
name="db_attributes",
field=models.ManyToManyField(
help_text="attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).",
to="typeclasses.Attribute",
),
),
migrations.AlterField(
model_name='accountdb',
name='db_tags',
field=models.ManyToManyField(help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
model_name="accountdb",
name="db_tags",
field=models.ManyToManyField(
help_text="tags on this object. Tags are simple string markers to identify, group and alias objects.",
to="typeclasses.Tag",
),
),
migrations.AlterField(
model_name='accountdb',
name='username',
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username'),
model_name="accountdb",
name="username",
field=models.CharField(
error_messages={"unique": "A user with that username already exists."},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[django.contrib.auth.validators.ASCIIUsernameValidator()],
verbose_name="username",
),
),
]

View file

@ -8,31 +8,33 @@ from django.db import migrations
def forwards(apps, schema_editor):
try:
PlayerDB = apps.get_model('players', 'PlayerDB')
PlayerDB = apps.get_model("players", "PlayerDB")
except LookupError:
# playerdb not available. Skip.
return
AccountDB = apps.get_model('accounts', 'AccountDB')
AccountDB = apps.get_model("accounts", "AccountDB")
for player in PlayerDB.objects.all():
account = AccountDB(id=player.id,
password=player.password,
is_superuser=player.is_superuser,
last_login=player.last_login,
username=player.username,
first_name=player.first_name,
last_name=player.last_name,
email=player.email,
is_staff=player.is_staff,
is_active=player.is_active,
date_joined=player.date_joined,
db_key=player.db_key,
db_typeclass_path=player.db_typeclass_path,
db_date_created=player.db_date_created,
db_lock_storage=player.db_lock_storage,
db_is_connected=player.db_is_connected,
db_cmdset_storage=player.db_cmdset_storage,
db_is_bot=player.db_is_bot)
account = AccountDB(
id=player.id,
password=player.password,
is_superuser=player.is_superuser,
last_login=player.last_login,
username=player.username,
first_name=player.first_name,
last_name=player.last_name,
email=player.email,
is_staff=player.is_staff,
is_active=player.is_active,
date_joined=player.date_joined,
db_key=player.db_key,
db_typeclass_path=player.db_typeclass_path,
db_date_created=player.db_date_created,
db_lock_storage=player.db_lock_storage,
db_is_connected=player.db_is_connected,
db_cmdset_storage=player.db_cmdset_storage,
db_is_bot=player.db_is_bot,
)
account.save()
for group in player.groups.all():
account.groups.add(group)
@ -46,13 +48,9 @@ def forwards(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('accounts', '0006_auto_20170606_1731'),
]
dependencies = [("accounts", "0006_auto_20170606_1731")]
operations = [
migrations.RunPython(forwards, migrations.RunPython.noop)
]
operations = [migrations.RunPython(forwards, migrations.RunPython.noop)]
if global_apps.is_installed('players'):
dependencies.append(('players', '0006_auto_20170606_1731'))
if global_apps.is_installed("players"):
dependencies.append(("players", "0006_auto_20170606_1731"))

View file

@ -8,65 +8,93 @@ import evennia.accounts.manager
class Migration(migrations.Migration):
dependencies = [
('accounts', '0007_copy_player_to_account'),
]
dependencies = [("accounts", "0007_copy_player_to_account")]
operations = [
migrations.AlterModelManagers(
name='accountdb',
managers=[
('objects', evennia.accounts.manager.AccountDBManager()),
],
name="accountdb", managers=[("objects", evennia.accounts.manager.AccountDBManager())]
),
migrations.AlterField(
model_name='accountdb',
name='db_attributes',
field=models.ManyToManyField(help_text='attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'),
model_name="accountdb",
name="db_attributes",
field=models.ManyToManyField(
help_text="attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).",
to="typeclasses.Attribute",
),
),
migrations.AlterField(
model_name='accountdb',
name='db_cmdset_storage',
field=models.CharField(help_text='optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.', max_length=255, null=True, verbose_name='cmdset'),
model_name="accountdb",
name="db_cmdset_storage",
field=models.CharField(
help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.",
max_length=255,
null=True,
verbose_name="cmdset",
),
),
migrations.AlterField(
model_name='accountdb',
name='db_date_created',
field=models.DateTimeField(auto_now_add=True, verbose_name='creation date'),
model_name="accountdb",
name="db_date_created",
field=models.DateTimeField(auto_now_add=True, verbose_name="creation date"),
),
migrations.AlterField(
model_name='accountdb',
name='db_is_bot',
field=models.BooleanField(default=False, help_text='Used to identify irc/rss bots', verbose_name='is_bot'),
model_name="accountdb",
name="db_is_bot",
field=models.BooleanField(
default=False, help_text="Used to identify irc/rss bots", verbose_name="is_bot"
),
),
migrations.AlterField(
model_name='accountdb',
name='db_is_connected',
field=models.BooleanField(default=False, help_text='If player is connected to game or not', verbose_name='is_connected'),
model_name="accountdb",
name="db_is_connected",
field=models.BooleanField(
default=False,
help_text="If player is connected to game or not",
verbose_name="is_connected",
),
),
migrations.AlterField(
model_name='accountdb',
name='db_key',
field=models.CharField(db_index=True, max_length=255, verbose_name='key'),
model_name="accountdb",
name="db_key",
field=models.CharField(db_index=True, max_length=255, verbose_name="key"),
),
migrations.AlterField(
model_name='accountdb',
name='db_lock_storage',
field=models.TextField(blank=True, help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.", verbose_name='locks'),
model_name="accountdb",
name="db_lock_storage",
field=models.TextField(
blank=True,
help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.",
verbose_name="locks",
),
),
migrations.AlterField(
model_name='accountdb',
name='db_tags',
field=models.ManyToManyField(help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
model_name="accountdb",
name="db_tags",
field=models.ManyToManyField(
help_text="tags on this object. Tags are simple string markers to identify, group and alias objects.",
to="typeclasses.Tag",
),
),
migrations.AlterField(
model_name='accountdb',
name='db_typeclass_path',
field=models.CharField(help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.", max_length=255, null=True, verbose_name='typeclass'),
model_name="accountdb",
name="db_typeclass_path",
field=models.CharField(
help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.",
max_length=255,
null=True,
verbose_name="typeclass",
),
),
migrations.AlterField(
model_name='accountdb',
name='username',
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
model_name="accountdb",
name="username",
field=models.CharField(
error_messages={"unique": "A user with that username already exists."},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
verbose_name="username",
),
),
]

View file

@ -27,8 +27,8 @@ from evennia.server.signals import SIGNAL_ACCOUNT_POST_RENAME
__all__ = ("AccountDB",)
#_ME = _("me")
#_SELF = _("self")
# _ME = _("me")
# _SELF = _("self")
_MULTISESSION_MODE = settings.MULTISESSION_MODE
@ -39,11 +39,12 @@ _DA = object.__delattr__
_TYPECLASS = None
#------------------------------------------------------------
# ------------------------------------------------------------
#
# AccountDB
#
#------------------------------------------------------------
# ------------------------------------------------------------
class AccountDB(TypedObject, AbstractUser):
"""
@ -83,14 +84,22 @@ class AccountDB(TypedObject, AbstractUser):
# store a connected flag here too, not just in sessionhandler.
# This makes it easier to track from various out-of-process locations
db_is_connected = models.BooleanField(default=False,
verbose_name="is_connected",
help_text="If player is connected to game or not")
db_is_connected = models.BooleanField(
default=False,
verbose_name="is_connected",
help_text="If player is connected to game or not",
)
# database storage of persistant cmdsets.
db_cmdset_storage = models.CharField('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.")
db_cmdset_storage = models.CharField(
"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.",
)
# marks if this is a "virtual" bot account object
db_is_bot = models.BooleanField(default=False, verbose_name="is_bot", help_text="Used to identify irc/rss bots")
db_is_bot = models.BooleanField(
default=False, verbose_name="is_bot", help_text="Used to identify irc/rss bots"
)
# Database manager
objects = AccountDBManager()
@ -101,20 +110,20 @@ class AccountDB(TypedObject, AbstractUser):
__applabel__ = "accounts"
class Meta(object):
verbose_name = 'Account'
verbose_name = "Account"
# cmdset_storage property
# This seems very sensitive to caching, so leaving it be for now /Griatch
#@property
# @property
def __cmdset_storage_get(self):
"""
Getter. Allows for value = self.name. Returns a list of cmdset_storage.
"""
storage = self.db_cmdset_storage
# we need to check so storage is not None
return [path.strip() for path in storage.split(',')] if storage else []
return [path.strip() for path in storage.split(",")] if storage else []
#@cmdset_storage.setter
# @cmdset_storage.setter
def __cmdset_storage_set(self, value):
"""
Setter. Allows for self.name = value. Stores as a comma-separated
@ -123,11 +132,12 @@ class AccountDB(TypedObject, AbstractUser):
_SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value)))
_GA(self, "save")()
#@cmdset_storage.deleter
# @cmdset_storage.deleter
def __cmdset_storage_del(self):
"Deleter. Allows for del self.name"
_SA(self, "db_cmdset_storage", None)
_GA(self, "save")()
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set, __cmdset_storage_del)
#
@ -140,7 +150,7 @@ class AccountDB(TypedObject, AbstractUser):
def __repr__(self):
return f"{self.name}(account#{self.dbid})"
#@property
# @property
def __username_get(self):
return self.username
@ -157,7 +167,7 @@ class AccountDB(TypedObject, AbstractUser):
name = property(__username_get, __username_set, __username_del)
key = property(__username_get, __username_set, __username_del)
#@property
# @property
def __uid_get(self):
"Getter. Retrieves the user id"
return self.id
@ -167,4 +177,5 @@ class AccountDB(TypedObject, AbstractUser):
def __uid_del(self):
raise Exception("User id cannot be deleted!")
uid = property(__uid_get, __uid_set, __uid_del)

View file

@ -20,12 +20,15 @@ class TestAccountSessionHandler(TestCase):
def setUp(self):
self.account = create.create_account(
f"TestAccount{randint(0, 999999)}", email="test@test.com",
password="testpassword", typeclass=DefaultAccount)
f"TestAccount{randint(0, 999999)}",
email="test@test.com",
password="testpassword",
typeclass=DefaultAccount,
)
self.handler = AccountSessionHandler(self.account)
def tearDown(self):
if hasattr(self, 'account'):
if hasattr(self, "account"):
self.account.delete()
def test_get(self):
@ -67,22 +70,24 @@ class TestAccountSessionHandler(TestCase):
class TestDefaultGuest(EvenniaTest):
"Check DefaultGuest class"
ip = '212.216.134.22'
ip = "212.216.134.22"
@override_settings(GUEST_ENABLED=False)
def test_create_not_enabled(self):
# Guest account should not be permitted
account, errors = DefaultGuest.authenticate(ip=self.ip)
self.assertFalse(account, 'Guest account was created despite being disabled.')
self.assertFalse(account, "Guest account was created despite being disabled.")
def test_authenticate(self):
# Create a guest account
account, errors = DefaultGuest.authenticate(ip=self.ip)
self.assertTrue(account, 'Guest account should have been created.')
self.assertTrue(account, "Guest account should have been created.")
# Create a second guest account
account, errors = DefaultGuest.authenticate(ip=self.ip)
self.assertFalse(account, 'Two guest accounts were created with a single entry on the guest list!')
self.assertFalse(
account, "Two guest accounts were created with a single entry on the guest list!"
)
@patch("evennia.accounts.accounts.ChannelDB.objects.get_channel")
def test_create(self, get_channel):
@ -112,42 +117,52 @@ class TestDefaultGuest(EvenniaTest):
class TestDefaultAccountAuth(EvenniaTest):
def setUp(self):
super(TestDefaultAccountAuth, self).setUp()
self.password = "testpassword"
self.account.delete()
self.account = create.create_account(f"TestAccount{randint(100000, 999999)}", email="test@test.com", password=self.password, typeclass=DefaultAccount)
self.account = create.create_account(
f"TestAccount{randint(100000, 999999)}",
email="test@test.com",
password=self.password,
typeclass=DefaultAccount,
)
def test_authentication(self):
"Confirm Account authentication method is authenticating/denying users."
# Valid credentials
obj, errors = DefaultAccount.authenticate(self.account.name, self.password)
self.assertTrue(obj, 'Account did not authenticate given valid credentials.')
self.assertTrue(obj, "Account did not authenticate given valid credentials.")
# Invalid credentials
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy')
self.assertFalse(obj, 'Account authenticated using invalid credentials.')
obj, errors = DefaultAccount.authenticate(self.account.name, "xyzzy")
self.assertFalse(obj, "Account authenticated using invalid credentials.")
def test_create(self):
"Confirm Account creation is working as expected."
# Create a normal account
account, errors = DefaultAccount.create(username='ziggy', password='stardust11')
self.assertTrue(account, 'New account should have been created.')
account, errors = DefaultAccount.create(username="ziggy", password="stardust11")
self.assertTrue(account, "New account should have been created.")
# Try creating a duplicate account
account2, errors = DefaultAccount.create(username='Ziggy', password='starman11')
self.assertFalse(account2, 'Duplicate account name should not have been allowed.')
account2, errors = DefaultAccount.create(username="Ziggy", password="starman11")
self.assertFalse(account2, "Duplicate account name should not have been allowed.")
account.delete()
def test_throttle(self):
"Confirm throttle activates on too many failures."
for x in range(20):
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy', ip='12.24.36.48')
self.assertFalse(obj, 'Authentication was provided a bogus password; this should NOT have returned an account!')
obj, errors = DefaultAccount.authenticate(self.account.name, "xyzzy", ip="12.24.36.48")
self.assertFalse(
obj,
"Authentication was provided a bogus password; this should NOT have returned an account!",
)
self.assertTrue('too many login failures' in errors[-1].lower(), 'Failed logins should have been throttled.')
self.assertTrue(
"too many login failures" in errors[-1].lower(),
"Failed logins should have been throttled.",
)
def test_username_validation(self):
"Check username validators deny relevant usernames"
@ -156,7 +171,7 @@ class TestDefaultAccountAuth(EvenniaTest):
if not uses_database("mysql"):
# TODO As of Mar 2019, mysql does not pass this test due to collation problems
# that has not been possible to resolve
result, error = DefaultAccount.validate_username('¯\_(ツ)_/¯')
result, error = DefaultAccount.validate_username("¯\_(ツ)_/¯")
self.assertFalse(result, "Validator allowed kanji in username.")
# Should not allow duplicate username
@ -164,35 +179,44 @@ class TestDefaultAccountAuth(EvenniaTest):
self.assertFalse(result, "Duplicate username should not have passed validation.")
# Should not allow username too short
result, error = DefaultAccount.validate_username('xx')
result, error = DefaultAccount.validate_username("xx")
self.assertFalse(result, "2-character username passed validation.")
def test_password_validation(self):
"Check password validators deny bad passwords"
account = create.create_account(f"TestAccount{randint(100000, 999999)}",
email="test@test.com", password="testpassword", typeclass=DefaultAccount)
for bad in ('', '123', 'password', 'TestAccount', '#', 'xyzzy'):
account = create.create_account(
f"TestAccount{randint(100000, 999999)}",
email="test@test.com",
password="testpassword",
typeclass=DefaultAccount,
)
for bad in ("", "123", "password", "TestAccount", "#", "xyzzy"):
self.assertFalse(account.validate_password(bad, account=self.account)[0])
"Check validators allow sufficiently complex passwords"
for better in ('Mxyzptlk', "j0hn, i'M 0n1y d4nc1nG"):
for better in ("Mxyzptlk", "j0hn, i'M 0n1y d4nc1nG"):
self.assertTrue(account.validate_password(better, account=self.account)[0])
account.delete()
def test_password_change(self):
"Check password setting and validation is working as expected"
account = create.create_account(f"TestAccount{randint(100000, 999999)}",
email="test@test.com", password="testpassword", typeclass=DefaultAccount)
account = create.create_account(
f"TestAccount{randint(100000, 999999)}",
email="test@test.com",
password="testpassword",
typeclass=DefaultAccount,
)
from django.core.exceptions import ValidationError
# Try setting some bad passwords
for bad in ('', '#', 'TestAccount', 'password'):
for bad in ("", "#", "TestAccount", "password"):
valid, error = account.validate_password(bad, account)
self.assertFalse(valid)
# Try setting a better password (test for False; returns None on success)
self.assertFalse(account.set_password('Mxyzptlk'))
self.assertFalse(account.set_password("Mxyzptlk"))
account.delete()
@ -228,8 +252,11 @@ class TestDefaultAccount(TestCase):
import evennia.server.sessionhandler
account = create.create_account(
f"TestAccount{randint(0, 999999)}", email="test@test.com",
password="testpassword", typeclass=DefaultAccount)
f"TestAccount{randint(0, 999999)}",
email="test@test.com",
password="testpassword",
typeclass=DefaultAccount,
)
self.s1.uid = account.uid
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
@ -239,7 +266,9 @@ class TestDefaultAccount(TestCase):
obj = Mock()
self.s1.puppet = obj
account.puppet_object(self.s1, obj)
self.s1.data_out.assert_called_with(options=None, text="You are already puppeting this object.")
self.s1.data_out.assert_called_with(
options=None, text="You are already puppeting this object."
)
self.assertIsNone(obj.at_post_puppet.call_args)
def test_puppet_object_no_permission(self):
@ -247,7 +276,12 @@ class TestDefaultAccount(TestCase):
import evennia.server.sessionhandler
account = create.create_account(f"TestAccount{randint(0, 999999)}", email="test@test.com", password="testpassword", typeclass=DefaultAccount)
account = create.create_account(
f"TestAccount{randint(0, 999999)}",
email="test@test.com",
password="testpassword",
typeclass=DefaultAccount,
)
self.s1.uid = account.uid
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
@ -257,7 +291,9 @@ class TestDefaultAccount(TestCase):
account.puppet_object(self.s1, obj)
self.assertTrue(self.s1.data_out.call_args[1]['text'].startswith("You don't have permission to puppet"))
self.assertTrue(
self.s1.data_out.call_args[1]["text"].startswith("You don't have permission to puppet")
)
self.assertIsNone(obj.at_post_puppet.call_args)
@override_settings(MULTISESSION_MODE=0)
@ -266,7 +302,12 @@ class TestDefaultAccount(TestCase):
import evennia.server.sessionhandler
account = create.create_account(f"TestAccount{randint(0, 999999)}", email="test@test.com", password="testpassword", typeclass=DefaultAccount)
account = create.create_account(
f"TestAccount{randint(0, 999999)}",
email="test@test.com",
password="testpassword",
typeclass=DefaultAccount,
)
self.s1.uid = account.uid
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
@ -281,7 +322,9 @@ class TestDefaultAccount(TestCase):
account.puppet_object(self.s1, obj)
# works because django.conf.settings.MULTISESSION_MODE is not in (1, 3)
self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("from another of your sessions.|n"))
self.assertTrue(
self.s1.data_out.call_args[1]["text"].endswith("from another of your sessions.|n")
)
self.assertTrue(obj.at_post_puppet.call_args[1] == {})
def test_puppet_object_already_puppeted(self):
@ -289,7 +332,12 @@ class TestDefaultAccount(TestCase):
import evennia.server.sessionhandler
account = create.create_account(f"TestAccount{randint(0, 999999)}", email="test@test.com", password="testpassword", typeclass=DefaultAccount)
account = create.create_account(
f"TestAccount{randint(0, 999999)}",
email="test@test.com",
password="testpassword",
typeclass=DefaultAccount,
)
self.account = account
self.s1.uid = account.uid
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
@ -304,28 +352,33 @@ class TestDefaultAccount(TestCase):
obj.at_post_puppet = Mock()
account.puppet_object(self.s1, obj)
self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("is already puppeted by another Account."))
self.assertTrue(
self.s1.data_out.call_args[1]["text"].endswith(
"is already puppeted by another Account."
)
)
self.assertIsNone(obj.at_post_puppet.call_args)
class TestAccountPuppetDeletion(EvenniaTest):
@override_settings(MULTISESSION_MODE=2)
def test_puppet_deletion(self):
# Check for existing chars
self.assertFalse(self.account.db._playable_characters,
'Account should not have any chars by default.')
self.assertFalse(
self.account.db._playable_characters, "Account should not have any chars by default."
)
# Add char1 to account's playable characters
self.account.db._playable_characters.append(self.char1)
self.assertTrue(self.account.db._playable_characters,
'Char was not added to account.')
self.assertTrue(self.account.db._playable_characters, "Char was not added to account.")
# See what happens when we delete char1.
self.char1.delete()
# Playable char list should be empty.
self.assertFalse(self.account.db._playable_characters,
f'Playable character list is not empty! {self.account.db._playable_characters}')
self.assertFalse(
self.account.db._playable_characters,
f"Playable character list is not empty! {self.account.db._playable_characters}",
)
class TestDefaultAccountEv(EvenniaTest):
@ -333,6 +386,7 @@ class TestDefaultAccountEv(EvenniaTest):
Testing using the EvenniaTest parent
"""
def test_characters_property(self):
"test existence of None in _playable_characters Attr"
self.account.db._playable_characters = [self.char1, None]
@ -353,7 +407,9 @@ class TestDefaultAccountEv(EvenniaTest):
self.assertEqual(idle, 10)
# test no sessions
with patch("evennia.accounts.accounts._SESSIONS.sessions_from_account", return_value=[]) as mock_sessh:
with patch(
"evennia.accounts.accounts._SESSIONS.sessions_from_account", return_value=[]
) as mock_sessh:
idle = self.account.idle_time
self.assertEqual(idle, None)
@ -364,18 +420,21 @@ class TestDefaultAccountEv(EvenniaTest):
self.assertEqual(conn, 10)
# test no sessions
with patch("evennia.accounts.accounts._SESSIONS.sessions_from_account", return_value=[]) as mock_sessh:
with patch(
"evennia.accounts.accounts._SESSIONS.sessions_from_account", return_value=[]
) as mock_sessh:
idle = self.account.connection_time
self.assertEqual(idle, None)
def test_create_account(self):
acct = create.account(
"TestAccount3", "test@test.com", "testpassword123",
"TestAccount3",
"test@test.com",
"testpassword123",
locks="test:all()",
tags=[("tag1", "category1"), ("tag2", "category2", "data1"), ("tag3", None)],
attributes=[("key1", "value1", "category1",
"edit:false()", True),
("key2", "value2")])
attributes=[("key1", "value1", "category1", "edit:false()", True), ("key2", "value2")],
)
acct.save()
self.assertTrue(acct.pk)
@ -389,7 +448,7 @@ class TestDefaultAccountEv(EvenniaTest):
ret = self.account.at_look(target=self.obj1, session=self.session)
self.assertTrue("Obj" in ret)
ret = self.account.at_look(target="Invalid", session=self.session)
self.assertEqual(ret, 'Invalid has no in-game appearance.')
self.assertEqual(ret, "Invalid has no in-game appearance.")
def test_msg(self):
self.account.msg

View file

@ -63,7 +63,7 @@ _COMMAND_RECURSION_LIMIT = 10
# This decides which command parser is to be used.
# You have to restart the server for changes to take effect.
_COMMAND_PARSER = utils.variable_from_module(*settings.COMMAND_PARSER.rsplit('.', 1))
_COMMAND_PARSER = utils.variable_from_module(*settings.COMMAND_PARSER.rsplit(".", 1))
# System command names - import these variables rather than trying to
# remember the actual string constants. If not defined, Evennia
@ -82,7 +82,7 @@ CMD_CHANNEL = "__send_to_channel_command"
CMD_LOGINSTART = "__unloggedin_look_command"
# Function for handling multiple command matches.
_SEARCH_AT_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
_SEARCH_AT_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
# Output strings. The first is the IN_GAME_ERRORS return, the second
# is the normal "production message to echo to the account.
@ -93,7 +93,8 @@ An untrapped error occurred.
""",
"""
An untrapped error occurred. Please file a bug report detailing the steps to reproduce.
""")
""",
)
_ERROR_CMDSETS = (
"""
@ -103,7 +104,8 @@ error in one of the cmdsets to merge.
"""
A cmdset merger-error occurred. Please file a bug report detailing the
steps to reproduce.
""")
""",
)
_ERROR_NOCMDSETS = (
"""
@ -114,7 +116,8 @@ multiple causes.
No command sets found! This is a sign of a critical bug. If
disconnecting/reconnecting doesn't" solve the problem, try to contact
the server admin through" some other means for assistance.
""")
""",
)
_ERROR_CMDHANDLER = (
"""
@ -125,10 +128,12 @@ traceback and steps to reproduce.
"""
A command handler bug occurred. Please notify staff - they should
likely file a bug report with the Evennia project.
""")
""",
)
_ERROR_RECURSION_LIMIT = "Command recursion limit ({recursion_limit}) " \
"reached for '{raw_cmdname}' ({cmdclass})."
_ERROR_RECURSION_LIMIT = (
"Command recursion limit ({recursion_limit}) " "reached for '{raw_cmdname}' ({cmdclass})."
)
# delayed imports
@ -137,6 +142,7 @@ _GET_INPUT = None
# helper functions
def _msg_err(receiver, stringtuple):
"""
Helper function for returning an error to the caller.
@ -153,13 +159,19 @@ def _msg_err(receiver, stringtuple):
tracestring = format_exc()
logger.log_trace()
if _IN_GAME_ERRORS:
receiver.msg(string.format(traceback=tracestring,
errmsg=stringtuple[0].strip(),
timestamp=timestamp).strip())
receiver.msg(
string.format(
traceback=tracestring, errmsg=stringtuple[0].strip(), timestamp=timestamp
).strip()
)
else:
receiver.msg(string.format(traceback=tracestring.splitlines()[-1],
errmsg=stringtuple[1].strip(),
timestamp=timestamp).strip())
receiver.msg(
string.format(
traceback=tracestring.splitlines()[-1],
errmsg=stringtuple[1].strip(),
timestamp=timestamp,
).strip()
)
def _progressive_cmd_run(cmd, generator, response=None):
@ -227,6 +239,7 @@ def _process_input(caller, prompt, result, cmd, generator):
# custom Exceptions
class NoCmdSets(Exception):
"No cmdsets found. Critical error."
pass
@ -248,6 +261,7 @@ class ErrorReported(Exception):
self.args = (raw_string,)
self.raw_string = raw_string
# Helper function
@ -280,6 +294,7 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
"""
try:
@inlineCallbacks
def _get_channel_cmdset(account_or_obj):
"""
@ -308,8 +323,9 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
if location:
# Gather all cmdsets stored on objects in the room and
# also in the caller's inventory and the location itself
local_objlist = yield (location.contents_get(exclude=obj) +
obj.contents_get() + [location])
local_objlist = yield (
location.contents_get(exclude=obj) + obj.contents_get() + [location]
)
local_objlist = [o for o in local_objlist if not o._is_deleted]
for lobj in local_objlist:
try:
@ -320,11 +336,18 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
# the call-type lock is checked here, it makes sure an account
# is not seeing e.g. the commands on a fellow account (which is why
# the no_superuser_bypass must be True)
local_obj_cmdsets = \
yield list(chain.from_iterable(
lobj.cmdset.cmdset_stack for lobj in local_objlist
if (lobj.cmdset.current and
lobj.access(caller, access_type='call', no_superuser_bypass=True))))
local_obj_cmdsets = yield list(
chain.from_iterable(
lobj.cmdset.cmdset_stack
for lobj in local_objlist
if (
lobj.cmdset.current
and lobj.access(
caller, access_type="call", no_superuser_bypass=True
)
)
)
)
for cset in local_obj_cmdsets:
# This is necessary for object sets, or we won't be able to
# separate the command sets from each other in a busy room. We
@ -370,7 +393,9 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj)
if current.no_exits:
# filter out all exits
local_obj_cmdsets = [cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"]
local_obj_cmdsets = [
cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
]
cmdsets += local_obj_cmdsets
if not current.no_channels:
# also objs may have channels
@ -392,7 +417,9 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj)
if current.no_exits:
# filter out all exits
local_obj_cmdsets = [cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"]
local_obj_cmdsets = [
cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
]
cmdsets += local_obj_cmdsets
if not current.no_channels:
# also objs may have channels
@ -408,7 +435,9 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
local_obj_cmdsets = yield _get_local_obj_cmdsets(obj)
if current.no_exits:
# filter out all exits
local_obj_cmdsets = [cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"]
local_obj_cmdsets = [
cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
]
cmdsets += yield local_obj_cmdsets
if not current.no_channels:
# also objs may have channels
@ -417,11 +446,11 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
raise Exception("get_and_merge_cmdsets: callertype %s is not valid." % callertype)
# weed out all non-found sets
cmdsets = yield [cmdset for cmdset in cmdsets
if cmdset and cmdset.key != "_EMPTY_CMDSET"]
cmdsets = yield [cmdset for cmdset in cmdsets if cmdset and cmdset.key != "_EMPTY_CMDSET"]
# report cmdset errors to user (these should already have been logged)
yield [report_to.msg(cmdset.errmessage) for cmdset in cmdsets
if cmdset.key == "_CMDSET_ERROR"]
yield [
report_to.msg(cmdset.errmessage) for cmdset in cmdsets if cmdset.key == "_CMDSET_ERROR"
]
if cmdsets:
# faster to do tuple on list than to build tuple directly
@ -463,14 +492,23 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
except Exception:
_msg_err(caller, _ERROR_CMDSETS)
raise
#raise ErrorReported
# raise ErrorReported
# Main command-handler function
@inlineCallbacks
def cmdhandler(called_by, raw_string, _testing=False, callertype="session", session=None,
cmdobj=None, cmdobj_key=None, **kwargs):
def cmdhandler(
called_by,
raw_string,
_testing=False,
callertype="session",
session=None,
cmdobj=None,
cmdobj_key=None,
**kwargs,
):
"""
This is the main mechanism that handles any string sent to the engine.
@ -557,7 +595,7 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
# since this may be different for every command when
# merging multuple cmdsets
if hasattr(cmd, 'obj') and hasattr(cmd.obj, 'scripts'):
if hasattr(cmd, "obj") and hasattr(cmd.obj, "scripts"):
# cmd.obj is automatically made available by the cmdhandler.
# we make sure to validate its scripts.
yield cmd.obj.scripts.validate()
@ -572,9 +610,11 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
_COMMAND_NESTING[called_by] += 1
if _COMMAND_NESTING[called_by] > _COMMAND_RECURSION_LIMIT:
err = _ERROR_RECURSION_LIMIT.format(recursion_limit=_COMMAND_RECURSION_LIMIT,
raw_cmdname=raw_cmdname,
cmdclass=cmd.__class__)
err = _ERROR_RECURSION_LIMIT.format(
recursion_limit=_COMMAND_RECURSION_LIMIT,
raw_cmdname=raw_cmdname,
cmdclass=cmd.__class__,
)
raise RuntimeError(err)
# pre-command hook
@ -653,8 +693,9 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
else:
# no explicit cmdobject given, figure it out
cmdset = yield get_and_merge_cmdsets(caller, session, account, obj,
callertype, raw_string)
cmdset = yield get_and_merge_cmdsets(
caller, session, account, obj, callertype, raw_string
)
if not cmdset:
# this is bad and shouldn't happen.
raise NoCmdSets
@ -683,7 +724,9 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
syscmd.matches = matches
else:
# fall back to default error handling
sysarg = yield _SEARCH_AT_RESULT([match[2] for match in matches], caller, query=matches[0][0])
sysarg = yield _SEARCH_AT_RESULT(
[match[2] for match in matches], caller, query=matches[0][0]
)
raise ExecSystemCommand(syscmd, sysarg)
cmdname, args, cmd, raw_cmdname = "", "", None, ""
@ -701,17 +744,22 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
else:
# fallback to default error text
sysarg = _("Command '%s' is not available.") % raw_string
suggestions = string_suggestions(raw_string,
cmdset.get_all_cmd_keys_and_aliases(caller),
cutoff=0.7, maxnum=3)
suggestions = string_suggestions(
raw_string,
cmdset.get_all_cmd_keys_and_aliases(caller),
cutoff=0.7,
maxnum=3,
)
if suggestions:
sysarg += _(" Maybe you meant %s?") % utils.list_to_string(suggestions, _('or'), addquote=True)
sysarg += _(" Maybe you meant %s?") % utils.list_to_string(
suggestions, _("or"), addquote=True
)
else:
sysarg += _(" Type \"help\" for help.")
sysarg += _(' Type "help" for help.')
raise ExecSystemCommand(syscmd, sysarg)
# Check if this is a Channel-cmd match.
if hasattr(cmd, 'is_channel') and cmd.is_channel:
if hasattr(cmd, "is_channel") and cmd.is_channel:
# even if a user-defined syscmd is not defined, the
# found cmd is already a system command in its own right.
syscmd = yield cmdset.get(CMD_CHANNEL)
@ -738,8 +786,9 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
sysarg = exc.sysarg
if syscmd:
ret = yield _run_command(syscmd, syscmd.key, sysarg,
unformatted_raw_string, cmdset, session, account)
ret = yield _run_command(
syscmd, syscmd.key, sysarg, unformatted_raw_string, cmdset, session, account
)
returnValue(ret)
elif sysarg:
# return system arg

View file

@ -65,22 +65,33 @@ def build_matches(raw_string, cmdset, include_prefixes=False):
# use the cmdname as-is
l_raw_string = raw_string.lower()
for cmd in cmdset:
matches.extend([create_match(cmdname, raw_string, cmd, cmdname)
for cmdname in [cmd.key] + cmd.aliases
if cmdname and l_raw_string.startswith(cmdname.lower()) and
(not cmd.arg_regex or
cmd.arg_regex.match(l_raw_string[len(cmdname):]))])
matches.extend(
[
create_match(cmdname, raw_string, cmd, cmdname)
for cmdname in [cmd.key] + cmd.aliases
if cmdname
and l_raw_string.startswith(cmdname.lower())
and (not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname) :]))
]
)
else:
# strip prefixes set in settings
raw_string = (raw_string.lstrip(_CMD_IGNORE_PREFIXES)
if len(raw_string) > 1 else raw_string)
raw_string = (
raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string
)
l_raw_string = raw_string.lower()
for cmd in cmdset:
for raw_cmdname in [cmd.key] + cmd.aliases:
cmdname = (raw_cmdname.lstrip(_CMD_IGNORE_PREFIXES)
if len(raw_cmdname) > 1 else raw_cmdname)
if cmdname and l_raw_string.startswith(cmdname.lower()) and \
(not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname):])):
cmdname = (
raw_cmdname.lstrip(_CMD_IGNORE_PREFIXES)
if len(raw_cmdname) > 1
else raw_cmdname
)
if (
cmdname
and l_raw_string.startswith(cmdname.lower())
and (not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname) :]))
):
matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
except Exception:
log_trace("cmdhandler error. raw_input:%s" % raw_string)
@ -168,18 +179,19 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
return cmdparser(new_raw_string, cmdset, caller, match_index=int(mindex))
if _CMD_IGNORE_PREFIXES:
# still no match. Try to strip prefixes
raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string
raw_string = (
raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string
)
matches = build_matches(raw_string, cmdset, include_prefixes=False)
# only select command matches we are actually allowed to call.
matches = [match for match in matches if match[2].access(caller, 'cmd')]
matches = [match for match in matches if match[2].access(caller, "cmd")]
# try to bring the number of matches down to 1
if len(matches) > 1:
# See if it helps to analyze the match with preserved case but only if
# it leaves at least one match.
trimmed = [match for match in matches
if raw_string.startswith(match[0])]
trimmed = [match for match in matches if raw_string.startswith(match[0])]
if trimmed:
matches = trimmed
@ -188,14 +200,14 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
matches = sorted(matches, key=lambda m: m[3])
# only pick the matches with highest count quality
quality = [mat[3] for mat in matches]
matches = matches[-quality.count(quality[-1]):]
matches = matches[-quality.count(quality[-1]) :]
if len(matches) > 1:
# still multiple matches. Fall back to ratio-based quality.
matches = sorted(matches, key=lambda m: m[4])
# only pick the highest rated ratio match
quality = [mat[4] for mat in matches]
matches = matches[-quality.count(quality[-1]):]
matches = matches[-quality.count(quality[-1]) :]
if len(matches) > 1 and match_index is not None and 0 < match_index <= len(matches):
# We couldn't separate match by quality, but we have an

View file

@ -29,6 +29,7 @@ Set theory.
from weakref import WeakKeyDictionary
from django.utils.translation import ugettext as _
from evennia.utils.utils import inherits_from, is_iter
__all__ = ("CmdSet",)
@ -38,6 +39,7 @@ class _CmdSetMeta(type):
the cmdset class.
"""
def __init__(cls, *args, **kwargs):
"""
Fixes some things in the cmdclass
@ -45,7 +47,7 @@ class _CmdSetMeta(type):
"""
# by default we key the cmdset the same as the
# name of its class.
if not hasattr(cls, 'key') or not cls.key:
if not hasattr(cls, "key") or not cls.key:
cls.key = cls.__name__
cls.path = "%s.%s" % (cls.__module__, cls.__name__)
@ -156,9 +158,18 @@ class CmdSet(object, metaclass=_CmdSetMeta):
key_mergetypes = {}
errmessage = ""
# pre-store properties to duplicate straight off
to_duplicate = ("key", "cmdsetobj", "no_exits", "no_objs",
"no_channels", "permanent", "mergetype",
"priority", "duplicates", "errmessage")
to_duplicate = (
"key",
"cmdsetobj",
"no_exits",
"no_objs",
"no_channels",
"permanent",
"mergetype",
"priority",
"duplicates",
"errmessage",
)
def __init__(self, cmdsetobj=None, key=None):
"""
@ -211,8 +222,7 @@ class CmdSet(object, metaclass=_CmdSetMeta):
if cmdset_a.duplicates and cmdset_a.priority == cmdset_b.priority:
cmdset_c.commands.extend(cmdset_b.commands)
else:
cmdset_c.commands.extend([cmd for cmd in cmdset_b
if cmd not in cmdset_a])
cmdset_c.commands.extend([cmd for cmd in cmdset_b if cmd not in cmdset_a])
return cmdset_c
def _intersect(self, cmdset_a, cmdset_b):
@ -324,7 +334,7 @@ class CmdSet(object, metaclass=_CmdSetMeta):
commands (str): Representation of commands in Cmdset.
"""
return ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o:o.key)])
return ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])
def __iter__(self):
"""
@ -375,8 +385,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
# A higher or equal priority to B
# preserve system __commands
sys_commands = sys_commands_a + [cmd for cmd in sys_commands_b
if cmd not in sys_commands_a]
sys_commands = sys_commands_a + [
cmd for cmd in sys_commands_b if cmd not in sys_commands_a
]
mergetype = cmdset_a.key_mergetypes.get(self.key, cmdset_a.mergetype)
if mergetype == "Intersect":
@ -391,7 +402,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
# pass through options whenever they are set, unless the merging or higher-prio
# set changes the setting (i.e. has a non-None value). We don't pass through
# the duplicates setting; that is per-merge
cmdset_c.no_channels = self.no_channels if cmdset_a.no_channels is None else cmdset_a.no_channels
cmdset_c.no_channels = (
self.no_channels if cmdset_a.no_channels is None else cmdset_a.no_channels
)
cmdset_c.no_exits = self.no_exits if cmdset_a.no_exits is None else cmdset_a.no_exits
cmdset_c.no_objs = self.no_objs if cmdset_a.no_objs is None else cmdset_a.no_objs
@ -399,8 +412,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
# B higher priority than A
# preserver system __commands
sys_commands = sys_commands_b + [cmd for cmd in sys_commands_a
if cmd not in sys_commands_b]
sys_commands = sys_commands_b + [
cmd for cmd in sys_commands_a if cmd not in sys_commands_b
]
mergetype = self.key_mergetypes.get(cmdset_a.key, self.mergetype)
if mergetype == "Intersect":
@ -415,7 +429,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
# pass through options whenever they are set, unless the higher-prio
# set changes the setting (i.e. has a non-None value). We don't pass through
# the duplicates setting; that is per-merge
cmdset_c.no_channels = cmdset_a.no_channels if self.no_channels is None else self.no_channels
cmdset_c.no_channels = (
cmdset_a.no_channels if self.no_channels is None else self.no_channels
)
cmdset_c.no_exits = cmdset_a.no_exits if self.no_exits is None else self.no_exits
cmdset_c.no_objs = cmdset_a.no_objs if self.no_objs is None else self.no_objs
@ -465,8 +481,7 @@ class CmdSet(object, metaclass=_CmdSetMeta):
string += "infinite loop. When adding a cmdset to another, "
string += "make sure they are not themself cyclically added to "
string += "the new cmdset somewhere in the chain."
raise RuntimeError(_(string) % {"cmd": cmd,
"class": self.__class__})
raise RuntimeError(_(string) % {"cmd": cmd, "class": self.__class__})
cmds = cmd.commands
elif is_iter(cmd):
cmds = [self._instantiate(c) for c in cmd]
@ -476,7 +491,7 @@ class CmdSet(object, metaclass=_CmdSetMeta):
system_commands = self.system_commands
for cmd in cmds:
# add all commands
if not hasattr(cmd, 'obj'):
if not hasattr(cmd, "obj"):
cmd.obj = self.cmdsetobj
try:
ic = commands.index(cmd)
@ -578,8 +593,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
for cmd in self.commands:
if cmd.key in unique:
ocmd = unique[cmd.key]
if (hasattr(cmd, 'obj') and cmd.obj == caller) and not \
(hasattr(ocmd, 'obj') and ocmd.obj == caller):
if (hasattr(cmd, "obj") and cmd.obj == caller) and not (
hasattr(ocmd, "obj") and ocmd.obj == caller
):
unique[cmd.key] = cmd
else:
unique[cmd.key] = cmd
@ -601,8 +617,7 @@ class CmdSet(object, metaclass=_CmdSetMeta):
"""
names = []
if caller:
[names.extend(cmd._keyaliases) for cmd in self.commands
if cmd.access(caller)]
[names.extend(cmd._keyaliases) for cmd in self.commands if cmd.access(caller)]
else:
[names.extend(cmd._keyaliases) for cmd in self.commands]
return names

View file

@ -73,6 +73,7 @@ from evennia.commands.cmdset import CmdSet
from evennia.server.models import ServerConfig
from django.utils.translation import ugettext as _
__all__ = ("import_cmdset", "CmdSetHandler")
_CACHED_CMDSETS = {}
@ -86,37 +87,41 @@ _CMDSET_FALLBACKS = settings.CMDSET_FALLBACKS
_ERROR_CMDSET_IMPORT = _(
"""{traceback}
Error loading cmdset '{path}'
(Traceback was logged {timestamp})""")
(Traceback was logged {timestamp})"""
)
_ERROR_CMDSET_KEYERROR = _(
"""Error loading cmdset: No cmdset class '{classname}' in '{path}'.
(Traceback was logged {timestamp})""")
(Traceback was logged {timestamp})"""
)
_ERROR_CMDSET_SYNTAXERROR = _(
"""{traceback}
SyntaxError encountered when loading cmdset '{path}'.
(Traceback was logged {timestamp})""")
(Traceback was logged {timestamp})"""
)
_ERROR_CMDSET_EXCEPTION = _(
"""{traceback}
Compile/Run error when loading cmdset '{path}'.",
(Traceback was logged {timestamp})""")
(Traceback was logged {timestamp})"""
)
_ERROR_CMDSET_FALLBACK = _(
"""
Error encountered for cmdset at path '{path}'.
Replacing with fallback '{fallback_path}'.
""")
_ERROR_CMDSET_NO_FALLBACK = _(
"""Fallback path '{fallback_path}' failed to generate a cmdset."""
"""
)
_ERROR_CMDSET_NO_FALLBACK = _("""Fallback path '{fallback_path}' failed to generate a cmdset.""")
class _ErrorCmdSet(CmdSet):
"""
This is a special cmdset used to report errors.
"""
key = "_CMDSET_ERROR"
errmessage = "Error when loading cmdset."
@ -125,6 +130,7 @@ class _EmptyCmdSet(CmdSet):
"""
This cmdset represents an empty cmdset
"""
key = "_EMPTY_CMDSET"
priority = -101
mergetype = "Union"
@ -153,8 +159,9 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
for the benefit of the handler.
"""
python_paths = [path] + ["%s.%s" % (prefix, path)
for prefix in _CMDSET_PATHS if not path.startswith(prefix)]
python_paths = [path] + [
"%s.%s" % (prefix, path) for prefix in _CMDSET_PATHS if not path.startswith(prefix)
]
errstring = ""
for python_path in python_paths:
@ -199,30 +206,44 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
logger.log_trace()
errstring += _ERROR_CMDSET_IMPORT
if _IN_GAME_ERRORS:
errstring = errstring.format(path=python_path, traceback=format_exc(), timestamp=logger.timeformat())
errstring = errstring.format(
path=python_path, traceback=format_exc(), timestamp=logger.timeformat()
)
else:
errstring = errstring.format(path=python_path, traceback=err, timestamp=logger.timeformat())
errstring = errstring.format(
path=python_path, traceback=err, timestamp=logger.timeformat()
)
break
except KeyError:
logger.log_trace()
errstring += _ERROR_CMDSET_KEYERROR
errstring = errstring.format(classname=classname, path=python_path, timestamp=logger.timeformat())
errstring = errstring.format(
classname=classname, path=python_path, timestamp=logger.timeformat()
)
break
except SyntaxError as err:
logger.log_trace()
errstring += _ERROR_CMDSET_SYNTAXERROR
if _IN_GAME_ERRORS:
errstring = errstring.format(path=python_path, traceback=format_exc(), timestamp=logger.timeformat())
errstring = errstring.format(
path=python_path, traceback=format_exc(), timestamp=logger.timeformat()
)
else:
errstring = errstring.format(path=python_path, traceback=err, timestamp=logger.timeformat())
errstring = errstring.format(
path=python_path, traceback=err, timestamp=logger.timeformat()
)
break
except Exception as err:
logger.log_trace()
errstring += _ERROR_CMDSET_EXCEPTION
if _IN_GAME_ERRORS:
errstring = errstring.format(path=python_path, traceback=format_exc(), timestamp=logger.timeformat())
errstring = errstring.format(
path=python_path, traceback=format_exc(), timestamp=logger.timeformat()
)
else:
errstring = errstring.format(path=python_path, traceback=err, timestamp=logger.timeformat())
errstring = errstring.format(
path=python_path, traceback=err, timestamp=logger.timeformat()
)
break
if errstring:
@ -237,6 +258,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
return err_cmdset
return None # undefined error
# classes
@ -296,9 +318,14 @@ class CmdSetHandler(object):
permstring = "perm"
if mergetype != cmdset.mergetype:
mergetype = "%s^" % (mergetype)
string += "\n %i: <%s (%s, prio %i, %s)>: %s" % \
(snum, cmdset.key, mergetype,
cmdset.priority, permstring, cmdset)
string += "\n %i: <%s (%s, prio %i, %s)>: %s" % (
snum,
cmdset.key,
mergetype,
cmdset.priority,
permstring,
cmdset,
)
mergelist.append(str(snum))
string += "\n"
@ -310,19 +337,24 @@ class CmdSetHandler(object):
mergetype = mergetype.format(mergetype=mergetype, cmdset=merged_on)
if mergelist:
tmpstring = _(" <Merged {mergelist} {mergetype}, prio {prio}>: {current}")
string += tmpstring.format(mergelist="+".join(mergelist),
mergetype=mergetype, prio=self.current.priority,
current=self.current)
string += tmpstring.format(
mergelist="+".join(mergelist),
mergetype=mergetype,
prio=self.current.priority,
current=self.current,
)
else:
permstring = "non-perm"
if self.current.permanent:
permstring = "perm"
tmpstring = _(" <{key} ({mergetype}, prio {prio}, {permstring})>:\n {keylist}")
string += tmpstring.format(key=self.current.key, mergetype=mergetype,
prio=self.current.priority,
permstring=permstring,
keylist=", ".join(cmd.key for
cmd in sorted(self.current, key=lambda o: o.key)))
string += tmpstring.format(
key=self.current.key,
mergetype=mergetype,
prio=self.current.priority,
permstring=permstring,
keylist=", ".join(cmd.key for cmd in sorted(self.current, key=lambda o: o.key)),
)
return string.strip()
def _import_cmdset(self, cmdset_path, emit_to_obj=None):
@ -363,23 +395,27 @@ class CmdSetHandler(object):
elif path:
cmdset = self._import_cmdset(path)
if cmdset:
if cmdset.key == '_CMDSET_ERROR':
if cmdset.key == "_CMDSET_ERROR":
# If a cmdset fails to load, check if we have a fallback path to use
fallback_path = _CMDSET_FALLBACKS.get(path, None)
if fallback_path:
err = _ERROR_CMDSET_FALLBACK.format(path=path, fallback_path=fallback_path)
err = _ERROR_CMDSET_FALLBACK.format(
path=path, fallback_path=fallback_path
)
logger.log_err(err)
if _IN_GAME_ERRORS:
self.obj.msg(err)
cmdset = self._import_cmdset(fallback_path)
# If no cmdset is returned from the fallback, we can't go further
if not cmdset:
err = _ERROR_CMDSET_NO_FALLBACK.format(fallback_path=fallback_path)
err = _ERROR_CMDSET_NO_FALLBACK.format(
fallback_path=fallback_path
)
logger.log_err(err)
if _IN_GAME_ERRORS:
self.obj.msg(err)
continue
cmdset.permanent = cmdset.key != '_CMDSET_ERROR'
cmdset.permanent = cmdset.key != "_CMDSET_ERROR"
self.cmdset_stack.append(cmdset)
# merge the stack into a new merged cmdset
@ -429,9 +465,9 @@ class CmdSetHandler(object):
elif isinstance(cmdset, str):
# this is (maybe) a python path. Try to import from cache.
cmdset = self._import_cmdset(cmdset)
if cmdset and cmdset.key != '_CMDSET_ERROR':
if cmdset and cmdset.key != "_CMDSET_ERROR":
cmdset.permanent = permanent
if permanent and cmdset.key != '_CMDSET_ERROR':
if permanent and cmdset.key != "_CMDSET_ERROR":
# store the path permanently
storage = self.obj.cmdset_storage or [""]
if default_cmdset:
@ -498,13 +534,15 @@ class CmdSetHandler(object):
self.obj.cmdset_storage = storage
else:
# try it as a callable
if callable(cmdset) and hasattr(cmdset, 'path'):
delcmdsets = [cset for cset in self.cmdset_stack[1:]
if cset.path == cmdset.path]
if callable(cmdset) and hasattr(cmdset, "path"):
delcmdsets = [cset for cset in self.cmdset_stack[1:] if cset.path == cmdset.path]
else:
# try it as a path or key
delcmdsets = [cset for cset in self.cmdset_stack[1:]
if cset.path == cmdset or cset.key == cmdset]
delcmdsets = [
cset
for cset in self.cmdset_stack[1:]
if cset.path == cmdset or cset.key == cmdset
]
storage = []
if any(cset.permanent for cset in delcmdsets):
@ -530,6 +568,7 @@ class CmdSetHandler(object):
pass
# re-sync the cmdsethandler.
self.update()
# legacy alias
delete = remove
@ -539,6 +578,7 @@ class CmdSetHandler(object):
"""
self.remove(default_cmdset=True)
# legacy alias
delete_default = remove_default
@ -582,22 +622,26 @@ class CmdSetHandler(object):
has_cmdset (bool): Whether or not the cmdset is in the handler.
"""
if callable(cmdset) and hasattr(cmdset, 'path'):
if callable(cmdset) and hasattr(cmdset, "path"):
# try it as a callable
if must_be_default:
return self.cmdset_stack and (self.cmdset_stack[0].path == cmdset.path)
else:
return any([cset for cset in self.cmdset_stack
if cset.path == cmdset.path])
return any([cset for cset in self.cmdset_stack if cset.path == cmdset.path])
else:
# try it as a path or key
if must_be_default:
return self.cmdset_stack and (
self.cmdset_stack[0].key == cmdset or
self.cmdset_stack[0].path == cmdset)
self.cmdset_stack[0].key == cmdset or self.cmdset_stack[0].path == cmdset
)
else:
return any([cset for cset in self.cmdset_stack
if cset.path == cmdset or cset.key == cmdset])
return any(
[
cset
for cset in self.cmdset_stack
if cset.path == cmdset or cset.key == cmdset
]
)
# backwards-compatability alias
has_cmdset = has

View file

@ -37,12 +37,10 @@ def _init_command(cls, **kwargs):
cls.key = cls.key.lower()
if cls.aliases and not is_iter(cls.aliases):
try:
cls.aliases = [str(alias).strip().lower()
for alias in cls.aliases.split(',')]
cls.aliases = [str(alias).strip().lower() for alias in cls.aliases.split(",")]
except Exception:
cls.aliases = []
cls.aliases = list(set(alias for alias in cls.aliases
if alias and alias != cls.key))
cls.aliases = list(set(alias for alias in cls.aliases if alias and alias != cls.key))
# optimization - a set is much faster to match against than a list
cls._matchset = set([cls.key] + cls.aliases)
@ -55,24 +53,24 @@ def _init_command(cls, **kwargs):
# pre-process locks as defined in class definition
temp = []
if hasattr(cls, 'permissions'):
if hasattr(cls, "permissions"):
cls.locks = cls.permissions
if not hasattr(cls, 'locks'):
if not hasattr(cls, "locks"):
# default if one forgets to define completely
cls.locks = "cmd:all()"
if "cmd:" not in cls.locks:
cls.locks = "cmd:all();" + cls.locks
for lockstring in cls.locks.split(';'):
if lockstring and ':' not in lockstring:
for lockstring in cls.locks.split(";"):
if lockstring and ":" not in lockstring:
lockstring = "cmd:%s" % lockstring
temp.append(lockstring)
cls.lock_storage = ";".join(temp)
if hasattr(cls, 'arg_regex') and isinstance(cls.arg_regex, str):
if hasattr(cls, "arg_regex") and isinstance(cls.arg_regex, str):
cls.arg_regex = re.compile(r"%s" % cls.arg_regex, re.I + re.UNICODE)
if not hasattr(cls, "auto_help"):
cls.auto_help = True
if not hasattr(cls, 'is_exit'):
if not hasattr(cls, "is_exit"):
cls.is_exit = False
if not hasattr(cls, "help_category"):
cls.help_category = "general"
@ -83,10 +81,12 @@ class CommandMeta(type):
"""
The metaclass cleans up all properties on the class
"""
def __init__(cls, *args, **kwargs):
_init_command(cls, **kwargs)
super().__init__(*args, **kwargs)
# The Command class is the basic unit of an Evennia command; when
# defining new commands, the admin subclass this class and
# define their own parser method to handle the input. The
@ -215,7 +215,7 @@ class Command(object, metaclass=CommandMeta):
str, too.
"""
return hash('\n'.join(self._matchset))
return hash("\n".join(self._matchset))
def __ne__(self, cmd):
"""
@ -283,7 +283,7 @@ class Command(object, metaclass=CommandMeta):
"""
if isinstance(new_aliases, str):
new_aliases = new_aliases.split(';')
new_aliases = new_aliases.split(";")
aliases = (str(alias).strip().lower() for alias in make_iter(new_aliases))
self.aliases = list(set(alias for alias in aliases if alias != self.key))
self._optimize()
@ -319,8 +319,7 @@ class Command(object, metaclass=CommandMeta):
"""
return self.lockhandler.check(srcobj, access_type, default=default)
def msg(self, text=None, to_obj=None, from_obj=None,
session=None, **kwargs):
def msg(self, text=None, to_obj=None, from_obj=None, session=None, **kwargs):
"""
This is a shortcut instead of calling msg() directly on an
object - it will detect if caller is an Object or an Account and
@ -410,7 +409,9 @@ class Command(object, metaclass=CommandMeta):
set in self.parse())
"""
variables = '\n'.join(" |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items())
variables = "\n".join(
" |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items()
)
string = f"""
Command {self} has no defined `func()` - showing on-command variables:
{variables}
@ -430,8 +431,10 @@ Command {self} has no defined `func()` - showing on-command variables:
string += "object storing cmdset (self.obj): |w%s|n\n" % self.obj
string += "command string given (self.cmdstring): |w%s|n\n" % self.cmdstring
# show cmdset.key instead of cmdset to shorten output
string += fill("current cmdset (self.cmdset): |w%s|n\n" %
(self.cmdset.key if self.cmdset.key else self.cmdset.__class__))
string += fill(
"current cmdset (self.cmdset): |w%s|n\n"
% (self.cmdset.key if self.cmdset.key else self.cmdset.__class__)
)
self.caller.msg(string)
@ -453,7 +456,7 @@ Command {self} has no defined `func()` - showing on-command variables:
object, conventionally with a preceding space.
"""
if hasattr(self, 'obj') and self.obj and self.obj != caller:
if hasattr(self, "obj") and self.obj and self.obj != caller:
return " (%s)" % self.obj.get_display_name(caller).strip()
return ""
@ -486,7 +489,7 @@ Command {self} has no defined `func()` - showing on-command variables:
"""
if self.session:
return self.session.protocol_flags['SCREENWIDTH'][0]
return self.session.protocol_flags["SCREENWIDTH"][0]
def styled_table(self, *args, **kwargs):
"""
@ -503,35 +506,48 @@ Command {self} has no defined `func()` - showing on-command variables:
or incomplete and ready for use with `.add_row` or `.add_collumn`.
"""
border_color = self.account.options.get('border_color')
column_color = self.account.options.get('column_names_color')
border_color = self.account.options.get("border_color")
column_color = self.account.options.get("column_names_color")
colornames = ['|%s%s|n' % (column_color, col) for col in args]
colornames = ["|%s%s|n" % (column_color, col) for col in args]
h_line_char = kwargs.pop('header_line_char', '~')
header_line_char = ANSIString(f'|{border_color}{h_line_char}|n')
c_char = kwargs.pop('corner_char', '+')
corner_char = ANSIString(f'|{border_color}{c_char}|n')
h_line_char = kwargs.pop("header_line_char", "~")
header_line_char = ANSIString(f"|{border_color}{h_line_char}|n")
c_char = kwargs.pop("corner_char", "+")
corner_char = ANSIString(f"|{border_color}{c_char}|n")
b_left_char = kwargs.pop('border_left_char', '||')
border_left_char = ANSIString(f'|{border_color}{b_left_char}|n')
b_left_char = kwargs.pop("border_left_char", "||")
border_left_char = ANSIString(f"|{border_color}{b_left_char}|n")
b_right_char = kwargs.pop('border_right_char', '||')
border_right_char = ANSIString(f'|{border_color}{b_right_char}|n')
b_right_char = kwargs.pop("border_right_char", "||")
border_right_char = ANSIString(f"|{border_color}{b_right_char}|n")
b_bottom_char = kwargs.pop('border_bottom_char', '-')
border_bottom_char = ANSIString(f'|{border_color}{b_bottom_char}|n')
b_bottom_char = kwargs.pop("border_bottom_char", "-")
border_bottom_char = ANSIString(f"|{border_color}{b_bottom_char}|n")
b_top_char = kwargs.pop('border_top_char', '-')
border_top_char = ANSIString(f'|{border_color}{b_top_char}|n')
b_top_char = kwargs.pop("border_top_char", "-")
border_top_char = ANSIString(f"|{border_color}{b_top_char}|n")
table = EvTable(*colornames, header_line_char=header_line_char, corner_char=corner_char,
border_left_char=border_left_char, border_right_char=border_right_char,
border_top_char=border_top_char, **kwargs)
table = EvTable(
*colornames,
header_line_char=header_line_char,
corner_char=corner_char,
border_left_char=border_left_char,
border_right_char=border_right_char,
border_top_char=border_top_char,
**kwargs,
)
return table
def _render_decoration(self, header_text=None, fill_character=None, edge_character=None,
mode='header', color_header=True, width=None):
def _render_decoration(
self,
header_text=None,
fill_character=None,
edge_character=None,
mode="header",
color_header=True,
width=None,
):
"""
Helper for formatting a string into a pretty display, for a header, separator or footer.
@ -550,9 +566,9 @@ Command {self} has no defined `func()` - showing on-command variables:
"""
colors = dict()
colors['border'] = self.account.options.get('border_color')
colors['headertext'] = self.account.options.get('%s_text_color' % mode)
colors['headerstar'] = self.account.options.get('%s_star_color' % mode)
colors["border"] = self.account.options.get("border_color")
colors["headertext"] = self.account.options.get("%s_text_color" % mode)
colors["headerstar"] = self.account.options.get("%s_star_color" % mode)
width = width or self.client_width()
if edge_character:
@ -561,17 +577,19 @@ Command {self} has no defined `func()` - showing on-command variables:
if header_text:
if color_header:
header_text = ANSIString(header_text).clean()
header_text = ANSIString('|n|%s%s|n' % (colors['headertext'], header_text))
if mode == 'header':
begin_center = ANSIString("|n|%s<|%s* |n" % (colors['border'], colors['headerstar']))
end_center = ANSIString("|n |%s*|%s>|n" % (colors['headerstar'], colors['border']))
header_text = ANSIString("|n|%s%s|n" % (colors["headertext"], header_text))
if mode == "header":
begin_center = ANSIString(
"|n|%s<|%s* |n" % (colors["border"], colors["headerstar"])
)
end_center = ANSIString("|n |%s*|%s>|n" % (colors["headerstar"], colors["border"]))
center_string = ANSIString(begin_center + header_text + end_center)
else:
center_string = ANSIString('|n |%s%s |n' % (colors['headertext'], header_text))
center_string = ANSIString("|n |%s%s |n" % (colors["headertext"], header_text))
else:
center_string = ''
center_string = ""
fill_character = self.account.options.get('%s_fill' % mode)
fill_character = self.account.options.get("%s_fill" % mode)
remain_fill = width - len(center_string)
if remain_fill % 2 == 0:
@ -581,13 +599,15 @@ Command {self} has no defined `func()` - showing on-command variables:
right_width = math.floor(remain_fill / 2)
left_width = math.ceil(remain_fill / 2)
right_fill = ANSIString('|n|%s%s|n' % (colors['border'], fill_character * int(right_width)))
left_fill = ANSIString('|n|%s%s|n' % (colors['border'], fill_character * int(left_width)))
right_fill = ANSIString("|n|%s%s|n" % (colors["border"], fill_character * int(right_width)))
left_fill = ANSIString("|n|%s%s|n" % (colors["border"], fill_character * int(left_width)))
if edge_character:
edge_fill = ANSIString('|n|%s%s|n' % (colors['border'], edge_character))
edge_fill = ANSIString("|n|%s%s|n" % (colors["border"], edge_character))
main_string = ANSIString(center_string)
final_send = ANSIString(edge_fill) + left_fill + main_string + right_fill + ANSIString(edge_fill)
final_send = (
ANSIString(edge_fill) + left_fill + main_string + right_fill + ANSIString(edge_fill)
)
else:
final_send = left_fill + ANSIString(center_string) + right_fill
return final_send
@ -597,8 +617,8 @@ Command {self} has no defined `func()` - showing on-command variables:
Create a pretty header.
"""
if 'mode' not in kwargs:
kwargs['mode'] = 'header'
if "mode" not in kwargs:
kwargs["mode"] = "header"
return self._render_decoration(*args, **kwargs)
def styled_separator(self, *args, **kwargs):
@ -606,8 +626,8 @@ Command {self} has no defined `func()` - showing on-command variables:
Create a separator.
"""
if 'mode' not in kwargs:
kwargs['mode'] = 'separator'
if "mode" not in kwargs:
kwargs["mode"] = "separator"
return self._render_decoration(*args, **kwargs)
def styled_footer(self, *args, **kwargs):
@ -615,8 +635,8 @@ Command {self} has no defined `func()` - showing on-command variables:
Create a pretty footer.
"""
if 'mode' not in kwargs:
kwargs['mode'] = 'footer'
if "mode" not in kwargs:
kwargs["mode"] = "footer"
return self._render_decoration(*args, **kwargs)

View file

@ -30,9 +30,21 @@ _MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS
_MULTISESSION_MODE = settings.MULTISESSION_MODE
# limit symbol import for API
__all__ = ("CmdOOCLook", "CmdIC", "CmdOOC", "CmdPassword", "CmdQuit",
"CmdCharCreate", "CmdOption", "CmdSessions", "CmdWho",
"CmdColorTest", "CmdQuell", "CmdCharDelete", "CmdStyle")
__all__ = (
"CmdOOCLook",
"CmdIC",
"CmdOOC",
"CmdPassword",
"CmdQuit",
"CmdCharCreate",
"CmdOption",
"CmdSessions",
"CmdWho",
"CmdColorTest",
"CmdQuell",
"CmdCharDelete",
"CmdStyle",
)
class MuxAccountLookCommand(COMMAND_DEFAULT_CLASS):
@ -60,8 +72,9 @@ class MuxAccountLookCommand(COMMAND_DEFAULT_CLASS):
self.account.db._playable_characters = playable
# store playable property
if self.args:
self.playable = dict((utils.to_str(char.key.lower()), char)
for char in playable).get(self.args.lower(), None)
self.playable = dict((utils.to_str(char.key.lower()), char) for char in playable).get(
self.args.lower(), None
)
else:
self.playable = playable
@ -119,6 +132,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
always be able to access your character using lower-case letters
if you want.
"""
key = "charcreate"
locks = "cmd:pperm(Player)"
help_category = "General"
@ -137,12 +151,13 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
charmax = _MAX_NR_CHARACTERS
if not account.is_superuser and \
(account.db._playable_characters and
len(account.db._playable_characters) >= charmax):
if not account.is_superuser and (
account.db._playable_characters and len(account.db._playable_characters) >= charmax
):
self.msg("You may only create a maximum of %i characters." % charmax)
return
from evennia.objects.models import ObjectDB
typeclass = settings.BASE_CHARACTER_TYPECLASS
if ObjectDB.objects.filter(db_typeclass_path=typeclass, db_key__iexact=key):
@ -156,21 +171,27 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
start_location = ObjectDB.objects.get_id(settings.START_LOCATION)
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
new_character = create.create_object(typeclass, key=key,
location=start_location,
home=default_home,
permissions=permissions)
new_character = create.create_object(
typeclass, key=key, location=start_location, home=default_home, permissions=permissions
)
# only allow creator (and developers) to puppet this char
new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer);delete:id(%i) or perm(Admin)" %
(new_character.id, account.id, account.id))
new_character.locks.add(
"puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer);delete:id(%i) or perm(Admin)"
% (new_character.id, account.id, account.id)
)
account.db._playable_characters.append(new_character)
if desc:
new_character.db.desc = desc
elif not new_character.db.desc:
new_character.db.desc = "This is a character."
self.msg("Created new character %s. Use |wic %s|n to enter the game as this character."
% (new_character.key, new_character.key))
logger.log_sec('Character Created: %s (Caller: %s, IP: %s).' % (new_character, account, self.session.address))
self.msg(
"Created new character %s. Use |wic %s|n to enter the game as this character."
% (new_character.key, new_character.key)
)
logger.log_sec(
"Character Created: %s (Caller: %s, IP: %s)."
% (new_character, account, self.session.address)
)
class CmdCharDelete(COMMAND_DEFAULT_CLASS):
@ -182,6 +203,7 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
Permanently deletes one of your characters.
"""
key = "chardelete"
locks = "cmd:pperm(Player)"
help_category = "General"
@ -195,13 +217,18 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
return
# use the playable_characters list to search
match = [char for char in utils.make_iter(account.db._playable_characters)
if char.key.lower() == self.args.lower()]
match = [
char
for char in utils.make_iter(account.db._playable_characters)
if char.key.lower() == self.args.lower()
]
if not match:
self.msg("You have no such character to delete.")
return
elif len(match) > 1:
self.msg("Aborting - there are two characters with the same name. Ask an admin to delete the right one.")
self.msg(
"Aborting - there are two characters with the same name. Ask an admin to delete the right one."
)
return
else: # one match
from evennia.utils.evmenu import get_input
@ -211,10 +238,15 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
# only take action
delobj = caller.ndb._char_to_delete
key = delobj.key
caller.db._playable_characters = [pc for pc in caller.db._playable_characters if pc != delobj]
caller.db._playable_characters = [
pc for pc in caller.db._playable_characters if pc != delobj
]
delobj.delete()
self.msg("Character '%s' was permanently deleted." % key)
logger.log_sec('Character Deleted: %s (Caller: %s, IP: %s).' % (key, account, self.session.address))
logger.log_sec(
"Character Deleted: %s (Caller: %s, IP: %s)."
% (key, account, self.session.address)
)
else:
self.msg("Deletion was aborted.")
del caller.ndb._char_to_delete
@ -223,11 +255,13 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
account.ndb._char_to_delete = match
# Return if caller has no permission to delete this
if not match.access(account, 'delete'):
if not match.access(account, "delete"):
self.msg("You do not have permission to delete this character.")
return
prompt = "|rThis will permanently destroy '%s'. This cannot be undone.|n Continue yes/[no]?"
prompt = (
"|rThis will permanently destroy '%s'. This cannot be undone.|n Continue yes/[no]?"
)
get_input(account, prompt % match.key, _callback)
@ -273,23 +307,33 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
return
if not new_character:
# search for a matching character
new_character = [char for char in search.object_search(self.args) if char.access(account, "puppet")]
new_character = [
char for char in search.object_search(self.args) if char.access(account, "puppet")
]
if not new_character:
self.msg("That is not a valid character choice.")
return
if len(new_character) > 1:
self.msg("Multiple targets with the same name:\n %s"
% ", ".join("%s(#%s)" % (obj.key, obj.id) for obj in new_character))
self.msg(
"Multiple targets with the same name:\n %s"
% ", ".join("%s(#%s)" % (obj.key, obj.id) for obj in new_character)
)
return
else:
new_character = new_character[0]
try:
account.puppet_object(session, new_character)
account.db._last_puppet = new_character
logger.log_sec('Puppet Success: (Caller: %s, Target: %s, IP: %s).' % (account, new_character, self.session.address))
logger.log_sec(
"Puppet Success: (Caller: %s, Target: %s, IP: %s)."
% (account, new_character, self.session.address)
)
except RuntimeError as exc:
self.msg("|rYou cannot become |C%s|n: %s" % (new_character.name, exc))
logger.log_sec('Puppet Failed: %s (Caller: %s, Target: %s, IP: %s).' % (exc, account, new_character, self.session.address))
logger.log_sec(
"Puppet Failed: %s (Caller: %s, Target: %s, IP: %s)."
% (exc, account, new_character, self.session.address)
)
# note that this is inheriting from MuxAccountLookCommand,
@ -354,6 +398,7 @@ class CmdSessions(COMMAND_DEFAULT_CLASS):
Lists the sessions currently connected to your account.
"""
key = "sessions"
locks = "cmd:all()"
help_category = "General"
@ -365,17 +410,18 @@ class CmdSessions(COMMAND_DEFAULT_CLASS):
"""Implement function"""
account = self.account
sessions = account.sessions.all()
table = self.styled_table("|wsessid",
"|wprotocol",
"|whost",
"|wpuppet/character",
"|wlocation")
table = self.styled_table(
"|wsessid", "|wprotocol", "|whost", "|wpuppet/character", "|wlocation"
)
for sess in sorted(sessions, key=lambda x: x.sessid):
char = account.get_puppet(sess)
table.add_row(str(sess.sessid), str(sess.protocol_key),
isinstance(sess.address, tuple) and sess.address[0] or sess.address,
char and str(char) or "None",
char and str(char.location) or "N/A")
table.add_row(
str(sess.sessid),
str(sess.protocol_key),
isinstance(sess.address, tuple) and sess.address[0] or sess.address,
char and str(char) or "None",
char and str(char.location) or "N/A",
)
self.msg("|wYour current session(s):|n\n%s" % table)
@ -411,19 +457,23 @@ class CmdWho(COMMAND_DEFAULT_CLASS):
if self.cmdstring == "doing":
show_session_data = False
else:
show_session_data = account.check_permstring("Developer") or account.check_permstring("Admins")
show_session_data = account.check_permstring("Developer") or account.check_permstring(
"Admins"
)
naccounts = SESSIONS.account_count()
if show_session_data:
# privileged info
table = self.styled_table("|wAccount Name",
"|wOn for",
"|wIdle",
"|wPuppeting",
"|wRoom",
"|wCmds",
"|wProtocol",
"|wHost")
table = self.styled_table(
"|wAccount Name",
"|wOn for",
"|wIdle",
"|wPuppeting",
"|wRoom",
"|wCmds",
"|wProtocol",
"|wHost",
)
for session in session_list:
if not session.logged_in:
continue
@ -432,14 +482,16 @@ class CmdWho(COMMAND_DEFAULT_CLASS):
account = session.get_account()
puppet = session.get_puppet()
location = puppet.location.key if puppet and puppet.location else "None"
table.add_row(utils.crop(account.get_display_name(account), width=25),
utils.time_format(delta_conn, 0),
utils.time_format(delta_cmd, 1),
utils.crop(puppet.get_display_name(account) if puppet else "None", width=25),
utils.crop(location, width=25),
session.cmd_total,
session.protocol_key,
isinstance(session.address, tuple) and session.address[0] or session.address)
table.add_row(
utils.crop(account.get_display_name(account), width=25),
utils.time_format(delta_conn, 0),
utils.time_format(delta_cmd, 1),
utils.crop(puppet.get_display_name(account) if puppet else "None", width=25),
utils.crop(location, width=25),
session.cmd_total,
session.protocol_key,
isinstance(session.address, tuple) and session.address[0] or session.address,
)
else:
# unprivileged
table = self.styled_table("|wAccount name", "|wOn for", "|wIdle")
@ -449,12 +501,16 @@ class CmdWho(COMMAND_DEFAULT_CLASS):
delta_cmd = time.time() - session.cmd_last_visible
delta_conn = time.time() - session.conn_time
account = session.get_account()
table.add_row(utils.crop(account.get_display_name(account), width=25),
utils.time_format(delta_conn, 0),
utils.time_format(delta_cmd, 1))
table.add_row(
utils.crop(account.get_display_name(account), width=25),
utils.time_format(delta_conn, 0),
utils.time_format(delta_cmd, 1),
)
is_one = naccounts == 1
self.msg("|wAccounts:|n\n%s\n%s unique account%s logged in."
% (table, "One" if is_one else naccounts, "" if is_one else "s"))
self.msg(
"|wAccounts:|n\n%s\n%s unique account%s logged in."
% (table, "One" if is_one else naccounts, "" if is_one else "s")
)
class CmdOption(COMMAND_DEFAULT_CLASS):
@ -474,6 +530,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
"""
key = "option"
aliases = "options"
switch_options = ("save", "clear")
@ -511,14 +568,18 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
if len(options["SCREENWIDTH"]) == 1:
options["SCREENWIDTH"] = options["SCREENWIDTH"][0]
else:
options["SCREENWIDTH"] = " \n".join("%s : %s" % (screenid, size)
for screenid, size in options["SCREENWIDTH"].items())
options["SCREENWIDTH"] = " \n".join(
"%s : %s" % (screenid, size)
for screenid, size in options["SCREENWIDTH"].items()
)
if "SCREENHEIGHT" in options:
if len(options["SCREENHEIGHT"]) == 1:
options["SCREENHEIGHT"] = options["SCREENHEIGHT"][0]
else:
options["SCREENHEIGHT"] = " \n".join("%s : %s" % (screenid, size)
for screenid, size in options["SCREENHEIGHT"].items())
options["SCREENHEIGHT"] = " \n".join(
"%s : %s" % (screenid, size)
for screenid, size in options["SCREENHEIGHT"].items()
)
options.pop("TTYPE", None)
header = ("Name", "Value", "Saved") if saved_options else ("Name", "Value")
@ -527,7 +588,9 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
row = [key, options[key]]
if saved_options:
saved = " |YYes|n" if key in saved_options else ""
changed = "|y*|n" if key in saved_options and flags[key] != saved_options[key] else ""
changed = (
"|y*|n" if key in saved_options and flags[key] != saved_options[key] else ""
)
row.append("%s%s" % (saved, changed))
table.add_row(*row)
self.msg("|wClient settings (%s):|n\n%s|n" % (self.session.protocol_key, table))
@ -563,30 +626,35 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
self.msg("Option |w%s|n was kept as '|w%s|n'." % (new_name, old_val))
else:
flags[new_name] = new_val
self.msg("Option |w%s|n was changed from '|w%s|n' to '|w%s|n'." % (new_name, old_val, new_val))
self.msg(
"Option |w%s|n was changed from '|w%s|n' to '|w%s|n'."
% (new_name, old_val, new_val)
)
return {new_name: new_val}
except Exception as err:
self.msg("|rCould not set option |w%s|r:|n %s" % (new_name, err))
return False
validators = {"ANSI": validate_bool,
"CLIENTNAME": utils.to_str,
"ENCODING": validate_encoding,
"MCCP": validate_bool,
"NOGOAHEAD": validate_bool,
"MXP": validate_bool,
"NOCOLOR": validate_bool,
"NOPKEEPALIVE": validate_bool,
"OOB": validate_bool,
"RAW": validate_bool,
"SCREENHEIGHT": validate_size,
"SCREENWIDTH": validate_size,
"SCREENREADER": validate_bool,
"TERM": utils.to_str,
"UTF-8": validate_bool,
"XTERM256": validate_bool,
"INPUTDEBUG": validate_bool,
"FORCEDENDLINE": validate_bool}
validators = {
"ANSI": validate_bool,
"CLIENTNAME": utils.to_str,
"ENCODING": validate_encoding,
"MCCP": validate_bool,
"NOGOAHEAD": validate_bool,
"MXP": validate_bool,
"NOCOLOR": validate_bool,
"NOPKEEPALIVE": validate_bool,
"OOB": validate_bool,
"RAW": validate_bool,
"SCREENHEIGHT": validate_size,
"SCREENWIDTH": validate_size,
"SCREENREADER": validate_bool,
"TERM": utils.to_str,
"UTF-8": validate_bool,
"XTERM256": validate_bool,
"INPUTDEBUG": validate_bool,
"FORCEDENDLINE": validate_bool,
}
name = self.lhs.upper()
val = self.rhs.strip()
@ -621,6 +689,7 @@ class CmdPassword(COMMAND_DEFAULT_CLASS):
Changes your password. Make sure to pick a safe one.
"""
key = "password"
locks = "cmd:pperm(Player)"
@ -650,7 +719,10 @@ class CmdPassword(COMMAND_DEFAULT_CLASS):
account.set_password(newpass)
account.save()
self.msg("Password changed.")
logger.log_sec('Password Changed: %s (Caller: %s, IP: %s).' % (account, account, self.session.address))
logger.log_sec(
"Password Changed: %s (Caller: %s, IP: %s)."
% (account, account, self.session.address)
)
class CmdQuit(COMMAND_DEFAULT_CLASS):
@ -666,6 +738,7 @@ class CmdQuit(COMMAND_DEFAULT_CLASS):
Gracefully disconnect your current session from the
game. Use the /all switch to disconnect from all sessions.
"""
key = "quit"
switch_options = ("all",)
locks = "cmd:all()"
@ -677,8 +750,10 @@ class CmdQuit(COMMAND_DEFAULT_CLASS):
"""hook function"""
account = self.account
if 'all' in self.switches:
account.msg("|RQuitting|n all sessions. Hope to see you soon again.", session=self.session)
if "all" in self.switches:
account.msg(
"|RQuitting|n all sessions. Hope to see you soon again.", session=self.session
)
reason = "quit/all"
for session in account.sessions.all():
account.disconnect_session_from_account(session, reason)
@ -688,7 +763,10 @@ class CmdQuit(COMMAND_DEFAULT_CLASS):
if nsess == 2:
account.msg("|RQuitting|n. One session is still connected.", session=self.session)
elif nsess > 2:
account.msg("|RQuitting|n. %i sessions are still connected." % (nsess - 1), session=self.session)
account.msg(
"|RQuitting|n. %i sessions are still connected." % (nsess - 1),
session=self.session,
)
else:
# we are quitting the last available session
account.msg("|RQuitting|n. Hope to see you again, soon.", session=self.session)
@ -708,6 +786,7 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
standard. No checking is done to determine your client supports
color - if not you will see rubbish appear.
"""
key = "color"
locks = "cmd:all()"
help_category = "General"
@ -719,7 +798,7 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
# relevant color tags to display. Replace if using another schema.
# This command can only show one set of markup.
slice_bright_fg = slice(7, 15) # from ANSI_PARSER.ansi_map
slice_dark_fg = slice(15, 23) # from ANSI_PARSER.ansi_map
slice_dark_fg = slice(15, 23) # from ANSI_PARSER.ansi_map
slice_dark_bg = slice(-8, None) # from ANSI_PARSER.ansi_map
slice_bright_bg = slice(None, None) # from ANSI_PARSER.ansi_xterm256_bright_bg_map
@ -735,8 +814,12 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
max_widths = [max([len(str(val)) for val in col]) for col in table]
ftable = []
for irow in range(len(table[0])):
ftable.append([str(col[irow]).ljust(max_widths[icol]) + " " *
extra_space for icol, col in enumerate(table)])
ftable.append(
[
str(col[irow]).ljust(max_widths[icol]) + " " * extra_space
for icol, col in enumerate(table)
]
)
return ftable
def func(self):
@ -745,26 +828,37 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
if self.args.startswith("a"):
# show ansi 16-color table
from evennia.utils import ansi
ap = ansi.ANSI_PARSER
# ansi colors
# show all ansi color-related codes
bright_fg = ["%s%s|n" % (code, code.replace("|", "||"))
for code, _ in ap.ansi_map[self.slice_bright_fg]]
dark_fg = ["%s%s|n" % (code, code.replace("|", "||"))
for code, _ in ap.ansi_map[self.slice_dark_fg]]
dark_bg = ["%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", ""))
for code, _ in ap.ansi_map[self.slice_dark_bg]]
bright_bg = ["%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", ""))
for code, _ in ap.ansi_xterm256_bright_bg_map[self.slice_bright_bg]]
bright_fg = [
"%s%s|n" % (code, code.replace("|", "||"))
for code, _ in ap.ansi_map[self.slice_bright_fg]
]
dark_fg = [
"%s%s|n" % (code, code.replace("|", "||"))
for code, _ in ap.ansi_map[self.slice_dark_fg]
]
dark_bg = [
"%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", ""))
for code, _ in ap.ansi_map[self.slice_dark_bg]
]
bright_bg = [
"%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", ""))
for code, _ in ap.ansi_xterm256_bright_bg_map[self.slice_bright_bg]
]
dark_fg.extend(["" for _ in range(len(bright_fg) - len(dark_fg))])
table = utils.format_table([bright_fg, dark_fg, bright_bg, dark_bg])
string = "ANSI colors:"
for row in table:
string += "\n " + " ".join(row)
self.msg(string)
self.msg("||X : black. ||/ : return, ||- : tab, ||_ : space, ||* : invert, ||u : underline\n"
"To combine background and foreground, add background marker last, e.g. ||r||[B.\n"
"Note: bright backgrounds like ||[r requires your client handling Xterm256 colors.")
self.msg(
"||X : black. ||/ : return, ||- : tab, ||_ : space, ||* : invert, ||u : underline\n"
"To combine background and foreground, add background marker last, e.g. ||r||[B.\n"
"Note: bright backgrounds like ||[r requires your client handling Xterm256 colors."
)
elif self.args.startswith("x"):
# show xterm256 table
@ -775,8 +869,10 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
# foreground table
table[ir].append("|%i%i%i%s|n" % (ir, ig, ib, "||%i%i%i" % (ir, ig, ib)))
# background table
table[6 + ir].append("|%i%i%i|[%i%i%i%s|n"
% (5 - ir, 5 - ig, 5 - ib, ir, ig, ib, "||[%i%i%i" % (ir, ig, ib)))
table[6 + ir].append(
"|%i%i%i|[%i%i%i%s|n"
% (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 += "\n" + "\n".join("".join(row) for row in table)
@ -845,24 +941,30 @@ class CmdQuell(COMMAND_DEFAULT_CLASS):
def func(self):
"""Perform the command"""
account = self.account
permstr = account.is_superuser and " (superuser)" or "(%s)" % (", ".join(account.permissions.all()))
if self.cmdstring in ('unquell', 'unquell'):
if not account.attributes.get('_quell'):
permstr = (
account.is_superuser
and " (superuser)"
or "(%s)" % (", ".join(account.permissions.all()))
)
if self.cmdstring in ("unquell", "unquell"):
if not account.attributes.get("_quell"):
self.msg("Already using normal Account permissions %s." % permstr)
else:
account.attributes.remove('_quell')
account.attributes.remove("_quell")
self.msg("Account permissions %s restored." % permstr)
else:
if account.attributes.get('_quell'):
if account.attributes.get("_quell"):
self.msg("Already quelling Account %s permissions." % permstr)
return
account.attributes.add('_quell', True)
account.attributes.add("_quell", True)
puppet = self.session.puppet
if puppet:
cpermstr = "(%s)" % ", ".join(puppet.permissions.all())
cpermstr = "Quelling to current puppet's permissions %s." % cpermstr
cpermstr += "\n(Note: If this is higher than Account permissions %s," \
" the lowest of the two will be used.)" % permstr
cpermstr += (
"\n(Note: If this is higher than Account permissions %s,"
" the lowest of the two will be used.)" % permstr
)
cpermstr += "\nUse unquell to return to normal permission usage."
self.msg(cpermstr)
else:
@ -884,7 +986,7 @@ class CmdStyle(COMMAND_DEFAULT_CLASS):
"""
key = "style"
switch_options = ['clear']
switch_options = ["clear"]
def func(self):
if not self.args:
@ -893,11 +995,12 @@ class CmdStyle(COMMAND_DEFAULT_CLASS):
self.set()
def list_styles(self):
table = self.styled_table('Option', 'Description', 'Type', 'Value', width=78)
table = self.styled_table("Option", "Description", "Type", "Value", width=78)
for op_key in self.account.options.options_dict.keys():
op_found = self.account.options.get(op_key, return_obj=True)
table.add_row(op_key, op_found.description,
op_found.__class__.__name__, op_found.display())
table.add_row(
op_key, op_found.description, op_found.__class__.__name__, op_found.display()
)
self.msg(str(table))
def set(self):
@ -906,4 +1009,4 @@ class CmdStyle(COMMAND_DEFAULT_CLASS):
except ValueError as e:
self.msg(str(e))
return
self.msg('Style %s set to %s' % (self.lhs, result))
self.msg("Style %s set to %s" % (self.lhs, result))

View file

@ -16,8 +16,16 @@ COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
# limit members for API inclusion
__all__ = ("CmdBoot", "CmdBan", "CmdUnban",
"CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall", "CmdForce")
__all__ = (
"CmdBoot",
"CmdBan",
"CmdUnban",
"CmdEmit",
"CmdNewPassword",
"CmdPerm",
"CmdWall",
"CmdForce",
)
class CmdBoot(COMMAND_DEFAULT_CLASS):
@ -49,14 +57,14 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
caller.msg("Usage: boot[/switches] <account> [:reason]")
return
if ':' in args:
args, reason = [a.strip() for a in args.split(':', 1)]
if ":" in args:
args, reason = [a.strip() for a in args.split(":", 1)]
else:
args, reason = args, ""
boot_list = []
if 'sid' in self.switches:
if "sid" in self.switches:
# Boot a particular session id.
sessions = SESSIONS.get_sessions(True)
for sess in sessions:
@ -71,8 +79,8 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
caller.msg("Account %s was not found." % args)
return
pobj = pobj[0]
if not pobj.access(caller, 'boot'):
string = "You don't have the permission to boot %s." % (pobj.key, )
if not pobj.access(caller, "boot"):
string = "You don't have the permission to boot %s." % (pobj.key,)
caller.msg(string)
return
# we have a bootable object with a connected user
@ -87,7 +95,7 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
# Carry out the booting of the sessions in the boot list.
feedback = None
if 'quiet' not in self.switches:
if "quiet" not in self.switches:
feedback = "You have been disconnected by %s.\n" % caller.name
if reason:
feedback += "\nReason given: %s" % reason
@ -97,7 +105,10 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
session.account.disconnect_session_from_account(session)
if pobj and boot_list:
logger.log_sec('Booted: %s (Reason: %s, Caller: %s, IP: %s).' % (pobj, reason, caller, self.session.address))
logger.log_sec(
"Booted: %s (Reason: %s, Caller: %s, IP: %s)."
% (pobj, reason, caller, self.session.address)
)
# regex matching IP addresses with wildcards, eg. 233.122.4.*
@ -118,9 +129,7 @@ def list_bans(cmd, banlist):
table = cmd.styled_table("|wid", "|wname/ip", "|wdate", "|wreason")
for inum, ban in enumerate(banlist):
table.add_row(str(inum + 1),
ban[0] and ban[0] or ban[1],
ban[3], ban[4])
table.add_row(str(inum + 1), ban[0] and ban[0] or ban[1], ban[3], ban[4])
return "|wActive bans:|n\n%s" % table
@ -157,6 +166,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
or region.
"""
key = "ban"
aliases = ["bans"]
locks = "cmd:perm(ban) or perm(Developer)"
@ -175,20 +185,20 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
'reason' is any optional info given to the command. Unset
values in each tuple is set to the empty string.
"""
banlist = ServerConfig.objects.conf('server_bans')
banlist = ServerConfig.objects.conf("server_bans")
if not banlist:
banlist = []
if not self.args or (self.switches and
not any(switch in ('ip', 'name')
for switch in self.switches)):
if not self.args or (
self.switches and not any(switch in ("ip", "name") for switch in self.switches)
):
self.caller.msg(list_bans(self, banlist))
return
now = time.ctime()
reason = ""
if ':' in self.args:
ban, reason = self.args.rsplit(':', 1)
if ":" in self.args:
ban, reason = self.args.rsplit(":", 1)
else:
ban = self.args
ban = ban.lower()
@ -202,15 +212,18 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
typ = "IP"
ban = ipban[0]
# replace * with regex form and compile it
ipregex = ban.replace('.', '\.')
ipregex = ipregex.replace('*', '[0-9]{1,3}')
ipregex = ban.replace(".", "\.")
ipregex = ipregex.replace("*", "[0-9]{1,3}")
ipregex = re.compile(r"%s" % ipregex)
bantup = ("", ban, ipregex, now, reason)
# save updated banlist
banlist.append(bantup)
ServerConfig.objects.conf('server_bans', banlist)
ServerConfig.objects.conf("server_bans", banlist)
self.caller.msg("%s-Ban |w%s|n was added." % (typ, ban))
logger.log_sec('Banned %s: %s (Caller: %s, IP: %s).' % (typ, ban.strip(), self.caller, self.session.address))
logger.log_sec(
"Banned %s: %s (Caller: %s, IP: %s)."
% (typ, ban.strip(), self.caller, self.session.address)
)
class CmdUnban(COMMAND_DEFAULT_CLASS):
@ -226,6 +239,7 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
unban.
"""
key = "unban"
locks = "cmd:perm(unban) or perm(Developer)"
help_category = "Admin"
@ -233,7 +247,7 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
def func(self):
"""Implement unbanning"""
banlist = ServerConfig.objects.conf('server_bans')
banlist = ServerConfig.objects.conf("server_bans")
if not self.args:
self.caller.msg(list_bans(self, banlist))
@ -253,11 +267,13 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
# all is ok, clear ban
ban = banlist[num - 1]
del banlist[num - 1]
ServerConfig.objects.conf('server_bans', banlist)
ServerConfig.objects.conf("server_bans", banlist)
value = " ".join([s for s in ban[:2]])
self.caller.msg("Cleared ban %s: %s" %
(num, value))
logger.log_sec('Unbanned: %s (Caller: %s, IP: %s).' % (value.strip(), self.caller, self.session.address))
self.caller.msg("Cleared ban %s: %s" % (num, value))
logger.log_sec(
"Unbanned: %s (Caller: %s, IP: %s)."
% (value.strip(), self.caller, self.session.address)
)
class CmdEmit(COMMAND_DEFAULT_CLASS):
@ -280,6 +296,7 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
limited forms of emit, for sending to rooms and
to accounts respectively.
"""
key = "emit"
aliases = ["pemit", "remit"]
switch_options = ("room", "accounts", "contents")
@ -300,15 +317,15 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
caller.msg(string)
return
rooms_only = 'rooms' in self.switches
accounts_only = 'accounts' in self.switches
send_to_contents = 'contents' in self.switches
rooms_only = "rooms" in self.switches
accounts_only = "accounts" in self.switches
send_to_contents = "contents" in self.switches
# we check which command was used to force the switches
if self.cmdstring == 'remit':
if self.cmdstring == "remit":
rooms_only = True
send_to_contents = True
elif self.cmdstring == 'pemit':
elif self.cmdstring == "pemit":
accounts_only = True
if not self.rhs:
@ -329,7 +346,7 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
if accounts_only and not obj.has_account:
caller.msg("%s has no active account. Ignored." % objname)
continue
if obj.access(caller, 'tell'):
if obj.access(caller, "tell"):
obj.msg(message)
if send_to_contents and hasattr(obj, "msg_contents"):
obj.msg_contents(message)
@ -382,9 +399,10 @@ class CmdNewPassword(COMMAND_DEFAULT_CLASS):
account.save()
self.msg("%s - new password set to '%s'." % (account.name, newpass))
if account.character != caller:
account.msg("%s has changed your password to '%s'." % (caller.name,
newpass))
logger.log_sec('Password Changed: %s (Caller: %s, IP: %s).' % (account, caller, self.session.address))
account.msg("%s has changed your password to '%s'." % (caller.name, newpass))
logger.log_sec(
"Password Changed: %s (Caller: %s, IP: %s)." % (account, caller, self.session.address)
)
class CmdPerm(COMMAND_DEFAULT_CLASS):
@ -402,6 +420,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
This command sets/clears individual permission strings on an object
or account. If no permission is given, list all permissions on <object>.
"""
key = "perm"
aliases = "setperm"
switch_options = ("del", "account")
@ -420,7 +439,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
caller.msg(string)
return
accountmode = 'account' in self.switches or lhs.startswith('*')
accountmode = "account" in self.switches or lhs.startswith("*")
lhs = lhs.lstrip("*")
if accountmode:
@ -431,7 +450,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
return
if not rhs:
if not obj.access(caller, 'examine'):
if not obj.access(caller, "examine"):
caller.msg("You are not allowed to examine this object.")
return
@ -440,9 +459,11 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
string += "<None>"
else:
string += ", ".join(obj.permissions.all())
if (hasattr(obj, 'account') and
hasattr(obj.account, 'is_superuser') and
obj.account.is_superuser):
if (
hasattr(obj, "account")
and hasattr(obj.account, "is_superuser")
and obj.account.is_superuser
):
string += "\n(... but this object is currently controlled by a SUPERUSER! "
string += "All access checks are passed automatically.)"
caller.msg(string)
@ -451,22 +472,33 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
# we supplied an argument on the form obj = perm
locktype = "edit" if accountmode else "control"
if not obj.access(caller, locktype):
caller.msg("You are not allowed to edit this %s's permissions."
% ("account" if accountmode else "object"))
caller.msg(
"You are not allowed to edit this %s's permissions."
% ("account" if accountmode else "object")
)
return
caller_result = []
target_result = []
if 'del' in switches:
if "del" in switches:
# delete the given permission(s) from object.
for perm in self.rhslist:
obj.permissions.remove(perm)
if obj.permissions.get(perm):
caller_result.append("\nPermissions %s could not be removed from %s." % (perm, obj.name))
caller_result.append(
"\nPermissions %s could not be removed from %s." % (perm, obj.name)
)
else:
caller_result.append("\nPermission %s removed from %s (if they existed)." % (perm, obj.name))
target_result.append("\n%s revokes the permission(s) %s from you." % (caller.name, perm))
logger.log_sec('Permissions Deleted: %s, %s (Caller: %s, IP: %s).' % (perm, obj, caller, self.session.address))
caller_result.append(
"\nPermission %s removed from %s (if they existed)." % (perm, obj.name)
)
target_result.append(
"\n%s revokes the permission(s) %s from you." % (caller.name, perm)
)
logger.log_sec(
"Permissions Deleted: %s, %s (Caller: %s, IP: %s)."
% (perm, obj, caller, self.session.address)
)
else:
# add a new permission
permissions = obj.permissions.all()
@ -475,20 +507,32 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
# don't allow to set a permission higher in the hierarchy than
# the one the caller has (to prevent self-escalation)
if (perm.lower() in PERMISSION_HIERARCHY and not
obj.locks.check_lockstring(caller, "dummy:perm(%s)" % perm)):
caller.msg("You cannot assign a permission higher than the one you have yourself.")
if perm.lower() in PERMISSION_HIERARCHY and not obj.locks.check_lockstring(
caller, "dummy:perm(%s)" % perm
):
caller.msg(
"You cannot assign a permission higher than the one you have yourself."
)
return
if perm in permissions:
caller_result.append("\nPermission '%s' is already defined on %s." % (perm, obj.name))
caller_result.append(
"\nPermission '%s' is already defined on %s." % (perm, obj.name)
)
else:
obj.permissions.add(perm)
plystring = "the Account" if accountmode else "the Object/Character"
caller_result.append("\nPermission '%s' given to %s (%s)." % (perm, obj.name, plystring))
target_result.append("\n%s gives you (%s, %s) the permission '%s'."
% (caller.name, obj.name, plystring, perm))
logger.log_sec('Permissions Added: %s, %s (Caller: %s, IP: %s).' % (obj, perm, caller, self.session.address))
caller_result.append(
"\nPermission '%s' given to %s (%s)." % (perm, obj.name, plystring)
)
target_result.append(
"\n%s gives you (%s, %s) the permission '%s'."
% (caller.name, obj.name, plystring, perm)
)
logger.log_sec(
"Permissions Added: %s, %s (Caller: %s, IP: %s)."
% (obj, perm, caller, self.session.address)
)
caller.msg("".join(caller_result).strip())
if target_result:
@ -505,6 +549,7 @@ class CmdWall(COMMAND_DEFAULT_CLASS):
Announces a message to all connected sessions
including all currently unlogged in.
"""
key = "wall"
locks = "cmd:perm(wall) or perm(Admin)"
help_category = "Admin"
@ -514,7 +559,7 @@ class CmdWall(COMMAND_DEFAULT_CLASS):
if not self.args:
self.caller.msg("Usage: wall <message>")
return
message = "%s shouts \"%s\"" % (self.caller.name, self.args)
message = '%s shouts "%s"' % (self.caller.name, self.args)
self.msg("Announcing to all connected sessions ...")
SESSIONS.announce_all(message)
@ -529,6 +574,7 @@ class CmdForce(COMMAND_DEFAULT_CLASS):
Example:
force bob=get stick
"""
key = "force"
locks = "cmd:perm(spawn) or perm(Builder)"
help_category = "Building"

View file

@ -84,6 +84,7 @@ print "leaving run ..."
# Helper functions
# -------------------------------------------------------------
def format_header(caller, entry):
"""
Formats a header
@ -100,7 +101,7 @@ def format_header(caller, entry):
header = "|w%02i/%02i|G: %s|n" % (ptr, stacklen, header)
# add extra space to the side for padding.
header = "%s%s" % (header, " " * (width - len(header)))
header = header.replace('\n', '\\n')
header = header.replace("\n", "\\n")
return header
@ -110,7 +111,7 @@ def format_code(entry):
Formats the viewing of code and errors
"""
code = ""
for line in entry.split('\n'):
for line in entry.split("\n"):
code += "\n|G>>>|n %s" % line
return code.strip()
@ -141,8 +142,7 @@ def batch_code_exec(caller):
code = stack[ptr]
caller.msg(format_header(caller, code))
err = BATCHCODE.code_exec(code,
extra_environ={"caller": caller}, debug=debug)
err = BATCHCODE.code_exec(code, extra_environ={"caller": caller}, debug=debug)
if err:
caller.msg(format_code(err))
return False
@ -185,7 +185,7 @@ def show_curr(caller, showall=False):
codeall = entry.strip()
string += "|G(hh for help)"
if showall:
for line in codeall.split('\n'):
for line in codeall.split("\n"):
string += "\n|G||n %s" % line
caller.msg(string)
@ -214,6 +214,7 @@ def purge_processor(caller):
caller.scripts.validate() # this will purge interactive mode
# -------------------------------------------------------------
# main access commands
# -------------------------------------------------------------
@ -234,6 +235,7 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
Runs batches of commands from a batch-cmd text file (*.ev).
"""
key = "batchcommands"
aliases = ["batchcommand", "batchcmd"]
switch_options = ("interactive",)
@ -263,8 +265,10 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
err = "{}\n".format(str(err))
else:
err = ""
string = "%s'%s' could not load. You have to supply python paths " \
"from one of the defined batch-file directories\n (%s)."
string = (
"%s'%s' could not load. You have to supply python paths "
"from one of the defined batch-file directories\n (%s)."
)
caller.msg(string % (err, python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
return
if not commands:
@ -282,7 +286,7 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
caller.ndb.batch_cmdset_backup = list(caller.cmdset.cmdset_stack)
caller.cmdset.add(BatchSafeCmdSet)
if 'inter' in switches or 'interactive' in switches:
if "inter" in switches or "interactive" in switches:
# Allow more control over how batch file is executed
# Set interactive state directly
@ -291,9 +295,10 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
caller.msg("\nBatch-command processor - Interactive mode for %s ..." % python_path)
show_curr(caller)
else:
caller.msg("Running Batch-command processor - Automatic mode "
"for %s (this might take some time) ..."
% python_path)
caller.msg(
"Running Batch-command processor - Automatic mode "
"for %s (this might take some time) ..." % python_path
)
procpool = False
if "PythonProcPool" in utils.server_services():
@ -312,11 +317,13 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
caller.msg(" |RError from processor: '%s'" % e)
purge_processor(caller)
utils.run_async(_PROCPOOL_BATCHCMD_SOURCE,
commands=commands,
caller=caller,
at_return=callback,
at_err=errback)
utils.run_async(
_PROCPOOL_BATCHCMD_SOURCE,
commands=commands,
caller=caller,
at_return=callback,
at_err=errback,
)
else:
# run in-process (might block)
for _ in range(len(commands)):
@ -350,6 +357,7 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
Runs batches of commands from a batch-code text file (*.py).
"""
key = "batchcode"
aliases = ["batchcodes"]
switch_options = ("interactive", "debug")
@ -366,7 +374,7 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
caller.msg("Usage: batchcode[/interactive/debug] <path.to.file>")
return
python_path = self.args
debug = 'debug' in self.switches
debug = "debug" in self.switches
# parse indata file
try:
@ -379,8 +387,10 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
err = "{}\n".format(str(err))
else:
err = ""
string = "%s'%s' could not load. You have to supply python paths " \
"from one of the defined batch-file directories\n (%s)."
string = (
"%s'%s' could not load. You have to supply python paths "
"from one of the defined batch-file directories\n (%s)."
)
caller.msg(string % (err, python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
return
if not codes:
@ -399,7 +409,7 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
caller.ndb.batch_cmdset_backup = list(caller.cmdset.cmdset_stack)
caller.cmdset.add(BatchSafeCmdSet)
if 'inter' in switches or 'interactive'in switches:
if "inter" in switches or "interactive" in switches:
# Allow more control over how batch file is executed
# Set interactive state directly
@ -425,11 +435,14 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
def errback(e):
caller.msg(" |RError from processor: '%s'" % e)
purge_processor(caller)
utils.run_async(_PROCPOOL_BATCHCODE_SOURCE,
codes=codes,
caller=caller,
at_return=callback,
at_err=errback)
utils.run_async(
_PROCPOOL_BATCHCODE_SOURCE,
codes=codes,
caller=caller,
at_return=callback,
at_err=errback,
)
else:
# un in-process (will block)
for _ in range(len(codes)):
@ -449,6 +462,7 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
# (these are the same for both processors)
# -------------------------------------------------------------
class CmdStateAbort(_COMMAND_DEFAULT_CLASS):
"""
abort
@ -457,6 +471,7 @@ class CmdStateAbort(_COMMAND_DEFAULT_CLASS):
the default cmdset, regardless of what current cmdset the processor might
have put us in (e.g. when testing buggy scripts etc).
"""
key = "abort"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -474,6 +489,7 @@ class CmdStateLL(_COMMAND_DEFAULT_CLASS):
Look at the full source for the current
command definition.
"""
key = "ll"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -488,6 +504,7 @@ class CmdStatePP(_COMMAND_DEFAULT_CLASS):
Process the currently shown command definition.
"""
key = "pp"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -510,6 +527,7 @@ class CmdStateRR(_COMMAND_DEFAULT_CLASS):
Reload the batch file, keeping the current
position in it.
"""
key = "rr"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -532,6 +550,7 @@ class CmdStateRRR(_COMMAND_DEFAULT_CLASS):
Reload the batch file, starting over
from the beginning.
"""
key = "rrr"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -553,6 +572,7 @@ class CmdStateNN(_COMMAND_DEFAULT_CLASS):
Go to next command. No commands are executed.
"""
key = "nn"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -575,6 +595,7 @@ class CmdStateNL(_COMMAND_DEFAULT_CLASS):
Go to next command, viewing its full source.
No commands are executed.
"""
key = "nl"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -597,6 +618,7 @@ class CmdStateBB(_COMMAND_DEFAULT_CLASS):
Backwards to previous command. No commands
are executed.
"""
key = "bb"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -619,6 +641,7 @@ class CmdStateBL(_COMMAND_DEFAULT_CLASS):
Backwards to previous command, viewing its full
source. No commands are executed.
"""
key = "bl"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -642,6 +665,7 @@ class CmdStateSS(_COMMAND_DEFAULT_CLASS):
one. If steps is given,
process this many commands.
"""
key = "ss"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -671,6 +695,7 @@ class CmdStateSL(_COMMAND_DEFAULT_CLASS):
one, viewing its full source. If steps is given,
process this many commands.
"""
key = "sl"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -699,6 +724,7 @@ class CmdStateCC(_COMMAND_DEFAULT_CLASS):
Continue to process all remaining
commands.
"""
key = "cc"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -727,6 +753,7 @@ class CmdStateJJ(_COMMAND_DEFAULT_CLASS):
Jump to specific command number
"""
key = "jj"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -751,6 +778,7 @@ class CmdStateJL(_COMMAND_DEFAULT_CLASS):
Jump to specific command number and view its full source.
"""
key = "jl"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -775,6 +803,7 @@ class CmdStateQQ(_COMMAND_DEFAULT_CLASS):
Quit the batchprocessor.
"""
key = "qq"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -827,12 +856,14 @@ class CmdStateHH(_COMMAND_DEFAULT_CLASS):
#
# -------------------------------------------------------------
class BatchSafeCmdSet(CmdSet):
"""
The base cmdset for the batch processor.
This sets a 'safe' abort command that will
always be available to get out of everything.
"""
key = "Batch_default"
priority = 150 # override other cmdsets.
@ -845,6 +876,7 @@ class BatchInteractiveCmdSet(CmdSet):
"""
The cmdset for the interactive batch processor mode.
"""
key = "Batch_interactive"
priority = 104

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@ class CharacterCmdSet(CmdSet):
"""
Implements the default command set.
"""
key = "DefaultCharacter"
priority = 0

View file

@ -9,6 +9,7 @@ class SessionCmdSet(CmdSet):
"""
Sets up the unlogged cmdset.
"""
key = "DefaultSession"
priority = -20

View file

@ -11,6 +11,7 @@ class UnloggedinCmdSet(CmdSet):
"""
Sets up the unlogged cmdset.
"""
key = "DefaultUnloggedin"
priority = 0

View file

@ -23,10 +23,22 @@ CHANNEL_DEFAULT_TYPECLASS = class_from_module(settings.BASE_CHANNEL_TYPECLASS)
# limit symbol import for API
__all__ = ("CmdAddCom", "CmdDelCom", "CmdAllCom",
"CmdChannels", "CmdCdestroy", "CmdCBoot", "CmdCemit",
"CmdCWho", "CmdChannelCreate", "CmdClock", "CmdCdesc",
"CmdPage", "CmdIRC2Chan", "CmdRSS2Chan")
__all__ = (
"CmdAddCom",
"CmdDelCom",
"CmdAllCom",
"CmdChannels",
"CmdCdestroy",
"CmdCBoot",
"CmdCemit",
"CmdCWho",
"CmdChannelCreate",
"CmdClock",
"CmdCdesc",
"CmdPage",
"CmdIRC2Chan",
"CmdRSS2Chan",
)
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
@ -38,8 +50,11 @@ def find_channel(caller, channelname, silent=False, noaliases=False):
channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname)
if not channels:
if not noaliases:
channels = [chan for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
if channelname in chan.aliases.all()]
channels = [
chan
for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
if channelname in chan.aliases.all()
]
if channels:
return channels[0]
if not silent:
@ -99,7 +114,7 @@ class CmdAddCom(COMMAND_DEFAULT_CLASS):
return
# check permissions
if not channel.access(account, 'listen'):
if not channel.access(account, "listen"):
self.msg("%s: You are not allowed to listen to this channel." % channel.key)
return
@ -171,8 +186,11 @@ class CmdDelCom(COMMAND_DEFAULT_CLASS):
delnicks = "all" in self.switches
# find all nicks linked to this channel and delete them
if delnicks:
for nick in [nick for nick in make_iter(caller.nicks.get(category="channel", return_obj=True))
if nick and nick.pk and nick.value[3].lower() == chkey]:
for nick in [
nick
for nick in make_iter(caller.nicks.get(category="channel", return_obj=True))
if nick and nick.pk and nick.value[3].lower() == chkey
]:
nick.delete()
disconnect = channel.disconnect(account)
if disconnect:
@ -227,8 +245,11 @@ class CmdAllCom(COMMAND_DEFAULT_CLASS):
if args == "on":
# get names of all channels available to listen to
# and activate them all
channels = [chan for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
if chan.access(caller, 'listen')]
channels = [
chan
for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
if chan.access(caller, "listen")
]
for channel in channels:
self.execute_cmd("addcom %s" % channel.key)
elif args == "off":
@ -238,15 +259,21 @@ class CmdAllCom(COMMAND_DEFAULT_CLASS):
self.execute_cmd("delcom %s" % channel.key)
elif args == "destroy":
# destroy all channels you control
channels = [chan for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
if chan.access(caller, 'control')]
channels = [
chan
for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
if chan.access(caller, "control")
]
for channel in channels:
self.execute_cmd("cdestroy %s" % channel.key)
elif args == "who":
# run a who, listing the subscribers on visible channels.
string = "\n|CChannel subscriptions|n"
channels = [chan for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
if chan.access(caller, 'listen')]
channels = [
chan
for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
if chan.access(caller, "listen")
]
if not channels:
string += "No channels."
for channel in channels:
@ -270,6 +297,7 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
Use 'comlist' to only view your current channel subscriptions.
Use addcom/delcom to join and leave channels
"""
key = "channels"
aliases = ["clist", "comlist", "chanlist", "channellist", "all channels"]
help_category = "Comms"
@ -284,8 +312,11 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
caller = self.caller
# all channels we have available to listen to
channels = [chan for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
if chan.access(caller, 'listen')]
channels = [
chan
for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
if chan.access(caller, "listen")
]
if not channels:
self.msg("No channels available.")
return
@ -294,22 +325,46 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
if self.cmdstring == "comlist":
# just display the subscribed channels with no extra info
comtable = self.styled_table("|wchannel|n", "|wmy aliases|n",
"|wdescription|n", align="l", maxwidth=_DEFAULT_WIDTH)
comtable = self.styled_table(
"|wchannel|n",
"|wmy aliases|n",
"|wdescription|n",
align="l",
maxwidth=_DEFAULT_WIDTH,
)
for chan in subs:
clower = chan.key.lower()
nicks = caller.nicks.get(category="channel", return_obj=True)
comtable.add_row(*["%s%s" % (chan.key, chan.aliases.all() and
"(%s)" % ",".join(chan.aliases.all()) or ""),
"%s" % ",".join(nick.db_key for nick in make_iter(nicks)
if nick and nick.value[3].lower() == clower),
chan.db.desc])
self.msg("\n|wChannel subscriptions|n (use |wchannels|n to list all,"
" |waddcom|n/|wdelcom|n to sub/unsub):|n\n%s" % comtable)
comtable.add_row(
*[
"%s%s"
% (
chan.key,
chan.aliases.all() and "(%s)" % ",".join(chan.aliases.all()) or "",
),
"%s"
% ",".join(
nick.db_key
for nick in make_iter(nicks)
if nick and nick.value[3].lower() == clower
),
chan.db.desc,
]
)
self.msg(
"\n|wChannel subscriptions|n (use |wchannels|n to list all,"
" |waddcom|n/|wdelcom|n to sub/unsub):|n\n%s" % comtable
)
else:
# full listing (of channels caller is able to listen to)
comtable = self.styled_table("|wsub|n", "|wchannel|n", "|wmy aliases|n",
"|wlocks|n", "|wdescription|n", maxwidth=_DEFAULT_WIDTH)
comtable = self.styled_table(
"|wsub|n",
"|wchannel|n",
"|wmy aliases|n",
"|wlocks|n",
"|wdescription|n",
maxwidth=_DEFAULT_WIDTH,
)
for chan in channels:
clower = chan.key.lower()
nicks = caller.nicks.get(category="channel", return_obj=True)
@ -320,17 +375,30 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
substatus = "|rMuted|n"
else:
substatus = "|gYes|n"
comtable.add_row(*[substatus,
"%s%s" % (chan.key, chan.aliases.all() and
"(%s)" % ",".join(chan.aliases.all()) or ""),
"%s" % ",".join(nick.db_key for nick in make_iter(nicks)
if nick.value[3].lower() == clower),
str(chan.locks),
chan.db.desc])
comtable.add_row(
*[
substatus,
"%s%s"
% (
chan.key,
chan.aliases.all() and "(%s)" % ",".join(chan.aliases.all()) or "",
),
"%s"
% ",".join(
nick.db_key
for nick in make_iter(nicks)
if nick.value[3].lower() == clower
),
str(chan.locks),
chan.db.desc,
]
)
comtable.reformat_column(0, width=9)
comtable.reformat_column(3, width=14)
self.msg("\n|wAvailable channels|n (use |wcomlist|n,|waddcom|n and |wdelcom|n"
" to manage subscriptions):\n%s" % comtable)
self.msg(
"\n|wAvailable channels|n (use |wcomlist|n,|waddcom|n and |wdelcom|n"
" to manage subscriptions):\n%s" % comtable
)
class CmdCdestroy(COMMAND_DEFAULT_CLASS):
@ -361,7 +429,7 @@ class CmdCdestroy(COMMAND_DEFAULT_CLASS):
if not channel:
self.msg("Could not find channel %s." % self.args)
return
if not channel.access(caller, 'control'):
if not channel.access(caller, "control"):
self.msg("You are not allowed to do that.")
return
channel_key = channel.key
@ -371,7 +439,10 @@ class CmdCdestroy(COMMAND_DEFAULT_CLASS):
channel.delete()
CHANNELHANDLER.update()
self.msg("Channel '%s' was destroyed." % channel_key)
logger.log_sec('Channel Deleted: %s (Caller: %s, IP: %s).' % (channel_key, caller, self.session.address))
logger.log_sec(
"Channel Deleted: %s (Caller: %s, IP: %s)."
% (channel_key, caller, self.session.address)
)
class CmdCBoot(COMMAND_DEFAULT_CLASS):
@ -410,9 +481,9 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
reason = ""
if ":" in self.rhs:
accountname, reason = self.rhs.rsplit(":", 1)
searchstring = accountname.lstrip('*')
searchstring = accountname.lstrip("*")
else:
searchstring = self.rhs.lstrip('*')
searchstring = self.rhs.lstrip("*")
account = self.caller.search(searchstring, account=True)
if not account:
return
@ -430,15 +501,19 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
string = "%s boots %s from channel.%s" % (self.caller, account.key, reason)
channel.msg(string)
# find all account's nicks linked to this channel and delete them
for nick in [nick for nick in
account.character.nicks.get(category="channel") or []
if nick.value[3].lower() == channel.key]:
for nick in [
nick
for nick in account.character.nicks.get(category="channel") or []
if nick.value[3].lower() == channel.key
]:
nick.delete()
# disconnect account
channel.disconnect(account)
CHANNELHANDLER.update()
logger.log_sec('Channel Boot: %s (Channel: %s, Reason: %s, Caller: %s, IP: %s).' % (
account, channel, reason, self.caller, self.session.address))
logger.log_sec(
"Channel Boot: %s (Channel: %s, Reason: %s, Caller: %s, IP: %s)."
% (account, channel, reason, self.caller, self.session.address)
)
class CmdCemit(COMMAND_DEFAULT_CLASS):
@ -499,6 +574,7 @@ class CmdCWho(COMMAND_DEFAULT_CLASS):
List who is connected to a given channel you have access to.
"""
key = "cwho"
locks = "cmd: not pperm(channel_banned)"
help_category = "Comms"
@ -560,19 +636,16 @@ class CmdChannelCreate(COMMAND_DEFAULT_CLASS):
lhs = self.lhs
channame = lhs
aliases = None
if ';' in lhs:
channame, aliases = lhs.split(';', 1)
aliases = [alias.strip().lower() for alias in aliases.split(';')]
if ";" in lhs:
channame, aliases = lhs.split(";", 1)
aliases = [alias.strip().lower() for alias in aliases.split(";")]
channel = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channame)
if channel:
self.msg("A channel with that name already exists.")
return
# Create and set the channel up
lockstring = "send:all();listen:all();control:id(%s)" % caller.id
new_chan = create.create_channel(channame.strip(),
aliases,
description,
locks=lockstring)
new_chan = create.create_channel(channame.strip(), aliases, description, locks=lockstring)
new_chan.connect(caller)
CHANNELHANDLER.update()
self.msg("Created channel %s and connected to it." % new_chan.key)
@ -662,14 +735,13 @@ class CmdCdesc(COMMAND_DEFAULT_CLASS):
self.msg("Channel '%s' not found." % self.lhs)
return
# check permissions
if not channel.access(caller, 'control'):
if not channel.access(caller, "control"):
self.msg("You cannot admin this channel.")
return
# set the description
channel.db.desc = self.rhs
channel.save()
self.msg("Description of channel '%s' set to '%s'." % (channel.key,
self.rhs))
self.msg("Description of channel '%s' set to '%s'." % (channel.key, self.rhs))
class CmdPage(COMMAND_DEFAULT_CLASS):
@ -690,7 +762,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
"""
key = "page"
aliases = ['tell']
aliases = ["tell"]
switch_options = ("last", "list")
locks = "cmd:not pperm(page_banned)"
help_category = "Comms"
@ -709,7 +781,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
# get last messages we've got
pages_we_got = Msg.objects.get_messages_by_receiver(caller)
if 'last' in self.switches:
if "last" in self.switches:
if pages_we_sent:
recv = ",".join(obj.key for obj in pages_we_sent[-1].receivers)
self.msg("You last paged |c%s|n:%s" % (recv, pages_we_sent[-1].message))
@ -735,11 +807,16 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
else:
lastpages = pages
template = "|w%s|n |c%s|n to |c%s|n: %s"
lastpages = "\n ".join(template %
(utils.datetime_format(page.date_created),
",".join(obj.key for obj in page.senders),
"|n,|c ".join([obj.name for obj in page.receivers]),
page.message) for page in lastpages)
lastpages = "\n ".join(
template
% (
utils.datetime_format(page.date_created),
",".join(obj.key for obj in page.senders),
"|n,|c ".join([obj.name for obj in page.receivers]),
page.message,
)
for page in lastpages
)
if lastpages:
string = "Your latest pages:\n %s" % lastpages
@ -765,7 +842,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
for receiver in set(receivers):
if isinstance(receiver, str):
pobj = caller.search(receiver)
elif hasattr(receiver, 'character'):
elif hasattr(receiver, "character"):
pobj = receiver
else:
self.msg("Who do you want to page?")
@ -781,24 +858,25 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
# if message begins with a :, we assume it is a 'page-pose'
if message.startswith(":"):
message = "%s %s" % (caller.key, message.strip(':').strip())
message = "%s %s" % (caller.key, message.strip(":").strip())
# create the persistent message object
create.create_message(caller, message,
receivers=recobjs)
create.create_message(caller, message, receivers=recobjs)
# tell the accounts they got a message.
received = []
rstrings = []
for pobj in recobjs:
if not pobj.access(caller, 'msg'):
if not pobj.access(caller, "msg"):
rstrings.append("You are not allowed to page %s." % pobj)
continue
pobj.msg("%s %s" % (header, message))
if hasattr(pobj, 'sessions') and not pobj.sessions.count():
if hasattr(pobj, "sessions") and not pobj.sessions.count():
received.append("|C%s|n" % pobj.name)
rstrings.append("%s is offline. They will see your message if they list their pages later."
% received[-1])
rstrings.append(
"%s is offline. They will see your message if they list their pages later."
% received[-1]
)
else:
received.append("|c%s|n" % pobj.name)
if rstrings:
@ -816,13 +894,31 @@ def _list_bots(cmd):
bots (str): A table of bots or an error message.
"""
ircbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")]
ircbots = [
bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")
]
if ircbots:
table = cmd.styled_table("|w#dbref|n", "|wbotname|n", "|wev-channel|n",
"|wirc-channel|n", "|wSSL|n", maxwidth=_DEFAULT_WIDTH)
table = cmd.styled_table(
"|w#dbref|n",
"|wbotname|n",
"|wev-channel|n",
"|wirc-channel|n",
"|wSSL|n",
maxwidth=_DEFAULT_WIDTH,
)
for ircbot in ircbots:
ircinfo = "%s (%s:%s)" % (ircbot.db.irc_channel, ircbot.db.irc_network, ircbot.db.irc_port)
table.add_row("#%i" % ircbot.id, ircbot.db.irc_botname, ircbot.db.ev_channel, ircinfo, ircbot.db.irc_ssl)
ircinfo = "%s (%s:%s)" % (
ircbot.db.irc_channel,
ircbot.db.irc_network,
ircbot.db.irc_port,
)
table.add_row(
"#%i" % ircbot.id,
ircbot.db.irc_botname,
ircbot.db.ev_channel,
ircinfo,
ircbot.db.irc_ssl,
)
return table
else:
return "No irc bots found."
@ -872,12 +968,12 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
self.msg(string)
return
if 'list' in self.switches:
if "list" in self.switches:
# show all connections
self.msg(_list_bots(self))
return
if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches:
if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches:
botname = "ircbot-%s" % self.lhs
matches = AccountDB.objects.filter(db_is_bot=True, username=botname)
dbref = utils.dbref(self.lhs)
@ -892,16 +988,19 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
return
if not self.args or not self.rhs:
string = "Usage: irc2chan[/switches] <evennia_channel> =" \
" <ircnetwork> <port> <#irchannel> <botname>[:typeclass]"
string = (
"Usage: irc2chan[/switches] <evennia_channel> ="
" <ircnetwork> <port> <#irchannel> <botname>[:typeclass]"
)
self.msg(string)
return
channel = self.lhs
self.rhs = self.rhs.replace('#', ' ') # to avoid Python comment issues
self.rhs = self.rhs.replace("#", " ") # to avoid Python comment issues
try:
irc_network, irc_port, irc_channel, irc_botname = \
[part.strip() for part in self.rhs.split(None, 4)]
irc_network, irc_port, irc_channel, irc_botname = [
part.strip() for part in self.rhs.split(None, 4)
]
irc_channel = "#%s" % irc_channel
except Exception:
string = "IRC bot definition '%s' is not valid." % self.rhs
@ -930,8 +1029,14 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
except Exception as err:
self.msg("|rError, could not create the bot:|n '%s'." % err)
return
bot.start(ev_channel=channel, irc_botname=irc_botname, irc_channel=irc_channel,
irc_network=irc_network, irc_port=irc_port, irc_ssl=irc_ssl)
bot.start(
ev_channel=channel,
irc_botname=irc_botname,
irc_channel=irc_channel,
irc_network=irc_network,
irc_port=irc_port,
irc_ssl=irc_ssl,
)
self.msg("Connection created. Starting IRC bot.")
@ -953,6 +1058,7 @@ class CmdIRCStatus(COMMAND_DEFAULT_CLASS):
messages sent to either channel will be lost.
"""
key = "ircstatus"
locks = "cmd:serversetting(IRC_ENABLED) and perm(ircstatus) or perm(Builder))"
help_category = "Comms"
@ -976,13 +1082,20 @@ class CmdIRCStatus(COMMAND_DEFAULT_CLASS):
if utils.dbref(botname):
matches = AccountDB.objects.filter(db_is_bot=True, id=utils.dbref(botname))
if not matches:
self.msg("No matching IRC-bot found. Use ircstatus without arguments to list active bots.")
self.msg(
"No matching IRC-bot found. Use ircstatus without arguments to list active bots."
)
return
ircbot = matches[0]
channel = ircbot.db.irc_channel
network = ircbot.db.irc_network
port = ircbot.db.irc_port
chtext = "IRC bot '%s' on channel %s (%s:%s)" % (ircbot.db.irc_botname, channel, network, port)
chtext = "IRC bot '%s' on channel %s (%s:%s)" % (
ircbot.db.irc_botname,
channel,
network,
port,
)
if option == "ping":
# check connection by sending outself a ping through the server.
self.caller.msg("Pinging through %s." % chtext)
@ -992,7 +1105,9 @@ class CmdIRCStatus(COMMAND_DEFAULT_CLASS):
# an asynchronous call.
self.caller.msg("Requesting nicklist from %s (%s:%s)." % (channel, network, port))
ircbot.get_nicklist(self.caller)
elif self.caller.locks.check_lockstring(self.caller, "dummy:perm(ircstatus) or perm(Developer)"):
elif self.caller.locks.check_lockstring(
self.caller, "dummy:perm(ircstatus) or perm(Developer)"
):
# reboot the client
self.caller.msg("Forcing a disconnect + reconnect of %s." % chtext)
ircbot.reconnect()
@ -1041,27 +1156,41 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
return
try:
import feedparser
assert feedparser # to avoid checker error of not being used
except ImportError:
string = "RSS requires python-feedparser (https://pypi.python.org/pypi/feedparser)." \
" Install before continuing."
string = (
"RSS requires python-feedparser (https://pypi.python.org/pypi/feedparser)."
" Install before continuing."
)
self.msg(string)
return
if 'list' in self.switches:
if "list" in self.switches:
# show all connections
rssbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")]
rssbots = [
bot
for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")
]
if rssbots:
table = self.styled_table("|wdbid|n", "|wupdate rate|n", "|wev-channel",
"|wRSS feed URL|n", border="cells", maxwidth=_DEFAULT_WIDTH)
table = self.styled_table(
"|wdbid|n",
"|wupdate rate|n",
"|wev-channel",
"|wRSS feed URL|n",
border="cells",
maxwidth=_DEFAULT_WIDTH,
)
for rssbot in rssbots:
table.add_row(rssbot.id, rssbot.db.rss_rate, rssbot.db.ev_channel, rssbot.db.rss_url)
table.add_row(
rssbot.id, rssbot.db.rss_rate, rssbot.db.ev_channel, rssbot.db.rss_url
)
self.msg(table)
else:
self.msg("No rss bots found.")
return
if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches:
if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches:
botname = "rssbot-%s" % self.lhs
matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname)
if not matches:
@ -1133,12 +1262,20 @@ class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
if "list" in self.switches:
# show all connections
gwbots = [bot for bot in
AccountDB.objects.filter(db_is_bot=True,
username__startswith="grapevinebot-")]
gwbots = [
bot
for bot in AccountDB.objects.filter(
db_is_bot=True, username__startswith="grapevinebot-"
)
]
if gwbots:
table = self.styled_table("|wdbid|n", "|wev-channel",
"|wgw-channel|n", border="cells", maxwidth=_DEFAULT_WIDTH)
table = self.styled_table(
"|wdbid|n",
"|wev-channel",
"|wgw-channel|n",
border="cells",
maxwidth=_DEFAULT_WIDTH,
)
for gwbot in gwbots:
table.add_row(gwbot.id, gwbot.db.ev_channel, gwbot.db.grapevine_channel)
self.msg(table)
@ -1146,7 +1283,7 @@ class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
self.msg("No grapevine bots found.")
return
if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches:
if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches:
botname = "grapevinebot-%s" % self.lhs
matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname)

View file

@ -9,9 +9,20 @@ from evennia.typeclasses.attributes import NickTemplateInvalid
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
# limit symbol import for API
__all__ = ("CmdHome", "CmdLook", "CmdNick",
"CmdInventory", "CmdSetDesc", "CmdGet", "CmdDrop", "CmdGive",
"CmdSay", "CmdWhisper", "CmdPose", "CmdAccess")
__all__ = (
"CmdHome",
"CmdLook",
"CmdNick",
"CmdInventory",
"CmdSetDesc",
"CmdGet",
"CmdDrop",
"CmdGive",
"CmdSay",
"CmdWhisper",
"CmdPose",
"CmdAccess",
)
class CmdHome(COMMAND_DEFAULT_CLASS):
@ -52,6 +63,7 @@ class CmdLook(COMMAND_DEFAULT_CLASS):
Observes your location or objects in your vicinity.
"""
key = "look"
aliases = ["l", "ls"]
locks = "cmd:all()"
@ -71,7 +83,7 @@ class CmdLook(COMMAND_DEFAULT_CLASS):
target = caller.search(self.args)
if not target:
return
self.msg((caller.at_look(target), {'type': 'look'}), options=None)
self.msg((caller.at_look(target), {"type": "look"}), options=None)
class CmdNick(COMMAND_DEFAULT_CLASS):
@ -116,6 +128,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
for everyone to use, you need build privileges and the alias command.
"""
key = "nick"
switch_options = ("inputline", "object", "account", "list", "delete", "clearall")
aliases = ["nickname", "nicks"]
@ -148,11 +161,13 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
specified_nicktype = bool(nicktypes)
nicktypes = nicktypes if specified_nicktype else ["inputline"]
nicklist = (utils.make_iter(caller.nicks.get(category="inputline", return_obj=True) or []) +
utils.make_iter(caller.nicks.get(category="object", return_obj=True) or []) +
utils.make_iter(caller.nicks.get(category="account", return_obj=True) or []))
nicklist = (
utils.make_iter(caller.nicks.get(category="inputline", return_obj=True) or [])
+ utils.make_iter(caller.nicks.get(category="object", return_obj=True) or [])
+ utils.make_iter(caller.nicks.get(category="account", return_obj=True) or [])
)
if 'list' in switches or self.cmdstring in ("nicks",):
if "list" in switches or self.cmdstring in ("nicks",):
if not nicklist:
string = "|wNo nicks defined.|n"
@ -160,18 +175,20 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
table = self.styled_table("#", "Type", "Nick match", "Replacement")
for inum, nickobj in enumerate(nicklist):
_, _, nickvalue, replacement = nickobj.value
table.add_row(str(inum + 1), nickobj.db_category, _cy(nickvalue), _cy(replacement))
table.add_row(
str(inum + 1), nickobj.db_category, _cy(nickvalue), _cy(replacement)
)
string = "|wDefined Nicks:|n\n%s" % table
caller.msg(string)
return
if 'clearall' in switches:
if "clearall" in switches:
caller.nicks.clear()
caller.account.nicks.clear()
caller.msg("Cleared all nicks.")
return
if 'delete' in switches or 'del' in switches:
if "delete" in switches or "del" in switches:
if not self.args or not self.lhs:
caller.msg("usage nick/delete <nick> or <#num> ('nicks' for list)")
return
@ -199,8 +216,10 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
nicktypestr = "%s-nick" % nicktype.capitalize()
_, _, old_nickstring, old_replstring = oldnick.value
caller.nicks.remove(old_nickstring, category=nicktype)
caller.msg("%s removed: '|w%s|n' -> |w%s|n." % (
nicktypestr, old_nickstring, old_replstring))
caller.msg(
"%s removed: '|w%s|n' -> |w%s|n."
% (nicktypestr, old_nickstring, old_replstring)
)
else:
caller.msg("No matching nicks to remove.")
return
@ -211,14 +230,19 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
if not specified_nicktype:
nicktypes = ("object", "account", "inputline")
for nicktype in nicktypes:
nicks = [nick for nick in
utils.make_iter(caller.nicks.get(category=nicktype, return_obj=True))
if nick]
nicks = [
nick
for nick in utils.make_iter(
caller.nicks.get(category=nicktype, return_obj=True)
)
if nick
]
for nick in nicks:
_, _, nick, repl = nick.value
if nick.startswith(self.lhs):
strings.append("{}-nick: '{}' -> '{}'".format(
nicktype.capitalize(), nick, repl))
strings.append(
"{}-nick: '{}' -> '{}'".format(nicktype.capitalize(), nick, repl)
)
if strings:
caller.msg("\n".join(strings))
else:
@ -239,8 +263,9 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
for nick in nicks:
_, _, nick, repl = nick.value
if nick.startswith(self.lhs):
strings.append("{}-nick: '{}' -> '{}'".format(
nicktype.capitalize(), nick, repl))
strings.append(
"{}-nick: '{}' -> '{}'".format(nicktype.capitalize(), nick, repl)
)
if strings:
caller.msg("\n".join(strings))
else:
@ -261,8 +286,9 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
for nick in nicks:
_, _, nick, repl = nick.value
if nick.startswith(self.lhs):
strings.append("{}-nick: '{}' -> '{}'".format(
nicktype.capitalize(), nick, repl))
strings.append(
"{}-nick: '{}' -> '{}'".format(nicktype.capitalize(), nick, repl)
)
if strings:
caller.msg("\n".join(strings))
else:
@ -301,17 +327,30 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
string += "\nIdentical %s already set." % nicktypestr.lower()
else:
string += "\n%s '|w%s|n' updated to map to '|w%s|n'." % (
nicktypestr, old_nickstring, replstring)
nicktypestr,
old_nickstring,
replstring,
)
else:
string += "\n%s '|w%s|n' mapped to '|w%s|n'." % (nicktypestr, nickstring, replstring)
string += "\n%s '|w%s|n' mapped to '|w%s|n'." % (
nicktypestr,
nickstring,
replstring,
)
try:
caller.nicks.add(nickstring, replstring, category=nicktype)
except NickTemplateInvalid:
caller.msg("You must use the same $-markers both in the nick and in the replacement.")
caller.msg(
"You must use the same $-markers both in the nick and in the replacement."
)
return
elif old_nickstring and old_replstring:
# just looking at the nick
string += "\n%s '|w%s|n' maps to '|w%s|n'." % (nicktypestr, old_nickstring, old_replstring)
string += "\n%s '|w%s|n' maps to '|w%s|n'." % (
nicktypestr,
old_nickstring,
old_replstring,
)
errstring = ""
string = errstring if errstring else string
caller.msg(_cy(string))
@ -327,6 +366,7 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
Shows your inventory.
"""
key = "inventory"
aliases = ["inv", "i"]
locks = "cmd:all()"
@ -355,6 +395,7 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
Picks up an object from your location and puts it in
your inventory.
"""
key = "get"
aliases = "grab"
locks = "cmd:all()"
@ -374,7 +415,7 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
if caller == obj:
caller.msg("You can't get yourself.")
return
if not obj.access(caller, 'get'):
if not obj.access(caller, "get"):
if obj.db.get_err_msg:
caller.msg(obj.db.get_err_msg)
else:
@ -387,10 +428,7 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
obj.move_to(caller, quiet=True)
caller.msg("You pick up %s." % obj.name)
caller.location.msg_contents("%s picks up %s." %
(caller.name,
obj.name),
exclude=caller)
caller.location.msg_contents("%s picks up %s." % (caller.name, obj.name), exclude=caller)
# calling at_get hook method
obj.at_get(caller)
@ -420,9 +458,12 @@ class CmdDrop(COMMAND_DEFAULT_CLASS):
# Because the DROP command by definition looks for items
# in inventory, call the search function using location = caller
obj = caller.search(self.args, location=caller,
nofound_string="You aren't carrying %s." % self.args,
multimatch_string="You carry more than one %s:" % self.args)
obj = caller.search(
self.args,
location=caller,
nofound_string="You aren't carrying %s." % self.args,
multimatch_string="You carry more than one %s:" % self.args,
)
if not obj:
return
@ -432,9 +473,7 @@ class CmdDrop(COMMAND_DEFAULT_CLASS):
obj.move_to(caller.location, quiet=True)
caller.msg("You drop %s." % (obj.name,))
caller.location.msg_contents("%s drops %s." %
(caller.name, obj.name),
exclude=caller)
caller.location.msg_contents("%s drops %s." % (caller.name, obj.name), exclude=caller)
# Call the object script's at_drop() method.
obj.at_drop(caller)
@ -449,6 +488,7 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
Gives an items from your inventory to another character,
placing it in their inventory.
"""
key = "give"
rhs_split = ("=", " to ") # Prefer = delimiter, but allow " to " usage.
locks = "cmd:all()"
@ -461,9 +501,12 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
if not self.args or not self.rhs:
caller.msg("Usage: give <inventory object> = <target>")
return
to_give = caller.search(self.lhs, location=caller,
nofound_string="You aren't carrying %s." % self.lhs,
multimatch_string="You carry more than one %s:" % self.lhs)
to_give = caller.search(
self.lhs,
location=caller,
nofound_string="You aren't carrying %s." % self.lhs,
multimatch_string="You carry more than one %s:" % self.lhs,
)
target = caller.search(self.rhs)
if not (to_give and target):
return
@ -497,6 +540,7 @@ class CmdSetDesc(COMMAND_DEFAULT_CLASS):
will be visible to people when they
look at you.
"""
key = "setdesc"
locks = "cmd:all()"
arg_regex = r"\s|$"
@ -606,6 +650,7 @@ class CmdPose(COMMAND_DEFAULT_CLASS):
Describe an action being taken. The pose text will
automatically begin with your name.
"""
key = "pose"
aliases = [":", "emote"]
locks = "cmd:all()"
@ -630,8 +675,7 @@ class CmdPose(COMMAND_DEFAULT_CLASS):
self.caller.msg(msg)
else:
msg = "%s%s" % (self.caller.name, self.args)
self.caller.location.msg_contents(text=(msg, {"type": "pose"}),
from_obj=self.caller)
self.caller.location.msg_contents(text=(msg, {"type": "pose"}), from_obj=self.caller)
class CmdAccess(COMMAND_DEFAULT_CLASS):
@ -644,6 +688,7 @@ class CmdAccess(COMMAND_DEFAULT_CLASS):
This command shows you the permission hierarchy and
which permission groups you are a member of.
"""
key = "access"
aliases = ["groups", "hierarchy"]
locks = "cmd:all()"
@ -665,6 +710,6 @@ class CmdAccess(COMMAND_DEFAULT_CLASS):
string += "\n|wYour access|n:"
string += "\nCharacter |c%s|n: %s" % (caller.key, cperms)
if hasattr(caller, 'account'):
if hasattr(caller, "account"):
string += "\nAccount |c%s|n: %s" % (caller.account.key, pperms)
caller.msg(string)

View file

@ -37,8 +37,9 @@ class CmdHelp(Command):
This will search for help on commands and other
topics related to the game.
"""
key = "help"
aliases = ['?']
aliases = ["?"]
locks = "cmd:all()"
arg_regex = r"\s|$"
@ -128,7 +129,11 @@ class CmdHelp(Command):
string += "\n\n" + _SEP + "\n\r |COther help entries|n\n" + _SEP
for category in sorted(hdict_db.keys()):
string += "\n\r |w%s|n:\n" % (str(category).title())
string += "|G" + fill(", ".join(sorted([str(topic) for topic in hdict_db[category]]))) + "|n"
string += (
"|G"
+ fill(", ".join(sorted([str(topic) for topic in hdict_db[category]])))
+ "|n"
)
return string
def check_show_help(self, cmd, caller):
@ -198,9 +203,15 @@ class CmdHelp(Command):
# retrieve all available commands and database topics
all_cmds = [cmd for cmd in cmdset if self.check_show_help(cmd, caller)]
all_topics = [topic for topic in HelpEntry.objects.all() if topic.access(caller, 'view', default=True)]
all_categories = list(set([cmd.help_category.lower() for cmd in all_cmds] + [topic.help_category.lower()
for topic in all_topics]))
all_topics = [
topic for topic in HelpEntry.objects.all() if topic.access(caller, "view", default=True)
]
all_categories = list(
set(
[cmd.help_category.lower() for cmd in all_cmds]
+ [topic.help_category.lower() for topic in all_topics]
)
)
if query in ("list", "all"):
# we want to list all available help entries, grouped by category
@ -222,13 +233,23 @@ class CmdHelp(Command):
# build vocabulary of suggestions and rate them by string similarity.
suggestions = None
if suggestion_maxnum > 0:
vocabulary = [cmd.key for cmd in all_cmds if cmd] + [topic.key for topic in all_topics] + all_categories
vocabulary = (
[cmd.key for cmd in all_cmds if cmd]
+ [topic.key for topic in all_topics]
+ all_categories
)
[vocabulary.extend(cmd.aliases) for cmd in all_cmds]
suggestions = [sugg for sugg in string_suggestions(query, set(vocabulary), cutoff=suggestion_cutoff,
maxnum=suggestion_maxnum)
if sugg != query]
suggestions = [
sugg
for sugg in string_suggestions(
query, set(vocabulary), cutoff=suggestion_cutoff, maxnum=suggestion_maxnum
)
if sugg != query
]
if not suggestions:
suggestions = [sugg for sugg in vocabulary if sugg != query and sugg.startswith(query)]
suggestions = [
sugg for sugg in vocabulary if sugg != query and sugg.startswith(query)
]
# try an exact command auto-help match
match = [cmd for cmd in all_cmds if cmd == query]
@ -237,38 +258,52 @@ class CmdHelp(Command):
# try an inexact match with prefixes stripped from query and cmds
_query = query[1:] if query[0] in CMD_IGNORE_PREFIXES else query
match = [cmd for cmd in all_cmds
for m in cmd._matchset if m == _query or
m[0] in CMD_IGNORE_PREFIXES and m[1:] == _query]
match = [
cmd
for cmd in all_cmds
for m in cmd._matchset
if m == _query or m[0] in CMD_IGNORE_PREFIXES and m[1:] == _query
]
if len(match) == 1:
formatted = self.format_help_entry(match[0].key,
match[0].get_help(caller, cmdset),
aliases=match[0].aliases,
suggested=suggestions)
formatted = self.format_help_entry(
match[0].key,
match[0].get_help(caller, cmdset),
aliases=match[0].aliases,
suggested=suggestions,
)
self.msg_help(formatted)
return
# try an exact database help entry match
match = list(HelpEntry.objects.find_topicmatch(query, exact=True))
if len(match) == 1:
formatted = self.format_help_entry(match[0].key,
match[0].entrytext,
aliases=match[0].aliases.all(),
suggested=suggestions)
formatted = self.format_help_entry(
match[0].key,
match[0].entrytext,
aliases=match[0].aliases.all(),
suggested=suggestions,
)
self.msg_help(formatted)
return
# try to see if a category name was entered
if query in all_categories:
self.msg_help(self.format_help_list({query: [cmd.key for cmd in all_cmds if cmd.help_category == query]},
{query: [topic.key for topic in all_topics
if topic.help_category == query]}))
self.msg_help(
self.format_help_list(
{query: [cmd.key for cmd in all_cmds if cmd.help_category == query]},
{query: [topic.key for topic in all_topics if topic.help_category == query]},
)
)
return
# no exact matches found. Just give suggestions.
self.msg(self.format_help_entry("", f"No help entry found for '{query}'",
None, suggested=suggestions), options={"type": "help"})
self.msg(
self.format_help_entry(
"", f"No help entry found for '{query}'", None, suggested=suggestions
),
options={"type": "help"},
)
def _loadhelp(caller):
@ -317,6 +352,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
is to let everyone read the help file.
"""
key = "sethelp"
switch_options = ("edit", "replace", "append", "extend", "delete")
locks = "cmd:perm(Helper)"
@ -329,7 +365,9 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
lhslist = self.lhslist
if not self.args:
self.msg("Usage: sethelp[/switches] <topic>[;alias;alias][,category[,locks,..] = <text>")
self.msg(
"Usage: sethelp[/switches] <topic>[;alias;alias][,category[,locks,..] = <text>"
)
return
nlist = len(lhslist)
@ -357,7 +395,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
lockstring = ",".join(lhslist[2:]) if nlist > 2 else "view:all()"
category = category.lower()
if 'edit' in switches:
if "edit" in switches:
# open the line editor to edit the helptext. No = is needed.
if old_entry:
topicstr = old_entry.key
@ -366,17 +404,22 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
old_entry.entrytext += "\n%s" % self.rhs
helpentry = old_entry
else:
helpentry = create.create_help_entry(topicstr,
self.rhs, category=category,
locks=lockstring, aliases=aliases)
helpentry = create.create_help_entry(
topicstr, self.rhs, category=category, locks=lockstring, aliases=aliases
)
self.caller.db._editing_help = helpentry
EvEditor(self.caller, loadfunc=_loadhelp, savefunc=_savehelp,
quitfunc=_quithelp, key="topic {}".format(topicstr),
persistent=True)
EvEditor(
self.caller,
loadfunc=_loadhelp,
savefunc=_savehelp,
quitfunc=_quithelp,
key="topic {}".format(topicstr),
persistent=True,
)
return
if 'append' in switches or "merge" in switches or "extend" in switches:
if "append" in switches or "merge" in switches or "extend" in switches:
# merge/append operations
if not old_entry:
self.msg("Could not find topic '%s'. You must give an exact name." % topicstr)
@ -384,14 +427,14 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
if not self.rhs:
self.msg("You must supply text to append/merge.")
return
if 'merge' in switches:
if "merge" in switches:
old_entry.entrytext += " " + self.rhs
else:
old_entry.entrytext += "\n%s" % self.rhs
old_entry.aliases.add(aliases)
self.msg("Entry updated:\n%s%s" % (old_entry.entrytext, aliastxt))
return
if 'delete' in switches or 'del' in switches:
if "delete" in switches or "del" in switches:
# delete the help entry
if not old_entry:
self.msg("Could not find topic '%s'%s." % (topicstr, aliastxt))
@ -405,7 +448,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
self.msg("You must supply a help text to add.")
return
if old_entry:
if 'replace' in switches:
if "replace" in switches:
# overwrite old entry
old_entry.key = topicstr
old_entry.entrytext = self.rhs
@ -416,22 +459,30 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
old_entry.save()
self.msg("Overwrote the old topic '%s'%s." % (topicstr, aliastxt))
else:
self.msg("Topic '%s'%s already exists. Use /replace to overwrite "
"or /append or /merge to add text to it." % (topicstr, aliastxt))
self.msg(
"Topic '%s'%s already exists. Use /replace to overwrite "
"or /append or /merge to add text to it." % (topicstr, aliastxt)
)
else:
# no old entry. Create a new one.
new_entry = create.create_help_entry(topicstr,
self.rhs, category=category,
locks=lockstring, aliases=aliases)
new_entry = create.create_help_entry(
topicstr, self.rhs, category=category, locks=lockstring, aliases=aliases
)
if new_entry:
self.msg("Topic '%s'%s was successfully created." % (topicstr, aliastxt))
if 'edit' in switches:
if "edit" in switches:
# open the line editor to edit the helptext
self.caller.db._editing_help = new_entry
EvEditor(self.caller, loadfunc=_loadhelp,
savefunc=_savehelp, quitfunc=_quithelp,
key="topic {}".format(new_entry.key),
persistent=True)
EvEditor(
self.caller,
loadfunc=_loadhelp,
savefunc=_savehelp,
quitfunc=_quithelp,
key="topic {}".format(new_entry.key),
persistent=True,
)
return
else:
self.msg("Error when creating topic '%s'%s! Contact an admin." % (topicstr, aliastxt))
self.msg(
"Error when creating topic '%s'%s! Contact an admin." % (topicstr, aliastxt)
)

View file

@ -121,38 +121,46 @@ class MuxCommand(Command):
switches = args[1:].split(None, 1)
if len(switches) > 1:
switches, args = switches
switches = switches.split('/')
switches = switches.split("/")
else:
args = ""
switches = switches[0].split('/')
switches = switches[0].split("/")
# If user-provides switches, parse them with parser switch options.
if switches and self.switch_options:
valid_switches, unused_switches, extra_switches = [], [], []
for element in switches:
option_check = [opt for opt in self.switch_options if opt == element]
if not option_check:
option_check = [opt for opt in self.switch_options if opt.startswith(element)]
option_check = [
opt for opt in self.switch_options if opt.startswith(element)
]
match_count = len(option_check)
if match_count > 1:
extra_switches.extend(option_check) # Either the option provided is ambiguous,
extra_switches.extend(
option_check
) # Either the option provided is ambiguous,
elif match_count == 1:
valid_switches.extend(option_check) # or it is a valid option abbreviation,
elif match_count == 0:
unused_switches.append(element) # or an extraneous option to be ignored.
if extra_switches: # User provided switches
self.msg('|g%s|n: |wAmbiguous switch supplied: Did you mean /|C%s|w?' %
(self.cmdstring, ' |nor /|C'.join(extra_switches)))
self.msg(
"|g%s|n: |wAmbiguous switch supplied: Did you mean /|C%s|w?"
% (self.cmdstring, " |nor /|C".join(extra_switches))
)
if unused_switches:
plural = '' if len(unused_switches) == 1 else 'es'
self.msg('|g%s|n: |wExtra switch%s "/|C%s|w" ignored.' %
(self.cmdstring, plural, '|n, /|C'.join(unused_switches)))
plural = "" if len(unused_switches) == 1 else "es"
self.msg(
'|g%s|n: |wExtra switch%s "/|C%s|w" ignored.'
% (self.cmdstring, plural, "|n, /|C".join(unused_switches))
)
switches = valid_switches # Only include valid_switches in command function call
arglist = [arg.strip() for arg in args.split()]
# check for arg1, arg2, ... = argA, argB, ... constructs
lhs, rhs = args.strip(), None
if lhs:
if delimiters and hasattr(delimiters, '__iter__'): # If delimiter is iterable,
if delimiters and hasattr(delimiters, "__iter__"): # If delimiter is iterable,
best_split = delimiters[0] # (default to first delimiter)
for this_split in delimiters: # try each delimiter
if this_split in lhs: # to find first successful split
@ -167,8 +175,8 @@ class MuxCommand(Command):
rhs = rhs.strip() if rhs is not None else None
lhs = lhs.strip()
# Further split left/right sides by comma delimiter
lhslist = [arg.strip() for arg in lhs.split(',')] if lhs is not None else ""
rhslist = [arg.strip() for arg in rhs.split(',')] if rhs is not None else ""
lhslist = [arg.strip() for arg in lhs.split(",")] if lhs is not None else ""
rhslist = [arg.strip() for arg in rhs.split(",")] if rhs is not None else ""
# save to object properties:
self.raw = raw
self.switches = switches
@ -200,7 +208,9 @@ class MuxCommand(Command):
by the cmdhandler right after self.parser() finishes, and so has access
to all the variables defined therein.
"""
variables = '\n'.join(" |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items())
variables = "\n".join(
" |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items()
)
string = f"""
Command {self} has no defined `func()` - showing on-command variables: No child func() defined for {self} - available variables:
{variables}

View file

@ -31,6 +31,7 @@ from evennia.commands.cmdhandler import CMD_CHANNEL
from evennia.utils import utils
from django.conf import settings
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
# Command called when there is no input at line
@ -41,6 +42,7 @@ class SystemNoInput(COMMAND_DEFAULT_CLASS):
"""
This is called when there is no input given
"""
key = CMD_NOINPUT
locks = "cmd:all()"
@ -57,6 +59,7 @@ class SystemNoMatch(COMMAND_DEFAULT_CLASS):
"""
No command was found matching the given input.
"""
key = CMD_NOMATCH
locks = "cmd:all()"
@ -86,6 +89,7 @@ class SystemMultimatch(COMMAND_DEFAULT_CLASS):
the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping.
"""
key = CMD_MULTIMATCH
locks = "cmd:all()"
@ -99,8 +103,7 @@ class SystemMultimatch(COMMAND_DEFAULT_CLASS):
# evennia.commands.cmdparse.create_match for more details.
matches = self.matches
# at_search_result will itself msg the multimatch options to the caller.
at_search_result(
[match[2] for match in matches], self.caller, query=matches[0][0])
at_search_result([match[2] for match in matches], self.caller, query=matches[0][0])
# Command called when the command given at the command line
@ -108,6 +111,7 @@ class SystemMultimatch(COMMAND_DEFAULT_CLASS):
# channel named 'ooc' and the user wrote
# > ooc Hello!
class SystemSendToChannel(COMMAND_DEFAULT_CLASS):
"""
This is a special command that the cmdhandler calls
@ -119,7 +123,7 @@ class SystemSendToChannel(COMMAND_DEFAULT_CLASS):
locks = "cmd:all()"
def parse(self):
channelname, msg = self.args.split(':', 1)
channelname, msg = self.args.split(":", 1)
self.args = channelname.strip(), msg.strip()
def func(self):
@ -140,7 +144,7 @@ class SystemSendToChannel(COMMAND_DEFAULT_CLASS):
string = "You are not connected to channel '%s'."
caller.msg(string % channelkey)
return
if not channel.access(caller, 'send'):
if not channel.access(caller, "send"):
string = "You are not permitted to send to channel '%s'."
caller.msg(string % channelkey)
return

View file

@ -32,9 +32,18 @@ _RESOURCE = None
_IDMAPPER = None
# limit symbol import for API
__all__ = ("CmdReload", "CmdReset", "CmdShutdown", "CmdPy",
"CmdScripts", "CmdObjects", "CmdService", "CmdAbout",
"CmdTime", "CmdServerLoad")
__all__ = (
"CmdReload",
"CmdReset",
"CmdShutdown",
"CmdPy",
"CmdScripts",
"CmdObjects",
"CmdService",
"CmdAbout",
"CmdTime",
"CmdServerLoad",
)
class CmdReload(COMMAND_DEFAULT_CLASS):
@ -48,8 +57,9 @@ class CmdReload(COMMAND_DEFAULT_CLASS):
affected. Non-persistent scripts will survive a reload (use
reset to purge) and at_reload() hooks will be called.
"""
key = "reload"
aliases = ['restart']
aliases = ["restart"]
locks = "cmd:perm(reload) or perm(Developer)"
help_category = "System"
@ -84,8 +94,9 @@ class CmdReset(COMMAND_DEFAULT_CLASS):
cmdsets etc will be wiped.
"""
key = "reset"
aliases = ['reboot']
aliases = ["reboot"]
locks = "cmd:perm(reload) or perm(Developer)"
help_category = "System"
@ -107,6 +118,7 @@ class CmdShutdown(COMMAND_DEFAULT_CLASS):
Gracefully shut down both Server and Portal.
"""
key = "shutdown"
locks = "cmd:perm(shutdown) or perm(Developer)"
help_category = "System"
@ -116,11 +128,11 @@ class CmdShutdown(COMMAND_DEFAULT_CLASS):
# Only allow shutdown if caller has session
if not self.caller.sessions.get():
return
self.msg('Shutting down server ...')
self.msg("Shutting down server ...")
announcement = "\nServer is being SHUT DOWN!\n"
if self.args:
announcement += "%s\n" % self.args
logger.log_info('Server shutdown by %s.' % self.caller.name)
logger.log_info("Server shutdown by %s." % self.caller.name)
SESSIONS.announce_all(announcement)
SESSIONS.portal_shutdown()
@ -135,13 +147,11 @@ def _py_code(caller, buf):
"""
measure_time = caller.db._py_measure_time
client_raw = caller.db._py_clientraw
string = "Executing code%s ..." % (
" (measure timing)" if measure_time else "")
string = "Executing code%s ..." % (" (measure timing)" if measure_time else "")
caller.msg(string)
_run_code_snippet(caller, buf, mode="exec",
measure_time=measure_time,
client_raw=client_raw,
show_input=False)
_run_code_snippet(
caller, buf, mode="exec", measure_time=measure_time, client_raw=client_raw, show_input=False
)
return True
@ -150,8 +160,9 @@ def _py_quit(caller):
caller.msg("Exited the code editor.")
def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
client_raw=False, show_input=True):
def _run_code_snippet(
caller, pycode, mode="eval", measure_time=False, client_raw=False, show_input=True
):
"""
Run code and try to display information to the caller.
@ -173,14 +184,10 @@ def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
if show_input:
for session in sessions:
try:
caller.msg(">>> %s" % pycode, session=session,
options={"raw": True})
caller.msg(">>> %s" % pycode, session=session, options={"raw": True})
except TypeError:
caller.msg(">>> %s" % pycode, options={"raw": True})
try:
# reroute standard output to game client console
old_stdout = sys.stdout
@ -214,7 +221,7 @@ def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
ret = eval(pycode_compiled, {}, available_vars)
except Exception:
errlist = traceback.format_exc().split('\n')
errlist = traceback.format_exc().split("\n")
if len(errlist) > 4:
errlist = errlist[4:]
ret = "\n".join("%s" % line for line in errlist if line)
@ -228,26 +235,25 @@ def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
for session in sessions:
try:
caller.msg(ret, session=session, options={"raw": True,
"client_raw": client_raw})
caller.msg(ret, session=session, options={"raw": True, "client_raw": client_raw})
except TypeError:
caller.msg(ret, options={"raw": True,
"client_raw": client_raw})
caller.msg(ret, options={"raw": True, "client_raw": client_raw})
def evennia_local_vars(caller):
"""Return Evennia local variables usable in the py command as a dictionary."""
import evennia
return {
'self': caller,
'me': caller,
'here': getattr(caller, "location", None),
'evennia': evennia,
'ev': evennia,
'inherits_from': utils.inherits_from,
"self": caller,
"me": caller,
"here": getattr(caller, "location", None),
"evennia": evennia,
"ev": evennia,
"inherits_from": utils.inherits_from,
}
class EvenniaPythonConsole(code.InteractiveConsole):
"""Evennia wrapper around a Python interactive console."""
@ -330,6 +336,7 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
should only be accessible by trusted server admins/superusers.|n
"""
key = "py"
aliases = ["!"]
switch_options = ("time", "edit", "clientraw")
@ -345,27 +352,40 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
if "edit" in self.switches:
caller.db._py_measure_time = "time" in self.switches
caller.db._py_clientraw = "clientraw" in self.switches
EvEditor(self.caller, loadfunc=_py_load, savefunc=_py_code,
quitfunc=_py_quit, key="Python exec: :w or :!", persistent=True,
codefunc=_py_code)
EvEditor(
self.caller,
loadfunc=_py_load,
savefunc=_py_code,
quitfunc=_py_quit,
key="Python exec: :w or :!",
persistent=True,
codefunc=_py_code,
)
return
if not pycode:
# Run in interactive mode
console = EvenniaPythonConsole(self.caller)
banner = (f"|gPython {sys.version} on {sys.platform}\n"
"Evennia interactive console mode - type 'exit()' to leave.|n")
banner = (
f"|gPython {sys.version} on {sys.platform}\n"
"Evennia interactive console mode - type 'exit()' to leave.|n"
)
self.msg(banner)
line = ""
prompt = ">>>"
while line.lower() not in ("exit", "exit()"):
line = yield(prompt)
line = yield (prompt)
prompt = "..." if console.push(line) else ">>>"
self.msg("|gClosing the Python console.|n")
return
_run_code_snippet(caller, self.args, measure_time="time" in self.switches,
client_raw="clientraw" in self.switches)
_run_code_snippet(
caller,
self.args,
measure_time="time" in self.switches,
client_raw="clientraw" in self.switches,
)
# helper function. Kept outside so it can be imported and run
# by other commands.
@ -376,9 +396,19 @@ def format_script_list(scripts):
if not scripts:
return "<No scripts>"
table = EvTable("|wdbref|n", "|wobj|n", "|wkey|n", "|wintval|n", "|wnext|n",
"|wrept|n", "|wdb", "|wtypeclass|n", "|wdesc|n",
align='r', border="tablecols")
table = EvTable(
"|wdbref|n",
"|wobj|n",
"|wkey|n",
"|wintval|n",
"|wnext|n",
"|wrept|n",
"|wdb",
"|wtypeclass|n",
"|wdesc|n",
align="r",
border="tablecols",
)
for script in scripts:
nextrep = script.time_until_next_repeat()
if nextrep is None:
@ -392,15 +422,17 @@ def format_script_list(scripts):
else:
rept = "-/-"
table.add_row(script.id,
script.obj.key if (hasattr(script, 'obj') and script.obj) else "<Global>",
script.key,
script.interval if script.interval > 0 else "--",
nextrep,
rept,
"*" if script.persistent else "-",
script.typeclass_path.rsplit('.', 1)[-1],
crop(script.desc, width=20))
table.add_row(
script.id,
script.obj.key if (hasattr(script, "obj") and script.obj) else "<Global>",
script.key,
script.interval if script.interval > 0 else "--",
nextrep,
rept,
"*" if script.persistent else "-",
script.typeclass_path.rsplit(".", 1)[-1],
crop(script.desc, width=20),
)
return "%s" % table
@ -425,6 +457,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
Use script for managing commands on objects.
"""
key = "scripts"
aliases = ["globalscript", "listscripts"]
switch_options = ("start", "stop", "kill", "validate")
@ -469,11 +502,11 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
caller.msg(string)
return
if self.switches and self.switches[0] in ('stop', 'del', 'delete', 'kill'):
if self.switches and self.switches[0] in ("stop", "del", "delete", "kill"):
# we want to delete something
if len(scripts) == 1:
# we have a unique match!
if 'kill' in self.switches:
if "kill" in self.switches:
string = "Killing script '%s'" % scripts[0].key
scripts[0].stop(kill=True)
else:
@ -508,8 +541,9 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
a list of <nr> latest objects in database. If not
given, <nr> defaults to 10.
"""
key = "objects"
aliases = ["listobjects", "listobjs", 'stats', 'db']
aliases = ["listobjects", "listobjs", "stats", "db"]
locks = "cmd:perm(listobjects) or perm(Builder)"
help_category = "System"
@ -529,33 +563,49 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
nobjs = nobjs or 1 # fix zero-div error with empty database
# total object sum table
totaltable = self.styled_table("|wtype|n", "|wcomment|n", "|wcount|n", "|w%|n",
border="table", align="l")
totaltable.align = 'l'
totaltable.add_row("Characters", "(BASE_CHARACTER_TYPECLASS + children)",
nchars, "%.2f" % ((float(nchars) / nobjs) * 100))
totaltable.add_row("Rooms", "(BASE_ROOM_TYPECLASS + children)",
nrooms, "%.2f" % ((float(nrooms) / nobjs) * 100))
totaltable.add_row("Exits", "(BASE_EXIT_TYPECLASS + children)",
nexits, "%.2f" % ((float(nexits) / nobjs) * 100))
totaltable = self.styled_table(
"|wtype|n", "|wcomment|n", "|wcount|n", "|w%|n", border="table", align="l"
)
totaltable.align = "l"
totaltable.add_row(
"Characters",
"(BASE_CHARACTER_TYPECLASS + children)",
nchars,
"%.2f" % ((float(nchars) / nobjs) * 100),
)
totaltable.add_row(
"Rooms",
"(BASE_ROOM_TYPECLASS + children)",
nrooms,
"%.2f" % ((float(nrooms) / nobjs) * 100),
)
totaltable.add_row(
"Exits",
"(BASE_EXIT_TYPECLASS + children)",
nexits,
"%.2f" % ((float(nexits) / nobjs) * 100),
)
totaltable.add_row("Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100))
# typeclass table
typetable = self.styled_table("|wtypeclass|n", "|wcount|n", "|w%|n",
border="table", align="l")
typetable.align = 'l'
typetable = self.styled_table(
"|wtypeclass|n", "|wcount|n", "|w%|n", border="table", align="l"
)
typetable.align = "l"
dbtotals = ObjectDB.objects.object_totals()
for path, count in dbtotals.items():
typetable.add_row(path, count, "%.2f" % ((float(count) / nobjs) * 100))
# last N table
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim):]
latesttable = self.styled_table("|wcreated|n", "|wdbref|n", "|wname|n",
"|wtypeclass|n", align="l", border="table")
latesttable.align = 'l'
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim) :]
latesttable = self.styled_table(
"|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table"
)
latesttable.align = "l"
for obj in objs:
latesttable.add_row(utils.datetime_format(obj.date_created),
obj.dbref, obj.key, obj.path)
latesttable.add_row(
utils.datetime_format(obj.date_created), obj.dbref, obj.key, obj.path
)
string = "\n|wObject subtype totals (out of %i Objects):|n\n%s" % (nobjs, totaltable)
string += "\n|wObject typeclass distribution:|n\n%s" % typetable
@ -578,9 +628,10 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
It will list the <nr> amount of latest registered accounts
If not given, <nr> defaults to 10.
"""
key = "accounts"
aliases = ["account", "listaccounts"]
switch_options = ("delete", )
switch_options = ("delete",)
locks = "cmd:perm(listaccounts) or perm(Admin)"
help_category = "System"
@ -618,11 +669,13 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
return
username = account.username
# ask for confirmation
confirm = ("It is often better to block access to an account rather than to delete it. "
"|yAre you sure you want to permanently delete "
"account '|n{}|y'|n yes/[no]?".format(username))
answer = yield(confirm)
if answer.lower() not in ('y', 'yes'):
confirm = (
"It is often better to block access to an account rather than to delete it. "
"|yAre you sure you want to permanently delete "
"account '|n{}|y'|n yes/[no]?".format(username)
)
answer = yield (confirm)
if answer.lower() not in ("y", "yes"):
caller.msg("Canceled deletion.")
return
@ -632,7 +685,10 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
if reason:
string += " Reason given:\n '%s'" % reason
account.msg(string)
logger.log_sec("Account Deleted: %s (Reason: %s, Caller: %s, IP: %s)." % (account, reason, caller, self.session.address))
logger.log_sec(
"Account Deleted: %s (Reason: %s, Caller: %s, IP: %s)."
% (account, reason, caller, self.session.address)
)
account.delete()
self.msg("Account %s was successfully deleted." % username)
return
@ -647,14 +703,20 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
# typeclass table
dbtotals = AccountDB.objects.object_totals()
typetable = self.styled_table("|wtypeclass|n", "|wcount|n", "|w%%|n", border="cells", align="l")
typetable = self.styled_table(
"|wtypeclass|n", "|wcount|n", "|w%%|n", border="cells", align="l"
)
for path, count in dbtotals.items():
typetable.add_row(path, count, "%.2f" % ((float(count) / naccounts) * 100))
# last N table
plyrs = AccountDB.objects.all().order_by("db_date_created")[max(0, naccounts - nlim):]
latesttable = self.styled_table("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", border="cells", align="l")
plyrs = AccountDB.objects.all().order_by("db_date_created")[max(0, naccounts - nlim) :]
latesttable = self.styled_table(
"|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", border="cells", align="l"
)
for ply in plyrs:
latesttable.add_row(utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.path)
latesttable.add_row(
utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.path
)
string = "\n|wAccount typeclass distribution:|n\n%s" % typetable
string += "\n|wLast %s Accounts created:|n\n%s" % (min(naccounts, nlim), latesttable)
@ -703,7 +765,9 @@ class CmdService(COMMAND_DEFAULT_CLASS):
if not switches or switches[0] == "list":
# Just display the list of installed services and their
# status, then exit.
table = self.styled_table("|wService|n (use services/start|stop|delete)", "|wstatus", align="l")
table = self.styled_table(
"|wService|n (use services/start|stop|delete)", "|wstatus", align="l"
)
for service in service_collection.services:
table.add_row(service.name, service.running and "|gRunning" or "|rNot Running")
caller.msg(str(table))
@ -714,8 +778,8 @@ class CmdService(COMMAND_DEFAULT_CLASS):
try:
service = service_collection.getServiceNamed(self.args)
except Exception:
string = 'Invalid service name. This command is case-sensitive. '
string += 'See service/list for valid service name (enter the full name exactly).'
string = "Invalid service name. This command is case-sensitive. "
string += "See service/list for valid service name (enter the full name exactly)."
caller.msg(string)
return
@ -725,9 +789,9 @@ class CmdService(COMMAND_DEFAULT_CLASS):
delmode = switches[0] == "delete"
if not service.running:
caller.msg('That service is not currently running.')
caller.msg("That service is not currently running.")
return
if service.name[:7] == 'Evennia':
if service.name[:7] == "Evennia":
if delmode:
caller.msg("You cannot remove a core Evennia service (named 'Evennia***').")
return
@ -749,7 +813,7 @@ class CmdService(COMMAND_DEFAULT_CLASS):
if switches[0] == "start":
# Attempt to start a service.
if service.running:
caller.msg('That service is already running.')
caller.msg("That service is already running.")
return
caller.msg("Starting service '%s'." % self.args)
service.startService()
@ -789,11 +853,13 @@ class CmdAbout(COMMAND_DEFAULT_CLASS):
|wMaintainer|n (2010-) Griatch (griatch AT gmail DOT com)
|wMaintainer|n (2006-10) Greg Taylor
""".format(version=utils.get_evennia_version(),
os=os.name,
python=sys.version.split()[0],
twisted=twisted.version.short(),
django=django.get_version())
""".format(
version=utils.get_evennia_version(),
os=os.name,
python=sys.version.split()[0],
twisted=twisted.version.short(),
django=django.get_version(),
)
self.caller.msg(string)
@ -807,6 +873,7 @@ class CmdTime(COMMAND_DEFAULT_CLASS):
List Server time statistics such as uptime
and the current time stamp.
"""
key = "time"
aliases = "uptime"
locks = "cmd:perm(time) or perm(Player)"
@ -821,11 +888,19 @@ class CmdTime(COMMAND_DEFAULT_CLASS):
table1.add_row("First start", datetime.datetime.fromtimestamp(gametime.server_epoch()))
table1.add_row("Current time", datetime.datetime.now())
table1.reformat_column(0, width=30)
table2 = self.styled_table("|wIn-Game time", "|wReal time x %g" % gametime.TIMEFACTOR, align="l", width=77, border_top=0)
table2 = self.styled_table(
"|wIn-Game time",
"|wReal time x %g" % gametime.TIMEFACTOR,
align="l",
width=77,
border_top=0,
)
epochtxt = "Epoch (%s)" % ("from settings" if settings.TIME_GAME_EPOCH else "server start")
table2.add_row(epochtxt, datetime.datetime.fromtimestamp(gametime.game_epoch()))
table2.add_row("Total time passed:", utils.time_format(gametime.gametime(), 2))
table2.add_row("Current time ", datetime.datetime.fromtimestamp(gametime.gametime(absolute=True)))
table2.add_row(
"Current time ", datetime.datetime.fromtimestamp(gametime.gametime(absolute=True))
)
table2.reformat_column(0, width=30)
self.caller.msg(str(table1) + "\n" + str(table2))
@ -866,6 +941,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
the released memory will instead be re-used by the program.
"""
key = "server"
aliases = ["serverload", "serverprocess"]
switch_options = ("mem", "flushmem")
@ -884,8 +960,10 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
prev, _ = _IDMAPPER.cache_size()
nflushed = _IDMAPPER.flush_cache()
now, _ = _IDMAPPER.cache_size()
string = "The Idmapper cache freed |w{idmapper}|n database objects.\n" \
"The Python garbage collector freed |w{gc}|n Python instances total."
string = (
"The Idmapper cache freed |w{idmapper}|n database objects.\n"
"The Python garbage collector freed |w{gc}|n Python instances total."
)
self.caller.msg(string.format(idmapper=(prev - now), gc=nflushed))
return
@ -900,6 +978,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
# unfortunately, since it's not specific to the process) /rant
try:
import psutil
has_psutil = True
except ImportError:
has_psutil = False
@ -920,8 +999,10 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
loadtable.add_row("Total computer memory usage", "%g MB (%g%%)" % (rmem, pmem))
loadtable.add_row("Process ID", "%g" % pid),
else:
loadtable = "Not available on Windows without 'psutil' library " \
"(install with |wpip install psutil|n)."
loadtable = (
"Not available on Windows without 'psutil' library "
"(install with |wpip install psutil|n)."
)
else:
# Linux / BSD (OSX) - proper pid-based statistics
@ -931,9 +1012,15 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
import resource as _RESOURCE
loadavg = os.getloadavg()[0]
rmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "rss")).read()) / 1000.0 # resident memory
vmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "vsz")).read()) / 1000.0 # virtual memory
pmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "%mem")).read()) # % of resident memory to total
rmem = (
float(os.popen("ps -p %d -o %s | tail -1" % (pid, "rss")).read()) / 1000.0
) # resident memory
vmem = (
float(os.popen("ps -p %d -o %s | tail -1" % (pid, "vsz")).read()) / 1000.0
) # virtual memory
pmem = float(
os.popen("ps -p %d -o %s | tail -1" % (pid, "%mem")).read()
) # % of resident memory to total
rusage = _RESOURCE.getrusage(_RESOURCE.RUSAGE_SELF)
if "mem" in self.switches:
@ -947,16 +1034,28 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
loadtable.add_row("Memory usage", "%g MB (%g%%)" % (rmem, pmem))
loadtable.add_row("Virtual address space", "")
loadtable.add_row("|x(resident+swap+caching)|n", "%g MB" % vmem)
loadtable.add_row("CPU time used (total)", "%s (%gs)"
% (utils.time_format(rusage.ru_utime), rusage.ru_utime))
loadtable.add_row("CPU time used (user)", "%s (%gs)"
% (utils.time_format(rusage.ru_stime), rusage.ru_stime))
loadtable.add_row("Page faults", "%g hard, %g soft, %g swapouts"
% (rusage.ru_majflt, rusage.ru_minflt, rusage.ru_nswap))
loadtable.add_row("Disk I/O", "%g reads, %g writes" % (rusage.ru_inblock, rusage.ru_oublock))
loadtable.add_row(
"CPU time used (total)",
"%s (%gs)" % (utils.time_format(rusage.ru_utime), rusage.ru_utime),
)
loadtable.add_row(
"CPU time used (user)",
"%s (%gs)" % (utils.time_format(rusage.ru_stime), rusage.ru_stime),
)
loadtable.add_row(
"Page faults",
"%g hard, %g soft, %g swapouts"
% (rusage.ru_majflt, rusage.ru_minflt, rusage.ru_nswap),
)
loadtable.add_row(
"Disk I/O", "%g reads, %g writes" % (rusage.ru_inblock, rusage.ru_oublock)
)
loadtable.add_row("Network I/O", "%g in, %g out" % (rusage.ru_msgrcv, rusage.ru_msgsnd))
loadtable.add_row("Context switching", "%g vol, %g forced, %g signals"
% (rusage.ru_nvcsw, rusage.ru_nivcsw, rusage.ru_nsignals))
loadtable.add_row(
"Context switching",
"%g vol, %g forced, %g signals"
% (rusage.ru_nvcsw, rusage.ru_nivcsw, rusage.ru_nsignals),
)
# os-generic
@ -964,8 +1063,11 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
# object cache count (note that sys.getsiseof is not called so this works for pypy too.
total_num, cachedict = _IDMAPPER.cache_size()
sorted_cache = sorted([(key, num) for key, num in cachedict.items() if num > 0],
key=lambda tup: tup[1], reverse=True)
sorted_cache = sorted(
[(key, num) for key, num in cachedict.items() if num > 0],
key=lambda tup: tup[1],
reverse=True,
)
memtable = self.styled_table("entity name", "number", "idmapper %", align="l")
for tup in sorted_cache:
memtable.add_row(tup[0], "%i" % tup[1], "%.2f" % (float(tup[1]) / total_num * 100))
@ -988,22 +1090,29 @@ class CmdTickers(COMMAND_DEFAULT_CLASS):
inspecting the current status.
"""
key = "tickers"
help_category = "System"
locks = "cmd:perm(tickers) or perm(Builder)"
def func(self):
from evennia import TICKER_HANDLER
all_subs = TICKER_HANDLER.all_display()
if not all_subs:
self.caller.msg("No tickers are currently active.")
return
table = self.styled_table("interval (s)", "object", "path/methodname", "idstring", "db")
for sub in all_subs:
table.add_row(sub[3],
"%s%s" % (sub[0] or "[None]",
sub[0] and " (#%s)" % (sub[0].id if hasattr(sub[0], "id") else "") or ""),
sub[1] if sub[1] else sub[2],
sub[4] or "[Unset]",
"*" if sub[5] else "-")
table.add_row(
sub[3],
"%s%s"
% (
sub[0] or "[None]",
sub[0] and " (#%s)" % (sub[0].id if hasattr(sub[0], "id") else "") or "",
),
sub[1] if sub[1] else sub[2],
sub[4] or "[Unset]",
"*" if sub[5] else "-",
)
self.caller.msg("|wActive tickers|n:\n" + str(table))

File diff suppressed because it is too large Load diff

View file

@ -14,8 +14,13 @@ from evennia.commands.cmdhandler import CMD_LOGINSTART
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
# limit symbol import for API
__all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
"CmdUnconnectedQuit", "CmdUnconnectedLook", "CmdUnconnectedHelp")
__all__ = (
"CmdUnconnectedConnect",
"CmdUnconnectedCreate",
"CmdUnconnectedQuit",
"CmdUnconnectedLook",
"CmdUnconnectedHelp",
)
MULTISESSION_MODE = settings.MULTISESSION_MODE
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
@ -45,7 +50,7 @@ def create_guest_account(session):
if account:
return enabled, account
else:
session.msg("|R%s|n" % '\n'.join(errors))
session.msg("|R%s|n" % "\n".join(errors))
return enabled, None
@ -68,10 +73,12 @@ def create_normal_account(session, name, password):
# Match account name and check password
# authenticate() handles all its own throttling
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
account, errors = Account.authenticate(
username=name, password=password, ip=address, session=session
)
if not account:
# No accountname or password match
session.msg("|R%s|n" % '\n'.join(errors))
session.msg("|R%s|n" % "\n".join(errors))
return None
return account
@ -89,6 +96,7 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
If you have spaces in your name, enclose it in double quotes.
"""
key = "connect"
aliases = ["conn", "con", "co"]
locks = "cmd:all()" # not really needed
@ -122,7 +130,7 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
session.sessionhandler.login(session, account)
return
else:
session.msg("|R%s|n" % '\n'.join(errors))
session.msg("|R%s|n" % "\n".join(errors))
return
if len(parts) != 2:
@ -133,11 +141,13 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
name, password = parts
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
account, errors = Account.authenticate(
username=name, password=password, ip=address, session=session
)
if account:
session.sessionhandler.login(session, account)
else:
session.msg("|R%s|n" % '\n'.join(errors))
session.msg("|R%s|n" % "\n".join(errors))
class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
@ -152,6 +162,7 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
If you have spaces in your name, enclose it in double quotes.
"""
key = "create"
aliases = ["cre", "cr"]
locks = "cmd:all()"
@ -174,25 +185,31 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
# this was (hopefully) due to no quotes being found
parts = parts[0].split(None, 1)
if len(parts) != 2:
string = "\n Usage (without <>): create <name> <password>" \
"\nIf <name> or <password> contains spaces, enclose it in double quotes."
string = (
"\n Usage (without <>): create <name> <password>"
"\nIf <name> or <password> contains spaces, enclose it in double quotes."
)
session.msg(string)
return
username, password = parts
# everything's ok. Create the new account account.
account, errors = Account.create(username=username, password=password, ip=address, session=session)
account, errors = Account.create(
username=username, 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\" <your password>'."
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 % (username, username))
else:
session.msg("|R%s|n" % '\n'.join(errors))
session.msg("|R%s|n" % "\n".join(errors))
class CmdUnconnectedQuit(COMMAND_DEFAULT_CLASS):
@ -206,6 +223,7 @@ class CmdUnconnectedQuit(COMMAND_DEFAULT_CLASS):
here for unconnected accounts for the sake of simplicity. The logged in
version is a bit more complicated.
"""
key = "quit"
aliases = ["q", "qu"]
locks = "cmd:all()"
@ -228,6 +246,7 @@ class CmdUnconnectedLook(COMMAND_DEFAULT_CLASS):
This is called by the server and kicks everything in gear.
All it does is display the connect screen.
"""
key = CMD_LOGINSTART
aliases = ["look", "l"]
locks = "cmd:all()"
@ -237,7 +256,7 @@ class CmdUnconnectedLook(COMMAND_DEFAULT_CLASS):
callables = utils.callables_from_module(CONNECTION_SCREEN_MODULE)
if "connection_screen" in callables:
connection_screen = callables['connection_screen']()
connection_screen = callables["connection_screen"]()
else:
connection_screen = utils.random_string_from_module(CONNECTION_SCREEN_MODULE)
if not connection_screen:
@ -255,6 +274,7 @@ class CmdUnconnectedHelp(COMMAND_DEFAULT_CLASS):
This is an unconnected version of the help command,
for simplicity. It shows a pane of info.
"""
key = "help"
aliases = ["h", "?"]
locks = "cmd:all()"
@ -262,8 +282,7 @@ class CmdUnconnectedHelp(COMMAND_DEFAULT_CLASS):
def func(self):
"""Shows help"""
string = \
"""
string = """
You are not yet logged into the game. Commands available at this point:
|wcreate|n - create a new account
@ -283,7 +302,7 @@ You can use the |wlook|n command if you want to see the connect screen again.
"""
if settings.STAFF_CONTACT_EMAIL:
string += 'For support, please contact: %s' % settings.STAFF_CONTACT_EMAIL
string += "For support, please contact: %s" % settings.STAFF_CONTACT_EMAIL
self.caller.msg(string)
@ -311,7 +330,7 @@ class CmdUnconnectedEncoding(COMMAND_DEFAULT_CLASS):
"""
key = "encoding"
aliases = ("encode")
aliases = "encode"
locks = "cmd:all()"
def func(self):
@ -323,7 +342,7 @@ class CmdUnconnectedEncoding(COMMAND_DEFAULT_CLASS):
return
sync = False
if 'clear' in self.switches:
if "clear" in self.switches:
# remove customization
old_encoding = self.session.protocol_flags.get("ENCODING", None)
if old_encoding:
@ -337,10 +356,15 @@ class CmdUnconnectedEncoding(COMMAND_DEFAULT_CLASS):
pencoding = self.session.protocol_flags.get("ENCODING", None)
string = ""
if pencoding:
string += "Default encoding: |g%s|n (change with |wencoding <encoding>|n)" % pencoding
string += (
"Default encoding: |g%s|n (change with |wencoding <encoding>|n)" % pencoding
)
encodings = settings.ENCODINGS
if encodings:
string += "\nServer's alternative encodings (tested in this order):\n |g%s|n" % ", ".join(encodings)
string += (
"\nServer's alternative encodings (tested in this order):\n |g%s|n"
% ", ".join(encodings)
)
if not string:
string = "No encodings found."
else:
@ -350,11 +374,16 @@ class CmdUnconnectedEncoding(COMMAND_DEFAULT_CLASS):
try:
codecs_lookup(encoding)
except LookupError:
string = "|rThe encoding '|w%s|r' is invalid. Keeping the previous encoding '|w%s|r'.|n"\
% (encoding, old_encoding)
string = (
"|rThe encoding '|w%s|r' is invalid. Keeping the previous encoding '|w%s|r'.|n"
% (encoding, old_encoding)
)
else:
self.session.protocol_flags["ENCODING"] = encoding
string = "Your custom text encoding was changed from '|w%s|n' to '|w%s|n'." % (old_encoding, encoding)
string = "Your custom text encoding was changed from '|w%s|n' to '|w%s|n'." % (
old_encoding,
encoding,
)
sync = True
if sync:
self.session.sessionhandler.session_portal_sync(self.session)
@ -371,6 +400,7 @@ class CmdUnconnectedScreenreader(COMMAND_DEFAULT_CLASS):
Used to flip screenreader mode on and off before logging in (when
logged in, use option screenreader on).
"""
key = "screenreader"
def func(self):
@ -390,14 +420,20 @@ class CmdUnconnectedInfo(COMMAND_DEFAULT_CLASS):
was created by looking at the MUDINFO implementation in MUX2, TinyMUSH, Rhost,
and PennMUSH.
"""
key = "info"
locks = "cmd:all()"
def func(self):
self.caller.msg("## 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(),
SESSIONS.account_count(), utils.get_evennia_version()))
self.caller.msg(
"## 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(),
SESSIONS.account_count(),
utils.get_evennia_version(),
)
)
def _create_account(session, accountname, password, permissions, typeclass=None, email=None):
@ -405,10 +441,15 @@ def _create_account(session, accountname, password, permissions, typeclass=None,
Helper function, creates an account of the specified typeclass.
"""
try:
new_account = create.create_account(accountname, email, password, permissions=permissions, typeclass=typeclass)
new_account = create.create_account(
accountname, email, password, permissions=permissions, typeclass=typeclass
)
except Exception as e:
session.msg("There was an error creating the Account:\n%s\n If this problem persists, contact an admin." % e)
session.msg(
"There was an error creating the Account:\n%s\n If this problem persists, contact an admin."
% e
)
logger.log_trace()
return False
@ -431,13 +472,17 @@ def _create_character(session, new_account, typeclass, home, permissions):
This is meant for Guest and MULTISESSION_MODE < 2 situations.
"""
try:
new_character = create.create_object(typeclass, key=new_account.key, home=home, permissions=permissions)
new_character = create.create_object(
typeclass, key=new_account.key, home=home, permissions=permissions
)
# set playable character list
new_account.db._playable_characters.append(new_character)
# allow only the character itself and the account to puppet this character (and Developers).
new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)" %
(new_character.id, new_account.id))
new_character.locks.add(
"puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)"
% (new_character.id, new_account.id)
)
# If no description is set, set a default description
if not new_character.db.desc:
@ -445,5 +490,8 @@ def _create_character(session, new_account, typeclass, home, permissions):
# We need to set this to have ic auto-connect to this character
new_account.db._last_puppet = new_character
except Exception as e:
session.msg("There was an error creating the Character:\n%s\n If this problem persists, contact an admin." % e)
session.msg(
"There was an error creating the Character:\n%s\n If this problem persists, contact an admin."
% e
)
logger.log_trace()

View file

@ -12,6 +12,7 @@ from evennia.commands import cmdparser
# Testing-command sets
class _CmdA(Command):
key = "A"
@ -80,6 +81,7 @@ class _CmdSetD(CmdSet):
self.add(_CmdC("D"))
self.add(_CmdD("D"))
# testing Command Sets
@ -263,6 +265,7 @@ class TestCmdSetMergers(TestCase):
cmdset_f = d + b + c + a # two last mergers duplicates=True
self.assertEqual(len(cmdset_f.commands), 10)
# test cmdhandler functions
@ -279,7 +282,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
"Test the cmdhandler.get_and_merge_cmdsets function."
def setUp(self):
self.patch(sys.modules['evennia.server.sessionhandler'], 'delay', _mockdelay)
self.patch(sys.modules["evennia.server.sessionhandler"], "delay", _mockdelay)
super().setUp()
self.cmdset_a = _CmdSetA()
self.cmdset_b = _CmdSetB()
@ -295,19 +298,25 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
a = self.cmdset_a
a.no_channels = True
self.set_cmdsets(self.session, a)
deferred = cmdhandler.get_and_merge_cmdsets(self.session, self.session, None, None, "session", "")
deferred = cmdhandler.get_and_merge_cmdsets(
self.session, self.session, None, None, "session", ""
)
def _callback(cmdset):
self.assertEqual(cmdset.key, "A")
deferred.addCallback(_callback)
return deferred
def test_from_account(self):
from evennia.commands.default.cmdset_account import AccountCmdSet
a = self.cmdset_a
a.no_channels = True
self.set_cmdsets(self.account, a)
deferred = cmdhandler.get_and_merge_cmdsets(self.account, None, self.account, None, "account", "")
deferred = cmdhandler.get_and_merge_cmdsets(
self.account, None, self.account, None, "account", ""
)
# get_and_merge_cmdsets converts to lower-case internally.
def _callback(cmdset):
@ -315,7 +324,8 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
pcmdset.at_cmdset_creation()
pcmds = [cmd.key for cmd in pcmdset.commands] + ["a", "b", "c", "d"]
self.assertTrue(all(cmd.key in pcmds for cmd in cmdset.commands))
#_callback = lambda cmdset: self.assertEqual(sum(1 for cmd in cmdset.commands if cmd.key in ("a", "b", "c", "d")), 4)
# _callback = lambda cmdset: self.assertEqual(sum(1 for cmd in cmdset.commands if cmd.key in ("a", "b", "c", "d")), 4)
deferred.addCallback(_callback)
return deferred
@ -324,7 +334,11 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
deferred = cmdhandler.get_and_merge_cmdsets(self.obj1, None, None, self.obj1, "object", "")
# get_and_merge_cmdsets converts to lower-case internally.
def _callback(cmdset): return self.assertEqual(sum(1 for cmd in cmdset.commands if cmd.key in ("a", "b", "c", "d")), 4)
def _callback(cmdset):
return self.assertEqual(
sum(1 for cmd in cmdset.commands if cmd.key in ("a", "b", "c", "d")), 4
)
deferred.addCallback(_callback)
return deferred
@ -340,6 +354,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
self.assertTrue(cmdset.no_exits)
self.assertTrue(cmdset.no_channels)
self.assertEqual(cmdset.key, "D")
deferred.addCallback(_callback)
return deferred
@ -347,6 +362,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
import evennia
from evennia.commands.default.cmdset_account import AccountCmdSet
from evennia.comms.channelhandler import CHANNEL_HANDLER
testchannel = evennia.create_channel("channeltest", locks="listen:all();send:all()")
CHANNEL_HANDLER.add(testchannel)
CHANNEL_HANDLER.update()
@ -354,14 +370,19 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
self.assertTrue(testchannel.has_connection(self.account))
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
self.set_cmdsets(self.account, a, b, c, d)
deferred = cmdhandler.get_and_merge_cmdsets(self.session, self.session, self.account, self.char1, "session", "")
deferred = cmdhandler.get_and_merge_cmdsets(
self.session, self.session, self.account, self.char1, "session", ""
)
def _callback(cmdset):
pcmdset = AccountCmdSet()
pcmdset.at_cmdset_creation()
pcmds = [cmd.key for cmd in pcmdset.commands] + ["a", "b", "c", "d"] + ["out"]
self.assertTrue(all(cmd.key or hasattr(cmd, "is_channel") in pcmds for cmd in cmdset.commands))
self.assertTrue(
all(cmd.key or hasattr(cmd, "is_channel") in pcmds for cmd in cmdset.commands)
)
self.assertTrue(any(hasattr(cmd, "is_channel") for cmd in cmdset.commands))
deferred.addCallback(_callback)
return deferred
@ -376,6 +397,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
def _callback(cmdset):
self.assertEqual(len(cmdset.commands), 9)
deferred.addCallback(_callback)
return deferred
@ -384,6 +406,7 @@ class AccessableCommand(Command):
def access(*args, **kwargs):
return True
class _CmdTest1(AccessableCommand):
key = "test1"
@ -402,6 +425,7 @@ class _CmdTest4(AccessableCommand):
class _CmdSetTest(CmdSet):
key = "test_cmdset"
def at_cmdset_creation(self):
self.add(_CmdTest1)
self.add(_CmdTest2)
@ -409,15 +433,16 @@ class _CmdSetTest(CmdSet):
class TestCmdParser(TestCase):
def test_create_match(self):
class DummyCmd:
pass
dummy = DummyCmd()
self.assertEqual(
cmdparser.create_match("look at", "look at target", dummy, "look"),
("look at", " target", dummy, 7, 0.5, "look"))
("look at", " target", dummy, 7, 0.5, "look"),
)
@override_settings(CMD_IGNORE_PREFIXES="@&/+")
def test_build_matches(self):
@ -427,38 +452,38 @@ class TestCmdParser(TestCase):
# normal parsing
self.assertEqual(
cmdparser.build_matches("test1 rock", a_cmdset, include_prefixes=False),
[("test1", " rock", bcmd, 5, 0.5, 'test1')]
)
[("test1", " rock", bcmd, 5, 0.5, "test1")],
)
# test prefix exclusion
bcmd = [cmd for cmd in a_cmdset.commands if cmd.key == "another command"][0]
self.assertEqual(
cmdparser.build_matches("@another command smiles to me ",
a_cmdset, include_prefixes=False),
[("another command", " smiles to me ", bcmd, 15, 0.5, 'another command')]
)
cmdparser.build_matches(
"@another command smiles to me ", a_cmdset, include_prefixes=False
),
[("another command", " smiles to me ", bcmd, 15, 0.5, "another command")],
)
# test prefix exclusion on the cmd class
bcmd = [cmd for cmd in a_cmdset.commands if cmd.key == "&the third command"][0]
self.assertEqual(
cmdparser.build_matches("the third command",
a_cmdset, include_prefixes=False),
[("the third command", "", bcmd, 17, 1.0, '&the third command')]
)
cmdparser.build_matches("the third command", a_cmdset, include_prefixes=False),
[("the third command", "", bcmd, 17, 1.0, "&the third command")],
)
@override_settings(SEARCH_MULTIMATCH_REGEX=r"(?P<number>[0-9]+)-(?P<name>.*)")
def test_num_prefixes(self):
self.assertEqual(cmdparser.try_num_prefixes("look me"),
(None, None))
self.assertEqual(cmdparser.try_num_prefixes("3-look me"),
('3', "look me"))
self.assertEqual(cmdparser.try_num_prefixes("567-look me"),
('567', "look me"))
self.assertEqual(cmdparser.try_num_prefixes("look me"), (None, None))
self.assertEqual(cmdparser.try_num_prefixes("3-look me"), ("3", "look me"))
self.assertEqual(cmdparser.try_num_prefixes("567-look me"), ("567", "look me"))
@override_settings(SEARCH_MULTIMATCH_REGEX=r"(?P<number>[0-9]+)-(?P<name>.*)",
CMD_IGNORE_PREFIXES="@&/+")
@override_settings(
SEARCH_MULTIMATCH_REGEX=r"(?P<number>[0-9]+)-(?P<name>.*)", CMD_IGNORE_PREFIXES="@&/+"
)
def test_cmdparser(self):
a_cmdset = _CmdSetTest()
bcmd = [cmd for cmd in a_cmdset.commands if cmd.key == "test1"][0]
self.assertEqual(cmdparser.cmdparser("test1hello", a_cmdset, None),
[("test1", "hello", bcmd, 5, 0.5, 'test1')])
self.assertEqual(
cmdparser.cmdparser("test1hello", a_cmdset, None),
[("test1", "hello", bcmd, 5, 0.5, "test1")],
)

View file

@ -14,6 +14,7 @@ class ChannelAttributeInline(AttributeInline):
Inline display of Channel Attribute - experimental
"""
model = ChannelDB.db_attributes.through
related_field = "channeldb"
@ -23,6 +24,7 @@ class ChannelTagInline(TagInline):
Inline display of Channel Tags - experimental
"""
model = ChannelDB.db_tags.through
related_field = "channeldb"
@ -32,16 +34,26 @@ class MsgAdmin(admin.ModelAdmin):
Defines display for Msg objects
"""
list_display = ('id', 'db_date_created', 'db_sender', 'db_receivers',
'db_channels', 'db_message', 'db_lock_storage')
list_display = (
"id",
"db_date_created",
"db_sender",
"db_receivers",
"db_channels",
"db_message",
"db_lock_storage",
)
list_display_links = ("id",)
ordering = ["db_date_created", 'db_sender', 'db_receivers', 'db_channels']
#readonly_fields = ['db_message', 'db_sender', 'db_receivers', 'db_channels']
search_fields = ['id', '^db_date_created', '^db_message']
ordering = ["db_date_created", "db_sender", "db_receivers", "db_channels"]
# readonly_fields = ['db_message', 'db_sender', 'db_receivers', 'db_channels']
search_fields = ["id", "^db_date_created", "^db_message"]
save_as = True
save_on_top = True
list_select_related = True
#admin.site.register(Msg, MsgAdmin)
# admin.site.register(Msg, MsgAdmin)
class ChannelAdmin(admin.ModelAdmin):
@ -49,17 +61,28 @@ class ChannelAdmin(admin.ModelAdmin):
Defines display for Channel objects
"""
inlines = [ChannelTagInline, ChannelAttributeInline]
list_display = ('id', 'db_key', 'db_lock_storage', "subscriptions")
list_display_links = ("id", 'db_key')
list_display = ("id", "db_key", "db_lock_storage", "subscriptions")
list_display_links = ("id", "db_key")
ordering = ["db_key"]
search_fields = ['id', 'db_key', 'db_tags__db_key']
search_fields = ["id", "db_key", "db_tags__db_key"]
save_as = True
save_on_top = True
list_select_related = True
raw_id_fields = ('db_object_subscriptions', 'db_account_subscriptions',)
raw_id_fields = ("db_object_subscriptions", "db_account_subscriptions")
fieldsets = (
(None, {'fields': (('db_key',), 'db_lock_storage', 'db_account_subscriptions', 'db_object_subscriptions')}),
(
None,
{
"fields": (
("db_key",),
"db_lock_storage",
"db_account_subscriptions",
"db_object_subscriptions",
)
},
),
)
def subscriptions(self, obj):
@ -93,6 +116,7 @@ class ChannelAdmin(admin.ModelAdmin):
def response_add(self, request, obj, post_url_continue=None):
from django.http import HttpResponseRedirect
from django.urls import reverse
return HttpResponseRedirect(reverse("admin:comms_channeldb_change", args=[obj.id]))

View file

@ -55,6 +55,7 @@ class ChannelCommand(command.Command):
{lower_channelkey}/history 30
"""
# ^note that channeldesc and lower_channelkey will be filled
# automatically by ChannelHandler
@ -106,12 +107,12 @@ class ChannelCommand(command.Command):
string = _("You are not connected to channel '%s'.")
self.msg(string % channelkey)
return
if not channel.access(caller, 'send'):
if not channel.access(caller, "send"):
string = _("You are not permitted to send to channel '%s'.")
self.msg(string % channelkey)
return
if msg == "on":
caller = caller if not hasattr(caller, 'account') else caller.account
caller = caller if not hasattr(caller, "account") else caller.account
unmuted = channel.unmute(caller)
if unmuted:
self.msg("You start listening to %s." % channel)
@ -119,7 +120,7 @@ class ChannelCommand(command.Command):
self.msg("You were already listening to %s." % channel)
return
if msg == "off":
caller = caller if not hasattr(caller, 'account') else caller.account
caller = caller if not hasattr(caller, "account") else caller.account
muted = channel.mute(caller)
if muted:
self.msg("You stop listening to %s." % channel)
@ -130,11 +131,14 @@ class ChannelCommand(command.Command):
# Try to view history
log_file = channel.attributes.get("log_file", default="channel_%s.log" % channel.key)
def send_msg(lines): return self.msg("".join(line.split("[-]", 1)[1]
if "[-]" in line else line for line in lines))
def send_msg(lines):
return self.msg(
"".join(line.split("[-]", 1)[1] if "[-]" in line else line for line in lines)
)
tail_log_file(log_file, self.history_start, 20, callback=send_msg)
else:
caller = caller if not hasattr(caller, 'account') else caller.account
caller = caller if not hasattr(caller, "account") else caller.account
if caller in channel.mutelist:
self.msg("You currently have %s muted." % channel)
return
@ -216,16 +220,19 @@ class ChannelHandler(object):
locks="cmd:all();%s" % channel.locks,
help_category="Channel names",
obj=channel,
is_channel=True)
is_channel=True,
)
# format the help entry
key = channel.key
cmd.__doc__ = cmd.__doc__.format(channelkey=key,
lower_channelkey=key.strip().lower(),
channeldesc=channel.attributes.get(
"desc", default="").strip())
cmd.__doc__ = cmd.__doc__.format(
channelkey=key,
lower_channelkey=key.strip().lower(),
channeldesc=channel.attributes.get("desc", default="").strip(),
)
self._cached_channel_cmds[channel] = cmd
self._cached_channels[key] = channel
self._cached_cmdsets = {}
add_channel = add # legacy alias
def remove(self, channel):
@ -290,12 +297,15 @@ class ChannelHandler(object):
else:
# create a new cmdset holding all viable channels
chan_cmdset = None
chan_cmds = [channelcmd for channel, channelcmd in self._cached_channel_cmds.items()
if channel.subscriptions.has(source_object) and
channelcmd.access(source_object, 'send')]
chan_cmds = [
channelcmd
for channel, channelcmd in self._cached_channel_cmds.items()
if channel.subscriptions.has(source_object)
and channelcmd.access(source_object, "send")
]
if chan_cmds:
chan_cmdset = cmdset.CmdSet()
chan_cmdset.key = 'ChannelCmdSet'
chan_cmdset.key = "ChannelCmdSet"
chan_cmdset.priority = 101
chan_cmdset.duplicates = True
for cmd in chan_cmds:

View file

@ -21,6 +21,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
create different types of communication channels.
"""
objects = ChannelManager()
def at_first_save(self):
@ -105,7 +106,12 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
listening = [ob for ob in subs if ob.is_connected and ob not in muted]
if subs:
# display listening subscribers in bold
string = ", ".join([account.key if account not in listening else "|w%s|n" % account.key for account in subs])
string = ", ".join(
[
account.key if account not in listening else "|w%s|n" % account.key
for account in subs
]
)
else:
string = "<None>"
return string
@ -163,7 +169,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
# check access
if not self.access(subscriber, 'listen'):
if not self.access(subscriber, "listen"):
return False
# pre-join hook
connect = self.pre_join_channel(subscriber)
@ -204,7 +210,14 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
self.post_leave_channel(subscriber)
return True
def access(self, accessing_obj, access_type='listen', default=False, no_superuser_bypass=False, **kwargs):
def access(
self,
accessing_obj,
access_type="listen",
default=False,
no_superuser_bypass=False,
**kwargs,
):
"""
Determines if another object has permission to access.
@ -221,8 +234,12 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
return (bool): Result of lock check.
"""
return self.locks.check(accessing_obj, access_type=access_type,
default=default, no_superuser_bypass=no_superuser_bypass)
return self.locks.check(
accessing_obj,
access_type=access_type,
default=default,
no_superuser_bypass=no_superuser_bypass,
)
@classmethod
def create(cls, key, account=None, *args, **kwargs):
@ -252,16 +269,18 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
errors = []
obj = None
ip = kwargs.pop('ip', '')
ip = kwargs.pop("ip", "")
try:
kwargs['desc'] = kwargs.pop('description', '')
kwargs['typeclass'] = kwargs.get('typeclass', cls)
kwargs["desc"] = kwargs.pop("description", "")
kwargs["typeclass"] = kwargs.get("typeclass", cls)
obj = create.create_channel(key, *args, **kwargs)
# Record creator id and creation IP
if ip: obj.db.creator_ip = ip
if account: obj.db.creator_id = account.id
if ip:
obj.db.creator_ip = ip
if account:
obj.db.creator_id = account.id
except Exception as exc:
errors.append("An error occurred while creating this '%s' object." % key)
@ -278,10 +297,12 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
self.aliases.clear()
super().delete()
from evennia.comms.channelhandler import CHANNELHANDLER
CHANNELHANDLER.update()
def message_transform(self, msgobj, emit=False, prefix=True,
sender_strings=None, external=False, **kwargs):
def message_transform(
self, msgobj, emit=False, prefix=True, sender_strings=None, external=False, **kwargs
):
"""
Generates the formatted string sent to listeners on a channel.
@ -333,16 +354,29 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
try:
# note our addition of the from_channel keyword here. This could be checked
# by a custom account.msg() to treat channel-receives differently.
entity.msg(msgobj.message, from_obj=msgobj.senders, options={"from_channel": self.id})
entity.msg(
msgobj.message, from_obj=msgobj.senders, options={"from_channel": self.id}
)
except AttributeError as e:
logger.log_trace("%s\nCannot send msg to '%s'." % (e, entity))
if msgobj.keep_log:
# log to file
logger.log_file(msgobj.message, self.attributes.get("log_file") or "channel_%s.log" % self.key)
logger.log_file(
msgobj.message, self.attributes.get("log_file") or "channel_%s.log" % self.key
)
def msg(self, msgobj, header=None, senders=None, sender_strings=None,
keep_log=None, online=False, emit=False, external=False):
def msg(
self,
msgobj,
header=None,
senders=None,
sender_strings=None,
keep_log=None,
online=False,
emit=False,
external=False,
):
"""
Send the given message to all accounts connected to channel. Note that
no permission-checking is done here; it is assumed to have been
@ -391,9 +425,9 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
msgobj = self.pre_send_message(msgobj)
if not msgobj:
return False
msgobj = self.message_transform(msgobj, emit=emit,
sender_strings=sender_strings,
external=external)
msgobj = self.message_transform(
msgobj, emit=emit, sender_strings=sender_strings, external=external
)
self.distribute_message(msgobj, online=online)
self.post_send_message(msgobj)
return True
@ -427,7 +461,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
prefix (str): The created channel prefix.
"""
return '' if emit else '[%s] ' % self.key
return "" if emit else "[%s] " % self.key
def format_senders(self, senders=None, **kwargs):
"""
@ -448,8 +482,8 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
if not senders:
return ''
return ', '.join(senders)
return ""
return ", ".join(senders)
def pose_transform(self, msgobj, sender_string, **kwargs):
"""
@ -472,16 +506,16 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
pose = False
message = msgobj.message
message_start = message.lstrip()
if message_start.startswith((':', ';')):
if message_start.startswith((":", ";")):
pose = True
message = message[1:]
if not message.startswith((':', "'", ',')):
if not message.startswith(' '):
message = ' ' + message
if not message.startswith((":", "'", ",")):
if not message.startswith(" "):
message = " " + message
if pose:
return '%s%s' % (sender_string, message)
return "%s%s" % (sender_string, message)
else:
return '%s: %s' % (sender_string, message)
return "%s: %s" % (sender_string, message)
def format_external(self, msgobj, senders, emit=False, **kwargs):
"""
@ -503,7 +537,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
if emit or not senders:
return msgobj.message
senders = ', '.join(senders)
senders = ", ".join(senders)
return self.pose_transform(msgobj, senders)
def format_message(self, msgobj, emit=False, **kwargs):
@ -522,14 +556,14 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
# We don't want to count things like external sources as senders for
# the purpose of constructing the message string.
senders = [sender for sender in msgobj.senders if hasattr(sender, 'key')]
senders = [sender for sender in msgobj.senders if hasattr(sender, "key")]
if not senders:
emit = True
if emit:
return msgobj.message
else:
senders = [sender.key for sender in msgobj.senders]
senders = ', '.join(senders)
senders = ", ".join(senders)
return self.pose_transform(msgobj, senders)
def pre_join_channel(self, joiner, **kwargs):
@ -643,8 +677,9 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
content_type = ContentType.objects.get_for_model(self.__class__)
return reverse("admin:%s_%s_change" % (content_type.app_label,
content_type.model), args=(self.id,))
return reverse(
"admin:%s_%s_change" % (content_type.app_label, content_type.model), args=(self.id,)
)
@classmethod
def web_get_create_url(cls):
@ -673,9 +708,9 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
try:
return reverse('%s-create' % slugify(cls._meta.verbose_name))
return reverse("%s-create" % slugify(cls._meta.verbose_name))
except:
return '#'
return "#"
def web_get_detail_url(self):
"""
@ -704,11 +739,12 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
try:
return reverse('%s-detail' % slugify(self._meta.verbose_name),
kwargs={'slug': slugify(self.db_key)})
return reverse(
"%s-detail" % slugify(self._meta.verbose_name),
kwargs={"slug": slugify(self.db_key)},
)
except:
return '#'
return "#"
def web_get_update_url(self):
"""
@ -737,10 +773,12 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
try:
return reverse('%s-update' % slugify(self._meta.verbose_name),
kwargs={'slug': slugify(self.db_key)})
return reverse(
"%s-update" % slugify(self._meta.verbose_name),
kwargs={"slug": slugify(self.db_key)},
)
except:
return '#'
return "#"
def web_get_delete_url(self):
"""
@ -768,10 +806,12 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
try:
return reverse('%s-delete' % slugify(self._meta.verbose_name),
kwargs={'slug': slugify(self.db_key)})
return reverse(
"%s-delete" % slugify(self._meta.verbose_name),
kwargs={"slug": slugify(self.db_key)},
)
except:
return '#'
return "#"
# Used by Django Sites/Admin
get_absolute_url = web_get_detail_url

View file

@ -6,7 +6,7 @@ Comm system components.
from django.db.models import Q
from evennia.typeclasses.managers import (TypedObjectManager, TypeclassManager)
from evennia.typeclasses.managers import TypedObjectManager, TypeclassManager
from evennia.utils import logger
_GA = object.__getattribute__
@ -22,6 +22,7 @@ class CommError(Exception):
"""
Raised by comm system, to allow feedback to player when caught.
"""
pass
@ -29,6 +30,7 @@ class CommError(Exception):
# helper functions
#
def dbref(inp, reqhash=True):
"""
Valid forms of dbref (database reference number) are either a
@ -46,7 +48,7 @@ def dbref(inp, reqhash=True):
if reqhash and not (isinstance(inp, str) and inp.startswith("#")):
return None
if isinstance(inp, str):
inp = inp.lstrip('#')
inp = inp.lstrip("#")
try:
if int(inp) < 0:
return None
@ -85,7 +87,7 @@ def identify_object(inp):
return inp, None
def to_object(inp, objtype='account'):
def to_object(inp, objtype="account"):
"""
Locates the object related to the given accountname or channel key.
If input was already the correct object, return it.
@ -101,28 +103,28 @@ def to_object(inp, objtype='account'):
obj, typ = identify_object(inp)
if typ == objtype:
return obj
if objtype == 'account':
if typ == 'object':
if objtype == "account":
if typ == "object":
return obj.account
if typ == 'string':
if typ == "string":
return _AccountDB.objects.get(user_username__iexact=obj)
if typ == 'dbref':
if typ == "dbref":
return _AccountDB.objects.get(id=obj)
logger.log_err("%s %s %s %s %s" % (objtype, inp, obj, typ, type(inp)))
raise CommError()
elif objtype == 'object':
if typ == 'account':
elif objtype == "object":
if typ == "account":
return obj.obj
if typ == 'string':
if typ == "string":
return _ObjectDB.objects.get(db_key__iexact=obj)
if typ == 'dbref':
if typ == "dbref":
return _ObjectDB.objects.get(id=obj)
logger.log_err("%s %s %s %s %s" % (objtype, inp, obj, typ, type(inp)))
raise CommError()
elif objtype == 'channel':
if typ == 'string':
elif objtype == "channel":
if typ == "string":
return _ChannelDB.objects.get(db_key__iexact=obj)
if typ == 'dbref':
if typ == "dbref":
return _ChannelDB.objects.get(id=obj)
logger.log_err("%s %s %s %s %s" % (objtype, inp, obj, typ, type(inp)))
raise CommError()
@ -134,6 +136,7 @@ def to_object(inp, objtype='account'):
# Msg manager
#
class MsgManager(TypedObjectManager):
"""
This MsgManager implements methods for searching and manipulating
@ -200,19 +203,25 @@ class MsgManager(TypedObjectManager):
obj, typ = identify_object(sender)
if exclude_channel_messages:
# explicitly exclude channel recipients
if typ == 'account':
return list(self.filter(db_sender_accounts=obj,
db_receivers_channels__isnull=True).exclude(db_hide_from_accounts=obj))
elif typ == 'object':
return list(self.filter(db_sender_objects=obj,
db_receivers_channels__isnull=True).exclude(db_hide_from_objects=obj))
if typ == "account":
return list(
self.filter(db_sender_accounts=obj, db_receivers_channels__isnull=True).exclude(
db_hide_from_accounts=obj
)
)
elif typ == "object":
return list(
self.filter(db_sender_objects=obj, db_receivers_channels__isnull=True).exclude(
db_hide_from_objects=obj
)
)
else:
raise CommError
else:
# get everything, channel or not
if typ == 'account':
if typ == "account":
return list(self.filter(db_sender_accounts=obj).exclude(db_hide_from_accounts=obj))
elif typ == 'object':
elif typ == "object":
return list(self.filter(db_sender_objects=obj).exclude(db_hide_from_objects=obj))
else:
raise CommError
@ -232,11 +241,11 @@ class MsgManager(TypedObjectManager):
"""
obj, typ = identify_object(recipient)
if typ == 'account':
if typ == "account":
return list(self.filter(db_receivers_accounts=obj).exclude(db_hide_from_accounts=obj))
elif typ == 'object':
elif typ == "object":
return list(self.filter(db_receivers_objects=obj).exclude(db_hide_from_objects=obj))
elif typ == 'channel':
elif typ == "channel":
return list(self.filter(db_receivers_channels=obj).exclude(db_hide_from_channels=obj))
else:
raise CommError
@ -287,20 +296,24 @@ class MsgManager(TypedObjectManager):
# filter by sender
sender, styp = identify_object(sender)
if styp == 'account':
if styp == "account":
sender_restrict = Q(db_sender_accounts=sender) & ~Q(db_hide_from_accounts=sender)
elif styp == 'object':
elif styp == "object":
sender_restrict = Q(db_sender_objects=sender) & ~Q(db_hide_from_objects=sender)
else:
sender_restrict = Q()
# filter by receiver
receiver, rtyp = identify_object(receiver)
if rtyp == 'account':
receiver_restrict = Q(db_receivers_accounts=receiver) & ~Q(db_hide_from_accounts=receiver)
elif rtyp == 'object':
if rtyp == "account":
receiver_restrict = Q(db_receivers_accounts=receiver) & ~Q(
db_hide_from_accounts=receiver
)
elif rtyp == "object":
receiver_restrict = Q(db_receivers_objects=receiver) & ~Q(db_hide_from_objects=receiver)
elif rtyp == 'channel':
receiver_restrict = Q(db_receivers_channels=receiver) & ~Q(db_hide_from_channels=receiver)
elif rtyp == "channel":
receiver_restrict = Q(db_receivers_channels=receiver) & ~Q(
db_hide_from_channels=receiver
)
else:
receiver_restrict = Q()
# filter by full text
@ -310,6 +323,7 @@ class MsgManager(TypedObjectManager):
fulltext_restrict = Q()
# execute the query
return list(self.filter(sender_restrict & receiver_restrict & fulltext_restrict))
# back-compatibility alias
message_search = search_message
@ -318,6 +332,7 @@ class MsgManager(TypedObjectManager):
# Channel manager
#
class ChannelDBManager(TypedObjectManager):
"""
This ChannelManager implements methods for searching and
@ -361,9 +376,10 @@ class ChannelDBManager(TypedObjectManager):
return self.get(id=dbref)
except self.model.DoesNotExist:
pass
results = self.filter(Q(db_key__iexact=channelkey) |
Q(db_tags__db_tagtype__iexact="alias",
db_tags__db_key__iexact=channelkey)).distinct()
results = self.filter(
Q(db_key__iexact=channelkey)
| Q(db_tags__db_tagtype__iexact="alias", db_tags__db_key__iexact=channelkey)
).distinct()
return results[0] if results else None
def get_subscriptions(self, subscriber):
@ -401,14 +417,17 @@ class ChannelDBManager(TypedObjectManager):
except self.model.DoesNotExist:
pass
if exact:
channels = self.filter(Q(db_key__iexact=ostring) |
Q(db_tags__db_tagtype__iexact="alias",
db_tags__db_key__iexact=ostring)).distinct()
channels = self.filter(
Q(db_key__iexact=ostring)
| Q(db_tags__db_tagtype__iexact="alias", db_tags__db_key__iexact=ostring)
).distinct()
else:
channels = self.filter(Q(db_key__icontains=ostring) |
Q(db_tags__db_tagtype__iexact="alias",
db_tags__db_key__icontains=ostring)).distinct()
channels = self.filter(
Q(db_key__icontains=ostring)
| Q(db_tags__db_tagtype__iexact="alias", db_tags__db_key__icontains=ostring)
).distinct()
return channels
# back-compatibility alias
channel_search = search_channel
@ -417,4 +436,5 @@ class ChannelManager(ChannelDBManager, TypeclassManager):
"""
Wrapper to group the typeclass manager to a consistent name.
"""
pass

View file

@ -6,39 +6,85 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='ChannelDB',
name="ChannelDB",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('db_key', models.CharField(max_length=255, verbose_name='key', db_index=True)),
('db_typeclass_path', models.CharField(help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.", max_length=255, null=True, verbose_name='typeclass')),
('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
('db_lock_storage', models.TextField(help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.", verbose_name='locks', blank=True)),
(
"id",
models.AutoField(
verbose_name="ID", serialize=False, auto_created=True, primary_key=True
),
),
("db_key", models.CharField(max_length=255, verbose_name="key", db_index=True)),
(
"db_typeclass_path",
models.CharField(
help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.",
max_length=255,
null=True,
verbose_name="typeclass",
),
),
(
"db_date_created",
models.DateTimeField(auto_now_add=True, verbose_name="creation date"),
),
(
"db_lock_storage",
models.TextField(
help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.",
verbose_name="locks",
blank=True,
),
),
],
options={
'verbose_name': 'Channel',
'verbose_name_plural': 'Channels',
},
options={"verbose_name": "Channel", "verbose_name_plural": "Channels"},
bases=(models.Model,),
),
migrations.CreateModel(
name='Msg',
name="Msg",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('db_sender_external', models.CharField(help_text="identifier for external sender, for example a sender over an IRC connection (i.e. someone who doesn't have an exixtence in-game).", max_length=255, null=True, verbose_name='external sender', db_index=True)),
('db_header', models.TextField(null=True, verbose_name='header', blank=True)),
('db_message', models.TextField(verbose_name='messsage')),
('db_date_sent', models.DateTimeField(auto_now_add=True, verbose_name='date sent', db_index=True)),
('db_lock_storage', models.TextField(help_text='access locks on this message.', verbose_name='locks', blank=True)),
('db_hide_from_channels', models.ManyToManyField(related_name='hide_from_channels_set', null=True, to='comms.ChannelDB')),
(
"id",
models.AutoField(
verbose_name="ID", serialize=False, auto_created=True, primary_key=True
),
),
(
"db_sender_external",
models.CharField(
help_text="identifier for external sender, for example a sender over an IRC connection (i.e. someone who doesn't have an exixtence in-game).",
max_length=255,
null=True,
verbose_name="external sender",
db_index=True,
),
),
("db_header", models.TextField(null=True, verbose_name="header", blank=True)),
("db_message", models.TextField(verbose_name="messsage")),
(
"db_date_sent",
models.DateTimeField(
auto_now_add=True, verbose_name="date sent", db_index=True
),
),
(
"db_lock_storage",
models.TextField(
help_text="access locks on this message.", verbose_name="locks", blank=True
),
),
(
"db_hide_from_channels",
models.ManyToManyField(
related_name="hide_from_channels_set", null=True, to="comms.ChannelDB"
),
),
],
options={
'verbose_name': 'Message',
},
options={"verbose_name": "Message"},
bases=(models.Model,),
),
]

View file

@ -6,16 +6,15 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('objects', '0001_initial'),
('comms', '0001_initial'),
]
dependencies = [("objects", "0001_initial"), ("comms", "0001_initial")]
operations = [
migrations.AddField(
model_name='msg',
name='db_hide_from_objects',
field=models.ManyToManyField(related_name='hide_from_objects_set', null=True, to='objects.ObjectDB'),
model_name="msg",
name="db_hide_from_objects",
field=models.ManyToManyField(
related_name="hide_from_objects_set", null=True, to="objects.ObjectDB"
),
preserve_default=True,
),
)
]

View file

@ -8,65 +8,108 @@ from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('objects', '0001_initial'),
("objects", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('typeclasses', '0001_initial'),
('comms', '0002_msg_db_hide_from_objects'),
("typeclasses", "0001_initial"),
("comms", "0002_msg_db_hide_from_objects"),
]
operations = [
migrations.AddField(
model_name='msg',
name='db_hide_from_accounts',
field=models.ManyToManyField(related_name='hide_from_accounts_set', null=True, to=settings.AUTH_USER_MODEL),
model_name="msg",
name="db_hide_from_accounts",
field=models.ManyToManyField(
related_name="hide_from_accounts_set", null=True, to=settings.AUTH_USER_MODEL
),
preserve_default=True,
),
migrations.AddField(
model_name='msg',
name='db_receivers_channels',
field=models.ManyToManyField(help_text='channel recievers', related_name='channel_set', null=True, to='comms.ChannelDB'),
model_name="msg",
name="db_receivers_channels",
field=models.ManyToManyField(
help_text="channel recievers",
related_name="channel_set",
null=True,
to="comms.ChannelDB",
),
preserve_default=True,
),
migrations.AddField(
model_name='msg',
name='db_receivers_objects',
field=models.ManyToManyField(help_text='object receivers', related_name='receiver_object_set', null=True, to='objects.ObjectDB'),
model_name="msg",
name="db_receivers_objects",
field=models.ManyToManyField(
help_text="object receivers",
related_name="receiver_object_set",
null=True,
to="objects.ObjectDB",
),
preserve_default=True,
),
migrations.AddField(
model_name='msg',
name='db_receivers_accounts',
field=models.ManyToManyField(help_text='account receivers', related_name='receiver_account_set', null=True, to=settings.AUTH_USER_MODEL),
model_name="msg",
name="db_receivers_accounts",
field=models.ManyToManyField(
help_text="account receivers",
related_name="receiver_account_set",
null=True,
to=settings.AUTH_USER_MODEL,
),
preserve_default=True,
),
migrations.AddField(
model_name='msg',
name='db_sender_objects',
field=models.ManyToManyField(related_name='sender_object_set', null=True, verbose_name='sender(object)', to='objects.ObjectDB', db_index=True),
model_name="msg",
name="db_sender_objects",
field=models.ManyToManyField(
related_name="sender_object_set",
null=True,
verbose_name="sender(object)",
to="objects.ObjectDB",
db_index=True,
),
preserve_default=True,
),
migrations.AddField(
model_name='msg',
name='db_sender_accounts',
field=models.ManyToManyField(related_name='sender_account_set', null=True, verbose_name='sender(account)', to=settings.AUTH_USER_MODEL, db_index=True),
model_name="msg",
name="db_sender_accounts",
field=models.ManyToManyField(
related_name="sender_account_set",
null=True,
verbose_name="sender(account)",
to=settings.AUTH_USER_MODEL,
db_index=True,
),
preserve_default=True,
),
migrations.AddField(
model_name='channeldb',
name='db_attributes',
field=models.ManyToManyField(help_text='attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute', null=True),
model_name="channeldb",
name="db_attributes",
field=models.ManyToManyField(
help_text="attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).",
to="typeclasses.Attribute",
null=True,
),
preserve_default=True,
),
migrations.AddField(
model_name='channeldb',
name='db_subscriptions',
field=models.ManyToManyField(related_name='subscription_set', null=True, verbose_name='subscriptions', to=settings.AUTH_USER_MODEL, db_index=True),
model_name="channeldb",
name="db_subscriptions",
field=models.ManyToManyField(
related_name="subscription_set",
null=True,
verbose_name="subscriptions",
to=settings.AUTH_USER_MODEL,
db_index=True,
),
preserve_default=True,
),
migrations.AddField(
model_name='channeldb',
name='db_tags',
field=models.ManyToManyField(help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag', null=True),
model_name="channeldb",
name="db_tags",
field=models.ManyToManyField(
help_text="tags on this object. Tags are simple string markers to identify, group and alias objects.",
to="typeclasses.Tag",
null=True,
),
preserve_default=True,
),
]

View file

@ -13,10 +13,6 @@ def convert_defaults(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('comms', '0003_auto_20140917_0756'),
]
dependencies = [("comms", "0003_auto_20140917_0756")]
operations = [
migrations.RunPython(convert_defaults),
]
operations = [migrations.RunPython(convert_defaults)]

View file

@ -17,10 +17,6 @@ def convert_channelnames(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('comms', '0004_auto_20150118_1631'),
]
dependencies = [("comms", "0004_auto_20150118_1631")]
operations = [
migrations.RunPython(convert_channelnames),
]
operations = [migrations.RunPython(convert_channelnames)]

View file

@ -6,16 +6,19 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('objects', '0004_auto_20150118_1622'),
('comms', '0005_auto_20150223_1517'),
]
dependencies = [("objects", "0004_auto_20150118_1622"), ("comms", "0005_auto_20150223_1517")]
operations = [
migrations.AddField(
model_name='channeldb',
name='db_object_subscriptions',
field=models.ManyToManyField(related_name='object_subscription_set', null=True, verbose_name='subscriptions', to='objects.ObjectDB', db_index=True),
model_name="channeldb",
name="db_object_subscriptions",
field=models.ManyToManyField(
related_name="object_subscription_set",
null=True,
verbose_name="subscriptions",
to="objects.ObjectDB",
db_index=True,
),
preserve_default=True,
),
)
]

View file

@ -7,14 +7,18 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('typeclasses', '0004_auto_20151101_1759'),
('comms', '0006_channeldb_db_object_subscriptions'),
("typeclasses", "0004_auto_20151101_1759"),
("comms", "0006_channeldb_db_object_subscriptions"),
]
operations = [
migrations.AddField(
model_name='msg',
name='db_tags',
field=models.ManyToManyField(help_text='tags on this message. Tags are simple string markers to identify, group and alias messages.', to='typeclasses.Tag', null=True),
),
model_name="msg",
name="db_tags",
field=models.ManyToManyField(
help_text="tags on this message. Tags are simple string markers to identify, group and alias messages.",
to="typeclasses.Tag",
null=True,
),
)
]

View file

@ -7,18 +7,11 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('comms', '0007_msg_db_tags'),
]
dependencies = [("comms", "0007_msg_db_tags")]
operations = [
migrations.AlterModelOptions(
name='msg',
options={'verbose_name': 'Msg'},
),
migrations.AlterModelOptions(name="msg", options={"verbose_name": "Msg"}),
migrations.RenameField(
model_name='msg',
old_name='db_date_sent',
new_name='db_date_created',
model_name="msg", old_name="db_date_sent", new_name="db_date_created"
),
]

View file

@ -8,59 +8,110 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comms', '0008_auto_20160905_0902'),
]
dependencies = [("comms", "0008_auto_20160905_0902")]
operations = [
migrations.AlterField(
model_name='msg',
name='db_hide_from_channels',
field=models.ManyToManyField(blank=True, null=True, related_name='hide_from_channels_set', to='comms.ChannelDB'),
model_name="msg",
name="db_hide_from_channels",
field=models.ManyToManyField(
blank=True, null=True, related_name="hide_from_channels_set", to="comms.ChannelDB"
),
),
migrations.AlterField(
model_name='msg',
name='db_hide_from_objects',
field=models.ManyToManyField(blank=True, null=True, related_name='hide_from_objects_set', to='objects.ObjectDB'),
model_name="msg",
name="db_hide_from_objects",
field=models.ManyToManyField(
blank=True, null=True, related_name="hide_from_objects_set", to="objects.ObjectDB"
),
),
migrations.AlterField(
model_name='msg',
name='db_hide_from_accounts',
field=models.ManyToManyField(blank=True, null=True, related_name='hide_from_accounts_set', to=settings.AUTH_USER_MODEL),
model_name="msg",
name="db_hide_from_accounts",
field=models.ManyToManyField(
blank=True,
null=True,
related_name="hide_from_accounts_set",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_channels',
field=models.ManyToManyField(blank=True, help_text='channel recievers', null=True, related_name='channel_set', to='comms.ChannelDB'),
model_name="msg",
name="db_receivers_channels",
field=models.ManyToManyField(
blank=True,
help_text="channel recievers",
null=True,
related_name="channel_set",
to="comms.ChannelDB",
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_objects',
field=models.ManyToManyField(blank=True, help_text='object receivers', null=True, related_name='receiver_object_set', to='objects.ObjectDB'),
model_name="msg",
name="db_receivers_objects",
field=models.ManyToManyField(
blank=True,
help_text="object receivers",
null=True,
related_name="receiver_object_set",
to="objects.ObjectDB",
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_accounts',
field=models.ManyToManyField(blank=True, help_text='account receivers', null=True, related_name='receiver_account_set', to=settings.AUTH_USER_MODEL),
model_name="msg",
name="db_receivers_accounts",
field=models.ManyToManyField(
blank=True,
help_text="account receivers",
null=True,
related_name="receiver_account_set",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_external',
field=models.CharField(blank=True, db_index=True, help_text="identifier for external sender, for example a sender over an IRC connection (i.e. someone who doesn't have an exixtence in-game).", max_length=255, null=True, verbose_name='external sender'),
model_name="msg",
name="db_sender_external",
field=models.CharField(
blank=True,
db_index=True,
help_text="identifier for external sender, for example a sender over an IRC connection (i.e. someone who doesn't have an exixtence in-game).",
max_length=255,
null=True,
verbose_name="external sender",
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_objects',
field=models.ManyToManyField(blank=True, db_index=True, null=True, related_name='sender_object_set', to='objects.ObjectDB', verbose_name='sender(object)'),
model_name="msg",
name="db_sender_objects",
field=models.ManyToManyField(
blank=True,
db_index=True,
null=True,
related_name="sender_object_set",
to="objects.ObjectDB",
verbose_name="sender(object)",
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_accounts',
field=models.ManyToManyField(blank=True, db_index=True, null=True, related_name='sender_account_set', to=settings.AUTH_USER_MODEL, verbose_name='sender(account)'),
model_name="msg",
name="db_sender_accounts",
field=models.ManyToManyField(
blank=True,
db_index=True,
null=True,
related_name="sender_account_set",
to=settings.AUTH_USER_MODEL,
verbose_name="sender(account)",
),
),
migrations.AlterField(
model_name='msg',
name='db_tags',
field=models.ManyToManyField(blank=True, help_text='tags on this message. Tags are simple string markers to identify, group and alias messages.', null=True, to='typeclasses.Tag'),
model_name="msg",
name="db_tags",
field=models.ManyToManyField(
blank=True,
help_text="tags on this message. Tags are simple string markers to identify, group and alias messages.",
null=True,
to="typeclasses.Tag",
),
),
]

View file

@ -8,19 +8,31 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comms', '0009_auto_20160921_1731'),
]
dependencies = [("comms", "0009_auto_20160921_1731")]
operations = [
migrations.AlterField(
model_name='channeldb',
name='db_object_subscriptions',
field=models.ManyToManyField(blank=True, db_index=True, null=True, related_name='object_subscription_set', to='objects.ObjectDB', verbose_name='subscriptions'),
model_name="channeldb",
name="db_object_subscriptions",
field=models.ManyToManyField(
blank=True,
db_index=True,
null=True,
related_name="object_subscription_set",
to="objects.ObjectDB",
verbose_name="subscriptions",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_subscriptions',
field=models.ManyToManyField(blank=True, db_index=True, null=True, related_name='subscription_set', to=settings.AUTH_USER_MODEL, verbose_name='subscriptions'),
model_name="channeldb",
name="db_subscriptions",
field=models.ManyToManyField(
blank=True,
db_index=True,
null=True,
related_name="subscription_set",
to=settings.AUTH_USER_MODEL,
verbose_name="subscriptions",
),
),
]

View file

@ -7,20 +7,30 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scripts', '0007_auto_20150403_2339'),
('comms', '0010_auto_20161206_1912'),
]
dependencies = [("scripts", "0007_auto_20150403_2339"), ("comms", "0010_auto_20161206_1912")]
operations = [
migrations.AddField(
model_name='msg',
name='db_receivers_scripts',
field=models.ManyToManyField(blank=True, help_text='script_receivers', null=True, related_name='receiver_script_set', to='scripts.ScriptDB'),
model_name="msg",
name="db_receivers_scripts",
field=models.ManyToManyField(
blank=True,
help_text="script_receivers",
null=True,
related_name="receiver_script_set",
to="scripts.ScriptDB",
),
),
migrations.AddField(
model_name='msg',
name='db_sender_scripts',
field=models.ManyToManyField(blank=True, db_index=True, null=True, related_name='sender_script_set', to='scripts.ScriptDB', verbose_name='sender(script)'),
model_name="msg",
name="db_sender_scripts",
field=models.ManyToManyField(
blank=True,
db_index=True,
null=True,
related_name="sender_script_set",
to="scripts.ScriptDB",
verbose_name="sender(script)",
),
),
]

View file

@ -8,74 +8,127 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comms', '0010_auto_20161206_1912'),
]
dependencies = [("comms", "0010_auto_20161206_1912")]
operations = [
migrations.AlterField(
model_name='channeldb',
name='db_attributes',
field=models.ManyToManyField(help_text='attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'),
model_name="channeldb",
name="db_attributes",
field=models.ManyToManyField(
help_text="attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).",
to="typeclasses.Attribute",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_object_subscriptions',
field=models.ManyToManyField(blank=True, db_index=True, related_name='object_subscription_set', to='objects.ObjectDB', verbose_name='subscriptions'),
model_name="channeldb",
name="db_object_subscriptions",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="object_subscription_set",
to="objects.ObjectDB",
verbose_name="subscriptions",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_subscriptions',
field=models.ManyToManyField(blank=True, db_index=True, related_name='subscription_set', to=settings.AUTH_USER_MODEL, verbose_name='subscriptions'),
model_name="channeldb",
name="db_subscriptions",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="subscription_set",
to=settings.AUTH_USER_MODEL,
verbose_name="subscriptions",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_tags',
field=models.ManyToManyField(help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
model_name="channeldb",
name="db_tags",
field=models.ManyToManyField(
help_text="tags on this object. Tags are simple string markers to identify, group and alias objects.",
to="typeclasses.Tag",
),
),
migrations.AlterField(
model_name='msg',
name='db_hide_from_channels',
field=models.ManyToManyField(blank=True, related_name='hide_from_channels_set', to='comms.ChannelDB'),
model_name="msg",
name="db_hide_from_channels",
field=models.ManyToManyField(
blank=True, related_name="hide_from_channels_set", to="comms.ChannelDB"
),
),
migrations.AlterField(
model_name='msg',
name='db_hide_from_objects',
field=models.ManyToManyField(blank=True, related_name='hide_from_objects_set', to='objects.ObjectDB'),
model_name="msg",
name="db_hide_from_objects",
field=models.ManyToManyField(
blank=True, related_name="hide_from_objects_set", to="objects.ObjectDB"
),
),
migrations.AlterField(
model_name='msg',
name='db_hide_from_accounts',
field=models.ManyToManyField(blank=True, related_name='hide_from_accounts_set', to=settings.AUTH_USER_MODEL),
model_name="msg",
name="db_hide_from_accounts",
field=models.ManyToManyField(
blank=True, related_name="hide_from_accounts_set", to=settings.AUTH_USER_MODEL
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_channels',
field=models.ManyToManyField(blank=True, help_text='channel recievers', related_name='channel_set', to='comms.ChannelDB'),
model_name="msg",
name="db_receivers_channels",
field=models.ManyToManyField(
blank=True,
help_text="channel recievers",
related_name="channel_set",
to="comms.ChannelDB",
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_objects',
field=models.ManyToManyField(blank=True, help_text='object receivers', related_name='receiver_object_set', to='objects.ObjectDB'),
model_name="msg",
name="db_receivers_objects",
field=models.ManyToManyField(
blank=True,
help_text="object receivers",
related_name="receiver_object_set",
to="objects.ObjectDB",
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_accounts',
field=models.ManyToManyField(blank=True, help_text='account receivers', related_name='receiver_account_set', to=settings.AUTH_USER_MODEL),
model_name="msg",
name="db_receivers_accounts",
field=models.ManyToManyField(
blank=True,
help_text="account receivers",
related_name="receiver_account_set",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_objects',
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_object_set', to='objects.ObjectDB', verbose_name='sender(object)'),
model_name="msg",
name="db_sender_objects",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="sender_object_set",
to="objects.ObjectDB",
verbose_name="sender(object)",
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_accounts',
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_account_set', to=settings.AUTH_USER_MODEL, verbose_name='sender(account)'),
model_name="msg",
name="db_sender_accounts",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="sender_account_set",
to=settings.AUTH_USER_MODEL,
verbose_name="sender(account)",
),
),
migrations.AlterField(
model_name='msg',
name='db_tags',
field=models.ManyToManyField(blank=True, help_text='tags on this message. Tags are simple string markers to identify, group and alias messages.', to='typeclasses.Tag'),
model_name="msg",
name="db_tags",
field=models.ManyToManyField(
blank=True,
help_text="tags on this message. Tags are simple string markers to identify, group and alias messages.",
to="typeclasses.Tag",
),
),
]

View file

@ -7,10 +7,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('comms', '0011_auto_20170606_1731'),
('comms', '0011_auto_20170217_2039'),
]
dependencies = [("comms", "0011_auto_20170606_1731"), ("comms", "0011_auto_20170217_2039")]
operations = [
]
operations = []

View file

@ -13,72 +13,131 @@ def _table_exists(db_cursor, tablename):
class Migration(migrations.Migration):
dependencies = [
('accounts', '0007_copy_player_to_account'),
('comms', '0012_merge_20170617_2017'),
("accounts", "0007_copy_player_to_account"),
("comms", "0012_merge_20170617_2017"),
]
db_cursor = connection.cursor()
operations = [
migrations.AddField(
model_name='channeldb',
name='db_account_subscriptions',
field=models.ManyToManyField(blank=True, db_index=True, related_name='account_subscription_set', to='accounts.AccountDB', verbose_name='account subscriptions'),
model_name="channeldb",
name="db_account_subscriptions",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="account_subscription_set",
to="accounts.AccountDB",
verbose_name="account subscriptions",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_object_subscriptions',
field=models.ManyToManyField(blank=True, db_index=True, related_name='object_subscription_set', to='objects.ObjectDB', verbose_name='object subscriptions'),
model_name="channeldb",
name="db_object_subscriptions",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="object_subscription_set",
to="objects.ObjectDB",
verbose_name="object subscriptions",
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_scripts',
field=models.ManyToManyField(blank=True, help_text='script_receivers', related_name='receiver_script_set', to='scripts.ScriptDB'),
model_name="msg",
name="db_receivers_scripts",
field=models.ManyToManyField(
blank=True,
help_text="script_receivers",
related_name="receiver_script_set",
to="scripts.ScriptDB",
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_scripts',
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_script_set', to='scripts.ScriptDB', verbose_name='sender(script)'),
model_name="msg",
name="db_sender_scripts",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="sender_script_set",
to="scripts.ScriptDB",
verbose_name="sender(script)",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_object_subscriptions',
field=models.ManyToManyField(blank=True, db_index=True, related_name='object_subscription_set', to='objects.ObjectDB', verbose_name='object subscriptions'),
model_name="channeldb",
name="db_object_subscriptions",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="object_subscription_set",
to="objects.ObjectDB",
verbose_name="object subscriptions",
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_scripts',
field=models.ManyToManyField(blank=True, help_text='script_receivers', related_name='receiver_script_set', to='scripts.ScriptDB'),
model_name="msg",
name="db_receivers_scripts",
field=models.ManyToManyField(
blank=True,
help_text="script_receivers",
related_name="receiver_script_set",
to="scripts.ScriptDB",
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_scripts',
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_script_set', to='scripts.ScriptDB', verbose_name='sender(script)'),
model_name="msg",
name="db_sender_scripts",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="sender_script_set",
to="scripts.ScriptDB",
verbose_name="sender(script)",
),
),
]
if _table_exists(db_cursor, 'comms_msg_db_hide_from_players'):
if _table_exists(db_cursor, "comms_msg_db_hide_from_players"):
# OBS - this is run BEFORE migrations are run!
# not a migration of an existing database
operations += [
migrations.AddField(
model_name='channeldb',
name='db_account_subscriptions',
field=models.ManyToManyField(blank=True, db_index=True, related_name='account_subscription_set', to='accounts.AccountDB', verbose_name='account subscriptions'),
model_name="channeldb",
name="db_account_subscriptions",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="account_subscription_set",
to="accounts.AccountDB",
verbose_name="account subscriptions",
),
),
migrations.AddField(
model_name='msg',
name='db_hide_from_accounts',
field=models.ManyToManyField(blank=True, related_name='hide_from_accounts_set', to='accounts.AccountDB'),
model_name="msg",
name="db_hide_from_accounts",
field=models.ManyToManyField(
blank=True, related_name="hide_from_accounts_set", to="accounts.AccountDB"
),
),
migrations.AddField(
model_name='msg',
name='db_receivers_accounts',
field=models.ManyToManyField(blank=True, help_text='account receivers', related_name='receiver_account_set', to='accounts.AccountDB'),
model_name="msg",
name="db_receivers_accounts",
field=models.ManyToManyField(
blank=True,
help_text="account receivers",
related_name="receiver_account_set",
to="accounts.AccountDB",
),
),
migrations.AddField(
model_name='msg',
name='db_sender_accounts',
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_account_set', to='accounts.AccountDB', verbose_name='sender(account)'),
model_name="msg",
name="db_sender_accounts",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="sender_account_set",
to="accounts.AccountDB",
verbose_name="sender(account)",
),
),
]

View file

@ -8,15 +8,16 @@ from django.db import migrations
# the player->account transition. Now it will do nothing since players.PlayerDB
# no longer exists.
def forwards(apps, schema_editor):
try:
apps.get_model('players', 'PlayerDB')
apps.get_model("players", "PlayerDB")
except LookupError:
return
AccountDB = apps.get_model('accounts', 'AccountDB')
AccountDB = apps.get_model("accounts", "AccountDB")
Msg = apps.get_model('comms', 'Msg')
Msg = apps.get_model("comms", "Msg")
for msg in Msg.objects.all():
for player in msg.db_sender_players.all():
account = AccountDB.objects.get(id=player.id)
@ -28,7 +29,7 @@ def forwards(apps, schema_editor):
account = AccountDB.objects.get(id=player.id)
msg.db_hide_from_accounts.add(account)
ChannelDB = apps.get_model('comms', 'ChannelDB')
ChannelDB = apps.get_model("comms", "ChannelDB")
for channel in ChannelDB.objects.all():
for player in channel.db_subscriptions.all():
account = AccountDB.objects.get(id=player.id)
@ -37,10 +38,6 @@ def forwards(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('comms', '0013_auto_20170705_1726'),
]
dependencies = [("comms", "0013_auto_20170705_1726")]
operations = [
migrations.RunPython(forwards)
]
operations = [migrations.RunPython(forwards)]

View file

@ -12,9 +12,7 @@ def _table_exists(db_cursor, tablename):
class Migration(migrations.Migration):
dependencies = [
('comms', '0014_auto_20170705_1736'),
]
dependencies = [("comms", "0014_auto_20170705_1736")]
db_cursor = connection.cursor()
@ -24,15 +22,9 @@ class Migration(migrations.Migration):
else:
operations = [
migrations.RemoveField(
model_name='channeldb',
name='db_subscriptions', # this is now db_account_subscriptions
),
migrations.RemoveField(
model_name='msg',
name='db_receivers_players',
),
migrations.RemoveField(
model_name='msg',
name='db_sender_players',
model_name="channeldb",
name="db_subscriptions", # this is now db_account_subscriptions
),
migrations.RemoveField(model_name="msg", name="db_receivers_players"),
migrations.RemoveField(model_name="msg", name="db_sender_players"),
]

View file

@ -7,18 +7,11 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comms', '0015_auto_20170706_2041'),
]
dependencies = [("comms", "0015_auto_20170706_2041")]
operations = [
migrations.RemoveField(
model_name='channeldb',
name='db_subscriptions',
),
migrations.RemoveField(model_name="channeldb", name="db_subscriptions"),
migrations.AlterField(
model_name='msg',
name='db_message',
field=models.TextField(verbose_name='message'),
model_name="msg", name="db_message", field=models.TextField(verbose_name="message")
),
]

View file

@ -7,114 +7,188 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comms', '0016_auto_20180925_1735'),
]
dependencies = [("comms", "0016_auto_20180925_1735")]
operations = [
migrations.AlterField(
model_name='channeldb',
name='db_account_subscriptions',
field=models.ManyToManyField(blank=True, db_index=True, related_name='account_subscription_set', to=settings.AUTH_USER_MODEL, verbose_name='account subscriptions'),
model_name="channeldb",
name="db_account_subscriptions",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="account_subscription_set",
to=settings.AUTH_USER_MODEL,
verbose_name="account subscriptions",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_attributes',
field=models.ManyToManyField(help_text='attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'),
model_name="channeldb",
name="db_attributes",
field=models.ManyToManyField(
help_text="attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).",
to="typeclasses.Attribute",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_date_created',
field=models.DateTimeField(auto_now_add=True, verbose_name='creation date'),
model_name="channeldb",
name="db_date_created",
field=models.DateTimeField(auto_now_add=True, verbose_name="creation date"),
),
migrations.AlterField(
model_name='channeldb',
name='db_key',
field=models.CharField(db_index=True, max_length=255, verbose_name='key'),
model_name="channeldb",
name="db_key",
field=models.CharField(db_index=True, max_length=255, verbose_name="key"),
),
migrations.AlterField(
model_name='channeldb',
name='db_lock_storage',
field=models.TextField(blank=True, help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.", verbose_name='locks'),
model_name="channeldb",
name="db_lock_storage",
field=models.TextField(
blank=True,
help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.",
verbose_name="locks",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_object_subscriptions',
field=models.ManyToManyField(blank=True, db_index=True, related_name='object_subscription_set', to='objects.ObjectDB', verbose_name='object subscriptions'),
model_name="channeldb",
name="db_object_subscriptions",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="object_subscription_set",
to="objects.ObjectDB",
verbose_name="object subscriptions",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_tags',
field=models.ManyToManyField(help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
model_name="channeldb",
name="db_tags",
field=models.ManyToManyField(
help_text="tags on this object. Tags are simple string markers to identify, group and alias objects.",
to="typeclasses.Tag",
),
),
migrations.AlterField(
model_name='channeldb',
name='db_typeclass_path',
field=models.CharField(help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.", max_length=255, null=True, verbose_name='typeclass'),
model_name="channeldb",
name="db_typeclass_path",
field=models.CharField(
help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.",
max_length=255,
null=True,
verbose_name="typeclass",
),
),
migrations.AlterField(
model_name='msg',
name='db_date_created',
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='date sent'),
model_name="msg",
name="db_date_created",
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="date sent"),
),
migrations.AlterField(
model_name='msg',
name='db_header',
field=models.TextField(blank=True, null=True, verbose_name='header'),
model_name="msg",
name="db_header",
field=models.TextField(blank=True, null=True, verbose_name="header"),
),
migrations.AlterField(
model_name='msg',
name='db_lock_storage',
field=models.TextField(blank=True, help_text='access locks on this message.', verbose_name='locks'),
model_name="msg",
name="db_lock_storage",
field=models.TextField(
blank=True, help_text="access locks on this message.", verbose_name="locks"
),
),
migrations.AlterField(
model_name='msg',
name='db_message',
field=models.TextField(verbose_name='message'),
model_name="msg", name="db_message", field=models.TextField(verbose_name="message")
),
migrations.AlterField(
model_name='msg',
name='db_receivers_accounts',
field=models.ManyToManyField(blank=True, help_text='account receivers', related_name='receiver_account_set', to=settings.AUTH_USER_MODEL),
model_name="msg",
name="db_receivers_accounts",
field=models.ManyToManyField(
blank=True,
help_text="account receivers",
related_name="receiver_account_set",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_channels',
field=models.ManyToManyField(blank=True, help_text='channel recievers', related_name='channel_set', to='comms.ChannelDB'),
model_name="msg",
name="db_receivers_channels",
field=models.ManyToManyField(
blank=True,
help_text="channel recievers",
related_name="channel_set",
to="comms.ChannelDB",
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_objects',
field=models.ManyToManyField(blank=True, help_text='object receivers', related_name='receiver_object_set', to='objects.ObjectDB'),
model_name="msg",
name="db_receivers_objects",
field=models.ManyToManyField(
blank=True,
help_text="object receivers",
related_name="receiver_object_set",
to="objects.ObjectDB",
),
),
migrations.AlterField(
model_name='msg',
name='db_receivers_scripts',
field=models.ManyToManyField(blank=True, help_text='script_receivers', related_name='receiver_script_set', to='scripts.ScriptDB'),
model_name="msg",
name="db_receivers_scripts",
field=models.ManyToManyField(
blank=True,
help_text="script_receivers",
related_name="receiver_script_set",
to="scripts.ScriptDB",
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_accounts',
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_account_set', to=settings.AUTH_USER_MODEL, verbose_name='sender(account)'),
model_name="msg",
name="db_sender_accounts",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="sender_account_set",
to=settings.AUTH_USER_MODEL,
verbose_name="sender(account)",
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_external',
field=models.CharField(blank=True, db_index=True, help_text="identifier for external sender, for example a sender over an IRC connection (i.e. someone who doesn't have an exixtence in-game).", max_length=255, null=True, verbose_name='external sender'),
model_name="msg",
name="db_sender_external",
field=models.CharField(
blank=True,
db_index=True,
help_text="identifier for external sender, for example a sender over an IRC connection (i.e. someone who doesn't have an exixtence in-game).",
max_length=255,
null=True,
verbose_name="external sender",
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_objects',
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_object_set', to='objects.ObjectDB', verbose_name='sender(object)'),
model_name="msg",
name="db_sender_objects",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="sender_object_set",
to="objects.ObjectDB",
verbose_name="sender(object)",
),
),
migrations.AlterField(
model_name='msg',
name='db_sender_scripts',
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_script_set', to='scripts.ScriptDB', verbose_name='sender(script)'),
model_name="msg",
name="db_sender_scripts",
field=models.ManyToManyField(
blank=True,
db_index=True,
related_name="sender_script_set",
to="scripts.ScriptDB",
verbose_name="sender(script)",
),
),
migrations.AlterField(
model_name='msg',
name='db_tags',
field=models.ManyToManyField(blank=True, help_text='tags on this message. Tags are simple string markers to identify, group and alias messages.', to='typeclasses.Tag'),
model_name="msg",
name="db_tags",
field=models.ManyToManyField(
blank=True,
help_text="tags on this message. Tags are simple string markers to identify, group and alias messages.",
to="typeclasses.Tag",
),
),
]

View file

@ -37,11 +37,12 @@ _DA = object.__delattr__
_CHANNELHANDLER = None
#------------------------------------------------------------
# ------------------------------------------------------------
#
# Msg
#
#------------------------------------------------------------
# ------------------------------------------------------------
class Msg(SharedMemoryModel):
"""
@ -68,6 +69,7 @@ class Msg(SharedMemoryModel):
- db_lock_storage: Internal storage of lock strings.
"""
#
# Msg database model setup
#
@ -78,48 +80,94 @@ class Msg(SharedMemoryModel):
# Sender is either an account, an object or an external sender, like
# an IRC channel; normally there is only one, but if co-modification of
# a message is allowed, there may be more than one "author"
db_sender_accounts = models.ManyToManyField("accounts.AccountDB", related_name='sender_account_set',
blank=True, verbose_name='sender(account)', db_index=True)
db_sender_accounts = models.ManyToManyField(
"accounts.AccountDB",
related_name="sender_account_set",
blank=True,
verbose_name="sender(account)",
db_index=True,
)
db_sender_objects = models.ManyToManyField("objects.ObjectDB", related_name='sender_object_set',
blank=True, verbose_name='sender(object)', db_index=True)
db_sender_scripts = models.ManyToManyField("scripts.ScriptDB", related_name='sender_script_set',
blank=True, verbose_name='sender(script)', db_index=True)
db_sender_external = models.CharField('external sender', max_length=255, null=True, blank=True, db_index=True,
help_text="identifier for external sender, for example a sender over an "
"IRC connection (i.e. someone who doesn't have an exixtence in-game).")
db_sender_objects = models.ManyToManyField(
"objects.ObjectDB",
related_name="sender_object_set",
blank=True,
verbose_name="sender(object)",
db_index=True,
)
db_sender_scripts = models.ManyToManyField(
"scripts.ScriptDB",
related_name="sender_script_set",
blank=True,
verbose_name="sender(script)",
db_index=True,
)
db_sender_external = models.CharField(
"external sender",
max_length=255,
null=True,
blank=True,
db_index=True,
help_text="identifier for external sender, for example a sender over an "
"IRC connection (i.e. someone who doesn't have an exixtence in-game).",
)
# The destination objects of this message. Stored as a
# comma-separated string of object dbrefs. Can be defined along
# with channels below.
db_receivers_accounts = models.ManyToManyField('accounts.AccountDB', related_name='receiver_account_set',
blank=True, help_text="account receivers")
db_receivers_accounts = models.ManyToManyField(
"accounts.AccountDB",
related_name="receiver_account_set",
blank=True,
help_text="account receivers",
)
db_receivers_objects = models.ManyToManyField('objects.ObjectDB', related_name='receiver_object_set',
blank=True, help_text="object receivers")
db_receivers_scripts = models.ManyToManyField('scripts.ScriptDB', related_name='receiver_script_set',
blank=True, help_text="script_receivers")
db_receivers_channels = models.ManyToManyField("ChannelDB", related_name='channel_set',
blank=True, help_text="channel recievers")
db_receivers_objects = models.ManyToManyField(
"objects.ObjectDB",
related_name="receiver_object_set",
blank=True,
help_text="object receivers",
)
db_receivers_scripts = models.ManyToManyField(
"scripts.ScriptDB",
related_name="receiver_script_set",
blank=True,
help_text="script_receivers",
)
db_receivers_channels = models.ManyToManyField(
"ChannelDB", related_name="channel_set", blank=True, help_text="channel recievers"
)
# header could be used for meta-info about the message if your system needs
# it, or as a separate store for the mail subject line maybe.
db_header = models.TextField('header', null=True, blank=True)
db_header = models.TextField("header", null=True, blank=True)
# the message body itself
db_message = models.TextField('message')
db_message = models.TextField("message")
# send date
db_date_created = models.DateTimeField('date sent', editable=False, auto_now_add=True, db_index=True)
db_date_created = models.DateTimeField(
"date sent", editable=False, auto_now_add=True, db_index=True
)
# lock storage
db_lock_storage = models.TextField('locks', blank=True,
help_text='access locks on this message.')
db_lock_storage = models.TextField(
"locks", blank=True, help_text="access locks on this message."
)
# these can be used to filter/hide a given message from supplied objects/accounts/channels
db_hide_from_accounts = models.ManyToManyField("accounts.AccountDB", related_name='hide_from_accounts_set', blank=True)
db_hide_from_accounts = models.ManyToManyField(
"accounts.AccountDB", related_name="hide_from_accounts_set", blank=True
)
db_hide_from_objects = models.ManyToManyField("objects.ObjectDB", related_name='hide_from_objects_set', blank=True)
db_hide_from_channels = models.ManyToManyField("ChannelDB", related_name='hide_from_channels_set', blank=True)
db_hide_from_objects = models.ManyToManyField(
"objects.ObjectDB", related_name="hide_from_objects_set", blank=True
)
db_hide_from_channels = models.ManyToManyField(
"ChannelDB", related_name="hide_from_channels_set", blank=True
)
db_tags = models.ManyToManyField(Tag, blank=True,
help_text='tags on this message. Tags are simple string markers to identify, group and alias messages.')
db_tags = models.ManyToManyField(
Tag,
blank=True,
help_text="tags on this message. Tags are simple string markers to identify, group and alias messages.",
)
# Database manager
objects = managers.MsgManager()
@ -150,15 +198,17 @@ class Msg(SharedMemoryModel):
# is the object in question).
# sender property (wraps db_sender_*)
#@property
# @property
def __senders_get(self):
"Getter. Allows for value = self.sender"
return list(self.db_sender_accounts.all()) + \
list(self.db_sender_objects.all()) + \
list(self.db_sender_scripts.all()) + \
self.extra_senders
return (
list(self.db_sender_accounts.all())
+ list(self.db_sender_objects.all())
+ list(self.db_sender_scripts.all())
+ self.extra_senders
)
#@sender.setter
# @sender.setter
def __senders_set(self, senders):
"Setter. Allows for self.sender = value"
for sender in make_iter(senders):
@ -179,7 +229,7 @@ class Msg(SharedMemoryModel):
elif clsname == "ScriptDB":
self.db_sender_scripts.add(sender)
#@sender.deleter
# @sender.deleter
def __senders_del(self):
"Deleter. Clears all senders"
self.db_sender_accounts.clear()
@ -188,6 +238,7 @@ class Msg(SharedMemoryModel):
self.db_sender_external = ""
self.extra_senders = []
self.save()
senders = property(__senders_get, __senders_set, __senders_del)
def remove_sender(self, senders):
@ -215,18 +266,20 @@ class Msg(SharedMemoryModel):
self.db_sender_accounts.remove(sender)
# receivers property
#@property
# @property
def __receivers_get(self):
"""
Getter. Allows for value = self.receivers.
Returns four lists of receivers: accounts, objects, scripts and channels.
"""
return list(self.db_receivers_accounts.all()) + \
list(self.db_receivers_objects.all()) + \
list(self.db_receivers_scripts.all()) + \
list(self.db_receivers_channels.all())
return (
list(self.db_receivers_accounts.all())
+ list(self.db_receivers_objects.all())
+ list(self.db_receivers_scripts.all())
+ list(self.db_receivers_channels.all())
)
#@receivers.setter
# @receivers.setter
def __receivers_set(self, receivers):
"""
Setter. Allows for self.receivers = value.
@ -247,7 +300,7 @@ class Msg(SharedMemoryModel):
elif clsname == "ChannelDB":
self.db_receivers_channels.add(receiver)
#@receivers.deleter
# @receivers.deleter
def __receivers_del(self):
"Deleter. Clears all receivers"
self.db_receivers_accounts.clear()
@ -255,6 +308,7 @@ class Msg(SharedMemoryModel):
self.db_receivers_scripts.clear()
self.db_receivers_channels.clear()
self.save()
receivers = property(__receivers_get, __receivers_set, __receivers_del)
def remove_receiver(self, receivers):
@ -281,12 +335,12 @@ class Msg(SharedMemoryModel):
self.db_receivers_channels.remove(receiver)
# channels property
#@property
# @property
def __channels_get(self):
"Getter. Allows for value = self.channels. Returns a list of channels."
return self.db_receivers_channels.all()
#@channels.setter
# @channels.setter
def __channels_set(self, value):
"""
Setter. Allows for self.channels = value.
@ -295,11 +349,12 @@ class Msg(SharedMemoryModel):
for val in (v for v in make_iter(value) if v):
self.db_receivers_channels.add(val)
#@channels.deleter
# @channels.deleter
def __channels_del(self):
"Deleter. Allows for del self.channels"
self.db_receivers_channels.clear()
self.save()
channels = property(__channels_get, __channels_set, __channels_del)
def __hide_from_get(self):
@ -307,9 +362,13 @@ class Msg(SharedMemoryModel):
Getter. Allows for value = self.hide_from.
Returns 3 lists of accounts, objects and channels
"""
return self.db_hide_from_accounts.all(), self.db_hide_from_objects.all(), self.db_hide_from_channels.all()
return (
self.db_hide_from_accounts.all(),
self.db_hide_from_objects.all(),
self.db_hide_from_channels.all(),
)
#@hide_from_sender.setter
# @hide_from_sender.setter
def __hide_from_set(self, hiders):
"Setter. Allows for self.hide_from = value. Will append to hiders"
for hider in make_iter(hiders):
@ -325,13 +384,14 @@ class Msg(SharedMemoryModel):
elif clsname == "ChannelDB":
self.db_hide_from_channels.add(hider.__dbclass__)
#@hide_from_sender.deleter
# @hide_from_sender.deleter
def __hide_from_del(self):
"Deleter. Allows for del self.hide_from_senders"
self.db_hide_from_accounts.clear()
self.db_hide_from_objects.clear()
self.db_hide_from_channels.clear()
self.save()
hide_from = property(__hide_from_get, __hide_from_set, __hide_from_del)
#
@ -341,10 +401,12 @@ class Msg(SharedMemoryModel):
def __str__(self):
"This handles what is shown when e.g. printing the message"
senders = ",".join(obj.key for obj in self.senders)
receivers = ",".join(["[%s]" % obj.key for obj in self.channels] + [obj.key for obj in self.receivers])
receivers = ",".join(
["[%s]" % obj.key for obj in self.channels] + [obj.key for obj in self.receivers]
)
return "%s->%s: %s" % (senders, receivers, crop(self.message, width=40))
def access(self, accessing_obj, access_type='read', default=False):
def access(self, accessing_obj, access_type="read", default=False):
"""
Checks lock access.
@ -357,14 +419,14 @@ class Msg(SharedMemoryModel):
result (bool): If access was granted or not.
"""
return self.locks.check(accessing_obj,
access_type=access_type, default=default)
return self.locks.check(accessing_obj, access_type=access_type, default=default)
#------------------------------------------------------------
# ------------------------------------------------------------
#
# TempMsg
#
#------------------------------------------------------------
# ------------------------------------------------------------
class TempMsg(object):
@ -375,7 +437,17 @@ class TempMsg(object):
"""
def __init__(self, senders=None, receivers=None, channels=None, message="", header="", type="", lockstring="", hide_from=None):
def __init__(
self,
senders=None,
receivers=None,
channels=None,
message="",
header="",
type="",
lockstring="",
hide_from=None,
):
"""
Creates the temp message.
@ -409,7 +481,9 @@ class TempMsg(object):
This handles what is shown when e.g. printing the message.
"""
senders = ",".join(obj.key for obj in self.senders)
receivers = ",".join(["[%s]" % obj.key for obj in self.channels] + [obj.key for obj in self.receivers])
receivers = ",".join(
["[%s]" % obj.key for obj in self.channels] + [obj.key for obj in self.receivers]
)
return "%s->%s: %s" % (senders, receivers, crop(self.message, width=40))
def remove_sender(self, sender):
@ -440,7 +514,7 @@ class TempMsg(object):
except ValueError:
pass # nothing to remove
def access(self, accessing_obj, access_type='read', default=False):
def access(self, accessing_obj, access_type="read", default=False):
"""
Checks lock access.
@ -453,15 +527,15 @@ class TempMsg(object):
result (bool): If access was granted or not.
"""
return self.locks.check(accessing_obj,
access_type=access_type, default=default)
return self.locks.check(accessing_obj, access_type=access_type, default=default)
#------------------------------------------------------------
# ------------------------------------------------------------
#
# Channel
#
#------------------------------------------------------------
# ------------------------------------------------------------
class SubscriptionHandler(object):
"""
@ -482,10 +556,18 @@ class SubscriptionHandler(object):
self._cache = None
def _recache(self):
self._cache = {account: True for account in self.obj.db_account_subscriptions.all()
if hasattr(account, 'pk') and account.pk}
self._cache.update({obj: True for obj in self.obj.db_object_subscriptions.all()
if hasattr(obj, 'pk') and obj.pk})
self._cache = {
account: True
for account in self.obj.db_account_subscriptions.all()
if hasattr(account, "pk") and account.pk
}
self._cache.update(
{
obj: True
for obj in self.obj.db_object_subscriptions.all()
if hasattr(obj, "pk") and obj.pk
}
)
def has(self, entity):
"""
@ -568,6 +650,7 @@ class SubscriptionHandler(object):
if self._cache is None:
self._recache()
return self._cache
get = all # alias
def online(self):
@ -581,8 +664,9 @@ class SubscriptionHandler(object):
recache_needed = False
for obj in self.all():
from django.core.exceptions import ObjectDoesNotExist
try:
if hasattr(obj, 'account') and obj.account:
if hasattr(obj, "account") and obj.account:
obj = obj.account
if not obj.is_connected:
continue
@ -617,11 +701,22 @@ class ChannelDB(TypedObject):
- db_object_subscriptions: The Object subscriptions.
"""
db_account_subscriptions = models.ManyToManyField("accounts.AccountDB",
related_name="account_subscription_set", blank=True, verbose_name='account subscriptions', db_index=True)
db_object_subscriptions = models.ManyToManyField("objects.ObjectDB",
related_name="object_subscription_set", blank=True, verbose_name='object subscriptions', db_index=True)
db_account_subscriptions = models.ManyToManyField(
"accounts.AccountDB",
related_name="account_subscription_set",
blank=True,
verbose_name="account subscriptions",
db_index=True,
)
db_object_subscriptions = models.ManyToManyField(
"objects.ObjectDB",
related_name="object_subscription_set",
blank=True,
verbose_name="object subscriptions",
db_index=True,
)
# Database manager
objects = managers.ChannelDBManager()

View file

@ -1,13 +1,12 @@
from evennia.utils.test_resources import EvenniaTest
from evennia import DefaultChannel
class ObjectCreationTest(EvenniaTest):
def test_channel_create(self):
description = "A place to talk about coffee."
obj, errors = DefaultChannel.create('coffeetalk', description=description)
obj, errors = DefaultChannel.create("coffeetalk", description=description)
self.assertTrue(obj, errors)
self.assertFalse(errors, errors)
self.assertEqual(description, obj.db.desc)
self.assertEqual(description, obj.db.desc)

View file

@ -13,8 +13,8 @@ See README.md for more info.
# but even so, you will get clashes when both using the tutorialworld and your
# own code, so somthing needs to be done here. See issue #766. /Griatch
#import evennia
# import evennia
# evennia._init()
#import barter, dice, extended_room, menu_login, talking_npc
#import chargen, email_login, gendersub, menusystem, slow_exit
#import tutorial_world, tutorial_examples
# import barter, dice, extended_room, menu_login, talking_npc
# import chargen, email_login, gendersub, menusystem, slow_exit
# import tutorial_world, tutorial_examples

View file

@ -386,6 +386,7 @@ class TradeHandler(object):
# trading commands (will go into CmdsetTrade, initialized by the
# CmdTrade command further down).
class CmdTradeBase(Command):
"""
Base command for Trade commands to inherit from. Implements the
@ -409,7 +410,7 @@ class CmdTradeBase(Command):
self.emote = ""
self.str_caller = "Your trade action: %s"
self.str_other = "%s:s trade action: " % self.caller.key + "%s"
if ':' in self.args:
if ":" in self.args:
self.args, self.emote = [part.strip() for part in self.args.rsplit(":", 1)]
self.str_caller = 'You say, "' + self.emote + '"\n [%s]'
if self.caller.has_account:
@ -420,6 +421,7 @@ class CmdTradeBase(Command):
# trade help
class CmdTradeHelp(CmdTradeBase):
"""
help command for the trade system.
@ -429,6 +431,7 @@ class CmdTradeHelp(CmdTradeBase):
Displays help for the trade commands.
"""
key = "trade help"
locks = "cmd:all()"
help_category = "Trade"
@ -465,6 +468,7 @@ class CmdTradeHelp(CmdTradeBase):
# offer
class CmdOffer(CmdTradeBase):
"""
offer one or more items in trade.
@ -475,6 +479,7 @@ class CmdOffer(CmdTradeBase):
Offer objects in trade. This will replace the currently
standing offer.
"""
key = "offer"
locks = "cmd:all()"
help_category = "Trading"
@ -491,7 +496,7 @@ class CmdOffer(CmdTradeBase):
return
# gather all offers
offers = [part.strip() for part in self.args.split(',')]
offers = [part.strip() for part in self.args.split(",")]
offerobjs = []
for offername in offers:
obj = caller.search(offername)
@ -502,7 +507,10 @@ class CmdOffer(CmdTradeBase):
# output
if len(offerobjs) > 1:
objnames = ", ".join("|w%s|n" % obj.key for obj in offerobjs[:-1]) + " and |w%s|n" % offerobjs[-1].key
objnames = (
", ".join("|w%s|n" % obj.key for obj in offerobjs[:-1])
+ " and |w%s|n" % offerobjs[-1].key
)
else:
objnames = "|w%s|n" % offerobjs[0].key
@ -512,6 +520,7 @@ class CmdOffer(CmdTradeBase):
# accept
class CmdAccept(CmdTradeBase):
"""
accept the standing offer
@ -525,6 +534,7 @@ class CmdAccept(CmdTradeBase):
your mind as long as the other party has not yet accepted. You can inspect
the current offer using the 'offers' command.
"""
key = "accept"
aliases = ["agree"]
locks = "cmd:all()"
@ -538,17 +548,31 @@ class CmdAccept(CmdTradeBase):
return
if self.tradehandler.accept(self.caller):
# deal finished. Trade ended and cleaned.
caller.msg(self.str_caller % "You |gaccept|n the deal. |gDeal is made and goods changed hands.|n")
self.msg_other(caller, self.str_other % "%s |gaccepts|n the deal."
" |gDeal is made and goods changed hands.|n" % caller.key)
caller.msg(
self.str_caller
% "You |gaccept|n the deal. |gDeal is made and goods changed hands.|n"
)
self.msg_other(
caller,
self.str_other % "%s |gaccepts|n the deal."
" |gDeal is made and goods changed hands.|n" % caller.key,
)
else:
# a one-sided accept.
caller.msg(self.str_caller % "You |Gaccept|n the offer. %s must now also accept." % self.other.key)
self.msg_other(caller, self.str_other % "%s |Gaccepts|n the offer. You must now also accept." % caller.key)
caller.msg(
self.str_caller
% "You |Gaccept|n the offer. %s must now also accept."
% self.other.key
)
self.msg_other(
caller,
self.str_other % "%s |Gaccepts|n the offer. You must now also accept." % caller.key,
)
# decline
class CmdDecline(CmdTradeBase):
"""
decline the standing offer
@ -561,6 +585,7 @@ class CmdDecline(CmdTradeBase):
has not yet accepted the deal. Also, changing the offer will automatically
decline the old offer.
"""
key = "decline"
locks = "cmd:all()"
help_category = "Trading"
@ -578,8 +603,12 @@ class CmdDecline(CmdTradeBase):
if self.tradehandler.decline(self.caller):
# changed a previous accept
caller.msg(self.str_caller % "You change your mind, |Rdeclining|n the current offer.")
self.msg_other(caller, self.str_other
% "%s changes their mind, |Rdeclining|n the current offer." % caller.key)
self.msg_other(
caller,
self.str_other
% "%s changes their mind, |Rdeclining|n the current offer."
% caller.key,
)
else:
# no acceptance to change
caller.msg(self.str_caller % "You |Rdecline|n the current offer.")
@ -593,6 +622,7 @@ class CmdDecline(CmdTradeBase):
# magical properties, ammo requirements or whatnot), then you need to add this
# here.
class CmdEvaluate(CmdTradeBase):
"""
evaluate objects on offer
@ -603,6 +633,7 @@ class CmdEvaluate(CmdTradeBase):
This allows you to examine any object currently on offer, to
determine if it's worth your while.
"""
key = "evaluate"
aliases = ["eval"]
locks = "cmd:all()"
@ -632,6 +663,7 @@ class CmdEvaluate(CmdTradeBase):
# status
class CmdStatus(CmdTradeBase):
"""
show a list of the current deal
@ -646,6 +678,7 @@ class CmdStatus(CmdTradeBase):
change your deal. You might also want to use 'say', 'emote' etc to
try to influence the other part in the deal.
"""
key = "status"
aliases = ["offers", "deal"]
locks = "cmd:all()"
@ -669,19 +702,27 @@ class CmdStatus(CmdTradeBase):
if not part_b_offerlist:
part_b_offerlist = "\n <nothing>"
string = "|gOffered by %s:|n%s\n|yOffered by %s:|n%s" % (self.part_a.key,
"".join(part_a_offerlist),
self.part_b.key,
"".join(part_b_offerlist))
string = "|gOffered by %s:|n%s\n|yOffered by %s:|n%s" % (
self.part_a.key,
"".join(part_a_offerlist),
self.part_b.key,
"".join(part_b_offerlist),
)
accept_a = self.tradehandler.part_a_accepted and "|gYes|n" or "|rNo|n"
accept_b = self.tradehandler.part_b_accepted and "|gYes|n" or "|rNo|n"
string += "\n\n%s agreed: %s, %s agreed: %s" % (self.part_a.key, accept_a, self.part_b.key, accept_b)
string += "\n\n%s agreed: %s, %s agreed: %s" % (
self.part_a.key,
accept_a,
self.part_b.key,
accept_b,
)
string += "\n Use 'offer', 'eval' and 'accept'/'decline' to trade. See also 'trade help'."
caller.msg(string)
# finish
class CmdFinish(CmdTradeBase):
"""
end the trade prematurely
@ -693,6 +734,7 @@ class CmdFinish(CmdTradeBase):
This ends the trade prematurely. No trade will take place.
"""
key = "end trade"
aliases = "finish trade"
locks = "cmd:all()"
@ -703,16 +745,20 @@ class CmdFinish(CmdTradeBase):
caller = self.caller
self.tradehandler.finish(force=True)
caller.msg(self.str_caller % "You |raborted|n trade. No deal was made.")
self.msg_other(caller, self.str_other % "%s |raborted|n trade. No deal was made." % caller.key)
self.msg_other(
caller, self.str_other % "%s |raborted|n trade. No deal was made." % caller.key
)
# custom Trading cmdset
class CmdsetTrade(CmdSet):
"""
This cmdset is added when trade is initated. It is handled by the
trade event handler.
"""
key = "cmdset_trade"
def at_cmdset_creation(self):
@ -729,6 +775,7 @@ class CmdsetTrade(CmdSet):
# access command - once both have given this, this will create the
# trading cmdset to start trade.
class CmdTrade(Command):
"""
Initiate trade with another party
@ -745,6 +792,7 @@ class CmdTrade(Command):
optional say part works like the say command and allows you to add
info to your choice.
"""
key = "trade"
aliases = ["barter"]
locks = "cmd:all()"
@ -764,7 +812,7 @@ class CmdTrade(Command):
# handle the emote manually here
selfemote = ""
theiremote = ""
if ':' in self.args:
if ":" in self.args:
self.args, emote = [part.strip() for part in self.args.rsplit(":", 1)]
selfemote = 'You say, "%s"\n ' % emote
if self.caller.has_account:
@ -776,12 +824,12 @@ class CmdTrade(Command):
# might not match the actual name in tradehandler (in the case of
# using this command to accept/decline a trade invitation).
part_a = self.caller
accept = 'accept' in self.args
decline = 'decline' in self.args
accept = "accept" in self.args
decline = "decline" in self.args
if accept:
part_b = self.args.rstrip('accept').strip()
part_b = self.args.rstrip("accept").strip()
elif decline:
part_b = self.args.rstrip('decline').strip()
part_b = self.args.rstrip("decline").strip()
else:
part_b = self.args
part_b = self.caller.search(part_b)
@ -824,7 +872,9 @@ class CmdTrade(Command):
# accept a trade proposal from part_b (so roles are reversed)
if part_a.ndb.tradehandler:
# already in a trade
part_a.msg("You are already in trade with %s. You need to end that first." % part_b.key)
part_a.msg(
"You are already in trade with %s. You need to end that first." % part_b.key
)
return
if part_b.ndb.tradehandler.join(part_a):
part_b.msg(theiremote + str_start_a % part_a.key)

View file

@ -160,9 +160,10 @@ def _menu_savefunc(caller, buf):
def _menu_quitfunc(caller):
caller.cmdset.add(BuildingMenuCmdSet,
permanent=caller.ndb._building_menu and
caller.ndb._building_menu.persistent or False)
caller.cmdset.add(
BuildingMenuCmdSet,
permanent=caller.ndb._building_menu and caller.ndb._building_menu.persistent or False,
)
if caller.ndb._building_menu:
caller.ndb._building_menu.move(back=True)
@ -234,6 +235,7 @@ def _call_or_get(value, menu=None, choice=None, string=None, obj=None, caller=No
# Helper functions, to be used in menu choices
def menu_setattr(menu, choice, obj, string):
"""
Set the value at the specified attribute.
@ -252,10 +254,16 @@ def menu_setattr(menu, choice, obj, string):
"""
attr = getattr(choice, "attr", None) if choice else None
if choice is None or string is None or attr is None or menu is None:
log_err(dedent("""
log_err(
dedent(
"""
The `menu_setattr` function was called to set the attribute {} of object {} to {},
but the choice {} of menu {} or another information is missing.
""".format(attr, obj, repr(string), choice, menu)).strip("\n")).strip()
""".format(
attr, obj, repr(string), choice, menu
)
).strip("\n")
).strip()
return
for part in attr.split(".")[:-1]:
@ -280,8 +288,10 @@ def menu_quit(caller, menu):
"""
if caller is None or menu is None:
log_err("The function `menu_quit` was called with missing "
"arguments: caller={}, menu={}".format(caller, menu))
log_err(
"The function `menu_quit` was called with missing "
"arguments: caller={}, menu={}".format(caller, menu)
)
if caller.cmdset.has(BuildingMenuCmdSet):
menu.close()
@ -303,12 +313,19 @@ def menu_edit(caller, choice, obj):
attr = choice.attr
caller.db._building_menu_to_edit = (obj, attr)
caller.cmdset.remove(BuildingMenuCmdSet)
EvEditor(caller, loadfunc=_menu_loadfunc, savefunc=_menu_savefunc, quitfunc=_menu_quitfunc,
key="editor", persistent=True)
EvEditor(
caller,
loadfunc=_menu_loadfunc,
savefunc=_menu_savefunc,
quitfunc=_menu_quitfunc,
key="editor",
persistent=True,
)
# Building menu commands and CmdSet
class CmdNoInput(Command):
"""No input has been found."""
@ -363,7 +380,9 @@ class CmdNoMatch(Command):
self.caller.msg(choice.format_text())
else:
for choice in self.menu.relevant_choices:
if choice.key.lower() == raw_string.lower() or any(raw_string.lower() == alias for alias in choice.aliases):
if choice.key.lower() == raw_string.lower() or any(
raw_string.lower() == alias for alias in choice.aliases
):
self.menu.move(choice.key)
return
@ -395,13 +414,26 @@ class BuildingMenuCmdSet(CmdSet):
# Menu classes
class Choice(object):
"""A choice object, created by `add_choice`."""
def __init__(self, title, key=None, aliases=None, attr=None, text=None,
glance=None, on_enter=None, on_nomatch=None, on_leave=None,
menu=None, caller=None, obj=None):
def __init__(
self,
title,
key=None,
aliases=None,
attr=None,
text=None,
glance=None,
on_enter=None,
on_nomatch=None,
on_leave=None,
menu=None,
caller=None,
obj=None,
):
"""Constructor.
Args:
@ -452,7 +484,9 @@ class Choice(object):
"""Format the choice text and return it, or an empty string."""
text = ""
if self.text:
text = _call_or_get(self.text, menu=self.menu, choice=self, string="", caller=self.caller, obj=self.obj)
text = _call_or_get(
self.text, menu=self.menu, choice=self, string="", caller=self.caller, obj=self.obj
)
text = dedent(text.strip("\n"))
text = text.format(obj=self.obj, caller=self.caller)
@ -466,8 +500,14 @@ class Choice(object):
"""
if self.on_enter:
_call_or_get(self.on_enter, menu=self.menu, choice=self, string=string,
caller=self.caller, obj=self.obj)
_call_or_get(
self.on_enter,
menu=self.menu,
choice=self,
string=string,
caller=self.caller,
obj=self.obj,
)
def nomatch(self, string):
"""Called when the user entered something in the choice.
@ -482,8 +522,14 @@ class Choice(object):
"""
if self.on_nomatch:
return _call_or_get(self.on_nomatch, menu=self.menu, choice=self,
string=string, caller=self.caller, obj=self.obj)
return _call_or_get(
self.on_nomatch,
menu=self.menu,
choice=self,
string=string,
caller=self.caller,
obj=self.obj,
)
return True
@ -495,8 +541,14 @@ class Choice(object):
"""
if self.on_leave:
_call_or_get(self.on_leave, menu=self.menu, choice=self,
string=string, caller=self.caller, obj=self.obj)
_call_or_get(
self.on_leave,
menu=self.menu,
choice=self,
string=string,
caller=self.caller,
obj=self.obj,
)
class BuildingMenu(object):
@ -527,8 +579,15 @@ class BuildingMenu(object):
joker_key = "*" # The special key meaning "anything" in a choice key
min_shortcut = 1 # The minimum length of shorcuts when `key` is not set
def __init__(self, caller=None, obj=None, title="Building menu: {obj}",
keys=None, parents=None, persistent=False):
def __init__(
self,
caller=None,
obj=None,
title="Building menu: {obj}",
keys=None,
parents=None,
persistent=False,
):
"""Constructor, you shouldn't override. See `init` instead.
Args:
@ -650,12 +709,12 @@ class BuildingMenu(object):
if self.persistent:
self.caller.db._building_menu = {
"class": type(self).__module__ + "." + type(self).__name__,
"obj": self.obj,
"title": self.title,
"keys": self.keys,
"parents": self.parents,
"persistent": self.persistent,
"class": type(self).__module__ + "." + type(self).__name__,
"obj": self.obj,
"title": self.title,
"keys": self.keys,
"parents": self.parents,
"persistent": self.persistent,
}
def _add_keys_choice(self):
@ -668,7 +727,7 @@ class BuildingMenu(object):
while length <= len(title):
i = 0
while i < len(title) - length + 1:
guess = title[i:i + length]
guess = title[i : i + length]
if guess not in self.cmds:
choice.key = guess
break
@ -698,8 +757,18 @@ class BuildingMenu(object):
"""
pass
def add_choice(self, title, key=None, aliases=None, attr=None, text=None, glance=None,
on_enter=None, on_nomatch=None, on_leave=None):
def add_choice(
self,
title,
key=None,
aliases=None,
attr=None,
text=None,
glance=None,
on_enter=None,
on_nomatch=None,
on_leave=None,
):
"""
Add a choice, a valid sub-menu, in the current builder menu.
@ -762,8 +831,10 @@ class BuildingMenu(object):
on_nomatch = menu_setattr
if key and key in self.cmds:
raise ValueError("A conflict exists between {} and {}, both use "
"key or alias {}".format(self.cmds[key], title, repr(key)))
raise ValueError(
"A conflict exists between {} and {}, both use "
"key or alias {}".format(self.cmds[key], title, repr(key))
)
if attr:
if glance is None:
@ -777,12 +848,24 @@ class BuildingMenu(object):
Use |y{back}|n to go back to the main menu.
Current value: |c{{{obj_attr}}}|n
""".format(attr=attr, obj_attr="obj." + attr,
back="|n or |y".join(self.keys_go_back))
""".format(
attr=attr, obj_attr="obj." + attr, back="|n or |y".join(self.keys_go_back)
)
choice = Choice(title, key=key, aliases=aliases, attr=attr, text=text, glance=glance,
on_enter=on_enter, on_nomatch=on_nomatch, on_leave=on_leave,
menu=self, caller=self.caller, obj=self.obj)
choice = Choice(
title,
key=key,
aliases=aliases,
attr=attr,
text=text,
glance=glance,
on_enter=on_enter,
on_nomatch=on_nomatch,
on_leave=on_leave,
menu=self,
caller=self.caller,
obj=self.obj,
)
self.choices.append(choice)
if key:
self.cmds[key] = choice
@ -792,8 +875,15 @@ class BuildingMenu(object):
return choice
def add_choice_edit(self, title="description", key="d", aliases=None, attr="db.desc",
glance="\n {obj.db.desc}", on_enter=None):
def add_choice_edit(
self,
title="description",
key="d",
aliases=None,
attr="db.desc",
glance="\n {obj.db.desc}",
on_enter=None,
):
"""
Add a simple choice to edit a given attribute in the EvEditor.
@ -817,8 +907,9 @@ class BuildingMenu(object):
"""
on_enter = on_enter or menu_edit
return self.add_choice(title, key=key, aliases=aliases, attr=attr,
glance=glance, on_enter=on_enter, text="")
return self.add_choice(
title, key=key, aliases=aliases, attr=attr, glance=glance, on_enter=on_enter, text=""
)
def add_choice_quit(self, title="quit the menu", key="q", aliases=None, on_enter=None):
"""
@ -883,17 +974,20 @@ class BuildingMenu(object):
try:
menu_class = class_from_module(parent_class)
except Exception:
log_trace("BuildingMenu: attempting to load class {} failed".format(
repr(parent_class)))
log_trace(
"BuildingMenu: attempting to load class {} failed".format(repr(parent_class))
)
return
# Create the parent menu
try:
building_menu = menu_class(self.caller, parent_obj,
keys=parent_keys, parents=tuple(parents))
building_menu = menu_class(
self.caller, parent_obj, keys=parent_keys, parents=tuple(parents)
)
except Exception:
log_trace("An error occurred while creating building menu {}".format(
repr(parent_class)))
log_trace(
"An error occurred while creating building menu {}".format(repr(parent_class))
)
return
else:
return building_menu.open()
@ -928,14 +1022,18 @@ class BuildingMenu(object):
try:
menu_class = class_from_module(submenu_class)
except Exception:
log_trace("BuildingMenu: attempting to load class {} failed".format(repr(submenu_class)))
log_trace(
"BuildingMenu: attempting to load class {} failed".format(repr(submenu_class))
)
return
# Create the submenu
try:
building_menu = menu_class(self.caller, submenu_obj, parents=parents)
except Exception:
log_trace("An error occurred while creating building menu {}".format(repr(submenu_class)))
log_trace(
"An error occurred while creating building menu {}".format(repr(submenu_class))
)
return
else:
return building_menu.open()
@ -975,7 +1073,9 @@ class BuildingMenu(object):
self.keys.append(key)
else: # Move backward
if not self.keys:
raise ValueError("you already are at the top of the tree, you cannot move backward.")
raise ValueError(
"you already are at the top of the tree, you cannot move backward."
)
del self.keys[-1]
@ -999,7 +1099,9 @@ class BuildingMenu(object):
# Display methods. Override for customization
def display_title(self):
"""Return the menu title to be displayed."""
return _call_or_get(self.title, menu=self, obj=self.obj, caller=self.caller).format(obj=self.obj)
return _call_or_get(self.title, menu=self, obj=self.obj, caller=self.caller).format(
obj=self.obj
)
def display_choice(self, choice):
"""Display the specified choice.
@ -1008,18 +1110,21 @@ class BuildingMenu(object):
choice (Choice): the menu choice.
"""
title = _call_or_get(choice.title, menu=self, choice=choice, obj=self.obj, caller=self.caller)
title = _call_or_get(
choice.title, menu=self, choice=choice, obj=self.obj, caller=self.caller
)
clear_title = title.lower()
pos = clear_title.find(choice.key.lower())
ret = " "
if pos >= 0:
ret += title[:pos] + "[|y" + choice.key.title() + "|n]" + title[pos + len(choice.key):]
ret += title[:pos] + "[|y" + choice.key.title() + "|n]" + title[pos + len(choice.key) :]
else:
ret += "[|y" + choice.key.title() + "|n] " + title
if choice.glance:
glance = _call_or_get(choice.glance, menu=self, choice=choice,
caller=self.caller, string="", obj=self.obj)
glance = _call_or_get(
choice.glance, menu=self, choice=choice, caller=self.caller, string="", obj=self.obj
)
glance = glance.format(obj=self.obj, caller=self.caller)
ret += ": " + glance
@ -1054,14 +1159,18 @@ class BuildingMenu(object):
if menu:
class_name = menu.get("class")
if not class_name:
log_err("BuildingMenu: on caller {}, a persistent attribute holds building menu "
"data, but no class could be found to restore the menu".format(caller))
log_err(
"BuildingMenu: on caller {}, a persistent attribute holds building menu "
"data, but no class could be found to restore the menu".format(caller)
)
return
try:
menu_class = class_from_module(class_name)
except Exception:
log_trace("BuildingMenu: attempting to load class {} failed".format(repr(class_name)))
log_trace(
"BuildingMenu: attempting to load class {} failed".format(repr(class_name))
)
return
# Create the menu
@ -1071,10 +1180,13 @@ class BuildingMenu(object):
parents = menu.get("parents")
persistent = menu.get("persistent", False)
try:
building_menu = menu_class(caller, obj, title=title, keys=keys,
parents=parents, persistent=persistent)
building_menu = menu_class(
caller, obj, title=title, keys=keys, parents=parents, persistent=persistent
)
except Exception:
log_trace("An error occurred while creating building menu {}".format(repr(class_name)))
log_trace(
"An error occurred while creating building menu {}".format(repr(class_name))
)
return
return building_menu
@ -1102,7 +1214,12 @@ class GenericBuildingMenu(BuildingMenu):
call `add_choice_quit` to add this choice with different options.
"""
self.add_choice("key", key="k", attr="key", glance="{obj.key}", text="""
self.add_choice(
"key",
key="k",
attr="key",
glance="{obj.key}",
text="""
-------------------------------------------------------------------------------
Editing the key of {{obj.key}}(#{{obj.id}})
@ -1110,7 +1227,10 @@ class GenericBuildingMenu(BuildingMenu):
Use |y{back}|n to go back to the main menu.
Current key: |c{{obj.key}}|n
""".format(back="|n or |y".join(self.keys_go_back)))
""".format(
back="|n or |y".join(self.keys_go_back)
),
)
self.add_choice_edit("description", key="d", attr="db.desc")

View file

@ -81,7 +81,9 @@ class CmdOOCLook(default_cmds.CmdLook):
if not avail_chars:
self.caller.msg("You have no characters to look at. Why not create one?")
return
objs = managers.objects.get_objs_with_key_and_typeclass(self.args.strip(), CHARACTER_TYPECLASS)
objs = managers.objects.get_objs_with_key_and_typeclass(
self.args.strip(), CHARACTER_TYPECLASS
)
objs = [obj for obj in objs if obj.id in avail_chars]
if not objs:
self.caller.msg("You cannot see this Character.")
@ -101,15 +103,17 @@ class CmdOOCLook(default_cmds.CmdLook):
charlist += "\n\n Use |w@ic <character name>|n to switch to that Character."
else:
charlist = "You have no Characters."
string = \
""" You, %s, are an |wOOC ghost|n without form. The world is hidden
string = """ You, %s, are an |wOOC ghost|n without form. The world is hidden
from you and besides chatting on channels your options are limited.
You need to have a Character in order to interact with the world.
%s
Use |wcreate <name>|n to create a new character and |whelp|n for a
list of available commands.""" % (self.caller.key, charlist)
list of available commands.""" % (
self.caller.key,
charlist,
)
self.caller.msg(string)
else:
@ -159,11 +163,15 @@ class CmdOOCCharacterCreate(Command):
new_character = create_object(CHARACTER_TYPECLASS, key=charname)
if not new_character:
self.caller.msg("|rThe Character couldn't be created. This is a bug. Please contact an admin.")
self.caller.msg(
"|rThe Character couldn't be created. This is a bug. Please contact an admin."
)
return
# make sure to lock the character to only be puppeted by this account
new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)" %
(new_character.id, self.caller.id))
new_character.locks.add(
"puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)"
% (new_character.id, self.caller.id)
)
# save dbref
avail_chars = self.caller.db._character_dbrefs

View file

@ -91,15 +91,21 @@ WEARSTYLE_MAXLENGTH = 50
# The order in which clothing types appear on the description. Untyped clothing or clothing
# with a type not given in this list goes last.
CLOTHING_TYPE_ORDER = ['hat', 'jewelry', 'top', 'undershirt', 'gloves', 'fullbody', 'bottom',
'underpants', 'socks', 'shoes', 'accessory']
CLOTHING_TYPE_ORDER = [
"hat",
"jewelry",
"top",
"undershirt",
"gloves",
"fullbody",
"bottom",
"underpants",
"socks",
"shoes",
"accessory",
]
# The maximum number of each type of clothes that can be worn. Unlimited if untyped or not specified.
CLOTHING_TYPE_LIMIT = {
'hat': 1,
'gloves': 1,
'socks': 1,
'shoes': 1
}
CLOTHING_TYPE_LIMIT = {"hat": 1, "gloves": 1, "socks": 1, "shoes": 1}
# The maximum number of clothing items that can be worn, or None for unlimited.
CLOTHING_OVERALL_LIMIT = 20
# What types of clothes will automatically cover what other types of clothes when worn.
@ -107,17 +113,18 @@ CLOTHING_OVERALL_LIMIT = 20
# on that auto-covers it - for example, it's perfectly possible to have your underpants
# showing if you put them on after your pants!
CLOTHING_TYPE_AUTOCOVER = {
'top': ['undershirt'],
'bottom': ['underpants'],
'fullbody': ['undershirt', 'underpants'],
'shoes': ['socks']
"top": ["undershirt"],
"bottom": ["underpants"],
"fullbody": ["undershirt", "underpants"],
"shoes": ["socks"],
}
# Types of clothes that can't be used to cover other clothes.
CLOTHING_TYPE_CANT_COVER_WITH = ['jewelry']
CLOTHING_TYPE_CANT_COVER_WITH = ["jewelry"]
# HELPER FUNCTIONS START HERE
def order_clothes_list(clothes_list):
"""
Orders a given clothes list by the order specified in CLOTHING_TYPE_ORDER.
@ -222,7 +229,6 @@ def single_type_count(clothes_list, type):
class Clothing(DefaultObject):
def wear(self, wearer, wearstyle, quiet=False):
"""
Sets clothes to 'worn' and optionally echoes to the room.
@ -246,8 +252,10 @@ class Clothing(DefaultObject):
to_cover = []
if self.db.clothing_type and self.db.clothing_type in CLOTHING_TYPE_AUTOCOVER:
for garment in get_worn_clothes(wearer):
if garment.db.clothing_type and garment.db.clothing_type \
in CLOTHING_TYPE_AUTOCOVER[self.db.clothing_type]:
if (
garment.db.clothing_type
and garment.db.clothing_type in CLOTHING_TYPE_AUTOCOVER[self.db.clothing_type]
):
to_cover.append(garment)
garment.db.covered_by = self
# Return if quiet
@ -282,7 +290,11 @@ class Clothing(DefaultObject):
thing.db.covered_by = False
uncovered_list.append(thing.name)
if len(uncovered_list) > 0:
remove_message = "%s removes %s, revealing %s." % (wearer, self.name, list_to_string(uncovered_list))
remove_message = "%s removes %s, revealing %s." % (
wearer,
self.name,
list_to_string(uncovered_list),
)
# Echo a message to the room
if not quiet:
wearer.location.msg_contents(remove_message)
@ -345,6 +357,7 @@ class ClothedCharacter(DefaultCharacter):
# COMMANDS START HERE
class CmdWear(MuxCommand):
"""
Puts on an item of clothing you are holding.
@ -390,7 +403,10 @@ class CmdWear(MuxCommand):
type_count = single_type_count(get_worn_clothes(self.caller), clothing.db.clothing_type)
if clothing.db.clothing_type in list(CLOTHING_TYPE_LIMIT.keys()):
if type_count >= CLOTHING_TYPE_LIMIT[clothing.db.clothing_type]:
self.caller.msg("You can't wear any more clothes of the type '%s'." % clothing.db.clothing_type)
self.caller.msg(
"You can't wear any more clothes of the type '%s'."
% clothing.db.clothing_type
)
return
if clothing.db.worn and len(self.arglist) == 1:
@ -399,9 +415,16 @@ class CmdWear(MuxCommand):
if len(self.arglist) > 1: # If wearstyle arguments given
wearstyle_list = self.arglist # Split arguments into a list of words
del wearstyle_list[0] # Leave first argument (the clothing item) out of the wearstyle
wearstring = ' '.join(str(e) for e in wearstyle_list) # Join list of args back into one string
if WEARSTYLE_MAXLENGTH and len(wearstring) > WEARSTYLE_MAXLENGTH: # If length of wearstyle exceeds limit
self.caller.msg("Please keep your wear style message to less than %i characters." % WEARSTYLE_MAXLENGTH)
wearstring = " ".join(
str(e) for e in wearstyle_list
) # Join list of args back into one string
if (
WEARSTYLE_MAXLENGTH and len(wearstring) > WEARSTYLE_MAXLENGTH
): # If length of wearstyle exceeds limit
self.caller.msg(
"Please keep your wear style message to less than %i characters."
% WEARSTYLE_MAXLENGTH
)
else:
wearstyle = wearstring
clothing.wear(self.caller, wearstyle)
@ -489,11 +512,17 @@ class CmdCover(MuxCommand):
self.caller.msg("%s is covered by something else!" % cover_with.name)
return
if to_cover.db.covered_by:
self.caller.msg("%s is already covered by %s." % (cover_with.name, to_cover.db.covered_by.name))
self.caller.msg(
"%s is already covered by %s." % (cover_with.name, to_cover.db.covered_by.name)
)
return
if not cover_with.db.worn:
cover_with.wear(self.caller, True) # Put on the item to cover with if it's not on already
self.caller.location.msg_contents("%s covers %s with %s." % (self.caller, to_cover.name, cover_with.name))
cover_with.wear(
self.caller, True
) # Put on the item to cover with if it's not on already
self.caller.location.msg_contents(
"%s covers %s with %s." % (self.caller, to_cover.name, cover_with.name)
)
to_cover.db.covered_by = cover_with
@ -564,9 +593,12 @@ class CmdDrop(MuxCommand):
# Because the DROP command by definition looks for items
# in inventory, call the search function using location = caller
obj = caller.search(self.args, location=caller,
nofound_string="You aren't carrying %s." % self.args,
multimatch_string="You carry more than one %s:" % self.args)
obj = caller.search(
self.args,
location=caller,
nofound_string="You aren't carrying %s." % self.args,
multimatch_string="You carry more than one %s:" % self.args,
)
if not obj:
return
@ -581,9 +613,7 @@ class CmdDrop(MuxCommand):
obj.move_to(caller.location, quiet=True)
caller.msg("You drop %s." % (obj.name,))
caller.location.msg_contents("%s drops %s." %
(caller.name, obj.name),
exclude=caller)
caller.location.msg_contents("%s drops %s." % (caller.name, obj.name), exclude=caller)
# Call the object script's at_drop() method.
obj.at_drop(caller)
@ -598,6 +628,7 @@ class CmdGive(MuxCommand):
Gives an items from your inventory to another character,
placing it in their inventory.
"""
key = "give"
locks = "cmd:all()"
arg_regex = r"\s|$"
@ -609,9 +640,12 @@ class CmdGive(MuxCommand):
if not self.args or not self.rhs:
caller.msg("Usage: give <inventory object> = <target>")
return
to_give = caller.search(self.lhs, location=caller,
nofound_string="You aren't carrying %s." % self.lhs,
multimatch_string="You carry more than one %s:" % self.lhs)
to_give = caller.search(
self.lhs,
location=caller,
nofound_string="You aren't carrying %s." % self.lhs,
multimatch_string="You carry more than one %s:" % self.lhs,
)
target = caller.search(self.rhs)
if not (to_give and target):
return
@ -623,7 +657,9 @@ class CmdGive(MuxCommand):
return
# This is new! Can't give away something that's worn.
if to_give.db.covered_by:
caller.msg("You can't give that away because it's covered by %s." % to_give.db.covered_by)
caller.msg(
"You can't give that away because it's covered by %s." % to_give.db.covered_by
)
return
# Remove clothes if they're given.
if to_give.db.worn:
@ -647,6 +683,7 @@ class CmdInventory(MuxCommand):
Shows your inventory.
"""
# Alternate version of the inventory command which separates
# worn and carried items.
@ -687,6 +724,7 @@ class ClothedCharacterCmdSet(default_cmds.CharacterCmdSet):
version of 'inventory' that differentiates between carried and worn
items.
"""
key = "DefaultCharacter"
def at_cmdset_creation(self):

View file

@ -110,70 +110,65 @@ _ANSI_SPACE = " "
#############################################################
CURLY_COLOR_ANSI_EXTRA_MAP = [
(r'{n', _ANSI_NORMAL), # reset
(r'{/', _ANSI_RETURN), # line break
(r'{-', _ANSI_TAB), # tab
(r'{_', _ANSI_SPACE), # space
(r'{*', _ANSI_INVERSE), # invert
(r'{^', _ANSI_BLINK), # blinking text (very annoying and not supported by all clients)
(r'{u', _ANSI_UNDERLINE), # underline
(r'{r', _ANSI_HILITE + _ANSI_RED),
(r'{g', _ANSI_HILITE + _ANSI_GREEN),
(r'{y', _ANSI_HILITE + _ANSI_YELLOW),
(r'{b', _ANSI_HILITE + _ANSI_BLUE),
(r'{m', _ANSI_HILITE + _ANSI_MAGENTA),
(r'{c', _ANSI_HILITE + _ANSI_CYAN),
(r'{w', _ANSI_HILITE + _ANSI_WHITE), # pure white
(r'{x', _ANSI_HILITE + _ANSI_BLACK), # dark grey
(r'{R', _ANSI_HILITE + _ANSI_RED),
(r'{G', _ANSI_HILITE + _ANSI_GREEN),
(r'{Y', _ANSI_HILITE + _ANSI_YELLOW),
(r'{B', _ANSI_HILITE + _ANSI_BLUE),
(r'{M', _ANSI_HILITE + _ANSI_MAGENTA),
(r'{C', _ANSI_HILITE + _ANSI_CYAN),
(r'{W', _ANSI_HILITE + _ANSI_WHITE), # light grey
(r'{X', _ANSI_HILITE + _ANSI_BLACK), # pure black
(r"{n", _ANSI_NORMAL), # reset
(r"{/", _ANSI_RETURN), # line break
(r"{-", _ANSI_TAB), # tab
(r"{_", _ANSI_SPACE), # space
(r"{*", _ANSI_INVERSE), # invert
(r"{^", _ANSI_BLINK), # blinking text (very annoying and not supported by all clients)
(r"{u", _ANSI_UNDERLINE), # underline
(r"{r", _ANSI_HILITE + _ANSI_RED),
(r"{g", _ANSI_HILITE + _ANSI_GREEN),
(r"{y", _ANSI_HILITE + _ANSI_YELLOW),
(r"{b", _ANSI_HILITE + _ANSI_BLUE),
(r"{m", _ANSI_HILITE + _ANSI_MAGENTA),
(r"{c", _ANSI_HILITE + _ANSI_CYAN),
(r"{w", _ANSI_HILITE + _ANSI_WHITE), # pure white
(r"{x", _ANSI_HILITE + _ANSI_BLACK), # dark grey
(r"{R", _ANSI_HILITE + _ANSI_RED),
(r"{G", _ANSI_HILITE + _ANSI_GREEN),
(r"{Y", _ANSI_HILITE + _ANSI_YELLOW),
(r"{B", _ANSI_HILITE + _ANSI_BLUE),
(r"{M", _ANSI_HILITE + _ANSI_MAGENTA),
(r"{C", _ANSI_HILITE + _ANSI_CYAN),
(r"{W", _ANSI_HILITE + _ANSI_WHITE), # light grey
(r"{X", _ANSI_HILITE + _ANSI_BLACK), # pure black
# hilight-able colors
(r'{h', _ANSI_HILITE),
(r'{H', _ANSI_UNHILITE),
(r'{!R', _ANSI_RED),
(r'{!G', _ANSI_GREEN),
(r'{!Y', _ANSI_YELLOW),
(r'{!B', _ANSI_BLUE),
(r'{!M', _ANSI_MAGENTA),
(r'{!C', _ANSI_CYAN),
(r'{!W', _ANSI_WHITE), # light grey
(r'{!X', _ANSI_BLACK), # pure black
(r"{h", _ANSI_HILITE),
(r"{H", _ANSI_UNHILITE),
(r"{!R", _ANSI_RED),
(r"{!G", _ANSI_GREEN),
(r"{!Y", _ANSI_YELLOW),
(r"{!B", _ANSI_BLUE),
(r"{!M", _ANSI_MAGENTA),
(r"{!C", _ANSI_CYAN),
(r"{!W", _ANSI_WHITE), # light grey
(r"{!X", _ANSI_BLACK), # pure black
# normal ANSI backgrounds
(r'{[R', _ANSI_BACK_RED),
(r'{[G', _ANSI_BACK_GREEN),
(r'{[Y', _ANSI_BACK_YELLOW),
(r'{[B', _ANSI_BACK_BLUE),
(r'{[M', _ANSI_BACK_MAGENTA),
(r'{[C', _ANSI_BACK_CYAN),
(r'{[W', _ANSI_BACK_WHITE), # light grey background
(r'{[X', _ANSI_BACK_BLACK), # pure black background
(r"{[R", _ANSI_BACK_RED),
(r"{[G", _ANSI_BACK_GREEN),
(r"{[Y", _ANSI_BACK_YELLOW),
(r"{[B", _ANSI_BACK_BLUE),
(r"{[M", _ANSI_BACK_MAGENTA),
(r"{[C", _ANSI_BACK_CYAN),
(r"{[W", _ANSI_BACK_WHITE), # light grey background
(r"{[X", _ANSI_BACK_BLACK), # pure black background
]
CURLY_COLOR_XTERM256_EXTRA_FG = [r'\{([0-5])([0-5])([0-5])'] # |123 - foreground colour
CURLY_COLOR_XTERM256_EXTRA_BG = [r'\{\[([0-5])([0-5])([0-5])'] # |[123 - background colour
CURLY_COLOR_XTERM256_EXTRA_GFG = [r'\{=([a-z])'] # |=a - greyscale foreground
CURLY_COLOR_XTERM256_EXTRA_GBG = [r'\{\[=([a-z])'] # |[=a - greyscale background
CURLY_COLOR_XTERM256_EXTRA_FG = [r"\{([0-5])([0-5])([0-5])"] # |123 - foreground colour
CURLY_COLOR_XTERM256_EXTRA_BG = [r"\{\[([0-5])([0-5])([0-5])"] # |[123 - background colour
CURLY_COLOR_XTERM256_EXTRA_GFG = [r"\{=([a-z])"] # |=a - greyscale foreground
CURLY_COLOR_XTERM256_EXTRA_GBG = [r"\{\[=([a-z])"] # |[=a - greyscale background
CURLY_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP = [
(r'{[r', r'{[500'),
(r'{[g', r'{[050'),
(r'{[y', r'{[550'),
(r'{[b', r'{[005'),
(r'{[m', r'{[505'),
(r'{[c', r'{[055'),
(r'{[w', r'{[555'), # white background
(r'{[x', r'{[222'), # dark grey background
(r"{[r", r"{[500"),
(r"{[g", r"{[050"),
(r"{[y", r"{[550"),
(r"{[b", r"{[005"),
(r"{[m", r"{[505"),
(r"{[c", r"{[055"),
(r"{[w", r"{[555"), # white background
(r"{[x", r"{[222"), # dark grey background
]
@ -191,48 +186,46 @@ CURLY_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP = [
#############################################################
MUX_COLOR_ANSI_EXTRA_MAP = [
(r'%cn', _ANSI_NORMAL), # reset
(r'%ch', _ANSI_HILITE), # highlight
(r'%r', _ANSI_RETURN), # line break
(r'%R', _ANSI_RETURN), #
(r'%t', _ANSI_TAB), # tab
(r'%T', _ANSI_TAB), #
(r'%b', _ANSI_SPACE), # space
(r'%B', _ANSI_SPACE),
(r'%cf', _ANSI_BLINK), # annoying and not supported by all clients
(r'%ci', _ANSI_INVERSE), # invert
(r'%cr', _ANSI_RED),
(r'%cg', _ANSI_GREEN),
(r'%cy', _ANSI_YELLOW),
(r'%cb', _ANSI_BLUE),
(r'%cm', _ANSI_MAGENTA),
(r'%cc', _ANSI_CYAN),
(r'%cw', _ANSI_WHITE),
(r'%cx', _ANSI_BLACK),
(r'%cR', _ANSI_BACK_RED),
(r'%cG', _ANSI_BACK_GREEN),
(r'%cY', _ANSI_BACK_YELLOW),
(r'%cB', _ANSI_BACK_BLUE),
(r'%cM', _ANSI_BACK_MAGENTA),
(r'%cC', _ANSI_BACK_CYAN),
(r'%cW', _ANSI_BACK_WHITE),
(r'%cX', _ANSI_BACK_BLACK)
(r"%cn", _ANSI_NORMAL), # reset
(r"%ch", _ANSI_HILITE), # highlight
(r"%r", _ANSI_RETURN), # line break
(r"%R", _ANSI_RETURN), #
(r"%t", _ANSI_TAB), # tab
(r"%T", _ANSI_TAB), #
(r"%b", _ANSI_SPACE), # space
(r"%B", _ANSI_SPACE),
(r"%cf", _ANSI_BLINK), # annoying and not supported by all clients
(r"%ci", _ANSI_INVERSE), # invert
(r"%cr", _ANSI_RED),
(r"%cg", _ANSI_GREEN),
(r"%cy", _ANSI_YELLOW),
(r"%cb", _ANSI_BLUE),
(r"%cm", _ANSI_MAGENTA),
(r"%cc", _ANSI_CYAN),
(r"%cw", _ANSI_WHITE),
(r"%cx", _ANSI_BLACK),
(r"%cR", _ANSI_BACK_RED),
(r"%cG", _ANSI_BACK_GREEN),
(r"%cY", _ANSI_BACK_YELLOW),
(r"%cB", _ANSI_BACK_BLUE),
(r"%cM", _ANSI_BACK_MAGENTA),
(r"%cC", _ANSI_BACK_CYAN),
(r"%cW", _ANSI_BACK_WHITE),
(r"%cX", _ANSI_BACK_BLACK),
]
MUX_COLOR_XTERM256_EXTRA_FG = [r'%c([0-5])([0-5])([0-5])'] # %c123 - foreground colour
MUX_COLOR_XTERM256_EXTRA_BG = [r'%c\[([0-5])([0-5])([0-5])'] # %c[123 - background colour
MUX_COLOR_XTERM256_EXTRA_GFG = [r'%c=([a-z])'] # %c=a - greyscale foreground
MUX_COLOR_XTERM256_EXTRA_GBG = [r'%c\[=([a-z])'] # %c[=a - greyscale background
MUX_COLOR_XTERM256_EXTRA_FG = [r"%c([0-5])([0-5])([0-5])"] # %c123 - foreground colour
MUX_COLOR_XTERM256_EXTRA_BG = [r"%c\[([0-5])([0-5])([0-5])"] # %c[123 - background colour
MUX_COLOR_XTERM256_EXTRA_GFG = [r"%c=([a-z])"] # %c=a - greyscale foreground
MUX_COLOR_XTERM256_EXTRA_GBG = [r"%c\[=([a-z])"] # %c[=a - greyscale background
MUX_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP = [
(r'%ch%cR', r'%c[500'),
(r'%ch%cG', r'%c[050'),
(r'%ch%cY', r'%c[550'),
(r'%ch%cB', r'%c[005'),
(r'%ch%cM', r'%c[505'),
(r'%ch%cC', r'%c[055'),
(r'%ch%cW', r'%c[555'), # white background
(r'%ch%cX', r'%c[222'), # dark grey background
(r"%ch%cR", r"%c[500"),
(r"%ch%cG", r"%c[050"),
(r"%ch%cY", r"%c[550"),
(r"%ch%cB", r"%c[005"),
(r"%ch%cM", r"%c[505"),
(r"%ch%cC", r"%c[055"),
(r"%ch%cW", r"%c[555"), # white background
(r"%ch%cX", r"%c[222"), # dark grey background
]

View file

@ -38,22 +38,28 @@ from django.conf import settings
from evennia import DefaultScript
from evennia.utils.create import create_script
from evennia.utils import gametime
# The game time speedup / slowdown relative real time
TIMEFACTOR = settings.TIME_FACTOR
# These are the unit names understood by the scheduler.
# Each unit must be consistent and expressed in seconds.
UNITS = getattr(settings, "TIME_UNITS", {
# default custom calendar
"sec": 1,
"min": 60,
"hr": 60 * 60,
"hour": 60 * 60,
"day": 60 * 60 * 24,
"week": 60 * 60 * 24 * 7,
"month": 60 * 60 * 24 * 7 * 4,
"yr": 60 * 60 * 24 * 7 * 4 * 12,
"year": 60 * 60 * 24 * 7 * 4 * 12, })
UNITS = getattr(
settings,
"TIME_UNITS",
{
# default custom calendar
"sec": 1,
"min": 60,
"hr": 60 * 60,
"hour": 60 * 60,
"day": 60 * 60 * 24,
"week": 60 * 60 * 24 * 7,
"month": 60 * 60 * 24 * 7 * 4,
"yr": 60 * 60 * 24 * 7 * 4 * 12,
"year": 60 * 60 * 24 * 7 * 4 * 12,
},
)
def time_to_tuple(seconds, *divisors):
@ -111,8 +117,7 @@ def gametime_to_realtime(format=False, **kwargs):
name = name[:-1]
if name not in UNITS:
raise ValueError("the unit {} isn't defined as a valid "
"game time unit".format(name))
raise ValueError("the unit {} isn't defined as a valid " "game time unit".format(name))
rtime += value * UNITS[name]
rtime /= TIMEFACTOR
if format:
@ -120,8 +125,7 @@ def gametime_to_realtime(format=False, **kwargs):
return rtime
def realtime_to_gametime(secs=0, mins=0, hrs=0, days=0, weeks=0,
months=0, yrs=0, format=False):
def realtime_to_gametime(secs=0, mins=0, hrs=0, days=0, weeks=0, months=0, yrs=0, format=False):
"""
This method calculates how much in-game time a real-world time
interval would correspond to. This is usually a lot less
@ -139,8 +143,15 @@ def realtime_to_gametime(secs=0, mins=0, hrs=0, days=0, weeks=0,
realtime_to_gametime(days=2) -> number of game-world seconds
"""
gtime = TIMEFACTOR * (secs + mins * 60 + hrs * 3600 + days * 86400 +
weeks * 604800 + months * 2628000 + yrs * 31536000)
gtime = TIMEFACTOR * (
secs
+ mins * 60
+ hrs * 3600
+ days * 86400
+ weeks * 604800
+ months * 2628000
+ yrs * 31536000
)
if format:
units = sorted(set(UNITS.values()), reverse=True)
# Remove seconds from the tuple
@ -258,14 +269,19 @@ def schedule(callback, repeat=False, **kwargs):
"""
seconds = real_seconds_until(**kwargs)
script = create_script("evennia.contrib.custom_gametime.GametimeScript",
key="GametimeScript", desc="A timegame-sensitive script",
interval=seconds, start_delay=True,
repeats=-1 if repeat else 1)
script = create_script(
"evennia.contrib.custom_gametime.GametimeScript",
key="GametimeScript",
desc="A timegame-sensitive script",
interval=seconds,
start_delay=True,
repeats=-1 if repeat else 1,
)
script.db.callback = callback
script.db.gametime = kwargs
return script
# Scripts dealing in gametime (use `schedule` to create it)

View file

@ -95,7 +95,7 @@ def roll_dice(dicenum, dicetype, modifier=None, conditional=None, return_tuple=F
if modifier:
# make sure to check types well before eval
mod, modvalue = modifier
if mod not in ('+', '-', '*', '/'):
if mod not in ("+", "-", "*", "/"):
raise TypeError("Non-supported dice modifier: %s" % mod)
modvalue = int(modvalue) # for safety
result = eval("%s %s %s" % (result, mod, modvalue))
@ -103,7 +103,7 @@ def roll_dice(dicenum, dicetype, modifier=None, conditional=None, return_tuple=F
if conditional:
# make sure to check types well before eval
cond, condvalue = conditional
if cond not in ('>', '<', '>=', '<=', '!=', '=='):
if cond not in (">", "<", ">=", "<=", "!=", "=="):
raise TypeError("Non-supported dice result conditional: %s" % conditional)
condvalue = int(condvalue) # for safety
outcome = eval("%s %s %s" % (result, cond, condvalue)) # True/False
@ -166,9 +166,11 @@ class CmdDice(default_cmds.MuxCommand):
modifier = None
conditional = None
if len_parts < 3 or parts[1] != 'd':
self.caller.msg("You must specify the die roll(s) as <nr>d<sides>."
" For example, 2d6 means rolling a 6-sided die 2 times.")
if len_parts < 3 or parts[1] != "d":
self.caller.msg(
"You must specify the die roll(s) as <nr>d<sides>."
" For example, 2d6 means rolling a 6-sided die 2 times."
)
return
# Limit the number of dice and sides a character can roll to prevent server slow down and crashes
@ -184,7 +186,7 @@ class CmdDice(default_cmds.MuxCommand):
pass
elif len_parts == 5:
# either e.g. 1d6 + 3 or something like 1d6 > 3
if parts[3] in ('+', '-', '*', '/'):
if parts[3] in ("+", "-", "*", "/"):
modifier = (parts[3], parts[4])
else: # assume it is a conditional
conditional = (parts[3], parts[4])
@ -198,14 +200,14 @@ class CmdDice(default_cmds.MuxCommand):
return
# do the roll
try:
result, outcome, diff, rolls = roll_dice(ndice,
nsides,
modifier=modifier,
conditional=conditional,
return_tuple=True)
result, outcome, diff, rolls = roll_dice(
ndice, nsides, modifier=modifier, conditional=conditional, return_tuple=True
)
except ValueError:
self.caller.msg("You need to enter valid integer numbers, modifiers and operators."
" |w%s|n was not understood." % self.args)
self.caller.msg(
"You need to enter valid integer numbers, modifiers and operators."
" |w%s|n was not understood." % self.args
)
return
# format output
if len(rolls) > 1:
@ -222,13 +224,13 @@ class CmdDice(default_cmds.MuxCommand):
roomrollstring = "%s rolls %s%s."
resultstring = " Roll(s): %s. Total result is |w%s|n."
if 'secret' in self.switches:
if "secret" in self.switches:
# don't echo to the room at all
string = yourollstring % (argstring, " (secret, not echoed)")
string += "\n" + resultstring % (rolls, result)
string += outcomestring + " (not echoed)"
self.caller.msg(string)
elif 'hidden' in self.switches:
elif "hidden" in self.switches:
# announce the roll to the room, result only to caller
string = yourollstring % (argstring, " (hidden)")
self.caller.msg(string)

View file

@ -39,11 +39,18 @@ 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.default import (
unloggedin as default_unloggedin,
) # Used in CmdUnconnectedCreate
# limit symbol import for API
__all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
"CmdUnconnectedQuit", "CmdUnconnectedLook", "CmdUnconnectedHelp")
__all__ = (
"CmdUnconnectedConnect",
"CmdUnconnectedCreate",
"CmdUnconnectedQuit",
"CmdUnconnectedLook",
"CmdUnconnectedHelp",
)
MULTISESSION_MODE = settings.MULTISESSION_MODE
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
@ -54,8 +61,10 @@ except Exception:
# malformed connection screen or no screen given
pass
if not CONNECTION_SCREEN:
CONNECTION_SCREEN = "\nEvennia: Error in CONNECTION_SCREEN MODULE" \
" (randomly picked connection screen variable is not a string). \nEnter 'help' for aid."
CONNECTION_SCREEN = (
"\nEvennia: Error in CONNECTION_SCREEN MODULE"
" (randomly picked connection screen variable is not a string). \nEnter 'help' for aid."
)
class CmdUnconnectedConnect(MuxCommand):
@ -67,6 +76,7 @@ class CmdUnconnectedConnect(MuxCommand):
Use the create command to first create an account before logging in.
"""
key = "connect"
aliases = ["conn", "con", "co"]
locks = "cmd:all()" # not really needed
@ -105,8 +115,10 @@ class CmdUnconnectedConnect(MuxCommand):
# Check IP and/or name bans
bans = ServerConfig.objects.conf("server_bans")
if bans and (any(tup[0] == account.name for tup in bans) or
any(tup[2].match(session.address[0]) for tup in bans if tup[2])):
if bans and (
any(tup[0] == account.name for tup in bans)
or any(tup[2].match(session.address[0]) for tup in bans if tup[2])
):
# this is a banned IP or name!
string = "|rYou have been banned and cannot continue from here."
string += "\nIf you feel this ban is in error, please email an admin.|x"
@ -128,6 +140,7 @@ class CmdUnconnectedCreate(MuxCommand):
This creates a new account account.
"""
key = "create"
aliases = ["cre", "cr"]
locks = "cmd:all()"
@ -152,7 +165,7 @@ class CmdUnconnectedCreate(MuxCommand):
else:
accountname, email, password = self.arglist
accountname = accountname.replace('"', '') # remove "
accountname = accountname.replace('"', "") # remove "
accountname = accountname.replace("'", "")
self.accountinfo = (accountname, email, password)
@ -163,7 +176,7 @@ class CmdUnconnectedCreate(MuxCommand):
try:
accountname, email, password = self.accountinfo
except ValueError:
string = "\n\r Usage (without <>): create \"<accountname>\" <email> <password>"
string = '\n\r Usage (without <>): create "<accountname>" <email> <password>'
session.msg(string)
return
if not email or not password:
@ -192,24 +205,32 @@ class CmdUnconnectedCreate(MuxCommand):
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):
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."
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])):
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"
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.")
return
@ -218,15 +239,21 @@ class CmdUnconnectedCreate(MuxCommand):
try:
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
typeclass = settings.BASE_CHARACTER_TYPECLASS
new_account = default_unloggedin._create_account(session, accountname, password, permissions, email=email)
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)
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\" <your password>'."
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))
@ -246,6 +273,7 @@ class CmdUnconnectedQuit(MuxCommand):
here for unconnected accounts for the sake of simplicity. The logged in
version is a bit more complicated.
"""
key = "quit"
aliases = ["q", "qu"]
locks = "cmd:all()"
@ -263,6 +291,7 @@ class CmdUnconnectedLook(MuxCommand):
This is called by the server and kicks everything in gear.
All it does is display the connect screen.
"""
key = CMD_LOGINSTART
aliases = ["look", "l"]
locks = "cmd:all()"
@ -277,6 +306,7 @@ class CmdUnconnectedHelp(MuxCommand):
This is an unconnected version of the help command,
for simplicity. It shows a pane of info.
"""
key = "help"
aliases = ["h", "?"]
locks = "cmd:all()"
@ -284,8 +314,7 @@ class CmdUnconnectedHelp(MuxCommand):
def func(self):
"""Shows help"""
string = \
"""
string = """
You are not yet logged into the game. Commands available at this point:
|wcreate, connect, look, help, quit|n
@ -316,10 +345,12 @@ You can use the |wlook|n command if you want to see the connect screen again.
# command set for the mux-like login
class UnloggedinCmdSet(CmdSet):
"""
Sets up the unlogged cmdset.
"""
key = "Unloggedin"
priority = 0

View file

@ -35,7 +35,7 @@ from evennia import syscmdkeys
from evennia.utils import variable_from_module
from .utils import create_evscaperoom_object
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
_RE_ARGSPLIT = re.compile(r"\s(with|on|to|in|at)\s", re.I + re.U)
_RE_EMOTE_SPEECH = re.compile(r"(\".*?\")|(\'.*?\')")
@ -111,6 +111,7 @@ class CmdEvscapeRoom(Command):
command [<obj1>|<arg1>] [<prep> <obj2>|<arg2>]
"""
# always separate the command from any args with a space
arg_regex = r"(/\w+?(\s|$))|\s|$"
help_category = "Evscaperoom"
@ -210,6 +211,7 @@ class CmdGiveUp(CmdEvscapeRoom):
Abandons your attempts at escaping and of ever winning the pie-eating contest.
"""
key = "give up"
aliases = ("abort", "chicken out", "quit", "q")
@ -224,10 +226,10 @@ class CmdGiveUp(CmdEvscapeRoom):
warning = _QUIT_WARNING_CAN_COME_BACK.format(roomname=self.room.name)
warning = _QUIT_WARNING.format(warning=warning)
ret = yield(warning)
ret = yield (warning)
if ret.upper() == "QUIT":
self.msg("|R ... Oh. Okay then. Off you go.|n\n")
yield(1)
yield (1)
self.room.log(f"QUIT: {self.caller.key} used the quit command")
@ -250,6 +252,7 @@ class CmdLook(CmdEvscapeRoom):
look [obj]
"""
key = "look"
aliases = ["l", "ls"]
obj1_search = None
@ -276,6 +279,7 @@ class CmdWho(CmdEvscapeRoom, default_cmds.CmdWho):
server as a whole.
"""
key = "who"
obj1_search = False
@ -285,7 +289,7 @@ class CmdWho(CmdEvscapeRoom, default_cmds.CmdWho):
caller = self.caller
if self.args == 'all':
if self.args == "all":
table = self.style_table("|wName", "|wRoom")
sessions = SESSION_HANDLER.get_sessions()
for session in sessions:
@ -298,15 +302,19 @@ class CmdWho(CmdEvscapeRoom, default_cmds.CmdWho):
account = session.get_account()
table.add_row(account.get_display_name(caller), "(OOC)")
txt = (f"|cPlayers active on this server|n:\n{table}\n"
"(use 'who' to see only those in your room)")
txt = (
f"|cPlayers active on this server|n:\n{table}\n"
"(use 'who' to see only those in your room)"
)
else:
chars = [f"{obj.get_display_name(caller)} - {obj.db.desc.strip()}"
for obj in self.room.get_all_characters()
if obj != caller]
chars = [
f"{obj.get_display_name(caller)} - {obj.db.desc.strip()}"
for obj in self.room.get_all_characters()
if obj != caller
]
chars = "\n".join([f"{caller.key} - {caller.db.desc.strip()} (you)"] + chars)
txt = (f"|cPlayers in this room (room-name '{self.room.name}')|n:\n {chars}")
txt = f"|cPlayers in this room (room-name '{self.room.name}')|n:\n {chars}"
caller.msg(txt)
@ -320,6 +328,7 @@ class CmdSpeak(Command):
shout
"""
key = "say"
aliases = [";", "shout", "whisper"]
arg_regex = r"\w|\s|$"
@ -329,7 +338,7 @@ class CmdSpeak(Command):
args = self.args.strip()
caller = self.caller
action = self.cmdname
action = "say" if action == ';' else action
action = "say" if action == ";" else action
room = self.caller.location
if not self.args:
@ -365,6 +374,7 @@ class CmdEmote(Command):
emote /me points to /box and /lever.
"""
key = "emote"
aliases = [":", "pose"]
arg_regex = r"\w|\s|$"
@ -379,7 +389,7 @@ class CmdEmote(Command):
emote = self.args.strip()
if not emote:
self.caller.msg("Usage: emote /me points to /door, saying \"look over there!\"")
self.caller.msg('Usage: emote /me points to /door, saying "look over there!"')
return
speech_clr = "|c"
@ -438,6 +448,7 @@ class CmdFocus(CmdEvscapeRoom):
looks and what actions is available.
"""
key = "focus"
aliases = ["examine", "e", "ex", "unfocus"]
@ -454,7 +465,9 @@ class CmdFocus(CmdEvscapeRoom):
return
if self.focus != self.obj1:
self.room.msg_room(self.caller, f"~You ~examine *{self.obj1.key}.", skip_caller=True)
self.room.msg_room(
self.caller, f"~You ~examine *{self.obj1.key}.", skip_caller=True
)
self.focus = self.obj1
self.obj1.at_focus(self.caller)
elif not self.focus:
@ -473,11 +486,13 @@ class CmdOptions(CmdEvscapeRoom):
options
"""
key = "options"
aliases = ["option"]
def func(self):
from .menu import run_option_menu
run_option_menu(self.caller, self.session)
@ -486,6 +501,7 @@ class CmdGet(CmdEvscapeRoom):
Use focus / examine instead.
"""
key = "get"
aliases = ["inventory", "i", "inv", "give"]
@ -501,6 +517,7 @@ class CmdRerouter(default_cmds.MuxCommand):
<action> [arg]
"""
# reroute commands from the default cmdset to the catch-all
# focus function where needed. This allows us to override
# individual default commands without replacing the entire
@ -512,9 +529,10 @@ class CmdRerouter(default_cmds.MuxCommand):
def func(self):
# reroute to another command
from evennia.commands import cmdhandler
cmdhandler.cmdhandler(self.session, self.raw_string,
cmdobj=CmdFocusInteraction(),
cmdobj_key=self.cmdname)
cmdhandler.cmdhandler(
self.session, self.raw_string, cmdobj=CmdFocusInteraction(), cmdobj_key=self.cmdname
)
class CmdFocusInteraction(CmdEvscapeRoom):
@ -532,6 +550,7 @@ class CmdFocusInteraction(CmdEvscapeRoom):
as keys into the method.
"""
# all commands not matching something else goes here.
key = syscmdkeys.CMD_NOMATCH
@ -571,19 +590,18 @@ class CmdStand(CmdEvscapeRoom):
Stand up from whatever position you had.
"""
key = "stand"
def func(self):
# Positionable objects will set this flag on you.
pos = self.caller.attributes.get(
"position", category=self.room.tagcategory)
pos = self.caller.attributes.get("position", category=self.room.tagcategory)
if pos:
# we have a position, clean up.
obj, position = pos
self.caller.attributes.remove(
"position", category=self.room.tagcategory)
self.caller.attributes.remove("position", category=self.room.tagcategory)
del obj.db.positions[self.caller]
self.room.msg_room(self.caller, "~You ~are back standing on the floor again.")
else:
@ -598,8 +616,9 @@ class CmdHelp(CmdEvscapeRoom, default_cmds.CmdHelp):
help <topic> or <command>
"""
key = 'help'
aliases = ['?']
key = "help"
aliases = ["?"]
def func(self):
if self.obj1:
@ -608,8 +627,10 @@ class CmdHelp(CmdEvscapeRoom, default_cmds.CmdHelp):
if not helptxt:
helptxt = f"There is no help to be had about {self.obj1.get_display_name(self.caller)}."
else:
helptxt = (f"|y{self.obj1.get_display_name(self.caller)}|n is "
"likely |rnot|n part of any of the Jester's trickery.")
helptxt = (
f"|y{self.obj1.get_display_name(self.caller)}|n is "
"likely |rnot|n part of any of the Jester's trickery."
)
elif self.arg1:
# fall back to the normal help command
super().func()
@ -621,6 +642,7 @@ class CmdHelp(CmdEvscapeRoom, default_cmds.CmdHelp):
# Debug/help command
class CmdCreateObj(CmdEvscapeRoom):
"""
Create command, only for Admins during debugging.
@ -631,6 +653,7 @@ class CmdCreateObj(CmdEvscapeRoom):
Here, :typeclass is a class in evscaperoom.commands
"""
key = "createobj"
aliases = ["cobj"]
locks = "cmd:perm(Admin)"
@ -668,6 +691,7 @@ class CmdSetFlag(CmdEvscapeRoom):
flag <obj> with <flagname>
"""
key = "flag"
aliases = ["setflag"]
locks = "cmd:perm(Admin)"
@ -700,6 +724,7 @@ class CmdJumpState(CmdEvscapeRoom):
jumpstate <statename>
"""
key = "jumpstate"
locks = "cmd:perm(Admin)"
@ -713,22 +738,26 @@ class CmdJumpState(CmdEvscapeRoom):
# Helper command to start the Evscaperoom menu
class CmdEvscapeRoomStart(Command):
"""
Go to the Evscaperoom start menu
"""
key = "evscaperoom"
help_category = "EvscapeRoom"
def func(self):
# need to import here to break circular import
from .menu import run_evscaperoom_menu
run_evscaperoom_menu(self.caller)
# command sets
class CmdSetEvScapeRoom(CmdSet):
priority = 1

View file

@ -55,7 +55,7 @@ def _move_to_room(caller, raw_string, **kwargs):
Helper to move a user to a room
"""
room = kwargs['room']
room = kwargs["room"]
room.msg_char(caller, f"Entering room |c'{room.name}'|n ...")
room.msg_room(caller, f"~You |c~were just tricked in here too!|n")
# we do a manual move since we don't want all hooks to fire.
@ -78,7 +78,9 @@ def _create_new_room(caller, raw_string, **kwargs):
_move_to_room(caller, "", room=room)
nrooms = EvscapeRoom.objects.all().count()
logger.log_info(f"Evscaperoom: {caller.key} created room '{key}' (#{room.id}). Now {nrooms} room(s) active.")
logger.log_info(
f"Evscaperoom: {caller.key} created room '{key}' (#{room.id}). Now {nrooms} room(s) active."
)
room.log(f"JOIN: {caller.key} created and joined room")
return "node_quit", {"quiet": True}
@ -96,10 +98,12 @@ def _get_all_rooms(caller):
if not room.pk or room.db.deleting:
continue
stats = room.db.stats or {"progress": 0}
progress = int(stats['progress'])
progress = int(stats["progress"])
nplayers = len(room.get_all_characters())
desc = (f"Join room |c'{room.get_display_name(caller)}'|n "
f"(complete: {progress}%, players: {nplayers})")
desc = (
f"Join room |c'{room.get_display_name(caller)}'|n "
f"(complete: {progress}%, players: {nplayers})"
)
room_map[desc] = room
room_option_descs.append(desc)
caller.ndb._menutree.room_map = room_map
@ -121,24 +125,34 @@ def node_start(caller, raw_string, **kwargs):
# build a list of available rooms
options = (
{"key": ("|y[s]et your description|n", "set your description",
"set", "desc", "description", "s"),
"goto": "node_set_desc"},
{"key": ("|y[c]reate/join a new room|n", "create a new room", "create", "c"),
"goto": "node_create_room"},
{"key": ("|r[q]uit the challenge", "quit", "q"),
"goto": "node_quit"})
{
"key": (
"|y[s]et your description|n",
"set your description",
"set",
"desc",
"description",
"s",
),
"goto": "node_set_desc",
},
{
"key": ("|y[c]reate/join a new room|n", "create a new room", "create", "c"),
"goto": "node_create_room",
},
{"key": ("|r[q]uit the challenge", "quit", "q"), "goto": "node_quit"},
)
return text, options
def node_set_desc(caller, raw_string, **kwargs):
current_desc = kwargs.get('desc', caller.db.desc)
current_desc = kwargs.get("desc", caller.db.desc)
text = ("Your current description is\n\n "
f" \"{current_desc}\""
"\n\nEnter your new description!")
text = (
"Your current description is\n\n " f' "{current_desc}"' "\n\nEnter your new description!"
)
def _temp_description(caller, raw_string, **kwargs):
desc = raw_string.strip()
@ -154,12 +168,10 @@ def node_set_desc(caller, raw_string, **kwargs):
return "node_start"
options = (
{"key": "_default",
"goto": _temp_description},
{"key": ("|g[a]ccept", "a"),
"goto": (_set_description, {"desc": current_desc})},
{"key": ("|r[c]ancel", "c"),
"goto": "node_start"})
{"key": "_default", "goto": _temp_description},
{"key": ("|g[a]ccept", "a"), "goto": (_set_description, {"desc": current_desc})},
{"key": ("|r[c]ancel", "c"), "goto": "node_start"},
)
return text, options
@ -168,34 +180,31 @@ def node_create_room(caller, raw_string, **kwargs):
text = _CREATE_ROOM_TEXT
options = (
{"key": ("|g[c]reate new room and start game|n", "c"),
"goto": _create_new_room},
{"key": ("|r[a]bort and go back|n", "a"),
"goto": "node_start"})
{"key": ("|g[c]reate new room and start game|n", "c"), "goto": _create_new_room},
{"key": ("|r[a]bort and go back|n", "a"), "goto": "node_start"},
)
return text, options
def node_join_room(caller, raw_string, **kwargs):
room = kwargs['room']
room = kwargs["room"]
stats = room.db.stats or {"progress": 0}
players = [char.key for char in room.get_all_characters()]
text = _JOIN_EXISTING_ROOM_TEXT.format(
roomname=room.get_display_name(caller),
percent=int(stats['progress']),
percent=int(stats["progress"]),
nplayers=len(players),
players=list_to_string(players)
players=list_to_string(players),
)
options = (
{"key": ("|g[a]ccept|n (default)", "a"),
"goto": (_move_to_room, kwargs)},
{"key": ("|r[c]ancel|n", "c"),
"goto": "node_start"},
{"key": "_default",
"goto": (_move_to_room, kwargs)})
{"key": ("|g[a]ccept|n (default)", "a"), "goto": (_move_to_room, kwargs)},
{"key": ("|r[c]ancel|n", "c"), "goto": "node_start"},
{"key": "_default", "goto": (_move_to_room, kwargs)},
)
return text, options
@ -210,9 +219,10 @@ def node_quit(caller, raw_string, **kwargs):
if caller.db.evscaperoom_standalone:
from evennia.commands import cmdhandler
from evennia import default_cmds
cmdhandler.cmdhandler(caller.ndb._menutree._session, "",
cmdobj=default_cmds.CmdQuit(),
cmdobj_key="@quit")
cmdhandler.cmdhandler(
caller.ndb._menutree._session, "", cmdobj=default_cmds.CmdQuit(), cmdobj_key="@quit"
)
return text, None # empty options exit the menu
@ -222,10 +232,11 @@ class EvscaperoomMenu(EvMenu):
Custom menu with a different formatting of options.
"""
node_border_char = "~"
def nodetext_formatter(self, text):
return justify(text.strip("\n").rstrip(), align='c', indent=1)
return justify(text.strip("\n").rstrip(), align="c", indent=1)
def options_formatter(self, optionlist):
main_options = []
@ -237,9 +248,7 @@ class EvscaperoomMenu(EvMenu):
main_options.append(key)
main_options = " | ".join(main_options)
room_choices = super().options_formatter(room_choices)
return "{}{}{}".format(main_options,
"\n\n" if room_choices else "",
room_choices)
return "{}{}{}".format(main_options, "\n\n" if room_choices else "", room_choices)
# access function
@ -248,20 +257,22 @@ def run_evscaperoom_menu(caller):
Run room selection menu
"""
menutree = {"node_start": node_start,
"node_quit": node_quit,
"node_set_desc": node_set_desc,
"node_create_room": node_create_room,
"node_join_room": node_join_room}
menutree = {
"node_start": node_start,
"node_quit": node_quit,
"node_set_desc": node_set_desc,
"node_create_room": node_create_room,
"node_join_room": node_join_room,
}
EvscaperoomMenu(caller, menutree, startnode="node_start",
cmd_on_exit=None, auto_quit=True)
EvscaperoomMenu(caller, menutree, startnode="node_start", cmd_on_exit=None, auto_quit=True)
# ------------------------------------------------------------
# In-game Options menu
# ------------------------------------------------------------
def _set_thing_style(caller, raw_string, **kwargs):
room = caller.location
options = caller.attributes.get("options", category=room.tagcategory, default={})
@ -272,7 +283,7 @@ def _set_thing_style(caller, raw_string, **kwargs):
def _toggle_screen_reader(caller, raw_string, **kwargs):
session = kwargs['session']
session = kwargs["session"]
# flip old setting
session.protocol_flags["SCREENREADER"] = not session.protocol_flags.get("SCREENREADER", False)
# sync setting with portal
@ -287,22 +298,33 @@ def node_options(caller, raw_string, **kwargs):
options = caller.attributes.get("options", category=room.tagcategory, default={})
things_style = options.get("things_style", 2)
session = kwargs['session'] # we give this as startnode_input when starting menu
session = kwargs["session"] # we give this as startnode_input when starting menu
screenreader = session.protocol_flags.get("SCREENREADER", False)
options = (
{"desc": "{}No item markings (hard mode)".format(
"|g(*)|n " if things_style == 0 else "( ) "),
"goto": (_set_thing_style, {"value": 0, 'session': session})},
{"desc": "{}Items marked as |yitem|n (with color)".format(
"|g(*)|n " if things_style == 1 else "( ) "),
"goto": (_set_thing_style, {"value": 1, 'session': session})},
{"desc": "{}Items are marked as |y[item]|n (screenreader friendly)".format(
"|g(*)|n " if things_style == 2 else "( ) "),
"goto": (_set_thing_style, {"value": 2, 'session': session})},
{"desc": "{}Screenreader mode".format(
"(*) " if screenreader else "( ) "),
"goto": (_toggle_screen_reader, kwargs)})
{
"desc": "{}No item markings (hard mode)".format(
"|g(*)|n " if things_style == 0 else "( ) "
),
"goto": (_set_thing_style, {"value": 0, "session": session}),
},
{
"desc": "{}Items marked as |yitem|n (with color)".format(
"|g(*)|n " if things_style == 1 else "( ) "
),
"goto": (_set_thing_style, {"value": 1, "session": session}),
},
{
"desc": "{}Items are marked as |y[item]|n (screenreader friendly)".format(
"|g(*)|n " if things_style == 2 else "( ) "
),
"goto": (_set_thing_style, {"value": 2, "session": session}),
},
{
"desc": "{}Screenreader mode".format("(*) " if screenreader else "( ) "),
"goto": (_toggle_screen_reader, kwargs),
},
)
return text, options
@ -310,6 +332,7 @@ class OptionsMenu(EvMenu):
"""
Custom display of Option menu
"""
def node_formatter(self, nodetext, optionstext):
return f"{nodetext}\n\n{optionstext}"
@ -321,5 +344,11 @@ def run_option_menu(caller, session):
"""
menutree = {"node_start": node_options}
OptionsMenu(caller, menutree, startnode="node_start",
cmd_on_exit="look", auto_quit=True, startnode_input=("", {"session": session}))
OptionsMenu(
caller,
menutree,
startnode="node_start",
cmd_on_exit="look",
auto_quit=True,
startnode_input=("", {"session": session}),
)

View file

@ -56,6 +56,7 @@ class EvscaperoomObject(DefaultObject):
Default object base for all objects related to the contrib.
"""
# these will be automatically filtered out by self.parse for
# focus-commands using arguments like (`combine [with] object`)
# override this per-class as necessary.
@ -63,10 +64,7 @@ class EvscaperoomObject(DefaultObject):
# this mapping allows for prettier descriptions of our current
# position
position_prep_map = {"sit": "sitting",
"kneel": "kneeling",
"lie": "lying",
"climb": "standing"}
position_prep_map = {"sit": "sitting", "kneel": "kneeling", "lie": "lying", "climb": "standing"}
def at_object_creation(self):
"""
@ -86,8 +84,9 @@ class EvscaperoomObject(DefaultObject):
@property
def tagcategory(self):
if not self._tagcategory:
self._tagcategory = (self.location.db.tagcategory
if self.location else self.db.tagcategory)
self._tagcategory = (
self.location.db.tagcategory if self.location else self.db.tagcategory
)
return self._tagcategory
@property
@ -162,16 +161,15 @@ class EvscaperoomObject(DefaultObject):
you = caller.key if caller else "they"
first_person, third_person = parse_for_perspectives(string, you=you)
for char in self.room.get_all_characters():
options = char.attributes.get(
"options", category=self.room.tagcategory, default={})
options = char.attributes.get("options", category=self.room.tagcategory, default={})
style = options.get("things_style", 2)
if char == caller:
if not skip_caller:
txt = parse_for_things(first_person, things_style=style)
char.msg((txt, {'type': 'your_action'}))
char.msg((txt, {"type": "your_action"}))
else:
txt = parse_for_things(third_person, things_style=style)
char.msg((txt, {'type': 'others_action'}))
char.msg((txt, {"type": "others_action"}))
def msg_char(self, caller, string, client_type="your_action"):
"""
@ -180,8 +178,7 @@ class EvscaperoomObject(DefaultObject):
"""
# we must clean away markers
first_person, _ = parse_for_perspectives(string)
options = caller.attributes.get(
"options", category=self.room.tagcategory, default={})
options = caller.attributes.get("options", category=self.room.tagcategory, default={})
style = options.get("things_style", 2)
txt = parse_for_things(first_person, things_style=style)
caller.msg((txt, {"type": client_type}))
@ -227,8 +224,7 @@ class EvscaperoomObject(DefaultObject):
"""
if new_position is None:
# reset position
caller.attributes.remove(
"position", category=self.tagcategory)
caller.attributes.remove("position", category=self.tagcategory)
if caller in self.db.positions:
del self.db.positions[caller]
else:
@ -279,8 +275,9 @@ class EvscaperoomObject(DefaultObject):
here.
"""
args = re.sub(r"|".join(r"^{}\s".format(prep) for prep in self.action_prepositions),
"", args)
args = re.sub(
r"|".join(r"^{}\s".format(prep) for prep in self.action_prepositions), "", args
)
return args
def get_cmd_signatures(self):
@ -307,11 +304,11 @@ class EvscaperoomObject(DefaultObject):
command_signatures = sorted(command_signatures)
if len(command_signatures) == 1:
helpstr = (f"It looks like {self.key} may be "
"suitable to {callsigns}.")
helpstr = f"It looks like {self.key} may be " "suitable to {callsigns}."
else:
helpstr = (f"At first glance, it looks like {self.key} might be "
"suitable to {callsigns}.")
helpstr = (
f"At first glance, it looks like {self.key} might be " "suitable to {callsigns}."
)
return command_signatures, helpstr
def get_short_desc(self, full_desc):
@ -319,7 +316,7 @@ class EvscaperoomObject(DefaultObject):
Extract the first sentence from the desc and use as the short desc.
"""
mat = re.match(r"(^.*?[.?!])", full_desc.strip(), re.M+re.U+re.I+re.S)
mat = re.match(r"(^.*?[.?!])", full_desc.strip(), re.M + re.U + re.I + re.S)
if mat:
return mat.group(0).strip()
return full_desc
@ -336,8 +333,7 @@ class EvscaperoomObject(DefaultObject):
callsigns = list_to_string(["*" + sig for sig in command_signatures], endsep="or")
# parse for *thing markers (use these as items)
options = caller.attributes.get(
"options", category=self.room.tagcategory, default={})
options = caller.attributes.get("options", category=self.room.tagcategory, default={})
style = options.get("things_style", 2)
helpstr = helpstr.format(callsigns=callsigns)
@ -354,7 +350,7 @@ class EvscaperoomObject(DefaultObject):
# accept a custom desc
desc = kwargs.get("desc", self.db.desc)
if kwargs.get('unfocused', False):
if kwargs.get("unfocused", False):
# use the shorter description
focused = ""
desc = self.get_short_desc(desc)
@ -364,8 +360,11 @@ class EvscaperoomObject(DefaultObject):
helptxt = kwargs.get("helptxt", f"\n\n({self.get_help(looker)})")
obj, pos = self.get_position(looker)
pos = (f" |w({self.position_prep_map[pos]} on "
f"{obj.get_display_name(looker)})" if obj else "")
pos = (
f" |w({self.position_prep_map[pos]} on " f"{obj.get_display_name(looker)})"
if obj
else ""
)
return f" ~~ |y{self.get_display_name(looker)}|n{focused}{pos}|n ~~\n\n{desc}{helptxt}"
@ -375,6 +374,7 @@ class Feelable(EvscaperoomObject):
Any object that you can feel the surface of.
"""
def at_focus_feel(self, caller, **kwargs):
self.msg_char(caller, f"You feel *{self.key}.")
@ -384,6 +384,7 @@ class Listenable(EvscaperoomObject):
Any object one can listen to.
"""
def at_focus_listen(self, caller, **kwargs):
self.msg_char(caller, f"You listen to *{self.key}")
@ -393,6 +394,7 @@ class Smellable(EvscaperoomObject):
Any object you can smell.
"""
def at_focus_smell(self, caller, **kwargs):
self.msg_char(caller, f"You smell *{self.key}.")
@ -402,6 +404,7 @@ class Rotatable(EvscaperoomObject):
Any object that you can lift up and look at from different angles
"""
rotate_flag = "rotatable"
start_rotatable = True
@ -417,6 +420,7 @@ class Rotatable(EvscaperoomObject):
self.at_rotate(caller)
else:
self.at_cannot_rotate(caller)
at_focus_turn = at_focus_rotate
def at_rotate(self, caller):
@ -432,6 +436,7 @@ class Openable(EvscaperoomObject):
a flag.
"""
# this flag must be set for item to open. None for unlocked.
unlock_flag = "unlocked"
open_flag = "open"
@ -482,6 +487,7 @@ class Readable(EvscaperoomObject):
from a flag.
"""
# this must be set to be able to read. None to
# always be able to read.
@ -515,11 +521,7 @@ class IndexReadable(Readable):
"""
# keys should be lower-key
index = {
"page1": "This is page1",
"page2": "This is page2",
"page two": "page2" # alias
}
index = {"page1": "This is page1", "page2": "This is page2", "page two": "page2"} # alias
def at_focus_read(self, caller, **kwargs):
@ -536,8 +538,10 @@ class IndexReadable(Readable):
self.at_read(caller, topic, entry)
def get_cmd_signatures(self):
txt = (f"You don't have the time to read this from beginning to end. "
"Use *read <topic> to look up something in particular.")
txt = (
f"You don't have the time to read this from beginning to end. "
"Use *read <topic> to look up something in particular."
)
return [], txt
def at_cannot_read(self, caller, topic, *args, **kwargs):
@ -556,10 +560,10 @@ class Movable(EvscaperoomObject):
change.
"""
# these are the possible locations (or directions) to move to
# name: callable
move_positions = {"left": "at_left",
"right": "at_right"}
move_positions = {"left": "at_left", "right": "at_right"}
start_position = "left"
def at_object_creation(self):
@ -571,7 +575,7 @@ class Movable(EvscaperoomObject):
return ["move", "push", "shove left/right"], txt
def at_focus_move(self, caller, **kwargs):
pos = self.parse(kwargs['args'])
pos = self.parse(kwargs["args"])
callfunc_name = self.move_positions.get(pos)
if callfunc_name:
@ -610,6 +614,7 @@ class BaseConsumable(EvscaperoomObject):
a custom object if needed).
"""
consume_flag = "consume"
# may only consume once
one_consume_only = True
@ -647,6 +652,7 @@ class Edible(BaseConsumable):
Any object specifically possible to eat.
"""
consume_flag = "eat"
def at_focus_eat(self, caller, **kwargs):
@ -658,6 +664,7 @@ class Drinkable(BaseConsumable):
Any object specifically possible to drink.
"""
consume_flag = "drink"
def at_focus_drink(self, caller, **kwargs):
@ -679,6 +686,7 @@ class BaseApplicable(EvscaperoomObject):
This acts an an abstract base class.
"""
# the target object this is to be used with must
# have this flag. It'll likely be unique to this
# object combination.
@ -689,7 +697,7 @@ class BaseApplicable(EvscaperoomObject):
Wrap this with the at_focus methods in the child classes
"""
args = self.parse(kwargs['args'])
args = self.parse(kwargs["args"])
if not args:
self.msg_char(caller, "You need to specify a target.")
return
@ -717,6 +725,7 @@ class Usable(BaseApplicable):
Any object that can be used with another object.
"""
target_flag = "usable"
def at_focus_use(self, caller, **kwargs):
@ -736,6 +745,7 @@ class Insertable(BaseApplicable):
This would cover a key, for example.
"""
# this would likely be a custom name
target_flag = "insertable"
@ -759,6 +769,7 @@ class Combinable(BaseApplicable):
a new one.
"""
# the other object must have this flag to be able to be combined
# (this is likely unique for a given combination)
target_flag = "combinable"
@ -767,7 +778,8 @@ class Combinable(BaseApplicable):
new_create_dict = {
"typeclass": "evscaperoom.objects.Combinable",
"key": "sword",
"aliases": ["combined"]}
"aliases": ["combined"],
}
# if set, destroy the two components used to make the new one
destroy_components = True
@ -784,11 +796,12 @@ class Combinable(BaseApplicable):
def at_apply(self, caller, action, other_obj):
create_dict = self.new_create_dict
if "location" not in create_dict:
create_dict['location'] = self.location
create_dict["location"] = self.location
new_obj = create_evscaperoom_object(**create_dict)
if new_obj and self.destroy_components:
self.msg_char(caller,
f"You combine *{self.key} with {other_obj.key} to make {new_obj.key}!")
self.msg_char(
caller, f"You combine *{self.key} with {other_obj.key} to make {new_obj.key}!"
)
other_obj.delete()
self.delete()
@ -800,14 +813,11 @@ class Mixable(EvscaperoomObject):
the ingredients should be 'used' with this object in order
mix, calling at_mix when they do.
"""
# ingredients can check for this before they allow to mix at all
mixer_flag = "mixer"
# ingredients must have these flags and this order
ingredient_recipe = [
"ingredient1",
"ingredient2",
"ingredient3"
]
ingredient_recipe = ["ingredient1", "ingredient2", "ingredient3"]
def at_object_creation(self):
super().at_object_creation()
@ -847,7 +857,9 @@ class Mixable(EvscaperoomObject):
if self.check_mixture():
self.at_mix_success(caller, ingredient, **kwargs)
else:
self.room.log(f"{self.name} mix failure: Tried {' + '.join([ing.key for ing in self.db.ingredients if ing])}")
self.room.log(
f"{self.name} mix failure: Tried {' + '.join([ing.key for ing in self.db.ingredients if ing])}"
)
self.db.ingredients = []
self.at_mix_failure(caller, ingredient, **kwargs)
@ -866,25 +878,31 @@ class HasButtons(EvscaperoomObject):
Any object with buttons to push/press
"""
# mapping keys/aliases to calling method
buttons = {'green button': "at_green_button",
'green': "at_green_button",
'red button': "at_red_button",
'red': "at_red_button"}
buttons = {
"green button": "at_green_button",
"green": "at_green_button",
"red button": "at_red_button",
"red": "at_red_button",
}
def get_cmd_signatures(self):
helptxt = ("It looks like you should be able to operate "
f"*{self.key} by means of "
"{callsigns}.")
helptxt = (
"It looks like you should be able to operate "
f"*{self.key} by means of "
"{callsigns}."
)
return ["push", "press red/green button"], helptxt
def at_focus_press(self, caller, **kwargs):
arg = self.parse(kwargs['args'])
arg = self.parse(kwargs["args"])
callfunc_name = self.buttons.get(arg)
if callfunc_name:
getattr(self, callfunc_name)(caller)
else:
self.at_nomatch(caller)
at_focus_push = at_focus_press
def at_nomatch(self, caller):
@ -903,6 +921,7 @@ class CodeInput(EvscaperoomObject):
to have an effect happen.
"""
# the code of this
code = "PASSWORD"
code_hint = "eight letters A-Z"
@ -912,7 +931,7 @@ class CodeInput(EvscaperoomObject):
def at_focus_code(self, caller, **kwargs):
args = self.parse(kwargs['args'].strip())
args = self.parse(kwargs["args"].strip())
if not args:
self.at_no_code(caller)
@ -967,6 +986,7 @@ class BasePositionable(EvscaperoomObject):
object or not.
"""
def at_object_creation(self):
super().at_object_creation()
# mapping {object: position}.
@ -992,11 +1012,16 @@ class BasePositionable(EvscaperoomObject):
self.at_position(caller, new_pos)
def at_cannot_position(self, caller, position, old_obj, old_pos):
self.msg_char(caller, f"You can't; you are currently {self.position_prep_map[old_pos]} on *{old_obj.key} "
"(better |wstand|n first).")
self.msg_char(
caller,
f"You can't; you are currently {self.position_prep_map[old_pos]} on *{old_obj.key} "
"(better |wstand|n first).",
)
def at_again_position(self, caller, position):
self.msg_char(caller, f"But you are already {self.position_prep_map[position]} on *{self.key}?")
self.msg_char(
caller, f"But you are already {self.position_prep_map[position]} on *{self.key}?"
)
def at_position(self, caller, position):
self.msg_room(caller, f"~You ~{position} on *{self.key}.")
@ -1017,6 +1042,7 @@ class Liable(BasePositionable):
Any object you can lie down on.
"""
def at_focus_lie(self, caller, **kwargs):
super().handle_position(caller, "lie", **kwargs)
@ -1026,6 +1052,7 @@ class Kneelable(BasePositionable):
Any object you can kneel on.
"""
def at_focus_kneel(self, caller, **kwargs):
super().handle_position(caller, "kneel", **kwargs)
@ -1037,6 +1064,7 @@ class Climbable(BasePositionable):
command, which resets your position.
"""
def at_focus_climb(self, caller, **kwargs):
super().handle_position(caller, "climb", **kwargs)
@ -1047,6 +1075,7 @@ class Positionable(Sittable, Liable, Kneelable, Climbable):
supported ways (sit, lie, kneel or climb)
"""
def get_cmd_signatures(self):
txt = "It looks like you can {callsigns} on it."
return ["sit", "lie", "kneel", "climb"], txt

View file

@ -32,7 +32,7 @@ class EvscapeRoom(EvscaperoomObject, DefaultRoom):
super().at_object_creation()
# starting state
self.db.state = None # name
self.db.state = None # name
self.db.prev_state = None
# this is used for tagging of all objects belonging to this
@ -43,11 +43,11 @@ class EvscapeRoom(EvscaperoomObject, DefaultRoom):
# room progress statistics
self.db.stats = {
"progress": 0, # in percent
"score": {}, # reason: score
"score": {}, # reason: score
"max_score": 100,
"hints_used": 0, # total across all states
"hints_total": 41,
"total_achievements": 14
"total_achievements": 14,
}
self.cmdset.add(CmdSetEvScapeRoom, permanent=True)
@ -69,21 +69,21 @@ class EvscapeRoom(EvscaperoomObject, DefaultRoom):
caller = f"[caller.key]: " if caller else ""
logger.log_file(
strip_ansi(f"{caller}{message.strip()}"),
filename=self.tagcategory + ".log")
strip_ansi(f"{caller}{message.strip()}"), filename=self.tagcategory + ".log"
)
def score(self, new_score, reason):
"""
We don't score individually but for everyone in room together.
You can only be scored for a given reason once."""
if reason not in self.db.stats['score']:
if reason not in self.db.stats["score"]:
self.log(f"score: {reason} ({new_score}pts)")
self.db.stats['score'][reason] = new_score
self.db.stats["score"][reason] = new_score
def progress(self, new_progress):
"Progress is what we set it to be (0-100%)"
self.log(f"progress: {new_progress}%")
self.db.stats['progress'] = new_progress
self.db.stats["progress"] = new_progress
def achievement(self, caller, achievement, subtext=""):
"""
@ -96,8 +96,7 @@ class EvscapeRoom(EvscaperoomObject, DefaultRoom):
subtext (str, optional): Eventual subtext/explanation
of the achievement.
"""
achievements = caller.attributes.get(
"achievements", category=self.tagcategory)
achievements = caller.attributes.get("achievements", category=self.tagcategory)
if not achievements:
achievements = {}
if achievement not in achievements:
@ -173,6 +172,7 @@ class EvscapeRoom(EvscaperoomObject, DefaultRoom):
"""
self.log(f"EXIT: {char} left room")
from .menu import run_evscaperoom_menu
self.character_cleanup(char)
char.location = char.home
@ -223,15 +223,18 @@ class EvscapeRoom(EvscaperoomObject, DefaultRoom):
def return_appearance(self, looker, **kwargs):
obj, pos = self.get_position(looker)
pos = (f"\n|x[{self.position_prep_map[pos]} on "
f"{obj.get_display_name(looker)}]|n" if obj else "")
pos = (
f"\n|x[{self.position_prep_map[pos]} on " f"{obj.get_display_name(looker)}]|n"
if obj
else ""
)
admin_only = ""
if self.check_perm(looker, "Admin"):
# only for admins
objs = DefaultObject.objects.filter_family(
db_location=self).exclude(id=looker.id)
admin_only = "\n|xAdmin only: " + \
list_to_string([obj.get_display_name(looker) for obj in objs])
objs = DefaultObject.objects.filter_family(db_location=self).exclude(id=looker.id)
admin_only = "\n|xAdmin only: " + list_to_string(
[obj.get_display_name(looker) for obj in objs]
)
return f"{self.db.desc}{pos}{admin_only}"

View file

@ -13,7 +13,6 @@ from evscaperoom.room import EvscapeRoom
class CleanupScript(DefaultScript):
def at_script_creation(self):
self.key = "evscaperoom_cleanup"

View file

@ -35,11 +35,13 @@ _GA = object.__getattribute__
# handler for managing states on room
class StateHandler(object):
"""
This sits on the room and is used to progress through the states.
"""
def __init__(self, room):
self.room = room
self.current_state_name = room.db.state or _FIRST_STATE
@ -109,12 +111,14 @@ class StateHandler(object):
# base state class
class BaseState(object):
"""
Base object holding all callables for a state. This is here to
allow easy overriding for child states.
"""
next_state = "unset"
# a sequence of hints to describe this state.
hints = []
@ -144,15 +148,20 @@ class BaseState(object):
Wrapper handling state method errors.
"""
@wraps(method)
def decorator(*args, **kwargs):
try:
return method(*args, **kwargs)
except Exception:
logger.log_trace(f"Error in State {__name__}")
self.room.msg_room(None, f"|rThere was an unexpected error in State {__name__}. "
"Please |wreport|r this as an issue.|n")
self.room.msg_room(
None,
f"|rThere was an unexpected error in State {__name__}. "
"Please |wreport|r this as an issue.|n",
)
raise # TODO
return decorator
def __getattribute__(self, key):
@ -176,8 +185,10 @@ class BaseState(object):
# return the next hint in the sequence.
self.room.db.state_hint_level = next_level
self.room.db.stats["hints_used"] += 1
self.room.log(f"HINT: {self.name.split('.')[-1]}, level {next_level + 1} "
f"(total used: {self.room.db.stats['hints_used']})")
self.room.log(
f"HINT: {self.name.split('.')[-1]}, level {next_level + 1} "
f"(total used: {self.room.db.stats['hints_used']})"
)
return self.hints[next_level]
else:
# no more hints for this state
@ -191,8 +202,7 @@ class BaseState(object):
if cinematic:
message = msg_cinematic(message, borders=borders)
if target:
options = target.attributes.get(
"options", category=self.room.tagcategory, default={})
options = target.attributes.get("options", category=self.room.tagcategory, default={})
style = options.get("things_style", 2)
# we assume this is a char
target.msg(parse_for_things(message, things_style=style))
@ -205,7 +215,7 @@ class BaseState(object):
"""
self.msg(message, target=target, borders=True, cinematic=True)
def create_object(self, typeclass=None, key='testobj', location=None, **kwargs):
def create_object(self, typeclass=None, key="testobj", location=None, **kwargs):
"""
This is a convenience-wrapper for quickly building EvscapeRoom objects.
@ -223,8 +233,12 @@ class BaseState(object):
if not location:
location = self.room
return create_evscaperoom_object(
typeclass=typeclass, key=key, location=location,
tags=[("room", self.room.tagcategory.lower())], **kwargs)
typeclass=typeclass,
key=key,
location=location,
tags=[("room", self.room.tagcategory.lower())],
**kwargs,
)
def get_object(self, key):
"""
@ -237,7 +251,8 @@ class BaseState(object):
"""
match = EvscaperoomObject.objects.filter_family(
db_key__iexact=key, db_tags__db_category=self.room.tagcategory.lower())
db_key__iexact=key, db_tags__db_category=self.room.tagcategory.lower()
)
if not match:
logger.log_err(f"get_object: No match for '{key}' in state ")
return None

View file

@ -40,6 +40,7 @@ class Door(objects.Openable):
The door leads out of the room.
"""
start_open = False
def at_object_creation(self):
@ -108,7 +109,6 @@ On the wall is a button marked
class HelpButton(objects.EvscaperoomObject):
def at_focus_push(self, caller, **kwargs):
"this adds the 'push' action to the button"
@ -116,8 +116,9 @@ class HelpButton(objects.EvscaperoomObject):
if hint is None:
self.msg_char(caller, "There are no more hints to be had.")
else:
self.msg_room(caller, f"{caller.key} pushes *button and gets the "
f"hint:\n \"{hint.strip()}\"|n")
self.msg_room(
caller, f"{caller.key} pushes *button and gets the " f'hint:\n "{hint.strip()}"|n'
)
# state
@ -144,9 +145,7 @@ class State(BaseState):
"""
# this makes these hints available to the .get_hint method.
hints = [STATE_HINT_LVL1,
STATE_HINT_LVL2,
STATE_HINT_LVL3]
hints = [STATE_HINT_LVL1, STATE_HINT_LVL2, STATE_HINT_LVL3]
def character_enters(self, char):
"Called when char enters room at this state"
@ -159,16 +158,13 @@ class State(BaseState):
self.room.db.desc = ROOM_DESC
# create the room objects
door = self.create_object(
Door, key="door to the cabin", aliases=["door"])
door = self.create_object(Door, key="door to the cabin", aliases=["door"])
door.db.desc = DOOR_DESC.strip()
key = self.create_object(
Key, key="key", aliases=["room key"])
key = self.create_object(Key, key="key", aliases=["room key"])
key.db.desc = KEY_DESC.strip()
button = self.create_object(
HelpButton, key="button", aliases=["help button"])
button = self.create_object(HelpButton, key="button", aliases=["help button"])
button.db.desc = BUTTON_DESC.strip()
def clean(self):

View file

@ -16,11 +16,9 @@ from . import utils
class TestEvscaperoomCommands(CommandTest):
def setUp(self):
super().setUp()
self.room1 = utils.create_evscaperoom_object(
"evscaperoom.room.EvscapeRoom", key='Testroom')
self.room1 = utils.create_evscaperoom_object("evscaperoom.room.EvscapeRoom", key="Testroom")
self.char1.location = self.room1
self.obj1.location = self.room1
@ -114,7 +112,7 @@ class TestEvscaperoomCommands(CommandTest):
self.assertEqual(cmd.obj1, None)
self.assertEqual(cmd.obj2, self.obj1)
self.assertEqual(cmd.arg1, 'foo')
self.assertEqual(cmd.arg1, "foo")
self.assertEqual(cmd.arg2, None)
cmd = commands.CmdEvscapeRoom()
@ -127,7 +125,7 @@ class TestEvscaperoomCommands(CommandTest):
self.assertEqual(cmd.obj1, self.obj1)
self.assertEqual(cmd.obj2, None)
self.assertEqual(cmd.arg1, None)
self.assertEqual(cmd.arg2, 'foo')
self.assertEqual(cmd.arg2, "foo")
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
@ -152,21 +150,20 @@ class TestEvscaperoomCommands(CommandTest):
cmd.caller = self.char1
cmd.room = self.room1
cmd.focus = self.obj1
self.assertEqual(self.char1.attributes.get(
"focus", category=self.room1.tagcategory), self.obj1)
self.assertEqual(
self.char1.attributes.get("focus", category=self.room1.tagcategory), self.obj1
)
def test_focus(self):
# don't focus on a non-room object
self.call(commands.CmdFocus(), "obj")
self.assertEqual(self.char1.attributes.get(
"focus", category=self.room1.tagcategory), None)
self.assertEqual(self.char1.attributes.get("focus", category=self.room1.tagcategory), None)
# should focus correctly
myobj = utils.create_evscaperoom_object(
objects.EvscaperoomObject, "mytestobj", location=self.room1)
objects.EvscaperoomObject, "mytestobj", location=self.room1
)
self.call(commands.CmdFocus(), "mytestobj")
self.assertEqual(self.char1.attributes.get(
"focus", category=self.room1.tagcategory), myobj)
self.assertEqual(self.char1.attributes.get("focus", category=self.room1.tagcategory), myobj)
def test_look(self):
self.call(commands.CmdLook(), "at obj", "Obj")
@ -181,31 +178,31 @@ class TestEvscaperoomCommands(CommandTest):
self.call(commands.CmdSpeak(), "Hi.", "You whisper: Hi.", cmdstring="whisper")
self.call(commands.CmdSpeak(), "HELLO!", "You shout: HELLO!", cmdstring="shout")
self.call(commands.CmdSpeak(), "Hello to obj",
"You say: Hello", cmdstring="say")
self.call(commands.CmdSpeak(), "Hello to obj",
"You shout: Hello", cmdstring="shout")
self.call(commands.CmdSpeak(), "Hello to obj", "You say: Hello", cmdstring="say")
self.call(commands.CmdSpeak(), "Hello to obj", "You shout: Hello", cmdstring="shout")
def test_emote(self):
self.call(commands.CmdEmote(),
"/me smiles to /obj",
f"Char(#{self.char1.id}) smiles to Obj(#{self.obj1.id})")
self.call(
commands.CmdEmote(),
"/me smiles to /obj",
f"Char(#{self.char1.id}) smiles to Obj(#{self.obj1.id})",
)
def test_focus_interaction(self):
self.call(commands.CmdFocusInteraction(), "", "Hm?")
class TestUtils(EvenniaTest):
def test_overwrite(self):
room = utils.create_evscaperoom_object(
"evscaperoom.room.EvscapeRoom", key='Testroom')
room = utils.create_evscaperoom_object("evscaperoom.room.EvscapeRoom", key="Testroom")
obj1 = utils.create_evscaperoom_object(
objects.EvscaperoomObject, key="testobj", location=room)
objects.EvscaperoomObject, key="testobj", location=room
)
id1 = obj1.id
obj2 = utils.create_evscaperoom_object(
objects.EvscaperoomObject, key="testobj", location=room)
objects.EvscaperoomObject, key="testobj", location=room
)
id2 = obj2.id
# we should have created a new object, deleting the old same-named one
@ -231,14 +228,12 @@ class TestUtils(EvenniaTest):
self.assertEqual(utils.parse_for_things(string, 2), "Looking at |y[book]|n and |y[key]|n.")
class TestEvScapeRoom(EvenniaTest):
def setUp(self):
super().setUp()
self.room = utils.create_evscaperoom_object(
"evscaperoom.room.EvscapeRoom", key='Testroom',
home=self.room1)
"evscaperoom.room.EvscapeRoom", key="Testroom", home=self.room1
)
self.roomtag = "evscaperoom_{}".format(self.room.key)
def tearDown(self):
@ -253,24 +248,21 @@ class TestEvScapeRoom(EvenniaTest):
self.assertEqual(list(room.get_all_characters()), [self.char1])
room.tag_character(self.char1, "opened_door")
self.assertEqual(self.char1.tags.get(
"opened_door", category=self.roomtag), "opened_door")
self.assertEqual(self.char1.tags.get("opened_door", category=self.roomtag), "opened_door")
room.tag_all_characters("tagged_all")
self.assertEqual(self.char1.tags.get(
"tagged_all", category=self.roomtag), "tagged_all")
self.assertEqual(self.char1.tags.get("tagged_all", category=self.roomtag), "tagged_all")
room.character_cleanup(self.char1)
self.assertEqual(self.char1.tags.get(category=self.roomtag), None)
class TestStates(EvenniaTest):
def setUp(self):
super().setUp()
self.room = utils.create_evscaperoom_object(
"evscaperoom.room.EvscapeRoom", key='Testroom',
home=self.room1)
"evscaperoom.room.EvscapeRoom", key="Testroom", home=self.room1
)
self.roomtag = "evscaperoom_#{}".format(self.room.id)
def tearDown(self):
@ -280,7 +272,8 @@ class TestStates(EvenniaTest):
dirname = path.join(path.dirname(__file__), "states")
states = []
for imp, module, ispackage in pkgutil.walk_packages(
path=[dirname], prefix="evscaperoom.states."):
path=[dirname], prefix="evscaperoom.states."
):
mod = mod_import(module)
states.append(mod)
return states

View file

@ -12,12 +12,13 @@ from evennia import create_object, search_object
from evennia.utils import justify, inherits_from
_BASE_TYPECLASS_PATH = "evscaperoom.objects."
_RE_PERSPECTIVE = re.compile(r"~(\w+)", re.I+re.U+re.M)
_RE_THING = re.compile(r"\*(\w+)", re.I+re.U+re.M)
_RE_PERSPECTIVE = re.compile(r"~(\w+)", re.I + re.U + re.M)
_RE_THING = re.compile(r"\*(\w+)", re.I + re.U + re.M)
def create_evscaperoom_object(typeclass=None, key="testobj", location=None,
delete_duplicates=True, **kwargs):
def create_evscaperoom_object(
typeclass=None, key="testobj", location=None, delete_duplicates=True, **kwargs
):
"""
This is a convenience-wrapper for quickly building EvscapeRoom objects. This
is called from the helper-method create_object on states, but is also useful
@ -38,25 +39,29 @@ def create_evscaperoom_object(typeclass=None, key="testobj", location=None,
"""
if not (callable(typeclass) or
typeclass.startswith("evennia") or
typeclass.startswith("typeclasses") or
typeclass.startswith("evscaperoom")):
if not (
callable(typeclass)
or typeclass.startswith("evennia")
or typeclass.startswith("typeclasses")
or typeclass.startswith("evscaperoom")
):
# unless we specify a full typeclass path or the class itself,
# auto-complete it
typeclass = _BASE_TYPECLASS_PATH + typeclass
if delete_duplicates:
old_objs = [obj for obj in search_object(key)
if not inherits_from(obj, "evennia.objects.objects.DefaultCharacter")]
old_objs = [
obj
for obj in search_object(key)
if not inherits_from(obj, "evennia.objects.objects.DefaultCharacter")
]
if location:
# delete only matching objects in the given location
[obj.delete() for obj in old_objs if obj.location == location]
else:
[obj.delete() for obj in old_objs]
new_obj = create_object(typeclass=typeclass, key=key,
location=location, **kwargs)
new_obj = create_object(typeclass=typeclass, key=key, location=location, **kwargs)
return new_obj
@ -74,9 +79,11 @@ def create_fantasy_word(length=5, capitalize=True):
if not length:
return ""
phonemes = ("ea oh ae aa eh ah ao aw ai er ey ow ia ih iy oy ua "
"uh uw a e i u y p b t d f v t dh "
"s z sh zh ch jh k ng g m n l r w").split()
phonemes = (
"ea oh ae aa eh ah ao aw ai er ey ow ia ih iy oy ua "
"uh uw a e i u y p b t d f v t dh "
"s z sh zh ch jh k ng g m n l r w"
).split()
word = [choice(phonemes)]
while len(word) < length:
word.append(choice(phonemes))
@ -113,6 +120,7 @@ def parse_for_perspectives(string, you=None):
"~You ~open"
-> "You open", "Bob opens"
"""
def _replace_third_person(match):
match = match.group(1)
lmatch = match.lower()
@ -122,7 +130,7 @@ def parse_for_perspectives(string, you=None):
if match[0].isupper():
return irregulars[lmatch].capitalize()
return irregulars[lmatch]
elif lmatch[-1] == 's':
elif lmatch[-1] == "s":
return match + "es"
else:
return match + "s" # simple, most normal form
@ -181,7 +189,7 @@ def msg_cinematic(text, borders=True):
"""
text = text.strip()
text = justify(text, align='c', indent=1)
text = justify(text, align="c", indent=1)
if borders:
text = add_msg_borders(text)
return text

View file

@ -94,7 +94,7 @@ from evennia import utils
from evennia import CmdSet
# error return function, needed by Extended Look command
_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
# regexes for in-desc replacements
RE_MORNING = re.compile(r"<morning>(.*?)</morning>", re.IGNORECASE)
@ -103,10 +103,12 @@ RE_EVENING = re.compile(r"<evening>(.*?)</evening>", re.IGNORECASE)
RE_NIGHT = re.compile(r"<night>(.*?)</night>", re.IGNORECASE)
# this map is just a faster way to select the right regexes (the first
# regex in each tuple will be parsed, the following will always be weeded out)
REGEXMAP = {"morning": (RE_MORNING, RE_AFTERNOON, RE_EVENING, RE_NIGHT),
"afternoon": (RE_AFTERNOON, RE_MORNING, RE_EVENING, RE_NIGHT),
"evening": (RE_EVENING, RE_MORNING, RE_AFTERNOON, RE_NIGHT),
"night": (RE_NIGHT, RE_MORNING, RE_AFTERNOON, RE_EVENING)}
REGEXMAP = {
"morning": (RE_MORNING, RE_AFTERNOON, RE_EVENING, RE_NIGHT),
"afternoon": (RE_AFTERNOON, RE_MORNING, RE_EVENING, RE_NIGHT),
"evening": (RE_EVENING, RE_MORNING, RE_AFTERNOON, RE_NIGHT),
"night": (RE_NIGHT, RE_MORNING, RE_AFTERNOON, RE_EVENING),
}
# set up the seasons and time slots. This assumes gametime started at the
# beginning of the year (so month 1 is equivalent to January), and that
@ -119,6 +121,7 @@ DAY_BOUNDARIES = (0, 6 / 24.0, 12 / 24.0, 18 / 24.0)
# implements the Extended Room
class ExtendedRoom(DefaultRoom):
"""
This room implements a more advanced `look` functionality depending on
@ -265,7 +268,6 @@ class ExtendedRoom(DefaultRoom):
if self.db.details and detailkey.lower() in self.db.details:
del self.db.details[detailkey.lower()]
def return_appearance(self, looker, **kwargs):
"""
This is called when e.g. the look command wants to retrieve
@ -321,6 +323,7 @@ class ExtendedRoom(DefaultRoom):
# Custom Look command supporting Room details. Add this to
# the Default cmdset to use.
class CmdExtendedRoomLook(default_cmds.CmdLook):
"""
look
@ -341,15 +344,21 @@ class CmdExtendedRoomLook(default_cmds.CmdLook):
caller = self.caller
args = self.args
if args:
looking_at_obj = caller.search(args,
candidates=caller.location.contents + caller.contents,
use_nicks=True,
quiet=True)
looking_at_obj = caller.search(
args,
candidates=caller.location.contents + caller.contents,
use_nicks=True,
quiet=True,
)
if not looking_at_obj:
# no object found. Check if there is a matching
# detail at location.
location = caller.location
if location and hasattr(location, "return_detail") and callable(location.return_detail):
if (
location
and hasattr(location, "return_detail")
and callable(location.return_detail)
):
detail = location.return_detail(args)
if detail:
# we found a detail instead. Show that.
@ -367,7 +376,7 @@ class CmdExtendedRoomLook(default_cmds.CmdLook):
caller.msg("You have no location to look at!")
return
if not hasattr(looking_at_obj, 'return_appearance'):
if not hasattr(looking_at_obj, "return_appearance"):
# this is likely due to us having an account instead
looking_at_obj = looking_at_obj.character
if not looking_at_obj.access(caller, "view"):
@ -382,6 +391,7 @@ class CmdExtendedRoomLook(default_cmds.CmdLook):
# Custom build commands for setting seasonal descriptions
# and detailing extended rooms.
class CmdExtendedRoomDesc(default_cmds.CmdDesc):
"""
`desc` - describe an object or room.
@ -411,6 +421,7 @@ class CmdExtendedRoomDesc(default_cmds.CmdDesc):
version of the `desc` command.
"""
aliases = ["describe"]
switch_options = () # Inherits from default_cmds.CmdDesc, but unused here
@ -442,13 +453,13 @@ class CmdExtendedRoomDesc(default_cmds.CmdDesc):
if not location:
caller.msg("No location was found!")
return
if switch == 'spring':
if switch == "spring":
location.db.spring_desc = self.args
elif switch == 'summer':
elif switch == "summer":
location.db.summer_desc = self.args
elif switch == 'autumn':
elif switch == "autumn":
location.db.autumn_desc = self.args
elif switch == 'winter':
elif switch == "winter":
location.db.winter_desc = self.args
# clear flag to force an update
self.reset_times(location)
@ -498,6 +509,7 @@ class CmdExtendedRoomDetail(default_cmds.MuxCommand):
To remove one or several details, use the @detail/del switch.
"""
key = "@detail"
locks = "cmd:perm(Builder)"
help_category = "Building"
@ -537,6 +549,7 @@ class CmdExtendedRoomDetail(default_cmds.MuxCommand):
# Simple command to view the current time and season
class CmdExtendedRoomGameTime(default_cmds.MuxCommand):
"""
Check the game time
@ -546,6 +559,7 @@ class CmdExtendedRoomGameTime(default_cmds.MuxCommand):
Shows the current in-game time and season.
"""
key = "time"
locks = "cmd:all()"
help_category = "General"
@ -565,11 +579,13 @@ class CmdExtendedRoomGameTime(default_cmds.MuxCommand):
# CmdSet for easily install all commands
class ExtendedRoomCmdSet(CmdSet):
"""
Groups the extended-room commands.
"""
def at_cmdset_creation(self):
self.add(CmdExtendedRoomLook)
self.add(CmdExtendedRoomDesc)

View file

@ -166,9 +166,18 @@ class FieldEvMenu(evmenu.EvMenu):
return nodetext
def init_fill_field(formtemplate, caller, formcallback, pretext="", posttext="",
submitcmd="submit", borderstyle="cells", formhelptext=None,
persistent=False, initial_formdata=None):
def init_fill_field(
formtemplate,
caller,
formcallback,
pretext="",
posttext="",
submitcmd="submit",
borderstyle="cells",
formhelptext=None,
persistent=False,
initial_formdata=None,
):
"""
Initializes a menu presenting a player with a fillable form - once the form
is submitted, the data will be passed as a dictionary to your chosen
@ -201,13 +210,14 @@ def init_fill_field(formtemplate, caller, formcallback, pretext="", posttext="",
# Provide default help text if none given
if formhelptext is None:
formhelptext = (
"Available commands:|/"
"|w<field> = <new value>:|n Set given field to new value, replacing the old value|/"
"|wclear <field>:|n Clear the value in the given field, making it blank|/"
"|wlook|n: Show the form's current values|/"
"|whelp|n: Display this help screen|/"
"|wquit|n: Quit the form menu without submitting|/"
"|w%s|n: Submit this form and quit the menu" % submitcmd)
"Available commands:|/"
"|w<field> = <new value>:|n Set given field to new value, replacing the old value|/"
"|wclear <field>:|n Clear the value in the given field, making it blank|/"
"|wlook|n: Show the form's current values|/"
"|whelp|n: Display this help screen|/"
"|wquit|n: Quit the form menu without submitting|/"
"|w%s|n: Submit this form and quit the menu" % submitcmd
)
# Pass kwargs to store data needed in the menu
kwargs = {
@ -218,12 +228,18 @@ def init_fill_field(formtemplate, caller, formcallback, pretext="", posttext="",
"posttext": posttext,
"submitcmd": submitcmd,
"borderstyle": borderstyle,
"formhelptext": formhelptext
"formhelptext": formhelptext,
}
# Initialize menu of selections
FieldEvMenu(caller, "evennia.contrib.fieldfill", startnode="menunode_fieldfill",
auto_look=False, persistent=persistent, **kwargs)
FieldEvMenu(
caller,
"evennia.contrib.fieldfill",
startnode="menunode_fieldfill",
auto_look=False,
persistent=persistent,
**kwargs,
)
def menunode_fieldfill(caller, raw_string, **kwargs):
@ -254,13 +270,19 @@ def menunode_fieldfill(caller, raw_string, **kwargs):
formhelptext = caller.db._menutree.formhelptext
# Syntax error
syntax_err = "Syntax: <field> = <new value>|/Or: clear <field>, help, look, quit|/'%s' to submit form" % submitcmd
syntax_err = (
"Syntax: <field> = <new value>|/Or: clear <field>, help, look, quit|/'%s' to submit form"
% submitcmd
)
# Display current form data
text = (display_formdata(formtemplate, formdata, pretext=pretext,
posttext=posttext, borderstyle=borderstyle), formhelptext)
options = ({"key": "_default",
"goto": "menunode_fieldfill"})
text = (
display_formdata(
formtemplate, formdata, pretext=pretext, posttext=posttext, borderstyle=borderstyle
),
formhelptext,
)
options = {"key": "_default", "goto": "menunode_fieldfill"}
if raw_string:
# Test for given 'submit' command
@ -275,7 +297,10 @@ def menunode_fieldfill(caller, raw_string, **kwargs):
blank_and_required.append(field["fieldname"])
if len(blank_and_required) > 0:
# List the required fields left empty to the player
caller.msg("The following blank fields require a value: %s" % list_to_string(blank_and_required))
caller.msg(
"The following blank fields require a value: %s"
% list_to_string(blank_and_required)
)
text = (None, formhelptext)
return text, options
@ -377,12 +402,18 @@ def menunode_fieldfill(caller, raw_string, **kwargs):
# Test for max/min
if max_value is not None:
if len(newvalue) > max_value:
caller.msg("Field '%s' has a maximum length of %i characters." % (matched_field, max_value))
caller.msg(
"Field '%s' has a maximum length of %i characters."
% (matched_field, max_value)
)
text = (None, formhelptext)
return text, options
if min_value is not None:
if len(newvalue) < min_value:
caller.msg("Field '%s' reqiures a minimum length of %i characters." % (matched_field, min_value))
caller.msg(
"Field '%s' reqiures a minimum length of %i characters."
% (matched_field, min_value)
)
text = (None, formhelptext)
return text, options
@ -402,14 +433,18 @@ def menunode_fieldfill(caller, raw_string, **kwargs):
return text, options
if min_value is not None:
if newvalue < min_value:
caller.msg("Field '%s' reqiures a minimum value of %i." % (matched_field, min_value))
caller.msg(
"Field '%s' reqiures a minimum value of %i." % (matched_field, min_value)
)
text = (None, formhelptext)
return text, options
# Field type bool verification
if fieldtype == "bool":
if newvalue.lower() != truestr.lower() and newvalue.lower() != falsestr.lower():
caller.msg("Please enter '%s' or '%s' for field '%s'." % (truestr, falsestr, matched_field))
caller.msg(
"Please enter '%s' or '%s' for field '%s'." % (truestr, falsestr, matched_field)
)
text = (None, formhelptext)
return text, options
if newvalue.lower() == truestr.lower():
@ -474,8 +509,7 @@ def form_template_to_dict(formtemplate):
return formdata
def display_formdata(formtemplate, formdata,
pretext="", posttext="", borderstyle="cells"):
def display_formdata(formtemplate, formdata, pretext="", posttext="", borderstyle="cells"):
"""
Displays a form's current data as a table. Used in the form menu.
@ -568,35 +602,40 @@ def verify_online_player(caller, value):
# besides strings and integers in the 'formdata' dictionary this way!
return matched_character
# Form template for the example 'delayed message' form
SAMPLE_FORM = [
{"fieldname": "Character",
"fieldtype": "text",
"max": 30,
"blankmsg": "(Name of an online player)",
"required": True,
"verifyfunc": verify_online_player
},
{"fieldname": "Delay",
"fieldtype": "number",
"min": 3,
"max": 30,
"default": 10,
"cantclear": True
},
{"fieldname": "Message",
"fieldtype": "text",
"min": 3,
"max": 200,
"blankmsg": "(Message up to 200 characters)"
},
{"fieldname": "Anonymous",
"fieldtype": "bool",
"truestr": "Yes",
"falsestr": "No",
"default": False
}
]
{
"fieldname": "Character",
"fieldtype": "text",
"max": 30,
"blankmsg": "(Name of an online player)",
"required": True,
"verifyfunc": verify_online_player,
},
{
"fieldname": "Delay",
"fieldtype": "number",
"min": 3,
"max": 30,
"default": 10,
"cantclear": True,
},
{
"fieldname": "Message",
"fieldtype": "text",
"min": 3,
"max": 200,
"blankmsg": "(Message up to 200 characters)",
},
{
"fieldname": "Anonymous",
"fieldtype": "bool",
"truestr": "Yes",
"falsestr": "No",
"default": False,
},
]
class CmdTestMenu(Command):
@ -621,15 +660,25 @@ class CmdTestMenu(Command):
"""
This performs the actual command.
"""
pretext = "|cSend a delayed message to another player ---------------------------------------|n"
posttext = ("|c--------------------------------------------------------------------------------|n|/"
"Syntax: type |c<field> = <new value>|n to change the values of the form. Given|/"
"player must be currently logged in, delay is given in seconds. When you are|/"
"finished, type '|csend|n' to send the message.|/")
pretext = (
"|cSend a delayed message to another player ---------------------------------------|n"
)
posttext = (
"|c--------------------------------------------------------------------------------|n|/"
"Syntax: type |c<field> = <new value>|n to change the values of the form. Given|/"
"player must be currently logged in, delay is given in seconds. When you are|/"
"finished, type '|csend|n' to send the message.|/"
)
init_fill_field(SAMPLE_FORM, self.caller, init_delayed_message,
pretext=pretext, posttext=posttext,
submitcmd="send", borderstyle="none")
init_fill_field(
SAMPLE_FORM,
self.caller,
init_delayed_message,
pretext=pretext,
posttext=posttext,
submitcmd="send",
borderstyle="none",
)
def sendmessage(obj, text):

View file

@ -33,24 +33,13 @@ from evennia import Command
# gender maps
_GENDER_PRONOUN_MAP = {"male": {"s": "he",
"o": "him",
"p": "his",
"a": "his"},
"female": {"s": "she",
"o": "her",
"p": "her",
"a": "hers"},
"neutral": {"s": "it",
"o": "it",
"p": "its",
"a": "its"},
"ambiguous": {"s": "they",
"o": "them",
"p": "their",
"a": "theirs"}
}
_RE_GENDER_PRONOUN = re.compile(r'(?<!\|)\|(?!\|)[sSoOpPaA]')
_GENDER_PRONOUN_MAP = {
"male": {"s": "he", "o": "him", "p": "his", "a": "his"},
"female": {"s": "she", "o": "her", "p": "her", "a": "hers"},
"neutral": {"s": "it", "o": "it", "p": "its", "a": "its"},
"ambiguous": {"s": "they", "o": "them", "p": "their", "a": "theirs"},
}
_RE_GENDER_PRONOUN = re.compile(r"(?<!\|)\|(?!\|)[sSoOpPaA]")
# in-game command for setting the gender
@ -63,6 +52,7 @@ class SetGender(Command):
@gender male||female||neutral||ambiguous
"""
key = "@gender"
aliases = "@sex"
locks = "call:all()"
@ -82,6 +72,7 @@ class SetGender(Command):
# Gender-aware character class
class GenderCharacter(DefaultCharacter):
"""
This is a Character class aware of gender.

View file

@ -22,11 +22,19 @@ below 0, rendering them as a completely full or empty bar with the
values displayed within.
"""
def display_meter(cur_value, max_value,
length=30, fill_color=["R", "Y", "G"],
empty_color="B", text_color="w",
align="left", pre_text="", post_text="",
show_values=True):
def display_meter(
cur_value,
max_value,
length=30,
fill_color=["R", "Y", "G"],
empty_color="B",
text_color="w",
align="left",
pre_text="",
post_text="",
show_values=True,
):
"""
Represents a current and maximum value given as a "bar" rendered with
ANSI or xterm256 background colors.
@ -70,34 +78,43 @@ def display_meter(cur_value, max_value,
bar_base_str = bar_base_str.center(length, " ")
else:
bar_base_str = bar_base_str.ljust(length, " ")
if max_value < 1: # Prevent divide by zero
max_value = 1
if cur_value < 0: # Prevent weirdly formatted 'negative bars'
cur_value = 0
if cur_value > max_value: # Display overfull bars correctly
cur_value = max_value
if max_value < 1: # Prevent divide by zero
max_value = 1
if cur_value < 0: # Prevent weirdly formatted 'negative bars'
cur_value = 0
if cur_value > max_value: # Display overfull bars correctly
cur_value = max_value
# Now it's time to determine where to put the color codes.
percent_full = float(cur_value) / float(max_value)
split_index = round(float(length) * percent_full)
# Determine point at which to split the bar
split_index = int(split_index)
# Separate the bar string into full and empty portions
full_portion = bar_base_str[:split_index]
empty_portion = bar_base_str[split_index:]
# Pick which fill color to use based on how full the bar is
fillcolor_index = (float(len(fill_color)) * percent_full)
fillcolor_index = float(len(fill_color)) * percent_full
fillcolor_index = max(0, int(round(fillcolor_index)) - 1)
fillcolor_code = "|[" + fill_color[fillcolor_index]
# Make color codes for empty bar portion and text_color
emptycolor_code = "|[" + empty_color
textcolor_code = "|" + text_color
# Assemble the final bar
final_bar = fillcolor_code + textcolor_code + full_portion + "|n" + emptycolor_code + textcolor_code + empty_portion + "|n"
final_bar = (
fillcolor_code
+ textcolor_code
+ full_portion
+ "|n"
+ emptycolor_code
+ textcolor_code
+ empty_portion
+ "|n"
)
return final_bar

View file

@ -99,8 +99,11 @@ class CallbackHandler(object):
"""
handler = type(self).script
if handler:
return self.format_callback(handler.add_callback(self.obj, callback_name, code,
author=author, valid=valid, parameters=parameters))
return self.format_callback(
handler.add_callback(
self.obj, callback_name, code, author=author, valid=valid, parameters=parameters
)
)
def edit(self, callback_name, number, code, author=None, valid=False):
"""
@ -122,8 +125,11 @@ class CallbackHandler(object):
"""
handler = type(self).script
if handler:
return self.format_callback(handler.edit_callback(self.obj, callback_name,
number, code, author=author, valid=valid))
return self.format_callback(
handler.edit_callback(
self.obj, callback_name, number, code, author=author, valid=valid
)
)
def remove(self, callback_name, number):
"""
@ -202,5 +208,18 @@ class CallbackHandler(object):
return Callback(**callback)
Callback = namedtuple("Callback", ("obj", "name", "number", "code", "author",
"valid", "parameters", "created_on", "updated_by", "updated_on"))
Callback = namedtuple(
"Callback",
(
"obj",
"name",
"number",
"code",
"author",
"valid",
"parameters",
"created_on",
"updated_by",
"updated_on",
),
)

View file

@ -16,8 +16,7 @@ COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
# Permissions
WITH_VALIDATION = getattr(settings, "callbackS_WITH_VALIDATION", None)
WITHOUT_VALIDATION = getattr(settings, "callbackS_WITHOUT_VALIDATION",
"developer")
WITHOUT_VALIDATION = getattr(settings, "callbackS_WITHOUT_VALIDATION", "developer")
VALIDATING = getattr(settings, "callbackS_VALIDATING", "developer")
# Split help text
@ -38,13 +37,9 @@ BASIC_SWITCHES = [
"tasks - show the list of differed tasks",
]
VALIDATOR_USAGES = [
"@call/accept [object name = <callback name> [callback number]]",
]
VALIDATOR_USAGES = ["@call/accept [object name = <callback name> [callback number]]"]
VALIDATOR_SWITCHES = [
"accept - show callbacks to be validated or accept one",
]
VALIDATOR_SWITCHES = ["accept - show callbacks to be validated or accept one"]
BASIC_TEXT = """
This command is used to manipulate callbacks. A callback can be linked to
@ -129,8 +124,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
caller = self.caller
lock = "perm({}) or perm(events_validating)".format(VALIDATING)
validator = caller.locks.check_lockstring(caller, lock)
lock = "perm({}) or perm(events_without_validation)".format(
WITHOUT_VALIDATION)
lock = "perm({}) or perm(events_without_validation)".format(WITHOUT_VALIDATION)
autovalid = caller.locks.check_lockstring(caller, lock)
# First and foremost, get the callback handler and set other variables
@ -142,8 +136,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
self.is_validator = validator
self.autovalid = autovalid
if self.handler is None:
caller.msg("The event handler is not running, can't "
"access the event system.")
caller.msg("The event handler is not running, can't " "access the event system.")
return
# Before the equal sign, there is an object name or nothing
@ -171,8 +164,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
elif switch in ["tasks", "task"]:
self.list_tasks()
else:
caller.msg("Mutually exclusive or invalid switches were "
"used, cannot proceed.")
caller.msg("Mutually exclusive or invalid switches were " "used, cannot proceed.")
def list_callbacks(self):
"""Display the list of callbacks connected to the object."""
@ -186,8 +178,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
# Check that the callback name can be found in this object
created = callbacks.get(callback_name)
if created is None:
self.msg("No callback {} has been set on {}.".format(callback_name,
obj))
self.msg("No callback {} has been set on {}.".format(callback_name, obj))
return
if parameters:
@ -197,8 +188,11 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
assert number >= 0
callback = callbacks[callback_name][number]
except (ValueError, AssertionError, IndexError):
self.msg("The callback {} {} cannot be found in {}.".format(
callback_name, parameters, obj))
self.msg(
"The callback {} {} cannot be found in {}.".format(
callback_name, parameters, obj
)
)
return
# Display the callback's details
@ -207,9 +201,13 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
updated_by = callback.get("updated_by")
updated_by = updated_by.key if updated_by else "|gUnknown|n"
created_on = callback.get("created_on")
created_on = created_on.strftime("%Y-%m-%d %H:%M:%S") if created_on else "|gUnknown|n"
created_on = (
created_on.strftime("%Y-%m-%d %H:%M:%S") if created_on else "|gUnknown|n"
)
updated_on = callback.get("updated_on")
updated_on = updated_on.strftime("%Y-%m-%d %H:%M:%S") if updated_on else "|gUnknown|n"
updated_on = (
updated_on.strftime("%Y-%m-%d %H:%M:%S") if updated_on else "|gUnknown|n"
)
msg = "Callback {} {} of {}:".format(callback_name, parameters, obj)
msg += "\nCreated by {} on {}.".format(author, created_on)
msg += "\nUpdated by {} on {}".format(updated_by, updated_on)
@ -241,9 +239,9 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
updated_on = callback.get("created_on")
if updated_on:
updated_on = "{} ago".format(time_format(
(now - updated_on).total_seconds(),
4).capitalize())
updated_on = "{} ago".format(
time_format((now - updated_on).total_seconds(), 4).capitalize()
)
else:
updated_on = "|gUnknown|n"
parameters = callback.get("parameters", "")
@ -256,8 +254,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
self.msg(str(table))
else:
names = list(set(list(types.keys()) + list(callbacks.keys())))
table = EvTable("Callback name", "Number", "Description",
valign="t", width=78)
table = EvTable("Callback name", "Number", "Description", valign="t", width=78)
table.reformat_column(0, width=20)
table.reformat_column(1, width=10, align="r")
table.reformat_column(2, width=48)
@ -279,8 +276,10 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
# Check that the callback exists
if not callback_name.startswith("chain_") and callback_name not in types:
self.msg("The callback name {} can't be found in {} of "
"typeclass {}.".format(callback_name, obj, type(obj)))
self.msg(
"The callback name {} can't be found in {} of "
"typeclass {}.".format(callback_name, obj, type(obj))
)
return
definition = types.get(callback_name, (None, "Chained event."))
@ -288,17 +287,24 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
self.msg(raw(description.strip("\n")))
# Open the editor
callback = self.handler.add_callback(obj, callback_name, "",
self.caller, False, parameters=self.parameters)
callback = self.handler.add_callback(
obj, callback_name, "", self.caller, False, parameters=self.parameters
)
# Lock this callback right away
self.handler.db.locked.append((obj, callback_name, callback["number"]))
# Open the editor for this callback
self.caller.db._callback = callback
EvEditor(self.caller, loadfunc=_ev_load, savefunc=_ev_save,
quitfunc=_ev_quit, key="Callback {} of {}".format(
callback_name, obj), persistent=True, codefunc=_ev_save)
EvEditor(
self.caller,
loadfunc=_ev_load,
savefunc=_ev_save,
quitfunc=_ev_quit,
key="Callback {} of {}".format(callback_name, obj),
persistent=True,
codefunc=_ev_save,
)
def edit_callback(self):
"""Edit a callback."""
@ -315,8 +321,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
# Check that the callback exists
if callback_name not in callbacks:
self.msg("The callback name {} can't be found in {}.".format(
callback_name, obj))
self.msg("The callback name {} can't be found in {}.".format(callback_name, obj))
return
# If there's only one callback, just edit it
@ -335,8 +340,11 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
assert number >= 0
callback = callbacks[callback_name][number]
except (ValueError, AssertionError, IndexError):
self.msg("The callback {} {} cannot be found in {}.".format(
callback_name, parameters, obj))
self.msg(
"The callback {} {} cannot be found in {}.".format(
callback_name, parameters, obj
)
)
return
# If caller can't edit without validation, forbid editing
@ -360,9 +368,15 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
# Open the editor
callback = dict(callback)
self.caller.db._callback = callback
EvEditor(self.caller, loadfunc=_ev_load, savefunc=_ev_save,
quitfunc=_ev_quit, key="Callback {} of {}".format(
callback_name, obj), persistent=True, codefunc=_ev_save)
EvEditor(
self.caller,
loadfunc=_ev_load,
savefunc=_ev_save,
quitfunc=_ev_quit,
key="Callback {} of {}".format(callback_name, obj),
persistent=True,
codefunc=_ev_save,
)
def del_callback(self):
"""Delete a callback."""
@ -379,8 +393,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
# Check that the callback exists
if callback_name not in callbacks:
self.msg("The callback name {} can't be found in {}.".format(
callback_name, obj))
self.msg("The callback name {} can't be found in {}.".format(callback_name, obj))
return
# If there's only one callback, just delete it
@ -389,8 +402,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
callback = callbacks[callback_name][0]
else:
if not parameters:
self.msg("Which callback do you wish to delete? Specify "
"a number.")
self.msg("Which callback do you wish to delete? Specify " "a number.")
self.list_callbacks()
return
@ -400,8 +412,11 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
assert number >= 0
callback = callbacks[callback_name][number]
except (ValueError, AssertionError, IndexError):
self.msg("The callback {} {} cannot be found in {}.".format(
callback_name, parameters, obj))
self.msg(
"The callback {} {} cannot be found in {}.".format(
callback_name, parameters, obj
)
)
return
# If caller can't edit without validation, forbid deleting
@ -417,8 +432,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
# Delete the callback
self.handler.del_callback(obj, callback_name, number)
self.msg("The callback {}[{}] of {} was deleted.".format(
callback_name, number + 1, obj))
self.msg("The callback {}[{}] of {} was deleted.".format(callback_name, number + 1, obj))
def accept_callback(self):
"""Accept a callback."""
@ -428,8 +442,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
# If no object, display the list of callbacks to be checked
if obj is None:
table = EvTable("ID", "Type", "Object", "Name", "Updated by",
"On", width=78)
table = EvTable("ID", "Type", "Object", "Name", "Updated by", "On", width=78)
table.reformat_column(0, align="r")
now = datetime.now()
for obj, name, number in self.handler.db.to_valid:
@ -450,9 +463,9 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
updated_on = callback.get("created_on")
if updated_on:
updated_on = "{} ago".format(time_format(
(now - updated_on).total_seconds(),
4).capitalize())
updated_on = "{} ago".format(
time_format((now - updated_on).total_seconds(), 4).capitalize()
)
else:
updated_on = "|gUnknown|n"
@ -471,8 +484,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
# Check that the callback exists
if callback_name not in callbacks:
self.msg("The callback name {} can't be found in {}.".format(
callback_name, obj))
self.msg("The callback name {} can't be found in {}.".format(callback_name, obj))
return
if not parameters:
@ -486,8 +498,9 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
assert number >= 0
callback = callbacks[callback_name][number]
except (ValueError, AssertionError, IndexError):
self.msg("The callback {} {} cannot be found in {}.".format(
callback_name, parameters, obj))
self.msg(
"The callback {} {} cannot be found in {}.".format(callback_name, parameters, obj)
)
return
# Accept the callback
@ -495,8 +508,9 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
self.msg("This callback has already been accepted.")
else:
self.handler.accept_callback(obj, callback_name, number)
self.msg("The callback {} {} of {} has been accepted.".format(
callback_name, parameters, obj))
self.msg(
"The callback {} {} of {} has been accepted.".format(callback_name, parameters, obj)
)
def list_tasks(self):
"""List the active tasks."""
@ -520,6 +534,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
self.msg(str(table))
# Private functions to handle editing
@ -529,36 +544,40 @@ def _ev_load(caller):
def _ev_save(caller, buf):
"""Save and add the callback."""
lock = "perm({}) or perm(events_without_validation)".format(
WITHOUT_VALIDATION)
lock = "perm({}) or perm(events_without_validation)".format(WITHOUT_VALIDATION)
autovalid = caller.locks.check_lockstring(caller, lock)
callback = caller.db._callback
handler = get_event_handler()
if not handler or not callback or not all(key in callback for key in
("obj", "name", "number", "valid")):
if (
not handler
or not callback
or not all(key in callback for key in ("obj", "name", "number", "valid"))
):
caller.msg("Couldn't save this callback.")
return False
if (callback["obj"], callback["name"], callback["number"]) in handler.db.locked:
handler.db.locked.remove((callback["obj"], callback["name"],
callback["number"]))
handler.db.locked.remove((callback["obj"], callback["name"], callback["number"]))
handler.edit_callback(callback["obj"], callback["name"], callback["number"], buf,
caller, valid=autovalid)
handler.edit_callback(
callback["obj"], callback["name"], callback["number"], buf, caller, valid=autovalid
)
return True
def _ev_quit(caller):
callback = caller.db._callback
handler = get_event_handler()
if not handler or not callback or not all(key in callback for key in
("obj", "name", "number", "valid")):
if (
not handler
or not callback
or not all(key in callback for key in ("obj", "name", "number", "valid"))
):
caller.msg("Couldn't save this callback.")
return False
if (callback["obj"], callback["name"], callback["number"]) in handler.db.locked:
handler.db.locked.remove((callback["obj"], callback["name"],
callback["number"]))
handler.db.locked.remove((callback["obj"], callback["name"], callback["number"]))
del caller.db._callback
caller.msg("Exited the code editor.")

View file

@ -87,6 +87,7 @@ class EventHandler(DefaultScript):
# Place the script in the CallbackHandler
from evennia.contrib.ingame_python import typeclasses
CallbackHandler.script = self
DefaultObject.callbacks = typeclasses.EventObject.callbacks
@ -94,8 +95,11 @@ class EventHandler(DefaultScript):
try:
self.ndb.channel = ChannelDB.objects.get(db_key="everror")
except ChannelDB.DoesNotExist:
self.ndb.channel = create_channel("everror", desc="Event errors",
locks="control:false();listen:perm(Builders);send:false()")
self.ndb.channel = create_channel(
"everror",
desc="Event errors",
locks="control:false();listen:perm(Builders);send:false()",
)
def get_events(self, obj):
"""
@ -200,8 +204,7 @@ class EventHandler(DefaultScript):
return callbacks
def add_callback(self, obj, callback_name, code, author=None, valid=False,
parameters=""):
def add_callback(self, obj, callback_name, code, author=None, valid=False, parameters=""):
"""
Add the specified callback.
@ -228,21 +231,22 @@ class EventHandler(DefaultScript):
callbacks = obj_callbacks[callback_name]
# Add the callback in the list
callbacks.append({
"created_on": datetime.now(),
"author": author,
"valid": valid,
"code": code,
"parameters": parameters,
})
callbacks.append(
{
"created_on": datetime.now(),
"author": author,
"valid": valid,
"code": code,
"parameters": parameters,
}
)
# If not valid, set it in 'to_valid'
if not valid:
self.db.to_valid.append((obj, callback_name, len(callbacks) - 1))
# Call the custom_add if needed
custom_add = self.get_events(obj).get(
callback_name, [None, None, None, None])[3]
custom_add = self.get_events(obj).get(callback_name, [None, None, None, None])[3]
if custom_add:
custom_add(obj, callback_name, len(callbacks) - 1, parameters)
@ -253,8 +257,7 @@ class EventHandler(DefaultScript):
definition["number"] = len(callbacks) - 1
return definition
def edit_callback(self, obj, callback_name, number, code, author=None,
valid=False):
def edit_callback(self, obj, callback_name, number, code, author=None, valid=False):
"""
Edit the specified callback.
@ -288,12 +291,9 @@ class EventHandler(DefaultScript):
raise RuntimeError("this callback is locked.")
# Edit the callback
callbacks[number].update({
"updated_on": datetime.now(),
"updated_by": author,
"valid": valid,
"code": code,
})
callbacks[number].update(
{"updated_on": datetime.now(), "updated_by": author, "valid": valid, "code": code}
)
# If not valid, set it in 'to_valid'
if not valid and (obj, callback_name, number) not in self.db.to_valid:
@ -334,8 +334,9 @@ class EventHandler(DefaultScript):
except IndexError:
return
else:
logger.log_info("Deleting callback {} {} of {}:\n{}".format(
callback_name, number, obj, code))
logger.log_info(
"Deleting callback {} {} of {}:\n{}".format(callback_name, number, obj, code)
)
del callbacks[number]
# Change IDs of callbacks to be validated
@ -349,8 +350,7 @@ class EventHandler(DefaultScript):
i -= 1
elif t_number > number:
# Change the ID for this callback
self.db.to_valid.insert(i, (t_obj, t_callback_name,
t_number - 1))
self.db.to_valid.insert(i, (t_obj, t_callback_name, t_number - 1))
del self.db.to_valid[i + 1]
i += 1
@ -415,13 +415,16 @@ class EventHandler(DefaultScript):
# Errors should not pass silently
allowed = ("number", "parameters", "locals")
if any(k for k in kwargs if k not in allowed):
raise TypeError("Unknown keyword arguments were specified "
"to call callbacks: {}".format(kwargs))
raise TypeError(
"Unknown keyword arguments were specified " "to call callbacks: {}".format(kwargs)
)
event = self.get_events(obj).get(callback_name)
if locals is None and not event:
logger.log_err("The callback {} for the object {} (typeclass "
"{}) can't be found".format(callback_name, obj, type(obj)))
logger.log_err(
"The callback {} for the object {} (typeclass "
"{}) can't be found".format(callback_name, obj, type(obj))
)
return False
# Prepare the locals if necessary
@ -431,9 +434,10 @@ class EventHandler(DefaultScript):
try:
locals[variable] = args[i]
except IndexError:
logger.log_trace("callback {} of {} ({}): need variable "
"{} in position {}".format(callback_name, obj,
type(obj), variable, i))
logger.log_trace(
"callback {} of {} ({}): need variable "
"{} in position {}".format(callback_name, obj, type(obj), variable, i)
)
return False
else:
locals = {key: value for key, value in locals.items()}
@ -483,9 +487,10 @@ class EventHandler(DefaultScript):
number = callback["number"]
obj = callback["obj"]
oid = obj.id
logger.log_err("An error occurred during the callback {} of "
"{} (#{}), number {}\n{}".format(callback_name, obj,
oid, number + 1, "\n".join(trace)))
logger.log_err(
"An error occurred during the callback {} of "
"{} (#{}), number {}\n{}".format(callback_name, obj, oid, number + 1, "\n".join(trace))
)
# Create the error message
line = "|runknown|n"
@ -505,9 +510,9 @@ class EventHandler(DefaultScript):
break
exc = raw(trace[-1].strip("\n").splitlines()[-1])
err_msg = "Error in {} of {} (#{})[{}], line {}:" \
" {}\n{}".format(callback_name, obj,
oid, number + 1, lineno, line, exc)
err_msg = "Error in {} of {} (#{})[{}], line {}:" " {}\n{}".format(
callback_name, obj, oid, number + 1, lineno, line, exc
)
# Inform the last updater if connected
updater = callback.get("updated_by")
@ -517,9 +522,9 @@ class EventHandler(DefaultScript):
if updater and updater.sessions.all():
updater.msg(err_msg)
else:
err_msg = "Error in {} of {} (#{})[{}], line {}:" \
" {}\n {}".format(callback_name, obj,
oid, number + 1, lineno, line, exc)
err_msg = "Error in {} of {} (#{})[{}], line {}:" " {}\n {}".format(
callback_name, obj, oid, number + 1, lineno, line, exc
)
self.ndb.channel.msg(err_msg)
def add_event(self, typeclass, name, variables, help_text, custom_call, custom_add):
@ -656,8 +661,7 @@ def complete_task(task_id):
return
if task_id not in script.db.tasks:
logger.log_err("The task #{} was scheduled, but it cannot be "
"found".format(task_id))
logger.log_err("The task #{} was scheduled, but it cannot be " "found".format(task_id))
return
delta, obj, callback_name, locals = script.db.tasks.pop(task_id)

View file

@ -31,8 +31,7 @@ class TestEventHandler(EvenniaTest):
def setUp(self):
"""Create the event handler."""
super().setUp()
self.handler = create_script(
"evennia.contrib.ingame_python.scripts.EventHandler")
self.handler = create_script("evennia.contrib.ingame_python.scripts.EventHandler")
# Copy old events if necessary
if OLD_EVENTS:
@ -64,8 +63,9 @@ class TestEventHandler(EvenniaTest):
def test_add_validation(self):
"""Add a callback while needing validation."""
author = self.char1
self.handler.add_callback(self.room1, "dummy",
"character.db.strength = 40", author=author, valid=False)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 40", author=author, valid=False
)
callback = self.handler.get_callbacks(self.room1).get("dummy")
callback = callback[0]
self.assertIsNotNone(callback)
@ -78,19 +78,20 @@ class TestEventHandler(EvenniaTest):
# Run this dummy callback (shouldn't do anything)
self.char1.db.strength = 10
locals = {"character": self.char1}
self.assertTrue(self.handler.call(
self.room1, "dummy", locals=locals))
self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
self.assertEqual(self.char1.db.strength, 10)
def test_edit(self):
"""Test editing a callback."""
author = self.char1
self.handler.add_callback(self.room1, "dummy",
"character.db.strength = 60", author=author, valid=True)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 60", author=author, valid=True
)
# Edit it right away
self.handler.edit_callback(self.room1, "dummy", 0,
"character.db.strength = 65", author=self.char2, valid=True)
self.handler.edit_callback(
self.room1, "dummy", 0, "character.db.strength = 65", author=self.char2, valid=True
)
# Check that the callback was written
callback = self.handler.get_callbacks(self.room1).get("dummy")
@ -103,36 +104,39 @@ class TestEventHandler(EvenniaTest):
# Run this dummy callback
self.char1.db.strength = 10
locals = {"character": self.char1}
self.assertTrue(self.handler.call(
self.room1, "dummy", locals=locals))
self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
self.assertEqual(self.char1.db.strength, 65)
def test_edit_validation(self):
"""Edit a callback when validation isn't automatic."""
author = self.char1
self.handler.add_callback(self.room1, "dummy",
"character.db.strength = 70", author=author, valid=True)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 70", author=author, valid=True
)
# Edit it right away
self.handler.edit_callback(self.room1, "dummy", 0,
"character.db.strength = 80", author=self.char2, valid=False)
self.handler.edit_callback(
self.room1, "dummy", 0, "character.db.strength = 80", author=self.char2, valid=False
)
# Run this dummy callback (shouldn't do anything)
self.char1.db.strength = 10
locals = {"character": self.char1}
self.assertTrue(self.handler.call(
self.room1, "dummy", locals=locals))
self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
self.assertEqual(self.char1.db.strength, 10)
def test_del(self):
"""Try to delete a callback."""
# Add 3 callbacks
self.handler.add_callback(self.room1, "dummy",
"character.db.strength = 5", author=self.char1, valid=True)
self.handler.add_callback(self.room1, "dummy",
"character.db.strength = 8", author=self.char2, valid=False)
self.handler.add_callback(self.room1, "dummy",
"character.db.strength = 9", author=self.char1, valid=True)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 5", author=self.char1, valid=True
)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 8", author=self.char2, valid=False
)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 9", author=self.char1, valid=True
)
# Note that the second callback isn't valid
self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid)
@ -160,17 +164,18 @@ class TestEventHandler(EvenniaTest):
# Call the remaining callback
self.char1.db.strength = 10
locals = {"character": self.char1}
self.assertTrue(self.handler.call(
self.room1, "dummy", locals=locals))
self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
self.assertEqual(self.char1.db.strength, 9)
def test_accept(self):
"""Accept an callback."""
# Add 2 callbacks
self.handler.add_callback(self.room1, "dummy",
"character.db.strength = 5", author=self.char1, valid=True)
self.handler.add_callback(self.room1, "dummy",
"character.db.strength = 8", author=self.char2, valid=False)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 5", author=self.char1, valid=True
)
self.handler.add_callback(
self.room1, "dummy", "character.db.strength = 8", author=self.char2, valid=False
)
# Note that the second callback isn't valid
self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid)
@ -185,8 +190,7 @@ class TestEventHandler(EvenniaTest):
# Call the dummy callback
self.char1.db.strength = 10
locals = {"character": self.char1}
self.assertTrue(self.handler.call(
self.room1, "dummy", locals=locals))
self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
self.assertEqual(self.char1.db.strength, 8)
def test_call(self):
@ -195,21 +199,22 @@ class TestEventHandler(EvenniaTest):
self.char2.key = "two"
# Add an callback
code = dedent("""
code = dedent(
"""
if character.key == "one":
character.db.health = 50
else:
character.db.health = 0
""".strip("\n"))
self.handler.add_callback(self.room1, "dummy", code,
author=self.char1, valid=True)
""".strip(
"\n"
)
)
self.handler.add_callback(self.room1, "dummy", code, author=self.char1, valid=True)
# Call the dummy callback
self.assertTrue(self.handler.call(
self.room1, "dummy", locals={"character": self.char1}))
self.assertTrue(self.handler.call(self.room1, "dummy", locals={"character": self.char1}))
self.assertEqual(self.char1.db.health, 50)
self.assertTrue(self.handler.call(
self.room1, "dummy", locals={"character": self.char2}))
self.assertTrue(self.handler.call(self.room1, "dummy", locals={"character": self.char2}))
self.assertEqual(self.char2.db.health, 0)
def test_handler(self):
@ -217,8 +222,7 @@ class TestEventHandler(EvenniaTest):
self.assertIsNotNone(self.char1.callbacks)
# Add an callback
callback = self.room1.callbacks.add("dummy", "pass", author=self.char1,
valid=True)
callback = self.room1.callbacks.add("dummy", "pass", author=self.char1, valid=True)
self.assertEqual(callback.obj, self.room1)
self.assertEqual(callback.name, "dummy")
self.assertEqual(callback.code, "pass")
@ -227,14 +231,14 @@ class TestEventHandler(EvenniaTest):
self.assertIn([callback], list(self.room1.callbacks.all().values()))
# Edit this very callback
new = self.room1.callbacks.edit("dummy", 0, "character.db.say = True",
author=self.char1, valid=True)
new = self.room1.callbacks.edit(
"dummy", 0, "character.db.say = True", author=self.char1, valid=True
)
self.assertIn([new], list(self.room1.callbacks.all().values()))
self.assertNotIn([callback], list(self.room1.callbacks.all().values()))
# Try to call this callback
self.assertTrue(self.room1.callbacks.call("dummy",
locals={"character": self.char2}))
self.assertTrue(self.room1.callbacks.call("dummy", locals={"character": self.char2}))
self.assertTrue(self.char2.db.say)
# Delete the callback
@ -249,8 +253,7 @@ class TestCmdCallback(CommandTest):
def setUp(self):
"""Create the callback handler."""
super().setUp()
self.handler = create_script(
"evennia.contrib.ingame_python.scripts.EventHandler")
self.handler = create_script("evennia.contrib.ingame_python.scripts.EventHandler")
# Copy old events if necessary
if OLD_EVENTS:
@ -269,7 +272,8 @@ class TestCmdCallback(CommandTest):
OLD_EVENTS.update(self.handler.ndb.events)
self.handler.stop()
for script in ScriptDB.objects.filter(
db_typeclass_path="evennia.contrib.ingame_python.scripts.TimeEventScript"):
db_typeclass_path="evennia.contrib.ingame_python.scripts.TimeEventScript"
):
script.stop()
CallbackHandler.script = None
@ -287,8 +291,7 @@ class TestCmdCallback(CommandTest):
self.assertIn(cols[2].strip(), ("0 (0)", ""))
# Add some callback
self.handler.add_callback(self.exit, "traverse", "pass",
author=self.char1, valid=True)
self.handler.add_callback(self.exit, "traverse", "pass", author=self.char1, valid=True)
# Try to obtain more details on a specific callback on exit
table = self.call(CmdCallback(), "out = traverse")
@ -322,13 +325,19 @@ class TestCmdCallback(CommandTest):
self.assertIsNotNone(editor)
# Edit the callback
editor.update_buffer(dedent("""
editor.update_buffer(
dedent(
"""
if character.key == "one":
character.msg("You can pass.")
else:
character.msg("You can't pass.")
deny()
""".strip("\n")))
""".strip(
"\n"
)
)
)
editor.save_buffer()
editor.quit()
callback = self.exit.callbacks.get("traverse")[0]
@ -343,9 +352,15 @@ class TestCmdCallback(CommandTest):
self.assertIsNotNone(editor)
# Edit the callback
editor.update_buffer(dedent("""
editor.update_buffer(
dedent(
"""
character.msg("No way.")
""".strip("\n")))
""".strip(
"\n"
)
)
)
editor.save_buffer()
editor.quit()
callback = self.exit.callbacks.get("traverse")[1]
@ -355,19 +370,16 @@ class TestCmdCallback(CommandTest):
def test_del(self):
"""Add and remove an callback."""
self.handler.add_callback(self.exit, "traverse", "pass",
author=self.char1, valid=True)
self.handler.add_callback(self.exit, "traverse", "pass", author=self.char1, valid=True)
# Try to delete the callback
# char2 shouldn't be allowed to do so (that's not HIS callback)
self.call(CmdCallback(), "/del out = traverse 1", caller=self.char2)
self.assertTrue(len(self.handler.get_callbacks(self.exit).get(
"traverse", [])) == 1)
self.assertTrue(len(self.handler.get_callbacks(self.exit).get("traverse", [])) == 1)
# Now, char1 should be allowed to delete it
self.call(CmdCallback(), "/del out = traverse 1")
self.assertTrue(len(self.handler.get_callbacks(self.exit).get(
"traverse", [])) == 0)
self.assertTrue(len(self.handler.get_callbacks(self.exit).get("traverse", [])) == 0)
def test_lock(self):
"""Test the lock of multiple editing."""
@ -388,9 +400,15 @@ class TestCmdCallback(CommandTest):
self.assertIsNotNone(editor)
# Edit the callback
editor.update_buffer(dedent("""
editor.update_buffer(
dedent(
"""
room.msg_contents("It's 8 PM, everybody up!")
""".strip("\n")))
""".strip(
"\n"
)
)
)
editor.save_buffer()
editor.quit()
callback = self.room1.callbacks.get("time")[0]
@ -414,8 +432,7 @@ class TestDefaultCallbacks(CommandTest):
def setUp(self):
"""Create the callback handler."""
super().setUp()
self.handler = create_script(
"evennia.contrib.ingame_python.scripts.EventHandler")
self.handler = create_script("evennia.contrib.ingame_python.scripts.EventHandler")
# Copy old events if necessary
if OLD_EVENTS:
@ -439,33 +456,36 @@ class TestDefaultCallbacks(CommandTest):
def test_exit(self):
"""Test the callbacks of an exit."""
self.char1.key = "char1"
code = dedent("""
code = dedent(
"""
if character.key == "char1":
character.msg("You can leave.")
else:
character.msg("You cannot leave.")
deny()
""".strip("\n"))
""".strip(
"\n"
)
)
# Enforce self.exit.destination since swapping typeclass lose it
self.exit.destination = self.room2
# Try the can_traverse callback
self.handler.add_callback(self.exit, "can_traverse", code,
author=self.char1, valid=True)
self.handler.add_callback(self.exit, "can_traverse", code, author=self.char1, valid=True)
# Have char1 move through the exit
self.call(ExitCommand(), "", "You can leave.", obj=self.exit)
self.assertIs(self.char1.location, self.room2)
# Have char2 move through this exit
self.call(ExitCommand(), "", "You cannot leave.", obj=self.exit,
caller=self.char2)
self.call(ExitCommand(), "", "You cannot leave.", obj=self.exit, caller=self.char2)
self.assertIs(self.char2.location, self.room1)
# Try the traverse callback
self.handler.del_callback(self.exit, "can_traverse", 0)
self.handler.add_callback(self.exit, "traverse", "character.msg('Fine!')",
author=self.char1, valid=True)
self.handler.add_callback(
self.exit, "traverse", "character.msg('Fine!')", author=self.char1, valid=True
)
# Have char2 move through the exit
self.call(ExitCommand(), "", obj=self.exit, caller=self.char2)
@ -478,16 +498,17 @@ class TestDefaultCallbacks(CommandTest):
# Test msg_arrive and msg_leave
code = 'message = "{character} goes out."'
self.handler.add_callback(self.exit, "msg_leave", code,
author=self.char1, valid=True)
self.handler.add_callback(self.exit, "msg_leave", code, author=self.char1, valid=True)
# Have char1 move through the exit
old_msg = self.char2.msg
try:
self.char2.msg = Mock()
self.call(ExitCommand(), "", obj=self.exit)
stored_msg = [args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
for name, args, kwargs in self.char2.msg.mock_calls]
stored_msg = [
args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
for name, args, kwargs in self.char2.msg.mock_calls
]
# Get the first element of a tuple if msg received a tuple instead of a string
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
returned_msg = ansi.parse_ansi("\n".join(stored_msg), strip_ansi=True)
@ -496,19 +517,24 @@ class TestDefaultCallbacks(CommandTest):
self.char2.msg = old_msg
# Create a return exit
back = create_object("evennia.objects.objects.DefaultExit",
key="in", location=self.room2, destination=self.room1)
back = create_object(
"evennia.objects.objects.DefaultExit",
key="in",
location=self.room2,
destination=self.room1,
)
code = 'message = "{character} goes in."'
self.handler.add_callback(self.exit, "msg_arrive", code,
author=self.char1, valid=True)
self.handler.add_callback(self.exit, "msg_arrive", code, author=self.char1, valid=True)
# Have char1 move through the exit
old_msg = self.char2.msg
try:
self.char2.msg = Mock()
self.call(ExitCommand(), "", obj=back)
stored_msg = [args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
for name, args, kwargs in self.char2.msg.mock_calls]
stored_msg = [
args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
for name, args, kwargs in self.char2.msg.mock_calls
]
# Get the first element of a tuple if msg received a tuple instead of a string
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
returned_msg = ansi.parse_ansi("\n".join(stored_msg), strip_ansi=True)

View file

@ -212,15 +212,16 @@ class EventCharacter(DefaultCharacter):
# Get the exit from location to destination
location = self.location
exits = [o for o in location.contents if o.location is location and o.destination is destination]
exits = [
o for o in location.contents if o.location is location and o.destination is destination
]
mapping = mapping or {}
mapping.update({
"character": self,
})
mapping.update({"character": self})
if exits:
exits[0].callbacks.call("msg_leave", self, exits[0],
location, destination, string, mapping)
exits[0].callbacks.call(
"msg_leave", self, exits[0], location, destination, string, mapping
)
string = exits[0].callbacks.get_variable("message")
mapping = exits[0].callbacks.get_variable("mapping")
@ -267,15 +268,18 @@ class EventCharacter(DefaultCharacter):
destination = self.location
exits = []
mapping = mapping or {}
mapping.update({
"character": self,
})
mapping.update({"character": self})
if origin:
exits = [o for o in destination.contents if o.location is destination and o.destination is origin]
exits = [
o
for o in destination.contents
if o.location is destination and o.destination is origin
]
if exits:
exits[0].callbacks.call("msg_arrive", self, exits[0],
origin, destination, string, mapping)
exits[0].callbacks.call(
"msg_arrive", self, exits[0], origin, destination, string, mapping
)
string = exits[0].callbacks.get_variable("message")
mapping = exits[0].callbacks.get_variable("mapping")
@ -305,14 +309,16 @@ class EventCharacter(DefaultCharacter):
origin = self.location
Room = DefaultRoom
if isinstance(origin, Room) and isinstance(destination, Room):
can = self.callbacks.call("can_move", self,
origin, destination)
can = self.callbacks.call("can_move", self, origin, destination)
if can:
can = origin.callbacks.call("can_move", self, origin)
if can:
# Call other character's 'can_part' event
for present in [o for o in origin.contents if isinstance(
o, DefaultCharacter) and o is not self]:
for present in [
o
for o in origin.contents
if isinstance(o, DefaultCharacter) and o is not self
]:
can = present.callbacks.call("can_part", present, self)
if not can:
break
@ -344,8 +350,9 @@ class EventCharacter(DefaultCharacter):
destination.callbacks.call("move", self, origin, destination)
# Call the 'greet' event of characters in the location
for present in [o for o in destination.contents if isinstance(
o, DefaultCharacter) and o is not self]:
for present in [
o for o in destination.contents if isinstance(o, DefaultCharacter) and o is not self
]:
present.callbacks.call("greet", present, self)
def at_object_delete(self):
@ -427,7 +434,11 @@ class EventCharacter(DefaultCharacter):
"""
# First, try the location
location = getattr(self, "location", None)
location = location if location and inherits_from(location, "evennia.objects.objects.DefaultRoom") else None
location = (
location
if location and inherits_from(location, "evennia.objects.objects.DefaultRoom")
else None
)
if location and not kwargs.get("whisper", False):
allow = location.callbacks.call("can_say", self, location, message, parameters=message)
message = location.callbacks.get_variable("message")
@ -436,7 +447,9 @@ class EventCharacter(DefaultCharacter):
# Browse all the room's other characters
for obj in location.contents:
if obj is self or not inherits_from(obj, "evennia.objects.objects.DefaultCharacter"):
if obj is self or not inherits_from(
obj, "evennia.objects.objects.DefaultCharacter"
):
continue
allow = obj.callbacks.call("can_say", self, obj, message, parameters=message)
@ -490,14 +503,22 @@ class EventCharacter(DefaultCharacter):
super().at_say(message, **kwargs)
location = getattr(self, "location", None)
location = location if location and inherits_from(location, "evennia.objects.objects.DefaultRoom") else None
location = (
location
if location and inherits_from(location, "evennia.objects.objects.DefaultRoom")
else None
)
if location and not kwargs.get("whisper", False):
location.callbacks.call("say", self, location, message,
parameters=message)
location.callbacks.call("say", self, location, message, parameters=message)
# Call the other characters' "say" event
presents = [obj for obj in location.contents if obj is not self and inherits_from(obj, "evennia.objects.objects.DefaultCharacter")]
presents = [
obj
for obj in location.contents
if obj is not self
and inherits_from(obj, "evennia.objects.objects.DefaultCharacter")
]
for present in presents:
present.callbacks.call("say", self, present, message, parameters=message)
@ -602,8 +623,14 @@ class EventExit(DefaultExit):
_events = {
"can_traverse": (["character", "exit", "room"], EXIT_CAN_TRAVERSE),
"msg_arrive": (["character", "exit", "origin", "destination", "message", "mapping"], EXIT_MSG_ARRIVE),
"msg_leave": (["character", "exit", "origin", "destination", "message", "mapping"], EXIT_MSG_LEAVE),
"msg_arrive": (
["character", "exit", "origin", "destination", "message", "mapping"],
EXIT_MSG_ARRIVE,
),
"msg_leave": (
["character", "exit", "origin", "destination", "message", "mapping"],
EXIT_MSG_LEAVE,
),
"time": (["exit"], EXIT_TIME, None, time_event),
"traverse": (["character", "exit", "origin", "destination"], EXIT_TRAVERSE),
}
@ -630,8 +657,7 @@ class EventExit(DefaultExit):
"""
is_character = inherits_from(traversing_object, DefaultCharacter)
if is_character:
allow = self.callbacks.call("can_traverse", traversing_object,
self, self.location)
allow = self.callbacks.call("can_traverse", traversing_object, self, self.location)
if not allow:
return
@ -639,8 +665,9 @@ class EventExit(DefaultExit):
# After traversing
if is_character:
self.callbacks.call("traverse", traversing_object,
self, self.location, self.destination)
self.callbacks.call(
"traverse", traversing_object, self, self.location, self.destination
)
# Object help

View file

@ -85,6 +85,7 @@ def register_events(path_or_typeclass):
return typeclass
# Custom callbacks for specific event types
@ -108,8 +109,10 @@ def get_next_wait(format):
"""
calendar = getattr(settings, "EVENTS_CALENDAR", None)
if calendar is None:
logger.log_err("A time-related event has been set whereas "
"the gametime calendar has not been set in the settings.")
logger.log_err(
"A time-related event has been set whereas "
"the gametime calendar has not been set in the settings."
)
return
elif calendar == "standard":
rsu = standard_rsu
@ -135,8 +138,9 @@ def get_next_wait(format):
break
if not piece.isdigit():
logger.log_trace("The time specified '{}' in {} isn't "
"a valid number".format(piece, format))
logger.log_trace(
"The time specified '{}' in {} isn't " "a valid number".format(piece, format)
)
return
# Convert the piece to int
@ -171,7 +175,9 @@ def time_event(obj, event_name, number, parameters):
"""
seconds, usual, key = get_next_wait(parameters)
script = create_script("evennia.contrib.ingame_python.scripts.TimeEventScript", interval=seconds, obj=obj)
script = create_script(
"evennia.contrib.ingame_python.scripts.TimeEventScript", interval=seconds, obj=obj
)
script.key = key
script.desc = "event on {}".format(key)
script.db.time_format = parameters

View file

@ -80,6 +80,7 @@ class CmdMail(default_cmds.MuxAccountCommand):
@mail/reply 9=Thanks for the info!
"""
key = "@mail"
aliases = ["mail"]
lock = "cmd:all()"
@ -92,8 +93,9 @@ class CmdMail(default_cmds.MuxAccountCommand):
"""
super().parse()
self.caller_is_account = bool(inherits_from(self.caller,
"evennia.accounts.accounts.DefaultAccount"))
self.caller_is_account = bool(
inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount")
)
def search_targets(self, namelist):
"""
@ -143,9 +145,9 @@ class CmdMail(default_cmds.MuxAccountCommand):
"""
for recipient in recipients:
recipient.msg("You have received a new @mail from %s" % caller)
new_message = create.create_message(self.caller, message,
receivers=recipient,
header=subject)
new_message = create.create_message(
self.caller, message, receivers=recipient, header=subject
)
new_message.tags.add("new", category="mail")
if recipients:
@ -176,7 +178,7 @@ class CmdMail(default_cmds.MuxAccountCommand):
if all_mail[mind]:
mail = all_mail[mind]
question = "Delete message {} ({}) [Y]/N?".format(mind + 1, mail.header)
ret = yield(question)
ret = yield (question)
# handle not ret, it will be None during unit testing
if not ret or ret.strip().upper() not in ("N", "No"):
all_mail[mind].delete()
@ -192,8 +194,9 @@ class CmdMail(default_cmds.MuxAccountCommand):
elif "forward" in self.switches or "fwd" in self.switches:
try:
if not self.rhs:
self.caller.msg("Cannot forward a message without a target list. "
"Please try again.")
self.caller.msg(
"Cannot forward a message without a target list. " "Please try again."
)
return
elif not self.lhs:
self.caller.msg("You must define a message to forward.")
@ -208,9 +211,14 @@ class CmdMail(default_cmds.MuxAccountCommand):
if all_mail[mind]:
old_message = all_mail[mind]
self.send_mail(self.search_targets(self.lhslist), "FWD: " + old_message.header,
message + "\n---- Original Message ----\n" + old_message.message,
self.caller)
self.send_mail(
self.search_targets(self.lhslist),
"FWD: " + old_message.header,
message
+ "\n---- Original Message ----\n"
+ old_message.message,
self.caller,
)
self.caller.msg("Message forwarded.")
else:
raise IndexError
@ -218,8 +226,12 @@ class CmdMail(default_cmds.MuxAccountCommand):
mind = max(0, min(mind_max, int(self.rhs) - 1))
if all_mail[mind]:
old_message = all_mail[mind]
self.send_mail(self.search_targets(self.lhslist), "FWD: " + old_message.header,
"\n---- Original Message ----\n" + old_message.message, self.caller)
self.send_mail(
self.search_targets(self.lhslist),
"FWD: " + old_message.header,
"\n---- Original Message ----\n" + old_message.message,
self.caller,
)
self.caller.msg("Message forwarded.")
old_message.tags.remove("new", category="mail")
old_message.tags.add("fwd", category="mail")
@ -243,8 +255,12 @@ class CmdMail(default_cmds.MuxAccountCommand):
mind = max(0, min(mind_max, int(self.lhs) - 1))
if all_mail[mind]:
old_message = all_mail[mind]
self.send_mail(old_message.senders, "RE: " + old_message.header,
self.rhs + "\n---- Original Message ----\n" + old_message.message, self.caller)
self.send_mail(
old_message.senders,
"RE: " + old_message.header,
self.rhs + "\n---- Original Message ----\n" + old_message.message,
self.caller,
)
old_message.tags.remove("new", category="mail")
old_message.tags.add("-", category="mail")
return
@ -275,10 +291,15 @@ class CmdMail(default_cmds.MuxAccountCommand):
messageForm = []
if message:
messageForm.append(_HEAD_CHAR * _WIDTH)
messageForm.append("|wFrom:|n %s" % (message.senders[0].get_display_name(self.caller)))
messageForm.append(
"|wFrom:|n %s" % (message.senders[0].get_display_name(self.caller))
)
# note that we cannot use %-d format here since Windows does not support it
day = message.db_date_created.day
messageForm.append("|wSent:|n %s" % message.db_date_created.strftime(f"%b {day}, %Y - %H:%M:%S"))
messageForm.append(
"|wSent:|n %s"
% message.db_date_created.strftime(f"%b {day}, %Y - %H:%M:%S")
)
messageForm.append("|wSubject:|n %s" % message.header)
messageForm.append(_SUB_HEAD_CHAR * _WIDTH)
messageForm.append(message.message)
@ -292,20 +313,30 @@ class CmdMail(default_cmds.MuxAccountCommand):
messages = self.get_all_mail()
if messages:
table = evtable.EvTable("|wID|n", "|wFrom|n", "|wSubject|n",
"|wArrived|n", "",
table=None, border="header",
header_line_char=_SUB_HEAD_CHAR, width=_WIDTH)
table = evtable.EvTable(
"|wID|n",
"|wFrom|n",
"|wSubject|n",
"|wArrived|n",
"",
table=None,
border="header",
header_line_char=_SUB_HEAD_CHAR,
width=_WIDTH,
)
index = 1
for message in messages:
status = str(message.db_tags.last().db_key.upper())
if status == "NEW":
status = "|gNEW|n"
table.add_row(index, message.senders[0].get_display_name(self.caller),
message.header,
datetime_format(message.db_date_created),
status)
table.add_row(
index,
message.senders[0].get_display_name(self.caller),
message.header,
datetime_format(message.db_date_created),
status,
)
index += 1
table.reformat_column(0, width=6)
@ -323,5 +354,6 @@ class CmdMail(default_cmds.MuxAccountCommand):
# character - level version of the command
class CmdMailCharacter(CmdMail):
account_caller = False

View file

@ -133,9 +133,11 @@ def example1_build_mountains(x, y, **kwargs):
room = create_object(rooms.Room, key="mountains" + str(x) + str(y))
# Generate a description by randomly selecting an entry from a list.
room_desc = ["Mountains as far as the eye can see",
"Your path is surrounded by sheer cliffs",
"Haven't you seen that rock before?"]
room_desc = [
"Mountains as far as the eye can see",
"Your path is surrounded by sheer cliffs",
"Haven't you seen that rock before?",
]
room.db.desc = random.choice(room_desc)
# Create a random number of objects to populate the room.
@ -157,15 +159,17 @@ def example1_build_temple(x, y, **kwargs):
room = create_object(rooms.Room, key="temple" + str(x) + str(y))
# Set the description.
room.db.desc = ("In what, from the outside, appeared to be a grand and "
"ancient temple you've somehow found yourself in the the "
"Evennia Inn! It consists of one large room filled with "
"tables. The bardisk extends along the east wall, where "
"multiple barrels and bottles line the shelves. The "
"barkeep seems busy handing out ale and chatting with "
"the patrons, which are a rowdy and cheerful lot, "
"keeping the sound level only just below thunderous. "
"This is a rare spot of mirth on this dread moor.")
room.db.desc = (
"In what, from the outside, appeared to be a grand and "
"ancient temple you've somehow found yourself in the the "
"Evennia Inn! It consists of one large room filled with "
"tables. The bardisk extends along the east wall, where "
"multiple barrels and bottles line the shelves. The "
"barkeep seems busy handing out ale and chatting with "
"the patrons, which are a rowdy and cheerful lot, "
"keeping the sound level only just below thunderous. "
"This is a rare spot of mirth on this dread moor."
)
# Send a message to the account
kwargs["caller"].msg(room.key + " " + room.dbref)
@ -175,9 +179,11 @@ def example1_build_temple(x, y, **kwargs):
# Include your trigger characters and build functions in a legend dict.
EXAMPLE1_LEGEND = {("", ""): example1_build_forest,
("", "n"): example1_build_mountains,
(""): example1_build_temple}
EXAMPLE1_LEGEND = {
("", ""): example1_build_forest,
("", "n"): example1_build_mountains,
(""): example1_build_temple,
}
# ---------- EXAMPLE 2 ---------- #
# @mapbuilder/two evennia.contrib.mapbuilder.EXAMPLE2_MAP EXAMPLE2_LEGEND
@ -185,11 +191,11 @@ EXAMPLE1_LEGEND = {("♣", "♠"): example1_build_forest,
# -*- coding: utf-8 -*-
# Add the necessary imports for your instructions here.
#from evennia import create_object
#from typeclasses import rooms, exits
#from evennia.utils import utils
#from random import randint
#import random
# from evennia import create_object
# from typeclasses import rooms, exits
# from evennia.utils import utils
# from random import randint
# import random
# This is the same layout as Example 1 but included are characters for exits.
# We can use these characters to determine which rooms should be connected.
@ -230,16 +236,15 @@ def example2_build_verticle_exit(x, y, **kwargs):
south_room = kwargs["room_dict"][(x, y + 1)]
# create exits in the rooms
create_object(exits.Exit, key="south",
aliases=["s"], location=north_room,
destination=south_room)
create_object(
exits.Exit, key="south", aliases=["s"], location=north_room, destination=south_room
)
create_object(exits.Exit, key="north",
aliases=["n"], location=south_room,
destination=north_room)
create_object(
exits.Exit, key="north", aliases=["n"], location=south_room, destination=north_room
)
kwargs["caller"].msg("Connected: " + north_room.key +
" & " + south_room.key)
kwargs["caller"].msg("Connected: " + north_room.key + " & " + south_room.key)
def example2_build_horizontal_exit(x, y, **kwargs):
@ -251,22 +256,19 @@ def example2_build_horizontal_exit(x, y, **kwargs):
west_room = kwargs["room_dict"][(x - 1, y)]
east_room = kwargs["room_dict"][(x + 1, y)]
create_object(exits.Exit, key="east",
aliases=["e"], location=west_room,
destination=east_room)
create_object(exits.Exit, key="east", aliases=["e"], location=west_room, destination=east_room)
create_object(exits.Exit, key="west",
aliases=["w"], location=east_room,
destination=west_room)
create_object(exits.Exit, key="west", aliases=["w"], location=east_room, destination=west_room)
kwargs["caller"].msg("Connected: " + west_room.key +
" & " + east_room.key)
kwargs["caller"].msg("Connected: " + west_room.key + " & " + east_room.key)
# Include your trigger characters and build functions in a legend dict.
EXAMPLE2_LEGEND = {("", ""): example2_build_forest,
("|"): example2_build_verticle_exit,
("-"): example2_build_horizontal_exit}
EXAMPLE2_LEGEND = {
("", ""): example2_build_forest,
("|"): example2_build_verticle_exit,
("-"): example2_build_horizontal_exit,
}
# ---------- END OF EXAMPLES ---------- #
@ -285,7 +287,7 @@ def _map_to_list(game_map):
list (list): The map split into rows
"""
return game_map.split('\n')
return game_map.split("\n")
def build_map(caller, game_map, legend, iterations=1, build_exits=True):
@ -325,9 +327,9 @@ def build_map(caller, game_map, legend, iterations=1, build_exits=True):
for key in legend:
# obs - we must use == for strings
if game_map[y][x] == key:
room = legend[key](x, y, iteration=iteration,
room_dict=room_dict,
caller=caller)
room = legend[key](
x, y, iteration=iteration, room_dict=room_dict, caller=caller
)
if iteration == 0:
room_dict[(x, y)] = room
@ -341,33 +343,50 @@ def build_map(caller, game_map, legend, iterations=1, build_exits=True):
# north
if (x, y - 1) in room_dict:
if room_dict[(x, y - 1)]:
create_object(exits.Exit, key="north",
aliases=["n"], location=location,
destination=room_dict[(x, y - 1)])
create_object(
exits.Exit,
key="north",
aliases=["n"],
location=location,
destination=room_dict[(x, y - 1)],
)
# east
if (x + 1, y) in room_dict:
if room_dict[(x + 1, y)]:
create_object(exits.Exit, key="east",
aliases=["e"], location=location,
destination=room_dict[(x + 1, y)])
create_object(
exits.Exit,
key="east",
aliases=["e"],
location=location,
destination=room_dict[(x + 1, y)],
)
# south
if (x, y + 1) in room_dict:
if room_dict[(x, y + 1)]:
create_object(exits.Exit, key="south",
aliases=["s"], location=location,
destination=room_dict[(x, y + 1)])
create_object(
exits.Exit,
key="south",
aliases=["s"],
location=location,
destination=room_dict[(x, y + 1)],
)
# west
if (x - 1, y) in room_dict:
if room_dict[(x - 1, y)]:
create_object(exits.Exit, key="west",
aliases=["w"], location=location,
destination=room_dict[(x - 1, y)])
create_object(
exits.Exit,
key="west",
aliases=["w"],
location=location,
destination=room_dict[(x - 1, y)],
)
caller.msg("Map Created.")
# access command
@ -399,6 +418,7 @@ class CmdMapBuilder(COMMAND_DEFAULT_CLASS):
automatically generated but is turned off by switches which also determines
how many times the map is iterated over.
"""
key = "@mapbuilder"
aliases = ["@buildmap"]
locks = "cmd:superuser()"
@ -412,8 +432,7 @@ class CmdMapBuilder(COMMAND_DEFAULT_CLASS):
# Check if arguments passed.
if not self.args or (len(args) != 2):
caller.msg("Usage: @mapbuilder <path.to.module.VARNAME> "
"<path.to.module.MAP_LEGEND>")
caller.msg("Usage: @mapbuilder <path.to.module.VARNAME> " "<path.to.module.MAP_LEGEND>")
return
# Set up base variables.
@ -424,17 +443,18 @@ class CmdMapBuilder(COMMAND_DEFAULT_CLASS):
# Breaks down path_to_map into [PATH, VARIABLE]
path_to_map = args[0]
path_to_map = path_to_map.rsplit('.', 1)
path_to_map = path_to_map.rsplit(".", 1)
try:
# Retrieves map variable from module or raises error.
game_map = utils.variable_from_module(path_to_map[0],
path_to_map[1])
game_map = utils.variable_from_module(path_to_map[0], path_to_map[1])
if not game_map:
raise ValueError("Command Aborted!\n"
"Path to map variable failed.\n"
"Usage: @mapbuilder <path.to.module."
"VARNAME> <path.to.module.MAP_LEGEND>")
raise ValueError(
"Command Aborted!\n"
"Path to map variable failed.\n"
"Usage: @mapbuilder <path.to.module."
"VARNAME> <path.to.module.MAP_LEGEND>"
)
except Exception as exc:
# Or relays error message if fails.
@ -445,7 +465,7 @@ class CmdMapBuilder(COMMAND_DEFAULT_CLASS):
# Breaks down path_to_legend into [PATH, VARIABLE]
path_to_legend = args[1]
path_to_legend = path_to_legend.rsplit('.', 1)
path_to_legend = path_to_legend.rsplit(".", 1)
# If no path given default to path_to_map's path
if len(path_to_legend) == 1:
@ -453,13 +473,14 @@ class CmdMapBuilder(COMMAND_DEFAULT_CLASS):
try:
# Retrieves legend variable from module or raises error if fails.
legend = utils.variable_from_module(path_to_legend[0],
path_to_legend[1])
legend = utils.variable_from_module(path_to_legend[0], path_to_legend[1])
if not legend:
raise ValueError("Command Aborted!\n"
"Path to legend variable failed.\n"
"Usage: @mapbuilder <path.to.module."
"VARNAME> <path.to.module.MAP_LEGEND>")
raise ValueError(
"Command Aborted!\n"
"Path to legend variable failed.\n"
"Usage: @mapbuilder <path.to.module."
"VARNAME> <path.to.module.MAP_LEGEND>"
)
except Exception as exc:
# Or relays error message if fails.

View file

@ -24,25 +24,27 @@ from django.conf import settings
from evennia import Command, CmdSet
from evennia import syscmdkeys
from evennia.utils.evmenu import EvMenu
from evennia.utils.utils import (
random_string_from_module, class_from_module, callables_from_module)
from evennia.utils.utils import random_string_from_module, class_from_module, callables_from_module
_CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
_GUEST_ENABLED = settings.GUEST_ENABLED
_ACCOUNT = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
_GUEST = class_from_module(settings.BASE_GUEST_TYPECLASS)
_ACCOUNT_HELP = ("Enter the name you used to log into the game before, "
"or a new account-name if you are new.")
_PASSWORD_HELP = ("Password should be a minimum of 8 characters (preferably longer) and "
"can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only.")
_ACCOUNT_HELP = (
"Enter the name you used to log into the game before, " "or a new account-name if you are new."
)
_PASSWORD_HELP = (
"Password should be a minimum of 8 characters (preferably longer) and "
"can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."
)
# Menu nodes
def _show_help(caller, raw_string, **kwargs):
"""Echo help message, then re-run node that triggered it"""
help_entry = kwargs['help_entry']
help_entry = kwargs["help_entry"]
caller.msg(help_entry)
return None # re-run calling node
@ -53,6 +55,7 @@ def node_enter_username(caller, raw_text, **kwargs):
Start login by displaying the connection screen and ask for a user name.
"""
def _check_input(caller, username, **kwargs):
"""
'Goto-callable', set up to be called from the _default option below.
@ -65,9 +68,9 @@ def node_enter_username(caller, raw_text, **kwargs):
and what kwarg it will be called with.
"""
username = username.rstrip('\n')
username = username.rstrip("\n")
if username == 'guest' and _GUEST_ENABLED:
if username == "guest" and _GUEST_ENABLED:
# do an immediate guest login
session = caller
address = session.address
@ -76,7 +79,7 @@ def node_enter_username(caller, raw_text, **kwargs):
return "node_quit_or_login", {"login": True, "account": account}
else:
session.msg("|R{}|n".format("\n".join(errors)))
return None # re-run the username node
return None # re-run the username node
try:
_ACCOUNT.objects.get(username__iexact=username)
@ -86,28 +89,26 @@ def node_enter_username(caller, raw_text, **kwargs):
new_user = False
# pass username/new_user into next node as kwargs
return "node_enter_password", {'new_user': new_user, 'username': username}
return "node_enter_password", {"new_user": new_user, "username": username}
callables = callables_from_module(_CONNECTION_SCREEN_MODULE)
if "connection_screen" in callables:
connection_screen = callables['connection_screen']()
connection_screen = callables["connection_screen"]()
else:
connection_screen = random_string_from_module(_CONNECTION_SCREEN_MODULE)
if _GUEST_ENABLED:
text = "Enter a new or existing user name to login (write 'guest' for a guest login):"
else:
else:
text = "Enter a new or existing user name to login:"
text = "{}\n\n{}".format(connection_screen, text)
options = ({"key": "",
"goto": "node_enter_username"},
{"key": ("quit", "q"),
"goto": "node_quit_or_login"},
{"key": ("help", "h"),
"goto": (_show_help, {"help_entry": _ACCOUNT_HELP, **kwargs})},
{"key": "_default",
"goto": _check_input})
options = (
{"key": "", "goto": "node_enter_username"},
{"key": ("quit", "q"), "goto": "node_quit_or_login"},
{"key": ("help", "h"), "goto": (_show_help, {"help_entry": _ACCOUNT_HELP, **kwargs})},
{"key": "_default", "goto": _check_input},
)
return text, options
@ -116,6 +117,7 @@ def node_enter_password(caller, raw_string, **kwargs):
Handle password input.
"""
def _check_input(caller, password, **kwargs):
"""
'Goto-callable', set up to be called from the _default option below.
@ -129,20 +131,22 @@ def node_enter_password(caller, raw_string, **kwargs):
"""
# these flags were set by the goto-callable
username = kwargs['username']
new_user = kwargs['new_user']
password = password.rstrip('\n')
username = kwargs["username"]
new_user = kwargs["new_user"]
password = password.rstrip("\n")
session = caller
address = session.address
if new_user:
# create a new account
account, errors = _ACCOUNT.create(username=username, password=password,
ip=address, session=session)
account, errors = _ACCOUNT.create(
username=username, password=password, ip=address, session=session
)
else:
# check password against existing account
account, errors = _ACCOUNT.authenticate(username=username, password=password,
ip=address, session=session)
account, errors = _ACCOUNT.authenticate(
username=username, password=password, ip=address, session=session
)
if account:
if new_user:
@ -152,32 +156,31 @@ def node_enter_password(caller, raw_string, **kwargs):
else:
# restart due to errors
session.msg("|R{}".format("\n".join(errors)))
kwargs['retry_password'] = True
kwargs["retry_password"] = True
return "node_enter_password", kwargs
def _restart_login(caller, *args, **kwargs):
caller.msg("|yCancelled login.|n")
return "node_enter_username"
username = kwargs['username']
username = kwargs["username"]
if kwargs["new_user"]:
if kwargs.get('retry_password'):
if kwargs.get("retry_password"):
# Attempting to fix password
text = "Enter a new password:"
else:
text = ("Creating a new account |c{}|n. "
"Enter a password (empty to abort):".format(username))
text = "Creating a new account |c{}|n. " "Enter a password (empty to abort):".format(
username
)
else:
text = "Enter the password for account |c{}|n (empty to abort):".format(username)
options = ({"key": "",
"goto": _restart_login},
{"key": ("quit", "q"),
"goto": "node_quit_or_login"},
{"key": ("help", "h"),
"goto": (_show_help, {"help_entry": _PASSWORD_HELP, **kwargs})},
{"key": "_default",
"goto": (_check_input, kwargs)})
options = (
{"key": "", "goto": _restart_login},
{"key": ("quit", "q"), "goto": "node_quit_or_login"},
{"key": ("help", "h"), "goto": (_show_help, {"help_entry": _PASSWORD_HELP, **kwargs})},
{"key": "_default", "goto": (_check_input, kwargs)},
)
return text, options
@ -198,6 +201,7 @@ def node_quit_or_login(caller, raw_text, **kwargs):
# EvMenu helper function
def _node_formatter(nodetext, optionstext, caller=None):
"""Do not display the options, only the text.
@ -211,6 +215,7 @@ def _node_formatter(nodetext, optionstext, caller=None):
# Commands and CmdSets
class UnloggedinCmdSet(CmdSet):
"Cmdset for the unloggedin state"
key = "DefaultUnloggedin"
@ -228,6 +233,7 @@ class CmdUnloggedinLook(Command):
to the menu's own look command.
"""
key = syscmdkeys.CMD_LOGINSTART
locks = "cmd:all()"
arg_regex = r"^$"
@ -237,6 +243,12 @@ class CmdUnloggedinLook(Command):
Run the menu using the nodes in this module.
"""
EvMenu(self.caller, "evennia.contrib.menu_login",
startnode="node_enter_username", auto_look=False, auto_quit=False,
cmd_on_exit=None, node_formatter=_node_formatter)
EvMenu(
self.caller,
"evennia.contrib.menu_login",
startnode="node_enter_username",
auto_look=False,
auto_quit=False,
cmd_on_exit=None,
node_formatter=_node_formatter,
)

View file

@ -38,6 +38,7 @@ _RE_KEYS = re.compile(r"([\w\s]+)(?:\+*?)", re.U + re.I)
# Helper functions for the Command
class DescValidateError(ValueError):
"Used for tracebacks from desc systems"
pass
@ -95,6 +96,7 @@ def _update_store(caller, key=None, desc=None, delete=False, swapkey=None):
else:
raise DescValidateError("No description was set.")
# eveditor save/load/quit functions
@ -123,6 +125,7 @@ def _quit_editor(caller):
# The actual command class
class CmdMultiDesc(default_cmds.MuxCommand):
"""
Manage multiple descriptions
@ -144,6 +147,7 @@ class CmdMultiDesc(default_cmds.MuxCommand):
paragraph and + + or ansi space ||_ to add extra whitespace.
"""
key = "+desc"
aliases = ["desc"]
locks = "cmd:all()"
@ -166,11 +170,14 @@ class CmdMultiDesc(default_cmds.MuxCommand):
_update_store(caller)
do_crop = "full" not in switches
if do_crop:
outtext = ["|w%s:|n %s" % (key, crop(desc))
for key, desc in caller.db.multidesc]
outtext = [
"|w%s:|n %s" % (key, crop(desc)) for key, desc in caller.db.multidesc
]
else:
outtext = ["\n|w%s:|n|n\n%s\n%s" % (key, "-" * (len(key) + 1), desc)
for key, desc in caller.db.multidesc]
outtext = [
"\n|w%s:|n|n\n%s\n%s" % (key, "-" * (len(key) + 1), desc)
for key, desc in caller.db.multidesc
]
caller.msg("|wStored descs:|n\n" + "\n".join(outtext))
return
@ -184,8 +191,14 @@ class CmdMultiDesc(default_cmds.MuxCommand):
# this is used by the editor to know what to edit; it's deleted automatically
caller.db._multidesc_editkey = args
# start the editor
EvEditor(caller, loadfunc=_load_editor, savefunc=_save_editor,
quitfunc=_quit_editor, key="multidesc editor", persistent=True)
EvEditor(
caller,
loadfunc=_load_editor,
savefunc=_save_editor,
quitfunc=_quit_editor,
key="multidesc editor",
persistent=True,
)
elif "delete" in switches or "del" in switches:
# delete a multidesc entry.

View file

@ -81,17 +81,18 @@ from evennia.utils import search, utils, logger
from evennia.prototypes.spawner import spawn
# Tag used by puzzles
_PUZZLES_TAG_CATEGORY = 'puzzles'
_PUZZLES_TAG_RECIPE = 'puzzle_recipe'
_PUZZLES_TAG_CATEGORY = "puzzles"
_PUZZLES_TAG_RECIPE = "puzzle_recipe"
# puzzle part and puzzle result
_PUZZLES_TAG_MEMBER = 'puzzle_member'
_PUZZLES_TAG_MEMBER = "puzzle_member"
_PUZZLE_DEFAULT_FAIL_USE_MESSAGE = 'You try to utilize %s but nothing happens ... something amiss?'
_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = 'You are a Genius!!!'
_PUZZLE_DEFAULT_FAIL_USE_MESSAGE = "You try to utilize %s but nothing happens ... something amiss?"
_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = "You are a Genius!!!"
_PUZZLE_DEFAULT_SUCCESS_USE_LOCATION_MESSAGE = "|c{caller}|n performs some kind of tribal dance and |y{result_names}|n seems to appear from thin air"
# ----------- UTILITY FUNCTIONS ------------
def proto_def(obj, with_tags=True):
"""
Basic properties needed to spawn
@ -99,20 +100,20 @@ def proto_def(obj, with_tags=True):
"""
protodef = {
# TODO: Don't we need to honor ALL properties? attributes, contents, etc.
'prototype_key': '%s(%s)' % (obj.key, obj.dbref),
'key': obj.key,
'typeclass': obj.typeclass_path,
'desc': obj.db.desc,
'location': obj.location,
'home': obj.home,
'locks': ';'.join(obj.locks.all()),
'permissions': obj.permissions.all()[:],
"prototype_key": "%s(%s)" % (obj.key, obj.dbref),
"key": obj.key,
"typeclass": obj.typeclass_path,
"desc": obj.db.desc,
"location": obj.location,
"home": obj.home,
"locks": ";".join(obj.locks.all()),
"permissions": obj.permissions.all()[:],
}
if with_tags:
tags = obj.tags.all(return_key_and_category=True)
tags = [(t[0], t[1], None) for t in tags]
tags.append((_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY, None))
protodef['tags'] = tags
protodef["tags"] = tags
return protodef
@ -130,14 +131,15 @@ def maskout_protodef(protodef, mask):
# Colorize the default success message
def _colorize_message(msg):
_i = 0
_colors = ['|r', '|g', '|y']
_colors = ["|r", "|g", "|y"]
_msg = []
for l in msg:
_msg += _colors[_i] + l
_i = (_i + 1) % len(_colors)
msg = ''.join(_msg) + '|n'
msg = "".join(_msg) + "|n"
return msg
_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = _colorize_message(_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE)
# ------------------------------------------
@ -182,13 +184,13 @@ class CmdCreatePuzzleRecipe(MuxCommand):
"""
key = '@puzzle'
aliases = '@puzzlerecipe'
locks = 'cmd:perm(puzzle) or perm(Builder)'
help_category = 'Puzzles'
key = "@puzzle"
aliases = "@puzzlerecipe"
locks = "cmd:perm(puzzle) or perm(Builder)"
help_category = "Puzzles"
confirm = True
default_confirm = 'no'
default_confirm = "no"
def func(self):
caller = self.caller
@ -200,27 +202,26 @@ class CmdCreatePuzzleRecipe(MuxCommand):
puzzle_name = self.lhslist[0]
if len(puzzle_name) == 0:
caller.msg('Invalid puzzle name %r.' % puzzle_name)
caller.msg("Invalid puzzle name %r." % puzzle_name)
return
# if there is another puzzle with same name
# warn user that parts and results will be
# interchangable
_puzzles = search.search_script_attribute(
key='puzzle_name',
value=puzzle_name
)
_puzzles = search.search_script_attribute(key="puzzle_name", value=puzzle_name)
_puzzles = list(filter(lambda p: isinstance(p, PuzzleRecipe), _puzzles))
if _puzzles:
confirm = 'There are %d puzzles with the same name.\n' % len(_puzzles) \
+ 'Its parts and results will be interchangeable.\n' \
+ 'Continue yes/[no]? '
answer = ''
while answer.strip().lower() not in ('y', 'yes', 'n', 'no'):
answer = yield(confirm)
answer = self.default_confirm if answer == '' else answer
if answer.strip().lower() in ('n', 'no'):
caller.msg('Cancelled: no puzzle created.')
confirm = (
"There are %d puzzles with the same name.\n" % len(_puzzles)
+ "Its parts and results will be interchangeable.\n"
+ "Continue yes/[no]? "
)
answer = ""
while answer.strip().lower() not in ("y", "yes", "n", "no"):
answer = yield (confirm)
answer = self.default_confirm if answer == "" else answer
if answer.strip().lower() in ("n", "no"):
caller.msg("Cancelled: no puzzle created.")
return
def is_valid_obj_location(obj):
@ -235,7 +236,7 @@ class CmdCreatePuzzleRecipe(MuxCommand):
# located.
# Parts and results may have different valid locations
if not inherits_from(obj.location, DefaultRoom):
caller.msg('Invalid location for %s' % (obj.key))
caller.msg("Invalid location for %s" % (obj.key))
valid = False
return valid
@ -246,20 +247,20 @@ class CmdCreatePuzzleRecipe(MuxCommand):
return is_valid_obj_location(part)
def is_valid_inheritance(obj):
valid = not inherits_from(obj, DefaultCharacter) \
and not inherits_from(obj, DefaultRoom) \
and not inherits_from(obj, DefaultExit)
valid = (
not inherits_from(obj, DefaultCharacter)
and not inherits_from(obj, DefaultRoom)
and not inherits_from(obj, DefaultExit)
)
if not valid:
caller.msg('Invalid typeclass for %s' % (obj))
caller.msg("Invalid typeclass for %s" % (obj))
return valid
def is_valid_part(part):
return is_valid_inheritance(part) \
and is_valid_part_location(part)
return is_valid_inheritance(part) and is_valid_part_location(part)
def is_valid_result(result):
return is_valid_inheritance(result) \
and is_valid_result_location(result)
return is_valid_inheritance(result) and is_valid_result_location(result)
parts = []
for objname in self.lhslist[1:]:
@ -280,26 +281,28 @@ class CmdCreatePuzzleRecipe(MuxCommand):
results.append(obj)
for part in parts:
caller.msg('Part %s(%s)' % (part.name, part.dbref))
caller.msg("Part %s(%s)" % (part.name, part.dbref))
for result in results:
caller.msg('Result %s(%s)' % (result.name, result.dbref))
caller.msg("Result %s(%s)" % (result.name, result.dbref))
proto_parts = [proto_def(obj) for obj in parts]
proto_results = [proto_def(obj) for obj in results]
puzzle = create_script(PuzzleRecipe, key=puzzle_name)
puzzle.save_recipe(puzzle_name, proto_parts, proto_results)
puzzle.locks.add('control:id(%s) or perm(Builder)' % caller.dbref[1:])
puzzle.locks.add("control:id(%s) or perm(Builder)" % caller.dbref[1:])
caller.msg(
"Puzzle |y'%s' |w%s(%s)|n has been created |gsuccessfully|n."
% (puzzle.db.puzzle_name, puzzle.name, puzzle.dbref))
% (puzzle.db.puzzle_name, puzzle.name, puzzle.dbref)
)
caller.msg(
'You may now dispose of all parts and results. \n'
'Use @puzzleedit #{dbref} to customize this puzzle further. \n'
'Use @armpuzzle #{dbref} to arm a new puzzle instance.'.format(dbref=puzzle.dbref))
"You may now dispose of all parts and results. \n"
"Use @puzzleedit #{dbref} to customize this puzzle further. \n"
"Use @armpuzzle #{dbref} to arm a new puzzle instance.".format(dbref=puzzle.dbref)
)
class CmdEditPuzzle(MuxCommand):
@ -331,9 +334,9 @@ class CmdEditPuzzle(MuxCommand):
"""
key = '@puzzleedit'
locks = 'cmd:perm(puzzleedit) or perm(Builder)'
help_category = 'Puzzles'
key = "@puzzleedit"
locks = "cmd:perm(puzzleedit) or perm(Builder)"
help_category = "Puzzles"
def func(self):
self._USAGE = "Usage: @puzzleedit[/switches] <dbref>[/attribute = <value>]"
@ -343,8 +346,8 @@ class CmdEditPuzzle(MuxCommand):
caller.msg(self._USAGE)
return
if '/' in self.lhslist[0]:
recipe_dbref, attr = self.lhslist[0].split('/')
if "/" in self.lhslist[0]:
recipe_dbref, attr = self.lhslist[0].split("/")
else:
recipe_dbref = self.lhslist[0]
@ -354,75 +357,73 @@ class CmdEditPuzzle(MuxCommand):
puzzle = search.search_script(recipe_dbref)
if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe):
caller.msg('%s(%s) is not a puzzle' % (puzzle[0].name, recipe_dbref))
caller.msg("%s(%s) is not a puzzle" % (puzzle[0].name, recipe_dbref))
return
puzzle = puzzle[0]
puzzle_name_id = '%s(%s)' % (puzzle.name, puzzle.dbref)
puzzle_name_id = "%s(%s)" % (puzzle.name, puzzle.dbref)
if 'delete' in self.switches:
if not (puzzle.access(caller, 'control') or puzzle.access(caller, 'delete')):
if "delete" in self.switches:
if not (puzzle.access(caller, "control") or puzzle.access(caller, "delete")):
caller.msg("You don't have permission to delete %s." % puzzle_name_id)
return
puzzle.delete()
caller.msg('%s was deleted' % puzzle_name_id)
caller.msg("%s was deleted" % puzzle_name_id)
return
elif 'addpart' in self.switches:
elif "addpart" in self.switches:
objs = self._get_objs()
if objs:
added = self._add_parts(objs, puzzle)
caller.msg('%s were added to parts' % (', '.join(added)))
caller.msg("%s were added to parts" % (", ".join(added)))
return
elif 'delpart' in self.switches:
elif "delpart" in self.switches:
objs = self._get_objs()
if objs:
removed = self._remove_parts(objs, puzzle)
caller.msg('%s were removed from parts' % (', '.join(removed)))
caller.msg("%s were removed from parts" % (", ".join(removed)))
return
elif 'addresult' in self.switches:
elif "addresult" in self.switches:
objs = self._get_objs()
if objs:
added = self._add_results(objs, puzzle)
caller.msg('%s were added to results' % (', '.join(added)))
caller.msg("%s were added to results" % (", ".join(added)))
return
elif 'delresult' in self.switches:
elif "delresult" in self.switches:
objs = self._get_objs()
if objs:
removed = self._remove_results(objs, puzzle)
caller.msg('%s were removed from results' % (', '.join(removed)))
caller.msg("%s were removed from results" % (", ".join(removed)))
return
else:
# edit attributes
if not (puzzle.access(caller, 'control') or puzzle.access(caller, 'edit')):
if not (puzzle.access(caller, "control") or puzzle.access(caller, "edit")):
caller.msg("You don't have permission to edit %s." % puzzle_name_id)
return
if attr == 'use_success_message':
if attr == "use_success_message":
puzzle.db.use_success_message = self.rhs
caller.msg(
"%s use_success_message = %s\n" % (
puzzle_name_id, puzzle.db.use_success_message)
"%s use_success_message = %s\n"
% (puzzle_name_id, puzzle.db.use_success_message)
)
return
elif attr == 'use_success_location_message':
elif attr == "use_success_location_message":
puzzle.db.use_success_location_message = self.rhs
caller.msg(
"%s use_success_location_message = %s\n" % (
puzzle_name_id, puzzle.db.use_success_location_message)
"%s use_success_location_message = %s\n"
% (puzzle_name_id, puzzle.db.use_success_location_message)
)
return
elif attr == 'mask':
elif attr == "mask":
puzzle.db.mask = tuple(self.rhslist)
caller.msg(
"%s mask = %r\n" % (puzzle_name_id, puzzle.db.mask)
)
caller.msg("%s mask = %r\n" % (puzzle_name_id, puzzle.db.mask))
return
def _get_objs(self):
@ -491,9 +492,9 @@ class CmdArmPuzzle(MuxCommand):
"""
key = '@armpuzzle'
locks = 'cmd:perm(armpuzzle) or perm(Builder)'
help_category = 'Puzzles'
key = "@armpuzzle"
locks = "cmd:perm(armpuzzle) or perm(Builder)"
help_category = "Puzzles"
def func(self):
caller = self.caller
@ -504,18 +505,21 @@ class CmdArmPuzzle(MuxCommand):
puzzle = search.search_script(self.args)
if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe):
caller.msg('Invalid puzzle %r' % (self.args))
caller.msg("Invalid puzzle %r" % (self.args))
return
puzzle = puzzle[0]
caller.msg(
"Puzzle Recipe %s(%s) '%s' found.\nSpawning %d parts ..." % (
puzzle.name, puzzle.dbref, puzzle.db.puzzle_name, len(puzzle.db.parts)))
"Puzzle Recipe %s(%s) '%s' found.\nSpawning %d parts ..."
% (puzzle.name, puzzle.dbref, puzzle.db.puzzle_name, len(puzzle.db.parts))
)
for proto_part in puzzle.db.parts:
part = spawn(proto_part)[0]
caller.msg("Part %s(%s) spawned and placed at %s(%s)" % (
part.name, part.dbref, part.location, part.location.dbref))
caller.msg(
"Part %s(%s) spawned and placed at %s(%s)"
% (part.name, part.dbref, part.location, part.location.dbref)
)
part.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY)
part.db.puzzle_name = puzzle.db.puzzle_name
@ -531,7 +535,7 @@ def _lookups_parts_puzzlenames_protodefs(parts):
parts_dict[part.dbref] = part
protodef = proto_def(part, with_tags=False)
# remove 'prototype_key' as it will prevent equality
del(protodef['prototype_key'])
del protodef["prototype_key"]
puzzle_ingredients[part.dbref] = protodef
tags_categories = part.tags.all(return_key_and_category=True)
for tag, category in tags_categories:
@ -547,10 +551,7 @@ def _puzzles_by_names(names):
# Find all puzzles by puzzle name (i.e. tag name)
puzzles = []
for puzzle_name in names:
_puzzles = search.search_script_attribute(
key='puzzle_name',
value=puzzle_name
)
_puzzles = search.search_script_attribute(key="puzzle_name", value=puzzle_name)
_puzzles = list(filter(lambda p: isinstance(p, PuzzleRecipe), _puzzles))
if not _puzzles:
continue
@ -567,8 +568,8 @@ def _matching_puzzles(puzzles, puzzlename_tags_dict, puzzle_ingredients):
puzzle_mask = puzzle.db.mask[:]
# remove tags and prototype_key as they prevent equality
for i, puzzle_protopart in enumerate(puzzle_protoparts[:]):
del(puzzle_protopart['tags'])
del(puzzle_protopart['prototype_key'])
del puzzle_protopart["tags"]
del puzzle_protopart["prototype_key"]
puzzle_protopart = maskout_protodef(puzzle_protopart, puzzle_mask)
puzzle_protoparts[i] = puzzle_protopart
@ -596,19 +597,19 @@ class CmdUsePuzzleParts(MuxCommand):
use <part1[,part2,...>]
"""
key = 'use'
aliases = 'combine'
locks = 'cmd:pperm(use) or pperm(Player)'
help_category = 'Puzzles'
key = "use"
aliases = "combine"
locks = "cmd:pperm(use) or pperm(Player)"
help_category = "Puzzles"
def func(self):
caller = self.caller
if not self.lhs:
caller.msg('Use what?')
caller.msg("Use what?")
return
many = 'these' if len(self.lhslist) > 1 else 'this'
many = "these" if len(self.lhslist) > 1 else "this"
# either all are parts, or abort finding matching puzzles
parts = []
@ -616,8 +617,8 @@ class CmdUsePuzzleParts(MuxCommand):
for partname in partnames:
part = caller.search(
partname,
multimatch_string='Which %s. There are many.\n' % (partname),
nofound_string='There is no %s around.' % (partname)
multimatch_string="Which %s. There are many.\n" % (partname),
nofound_string="There is no %s around." % (partname),
)
if not part:
@ -626,15 +627,16 @@ class CmdUsePuzzleParts(MuxCommand):
if not part.tags.get(_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY):
# not a puzzle part ... abort
caller.msg('You have no idea how %s can be used' % (many))
caller.msg("You have no idea how %s can be used" % (many))
return
# a valid part
parts.append(part)
# Create lookup dicts by part's dbref and by puzzle_name(tags)
parts_dict, puzzlename_tags_dict, puzzle_ingredients = \
_lookups_parts_puzzlenames_protodefs(parts)
parts_dict, puzzlename_tags_dict, puzzle_ingredients = _lookups_parts_puzzlenames_protodefs(
parts
)
# Find all puzzles by puzzle name (i.e. tag name)
puzzles = _puzzles_by_names(puzzlename_tags_dict.keys())
@ -644,8 +646,7 @@ class CmdUsePuzzleParts(MuxCommand):
# Create lookup dict of puzzles by dbref
puzzles_dict = dict((puzzle.dbref, puzzle) for puzzle in puzzles)
# Check if parts can be combined to solve a puzzle
matched_puzzles = _matching_puzzles(
puzzles, puzzlename_tags_dict, puzzle_ingredients)
matched_puzzles = _matching_puzzles(puzzles, puzzlename_tags_dict, puzzle_ingredients)
if len(matched_puzzles) == 0:
# TODO: we could use part.fail_message instead, if there was one
@ -671,7 +672,7 @@ class CmdUsePuzzleParts(MuxCommand):
# just hint how many.
if len(largest_puzzles) > 1:
caller.msg(
'Your gears start turning and %d different ideas come to your mind ...\n'
"Your gears start turning and %d different ideas come to your mind ...\n"
% (len(largest_puzzles))
)
puzzletuple = choice(largest_puzzles)
@ -690,12 +691,11 @@ class CmdUsePuzzleParts(MuxCommand):
for dbref in matched_dbrefparts:
parts_dict[dbref].delete()
result_names = ', '.join(result_names)
result_names = ", ".join(result_names)
caller.msg(puzzle.db.use_success_message)
caller.location.msg_contents(
puzzle.db.use_success_location_message.format(
caller=caller, result_names=result_names),
exclude=(caller,)
puzzle.db.use_success_location_message.format(caller=caller, result_names=result_names),
exclude=(caller,),
)
@ -707,15 +707,14 @@ class CmdListPuzzleRecipes(MuxCommand):
@lspuzzlerecipes
"""
key = '@lspuzzlerecipes'
locks = 'cmd:perm(lspuzzlerecipes) or perm(Builder)'
help_category = 'Puzzles'
key = "@lspuzzlerecipes"
locks = "cmd:perm(lspuzzlerecipes) or perm(Builder)"
help_category = "Puzzles"
def func(self):
caller = self.caller
recipes = search.search_script_tag(
_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY)
recipes = search.search_script_tag(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY)
div = "-" * 60
text = [div]
@ -723,26 +722,28 @@ class CmdListPuzzleRecipes(MuxCommand):
msgf_item = "%2s|c%15s|n: |w%s|n"
for recipe in recipes:
text.append(msgf_recipe % (recipe.db.puzzle_name, recipe.name, recipe.dbref))
text.append('Success Caller message:\n' + recipe.db.use_success_message + '\n')
text.append('Success Location message:\n' + recipe.db.use_success_location_message + '\n')
text.append('Mask:\n' + str(recipe.db.mask) + '\n')
text.append('Parts')
text.append("Success Caller message:\n" + recipe.db.use_success_message + "\n")
text.append(
"Success Location message:\n" + recipe.db.use_success_location_message + "\n"
)
text.append("Mask:\n" + str(recipe.db.mask) + "\n")
text.append("Parts")
for protopart in recipe.db.parts[:]:
mark = '-'
mark = "-"
for k, v in protopart.items():
text.append(msgf_item % (mark, k, v))
mark = ''
text.append('Results')
mark = ""
text.append("Results")
for protoresult in recipe.db.results[:]:
mark = '-'
mark = "-"
for k, v in protoresult.items():
text.append(msgf_item % (mark, k, v))
mark = ''
mark = ""
else:
text.append(div)
text.append('Found |r%d|n puzzle(s).' % (len(recipes)))
text.append("Found |r%d|n puzzle(s)." % (len(recipes)))
text.append(div)
caller.msg('\n'.join(text))
caller.msg("\n".join(text))
class CmdListArmedPuzzles(MuxCommand):
@ -753,35 +754,34 @@ class CmdListArmedPuzzles(MuxCommand):
@lsarmedpuzzles
"""
key = '@lsarmedpuzzles'
locks = 'cmd:perm(lsarmedpuzzles) or perm(Builder)'
help_category = 'Puzzles'
key = "@lsarmedpuzzles"
locks = "cmd:perm(lsarmedpuzzles) or perm(Builder)"
help_category = "Puzzles"
def func(self):
caller = self.caller
armed_puzzles = search.search_tag(
_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY)
armed_puzzles = search.search_tag(_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY)
armed_puzzles = dict((k, list(g)) for k, g in itertools.groupby(
armed_puzzles,
lambda ap: ap.db.puzzle_name))
armed_puzzles = dict(
(k, list(g)) for k, g in itertools.groupby(armed_puzzles, lambda ap: ap.db.puzzle_name)
)
div = '-' * 60
div = "-" * 60
msgf_pznm = "Puzzle name: |y%s|n"
msgf_item = "|m%25s|w(%s)|n at |c%25s|w(%s)|n"
text = [div]
for pzname, items in armed_puzzles.items():
text.append(msgf_pznm % (pzname))
for item in items:
text.append(msgf_item % (
item.name, item.dbref,
item.location.name, item.location.dbref))
text.append(
msgf_item % (item.name, item.dbref, item.location.name, item.location.dbref)
)
else:
text.append(div)
text.append('Found |r%d|n armed puzzle(s).' % (len(armed_puzzles)))
text.append("Found |r%d|n armed puzzle(s)." % (len(armed_puzzles)))
text.append(div)
caller.msg('\n'.join(text))
caller.msg("\n".join(text))
class PuzzleSystemCmdSet(CmdSet):

View file

@ -156,7 +156,9 @@ class RandomStringGenerator(object):
self._find_elements(regex)
def __repr__(self):
return "<evennia.contrib.random_string_generator.RandomStringGenerator for {}>".format(self.name)
return "<evennia.contrib.random_string_generator.RandomStringGenerator for {}>".format(
self.name
)
def _get_script(self):
"""Get or create the script."""
@ -191,7 +193,9 @@ class RandomStringGenerator(object):
# If `.`, break here
if name == "any":
raise RejectedRegex("the . definition is too broad, specify what you need more precisely")
raise RejectedRegex(
"the . definition is too broad, specify what you need more precisely"
)
elif name == "at":
# Either the beginning or end, we ignore it
continue
@ -332,8 +336,11 @@ class RandomStringGenerator(object):
script = self._get_script()
generated = script.db.generated.get(self.name, [])
if element not in generated:
raise ValueError("the string {} isn't stored as generated by the generator {}".format(
element, self.name))
raise ValueError(
"the string {} isn't stored as generated by the generator {}".format(
element, self.name
)
)
generated.remove(element)

View file

@ -97,15 +97,17 @@ from evennia import DefaultScript
from evennia.utils import logger
#------------------------------------------------------------
# ------------------------------------------------------------
#
# Obfuscate language
#
#------------------------------------------------------------
# ------------------------------------------------------------
# default language grammar
_PHONEMES = "ea oh ae aa eh ah ao aw ai er ey ow ia ih iy oy ua uh uw a e i u y p b t d f v t dh " \
"s z sh zh ch jh k ng g m n l r w"
_PHONEMES = (
"ea oh ae aa eh ah ao aw ai er ey ow ia ih iy oy ua uh uw a e i u y p b t d f v t dh "
"s z sh zh ch jh k ng g m n l r w"
)
_VOWELS = "eaoiuy"
# these must be able to be constructed from phonemes (so for example,
# if you have v here, there must exist at least one single-character
@ -114,8 +116,8 @@ _GRAMMAR = "v cv vc cvv vcc vcv cvcc vccv cvccv cvcvcc cvccvcv vccvccvc cvcvccvv
_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.DOTALL + re.UNICODE
_RE_GRAMMAR = re.compile(r"vv|cc|v|c", _RE_FLAGS)
_RE_WORD = re.compile(r'\w+', _RE_FLAGS)
_RE_EXTRA_CHARS = re.compile(r'\s+(?=\W)|[,.?;](?=[,.?;]|\s+[,.?;])', _RE_FLAGS)
_RE_WORD = re.compile(r"\w+", _RE_FLAGS)
_RE_EXTRA_CHARS = re.compile(r"\s+(?=\W)|[,.?;](?=[,.?;]|\s+[,.?;])", _RE_FLAGS)
class LanguageError(RuntimeError):
@ -159,13 +161,20 @@ class LanguageHandler(DefaultScript):
self.persistent = True
self.db.language_storage = {}
def add(self, key="default", phonemes=_PHONEMES,
grammar=_GRAMMAR, word_length_variance=0,
noun_translate=False,
noun_prefix="",
noun_postfix="",
vowels=_VOWELS, manual_translations=None,
auto_translations=None, force=False):
def add(
self,
key="default",
phonemes=_PHONEMES,
grammar=_GRAMMAR,
word_length_variance=0,
noun_translate=False,
noun_prefix="",
noun_postfix="",
vowels=_VOWELS,
manual_translations=None,
auto_translations=None,
force=False,
):
"""
Add a new language. Note that you generally only need to do
this once per language and that adding an existing language
@ -229,7 +238,8 @@ class LanguageHandler(DefaultScript):
if key in self.db.language_storage and not force:
raise LanguageExistsError(
"Language is already created. Re-adding it will re-build"
" its dictionary map. Use 'force=True' keyword if you are sure.")
" its dictionary map. Use 'force=True' keyword if you are sure."
)
# create grammar_component->phoneme mapping
# {"vv": ["ea", "oh", ...], ...}
@ -244,25 +254,25 @@ class LanguageHandler(DefaultScript):
gramdict = defaultdict(list)
for gram in grammar.split():
if re.search("\W|(!=[cv])", gram):
raise LanguageError("The grammar '%s' is invalid (only 'c' and 'v' are allowed)" % gram)
raise LanguageError(
"The grammar '%s' is invalid (only 'c' and 'v' are allowed)" % gram
)
gramdict[len(gram)].append(gram)
grammar = dict(gramdict)
# create automatic translation
translation = {}
if auto_translations:
if isinstance(auto_translations, str):
# path to a file rather than a list
with open(auto_translations, 'r') as f:
with open(auto_translations, "r") as f:
auto_translations = f.readlines()
for word in auto_translations:
word = word.strip()
lword = len(word)
new_word = ""
wlen = max(0, lword + sum(randint(-1, 1) for i
in range(word_length_variance)))
wlen = max(0, lword + sum(randint(-1, 1) for i in range(word_length_variance)))
if wlen not in grammar:
# always create a translation, use random length
structure = choice(grammar[choice(list(grammar))])
@ -275,16 +285,20 @@ class LanguageHandler(DefaultScript):
if manual_translations:
# update with manual translations
translation.update(dict((key.lower(), value.lower()) for key, value in manual_translations.items()))
translation.update(
dict((key.lower(), value.lower()) for key, value in manual_translations.items())
)
# store data
storage = {"translation": translation,
"grammar": grammar,
"grammar2phonemes": dict(grammar2phonemes),
"word_length_variance": word_length_variance,
"noun_translate": noun_translate,
"noun_prefix": noun_prefix,
"noun_postfix": noun_postfix}
storage = {
"translation": translation,
"grammar": grammar,
"grammar2phonemes": dict(grammar2phonemes),
"word_length_variance": word_length_variance,
"noun_translate": noun_translate,
"noun_prefix": noun_prefix,
"noun_postfix": noun_postfix,
}
self.db.language_storage[key] = storage
def _translate_sub(self, match):
@ -321,14 +335,17 @@ class LanguageHandler(DefaultScript):
# make up translation on the fly. Length can
# vary from un-translated word.
wlen = max(0, lword + sum(randint(-1, 1) for i
in range(self.language["word_length_variance"])))
wlen = max(
0,
lword
+ sum(randint(-1, 1) for i in range(self.language["word_length_variance"])),
)
grammar = self.language["grammar"]
if wlen not in grammar:
if randint(0, 1) == 0:
# this word has no direct translation!
wlen = 0
new_word = ''
new_word = ""
else:
# use random word length
wlen = choice(list(grammar.keys()))
@ -341,14 +358,16 @@ class LanguageHandler(DefaultScript):
try:
new_word += choice(grammar2phonemes[match.group()])
except KeyError:
logger.log_trace("You need to supply at least one example of each of "
"the four base phonemes (c, v, cc, vv)")
logger.log_trace(
"You need to supply at least one example of each of "
"the four base phonemes (c, v, cc, vv)"
)
# abort translation here
new_word = ''
new_word = ""
break
if word.istitle():
title_word = ''
title_word = ""
if not start_sentence and not self.language.get("noun_translate", False):
# don't translate what we identify as proper nouns (names)
title_word = word
@ -357,9 +376,11 @@ class LanguageHandler(DefaultScript):
if title_word:
# Regardless of if we translate or not, we will add the custom prefix/postfixes
new_word = "%s%s%s" % (self.language["noun_prefix"],
title_word.capitalize(),
self.language["noun_postfix"])
new_word = "%s%s%s" % (
self.language["noun_prefix"],
title_word.capitalize(),
self.language["noun_postfix"],
)
if len(word) > 1 and word.isupper():
# keep LOUD words loud also when translated
@ -427,6 +448,7 @@ def obfuscate_language(text, level=0.0, language="default"):
except LanguageHandler.DoesNotExist:
if not _LANGUAGE_HANDLER:
from evennia import create_script
_LANGUAGE_HANDLER = create_script(LanguageHandler)
return _LANGUAGE_HANDLER.translate(text, level=level, language=language)
@ -444,6 +466,7 @@ def add_language(**kwargs):
except LanguageHandler.DoesNotExist:
if not _LANGUAGE_HANDLER:
from evennia import create_script
_LANGUAGE_HANDLER = create_script(LanguageHandler)
_LANGUAGE_HANDLER.add(**kwargs)
@ -463,11 +486,12 @@ def available_languages():
except LanguageHandler.DoesNotExist:
if not _LANGUAGE_HANDLER:
from evennia import create_script
_LANGUAGE_HANDLER = create_script(LanguageHandler)
return list(_LANGUAGE_HANDLER.attributes.get("language_storage", {}))
#------------------------------------------------------------
# ------------------------------------------------------------
#
# Whisper obscuration
#
@ -479,24 +503,25 @@ def available_languages():
# give a user some idea of the sentence structure. Then the word
# lengths are also obfuscated and finally the whisper # length itself.
#
#------------------------------------------------------------
# ------------------------------------------------------------
_RE_WHISPER_OBSCURE = [
re.compile(r"^$", _RE_FLAGS), # This is a Test! #0 full whisper
re.compile(r"[ae]", _RE_FLAGS), # This -s - Test! #1 add uy
re.compile(r"[aeuy]", _RE_FLAGS), # This -s - Test! #2 add oue
re.compile(r"[aeiouy]", _RE_FLAGS), # Th-s -s - T-st! #3 add all consonants
re.compile(r"^$", _RE_FLAGS), # This is a Test! #0 full whisper
re.compile(r"[ae]", _RE_FLAGS), # This -s - Test! #1 add uy
re.compile(r"[aeuy]", _RE_FLAGS), # This -s - Test! #2 add oue
re.compile(r"[aeiouy]", _RE_FLAGS), # Th-s -s - T-st! #3 add all consonants
re.compile(r"[aeiouybdhjlmnpqrv]", _RE_FLAGS), # T--s -s - T-st! #4 add hard consonants
re.compile(r"[a-eg-rt-z]", _RE_FLAGS), # T--s -s - T-s-! #5 add all capitals
re.compile(r"[a-eg-rt-z]", _RE_FLAGS), # T--s -s - T-s-! #5 add all capitals
re.compile(r"[A-EG-RT-Za-eg-rt-z]", _RE_FLAGS), # ---s -s - --s-! #6 add f
re.compile(r"[A-EG-RT-Za-rt-z]", _RE_FLAGS), # ---s -s - --s-! #7 add s
re.compile(r"[A-EG-RT-Za-z]", _RE_FLAGS), # ---- -- - ----! #8 add capital F
re.compile(r"[A-RT-Za-z]", _RE_FLAGS), # ---- -- - ----! #9 add capital S
re.compile(r"[\w]", _RE_FLAGS), # ---- -- - ----! #10 non-alphanumerals
re.compile(r"[\S]", _RE_FLAGS), # ---- -- - ---- #11 words
re.compile(r"[\w\W]", _RE_FLAGS), # -------------- #12 whisper length
re.compile(r".*", _RE_FLAGS)] # ... #13 (always same length)
re.compile(r"[A-EG-RT-Za-rt-z]", _RE_FLAGS), # ---s -s - --s-! #7 add s
re.compile(r"[A-EG-RT-Za-z]", _RE_FLAGS), # ---- -- - ----! #8 add capital F
re.compile(r"[A-RT-Za-z]", _RE_FLAGS), # ---- -- - ----! #9 add capital S
re.compile(r"[\w]", _RE_FLAGS), # ---- -- - ----! #10 non-alphanumerals
re.compile(r"[\S]", _RE_FLAGS), # ---- -- - ---- #11 words
re.compile(r"[\w\W]", _RE_FLAGS), # -------------- #12 whisper length
re.compile(r".*", _RE_FLAGS),
] # ... #13 (always same length)
def obfuscate_whisper(whisper, level=0.0):
@ -517,4 +542,4 @@ def obfuscate_whisper(whisper, level=0.0):
if olevel == 13:
return "..."
else:
return _RE_WHISPER_OBSCURE[olevel].sub('-', whisper)
return _RE_WHISPER_OBSCURE[olevel].sub("-", whisper)

View file

@ -103,10 +103,10 @@ from evennia import Command, CmdSet
from evennia import ansi
from evennia.utils.utils import lazy_property, make_iter, variable_from_module
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
#------------------------------------------------------------
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
# ------------------------------------------------------------
# Emote parser
#------------------------------------------------------------
# ------------------------------------------------------------
# Settings
@ -124,11 +124,9 @@ _NUM_SEP = "-"
# Texts
_EMOTE_NOMATCH_ERROR = \
"""|RNo match for |r{ref}|R.|n"""
_EMOTE_NOMATCH_ERROR = """|RNo match for |r{ref}|R.|n"""
_EMOTE_MULTIMATCH_ERROR = \
"""|RMultiple possibilities for {ref}:
_EMOTE_MULTIMATCH_ERROR = """|RMultiple possibilities for {ref}:
|r{reflist}|n"""
_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.UNICODE
@ -139,8 +137,7 @@ _RE_PREFIX = re.compile(r"^%s" % _PREFIX, re.UNICODE)
# separate multimatches from one another and word is the first word in the
# marker. So entering "/tall man" will return groups ("", "tall")
# and "/2-tall man" will return groups ("2", "tall").
_RE_OBJ_REF_START = re.compile(r"%s(?:([0-9]+)%s)*(\w+)" %
(_PREFIX, _NUM_SEP), _RE_FLAGS)
_RE_OBJ_REF_START = re.compile(r"%s(?:([0-9]+)%s)*(\w+)" % (_PREFIX, _NUM_SEP), _RE_FLAGS)
_RE_LEFT_BRACKETS = re.compile(r"\{+", _RE_FLAGS)
_RE_RIGHT_BRACKETS = re.compile(r"\}+", _RE_FLAGS)
@ -233,7 +230,10 @@ def ordered_permutation_regex(sentence):
elif comb:
break
if comb:
solution.append(_PREFIX + r"[0-9]*%s*%s(?=\W|$)+" % (_NUM_SEP, re_escape(" ".join(comb)).rstrip("\\")))
solution.append(
_PREFIX
+ r"[0-9]*%s*%s(?=\W|$)+" % (_NUM_SEP, re_escape(" ".join(comb)).rstrip("\\"))
)
# combine into a match regex, first matching the longest down to the shortest components
regex = r"|".join(sorted(set(solution), key=lambda item: (-len(item), item)))
@ -257,9 +257,11 @@ def regex_tuple_from_key_alias(obj):
(ordered_permutation_regex, obj, key/alias)
"""
return (re.compile(ordered_permutation_regex(" ".join([obj.key] + obj.aliases.all())), _RE_FLAGS),
obj,
obj.key)
return (
re.compile(ordered_permutation_regex(" ".join([obj.key] + obj.aliases.all())), _RE_FLAGS),
obj,
obj.key,
)
def parse_language(speaker, emote):
@ -357,14 +359,20 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False):
"""
# Load all candidate regex tuples [(regex, obj, sdesc/recog),...]
candidate_regexes = \
([(_RE_SELF_REF, sender, sender.sdesc.get())] if hasattr(sender, "sdesc") else []) + \
([sender.recog.get_regex_tuple(obj) for obj in candidates] if hasattr(sender, "recog") else []) + \
[obj.sdesc.get_regex_tuple()
for obj in candidates if hasattr(obj, "sdesc")] + \
[regex_tuple_from_key_alias(obj) # handle objects without sdescs
for obj in candidates if not (hasattr(obj, "recog") and
hasattr(obj, "sdesc"))]
candidate_regexes = (
([(_RE_SELF_REF, sender, sender.sdesc.get())] if hasattr(sender, "sdesc") else [])
+ (
[sender.recog.get_regex_tuple(obj) for obj in candidates]
if hasattr(sender, "recog")
else []
)
+ [obj.sdesc.get_regex_tuple() for obj in candidates if hasattr(obj, "sdesc")]
+ [
regex_tuple_from_key_alias(obj) # handle objects without sdescs
for obj in candidates
if not (hasattr(obj, "recog") and hasattr(obj, "sdesc"))
]
)
# filter out non-found data
candidate_regexes = [tup for tup in candidate_regexes if tup]
@ -434,16 +442,26 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False):
errors.append(_EMOTE_NOMATCH_ERROR.format(ref=marker_match.group()))
elif nmatches == 1:
key = "#%i" % obj.id
string = string[:istart0] + "{%s}" % key + string[istart + maxscore:]
string = string[:istart0] + "{%s}" % key + string[istart + maxscore :]
mapping[key] = obj
else:
refname = marker_match.group()
reflist = ["%s%s%s (%s%s)" % (inum + 1, _NUM_SEP,
_RE_PREFIX.sub("", refname), text,
" (%s)" % sender.key if sender == ob else "")
for inum, (ob, text) in enumerate(obj)]
errors.append(_EMOTE_MULTIMATCH_ERROR.format(
ref=marker_match.group(), reflist="\n ".join(reflist)))
reflist = [
"%s%s%s (%s%s)"
% (
inum + 1,
_NUM_SEP,
_RE_PREFIX.sub("", refname),
text,
" (%s)" % sender.key if sender == ob else "",
)
for inum, (ob, text) in enumerate(obj)
]
errors.append(
_EMOTE_MULTIMATCH_ERROR.format(
ref=marker_match.group(), reflist="\n ".join(reflist)
)
)
if search_mode:
# return list of object(s) matching
if nmatches == 0:
@ -495,8 +513,8 @@ def send_emote(sender, receivers, emote, anonymous_add="first"):
# no self-reference in the emote - add to the end
key = "#%i" % sender.id
obj_mapping[key] = sender
if anonymous_add == 'first':
possessive = "" if emote.startswith('\'') else " "
if anonymous_add == "first":
possessive = "" if emote.startswith("'") else " "
emote = "%s%s%s" % ("{{%s}}" % key, possessive, emote)
else:
emote = "%s [%s]" % (emote, "{{%s}}" % key)
@ -529,11 +547,19 @@ def send_emote(sender, receivers, emote, anonymous_add="first"):
try:
recog_get = receiver.recog.get
receiver_sdesc_mapping = dict((ref, process_recog(recog_get(obj), obj)) for ref, obj in obj_mapping.items())
receiver_sdesc_mapping = dict(
(ref, process_recog(recog_get(obj), obj)) for ref, obj in obj_mapping.items()
)
except AttributeError:
receiver_sdesc_mapping = dict((ref, process_sdesc(obj.sdesc.get(), obj)
if hasattr(obj, "sdesc") else process_sdesc(obj.key, obj))
for ref, obj in obj_mapping.items())
receiver_sdesc_mapping = dict(
(
ref,
process_sdesc(obj.sdesc.get(), obj)
if hasattr(obj, "sdesc")
else process_sdesc(obj.key, obj),
)
for ref, obj in obj_mapping.items()
)
# make sure receiver always sees their real name
rkey = "#%i" % receiver.id
if rkey in receiver_sdesc_mapping:
@ -542,9 +568,10 @@ def send_emote(sender, receivers, emote, anonymous_add="first"):
# do the template replacement of the sdesc/recog {#num} markers
receiver.msg(sendemote.format(**receiver_sdesc_mapping))
#------------------------------------------------------------
# ------------------------------------------------------------
# Handlers for sdesc and recog
#------------------------------------------------------------
# ------------------------------------------------------------
class SdescHandler(object):
@ -601,11 +628,13 @@ class SdescHandler(object):
"""
# strip emote components from sdesc
sdesc = _RE_REF.sub(r"\1",
_RE_REF_LANG.sub(r"\1",
_RE_SELF_REF.sub(r"",
_RE_LANGUAGE.sub(r"",
_RE_OBJ_REF_START.sub(r"", sdesc)))))
sdesc = _RE_REF.sub(
r"\1",
_RE_REF_LANG.sub(
r"\1",
_RE_SELF_REF.sub(r"", _RE_LANGUAGE.sub(r"", _RE_OBJ_REF_START.sub(r"", sdesc))),
),
)
# make an sdesc clean of ANSI codes
cleaned_sdesc = ansi.strip_ansi(sdesc)
@ -614,7 +643,10 @@ class SdescHandler(object):
raise SdescError("Short desc cannot be empty.")
if len(cleaned_sdesc) > max_length:
raise SdescError("Short desc can max be %i chars long (was %i chars)." % (max_length, len(cleaned_sdesc)))
raise SdescError(
"Short desc can max be %i chars long (was %i chars)."
% (max_length, len(cleaned_sdesc))
)
# store to attributes
sdesc_regex = ordered_permutation_regex(cleaned_sdesc)
@ -681,10 +713,10 @@ class RecogHandler(object):
self.ref2recog = self.obj.attributes.get("_recog_ref2recog", default={})
obj2regex = self.obj.attributes.get("_recog_obj2regex", default={})
obj2recog = self.obj.attributes.get("_recog_obj2recog", default={})
self.obj2regex = dict((obj, re.compile(regex, _RE_FLAGS))
for obj, regex in obj2regex.items() if obj)
self.obj2recog = dict((obj, recog)
for obj, recog in obj2recog.items() if obj)
self.obj2regex = dict(
(obj, re.compile(regex, _RE_FLAGS)) for obj, regex in obj2regex.items() if obj
)
self.obj2recog = dict((obj, recog) for obj, recog in obj2recog.items() if obj)
def add(self, obj, recog, max_length=60):
"""
@ -711,10 +743,12 @@ class RecogHandler(object):
# strip emote components from recog
recog = _RE_REF.sub(
r"\1", _RE_REF_LANG.sub(
r"\1", _RE_SELF_REF.sub(
r"", _RE_LANGUAGE.sub(
r"", _RE_OBJ_REF_START.sub(r"", recog)))))
r"\1",
_RE_REF_LANG.sub(
r"\1",
_RE_SELF_REF.sub(r"", _RE_LANGUAGE.sub(r"", _RE_OBJ_REF_START.sub(r"", recog))),
),
)
# make an recog clean of ANSI codes
cleaned_recog = ansi.strip_ansi(recog)
@ -723,7 +757,10 @@ class RecogHandler(object):
raise SdescError("Recog string cannot be empty.")
if len(cleaned_recog) > max_length:
raise RecogError("Recog string cannot be longer than %i chars (was %i chars)" % (max_length, len(cleaned_recog)))
raise RecogError(
"Recog string cannot be longer than %i chars (was %i chars)"
% (max_length, len(cleaned_recog))
)
# mapping #dbref:obj
key = "#%i" % obj.id
@ -756,8 +793,7 @@ class RecogHandler(object):
# check an eventual recog_masked lock on the object
# to avoid revealing masked characters. If lock
# does not exist, pass automatically.
return self.obj2recog.get(obj, obj.sdesc.get()
if hasattr(obj, "sdesc") else obj.key)
return self.obj2recog.get(obj, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key)
else:
# recog_mask log not passed, disable recog
return obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
@ -784,9 +820,10 @@ class RecogHandler(object):
return self.obj2regex[obj], obj, self.obj2regex[obj]
return None
#------------------------------------------------------------
# ------------------------------------------------------------
# RP Commands
#------------------------------------------------------------
# ------------------------------------------------------------
class RPCommand(Command):
@ -818,6 +855,7 @@ class CmdEmote(RPCommand): # replaces the main emote
a different language.
"""
key = "emote"
aliases = [":"]
locks = "cmd:all()"
@ -832,7 +870,7 @@ class CmdEmote(RPCommand): # replaces the main emote
targets = self.caller.location.contents
if not emote.endswith((".", "?", "!")): # If emote is not punctuated,
emote = "%s." % emote # add a full-stop for good measure.
send_emote(self.caller, targets, emote, anonymous_add='first')
send_emote(self.caller, targets, emote, anonymous_add="first")
class CmdSay(RPCommand): # replaces standard say
@ -861,7 +899,7 @@ class CmdSay(RPCommand): # replaces standard say
# calling the speech hook on the location
speech = caller.location.at_before_say(self.args)
# preparing the speech with sdesc/speech parsing.
speech = "/me says, \"{speech}\"".format(speech=speech)
speech = '/me says, "{speech}"'.format(speech=speech)
targets = self.caller.location.contents
send_emote(self.caller, targets, speech, anonymous_add=None)
@ -876,6 +914,7 @@ class CmdSdesc(RPCommand): # set/look at own sdesc
Assigns a short description to yourself.
"""
key = "sdesc"
locks = "cmd:all()"
@ -921,6 +960,7 @@ class CmdPose(RPCommand): # set current pose and default pose
sdesc in the emote, regardless of who is seeing it.
"""
key = "pose"
def parse(self):
@ -981,8 +1021,10 @@ class CmdPose(RPCommand): # set current pose and default pose
else:
# set the pose. We do one-time ref->sdesc mapping here.
parsed, mapping = parse_sdescs_and_recogs(caller, caller.location.contents, pose)
mapping = dict((ref, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key)
for ref, obj in mapping.items())
mapping = dict(
(ref, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key)
for ref, obj in mapping.items()
)
pose = parsed.format(**mapping)
if len(target_name) + len(pose) > 60:
@ -1010,6 +1052,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
forget said alias.
"""
key = "recog"
aliases = ["recognize", "forget"]
@ -1040,10 +1083,17 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
if nmatches == 0:
caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc))
elif nmatches > 1:
reflist = ["%s%s%s (%s%s)" % (inum + 1, _NUM_SEP,
_RE_PREFIX.sub("", sdesc), caller.recog.get(obj),
" (%s)" % caller.key if caller == obj else "")
for inum, obj in enumerate(matches)]
reflist = [
"%s%s%s (%s%s)"
% (
inum + 1,
_NUM_SEP,
_RE_PREFIX.sub("", sdesc),
caller.recog.get(obj),
" (%s)" % caller.key if caller == obj else "",
)
for inum, obj in enumerate(matches)
]
caller.msg(_EMOTE_MULTIMATCH_ERROR.format(ref=sdesc, reflist="\n ".join(reflist)))
else:
obj = matches[0]
@ -1078,6 +1128,7 @@ class CmdMask(RPCommand):
people's recognitions of you will be disabled.
"""
key = "mask"
aliases = ["unmask"]
@ -1126,9 +1177,10 @@ class RPSystemCmdSet(CmdSet):
self.add(CmdMask())
#------------------------------------------------------------
# ------------------------------------------------------------
# RP typeclasses
#------------------------------------------------------------
# ------------------------------------------------------------
class ContribRPObject(DefaultObject):
"""
@ -1146,18 +1198,21 @@ class ContribRPObject(DefaultObject):
self.db.pose = ""
self.db.pose_default = "is here."
def search(self, searchdata,
global_search=False,
use_nicks=True,
typeclass=None,
location=None,
attribute_name=None,
quiet=False,
exact=False,
candidates=None,
nofound_string=None,
multimatch_string=None,
use_dbref=None):
def search(
self,
searchdata,
global_search=False,
use_nicks=True,
typeclass=None,
location=None,
attribute_name=None,
quiet=False,
exact=False,
candidates=None,
nofound_string=None,
multimatch_string=None,
use_dbref=None,
):
"""
Returns an Object matching a search string/condition, taking
sdescs into account.
@ -1228,17 +1283,23 @@ class ContribRPObject(DefaultObject):
if is_string:
# searchdata is a string; wrap some common self-references
if searchdata.lower() in ("here", ):
if searchdata.lower() in ("here",):
return [self.location] if quiet else self.location
if searchdata.lower() in ("me", "self",):
if searchdata.lower() in ("me", "self"):
return [self] if quiet else self
if use_nicks:
# do nick-replacement on search
searchdata = self.nicks.nickreplace(searchdata, categories=("object", "account"), include_account=True)
searchdata = self.nicks.nickreplace(
searchdata, categories=("object", "account"), include_account=True
)
if(global_search or (is_string and searchdata.startswith("#") and
len(searchdata) > 1 and searchdata[1:].isdigit())):
if global_search or (
is_string
and searchdata.startswith("#")
and len(searchdata) > 1
and searchdata[1:].isdigit()
):
# only allow exact matching if searching the entire database
# or unique #dbrefs
exact = True
@ -1268,17 +1329,19 @@ class ContribRPObject(DefaultObject):
def search_obj(string):
"helper wrapper for searching"
return ObjectDB.objects.object_search(string,
attribute_name=attribute_name,
typeclass=typeclass,
candidates=candidates,
exact=exact,
use_dbref=use_dbref)
return ObjectDB.objects.object_search(
string,
attribute_name=attribute_name,
typeclass=typeclass,
candidates=candidates,
exact=exact,
use_dbref=use_dbref,
)
if candidates:
candidates = parse_sdescs_and_recogs(self, candidates,
_PREFIX + searchdata,
search_mode=True)
candidates = parse_sdescs_and_recogs(
self, candidates, _PREFIX + searchdata, search_mode=True
)
results = []
for candidate in candidates:
# we search by candidate keys here; this allows full error
@ -1286,8 +1349,7 @@ class ContribRPObject(DefaultObject):
# in eventual error reporting later (not their keys). Doing
# it like this e.g. allows for use of the typeclass kwarg
# limiter.
results.extend([obj for obj in search_obj(candidate.key)
if obj not in results])
results.extend([obj for obj in search_obj(candidate.key) if obj not in results])
if not results and is_builder:
# builders get a chance to search only by key+alias
@ -1300,9 +1362,13 @@ class ContribRPObject(DefaultObject):
if quiet:
return results
return _AT_SEARCH_RESULT(results, self, query=searchdata,
nofound_string=nofound_string,
multimatch_string=multimatch_string)
return _AT_SEARCH_RESULT(
results,
self,
query=searchdata,
nofound_string=nofound_string,
multimatch_string=multimatch_string,
)
def get_display_name(self, looker, **kwargs):
"""
@ -1325,7 +1391,7 @@ class ContribRPObject(DefaultObject):
The RPObject version doesn't add color to its display.
"""
idstr = "(#%s)" % self.id if self.access(looker, access_type='control') else ""
idstr = "(#%s)" % self.id if self.access(looker, access_type="control") else ""
if looker == self:
sdesc = self.key
else:
@ -1348,8 +1414,7 @@ class ContribRPObject(DefaultObject):
if not looker:
return ""
# get and identify all objects
visible = (con for con in self.contents if con != looker and
con.access(looker, "view"))
visible = (con for con in self.contents if con != looker and con.access(looker, "view"))
exits, users, things = [], [], []
for con in visible:
key = con.get_display_name(looker, pose=True)
@ -1375,6 +1440,7 @@ class ContribRPRoom(ContribRPObject):
"""
Dummy inheritance for rooms.
"""
pass
@ -1382,6 +1448,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
"""
This is a character class that has poses, sdesc and recog.
"""
# Handlers
@lazy_property
def sdesc(self):
@ -1413,7 +1480,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
characters stand out from other objects.
"""
idstr = "(#%s)" % self.id if self.access(looker, access_type='control') else ""
idstr = "(#%s)" % self.id if self.access(looker, access_type="control") else ""
if looker == self:
sdesc = self.key
else:
@ -1499,4 +1566,3 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
"""
return "%s|w%s|n" % ("|W(%s)" % language if language else "", text)

View file

@ -31,7 +31,7 @@ def to_file(data):
"""
# Bucket logs by day and remove objects before serialization
bucket = data.pop('objects')['time'].strftime('%Y-%m-%d')
bucket = data.pop("objects")["time"].strftime("%Y-%m-%d")
# Write it
log_file(json.dumps(data), filename="audit_%s.log" % bucket)
@ -54,7 +54,7 @@ def to_syslog(data):
"""
# Remove objects before serialization
data.pop('objects')
data.pop("objects")
# Write it out
syslog.syslog(json.dumps(data))

View file

@ -15,31 +15,36 @@ from evennia.utils import utils, logger, mod_import, get_evennia_version
from evennia.server.serversession import ServerSession
# Attributes governing auditing of commands and where to send log objects
AUDIT_CALLBACK = getattr(ev_settings, 'AUDIT_CALLBACK',
'evennia.contrib.security.auditing.outputs.to_file')
AUDIT_IN = getattr(ev_settings, 'AUDIT_IN', False)
AUDIT_OUT = getattr(ev_settings, 'AUDIT_OUT', False)
AUDIT_ALLOW_SPARSE = getattr(ev_settings, 'AUDIT_ALLOW_SPARSE', False)
AUDIT_CALLBACK = getattr(
ev_settings, "AUDIT_CALLBACK", "evennia.contrib.security.auditing.outputs.to_file"
)
AUDIT_IN = getattr(ev_settings, "AUDIT_IN", False)
AUDIT_OUT = getattr(ev_settings, "AUDIT_OUT", False)
AUDIT_ALLOW_SPARSE = getattr(ev_settings, "AUDIT_ALLOW_SPARSE", False)
AUDIT_MASKS = [
{'connect': r"^[@\s]*[connect]{5,8}\s+(\".+?\"|[^\s]+)\s+(?P<secret>.+)"},
{'connect': r"^[@\s]*[connect]{5,8}\s+(?P<secret>[\w]+)"},
{'create': r"^[^@]?[create]{5,6}\s+(\w+|\".+?\")\s+(?P<secret>[\w]+)"},
{'create': r"^[^@]?[create]{5,6}\s+(?P<secret>[\w]+)"},
{'userpassword': r"^[@\s]*[userpassword]{11,14}\s+(\w+|\".+?\")\s+=*\s*(?P<secret>[\w]+)"},
{'userpassword': r"^.*new password set to '(?P<secret>[^']+)'\."},
{'userpassword': r"^.* has changed your password to '(?P<secret>[^']+)'\."},
{'password': r"^[@\s]*[password]{6,9}\s+(?P<secret>.*)"},
] + getattr(ev_settings, 'AUDIT_MASKS', [])
{"connect": r"^[@\s]*[connect]{5,8}\s+(\".+?\"|[^\s]+)\s+(?P<secret>.+)"},
{"connect": r"^[@\s]*[connect]{5,8}\s+(?P<secret>[\w]+)"},
{"create": r"^[^@]?[create]{5,6}\s+(\w+|\".+?\")\s+(?P<secret>[\w]+)"},
{"create": r"^[^@]?[create]{5,6}\s+(?P<secret>[\w]+)"},
{"userpassword": r"^[@\s]*[userpassword]{11,14}\s+(\w+|\".+?\")\s+=*\s*(?P<secret>[\w]+)"},
{"userpassword": r"^.*new password set to '(?P<secret>[^']+)'\."},
{"userpassword": r"^.* has changed your password to '(?P<secret>[^']+)'\."},
{"password": r"^[@\s]*[password]{6,9}\s+(?P<secret>.*)"},
] + getattr(ev_settings, "AUDIT_MASKS", [])
if AUDIT_CALLBACK:
try:
AUDIT_CALLBACK = getattr(
mod_import('.'.join(AUDIT_CALLBACK.split('.')[:-1])), AUDIT_CALLBACK.split('.')[-1])
mod_import(".".join(AUDIT_CALLBACK.split(".")[:-1])), AUDIT_CALLBACK.split(".")[-1]
)
logger.log_sec("Auditing module online.")
logger.log_sec("Audit record User input: {}, output: {}.\n"
"Audit sparse recording: {}, Log callback: {}".format(
AUDIT_IN, AUDIT_OUT, AUDIT_ALLOW_SPARSE, AUDIT_CALLBACK))
logger.log_sec(
"Audit record User input: {}, output: {}.\n"
"Audit sparse recording: {}, Log callback: {}".format(
AUDIT_IN, AUDIT_OUT, AUDIT_ALLOW_SPARSE, AUDIT_CALLBACK
)
)
except Exception as e:
logger.log_err("Failed to activate Auditing module. %s" % e)
@ -59,6 +64,7 @@ class AuditedServerSession(ServerSession):
See README.md for installation/configuration instructions.
"""
def audit(self, **kwargs):
"""
Extracts messages and system data from a Session object upon message
@ -79,7 +85,7 @@ class AuditedServerSession(ServerSession):
time_str = str(time_obj)
session = self
src = kwargs.pop('src', '?')
src = kwargs.pop("src", "?")
bytecount = 0
# Do not log empty lines
@ -91,22 +97,22 @@ class AuditedServerSession(ServerSession):
# Capture Account name and dbref together
account = session.get_account()
account_token = ''
account_token = ""
if account:
account_token = '%s%s' % (account.key, account.dbref)
account_token = "%s%s" % (account.key, account.dbref)
# Capture Character name and dbref together
char = session.get_puppet()
char_token = ''
char_token = ""
if char:
char_token = '%s%s' % (char.key, char.dbref)
char_token = "%s%s" % (char.key, char.dbref)
# Capture Room name and dbref together
room = None
room_token = ''
room_token = ""
if char:
room = char.location
room_token = '%s%s' % (room.key, room.dbref)
room_token = "%s%s" % (room.key, room.dbref)
# Try to compile an input/output string
def drill(obj, bucket):
@ -119,44 +125,44 @@ class AuditedServerSession(ServerSession):
bucket.append(obj)
return bucket
text = kwargs.pop('text', '')
text = kwargs.pop("text", "")
if utils.is_iter(text):
text = '|'.join(drill(text, []))
text = "|".join(drill(text, []))
# Mask any PII in message, where possible
bytecount = len(text.encode('utf-8'))
bytecount = len(text.encode("utf-8"))
text = self.mask(text)
# Compile the IP, Account, Character, Room, and the message.
log = {
'time': time_str,
'hostname': socket.getfqdn(),
'application': '%s' % ev_settings.SERVERNAME,
'version': get_evennia_version(),
'pid': os.getpid(),
'direction': 'SND' if src == 'server' else 'RCV',
'protocol': self.protocol_key,
'ip': client_ip,
'session': 'session#%s' % self.sessid,
'account': account_token,
'character': char_token,
'room': room_token,
'text': text.strip(),
'bytes': bytecount,
'data': kwargs,
'objects': {
'time': time_obj,
'session': self,
'account': account,
'character': char,
'room': room,
}
"time": time_str,
"hostname": socket.getfqdn(),
"application": "%s" % ev_settings.SERVERNAME,
"version": get_evennia_version(),
"pid": os.getpid(),
"direction": "SND" if src == "server" else "RCV",
"protocol": self.protocol_key,
"ip": client_ip,
"session": "session#%s" % self.sessid,
"account": account_token,
"character": char_token,
"room": room_token,
"text": text.strip(),
"bytes": bytecount,
"data": kwargs,
"objects": {
"time": time_obj,
"session": self,
"account": account,
"character": char,
"room": room,
},
}
# Remove any keys with blank values
if AUDIT_ALLOW_SPARSE is False:
log['data'] = {k: v for k, v in log['data'].items() if v}
log['objects'] = {k: v for k, v in log['objects'].items() if v}
log["data"] = {k: v for k, v in log["data"].items() if v}
log["objects"] = {k: v for k, v in log["objects"].items() if v}
log = {k: v for k, v in log.items() if v}
return log
@ -178,7 +184,7 @@ class AuditedServerSession(ServerSession):
is_embedded = False
match = re.match(".*Command.*'(.+)'.*is not available.*", msg, flags=re.IGNORECASE)
if match:
msg = match.group(1).replace('\\', '')
msg = match.group(1).replace("\\", "")
submsg = msg
is_embedded = True
@ -192,11 +198,13 @@ class AuditedServerSession(ServerSession):
continue
if match:
term = match.group('secret')
masked = re.sub(term, '*' * len(term.zfill(8)), msg)
term = match.group("secret")
masked = re.sub(term, "*" * len(term.zfill(8)), msg)
if is_embedded:
msg = re.sub(submsg, '%s <Masked: %s>' % (masked, command), _msg, flags=re.IGNORECASE)
msg = re.sub(
submsg, "%s <Masked: %s>" % (masked, command), _msg, flags=re.IGNORECASE
)
else:
msg = masked
@ -214,7 +222,7 @@ class AuditedServerSession(ServerSession):
"""
if AUDIT_CALLBACK and AUDIT_OUT:
try:
log = self.audit(src='server', **kwargs)
log = self.audit(src="server", **kwargs)
if log:
AUDIT_CALLBACK(log)
except Exception as e:
@ -232,7 +240,7 @@ class AuditedServerSession(ServerSession):
"""
if AUDIT_CALLBACK and AUDIT_IN:
try:
log = self.audit(src='client', **kwargs)
log = self.audit(src="client", **kwargs)
if log:
AUDIT_CALLBACK(log)
except Exception as e:

Some files were not shown because too many files have changed in this diff Show more