Merge pull request #1 from evennia/master

Pull from evennia master
This commit is contained in:
Kenneth Aalberg 2019-11-19 21:23:22 +01:00 committed by GitHub
commit f87fdf08e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
344 changed files with 22025 additions and 12155 deletions

27
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View file

@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] Enter a brief description here"
labels: bug
assignees: ''
---
#### Describe the bug
A clear and concise description of what the bug is.
#### To Reproduce
Steps to reproduce the behavior:
1.
2.
3.
4. See error
#### Expected behavior
A clear and concise description of what you expected to happen.
#### Environment, Evennia version, OS etc
If unsure, run `evennia -v` or get the first few lines of the `about` command in-game.
#### Additional context
Any other context about the problem, or ideas on how to solve.

View file

@ -0,0 +1,14 @@
---
name: Documentation issue
about: Documentation problems and suggestions
title: '[Documentation] Enter a brief description here'
labels: documentation
assignees: ''
---
#### Documentation issue
Describe what the issue is and where it can/should be found.
#### Suggested change
The suggested change.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature Request] Enter a brief description here"
labels: feature-request
assignees: ''
---
#### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
#### Describe the solution you'd like
A clear and concise description of what you want to happen.
#### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
#### Additional context
Add any other context or screenshots about the feature request here.

View file

@ -34,6 +34,7 @@ install:
- pip install -e .
before_script:
- make format
- evennia --init testing_mygame
- cp .travis/${TESTING_DB}_settings.py testing_mygame/server/conf/settings.py
- cd testing_mygame

View file

@ -2,10 +2,38 @@
## Evennia 1.0 (2019-) (WIP)
### Already in master
- new `drop:holds()` lock default to limit dropping nonsensical things. Access check
defaults to True for backwards-compatibility in 0.9, will be False in 1.0
### Already in master
- `py` command now reroutes stdout to output results in-game client. `py`
without arguments starts a full interactive Python console.
without arguments starts a full interactive Python console.
- Webclient default to a single input pane instead of two. Now defaults to no help-popup.
- Webclient fix of prompt display
- Webclient multimedia support for relaying images, video and sounds via
`.msg(image=URL)`, `.msg(video=URL)`
and `.msg(audio=URL)`
- Add Spanish translation (fermuch)
- Expand `GLOBAL_SCRIPTS` container to always start scripts and to include all
global scripts regardless of how they were created.
- Change settings to always use lists instead of tuples, to make mutable
settings easier to add to. (#1912)
- Make new `CHANNEL_MUDINFO` setting for specifying the mudinfo channel
- Make `CHANNEL_CONNECTINFO` take full channel definition
- Make `DEFAULT_CHANNELS` list auto-create channels missing at reload
- Webclient `ANSI->HTML` parser updated. Webclient line width changed from 1.6em to 1.1em
to better make ANSI graphics look the same as for third-party clients
- `AttributeHandler.get(return_list=True)` will return `[]` if there are no
Attributes instead of `[None]`.
- 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.
- Make default `set` command able to edit nested structures (PR by Aaron McMillan)
- Allow running Evennia test suite from core repo with `make test`.
- Return `store_key` from `TickerHandler.add` and add `store_key` as a kwarg to
the `TickerHandler.remove` method. This makes it easier to manage tickers.
## Evennia 0.9 (2018-2019)
@ -44,12 +72,12 @@ without arguments starts a full interactive Python console.
- Change webclient from old txws version to use more supported/feature-rich Autobahn websocket library
#### Evennia game index
#### Evennia game index
- Made Evennia game index client a part of core - now configured from settings file (old configs
need to be moved)
- The `evennia connections` command starts a wizard that helps you connect your game to the game index.
- The game index now accepts games with no public telnet/webclient info (for early prototypes).
- The game index now accepts games with no public telnet/webclient info (for early prototypes).
#### New golden-layout based Webclient UI (@friarzen)
- Features
@ -184,9 +212,9 @@ without arguments starts a full interactive Python console.
### Contribs
- Evscaperoom - a full puzzle engine for making multiplayer escape rooms in Evennia. Used to make
the entry for the MUD-Coder's Guild's 2019 Game Jam with the theme "One Room", where it ranked #1.
- Evennia game-index client no longer a contrib - moved into server core and configured with new
- Evscaperoom - a full puzzle engine for making multiplayer escape rooms in Evennia. Used to make
the entry for the MUD-Coder's Guild's 2019 Game Jam with the theme "One Room", where it ranked #1.
- Evennia game-index client no longer a contrib - moved into server core and configured with new
setting `GAME_INDEX_ENABLED`.
- The `extended_room` contrib saw some backwards-incompatible refactoring:
+ All commands now begin with `CmdExtendedRoom`. So before it was `CmdExtendedLook`, now

View file

@ -13,15 +13,16 @@
# folder).
#
# You will end up in a shell where the `evennia` command is available. From here you
# can install and run the game normally. Use Ctrl-D to exit the evennia docker container.
# can initialize and/or run the game normally. Use Ctrl-D to exit the evennia docker container.
# For more info see: https://github.com/evennia/evennia/wiki/Getting-Started#quick-start
#
# You can also start evennia directly by passing arguments to the folder:
#
# docker run -it --rm -p 4000:4000 -p 4001:4001 -p 4002:4002 -v $PWD:/usr/src/game evennia/evennia evennia start -l
#
# This will start Evennia running as the core process of the container. Note that you *must* use -l
# or one of the foreground modes (like evennia ipstart) since otherwise the container will immediately
# die since no foreground process keeps it up.
# or one of the foreground modes (like evennia ipstart), since otherwise the container will immediately
# die because of having no foreground process.
#
# The evennia/evennia base image is found on DockerHub and can also be used
# as a base for creating your own custom containerized Evennia game. For more
@ -65,6 +66,13 @@ WORKDIR /usr/src/game
# set bash prompt
ENV PS1 "evennia|docker \w $ "
# create and switch to a non-root user for runtime security
# -D - do not set a password
# -H - do not create a home directory
# -s /bin/false - set login shell to /bin/false
RUN adduser -D -H -s /bin/false evennia
USER evennia
# startup a shell when we start the container
ENTRYPOINT ["/usr/src/evennia/bin/unix/evennia-docker-start.sh"]

38
Makefile Normal file
View file

@ -0,0 +1,38 @@
# 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:
pip install -e .
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

@ -4,7 +4,6 @@ index ec5fc29..62b7936 100644
+++ b/evennia/comms/migrations/0015_auto_20170706_2041.py
@@ -2,7 +2,12 @@
# Generated by Django 1.11.2 on 2017-07-06 20:41
from __future__ import unicode_literals
-from django.db import migrations
+from django.db import migrations, connection
@ -60,7 +59,6 @@ index b27c75c..6e40252 100644
+++ b/evennia/objects/migrations/0007_objectdb_db_account.py
@@ -2,21 +2,31 @@
# Generated by Django 1.11.2 on 2017-07-05 17:27
from __future__ import unicode_literals
-from django.db import migrations, models
+from django.db import migrations, models, connection
@ -104,7 +102,6 @@ index 80161a1..10fb225 100644
+++ b/evennia/objects/migrations/0009_remove_objectdb_db_player.py
@@ -2,7 +2,12 @@
# Generated by Django 1.11.2 on 2017-07-06 20:41
from __future__ import unicode_literals
-from django.db import migrations
+from django.db import migrations, connection
@ -144,7 +141,6 @@ index 99baf70..23f6df9 100644
+++ b/evennia/scripts/migrations/0009_scriptdb_db_account.py
@@ -2,21 +2,31 @@
# Generated by Django 1.11.2 on 2017-07-05 17:27
from __future__ import unicode_literals
-from django.db import migrations, models
+from django.db import migrations, models, connection
@ -188,7 +184,6 @@ index d3746a5..20fa63f 100644
+++ b/evennia/scripts/migrations/0011_remove_scriptdb_db_player.py
@@ -2,7 +2,12 @@
# Generated by Django 1.11.2 on 2017-07-06 20:41
from __future__ import unicode_literals
-from django.db import migrations
+from django.db import migrations, connection

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)
@ -390,6 +409,9 @@ def _init():
BASE_GUEST_TYPECLASS = class_from_module(settings.BASE_GUEST_TYPECLASS)
del class_from_module
# delayed starts
GLOBAL_SCRIPTS.start()
def set_trace(term_size=(140, 40), debugger="auto"):
"""
@ -419,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,29 +26,29 @@ 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
from evennia.utils.optionhandler import OptionHandler
from django.utils.translation import ugettext as _
from future.utils import with_metaclass
from random import getrandbits
__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
_CONNECT_CHANNEL = None
_MUDINFO_CHANNEL = None
# Create throttles for too many account-creations and login attempts
CREATION_THROTTLE = Throttle(limit=2, timeout=10 * 60)
@ -113,7 +113,7 @@ class AccountSessionHandler(object):
return len(self.get())
class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
class DefaultAccount(AccountDB, metaclass=TypeclassBase):
"""
This is the base Typeclass for all Accounts. Accounts represent
the person playing the game and tracks account info, password
@ -203,12 +203,14 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
@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
@ -267,9 +269,9 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# 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("You don't have permission to puppet '%s'." % obj.key)
self.msg(f"You don't have permission to puppet '{obj.key}'.")
return
if obj.account:
# object already puppeted
@ -278,19 +280,19 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# we may take over another of our sessions
# output messages to the affected sessions
if _MULTISESSION_MODE in (1, 3):
txt1 = "Sharing |c%s|n with another of your sessions."
txt2 = "|c%s|n|G is now shared from another of your sessions.|n"
self.msg(txt1 % obj.name, session=session)
self.msg(txt2 % obj.name, session=obj.sessions.all())
txt1 = f"Sharing |c{obj.name}|n with another of your sessions."
txt2 = f"|c{obj.name}|n|G is now shared from another of your sessions.|n"
self.msg(txt1, session=session)
self.msg(txt2, session=obj.sessions.all())
else:
txt1 = "Taking over |c%s|n from another of your sessions."
txt2 = "|c%s|n|R is now acted from another of your sessions.|n"
self.msg(txt1 % obj.name, session=session)
self.msg(txt2 % obj.name, session=obj.sessions.all())
txt1 = f"Taking over |c{obj.name}|n from another of your sessions."
txt2 = f"|c{obj.name}|n|R is now acted from another of your sessions.|n"
self.msg(txt1, session=session)
self.msg(txt2, session=obj.sessions.all())
self.unpuppet_object(obj.sessions.get())
elif obj.account.is_connected:
# controlled by another account
self.msg("|c%s|R is already puppeted by another Account." % obj.key)
self.msg(f"|c{obj.key}|R is already puppeted by another Account.")
return
# do the puppeting
@ -390,6 +392,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
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)
@ -408,21 +411,23 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
"""
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.
@ -437,16 +442,18 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
objs = []
for validator in validator_config:
try:
klass = import_string(validator['NAME'])
klass = import_string(validator["NAME"])
except ImportError:
msg = ("The module in NAME could not be imported: %s. "
"Check your AUTH_USERNAME_VALIDATORS setting.")
raise ImproperlyConfigured(msg % validator['NAME'])
objs.append(klass(**validator.get('OPTIONS', {})))
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", {})))
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.
@ -481,7 +488,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# 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
@ -492,27 +499,29 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
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('Authentication Denied (Banned): %s (IP: %s).' % (username, 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('Authentication Failure: %s (IP: %s).' % (username, 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:
@ -521,7 +530,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
return None, errors
# Account successfully authenticated
logger.log_sec('Authentication Success: %s (IP: %s).' % (account, ip))
logger.log_sec(f"Authentication Success: {account} (IP: {ip}).")
return account, errors
@classmethod
@ -629,7 +638,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
"""
super(DefaultAccount, self).set_password(password)
logger.log_sec("Password successfully changed for %s." % self)
logger.log_sec(f"Password successfully changed for {self}.")
self.at_password_change()
@classmethod
@ -660,17 +669,19 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
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
@ -697,19 +708,25 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
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 account.
# everything's ok. Create the new account.
try:
try:
account = create.create_account(username, email, password, permissions=permissions, typeclass=typeclass)
logger.log_sec('Account Created: %s (IP: %s).' % (account, 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
@ -725,20 +742,26 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# join the new account to the public channel
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
if not pchannel or not pchannel.connect(account):
string = "New account '%s' could not connect to public channel!" % account.key
string = f"New account '{account.key}' could not connect to public channel!"
errors.append(string)
logger.log_err(string)
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)
@ -759,7 +782,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# 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
@ -787,6 +810,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
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):
@ -827,7 +851,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
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()
@ -854,17 +878,29 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
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.
@ -901,17 +937,25 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# 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
@ -919,7 +963,9 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
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.
@ -939,8 +985,12 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
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
@ -975,9 +1025,11 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
"""
# 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
@ -992,8 +1044,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
"""
# 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)
@ -1034,7 +1085,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
updates = []
if not cdict.get("key"):
if not self.db_key:
self.db_key = "#%i" % self.dbid
self.db_key = f"#{self.dbid}"
updates.append("db_key")
elif self.key != cdict.get("key"):
updates.append("db_key")
@ -1145,19 +1196,20 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
message (str): A message to send to the connect channel.
"""
global _CONNECT_CHANNEL
if not _CONNECT_CHANNEL:
global _MUDINFO_CHANNEL
if not _MUDINFO_CHANNEL:
try:
_CONNECT_CHANNEL = ChannelDB.objects.filter(db_key=settings.DEFAULT_CHANNELS[1]["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)
if _CONNECT_CHANNEL:
_CONNECT_CHANNEL.tempmsg("[%s, %s]: %s" % (_CONNECT_CHANNEL.key, now, message))
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:
logger.log_info("[%s]: %s" % (now, message))
logger.log_info(f"[{now}]: {message}")
def at_post_login(self, session=None, **kwargs):
"""
@ -1184,7 +1236,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
if session:
session.msg(logged_in={})
self._send_to_connect_channel("|G%s connected|n" % self.key)
self._send_to_connect_channel(f"|G{self.key} connected|n")
if _MULTISESSION_MODE == 0:
# in this mode we should have only one character available. We
# try to auto-connect to our last conneted object, if any
@ -1206,8 +1258,9 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# 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):
"""
@ -1234,8 +1287,8 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
"""
reason = " (%s)" % reason if reason else ""
self._send_to_connect_channel("|R%s disconnected%s|n" % (self.key, reason))
reason = f" ({reason if reason else ''})"
self._send_to_connect_channel(f"|R{self.key} disconnected{reason}|n")
def at_post_disconnect(self, **kwargs):
"""
@ -1352,22 +1405,30 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
is_su = self.is_superuser
# text shown when looking in the ooc area
result = ["Account |g%s|n (you are Out-of-Character)" % self.key]
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
"\n\n|wConnected sessions (%i):|n" % nsess)
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(with_metaclass(TypeclassBase, AccountDB)):
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("\n\nAvailable character%s (%i/unlimited):" % (string_s_ending, len(characters)))
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,14 +1468,16 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# character is already puppeted
sid = sess in sessions and sessions.index(sess) + 1
if sess and sid:
result.append("\n - |G%s|n [%s] (played by you in session %i)"
% (char.key, ", ".join(char.permissions.all()), sid))
result.append(
f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] (played by you in session {sid})"
)
else:
result.append("\n - |R%s|n [%s] (played by someone else)"
% (char.key, ", ".join(char.permissions.all())))
result.append(
f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] (played by someone else)"
)
else:
# character is "free to puppet"
result.append("\n - %s [%s]" % (char.key, ", ".join(char.permissions.all())))
result.append(f"\n - {char.key} [{', '.join(char.permissions.all())}]")
look_string = ("-" * 68) + "\n" + "".join(result) + "\n" + ("-" * 68)
return look_string
@ -1439,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:
@ -1455,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
@ -1498,7 +1572,7 @@ class DefaultGuest(DefaultAccount):
overriding the call (unused by default).
"""
self._send_to_connect_channel("|G%s connected|n" % self.key)
self._send_to_connect_channel(f"|G{self.key} connected|n")
self.puppet_object(session, self.db._last_puppet)
def at_server_shutdown(self):

View file

@ -2,8 +2,6 @@
# This sets up how models are displayed
# in the web admin interface.
#
from builtins import object
from django import forms
from django.conf import settings
from django.contrib import admin
@ -20,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):
@ -57,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
@ -87,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):
@ -149,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
@ -172,6 +184,7 @@ class AccountTagInline(TagInline):
Inline Account Tags.
"""
model = AccountDB.db_tags.through
related_field = "accountdb"
@ -181,6 +194,7 @@ class AccountAttributeInline(AttributeInline):
Inline Account Attributes.
"""
model = AccountDB.db_attributes.through
related_field = "accountdb"
@ -191,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'),
@ -222,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):
"""
@ -248,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,11 +102,13 @@ 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 = "%s" % self.key
script_key = str(self.key)
self.scripts.add(BotStarter, key=script_key)
self.is_bot = True
@ -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.
@ -187,7 +200,7 @@ class IRCBot(Bot):
# connect to Evennia channel
channel = search.channel_search(ev_channel)
if not channel:
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
raise RuntimeError(f"Evennia Channel '{ev_channel}' not found.")
channel = channel[0]
channel.connect(self)
self.db.ev_channel = channel
@ -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)
@ -307,19 +325,19 @@ class IRCBot(Bot):
if kwargs["type"] == "nicklist":
# the return of a nicklist request
if hasattr(self, "_nicklist_callers") and self._nicklist_callers:
chstr = "%s (%s:%s)" % (self.db.irc_channel, self.db.irc_network, self.db.irc_port)
chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower()))
for obj in self._nicklist_callers:
obj.msg("Nicks at %s:\n %s" % (chstr, nicklist))
obj.msg(f"Nicks at {chstr}:\n {nicklist}")
self._nicklist_callers = []
return
elif kwargs["type"] == "ping":
# the return of a ping
if hasattr(self, "_ping_callers") and self._ping_callers:
chstr = "%s (%s:%s)" % (self.db.irc_channel, self.db.irc_network, self.db.irc_port)
chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
for obj in self._ping_callers:
obj.msg("IRC ping return from %s took %ss." % (chstr, kwargs["timing"]))
obj.msg(f"IRC ping return from {chstr} took {kwargs['timing']}s.")
self._ping_callers = []
return
@ -338,13 +356,18 @@ 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)))
text = "Who list (online/idle): %s" % ", ".join(sorted(whos, key=lambda w: w.lower()))
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
text = "This is an Evennia IRC bot connecting from '%s'." % settings.SERVERNAME
text = f"This is an Evennia IRC bot connecting from '{settings.SERVERNAME}'."
else:
text = "I understand 'who' and 'about'."
super().msg(privmsg=((text,), {"user": user}))
@ -352,10 +375,10 @@ class IRCBot(Bot):
# something to send to the main channel
if kwargs["type"] == "action":
# An action (irc pose)
text = "%s@%s %s" % (kwargs["user"], kwargs["channel"], txt)
text = f"{kwargs['user']}@{kwargs['channel']} {txt}"
else:
# msg - A normal channel message
text = "%s@%s: %s" % (kwargs["user"], kwargs["channel"], txt)
text = f"{kwargs['user']}@{kwargs['channel']}: {txt}"
if not self.ndb.ev_channel and self.db.ev_channel:
# cache channel lookup
@ -364,6 +387,7 @@ class IRCBot(Bot):
if self.ndb.ev_channel:
self.ndb.ev_channel.msg(text, senders=self)
#
# RSS
@ -401,7 +425,7 @@ class RSSBot(Bot):
# connect to Evennia channel
channel = search.channel_search(ev_channel)
if not channel:
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
raise RuntimeError(f"Evennia Channel '{ev_channel}' not found.")
channel = channel[0]
self.db.ev_channel = channel
if rss_url:
@ -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):
@ -463,7 +487,7 @@ class GrapevineBot(Bot):
# connect to Evennia channel
channel = search.channel_search(ev_channel)
if not channel:
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
raise RuntimeError(f"Evennia Channel '{ev_channel}' not found.")
channel = channel[0]
channel.connect(self)
self.db.ev_channel = channel
@ -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):
"""
@ -164,14 +165,22 @@ class AccountDBManager(TypedObjectManager, UserManager):
if typeclass:
# we accept both strings and actual typeclasses
if callable(typeclass):
typeclass = "%s.%s" % (typeclass.__module__, typeclass.__name__)
typeclass = f"{typeclass.__module__}.{typeclass.__name__}"
else:
typeclass = "%s" % typeclass
typeclass = str(typeclass)
query["db_typeclass_path"] = typeclass
if exact:
return self.filter(**query)
matches = self.filter(**query)
else:
return self.filter(**query)
matches = self.filter(**query)
if not matches:
# 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},
)
return matches
# back-compatibility alias
account_search = search_account

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

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-28 18:20
from __future__ import unicode_literals
import django.contrib.auth.validators
from django.db import migrations, models
@ -9,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

@ -0,0 +1,27 @@
# Generated by Django 2.2.6 on 2019-10-25 12:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("accounts", "0008_auto_20190128_1820")]
operations = [
migrations.AlterField(
model_name="accountdb",
name="db_typeclass_path",
field=models.CharField(
db_index=True,
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="last_name",
field=models.CharField(blank=True, max_length=150, verbose_name="last name"),
),
]

View file

@ -15,8 +15,6 @@ persistently store attributes of its own. This is ideal for extra
account info and OOC account configuration variables etc.
"""
from builtins import object
from django.conf import settings
from django.db import models
from django.contrib.auth.models import AbstractUser
@ -29,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
@ -41,11 +39,12 @@ _DA = object.__delattr__
_TYPECLASS = None
#------------------------------------------------------------
# ------------------------------------------------------------
#
# AccountDB
#
#------------------------------------------------------------
# ------------------------------------------------------------
class AccountDB(TypedObject, AbstractUser):
"""
@ -85,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()
@ -103,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
@ -125,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)
#
@ -137,12 +145,12 @@ class AccountDB(TypedObject, AbstractUser):
#
def __str__(self):
return smart_str("%s(account %s)" % (self.name, self.dbid))
return smart_str(f"{self.name}(account {self.dbid})")
def __repr__(self):
return "%s(account#%s)" % (self.name, self.dbid)
return f"{self.name}(account#{self.dbid})"
#@property
# @property
def __username_get(self):
return self.username
@ -159,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
@ -169,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(
"TestAccount%s" % 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("TestAccount%s" % 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("TestAccount%s" % 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("TestAccount%s" % 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(
"TestAccount%s" % 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("TestAccount%s" % 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("TestAccount%s" % 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("TestAccount%s" % 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,
'Playable character list is not empty! %s' % 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

@ -59,24 +59,39 @@ def build_matches(raw_string, cmdset, include_prefixes=False):
matches (list) A list of match tuples created by `cmdparser.create_match`.
"""
l_raw_string = raw_string.lower()
matches = []
try:
if include_prefixes:
# 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
)
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)
@ -164,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
@ -184,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

@ -26,11 +26,10 @@ Set theory.
to affect the low-priority cmdset. Ex: A1,A3 + B1,B2,B4,B5 = B2,B4,B5
"""
from future.utils import listvalues, with_metaclass
from weakref import WeakKeyDictionary
from django.utils.translation import ugettext as _
from evennia.utils.utils import inherits_from, is_iter
__all__ = ("CmdSet",)
@ -40,6 +39,7 @@ class _CmdSetMeta(type):
the cmdset class.
"""
def __init__(cls, *args, **kwargs):
"""
Fixes some things in the cmdclass
@ -47,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__)
@ -57,7 +57,7 @@ class _CmdSetMeta(type):
super().__init__(*args, **kwargs)
class CmdSet(with_metaclass(_CmdSetMeta, object)):
class CmdSet(object, metaclass=_CmdSetMeta):
"""
This class describes a unique cmdset that understands priorities.
CmdSets can be merged and made to perform various set operations
@ -158,9 +158,18 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
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):
"""
@ -213,8 +222,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
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):
@ -326,7 +334,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
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):
"""
@ -377,8 +385,9 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
# 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":
@ -393,7 +402,9 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
# 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
@ -401,8 +412,9 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
# 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":
@ -417,7 +429,9 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
# 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
@ -467,8 +481,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
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]
@ -478,7 +491,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
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)
@ -580,12 +593,13 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
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
self.commands = listvalues(unique)
self.commands = list(unique.values())
def get_all_cmd_keys_and_aliases(self, caller=None):
"""
@ -603,8 +617,7 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
"""
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

@ -63,8 +63,6 @@ can then implement separate sets for different situations. For
example, you can have a 'On a boat' set, onto which you then tack on
the 'Fishing' set. Fishing from a boat? No problem!
"""
from builtins import object
from future.utils import raise_
import sys
from traceback import format_exc
from importlib import import_module
@ -75,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 = {}
@ -88,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."
@ -127,6 +130,7 @@ class _EmptyCmdSet(CmdSet):
"""
This cmdset represents an empty cmdset
"""
key = "_EMPTY_CMDSET"
priority = -101
mergetype = "Union"
@ -155,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:
@ -172,22 +177,22 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
if not cmdsetclass:
try:
module = import_module(modpath, package="evennia")
except ImportError:
except ImportError as exc:
if len(trace()) > 2:
# error in module, make sure to not hide it.
exc = sys.exc_info()
raise_(exc[1], None, exc[2])
dum, dum, tb = sys.exc_info()
raise exc.with_traceback(tb)
else:
# try next suggested path
errstring += _("\n(Unsuccessfully tried '%s')." % python_path)
continue
try:
cmdsetclass = getattr(module, classname)
except AttributeError:
except AttributeError as exc:
if len(trace()) > 2:
# Attribute error within module, don't hide it
exc = sys.exc_info()
raise_(exc[1], None, exc[2])
dum, dum, tb = sys.exc_info()
raise exc.with_traceback(tb)
else:
errstring += _("\n(Unsuccessfully tried '%s')." % python_path)
continue
@ -201,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:
@ -239,6 +258,7 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
return err_cmdset
return None # undefined error
# classes
@ -298,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"
@ -312,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):
@ -365,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
@ -431,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:
@ -500,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):
@ -532,6 +568,7 @@ class CmdSetHandler(object):
pass
# re-sync the cmdsethandler.
self.update()
# legacy alias
delete = remove
@ -541,6 +578,7 @@ class CmdSetHandler(object):
"""
self.remove(default_cmdset=True)
# legacy alias
delete_default = remove_default
@ -584,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

@ -4,8 +4,6 @@ The base Command class.
All commands in Evennia inherit from the 'Command' class in this module.
"""
from builtins import range
import re
import math
@ -16,8 +14,6 @@ from evennia.utils.utils import is_iter, fill, lazy_property, make_iter
from evennia.utils.evtable import EvTable
from evennia.utils.ansi import ANSIString
from future.utils import with_metaclass
def _init_command(cls, **kwargs):
"""
@ -41,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)
@ -59,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"
@ -87,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
@ -99,7 +95,7 @@ class CommandMeta(type):
# parsing errors.
class Command(with_metaclass(CommandMeta, object)):
class Command(object, metaclass=CommandMeta):
"""
Base command
@ -219,7 +215,7 @@ class Command(with_metaclass(CommandMeta, object)):
str, too.
"""
return hash('\n'.join(self._matchset))
return hash("\n".join(self._matchset))
def __ne__(self, cmd):
"""
@ -287,7 +283,7 @@ class Command(with_metaclass(CommandMeta, object)):
"""
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()
@ -323,8 +319,7 @@ class Command(with_metaclass(CommandMeta, object)):
"""
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
@ -414,7 +409,9 @@ class Command(with_metaclass(CommandMeta, object)):
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}
@ -434,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)
@ -457,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 ""
@ -490,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):
"""
@ -507,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.
@ -554,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:
@ -565,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:
@ -585,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
@ -601,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):
@ -610,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):
@ -619,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

@ -18,8 +18,6 @@ self.msg() and similar methods to reroute returns to the correct
method. Otherwise all text will be returned to all connected sessions.
"""
from builtins import range
import time
from codecs import lookup as codecs_lookup
from django.conf import settings
@ -32,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")
__all__ = (
"CmdOOCLook",
"CmdIC",
"CmdOOC",
"CmdPassword",
"CmdQuit",
"CmdCharCreate",
"CmdOption",
"CmdSessions",
"CmdWho",
"CmdColorTest",
"CmdQuell",
"CmdCharDelete",
"CmdStyle",
)
class MuxAccountLookCommand(COMMAND_DEFAULT_CLASS):
@ -62,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
@ -121,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"
@ -139,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):
@ -158,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):
@ -184,6 +203,7 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
Permanently deletes one of your characters.
"""
key = "chardelete"
locks = "cmd:pperm(Player)"
help_category = "General"
@ -197,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
@ -213,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
@ -225,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)
@ -275,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,
@ -356,6 +398,7 @@ class CmdSessions(COMMAND_DEFAULT_CLASS):
Lists the sessions currently connected to your account.
"""
key = "sessions"
locks = "cmd:all()"
help_category = "General"
@ -367,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)
@ -413,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
@ -434,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")
@ -451,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):
@ -476,6 +530,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
"""
key = "option"
aliases = "options"
switch_options = ("save", "clear")
@ -513,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")
@ -529,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))
@ -565,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()
@ -623,6 +689,7 @@ class CmdPassword(COMMAND_DEFAULT_CLASS):
Changes your password. Make sure to pick a safe one.
"""
key = "password"
locks = "cmd:pperm(Player)"
@ -652,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):
@ -668,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()"
@ -679,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)
@ -690,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)
@ -710,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"
@ -721,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
@ -737,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):
@ -747,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
@ -777,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)
@ -847,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:
@ -886,7 +986,7 @@ class CmdStyle(COMMAND_DEFAULT_CLASS):
"""
key = "style"
switch_options = ['clear']
switch_options = ["clear"]
def func(self):
if not self.args:
@ -895,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):
@ -908,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

@ -18,7 +18,6 @@ therefore always be limited to superusers only.
"""
import re
from builtins import range
from django.conf import settings
from evennia.utils.batchprocessors import BATCHCMD, BATCHCODE
@ -85,6 +84,7 @@ print "leaving run ..."
# Helper functions
# -------------------------------------------------------------
def format_header(caller, entry):
"""
Formats a header
@ -101,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
@ -111,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()
@ -142,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
@ -186,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)
@ -215,6 +214,7 @@ def purge_processor(caller):
caller.scripts.validate() # this will purge interactive mode
# -------------------------------------------------------------
# main access commands
# -------------------------------------------------------------
@ -235,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",)
@ -264,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:
@ -283,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
@ -292,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():
@ -313,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)):
@ -351,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")
@ -367,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:
@ -380,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:
@ -400,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
@ -426,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)):
@ -450,6 +462,7 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
# (these are the same for both processors)
# -------------------------------------------------------------
class CmdStateAbort(_COMMAND_DEFAULT_CLASS):
"""
abort
@ -458,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)"
@ -475,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)"
@ -489,6 +504,7 @@ class CmdStatePP(_COMMAND_DEFAULT_CLASS):
Process the currently shown command definition.
"""
key = "pp"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -511,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)"
@ -533,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)"
@ -554,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)"
@ -576,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)"
@ -598,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)"
@ -620,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)"
@ -643,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)"
@ -672,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)"
@ -700,6 +724,7 @@ class CmdStateCC(_COMMAND_DEFAULT_CLASS):
Continue to process all remaining
commands.
"""
key = "cc"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -728,6 +753,7 @@ class CmdStateJJ(_COMMAND_DEFAULT_CLASS):
Jump to specific command number
"""
key = "jj"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -752,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)"
@ -776,6 +803,7 @@ class CmdStateQQ(_COMMAND_DEFAULT_CLASS):
Quit the batchprocessor.
"""
key = "qq"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -828,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.
@ -846,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

@ -19,12 +19,26 @@ from evennia.utils import create, logger, utils, evtable
from evennia.utils.utils import make_iter, class_from_module
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
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
@ -33,11 +47,14 @@ def find_channel(caller, channelname, silent=False, noaliases=False):
Helper function for searching for a single channel with
some error handling.
"""
channels = ChannelDB.objects.channel_search(channelname)
channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname)
if not channels:
if not noaliases:
channels = [chan for chan in ChannelDB.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:
@ -97,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
@ -169,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:
@ -198,9 +218,9 @@ class CmdAllCom(COMMAND_DEFAULT_CLASS):
Usage:
allcom [on | off | who | destroy]
Allows the user to universally turn off or on all channels they are on,
as well as perform a 'who' for all channels they are on. Destroy deletes
all channels that you control.
Allows the user to universally turn off or on all channels they are on, as
well as perform a 'who' for all channels they are on. Destroy deletes all
channels that you control.
Without argument, works like comlist.
"""
@ -225,26 +245,35 @@ 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 ChannelDB.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":
# get names all subscribed channels and disconnect from them all
channels = ChannelDB.objects.get_subscriptions(caller)
channels = CHANNEL_DEFAULT_TYPECLASS.objects.get_subscriptions(caller)
for channel in channels:
self.execute_cmd("delcom %s" % channel.key)
elif args == "destroy":
# destroy all channels you control
channels = [chan for chan in ChannelDB.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 ChannelDB.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:
@ -268,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"
@ -282,32 +312,59 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
caller = self.caller
# all channels we have available to listen to
channels = [chan for chan in ChannelDB.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
# all channel we are already subscribed to
subs = ChannelDB.objects.get_subscriptions(caller)
subs = CHANNEL_DEFAULT_TYPECLASS.objects.get_subscriptions(caller)
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)
@ -318,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):
@ -359,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
@ -369,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):
@ -408,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
@ -428,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):
@ -497,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"
@ -558,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(';')]
channel = ChannelDB.objects.channel_search(channame)
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)
@ -660,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):
@ -688,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"
@ -707,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))
@ -733,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
@ -763,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?")
@ -779,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:
@ -814,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."
@ -870,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)
@ -890,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
@ -928,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.")
@ -951,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"
@ -974,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)
@ -990,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()
@ -1039,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:
@ -1131,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)
@ -1144,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|$"
@ -574,7 +618,7 @@ class CmdWhisper(COMMAND_DEFAULT_CLASS):
receivers = [recv.strip() for recv in self.lhs.split(",")]
receivers = [caller.search(receiver) for receiver in receivers]
receivers = [caller.search(receiver) for receiver in set(receivers)]
receivers = [recv for recv in receivers if recv]
speech = self.rhs
@ -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,37 +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("", "No help entry found for '%s'" % query, None, suggested=suggestions), {"type": "help"}))
self.msg(
self.format_help_entry(
"", f"No help entry found for '{query}'", None, suggested=suggestions
),
options={"type": "help"},
)
def _loadhelp(caller):
@ -316,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)"
@ -328,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)
@ -356,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
@ -365,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)
@ -383,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))
@ -404,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
@ -415,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)
@ -223,31 +230,30 @@ def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
sys.stdout = old_stdout
sys.stderr = old_stderr
if not ret:
if ret is None:
return
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."""
@ -264,6 +270,7 @@ class EvenniaPythonConsole(code.InteractiveConsole):
"""Push some code, whether complete or not."""
old_stdout = sys.stdout
old_stderr = sys.stderr
class FakeStd:
def __init__(self, caller):
self.caller = caller
@ -274,9 +281,14 @@ class EvenniaPythonConsole(code.InteractiveConsole):
fake_std = FakeStd(self.caller)
sys.stdout = fake_std
sys.stderr = fake_std
result = super().push(line)
sys.stdout = old_stdout
sys.stderr = old_stderr
result = None
try:
result = super().push(line)
except SystemExit:
pass
finally:
sys.stdout = old_stdout
sys.stderr = old_stderr
return result
@ -298,8 +310,8 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
being parsed as HTML in the webclient but not in telnet clients)
Without argument, open a Python console in-game. This is a full console,
accepting multi-line Python code for testing and debugging. Type `exit` to
return to the game. If Evennia is reloaded, thek console will be closed.
accepting multi-line Python code for testing and debugging. Type `exit()` to
return to the game. If Evennia is reloaded, the console will be closed.
Enter a line of instruction after the 'py' command to execute it
immediately. Separate multiple commands by ';' or open the code editor
@ -324,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")
@ -339,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.
@ -370,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:
@ -386,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
@ -419,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")
@ -463,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:
@ -502,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"
@ -523,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_TYPECKLASS + 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
@ -572,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"
@ -612,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
@ -626,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
@ -641,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)
@ -697,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))
@ -708,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
@ -719,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
@ -743,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()
@ -768,8 +838,13 @@ class CmdAbout(COMMAND_DEFAULT_CLASS):
"""Display information about server or target"""
string = """
|cEvennia|n {version}|n
MU* development system
|cEvennia|n MU* development system
|wEvennia version|n: {version}
|wOS|n: {os}
|wPython|n: {python}
|wTwisted|n: {twisted}
|wDjango|n: {django}
|wLicence|n https://opensource.org/licenses/BSD-3-Clause
|wWeb|n http://www.evennia.com
@ -778,15 +853,13 @@ class CmdAbout(COMMAND_DEFAULT_CLASS):
|wMaintainer|n (2010-) Griatch (griatch AT gmail DOT com)
|wMaintainer|n (2006-10) Greg Taylor
|wOS|n {os}
|wPython|n {python}
|wTwisted|n {twisted}
|wDjango|n {django}
""".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)
@ -800,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)"
@ -814,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))
@ -859,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")
@ -877,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
@ -893,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
@ -913,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
@ -924,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:
@ -940,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
@ -957,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))
@ -981,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

@ -3,13 +3,16 @@ Unit testing for the Command system itself.
"""
from django.test import override_settings
from evennia.utils.test_resources import EvenniaTest, TestCase
from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command
from evennia.commands import cmdparser
# Testing-command sets
class _CmdA(Command):
key = "A"
@ -78,6 +81,7 @@ class _CmdSetD(CmdSet):
self.add(_CmdC("D"))
self.add(_CmdD("D"))
# testing Command Sets
@ -261,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
@ -277,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()
@ -293,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):
@ -313,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
@ -322,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
@ -338,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
@ -345,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()
@ -352,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
@ -374,5 +397,93 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
def _callback(cmdset):
self.assertEqual(len(cmdset.commands), 9)
deferred.addCallback(_callback)
return deferred
class AccessableCommand(Command):
def access(*args, **kwargs):
return True
class _CmdTest1(AccessableCommand):
key = "test1"
class _CmdTest2(AccessableCommand):
key = "another command"
class _CmdTest3(AccessableCommand):
key = "&the third command"
class _CmdTest4(AccessableCommand):
key = "test2"
class _CmdSetTest(CmdSet):
key = "test_cmdset"
def at_cmdset_creation(self):
self.add(_CmdTest1)
self.add(_CmdTest2)
self.add(_CmdTest3)
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"),
)
@override_settings(CMD_IGNORE_PREFIXES="@&/+")
def test_build_matches(self):
a_cmdset = _CmdSetTest()
bcmd = [cmd for cmd in a_cmdset.commands if cmd.key == "test1"][0]
# normal parsing
self.assertEqual(
cmdparser.build_matches("test1 rock", a_cmdset, include_prefixes=False),
[("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")],
)
# 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")],
)
@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"))
@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")],
)

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

@ -23,8 +23,6 @@ update() on the channelhandler. Or use Channel.objects.delete() which
does this for you.
"""
from builtins import object
from django.conf import settings
from evennia.commands import cmdset, command
from evennia.utils.logger import tail_log_file
@ -57,6 +55,7 @@ class ChannelCommand(command.Command):
{lower_channelkey}/history 30
"""
# ^note that channeldesc and lower_channelkey will be filled
# automatically by ChannelHandler
@ -108,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)
@ -121,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)
@ -132,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
@ -218,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):
@ -292,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

@ -11,16 +11,17 @@ from evennia.comms.models import TempMsg, ChannelDB
from evennia.comms.managers import ChannelManager
from evennia.utils import create, logger
from evennia.utils.utils import make_iter
from future.utils import with_metaclass
_CHANNEL_HANDLER = None
class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
This is the base class for all Channel Comms. Inherit from this to
create different types of communication channels.
"""
objects = ChannelManager()
def at_first_save(self):
@ -105,7 +106,12 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
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(with_metaclass(TypeclassBase, ChannelDB)):
"""
# 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(with_metaclass(TypeclassBase, ChannelDB)):
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(with_metaclass(TypeclassBase, ChannelDB)):
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(with_metaclass(TypeclassBase, ChannelDB)):
"""
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(with_metaclass(TypeclassBase, ChannelDB)):
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(with_metaclass(TypeclassBase, ChannelDB)):
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(with_metaclass(TypeclassBase, ChannelDB)):
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(with_metaclass(TypeclassBase, ChannelDB)):
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(with_metaclass(TypeclassBase, ChannelDB)):
"""
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(with_metaclass(TypeclassBase, ChannelDB)):
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(with_metaclass(TypeclassBase, ChannelDB)):
"""
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(with_metaclass(TypeclassBase, ChannelDB)):
"""
# 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(with_metaclass(TypeclassBase, ChannelDB)):
"""
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(with_metaclass(TypeclassBase, ChannelDB)):
"""
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(with_metaclass(TypeclassBase, ChannelDB)):
"""
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(with_metaclass(TypeclassBase, ChannelDB)):
"""
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(with_metaclass(TypeclassBase, ChannelDB)):
"""
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

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-28 18:20
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
@ -8,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

@ -0,0 +1,22 @@
# Generated by Django 2.2.6 on 2019-10-25 12:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("comms", "0017_auto_20190128_1820")]
operations = [
migrations.AlterField(
model_name="channeldb",
name="db_typeclass_path",
field=models.CharField(
db_index=True,
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",
),
)
]

View file

@ -17,8 +17,6 @@ Channels are central objects that act as targets for Msgs. Accounts can
connect to channels by use of a ChannelConnect object (this object is
necessary to easily be able to delete connections on the fly).
"""
from builtins import object
from django.conf import settings
from django.utils import timezone
from django.db import models
@ -39,11 +37,12 @@ _DA = object.__delattr__
_CHANNELHANDLER = None
#------------------------------------------------------------
# ------------------------------------------------------------
#
# Msg
#
#------------------------------------------------------------
# ------------------------------------------------------------
class Msg(SharedMemoryModel):
"""
@ -70,6 +69,7 @@ class Msg(SharedMemoryModel):
- db_lock_storage: Internal storage of lock strings.
"""
#
# Msg database model setup
#
@ -80,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()
@ -152,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):
@ -181,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()
@ -190,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):
@ -217,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.
@ -249,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()
@ -257,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):
@ -283,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.
@ -297,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):
@ -309,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):
@ -327,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)
#
@ -343,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.
@ -359,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):
@ -377,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.
@ -411,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):
@ -442,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.
@ -455,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):
"""
@ -484,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):
"""
@ -570,6 +650,7 @@ class SubscriptionHandler(object):
if self._cache is None:
self._recache()
return self._cache
get = all # alias
def online(self):
@ -583,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
@ -619,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

@ -68,8 +68,6 @@ things you want from here into your game folder and change them there.
## Contrib packages
* EGI_Client (gtaylor 2016) - Client for reporting game status
to the Evennia game index (games.evennia.com).
* In-game Python (Vincent Le Goff 2017) - Allow trusted builders to script
objects and events using Python from in-game.
* Turnbattle (FlutterSprite 2017) - A turn-based combat engine meant

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

@ -94,8 +94,6 @@ in-game.
"""
from builtins import object
from evennia import Command, DefaultScript, CmdSet
TRADE_TIMEOUT = 60 # timeout for B to accept trade
@ -388,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
@ -411,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:
@ -422,6 +421,7 @@ class CmdTradeBase(Command):
# trade help
class CmdTradeHelp(CmdTradeBase):
"""
help command for the trade system.
@ -431,6 +431,7 @@ class CmdTradeHelp(CmdTradeBase):
Displays help for the trade commands.
"""
key = "trade help"
locks = "cmd:all()"
help_category = "Trade"
@ -467,6 +468,7 @@ class CmdTradeHelp(CmdTradeBase):
# offer
class CmdOffer(CmdTradeBase):
"""
offer one or more items in trade.
@ -477,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"
@ -493,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)
@ -504,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
@ -514,6 +520,7 @@ class CmdOffer(CmdTradeBase):
# accept
class CmdAccept(CmdTradeBase):
"""
accept the standing offer
@ -527,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()"
@ -540,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
@ -563,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"
@ -580,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.")
@ -595,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
@ -605,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()"
@ -634,6 +663,7 @@ class CmdEvaluate(CmdTradeBase):
# status
class CmdStatus(CmdTradeBase):
"""
show a list of the current deal
@ -648,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()"
@ -671,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
@ -695,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()"
@ -705,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):
@ -731,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
@ -747,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()"
@ -766,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:
@ -778,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)
@ -826,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

@ -51,18 +51,25 @@ inherit from ClothedCharacter in your game's characters.py file:
class Character(ClothedCharacter):
And do the same with the ClothedCharacterCmdSet in your game's
default_cmdsets.py:
And then add ClothedCharacterCmdSet in your character set in your
game's commands/default_cmdsets.py:
from evennia.contrib.clothing import ClothedCharacterCmdSet
class CharacterCmdSet(default_cmds.CharacterCmdSet):
...
at_cmdset_creation(self):
super().at_cmdset_creation()
...
self.add(ClothedCharacterCmdSet) # <-- add this
From here, you can use the default builder commands to create clothes
with which to test the system:
@create a pretty shirt : evennia.contrib.clothing.Clothing
@set shirt/clothing_type = 'top'
wear shirt
"""
@ -84,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.
@ -100,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.
@ -215,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.
@ -239,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
@ -275,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)
@ -338,6 +357,7 @@ class ClothedCharacter(DefaultCharacter):
# COMMANDS START HERE
class CmdWear(MuxCommand):
"""
Puts on an item of clothing you are holding.
@ -367,8 +387,9 @@ class CmdWear(MuxCommand):
clothing = self.caller.search(self.arglist[0], candidates=self.caller.contents)
wearstyle = True
if not clothing:
self.caller.msg("Thing to wear must be in your inventory.")
return
if not clothing.is_typeclass("evennia.contrib.clothing.Clothing"):
if not clothing.is_typeclass("evennia.contrib.clothing.Clothing", exact=False):
self.caller.msg("That's not clothes!")
return
@ -382,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:
@ -391,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)
@ -420,6 +451,7 @@ class CmdRemove(MuxCommand):
"""
clothing = self.caller.search(self.args, candidates=self.caller.contents)
if not clothing:
self.caller.msg("Thing to remove must be carried or worn.")
return
if not clothing.db.worn:
self.caller.msg("You're not wearing that!")
@ -460,10 +492,10 @@ class CmdCover(MuxCommand):
cover_with = self.caller.search(self.arglist[1], candidates=self.caller.contents)
if not to_cover or not cover_with:
return
if not to_cover.is_typeclass("evennia.contrib.clothing.Clothing"):
if not to_cover.is_typeclass("evennia.contrib.clothing.Clothing", exact=False):
self.caller.msg("%s isn't clothes!" % to_cover.name)
return
if not cover_with.is_typeclass("evennia.contrib.clothing.Clothing"):
if not cover_with.is_typeclass("evennia.contrib.clothing.Clothing", exact=False):
self.caller.msg("%s isn't clothes!" % cover_with.name)
return
if cover_with.db.clothing_type:
@ -480,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
@ -555,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
@ -572,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)
@ -589,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|$"
@ -600,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
@ -614,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:
@ -638,6 +683,7 @@ class CmdInventory(MuxCommand):
Shows your inventory.
"""
# Alternate version of the inventory command which separates
# worn and carried items.
@ -678,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

@ -29,8 +29,6 @@ at_cmdset_creation():
After a reload the dice (or roll) command will be available in-game.
"""
from builtins import range
import re
from random import randint
from evennia import default_cmds, CmdSet
@ -97,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))
@ -105,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
@ -168,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
@ -186,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])
@ -200,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:
@ -224,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

@ -15,7 +15,7 @@ system but they don't necessarily need to follow each other in the exact
sequence.
Each state module must make a class `State` available in the global scope. This
should be a child of `evennia.contribs.evscaperoom.state.BaseState`. The
should be a child of `evennia.contrib.evscaperoom.state.BaseState`. The
methods on this class will be called to initialize the state and clean up etc.
There are no other restrictions on the module.

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

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