Merge branch 'develop'

This commit is contained in:
Griatch 2019-06-29 18:46:27 +02:00
commit dc6ac210f4
318 changed files with 20372 additions and 4579 deletions

View file

@ -1,15 +1,49 @@
dist: xenial
language: python
cache: pip
services:
- postgresql
- mysql
python:
- "2.7"
sudo: false
install:
- pip install -e .
- "3.7"
env:
- TESTING_DB=sqlite3
- TESTING_DB=postgresql
- TESTING_DB=mysql
before_install:
# - psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE evennia TO evennia;"
- psql --version
- psql -U postgres -c "CREATE DATABASE evennia;"
- psql -U postgres -c "CREATE USER evennia WITH PASSWORD 'password';"
- psql -U postgres -c "ALTER USER evennia CREATEDB;"
- mysql --version
- mysql -u root -e "CREATE DATABASE evennia CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
- mysql -u root -e "CREATE USER 'evennia'@'localhost' IDENTIFIED BY 'password';"
- mysql -u root -e "GRANT ALL ON *.* TO 'evennia'@'localhost' IDENTIFIED BY 'password';"
install:
- pip install psycopg2-binary
- pip install mysqlclient
- pip install coveralls
- pip install codacy-coverage
- pip install -e .
before_script:
- evennia --init dummy
- cd dummy
- evennia --init testing_mygame
- cp .travis/${TESTING_DB}_settings.py testing_mygame/server/conf/settings.py
- cd testing_mygame
- evennia migrate
- evennia collectstatic --noinput
script:
- coverage run --source=../evennia --omit=*/migrations/*,*/urls.py,*/test*.py,*.sh,*.txt,*.md,*.pyc,*.service ../bin/unix/evennia test evennia
- coverage run --source=../evennia --omit=*/migrations/*,*/urls.py,*/test*.py,*.sh,*.txt,*.md,*.pyc,*.service ../bin/unix/evennia test --settings=settings --keepdb evennia
after_success:
- coveralls
- coverage xml
- python-codacy-coverage -r coverage.xml

1
.travis/my.conf Normal file
View file

@ -0,0 +1 @@
init_connect='SET collation_connection = utf8_general_ci; SET NAMES utf8;'

71
.travis/mysql_settings.py Normal file
View file

@ -0,0 +1,71 @@
"""
Evennia settings file.
The available options are found in the default settings file found
here:
/home/griatch/Devel/Home/evennia/evennia/evennia/settings_default.py
Remember:
Don't copy more from the default file than you actually intend to
change; this will make sure that you don't overload upstream updates
unnecessarily.
When changing a setting requiring a file system path (like
path/to/actual/file.py), use GAME_DIR and EVENNIA_DIR to reference
your game folder and the Evennia library folders respectively. Python
paths (path.to.module) should be given relative to the game's root
folder (typeclasses.foo) whereas paths within the Evennia library
needs to be given explicitly (evennia.foo).
If you want to share your game dir, including its settings, you can
put secret game- or server-specific settings in secret_settings.py.
"""
import os
# Use the defaults from Evennia unless explicitly overridden
from evennia.settings_default import *
######################################################################
# Evennia base server config
######################################################################
# This is the name of your game. Make it catchy!
SERVERNAME = "testing_mygame"
# Testing database types
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'evennia',
'USER': 'evennia',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '', # use default port
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': 'set collation_connection=utf8mb4_unicode_ci'
},
'TEST': {
'NAME': 'default',
'OPTIONS': {
'charset': 'utf8mb4',
# 'init_command': 'set collation_connection=utf8mb4_unicode_ci'
'init_command': "SET NAMES 'utf8mb4'"
}
}
}
}
######################################################################
# Settings given in secret_settings.py override those in this file.
######################################################################
try:
from server.conf.secret_settings import *
except ImportError:
print("secret_settings.py file not found or failed to import.")

View file

@ -0,0 +1,62 @@
"""
Evennia settings file.
The available options are found in the default settings file found
here:
/home/griatch/Devel/Home/evennia/evennia/evennia/settings_default.py
Remember:
Don't copy more from the default file than you actually intend to
change; this will make sure that you don't overload upstream updates
unnecessarily.
When changing a setting requiring a file system path (like
path/to/actual/file.py), use GAME_DIR and EVENNIA_DIR to reference
your game folder and the Evennia library folders respectively. Python
paths (path.to.module) should be given relative to the game's root
folder (typeclasses.foo) whereas paths within the Evennia library
needs to be given explicitly (evennia.foo).
If you want to share your game dir, including its settings, you can
put secret game- or server-specific settings in secret_settings.py.
"""
import os
# Use the defaults from Evennia unless explicitly overridden
from evennia.settings_default import *
######################################################################
# Evennia base server config
######################################################################
# This is the name of your game. Make it catchy!
SERVERNAME = "testing_mygame"
# Testing database types
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'evennia',
'USER': 'evennia',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '', # use default
'TEST': {
'NAME': 'default'
}
}
}
######################################################################
# Settings given in secret_settings.py override those in this file.
######################################################################
try:
from server.conf.secret_settings import *
except ImportError:
print("secret_settings.py file not found or failed to import.")

View file

@ -0,0 +1,47 @@
"""
Evennia settings file.
The available options are found in the default settings file found
here:
/home/griatch/Devel/Home/evennia/evennia/evennia/settings_default.py
Remember:
Don't copy more from the default file than you actually intend to
change; this will make sure that you don't overload upstream updates
unnecessarily.
When changing a setting requiring a file system path (like
path/to/actual/file.py), use GAME_DIR and EVENNIA_DIR to reference
your game folder and the Evennia library folders respectively. Python
paths (path.to.module) should be given relative to the game's root
folder (typeclasses.foo) whereas paths within the Evennia library
needs to be given explicitly (evennia.foo).
If you want to share your game dir, including its settings, you can
put secret game- or server-specific settings in secret_settings.py.
"""
import os
# Use the defaults from Evennia unless explicitly overridden
from evennia.settings_default import *
######################################################################
# Evennia base server config
######################################################################
# This is the name of your game. Make it catchy!
SERVERNAME = "testing_mygame"
# Using default sqlite3 settings
######################################################################
# Settings given in secret_settings.py override those in this file.
######################################################################
try:
from server.conf.secret_settings import *
except ImportError:
print("secret_settings.py file not found or failed to import.")

View file

@ -1,5 +1,203 @@
# Changelog
## Evennia 0.9 (2018-2019)
### Distribution
- New requirement: Python 3.7 (py2.7 support removed)
- Django 2.1
- Twisted 19.2.1
- Autobahn websockets (removed old tmwx)
- Docker image updated
### Commands
- Remove `@`-prefix from all default commands (prefixes still work, optional)
- Removed default `@delaccount` command, incorporating as `@account/delete` instead. Added confirmation
question.
- Add new `@force` command to have another object perform a command.
- Add the Portal uptime to the `@time` command.
- Make the `@link` command first make a local search before a global search.
- Have the default Unloggedin-look command look for optional `connection_screen()` callable in
`mygame/server/conf/connection_screen.py`. This allows for more flexible welcome screens
that are calculated on the fly.
- `@py` command now defaults to escaping html tags in its output when viewing in the webclient.
Use new `/clientraw` switch to get old behavior (issue #1369).
- Shorter and more informative, dynamic, listing of on-command vars if not
setting func() in child command class.
- New Command helper methods
- `.client_width()` returns client width of the session running the command.
- `.styled_table(*args, **kwargs)` returns a formatted evtable styled by user's options
- `.style_header(*args, **kwargs)` creates styled header entry
- `.style_separator(*args, **kwargs)` " separator
- `.style_footer(*args, **kwargs)` " footer
### Web
- Change webclient from old txws version to use more supported/feature-rich Autobahn websocket library
#### 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).
#### New golden-layout based Webclient UI (@friarzen)
- Features
- Much slicker behavior and more professional look
- Allows tabbing as well as click and drag of panes in any grid position
- Renaming tabs, assignments of data tags and output types are simple per-pane menus now
- Any number of input panes, with separate histories
- Button UI (disabled in JS by default)
#### Web/Django standard initiative (@strikaco)
- Features
- Adds a series of web-based forms and generic class-based views
- Accounts
- Register - Enhances registration; allows optional collection of email address
- Form - Adds a generic Django form for creating Accounts from the web
- Characters
- Create - Authenticated users can create new characters from the website (requires associated form)
- Detail - Authenticated and authorized users can view select details about characters
- List - Authenticated and authorized users can browse a list of all characters
- Manage - Authenticated users can edit or delete owned characters from the web
- Form - Adds a generic Django form for creating characters from the web
- Channels
- Detail - Authorized users can view channel logs from the web
- List - Authorized users can browse a list of all channels
- Help Entries
- Detail - Authorized users can view help entries from the web
- List - Authorized users can browse a list of all help entries from the web
- Navbar changes
- Characters - Link to character list
- Channels - Link to channel list
- Help - Link to help entry list
- Puppeting
- Users can puppet their own characters within the context of the website
- Dropdown
- Link to create characters
- Link to manage characters
- Link to quick-select puppets
- Link to password change workflow
- Functions
- Updates Bootstrap to v4 stable
- Enables use of Django Messages framework to communicate with users in browser
- Implements webclient/website `_shared_login` functionality as Django middleware
- 'account' and 'puppet' are added to all request contexts for authenticated users
- Adds unit tests for all web views
- Cosmetic
- Prettifies Django 'forgot password' workflow (requires SMTP to actually function)
- Prettifies Django 'change password' workflow
- Bugfixes
- Fixes bug on login page where error messages were not being displayed
- Remove strvalue field from admin; it made no sense to have here, being an optimization field
for internal use.
### Prototypes
- `evennia.prototypes.save_prototype` now takes the prototype as a normal
argument (`prototype`) instead of having to give it as `**prototype`.
- `evennia.prototypes.search_prototype` has a new kwarg `require_single=False` that
raises a KeyError exception if query gave 0 or >1 results.
- `evennia.prototypes.spawner` can now spawn by passing a `prototype_key`
### Typeclasses
- Add new methods on all typeclasses, useful specifically for object handling from the website/admin:
+ `web_get_admin_url()`: Returns the path to the object detail page in the Admin backend.
+ `web_get_create_url()`: Returns the path to the typeclass' creation page on the website, if implemented.
+ `web_get_absolute_url()`: Returns the path to the object's detail page on the website, if implemented.
+ `web_get_update_url()`: Returns the path to the object's update page on the website, if implemented.
+ `web_get_delete_url()`: Returns the path to the object's delete page on the website, if implemented.
- All typeclasses have new helper class method `create`, which encompasses useful functionality
that used to be embedded for example in the respective `@create` or `@connect` commands.
- DefaultAccount now has new class methods implementing many things that used to be in unloggedin
commands (these can now be customized on the class instead):
+ `is_banned()`: Checks if a given username or IP is banned.
+ `get_username_validators`: Return list of validators for username validation (see
`settings.AUTH_USERNAME_VALIDATORS`)
+ `authenticate`: Method to check given username/password.
+ `normalize_username`: Normalizes names so (for Unicode environments) users cannot mimic existing usernames by replacing select characters with visually-similar Unicode chars.
+ `validate_username`: Mechanism for validating a username based on predefined Django validators.
+ `validate_password`: Mechanism for validating a password based on predefined Django validators.
+ `set_password`: Apply password to account, using validation checks.
- `AttributeHandler.remove` and `TagHandler.remove` can now be used to delete by-category. If neither
key nor category is given, they now work the same as .clear().
### Protocols
- Support for `Grapevine` MUD-chat network ("channels" supported)
### Server
- Convert ServerConf model to store its values as a Picklefield (same as
Attributes) instead of using a custom solution.
- OOB: Add support for MSDP LIST, REPORT, UNREPORT commands (re-mapped to `msdp_list`,
`msdp_report`, `msdp_unreport`, inlinefuncs)
- Added `evennia.ANSIString` to flat API.
- Server/Portal log files now cycle to names on the form `server_.log_19_03_08_` instead of `server.log___19.3.8`, retaining
unix file sorting order.
- Django signals fire for important events: Puppet/Unpuppet, Object create/rename, Login,
Logout, Login fail Disconnect, Account create/rename
### Settings
- `GLOBAL_SCRIPTS` - dict defining typeclasses of global scripts to store on the new
`evennia.GLOBAL_SCRIPTS` container. These will auto-start when Evennia start and will always
exist.
- `OPTIONS_ACCOUNTS_DEFAULT` - option dict with option defaults and Option classes
- `OPTION_CLASS_MODULES` - classes representing an on-Account Option, on special form
- `VALIDATOR_FUNC_MODULES` - (general) text validator functions, for verifying an input
is on a specific form.
### Utils
- `evennia` launcher now fully handles all django-admin commands, like running tests in parallel.
- `evennia.utils.create.account` now also takes `tags` and `attrs` keywords.
- `evennia.utils.interactive` decorator can now allow you to use yield(secs) to pause operation
in any function, not just in Command.func. Likewise, response = yield(question) will work
if the decorated function has an argument or kwarg `caller`.
- Added many more unit tests.
- Swap argument order of `evennia.set_trace` to `set_trace(term_size=(140, 40), debugger='auto')`
since the size is more likely to be changed on the command line.
- `utils.to_str(text, session=None)` now acts as the old `utils.to_unicode` (which was removed).
This converts to the str() type (not to a byte-string as in Evennia 0.8), trying different
encodings. This function will also force-convert any object passed to it into a string (so
`force_string` flag was removed and assumed always set).
- `utils.to_bytes(text, session=None)` replaces the old `utils.to_str()` functionality and converts
str to bytes.
- `evennia.MONITOR_HANDLER.all` now takes keyword argument `obj` to only retrieve monitors from that specific
Object (rather than all monitors in the entire handler).
- Support adding `\f` in command doc strings to force where EvMore puts page breaks.
- Validation Functions now added with standard API to homogenize user input validation.
- Option Classes added to make storing user-options easier and smoother.
- `evennia.VALIDATOR_CONTAINER` and `evennia.OPTION_CONTAINER` added to load these.
### 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
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
it's `CmdExtendedRoomLook` etc.
+ The `detail` command was broken out of the `desc` command and is now a new, stand-alone command
`CmdExtendedRoomDetail`. This was done to make things easier to extend and to mimic how the detail
command works in the tutorial-world.
+ The `detail` command now also supports deleting details (like the tutorial-world version).
+ The new `ExtendedRoomCmdSet` includes all the extended-room commands and is now the recommended way
to install the extended-room contrib.
- Reworked `menu_login` contrib to use latest EvMenu standards. Now also supports guest logins.
- Mail contrib was refactored to have optional Command classes `CmdMail` for OOC+IC mail (added
to the CharacterCmdSet and `CmdMailCharacter` for IC-only mailing between chars (added to CharacterCmdSet)
### Translations
- Simplified chinese, courtesy of user MaxAlex.
## Evennia 0.8 (2018)
### Requirements
@ -19,7 +217,7 @@
to terminal and can be stopped with Ctrl-C. Using `evennia reload`, or reloading in-game, will
return Server to normal daemon operation.
- For validating passwords, use safe Django password-validation backend instead of custom Evennia one.
- Alias `evennia restart` to mean the same as `evennia reload`.
- Alias `evennia restart` to mean the same as `evennia reload`.
### Prototype changes
@ -111,8 +309,22 @@
- `tb_items` - Extends `tb_equip` with item use with conditions/status effects.
- `tb_magic` - Extends `tb_equip` with spellcasting.
- `tb_range` - Adds system for abstract positioning and movement.
- The `extended_room` contrib saw some backwards-incompatible refactoring:
- All commands now begin with `CmdExtendedRoom`. So before it was `CmdExtendedLook`, now
it's `CmdExtendedRoomLook` etc.
- The `detail` command was broken out of the `desc` command and is now a new, stand-alone command
`CmdExtendedRoomDetail`. This was done to make things easier to extend and to mimic how the detail
command works in the tutorial-world.
- The `detail` command now also supports deleting details (like the tutorial-world version).
- The new `ExtendedRoomCmdSet` includes all the extended-room commands and is now the recommended way
to install the extended-room contrib.
- Updates and some cleanup of existing contribs.
### Internationalization
- Polish translation by user ogotai
# Overviews
## Sept 2017:

View file

@ -7,7 +7,7 @@
# Usage:
# cd to a folder where you want your game data to be (or where it already is).
#
# docker run -it --rm -p 4000:4000 -p 4001:4001 -p 4005:4005 -v $PWD:/usr/src/game evennia/evennia
# docker run -it --rm -p 4000:4000 -p 4001:4001 -p 4002:4002 -v $PWD:/usr/src/game evennia/evennia
#
# (If your OS does not support $PWD, replace it with the full path to your current
# folder).
@ -16,8 +16,8 @@
# can install and run the game normally. Use Ctrl-D to exit the evennia docker container.
#
# You can also start evennia directly by passing arguments to the folder:
#
# docker run -it --rm -p 4000:4000 -p 4001:4001 -p 4005:4005 -v $PWD:/usr/src/game evennia/evennia evennia start -l
#
# 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
@ -27,13 +27,13 @@
# as a base for creating your own custom containerized Evennia game. For more
# info, see https://github.com/evennia/evennia/wiki/Running%20Evennia%20in%20Docker .
#
FROM alpine
FROM python:3.7-alpine
LABEL maintainer="www.evennia.com"
# install compilation environment
RUN apk update && apk add bash gcc jpeg-dev musl-dev procps py-pip \
py-setuptools py2-openssl python python-dev zlib-dev gettext
RUN apk update && apk add bash gcc jpeg-dev musl-dev procps \
libffi-dev openssl-dev zlib-dev gettext
# add the files required for pip installation
COPY ./setup.py /usr/src/evennia/
@ -69,4 +69,4 @@ ENV PS1 "evennia|docker \w $ "
ENTRYPOINT ["/usr/src/evennia/bin/unix/evennia-docker-start.sh"]
# expose the telnet, webserver and websocket client ports
EXPOSE 4000 4001 4005
EXPOSE 4000 4001 4002

View file

@ -83,7 +83,7 @@ index b27c75c..6e40252 100644
- migrations.AddField(
- model_name='objectdb',
- name='db_account',
- field=models.ForeignKey(help_text=b'an Account connected to this object, if any.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.AccountDB', verbose_name=b'account'),
- field=models.ForeignKey(help_text='an Account connected to this object, if any.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.AccountDB', verbose_name='account'),
- ),
- ]
+ db_cursor = connection.cursor()
@ -95,7 +95,7 @@ index b27c75c..6e40252 100644
+ migrations.AddField(
+ model_name='objectdb',
+ name='db_account',
+ field=models.ForeignKey(help_text=b'an Account connected to this object, if any.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.AccountDB', verbose_name=b'account'),
+ field=models.ForeignKey(help_text='an Account connected to this object, if any.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.AccountDB', verbose_name='account'),
+ ),
+ ]
diff --git a/evennia/objects/migrations/0009_remove_objectdb_db_player.py b/evennia/objects/migrations/0009_remove_objectdb_db_player.py
@ -167,7 +167,7 @@ index 99baf70..23f6df9 100644
- migrations.AddField(
- model_name='scriptdb',
- name='db_account',
- field=models.ForeignKey(blank=True, help_text=b'the account to store this script on (should not be set if db_obj is set)', null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.AccountDB', verbose_name=b'scripted account'),
- field=models.ForeignKey(blank=True, help_text='the account to store this script on (should not be set if db_obj is set)', null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.AccountDB', verbose_name='scripted account'),
- ),
- ]
+ db_cursor = connection.cursor()
@ -179,7 +179,7 @@ index 99baf70..23f6df9 100644
+ migrations.AddField(
+ model_name='scriptdb',
+ name='db_account',
+ field=models.ForeignKey(blank=True, help_text=b'the account to store this script on (should not be set if db_obj is set)', null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.AccountDB', verbose_name=b'scripted account'),
+ field=models.ForeignKey(blank=True, help_text='the account to store this script on (should not be set if db_obj is set)', null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.AccountDB', verbose_name='scripted account'),
+ ),
+ ]
diff --git a/evennia/scripts/migrations/0011_remove_scriptdb_db_player.py b/evennia/scripts/migrations/0011_remove_scriptdb_db_player.py

View file

@ -6,7 +6,7 @@ Created for the Player->Account renaming
Griatch 2017, released under the BSD license.
"""
from __future__ import print_function
import re
import sys
@ -130,7 +130,7 @@ def rename_in_tree(path, in_list, out_list, excl_list, fileend_list, is_interact
replacements in each file.
"""
repl_mapping = zip(in_list, out_list)
repl_mapping = list(zip(in_list, out_list))
for root, dirs, files in os.walk(path):
@ -155,13 +155,13 @@ def rename_in_tree(path, in_list, out_list, excl_list, fileend_list, is_interact
for src, dst in repl_mapping:
new_file = _case_sensitive_replace(new_file, src, dst)
if new_file != file:
inp = raw_input(_green("Rename %s\n -> %s\n Y/[N]? > " % (file, new_file)))
inp = input(_green("Rename %s\n -> %s\n Y/[N]? > " % (file, new_file)))
if inp.upper() == 'Y':
new_full_path = os.path.join(root, new_file)
try:
os.rename(full_path, new_full_path)
except OSError as err:
raw_input(_red("Could not rename - %s (return to skip)" % err))
input(_red("Could not rename - %s (return to skip)" % err))
else:
print("... Renamed.")
else:
@ -171,12 +171,12 @@ def rename_in_tree(path, in_list, out_list, excl_list, fileend_list, is_interact
for src, dst in repl_mapping:
new_root = _case_sensitive_replace(new_root, src, dst)
if new_root != root:
inp = raw_input(_green("Dir Rename %s\n -> %s\n Y/[N]? > " % (root, new_root)))
inp = input(_green("Dir Rename %s\n -> %s\n Y/[N]? > " % (root, new_root)))
if inp.upper() == 'Y':
try:
os.rename(root, new_root)
except OSError as err:
raw_input(_red("Could not rename - %s (return to skip)" % err))
input(_red("Could not rename - %s (return to skip)" % err))
else:
print("... Renamed.")
else:
@ -204,7 +204,7 @@ def rename_in_file(path, in_list, out_list, is_interactive):
with open(path, 'r') as fil:
org_text = fil.read()
repl_mapping = zip(in_list, out_list)
repl_mapping = list(zip(in_list, out_list))
if not is_interactive:
# just replace everything immediately
@ -239,12 +239,12 @@ def rename_in_file(path, in_list, out_list, is_interactive):
while True:
for iline, renamed_line in sorted(renamed.items(), key=lambda tup: tup[0]):
for iline, renamed_line in sorted(list(renamed.items()), key=lambda tup: tup[0]):
print("%3i orig: %s" % (iline + 1, org_lines[iline]))
print(" new : %s" % (_yellow(renamed_line)))
print(_green("%s (%i lines changed)" % (path, len(renamed))))
ret = raw_input(_green("Choose: "
ret = input(_green("Choose: "
"[q]uit, "
"[h]elp, "
"[s]kip file, "
@ -275,12 +275,12 @@ def rename_in_file(path, in_list, out_list, is_interactive):
print("Quit renaming program.")
sys.exit()
elif ret == "h":
raw_input(_HELP_TEXT.format(sources=in_list, targets=out_list))
input(_HELP_TEXT.format(sources=in_list, targets=out_list))
elif ret.startswith("i"):
# ignore one or more lines
ignores = [int(ind) - 1 for ind in ret[1:].split(',') if ind.strip().isdigit()]
if not ignores:
raw_input("Ignore example: i 2,7,34,133\n (return to continue)")
input("Ignore example: i 2,7,34,133\n (return to continue)")
continue
for ign in ignores:
renamed.pop(ign, None)

View file

@ -1 +1 @@
0.8.0
0.9.0-dev

View file

@ -16,9 +16,6 @@ to launch such a shell (using python or ipython depending on your install).
See www.evennia.com for full documentation.
"""
from __future__ import print_function
from __future__ import absolute_import
from builtins import object
# docstring header
@ -104,6 +101,7 @@ EvTable = None
EvForm = None
EvEditor = None
EvMore = None
ANSIString = None
# Handlers
SESSION_HANDLER = None
@ -112,6 +110,20 @@ TICKER_HANDLER = None
MONITOR_HANDLER = None
CHANNEL_HANDLER = None
# Containers
GLOBAL_SCRIPTS = None
OPTION_CLASSES = None
# typeclasses
BASE_ACCOUNT_TYPECLASS = None
BASE_OBJECT_TYPECLASS = None
BASE_CHARACTER_TYPECLASS = None
BASE_ROOM_TYPECLASS = None
BASE_EXIT_TYPECLASS = None
BASE_CHANNEL_TYPECLASS = None
BASE_SCRIPT_TYPECLASS = None
BASE_GUEST_TYPECLASS = None
def _create_version():
"""
@ -128,7 +140,10 @@ def _create_version():
except IOError as err:
print(err)
try:
version = "%s (rev %s)" % (version, check_output("git rev-parse --short HEAD", shell=True, cwd=root, stderr=STDOUT).strip())
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
pass
@ -138,6 +153,7 @@ def _create_version():
__version__ = _create_version()
del _create_version
def _init():
"""
This function is called automatically by the launcher only after
@ -148,11 +164,20 @@ def _init():
global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript
global ObjectDB, AccountDB, ScriptDB, ChannelDB, Msg
global Command, CmdSet, default_cmds, syscmdkeys, InterruptCommand
global search_object, search_script, search_account, search_channel, search_help, search_tag, search_message
global create_object, create_script, create_account, create_channel, create_message, create_help_entry
global search_object, search_script, search_account, search_channel
global search_help, search_tag, search_message
global create_object, create_script, create_account, create_channel
global create_message, create_help_entry
global settings, lockfuncs, logger, utils, gametime, ansi, spawn, managers
global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER, TASK_HANDLER
global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER
global CHANNEL_HANDLER, TASK_HANDLER
global GLOBAL_SCRIPTS, OPTION_CLASSES
global EvMenu, EvTable, EvForm, EvMore, EvEditor
global ANSIString
global BASE_ACCOUNT_TYPECLASS, BASE_OBJECT_TYPECLASS, BASE_CHARACTER_TYPECLASS
global BASE_ROOM_TYPECLASS, BASE_EXIT_TYPECLASS, BASE_CHANNEL_TYPECLASS
global BASE_SCRIPT_TYPECLASS, BASE_GUEST_TYPECLASS
from .accounts.accounts import DefaultAccount
from .accounts.accounts import DefaultGuest
@ -203,6 +228,7 @@ def _init():
from .utils.evtable import EvTable
from .utils.evform import EvForm
from .utils.eveditor import EvEditor
from .utils.ansi import ANSIString
# handlers
from .scripts.tickerhandler import TICKER_HANDLER
@ -211,6 +237,10 @@ def _init():
from .comms.channelhandler import CHANNEL_HANDLER
from .scripts.monitorhandler import MONITOR_HANDLER
# containers
from .utils.containers import GLOBAL_SCRIPTS
from .utils.containers import OPTION_CLASSES
# initialize the doc string
global __doc__
__doc__ = ansi.parse_ansi(DOCSTRING)
@ -344,23 +374,30 @@ def _init():
del SystemCmds
del _EvContainer
del object
del absolute_import
del print_function
# 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)
BASE_ROOM_TYPECLASS = class_from_module(settings.BASE_ROOM_TYPECLASS)
BASE_EXIT_TYPECLASS = class_from_module(settings.BASE_EXIT_TYPECLASS)
BASE_CHANNEL_TYPECLASS = class_from_module(settings.BASE_CHANNEL_TYPECLASS)
BASE_SCRIPT_TYPECLASS = class_from_module(settings.BASE_SCRIPT_TYPECLASS)
BASE_GUEST_TYPECLASS = class_from_module(settings.BASE_GUEST_TYPECLASS)
del class_from_module
def set_trace(debugger="auto", term_size=(140, 40)):
def set_trace(term_size=(140, 40), debugger="auto"):
"""
Helper function for running a debugger inside the Evennia event loop.
Args:
term_size (tuple, optional): Only used for Pudb and defines the size of the terminal
(width, height) in number of characters.
debugger (str, optional): One of 'auto', 'pdb' or 'pudb'. Pdb is the standard debugger. Pudb
is an external package with a different, more 'graphical', ncurses-based UI. With
'auto', will use pudb if possible, otherwise fall back to pdb. Pudb is available through
`pip install pudb`.
term_size (tuple, optional): Only used for Pudb and defines the size of the terminal
(width, height) in number of characters.
Notes:
To use:
@ -379,14 +416,12 @@ def set_trace(debugger="auto", term_size=(140, 40)):
"""
import sys
dbg = None
pudb_mode = False
if debugger in ('auto', 'pudb'):
try:
from pudb import debugger
dbg = debugger.Debugger(stdout=sys.__stdout__,
term_size=term_size)
pudb_mode = True
except ImportError:
if debugger == 'pudb':
raise
@ -395,13 +430,11 @@ def set_trace(debugger="auto", term_size=(140, 40)):
if not dbg:
import pdb
dbg = pdb.Pdb(stdout=sys.__stdout__)
pudb_mode = False
if pudb_mode:
# Stopped at breakpoint. Press 'n' to continue into the code.
dbg.set_trace()
else:
try:
# Start debugger, forcing it up one stack frame (otherwise `set_trace` will start debugger
# this point, not the actual code location)
dbg.set_trace(sys._getframe().f_back)
except Exception:
# Stopped at breakpoint. Press 'n' to continue into the code.
dbg.set_trace()

View file

@ -10,28 +10,35 @@ character object, so you should customize that
instead for most things).
"""
import re
import time
from django.conf import settings
from django.contrib.auth import password_validation
from django.core.exceptions import ValidationError
from django.contrib.auth import authenticate, password_validation
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.utils import timezone
from django.utils.module_loading import import_string
from evennia.typeclasses.models import TypeclassBase
from evennia.accounts.manager import AccountManager
from evennia.accounts.models import AccountDB
from evennia.objects.models import ObjectDB
from evennia.comms.models import ChannelDB
from evennia.commands import cmdhandler
from evennia.utils import logger
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, to_unicode, is_iter,
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",)
@ -43,6 +50,10 @@ _MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS
_CMDSET_ACCOUNT = settings.CMDSET_ACCOUNT
_CONNECT_CHANNEL = None
# Create throttles for too many account-creations and login attempts
CREATION_THROTTLE = Throttle(limit=2, timeout=10 * 60)
LOGIN_THROTTLE = Throttle(limit=5, timeout=5 * 60)
class AccountSessionHandler(object):
"""
@ -190,6 +201,28 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
def sessions(self):
return AccountSessionHandler(self)
@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'})
# Do not make this a lazy property; the web UI will not refresh it!
@property
def characters(self):
# Get playable characters list
objs = self.db._playable_characters
# Rebuild the list if legacy code left null values after deletion
if None in objs:
objs = [x for x in self.db._playable_characters if x]
self.db._playable_characters = objs
return objs
# session-related methods
def disconnect_session_from_account(self, session, reason=None):
@ -282,6 +315,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
obj.locks.cache_lock_bypass(obj)
# final hook
obj.at_post_puppet()
SIGNAL_OBJECT_POST_PUPPET.send(sender=obj, account=self, session=session)
def unpuppet_object(self, session):
"""
@ -304,6 +338,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
if not obj.sessions.count():
del obj.account
obj.at_post_unpuppet(self, session=session)
SIGNAL_OBJECT_POST_UNPUPPET.send(sender=obj, session=session, account=self)
# Just to be sure we're always clear.
session.puppet = None
session.puid = None
@ -359,6 +394,195 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
puppet = property(__get_single_puppet)
# utility methods
@classmethod
def is_banned(cls, **kwargs):
"""
Checks if a given username or IP is banned.
Kwargs:
ip (str, optional): IP address.
username (str, optional): Username.
Returns:
is_banned (bool): Whether either is banned or not.
"""
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])):
return True
return False
@classmethod
def get_username_validators(
cls, validator_config=getattr(
settings, 'AUTH_USERNAME_VALIDATORS', [])):
"""
Retrieves and instantiates validators for usernames.
Args:
validator_config (list): List of dicts comprising the battery of
validators to apply to a username.
Returns:
validators (list): List of instantiated Validator objects.
"""
objs = []
for validator in validator_config:
try:
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', {})))
return objs
@classmethod
def authenticate(cls, username, password, ip='', **kwargs):
"""
Checks the given username/password against the database to see if the
credentials are valid.
Note that this simply checks credentials and returns a valid reference
to the user-- it does not log them in!
To finish the job:
After calling this from a Command, associate the account with a Session:
- session.sessionhandler.login(session, account)
...or after calling this from a View, associate it with an HttpRequest:
- django.contrib.auth.login(account, request)
Args:
username (str): Username of account
password (str): Password of account
ip (str, optional): IP address of client
Kwargs:
session (Session, optional): Session requesting authentication
Returns:
account (DefaultAccount, None): Account whose credentials were
provided if not banned.
errors (list): Error messages of any failures.
"""
errors = []
if ip:
ip = str(ip)
# 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.')
# With throttle active, do not log continued hits-- it is a
# waste of storage and can be abused to make your logs harder to
# read and/or fill up your disk.
return None, errors
# Check IP and/or name bans
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.')
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.')
# Log auth failures while throttle is inactive
logger.log_sec('Authentication Failure: %s (IP: %s).' % (username, ip))
# Update throttle
if ip:
LOGIN_THROTTLE.update(ip, 'Too many authentication failures.')
# Try to call post-failure hook
session = kwargs.get('session', None)
if session:
account = AccountDB.objects.get_account_from_name(username)
if account:
account.at_failed_login(session)
return None, errors
# Account successfully authenticated
logger.log_sec('Authentication Success: %s (IP: %s).' % (account, ip))
return account, errors
@classmethod
def normalize_username(cls, username):
"""
Django: Applies NFKC Unicode normalization to usernames so that visually
identical characters with different Unicode code points are considered
identical.
(This deals with the Turkish "i" problem and similar
annoyances. Only relevant if you go out of your way to allow Unicode
usernames though-- Evennia accepts ASCII by default.)
In this case we're simply piggybacking on this feature to apply
additional normalization per Evennia's standards.
"""
username = super(DefaultAccount, cls).normalize_username(username)
# strip excessive spaces in accountname
username = re.sub(r"\s+", " ", username).strip()
return username
@classmethod
def validate_username(cls, username):
"""
Checks the given username against the username validator associated with
Account objects, and also checks the database to make sure it is unique.
Args:
username (str): Username to validate
Returns:
valid (bool): Whether or not the password passed validation
errors (list): Error messages of any failures
"""
valid = []
errors = []
# Make sure we're at least using the default validator
validators = cls.get_username_validators()
if not validators:
validators = [cls.username_validator]
# Try username against all enabled validators
for validator in validators:
try:
valid.append(not validator(username))
except ValidationError as e:
valid.append(False)
errors.extend(e.messages)
# Disqualify if any check failed
if False in valid:
valid = False
else:
valid = True
return valid, errors
@classmethod
def validate_password(cls, password, account=None):
"""
@ -392,33 +616,153 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
return valid, error
def set_password(self, password, force=False):
def set_password(self, password, **kwargs):
"""
Applies the given password to the account if it passes validation checks.
Can be overridden by using the 'force' flag.
Applies the given password to the account. Logs and triggers the `at_password_change` hook.
Args:
password (str): Password to set
password (str): Password to set.
Kwargs:
force (bool): Sets password without running validation checks.
Raises:
ValidationError
Returns:
None (None): Does not return a value.
Notes:
This is called by Django also when logging in; it should not be mixed up with validation, since that
would mean old passwords in the database (pre validation checks) could get invalidated.
"""
if not force:
# Run validation checks
valid, error = self.validate_password(password, account=self)
if error: raise error
super(DefaultAccount, self).set_password(password)
logger.log_info("Password succesfully changed for %s." % self)
logger.log_sec("Password successfully changed for %s." % self)
self.at_password_change()
@classmethod
def create(cls, *args, **kwargs):
"""
Creates an Account (or Account/Character pair for MULTISESSION_MODE<2)
with default (or overridden) permissions and having joined them to the
appropriate default channels.
Kwargs:
username (str): Username of Account owner
password (str): Password of Account owner
email (str, optional): Email address of Account owner
ip (str, optional): IP address of requesting connection
guest (bool, optional): Whether or not this is to be a Guest account
permissions (str, optional): Default permissions for the Account
typeclass (str, optional): Typeclass to use for new Account
character_typeclass (str, optional): Typeclass to use for new char
when applicable.
Returns:
account (Account): Account if successfully created; None if not
errors (list): List of error messages in string form
"""
account = None
errors = []
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)
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.")
return None, errors
# Normalize username
username = cls.normalize_username(username)
# Validate username
if not guest:
valid, errs = cls.validate_username(username)
if not valid:
# this echoes the restrictions made by django's auth
# module (except not allowing spaces, for convenience of
# logging in).
errors.extend(errs)
return None, errors
# Validate password
# Have to create a dummy Account object to check username similarity
valid, errs = cls.validate_password(password, account=cls(username=username))
if not valid:
errors.extend(errs)
return None, errors
# Check IP and/or name bans
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"
errors.append(string)
return None, errors
# everything's ok. Create the new account account.
try:
try:
account = create.create_account(username, email, password, permissions=permissions, typeclass=typeclass)
logger.log_sec('Account Created: %s (IP: %s).' % (account, ip))
except Exception as e:
errors.append("There was an error creating the Account. If this problem persists, contact an admin.")
logger.log_trace()
return None, errors
# This needs to be set so the engine knows this account is
# logging in for the first time. (so it knows to call the right
# hooks during login later)
account.db.FIRST_LOGIN = True
# Record IP address of creation, if available
if ip:
account.db.creator_ip = ip
# 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
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 = 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
)
errors.extend(errs)
if character:
# Update playable character list
if character not in account.characters:
account.db._playable_characters.append(character)
# We need to set this to have @ic auto-connect to this character
account.db._last_puppet = character
except Exception:
# We are in the middle between logged in and -not, so we have
# to handle tracebacks ourselves at this point. If we don't,
# we won't see any errors at all.
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
logger.log_trace()
# 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.')
SIGNAL_ACCOUNT_POST_CREATE.send(sender=account, ip=ip)
return account, errors
def delete(self, *args, **kwargs):
"""
Deletes the account permanently.
@ -442,7 +786,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
self.attributes.clear()
self.nicks.clear()
self.aliases.clear()
super(DefaultAccount, self).delete(*args, **kwargs)
super().delete(*args, **kwargs)
# methods inherited from database model
def msg(self, text=None, from_obj=None, session=None, options=None, **kwargs):
@ -483,13 +827,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
kwargs["options"] = options
if text is not None:
if not (isinstance(text, basestring) or isinstance(text, tuple)):
# sanitize text before sending across the wire
try:
text = to_str(text, force_string=True)
except Exception:
text = repr(text)
kwargs['text'] = text
kwargs['text'] = to_str(text)
# session relay
sessions = make_iter(session) if session else self.sessions.all()
@ -516,7 +854,6 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
commands at run-time.
"""
raw_string = to_unicode(raw_string)
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
@ -562,7 +899,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
"""
# handle me, self and *me, *self
if isinstance(searchdata, basestring):
if isinstance(searchdata, str):
# handle wrapping of common terms
if searchdata.lower() in ("me", "*me", "self", "*self",):
return self
@ -602,8 +939,8 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
result (bool): Result of access check.
"""
result = super(DefaultAccount, self).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
@ -1009,19 +1346,28 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# list of targets - make list to disconnect from db
characters = list(tar for tar in target if tar) if target else []
sessions = self.sessions.all()
if not sessions:
# no sessions, nothing to report
return ""
is_su = self.is_superuser
# text shown when looking in the ooc area
result = ["Account |g%s|n (you are Out-of-Character)" % self.key]
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
"\n\n|wConnected sessions (%i):|n" % nsess)
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.sessid == csessid and "|w* %s|n" % (isess + 1) or
" %s" % (isess + 1), addr))
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")
@ -1068,6 +1414,79 @@ class DefaultGuest(DefaultAccount):
their characters are deleted after disconnection.
"""
@classmethod
def create(cls, **kwargs):
"""
Forwards request to cls.authenticate(); returns a DefaultGuest object
if one is available for use.
"""
return cls.authenticate(**kwargs)
@classmethod
def authenticate(cls, **kwargs):
"""
Gets or creates a Guest account object.
Kwargs:
ip (str, optional): IP address of requestor; used for ban checking,
throttling and logging
Returns:
account (Object): Guest account object, if available
errors (list): List of error messages accrued during this request.
"""
errors = []
account = None
username = None
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.')
return None, errors
try:
# Find an available guest name.
for name in settings.GUEST_LIST:
if not AccountDB.objects.filter(username__iexact=name).count():
username = name
break
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.')
return None, errors
else:
# build a new account with the found guest username
password = "%016x" % getrandbits(64)
home = settings.GUEST_HOME
permissions = settings.PERMISSION_GUEST_DEFAULT
typeclass = settings.BASE_GUEST_TYPECLASS
# Call parent class creator
account, errs = super(DefaultGuest, cls).create(
guest=True,
username=username,
password=password,
permissions=permissions,
typeclass=typeclass,
home=home,
ip=ip,
)
errors.extend(errs)
return account, errors
except Exception as e:
# We are in the middle between logged in and -not, so we have
# to handle tracebacks ourselves at this point. If we don't,
# we won't see any errors at all.
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
logger.log_trace()
return None, errors
return account, errors
def at_post_login(self, session=None, **kwargs):
"""
In theory, guests only have one character regardless of which
@ -1087,11 +1506,10 @@ class DefaultGuest(DefaultAccount):
We repeat the functionality of `at_disconnect()` here just to
be on the safe side.
"""
super(DefaultGuest, self).at_server_shutdown()
super().at_server_shutdown()
characters = self.db._playable_characters
for character in characters:
if character:
print "deleting Character:", character
character.delete()
def at_post_disconnect(self, **kwargs):
@ -1103,7 +1521,7 @@ class DefaultGuest(DefaultAccount):
overriding the call (unused by default).
"""
super(DefaultGuest, self).at_post_disconnect()
super().at_post_disconnect()
characters = self.db._playable_characters
for character in characters:
if character:

View file

@ -247,9 +247,7 @@ class AccountDBAdmin(BaseUserAdmin):
def response_add(self, request, obj, post_url_continue=None):
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
if '_continue' in request.POST:
return HttpResponseRedirect(reverse("admin:accounts_accountdb_change", args=[obj.id]))
from django.urls import reverse
return HttpResponseRedirect(reverse("admin:accounts_accountdb_change", args=[obj.id]))

View file

@ -3,7 +3,7 @@ Bots are a special child typeclasses of
Account that are controlled by the server.
"""
from __future__ import print_function
import time
from django.conf import settings
from evennia.accounts.accounts import DefaultAccount
@ -15,6 +15,8 @@ _IDLE_TIMEOUT = settings.IDLE_TIMEOUT
_IRC_ENABLED = settings.IRC_ENABLED
_RSS_ENABLED = settings.RSS_ENABLED
_GRAPEVINE_ENABLED = settings.GRAPEVINE_ENABLED
_SESSIONS = None
@ -118,14 +120,14 @@ class Bot(DefaultAccount):
Evennia -> outgoing protocol
"""
super(Bot, self).msg(text=text, from_obj=from_obj, session=session, options=options, **kwargs)
super().msg(text=text, from_obj=from_obj, session=session, options=options, **kwargs)
def execute_cmd(self, raw_string, session=None):
"""
Incoming protocol -> Evennia
"""
super(Bot, self).msg(raw_string, session=session)
super().msg(raw_string, session=session)
def at_server_shutdown(self):
"""
@ -146,8 +148,11 @@ 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.
@ -203,7 +208,7 @@ class IRCBot(Bot):
"network": self.db.irc_network,
"port": self.db.irc_port,
"ssl": self.db.irc_ssl}
_SESSIONS.start_bot_session("evennia.server.portal.irc.IRCBotFactory", configdict)
_SESSIONS.start_bot_session(self.factory_path, configdict)
def at_msg_send(self, **kwargs):
"Shortcut here or we can end up in infinite loop"
@ -226,7 +231,7 @@ class IRCBot(Bot):
if not hasattr(self, "_nicklist_callers"):
self._nicklist_callers = []
self._nicklist_callers.append(caller)
super(IRCBot, self).msg(request_nicklist="")
super().msg(request_nicklist="")
return
def ping(self, caller):
@ -240,7 +245,7 @@ class IRCBot(Bot):
if not hasattr(self, "_ping_callers"):
self._ping_callers = []
self._ping_callers.append(caller)
super(IRCBot, self).msg(ping="")
super().msg(ping="")
def reconnect(self):
"""
@ -248,7 +253,7 @@ class IRCBot(Bot):
having to destroy/recreate the bot "account".
"""
super(IRCBot, self).msg(reconnect="")
super().msg(reconnect="")
def msg(self, text=None, **kwargs):
"""
@ -265,12 +270,15 @@ class IRCBot(Bot):
"""
from_obj = kwargs.get("from_obj", None)
options = kwargs.get("options", None) or {}
if not self.ndb.ev_channel and self.db.ev_channel:
# 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(IRCBot, self).msg(channel=text)
super().msg(channel=text)
def execute_cmd(self, session=None, txt=None, **kwargs):
"""
@ -284,13 +292,16 @@ class IRCBot(Bot):
Kwargs:
user (str): The name of the user who sent the message.
channel (str): The name of channel the message was sent to.
type (str): Nature of message. Either 'msg', 'action', 'nicklist' or 'ping'.
nicklist (list, optional): Set if `type='nicklist'`. This is a list of nicks returned by calling
the `self.get_nicklist`. It must look for a list `self._nicklist_callers`
which will contain all callers waiting for the nicklist.
timings (float, optional): Set if `type='ping'`. This is the return (in seconds) of a
ping request triggered with `self.ping`. The return must look for a list
`self._ping_callers` which will contain all callers waiting for the ping return.
type (str): Nature of message. Either 'msg', 'action', 'nicklist'
or 'ping'.
nicklist (list, optional): Set if `type='nicklist'`. This is a list
of nicks returned by calling the `self.get_nicklist`. It must look
for a list `self._nicklist_callers` which will contain all callers
waiting for the nicklist.
timings (float, optional): Set if `type='ping'`. This is the return
(in seconds) of a ping request triggered with `self.ping`. The
return must look for a list `self._ping_callers` which will contain
all callers waiting for the ping return.
"""
if kwargs["type"] == "nicklist":
@ -336,7 +347,7 @@ class IRCBot(Bot):
text = "This is an Evennia IRC bot connecting from '%s'." % settings.SERVERNAME
else:
text = "I understand 'who' and 'about'."
super(IRCBot, self).msg(privmsg=((text,), {"user": user}))
super().msg(privmsg=((text,), {"user": user}))
else:
# something to send to the main channel
if kwargs["type"] == "action":
@ -349,6 +360,7 @@ class IRCBot(Bot):
if not self.ndb.ev_channel and self.db.ev_channel:
# cache channel lookup
self.ndb.ev_channel = self.db.ev_channel
if self.ndb.ev_channel:
self.ndb.ev_channel.msg(text, senders=self)
@ -421,3 +433,100 @@ class RSSBot(Bot):
self.ndb.ev_channel = self.db.ev_channel
if self.ndb.ev_channel:
self.ndb.ev_channel.msg(txt, senders=self.id)
# 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):
"""
Start by telling the portal to connect to the grapevine network.
"""
if not _GRAPEVINE_ENABLED:
self.delete()
return
global _SESSIONS
if not _SESSIONS:
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
# connect to Evennia channel
if ev_channel:
# connect to Evennia channel
channel = search.channel_search(ev_channel)
if not channel:
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
channel = channel[0]
channel.connect(self)
self.db.ev_channel = channel
if grapevine_channel:
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}
_SESSIONS.start_bot_session(self.factory_path, configdict)
def at_msg_send(self, **kwargs):
"Shortcut here or we can end up in infinite loop"
pass
def msg(self, text=None, **kwargs):
"""
Takes text from connected channel (only).
Args:
text (str, optional): Incoming text from channel.
Kwargs:
options (dict): Options dict with the following allowed keys:
- from_channel (str): dbid of a channel this text originated from.
- from_obj (list): list of objects sending this text.
"""
from_obj = kwargs.get("from_obj", None)
options = kwargs.get("options", None) or {}
if not self.ndb.ev_channel and self.db.ev_channel:
# 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 not from_obj or from_obj != [self]:
# send outputfunc channel(msg, chan, sender)
# TODO we should refactor channel formatting to operate on the
# account/object level instead. For now, remove the channel/name
# 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), {}))
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.
"""
if event == "channels/broadcast":
# A private message to the bot - a command.
text = f"{sender}@{game}: {txt}"
if not self.ndb.ev_channel and self.db.ev_channel:
# simple cache of channel lookup
self.ndb.ev_channel = self.db.ev_channel
if self.ndb.ev_channel:
self.ndb.ev_channel.msg(text, senders=self)

View file

@ -164,9 +164,9 @@ class AccountDBManager(TypedObjectManager, UserManager):
if typeclass:
# we accept both strings and actual typeclasses
if callable(typeclass):
typeclass = u"%s.%s" % (typeclass.__module__, typeclass.__name__)
typeclass = "%s.%s" % (typeclass.__module__, typeclass.__name__)
else:
typeclass = u"%s" % typeclass
typeclass = "%s" % typeclass
query["db_typeclass_path"] = typeclass
if exact:
return self.filter(**query)

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
@ -28,15 +28,15 @@ class Migration(migrations.Migration):
('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=b'key', db_index=True)),
('db_typeclass_path', models.CharField(help_text=b"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=b'typeclass')),
('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name=b'creation date')),
('db_lock_storage', models.TextField(help_text=b"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=b'locks', blank=True)),
('db_is_connected', models.BooleanField(default=False, help_text=b'If account is connected to game or not', verbose_name=b'is_connected')),
('db_cmdset_storage', models.CharField(help_text=b'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=b'cmdset')),
('db_is_bot', models.BooleanField(default=False, help_text=b'Used to identify irc/rss bots', verbose_name=b'is_bot')),
('db_attributes', models.ManyToManyField(help_text=b'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=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag', null=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)),
('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')),
],

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import evennia.accounts.manager
@ -22,7 +22,7 @@ class Migration(migrations.Migration):
migrations.AlterModelManagers(
name='accountdb',
managers=[
(b'objects', evennia.accounts.manager.AccountDBManager()),
('objects', evennia.accounts.manager.AccountDBManager()),
],
),
migrations.AlterField(

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.9 on 2016-09-05 09:02
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-06-06 17:31
from __future__ import unicode_literals
import django.contrib.auth.validators
from django.db import migrations, models
@ -16,12 +16,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='accountdb',
name='db_attributes',
field=models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'),
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=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
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',

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-03 19:17
from __future__ import unicode_literals
from django.apps import apps as global_apps
from django.db import migrations

View file

@ -0,0 +1,73 @@
# -*- 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
import evennia.accounts.manager
class Migration(migrations.Migration):
dependencies = [
('accounts', '0007_copy_player_to_account'),
]
operations = [
migrations.AlterModelManagers(
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'),
),
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'),
),
migrations.AlterField(
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'),
),
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'),
),
migrations.AlterField(
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'),
),
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'),
),
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'),
),
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'),
),
]

View file

@ -25,6 +25,7 @@ from django.utils.encoding import smart_str
from evennia.accounts.manager import AccountDBManager
from evennia.typeclasses.models import TypedObject
from evennia.utils.utils import make_iter
from evennia.server.signals import SIGNAL_ACCOUNT_POST_RENAME
__all__ = ("AccountDB",)
@ -138,16 +139,18 @@ class AccountDB(TypedObject, AbstractUser):
def __str__(self):
return smart_str("%s(account %s)" % (self.name, self.dbid))
def __unicode__(self):
return u"%s(account#%s)" % (self.name, self.dbid)
def __repr__(self):
return "%s(account#%s)" % (self.name, self.dbid)
#@property
def __username_get(self):
return self.username
def __username_set(self, value):
old_name = self.username
self.username = value
self.save(update_fields=["username"])
SIGNAL_ACCOUNT_POST_RENAME.send(self, old_name=old_name, new_name=value)
def __username_del(self):
del self.username

View file

@ -1,12 +1,16 @@
from mock import Mock, MagicMock
# -*- coding: utf-8 -*-
import sys
from mock import Mock, MagicMock, patch
from random import randint
from unittest import TestCase
from django.test import override_settings
from evennia.accounts.accounts import AccountSessionHandler
from evennia.accounts.accounts import DefaultAccount
from evennia.server.session import Session
from evennia.accounts.accounts import DefaultAccount, DefaultGuest
from evennia.utils.test_resources import EvenniaTest
from evennia.utils import create
from evennia.utils.utils import uses_database
from django.conf import settings
@ -59,6 +63,139 @@ class TestAccountSessionHandler(TestCase):
self.assertEqual(self.handler.count(), len(self.handler.get()))
@override_settings(GUEST_ENABLED=True, GUEST_LIST=["bruce_wayne"])
class TestDefaultGuest(EvenniaTest):
"Check DefaultGuest class"
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.')
def test_authenticate(self):
# Create a guest account
account, errors = DefaultGuest.authenticate(ip=self.ip)
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!')
@patch("evennia.accounts.accounts.ChannelDB.objects.get_channel")
def test_create(self, get_channel):
get_channel.connect = MagicMock(return_value=True)
account, errors = DefaultGuest.create()
self.assertTrue(account, "Guest account should have been created.")
self.assertFalse(errors)
def test_at_post_login(self):
self.account.db._last_puppet = self.char1
self.account.at_post_login(self.session)
self.account.at_post_login()
def test_at_server_shutdown(self):
account, errors = DefaultGuest.create(ip=self.ip)
self.char1.delete = MagicMock()
account.db._playable_characters = [self.char1]
account.at_server_shutdown()
self.char1.delete.assert_called()
def test_at_post_disconnect(self):
account, errors = DefaultGuest.create(ip=self.ip)
self.char1.delete = MagicMock()
account.db._playable_characters = [self.char1]
account.at_post_disconnect()
self.char1.delete.assert_called()
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)
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.')
# 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.')
# Try creating a duplicate account
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!')
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"
# Should not accept Unicode by default, lest users pick names like this
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('¯\_(ツ)_/¯')
self.assertFalse(result, "Validator allowed kanji in username.")
# Should not allow duplicate username
result, error = DefaultAccount.validate_username(self.account.name)
self.assertFalse(result, "Duplicate username should not have passed validation.")
# Should not allow username too short
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'):
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"):
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)
from django.core.exceptions import ValidationError
# Try setting some bad passwords
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'))
account.delete()
class TestDefaultAccount(TestCase):
"Check DefaultAccount class"
@ -66,36 +203,6 @@ class TestDefaultAccount(TestCase):
self.s1 = MagicMock()
self.s1.puppet = None
self.s1.sessid = 0
self.s1.data_outj
def tearDown(self):
if hasattr(self, "account"):
self.account.delete()
def test_password_validation(self):
"Check password validators deny bad passwords"
self.account = create.create_account("TestAccount%s" % randint(0, 9),
email="test@test.com", password="testpassword", typeclass=DefaultAccount)
for bad in ('', '123', 'password', 'TestAccount', '#', 'xyzzy'):
self.assertFalse(self.account.validate_password(bad, account=self.account)[0])
"Check validators allow sufficiently complex passwords"
for better in ('Mxyzptlk', "j0hn, i'M 0n1y d4nc1nG"):
self.assertTrue(self.account.validate_password(better, account=self.account)[0])
def test_password_change(self):
"Check password setting and validation is working as expected"
self.account = create.create_account("TestAccount%s" % randint(0, 9),
email="test@test.com", password="testpassword", typeclass=DefaultAccount)
from django.core.exceptions import ValidationError
# Try setting some bad passwords
for bad in ('', '#', 'TestAccount', 'password'):
self.assertRaises(ValidationError, self.account.set_password, bad)
# Try setting a better password (test for False; returns None on success)
self.assertFalse(self.account.set_password('Mxyzptlk'))
def test_puppet_object_no_object(self):
"Check puppet_object method called with no object param"
@ -199,3 +306,90 @@ class TestDefaultAccount(TestCase):
account.puppet_object(self.s1, obj)
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.')
# 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.')
# 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)
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]
chars = self.account.characters
self.assertEqual(chars, [self.char1])
self.assertEqual(self.account.db._playable_characters, [self.char1])
def test_puppet_success(self):
self.account.msg = MagicMock()
with patch("evennia.accounts.accounts._MULTISESSION_MODE", 2):
self.account.puppet_object(self.session, self.char1)
self.account.msg.assert_called_with("You are already puppeting this object.")
@patch("evennia.accounts.accounts.time.time", return_value=10000)
def test_idle_time(self, mock_time):
self.session.cmd_last_visible = 10000 - 10
idle = self.account.idle_time
self.assertEqual(idle, 10)
# test no sessions
with patch("evennia.accounts.accounts._SESSIONS.sessions_from_account", return_value=[]) as mock_sessh:
idle = self.account.idle_time
self.assertEqual(idle, None)
@patch("evennia.accounts.accounts.time.time", return_value=10000)
def test_connection_time(self, mock_time):
self.session.conn_time = 10000 - 10
conn = self.account.connection_time
self.assertEqual(conn, 10)
# test no sessions
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",
locks="test:all()",
tags=[("tag1", "category1"), ("tag2", "category2", "data1"), ("tag3", None)],
attributes=[("key1", "value1", "category1",
"edit:false()", True),
("key2", "value2")])
acct.save()
self.assertTrue(acct.pk)
def test_at_look(self):
ret = self.account.at_look()
self.assertTrue("Out-of-Character" in ret)
ret = self.account.at_look(target=self.obj1)
self.assertTrue("Obj" in ret)
ret = self.account.at_look(session=self.session)
self.assertTrue("*" in ret) # * marks session is active in list
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.')
def test_msg(self):
self.account.msg

View file

@ -46,7 +46,7 @@ from django.conf import settings
from evennia.commands.command import InterruptCommand
from evennia.comms.channelhandler import CHANNELHANDLER
from evennia.utils import logger, utils
from evennia.utils.utils import string_suggestions, to_unicode
from evennia.utils.utils import string_suggestions
from django.utils.translation import ugettext as _
@ -190,7 +190,7 @@ def _progressive_cmd_run(cmd, generator, response=None):
try:
if response is None:
value = generator.next()
value = next(generator)
else:
value = generator.send(response)
except StopIteration:
@ -198,7 +198,7 @@ def _progressive_cmd_run(cmd, generator, response=None):
else:
if isinstance(value, (int, float)):
utils.delay(value, _progressive_cmd_run, cmd, generator)
elif isinstance(value, basestring):
elif isinstance(value, str):
_GET_INPUT(cmd.caller, value, _process_input, cmd=cmd, generator=generator)
else:
raise ValueError("unknown type for a yielded value in command: {}".format(type(value)))
@ -211,8 +211,8 @@ def _process_input(caller, prompt, result, cmd, generator):
Args:
caller (Character, Account or Session): the caller.
prompt (basestring): The sent prompt.
result (basestring): The unprocessed answer.
prompt (str): The sent prompt.
result (str): The unprocessed answer.
cmd (Command): The command itself.
generator (GeneratorType): The generator.
@ -443,7 +443,7 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
tempmergers[prio] = cmdset
# sort cmdsets after reverse priority (highest prio are merged in last)
cmdsets = yield sorted(tempmergers.values(), key=lambda x: x.priority)
cmdsets = yield sorted(list(tempmergers.values()), key=lambda x: x.priority)
# Merge all command sets into one, beginning with the lowest-prio one
cmdset = cmdsets[0]
@ -618,8 +618,6 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
finally:
_COMMAND_NESTING[called_by] -= 1
raw_string = to_unicode(raw_string, force_string=True)
session, account, obj = session, None, None
if callertype == "session":
session = called_by
@ -649,6 +647,7 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
args = raw_string
unformatted_raw_string = "%s%s" % (cmdname, args)
cmdset = None
raw_cmdname = cmdname
# session = session
# account = account

View file

@ -5,7 +5,7 @@ replacing cmdparser function. The replacement parser must accept the
same inputs as the default one.
"""
from __future__ import division
import re
from django.conf import settings
@ -15,6 +15,107 @@ _MULTIMATCH_REGEX = re.compile(settings.SEARCH_MULTIMATCH_REGEX, re.I + re.U)
_CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
def create_match(cmdname, string, cmdobj, raw_cmdname):
"""
Builds a command match by splitting the incoming string and
evaluating the quality of the match.
Args:
cmdname (str): Name of command to check for.
string (str): The string to match against.
cmdobj (str): The full Command instance.
raw_cmdname (str, optional): If CMD_IGNORE_PREFIX is set and the cmdname starts with
one of the prefixes to ignore, this contains the raw, unstripped cmdname,
otherwise it is None.
Returns:
match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname),
where `cmdname` is the command's name and `args` is the rest of the incoming
string, without said command name. `cmdobj` is
the Command instance, the cmdlen is the same as len(cmdname) and mratio
is a measure of how big a part of the full input string the cmdname
takes up - an exact match would be 1.0. Finally, the `raw_cmdname` is
the cmdname unmodified by eventual prefix-stripping.
"""
cmdlen, strlen = len(str(cmdname)), len(str(string))
mratio = 1 - (strlen - cmdlen) / (1.0 * strlen)
args = string[cmdlen:]
return (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname)
def build_matches(raw_string, cmdset, include_prefixes=False):
"""
Build match tuples by matching raw_string against available commands.
Args:
raw_string (str): Input string that can look in any way; the only assumption is
that the sought command's name/alias must be *first* in the string.
cmdset (CmdSet): The current cmdset to pick Commands from.
include_prefixes (bool): If set, include prefixes like @, ! etc (specified in settings)
in the match, otherwise strip them before matching.
Returns:
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
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):]))])
else:
# strip prefixes set in settings
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):])):
matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
except Exception:
log_trace("cmdhandler error. raw_input:%s" % raw_string)
return matches
def try_num_prefixes(raw_string):
"""
Test if user tried to separate multi-matches with a number separator
(default 1-name, 2-name etc). This is usually called last, if no other
match was found.
Args:
raw_string (str): The user input to parse.
Returns:
mindex, new_raw_string (tuple): If a multimatch-separator was detected,
this is stripped out as an integer to separate between the matches. The
new_raw_string is the result of stripping out that identifier. If no
such form was found, returns (None, None).
Example:
In the default configuration, entering 2-ball (e.g. in a room will more
than one 'ball' object), will lead to a multimatch and this function
will parse `"2-ball"` and return `(2, "ball")`.
"""
# no matches found
num_ref_match = _MULTIMATCH_REGEX.match(raw_string)
if num_ref_match:
# the user might be trying to identify the command
# with a #num-command style syntax. We expect the regex to
# contain the groups "number" and "name".
mindex, new_raw_string = num_ref_match.group("number"), num_ref_match.group("name")
return mindex, new_raw_string
else:
return None, None
def cmdparser(raw_string, cmdset, caller, match_index=None):
"""
This function is called by the cmdhandler once it has
@ -30,6 +131,10 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
the first run resulted in a multimatch, and the index is given
to select between the results for the second run.
Returns:
matches (list): This is a list of match-tuples as returned by `create_match`.
If no matches were found, this is an empty list.
Notes:
The cmdparser understand the following command combinations (where
[] marks optional parts.
@ -47,75 +152,11 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
the remaining arguments, and the matched cmdobject from the cmdset.
"""
def create_match(cmdname, string, cmdobj, raw_cmdname):
"""
Builds a command match by splitting the incoming string and
evaluating the quality of the match.
Args:
cmdname (str): Name of command to check for.
string (str): The string to match against.
cmdobj (str): The full Command instance.
raw_cmdname (str, optional): If CMD_IGNORE_PREFIX is set and the cmdname starts with
one of the prefixes to ignore, this contains the raw, unstripped cmdname,
otherwise it is None.
Returns:
match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname), where
`cmdname` is the command's name and `args` the rest of the incoming string,
without said command name. `cmdobj` is the Command instance, the cmdlen is
the same as len(cmdname) and mratio is a measure of how big a part of the
full input string the cmdname takes up - an exact match would be 1.0. Finally,
the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping.
"""
cmdlen, strlen = len(unicode(cmdname)), len(unicode(string))
mratio = 1 - (strlen - cmdlen) / (1.0 * strlen)
args = string[cmdlen:]
return (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname)
def build_matches(raw_string, include_prefixes=False):
l_raw_string = raw_string.lower()
matches = []
try:
if include_prefixes:
# use the cmdname as-is
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):]))])
else:
# strip prefixes set in settings
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):])):
matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
except Exception:
log_trace("cmdhandler error. raw_input:%s" % raw_string)
return matches
def try_num_prefixes(raw_string):
if not matches:
# no matches found
num_ref_match = _MULTIMATCH_REGEX.match(raw_string)
if num_ref_match:
# the user might be trying to identify the command
# with a #num-command style syntax. We expect the regex to
# contain the groups "number" and "name".
mindex, new_raw_string = num_ref_match.group("number"), num_ref_match.group("name")
return mindex, new_raw_string
return None, None
if not raw_string:
return []
# find mathces, first using the full name
matches = build_matches(raw_string, include_prefixes=True)
matches = build_matches(raw_string, cmdset, include_prefixes=True)
if not matches:
# try to match a number 1-cmdname, 2-cmdname etc
mindex, new_raw_string = try_num_prefixes(raw_string)
@ -124,7 +165,7 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
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
matches = build_matches(raw_string, include_prefixes=False)
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')]

View file

@ -54,7 +54,7 @@ class _CmdSetMeta(type):
if not isinstance(cls.key_mergetypes, dict):
cls.key_mergetypes = {}
super(_CmdSetMeta, cls).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
class CmdSet(with_metaclass(_CmdSetMeta, object)):

View file

@ -422,13 +422,13 @@ class CmdSetHandler(object):
it's a 'quirk' that has to be documented.
"""
if not (isinstance(cmdset, basestring) or utils.inherits_from(cmdset, CmdSet)):
if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)):
string = _("Only CmdSets can be added to the cmdsethandler!")
raise Exception(string)
if callable(cmdset):
cmdset = cmdset(self.obj)
elif isinstance(cmdset, basestring):
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':

View file

@ -7,11 +7,15 @@ All commands in Evennia inherit from the 'Command' class in this module.
from builtins import range
import re
import math
from django.conf import settings
from evennia.locks.lockhandler import LockHandler
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
@ -24,7 +28,10 @@ def _init_command(cls, **kwargs):
and (optionally) at instantiation time.
If kwargs are given, these are set as instance-specific properties
on the command.
on the command - but note that the Command instance is *re-used* on a given
host object, so a kwarg value set on the instance will *remain* on the instance
for subsequent uses of that Command on that particular object.
"""
for i in range(len(kwargs)):
# used for dynamic creation of commands
@ -65,7 +72,7 @@ def _init_command(cls, **kwargs):
temp.append(lockstring)
cls.lock_storage = ";".join(temp)
if hasattr(cls, 'arg_regex') and isinstance(cls.arg_regex, basestring):
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
@ -82,7 +89,7 @@ class CommandMeta(type):
"""
def __init__(cls, *args, **kwargs):
_init_command(cls, **kwargs)
super(CommandMeta, cls).__init__(*args, **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
@ -201,6 +208,19 @@ class Command(with_metaclass(CommandMeta, object)):
# probably got a string
return cmd in self._matchset
def __hash__(self):
"""
Python 3 requires that any class which implements __eq__ must also
implement __hash__ and that the corresponding hashes for equivalent
instances are themselves equivalent.
Technically, the following implementation is only valid for comparison
against other Commands, as our __eq__ supports comparison against
str, too.
"""
return hash('\n'.join(self._matchset))
def __ne__(self, cmd):
"""
The logical negation of __eq__. Since this is one of the most
@ -266,7 +286,7 @@ class Command(with_metaclass(CommandMeta, object)):
caches are properly updated as well.
"""
if isinstance(new_aliases, basestring):
if isinstance(new_aliases, str):
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))
@ -394,6 +414,14 @@ 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())
string = f"""
Command {self} has no defined `func()` - showing on-command variables:
{variables}
"""
self.caller.msg(string)
return
# a simple test command to show the available properties
string = "-" * 50
string += "\n|w%s|n - Command variables from evennia:\n" % self.key
@ -452,6 +480,149 @@ class Command(with_metaclass(CommandMeta, object)):
"""
return self.__doc__
def client_width(self):
"""
Get the client screenwidth for the session using this command.
Returns:
client width (int or None): The width (in characters) of the client window. None
if this command is run without a Session (such as by an NPC).
"""
if self.session:
return self.session.protocol_flags['SCREENWIDTH'][0]
def styled_table(self, *args, **kwargs):
"""
Create an EvTable styled by on user preferences.
Args:
*args (str): Column headers. If not colored explicitly, these will get colors
from user options.
Kwargs:
any (str, int or dict): EvTable options, including, optionally a `table` dict
detailing the contents of the table.
Returns:
table (EvTable): An initialized evtable entity, either complete (if using `table` kwarg)
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')
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')
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_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')
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):
"""
Helper for formatting a string into a pretty display, for a header, separator or footer.
Kwargs:
header_text (str): Text to include in header.
fill_character (str): This single character will be used to fill the width of the
display.
edge_character (str): This character caps the edges of the display.
mode(str): One of 'header', 'separator' or 'footer'.
color_header (bool): If the header should be colorized based on user options.
width (int): If not given, the client's width will be used if available.
Returns:
string (str): The decorated and formatted text.
"""
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)
width = width or self.width()
if edge_character:
width -= 2
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']))
center_string = ANSIString(begin_center + header_text + end_center)
else:
center_string = ANSIString('|n |%s%s |n' % (colors['headertext'], header_text))
else:
center_string = ''
fill_character = self.account.options.get('%s_fill' % mode)
remain_fill = width - len(center_string)
if remain_fill % 2 == 0:
right_width = remain_fill / 2
left_width = remain_fill / 2
else:
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)))
if 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)
else:
final_send = left_fill + ANSIString(center_string) + right_fill
return final_send
def styled_header(self, *args, **kwargs):
"""
Create a pretty header.
"""
if 'mode' not in kwargs:
kwargs['mode'] = 'header'
return self._render_decoration(*args, **kwargs)
def styled_separator(self, *args, **kwargs):
"""
Create a separator.
"""
if 'mode' not in kwargs:
kwargs['mode'] = 'separator'
return self._render_decoration(*args, **kwargs)
def styled_footer(self, *args, **kwargs):
"""
Create a pretty footer.
"""
if 'mode' not in kwargs:
kwargs['mode'] = 'footer'
return self._render_decoration(*args, **kwargs)
class InterruptCommand(Exception):

View file

@ -11,7 +11,7 @@ self.account to make sure to always use the account object rather than
self.caller (which change depending on the level you are calling from)
The property self.character can be used to access the character when
these commands are triggered with a connected character (such as the
case of the @ooc command), it is None if we are OOC.
case of the `ooc` command), it is None if we are OOC.
Note that under MULTISESSION_MODE > 2, Account commands should use
self.msg() and similar methods to reroute returns to the correct
@ -21,9 +21,10 @@ 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
from evennia.server.sessionhandler import SESSIONS
from evennia.utils import utils, create, search, evtable
from evennia.utils import utils, create, logger, search
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -46,7 +47,7 @@ class MuxAccountLookCommand(COMMAND_DEFAULT_CLASS):
def parse(self):
"""Custom parsing"""
super(MuxAccountLookCommand, self).parse()
super().parse()
if _MULTISESSION_MODE < 2:
# only one character allowed - not used in this mode
@ -101,7 +102,7 @@ class CmdOOCLook(MuxAccountLookCommand):
if _MULTISESSION_MODE < 2:
# only one character allowed
self.msg("You are out-of-character (OOC).\nUse |w@ic|n to get back into the game.")
self.msg("You are out-of-character (OOC).\nUse |wic|n to get back into the game.")
return
# call on-account look helper method
@ -113,14 +114,14 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
create a new character
Usage:
@charcreate <charname> [= desc]
charcreate <charname> [= desc]
Create a new character, optionally giving it a description. You
may use upper-case letters in the name - you will nevertheless
always be able to access your character using lower-case letters
if you want.
"""
key = "@charcreate"
key = "charcreate"
locks = "cmd:pperm(Player)"
help_category = "General"
@ -131,7 +132,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
"""create the new character"""
account = self.account
if not self.args:
self.msg("Usage: @charcreate <charname> [= description]")
self.msg("Usage: charcreate <charname> [= description]")
return
key = self.lhs
desc = self.rhs
@ -162,15 +163,16 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
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)" %
(new_character.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 |w@ic %s|n to enter the game as this 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))
class CmdCharDelete(COMMAND_DEFAULT_CLASS):
@ -178,11 +180,11 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
delete a character - this cannot be undone!
Usage:
@chardelete <charname>
chardelete <charname>
Permanently deletes one of your characters.
"""
key = "@chardelete"
key = "chardelete"
locks = "cmd:pperm(Player)"
help_category = "General"
@ -191,7 +193,7 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
account = self.account
if not self.args:
self.msg("Usage: @chardelete <charactername>")
self.msg("Usage: chardelete <charactername>")
return
# use the playable_characters list to search
@ -214,12 +216,19 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
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))
else:
self.msg("Deletion was aborted.")
del caller.ndb._char_to_delete
match = match[0]
account.ndb._char_to_delete = match
# Return if caller has no permission to delete this
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]?"
get_input(account, prompt % match.key, _callback)
@ -229,7 +238,7 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
control an object you have permission to puppet
Usage:
@ic <character>
ic <character>
Go in-character (IC) as a given Character.
@ -242,10 +251,10 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
as you the account have access right to puppet it.
"""
key = "@ic"
key = "ic"
# lock must be all() for different puppeted objects to access it.
locks = "cmd:all()"
aliases = "@puppet"
aliases = "puppet"
help_category = "General"
# this is used by the parent
@ -262,7 +271,7 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
if not self.args:
new_character = account.db._last_puppet
if not new_character:
self.msg("Usage: @ic <character>")
self.msg("Usage: ic <character>")
return
if not new_character:
# search for a matching character
@ -279,8 +288,10 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
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))
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))
# note that this is inheriting from MuxAccountLookCommand,
@ -290,16 +301,16 @@ class CmdOOC(MuxAccountLookCommand):
stop puppeting and go ooc
Usage:
@ooc
ooc
Go out-of-character (OOC).
This will leave your current character and put you in a incorporeal OOC state.
"""
key = "@ooc"
key = "ooc"
locks = "cmd:pperm(Player)"
aliases = "@unpuppet"
aliases = "unpuppet"
help_category = "General"
# this is used by the parent
@ -326,7 +337,7 @@ class CmdOOC(MuxAccountLookCommand):
if _MULTISESSION_MODE < 2:
# only one character allowed
self.msg("You are out-of-character (OOC).\nUse |w@ic|n to get back into the game.")
self.msg("You are out-of-character (OOC).\nUse |wic|n to get back into the game.")
return
self.msg(account.at_look(target=self.playable, session=session))
@ -340,12 +351,12 @@ class CmdSessions(COMMAND_DEFAULT_CLASS):
check your connected session(s)
Usage:
@sessions
sessions
Lists the sessions currently connected to your account.
"""
key = "@sessions"
key = "sessions"
locks = "cmd:all()"
help_category = "General"
@ -356,11 +367,11 @@ class CmdSessions(COMMAND_DEFAULT_CLASS):
"""Implement function"""
account = self.account
sessions = account.sessions.all()
table = evtable.EvTable("|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),
@ -404,10 +415,10 @@ class CmdWho(COMMAND_DEFAULT_CLASS):
else:
show_session_data = account.check_permstring("Developer") or account.check_permstring("Admins")
naccounts = (SESSIONS.account_count())
naccounts = SESSIONS.account_count()
if show_session_data:
# privileged info
table = evtable.EvTable("|wAccount Name",
table = self.styled_table("|wAccount Name",
"|wOn for",
"|wIdle",
"|wPuppeting",
@ -433,7 +444,7 @@ class CmdWho(COMMAND_DEFAULT_CLASS):
isinstance(session.address, tuple) and session.address[0] or session.address)
else:
# unprivileged
table = evtable.EvTable("|wAccount name", "|wOn for", "|wIdle")
table = self.styled_table("|wAccount name", "|wOn for", "|wIdle")
for session in session_list:
if not session.logged_in:
continue
@ -453,7 +464,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
Set an account option
Usage:
@option[/save] [name = value]
option[/save] [name = value]
Switches:
save - Save the current option settings for future logins.
@ -465,8 +476,8 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
"""
key = "@option"
aliases = "@options"
key = "option"
aliases = "options"
switch_options = ("save", "clear")
locks = "cmd:all()"
@ -489,7 +500,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
if "save" in self.switches:
# save all options
self.caller.db._saved_protocol_flags = flags
self.msg("|gSaved all options. Use @option/clear to remove.|n")
self.msg("|gSaved all options. Use option/clear to remove.|n")
if "clear" in self.switches:
# clear all saves
self.caller.db._saved_protocol_flags = {}
@ -503,17 +514,17 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
options["SCREENWIDTH"] = options["SCREENWIDTH"][0]
else:
options["SCREENWIDTH"] = " \n".join("%s : %s" % (screenid, size)
for screenid, size in options["SCREENWIDTH"].iteritems())
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"].iteritems())
for screenid, size in options["SCREENHEIGHT"].items())
options.pop("TTYPE", None)
header = ("Name", "Value", "Saved") if saved_options else ("Name", "Value")
table = evtable.EvTable(*header)
table = self.styled_table(*header)
for key in sorted(options):
row = [key, options[key]]
if saved_options:
@ -526,7 +537,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
return
if not self.rhs:
self.msg("Usage: @option [name = [value]]")
self.msg("Usage: option [name = [value]]")
return
# Try to assign new values
@ -534,7 +545,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
def validate_encoding(new_encoding):
# helper: change encoding
try:
utils.to_str(utils.to_unicode("test-string"), encoding=new_encoding)
codecs_lookup(new_encoding)
except LookupError:
raise RuntimeError("The encoding '|w%s|n' is invalid. " % new_encoding)
return val
@ -608,11 +619,11 @@ class CmdPassword(COMMAND_DEFAULT_CLASS):
change your password
Usage:
@password <old password> = <new password>
password <old password> = <new password>
Changes your password. Make sure to pick a safe one.
"""
key = "@password"
key = "password"
locks = "cmd:pperm(Player)"
# this is used by the parent
@ -623,7 +634,7 @@ class CmdPassword(COMMAND_DEFAULT_CLASS):
account = self.account
if not self.rhs:
self.msg("Usage: @password <oldpass> = <newpass>")
self.msg("Usage: password <oldpass> = <newpass>")
return
oldpass = self.lhslist[0] # Both of these are
newpass = self.rhslist[0] # already stripped by parse()
@ -641,6 +652,7 @@ 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))
class CmdQuit(COMMAND_DEFAULT_CLASS):
@ -648,7 +660,7 @@ class CmdQuit(COMMAND_DEFAULT_CLASS):
quit the game
Usage:
@quit
quit
Switch:
all - disconnect all connected sessions
@ -656,7 +668,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"
key = "quit"
switch_options = ("all",)
locks = "cmd:all()"
@ -690,7 +702,7 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
testing which colors your client support
Usage:
@color ansi||xterm256
color ansi||xterm256
Prints a color map along with in-mud color codes to use to produce
them. It also tests what is supported in your client. Choices are
@ -698,7 +710,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"
key = "color"
locks = "cmd:all()"
help_category = "General"
@ -793,7 +805,7 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
self.msg(string)
else:
# malformed input
self.msg("Usage: @color ansi||xterm256")
self.msg("Usage: color ansi||xterm256")
class CmdQuell(COMMAND_DEFAULT_CLASS):
@ -813,8 +825,8 @@ class CmdQuell(COMMAND_DEFAULT_CLASS):
Use the unquell command to revert back to normal operation.
"""
key = "@quell"
aliases = ["@unquell"]
key = "quell"
aliases = ["unquell"]
locks = "cmd:pperm(Player)"
help_category = "General"
@ -836,7 +848,7 @@ class CmdQuell(COMMAND_DEFAULT_CLASS):
"""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 self.cmdstring in ('unquell', 'unquell'):
if not account.attributes.get('_quell'):
self.msg("Already using normal Account permissions %s." % permstr)
else:
@ -853,8 +865,47 @@ class CmdQuell(COMMAND_DEFAULT_CLASS):
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 += "\nUse @unquell to return to normal permission usage."
cpermstr += "\nUse unquell to return to normal permission usage."
self.msg(cpermstr)
else:
self.msg("Quelling Account permissions%s. Use @unquell to get them back." % permstr)
self.msg("Quelling Account permissions%s. Use unquell to get them back." % permstr)
self._recache_locks(account)
class CmdStyle(COMMAND_DEFAULT_CLASS):
"""
In-game style options
Usage:
style
style <option> = <value>
Configure stylings for in-game display elements like table borders, help
entriest etc. Use without arguments to see all available options.
"""
key = "style"
switch_options = ['clear']
def func(self):
if not self.args:
self.list_styles()
return
self.set()
def list_styles(self):
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())
self.msg(str(table))
def set(self):
try:
result = self.account.options.set(self.lhs, self.rhs)
except ValueError as e:
self.msg(str(e))
return
self.msg('Style %s set to %s' % (self.lhs, result))

View file

@ -9,15 +9,15 @@ import re
from django.conf import settings
from evennia.server.sessionhandler import SESSIONS
from evennia.server.models import ServerConfig
from evennia.utils import evtable, search, class_from_module
from evennia.utils import evtable, logger, search, class_from_module
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", "CmdDelAccount",
"CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall")
__all__ = ("CmdBoot", "CmdBan", "CmdUnban",
"CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall", "CmdForce")
class CmdBoot(COMMAND_DEFAULT_CLASS):
@ -25,7 +25,7 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
kick an account from the server.
Usage
@boot[/switches] <account obj> [: reason]
boot[/switches] <account obj> [: reason]
Switches:
quiet - Silently boot without informing account
@ -35,7 +35,7 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
supplied it will be echoed to the user unless /quiet is set.
"""
key = "@boot"
key = "boot"
switch_options = ("quiet", "sid")
locks = "cmd:perm(boot) or perm(Admin)"
help_category = "Admin"
@ -46,7 +46,7 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
args = self.args
if not args:
caller.msg("Usage: @boot[/switches] <account> [:reason]")
caller.msg("Usage: boot[/switches] <account> [:reason]")
return
if ':' in args:
@ -96,20 +96,27 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
session.msg(feedback)
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))
# regex matching IP addresses with wildcards, eg. 233.122.4.*
IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}")
def list_bans(banlist):
def list_bans(cmd, banlist):
"""
Helper function to display a list of active bans. Input argument
is the banlist read into the two commands @ban and @unban below.
is the banlist read into the two commands ban and unban below.
Args:
cmd (Command): Instance of the Ban command.
banlist (list): List of bans to list.
"""
if not banlist:
return "No active bans were found."
table = evtable.EvTable("|wid", "|wname/ip", "|wdate", "|wreason")
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],
@ -122,7 +129,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
ban an account from the server
Usage:
@ban [<name or ip> [: reason]]
ban [<name or ip> [: reason]]
Without any arguments, shows numbered list of active bans.
@ -130,7 +137,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
reason to be able to later remember why the ban was put in place.
It is often preferable to ban an account from the server than to
delete an account with @delaccount. If banned by name, that account
delete an account with accounts/delete. If banned by name, that account
account can no longer be logged into.
IP (Internet Protocol) address banning allows blocking all access
@ -138,10 +145,10 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
wildcard.
Examples:
@ban thomas - ban account 'thomas'
@ban/ip 134.233.2.111 - ban specific ip address
@ban/ip 134.233.2.* - ban all in a subnet
@ban/ip 134.233.*.* - even wider ban
ban thomas - ban account 'thomas'
ban/ip 134.233.2.111 - ban specific ip address
ban/ip 134.233.2.* - ban all in a subnet
ban/ip 134.233.*.* - even wider ban
A single IP filter can be easy to circumvent by changing computers
or requesting a new IP address. Setting a wide IP block filter with
@ -150,8 +157,8 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
or region.
"""
key = "@ban"
aliases = ["@bans"]
key = "ban"
aliases = ["bans"]
locks = "cmd:perm(ban) or perm(Developer)"
help_category = "Admin"
@ -175,7 +182,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
if not self.args or (self.switches and
not any(switch in ('ip', 'name')
for switch in self.switches)):
self.caller.msg(list_bans(banlist))
self.caller.msg(list_bans(self, banlist))
return
now = time.ctime()
@ -203,6 +210,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
banlist.append(bantup)
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))
class CmdUnban(COMMAND_DEFAULT_CLASS):
@ -210,15 +218,15 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
remove a ban from an account
Usage:
@unban <banid>
unban <banid>
This will clear an account name/ip ban previously set with the @ban
This will clear an account name/ip ban previously set with the ban
command. Use this command without an argument to view a numbered
list of bans. Use the numbers in this list to select which one to
unban.
"""
key = "@unban"
key = "unban"
locks = "cmd:perm(unban) or perm(Developer)"
help_category = "Admin"
@ -228,7 +236,7 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
banlist = ServerConfig.objects.conf('server_bans')
if not self.args:
self.caller.msg(list_bans(banlist))
self.caller.msg(list_bans(self, banlist))
return
try:
@ -246,79 +254,10 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
ban = banlist[num - 1]
del banlist[num - 1]
ServerConfig.objects.conf('server_bans', banlist)
value = " ".join([s for s in ban[:2]])
self.caller.msg("Cleared ban %s: %s" %
(num, " ".join([s for s in ban[:2]])))
class CmdDelAccount(COMMAND_DEFAULT_CLASS):
"""
delete an account from the server
Usage:
@delaccount[/switch] <name> [: reason]
Switch:
delobj - also delete the account's currently
assigned in-game object.
Completely deletes a user from the server database,
making their nick and e-mail again available.
"""
key = "@delaccount"
switch_options = ("delobj",)
locks = "cmd:perm(delaccount) or perm(Developer)"
help_category = "Admin"
def func(self):
"""Implements the command."""
caller = self.caller
args = self.args
if hasattr(caller, 'account'):
caller = caller.account
if not args:
self.msg("Usage: @delaccount <account/user name or #id> [: reason]")
return
reason = ""
if ':' in args:
args, reason = [arg.strip() for arg in args.split(':', 1)]
# We use account_search since we want to be sure to find also accounts
# that lack characters.
accounts = search.account_search(args)
if not accounts:
self.msg('Could not find an account by that name.')
return
if len(accounts) > 1:
string = "There were multiple matches:\n"
string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts)
self.msg(string)
return
# one single match
account = accounts.first()
if not account.access(caller, 'delete'):
string = "You don't have the permissions to delete that account."
self.msg(string)
return
uname = account.username
# boot the account then delete
self.msg("Informing and disconnecting account ...")
string = "\nYour account '%s' is being *permanently* deleted.\n" % uname
if reason:
string += " Reason given:\n '%s'" % reason
account.msg(string)
account.delete()
self.msg("Account %s was successfully deleted." % uname)
(num, value))
logger.log_sec('Unbanned: %s (Caller: %s, IP: %s).' % (value.strip(), self.caller, self.session.address))
class CmdEmit(COMMAND_DEFAULT_CLASS):
@ -326,9 +265,9 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
admin command for emitting message to multiple objects
Usage:
@emit[/switches] [<obj>, <obj>, ... =] <message>
@remit [<obj>, <obj>, ... =] <message>
@pemit [<obj>, <obj>, ... =] <message>
emit[/switches] [<obj>, <obj>, ... =] <message>
remit [<obj>, <obj>, ... =] <message>
pemit [<obj>, <obj>, ... =] <message>
Switches:
room - limit emits to rooms only (default)
@ -337,12 +276,12 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
Emits a message to the selected objects or to
your immediate surroundings. If the object is a room,
send to its contents. @remit and @pemit are just
limited forms of @emit, for sending to rooms and
send to its contents. remit and pemit are just
limited forms of emit, for sending to rooms and
to accounts respectively.
"""
key = "@emit"
aliases = ["@pemit", "@remit"]
key = "emit"
aliases = ["pemit", "remit"]
switch_options = ("room", "accounts", "contents")
locks = "cmd:perm(emit) or perm(Builder)"
help_category = "Admin"
@ -355,9 +294,9 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
if not args:
string = "Usage: "
string += "\n@emit[/switches] [<obj>, <obj>, ... =] <message>"
string += "\n@remit [<obj>, <obj>, ... =] <message>"
string += "\n@pemit [<obj>, <obj>, ... =] <message>"
string += "\nemit[/switches] [<obj>, <obj>, ... =] <message>"
string += "\nremit [<obj>, <obj>, ... =] <message>"
string += "\npemit [<obj>, <obj>, ... =] <message>"
caller.msg(string)
return
@ -366,10 +305,10 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
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:
@ -406,12 +345,12 @@ class CmdNewPassword(COMMAND_DEFAULT_CLASS):
change the password of an account
Usage:
@userpassword <user obj> = <new password>
userpassword <user obj> = <new password>
Set an account's password.
"""
key = "@userpassword"
key = "userpassword"
locks = "cmd:perm(newpassword) or perm(Admin)"
help_category = "Admin"
@ -421,16 +360,16 @@ class CmdNewPassword(COMMAND_DEFAULT_CLASS):
caller = self.caller
if not self.rhs:
self.msg("Usage: @userpassword <user obj> = <new password>")
self.msg("Usage: userpassword <user obj> = <new password>")
return
# the account search also matches 'me' etc.
account = caller.search_account(self.lhs)
if not account:
return
newpass = self.rhs
# Validate password
validated, error = account.validate_password(newpass)
if not validated:
@ -438,13 +377,14 @@ class CmdNewPassword(COMMAND_DEFAULT_CLASS):
string = "\n".join(errors)
caller.msg(string)
return
account.set_password(newpass)
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))
class CmdPerm(COMMAND_DEFAULT_CLASS):
@ -452,8 +392,8 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
set the permissions of an account/object
Usage:
@perm[/switch] <object> [= <permission>[,<permission>,...]]
@perm[/switch] *<account> [= <permission>[,<permission>,...]]
perm[/switch] <object> [= <permission>[,<permission>,...]]
perm[/switch] *<account> [= <permission>[,<permission>,...]]
Switches:
del - delete the given permission from <object> or <account>.
@ -462,8 +402,8 @@ 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"
key = "perm"
aliases = "setperm"
switch_options = ("del", "account")
locks = "cmd:perm(perm) or perm(Developer)"
help_category = "Admin"
@ -476,7 +416,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
lhs, rhs = self.lhs, self.rhs
if not self.args:
string = "Usage: @perm[/switch] object [ = permission, permission, ...]"
string = "Usage: perm[/switch] object [ = permission, permission, ...]"
caller.msg(string)
return
@ -526,6 +466,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
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))
else:
# add a new permission
permissions = obj.permissions.all()
@ -547,6 +488,8 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
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:
obj.msg("".join(target_result).strip())
@ -557,20 +500,50 @@ class CmdWall(COMMAND_DEFAULT_CLASS):
make an announcement to all
Usage:
@wall <message>
wall <message>
Announces a message to all connected sessions
including all currently unlogged in.
"""
key = "@wall"
key = "wall"
locks = "cmd:perm(wall) or perm(Admin)"
help_category = "Admin"
def func(self):
"""Implements command"""
if not self.args:
self.caller.msg("Usage: @wall <message>")
self.caller.msg("Usage: wall <message>")
return
message = "%s shouts \"%s\"" % (self.caller.name, self.args)
self.msg("Announcing to all connected sessions ...")
SESSIONS.announce_all(message)
class CmdForce(COMMAND_DEFAULT_CLASS):
"""
forces an object to execute a command
Usage:
force <object>=<command string>
Example:
force bob=get stick
"""
key = "force"
locks = "cmd:perm(spawn) or perm(Builder)"
help_category = "Building"
perm_used = "edit"
def func(self):
"""Implements the force command"""
if not self.lhs or not self.rhs:
self.caller.msg("You must provide a target and a command string to execute.")
return
targ = self.caller.search(self.lhs)
if not targ:
return
if not targ.access(self.caller, self.perm_used):
self.caller.msg("You don't have permission to force them to execute commands.")
return
targ.execute_cmd(self.rhs)
self.caller.msg("You have forced %s to: %s" % (targ, self.rhs))

View file

@ -225,7 +225,7 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
build from batch-command file
Usage:
@batchcommands[/interactive] <python.path.to.file>
batchcommands[/interactive] <python.path.to.file>
Switch:
interactive - this mode will offer more control when
@ -235,8 +235,8 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
Runs batches of commands from a batch-cmd text file (*.ev).
"""
key = "@batchcommands"
aliases = ["@batchcommand", "@batchcmd"]
key = "batchcommands"
aliases = ["batchcommand", "batchcmd"]
switch_options = ("interactive",)
locks = "cmd:perm(batchcommands) or perm(Developer)"
help_category = "Building"
@ -248,7 +248,7 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
args = self.args
if not args:
caller.msg("Usage: @batchcommands[/interactive] <path.to.file>")
caller.msg("Usage: batchcommands[/interactive] <path.to.file>")
return
python_path = self.args
@ -260,9 +260,13 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
caller.msg(_UTF8_ERROR % (python_path, err))
return
except IOError as err:
string = "'%s' not found.\nYou have to supply the python path\n" \
"using one of the defined batch-file directories\n (%s)."
caller.msg(string % (python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
if err:
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)."
caller.msg(string % (err, python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
return
if not commands:
caller.msg("File %s seems empty of valid commands." % python_path)
@ -288,7 +292,8 @@ 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) ..."
caller.msg("Running Batch-command processor - Automatic mode "
"for %s (this might take some time) ..."
% python_path)
procpool = False
@ -332,7 +337,7 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
build from batch-code file
Usage:
@batchcode[/interactive] <python path to file>
batchcode[/interactive] <python path to file>
Switch:
interactive - this mode will offer more control when
@ -346,8 +351,8 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
Runs batches of commands from a batch-code text file (*.py).
"""
key = "@batchcode"
aliases = ["@batchcodes"]
key = "batchcode"
aliases = ["batchcodes"]
switch_options = ("interactive", "debug")
locks = "cmd:superuser()"
help_category = "Building"
@ -359,7 +364,7 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
args = self.args
if not args:
caller.msg("Usage: @batchcode[/interactive/debug] <path.to.file>")
caller.msg("Usage: batchcode[/interactive/debug] <path.to.file>")
return
python_path = self.args
debug = 'debug' in self.switches
@ -370,10 +375,14 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
except UnicodeDecodeError as err:
caller.msg(_UTF8_ERROR % (python_path, err))
return
except IOError:
string = "'%s' not found.\nYou have to supply the python path\n" \
except IOError as err:
if err:
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)."
caller.msg(string % (python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
caller.msg(string % (err, python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
return
if not codes:
caller.msg("File %s seems empty of functional code." % python_path)
@ -443,13 +452,13 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
class CmdStateAbort(_COMMAND_DEFAULT_CLASS):
"""
@abort
abort
This is a safety feature. It force-ejects us out of the processor and to
the default cmdset, regardless of what current cmdset the processor might
have put us in (e.g. when testing buggy scripts etc).
"""
key = "@abort"
key = "abort"
help_category = "BatchProcess"
locks = "cmd:perm(batchcommands)"
@ -804,7 +813,7 @@ class CmdStateHH(_COMMAND_DEFAULT_CLASS):
cc - continue processing to end, then quit.
qq - quit (abort all remaining commands)
@abort - this is a safety command that always is available
abort - this is a safety command that always is available
regardless of what cmdsets gets added to us during
batch-command processing. It immediately shuts down
the processor and returns us to the default cmdset.
@ -822,7 +831,7 @@ class CmdStateHH(_COMMAND_DEFAULT_CLASS):
class BatchSafeCmdSet(CmdSet):
"""
The base cmdset for the batch processor.
This sets a 'safe' @abort command that will
This sets a 'safe' abort command that will
always be available to get out of everything.
"""
key = "Batch_default"

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,7 @@ class AccountCmdSet(CmdSet):
self.add(account.CmdPassword())
self.add(account.CmdColorTest())
self.add(account.CmdQuell())
self.add(account.CmdStyle())
# nicks
self.add(general.CmdNick())
@ -55,7 +56,6 @@ class AccountCmdSet(CmdSet):
self.add(system.CmdPy())
# Admin commands
self.add(admin.CmdDelAccount())
self.add(admin.CmdNewPassword())
# Comm commands
@ -74,3 +74,4 @@ class AccountCmdSet(CmdSet):
self.add(comms.CmdIRC2Chan())
self.add(comms.CmdIRCStatus())
self.add(comms.CmdRSS2Chan())
self.add(comms.CmdGrapevine2Chan())

View file

@ -57,6 +57,7 @@ class CharacterCmdSet(CmdSet):
self.add(admin.CmdEmit())
self.add(admin.CmdPerm())
self.add(admin.CmdWall())
self.add(admin.CmdForce())
# Building and world manipulation
self.add(building.CmdTeleport())

View file

@ -9,14 +9,13 @@ for easy handling.
"""
import hashlib
import time
from past.builtins import cmp
from django.conf import settings
from evennia.comms.models import ChannelDB, Msg
from evennia.accounts.models import AccountDB
from evennia.accounts import bots
from evennia.comms.channelhandler import CHANNELHANDLER
from evennia.locks.lockhandler import LockException
from evennia.utils import create, utils, evtable
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)
@ -219,7 +218,7 @@ class CmdAllCom(COMMAND_DEFAULT_CLASS):
caller = self.caller
args = self.args
if not args:
self.execute_cmd("@channels")
self.execute_cmd("channels")
self.msg("(Usage: allcom on | off | who | destroy)")
return
@ -240,7 +239,7 @@ class CmdAllCom(COMMAND_DEFAULT_CLASS):
channels = [chan for chan in ChannelDB.objects.get_all_channels()
if chan.access(caller, 'control')]
for channel in channels:
self.execute_cmd("@cdestroy %s" % channel.key)
self.execute_cmd("cdestroy %s" % channel.key)
elif args == "who":
# run a who, listing the subscribers on visible channels.
string = "\n|CChannel subscriptions|n"
@ -261,16 +260,16 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
list all channels available to you
Usage:
@channels
@clist
channels
clist
comlist
Lists all channels available to you, whether you listen to them or not.
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"]
key = "channels"
aliases = ["clist", "comlist", "chanlist", "channellist", "all channels"]
help_category = "Comms"
locks = "cmd: not pperm(channel_banned)"
@ -293,8 +292,8 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
if self.cmdstring == "comlist":
# just display the subscribed channels with no extra info
comtable = evtable.EvTable("|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)
@ -303,12 +302,12 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
"%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 |w@channels|n to list all,"
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 = evtable.EvTable("|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)
@ -337,12 +336,12 @@ class CmdCdestroy(COMMAND_DEFAULT_CLASS):
destroy a channel you created
Usage:
@cdestroy <channel>
cdestroy <channel>
Destroys a channel that you control.
"""
key = "@cdestroy"
key = "cdestroy"
help_category = "Comms"
locks = "cmd: not pperm(channel_banned)"
@ -354,7 +353,7 @@ class CmdCdestroy(COMMAND_DEFAULT_CLASS):
caller = self.caller
if not self.args:
self.msg("Usage: @cdestroy <channelname>")
self.msg("Usage: cdestroy <channelname>")
return
channel = find_channel(caller, self.args)
if not channel:
@ -370,6 +369,7 @@ 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))
class CmdCBoot(COMMAND_DEFAULT_CLASS):
@ -377,7 +377,7 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
kick an account from a channel you control
Usage:
@cboot[/quiet] <channel> = <account> [:reason]
cboot[/quiet] <channel> = <account> [:reason]
Switch:
quiet - don't notify the channel
@ -386,7 +386,7 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
"""
key = "@cboot"
key = "cboot"
switch_options = ("quiet",)
locks = "cmd: not pperm(channel_banned)"
help_category = "Comms"
@ -398,7 +398,7 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
"""implement the function"""
if not self.args or not self.rhs:
string = "Usage: @cboot[/quiet] <channel> = <account> [:reason]"
string = "Usage: cboot[/quiet] <channel> = <account> [:reason]"
self.msg(string)
return
@ -435,6 +435,8 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
# 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))
class CmdCemit(COMMAND_DEFAULT_CLASS):
@ -442,7 +444,7 @@ class CmdCemit(COMMAND_DEFAULT_CLASS):
send an admin message to a channel you control
Usage:
@cemit[/switches] <channel> = <message>
cemit[/switches] <channel> = <message>
Switches:
sendername - attach the sender's name before the message
@ -454,8 +456,8 @@ class CmdCemit(COMMAND_DEFAULT_CLASS):
"""
key = "@cemit"
aliases = ["@cmsg"]
key = "cemit"
aliases = ["cmsg"]
switch_options = ("sendername", "quiet")
locks = "cmd: not pperm(channel_banned) and pperm(Player)"
help_category = "Comms"
@ -467,7 +469,7 @@ class CmdCemit(COMMAND_DEFAULT_CLASS):
"""Implement function"""
if not self.args or not self.rhs:
string = "Usage: @cemit[/switches] <channel> = <message>"
string = "Usage: cemit[/switches] <channel> = <message>"
self.msg(string)
return
channel = find_channel(self.caller, self.lhs)
@ -491,11 +493,11 @@ class CmdCWho(COMMAND_DEFAULT_CLASS):
show who is listening to a channel
Usage:
@cwho <channel>
cwho <channel>
List who is connected to a given channel you have access to.
"""
key = "@cwho"
key = "cwho"
locks = "cmd: not pperm(channel_banned)"
help_category = "Comms"
@ -506,7 +508,7 @@ class CmdCWho(COMMAND_DEFAULT_CLASS):
"""implement function"""
if not self.args:
string = "Usage: @cwho <channel>"
string = "Usage: cwho <channel>"
self.msg(string)
return
@ -527,12 +529,12 @@ class CmdChannelCreate(COMMAND_DEFAULT_CLASS):
create a new channel
Usage:
@ccreate <new channel>[;alias;alias...] = description
ccreate <new channel>[;alias;alias...] = description
Creates a new channel owned by you.
"""
key = "@ccreate"
key = "ccreate"
aliases = "channelcreate"
locks = "cmd:not pperm(channel_banned) and pperm(Player)"
help_category = "Comms"
@ -546,7 +548,7 @@ class CmdChannelCreate(COMMAND_DEFAULT_CLASS):
caller = self.caller
if not self.args:
self.msg("Usage @ccreate <channelname>[;alias;alias..] = description")
self.msg("Usage ccreate <channelname>[;alias;alias..] = description")
return
description = ""
@ -579,15 +581,15 @@ class CmdClock(COMMAND_DEFAULT_CLASS):
change channel locks of a channel you control
Usage:
@clock <channel> [= <lockstring>]
clock <channel> [= <lockstring>]
Changes the lock access restrictions of a channel. If no
lockstring was given, view the current lock definitions.
"""
key = "@clock"
key = "clock"
locks = "cmd:not pperm(channel_banned)"
aliases = ["@clock"]
aliases = ["clock"]
help_category = "Comms"
# this is used by the COMMAND_DEFAULT_CLASS parent
@ -597,7 +599,7 @@ class CmdClock(COMMAND_DEFAULT_CLASS):
"""run the function"""
if not self.args:
string = "Usage: @clock channel [= lockstring]"
string = "Usage: clock channel [= lockstring]"
self.msg(string)
return
@ -632,13 +634,13 @@ class CmdCdesc(COMMAND_DEFAULT_CLASS):
describe a channel you control
Usage:
@cdesc <channel> = <description>
cdesc <channel> = <description>
Changes the description of the channel as shown in
channel lists.
"""
key = "@cdesc"
key = "cdesc"
locks = "cmd:not pperm(channel_banned)"
help_category = "Comms"
@ -651,7 +653,7 @@ class CmdCdesc(COMMAND_DEFAULT_CLASS):
caller = self.caller
if not self.rhs:
self.msg("Usage: @cdesc <channel> = <description>")
self.msg("Usage: cdesc <channel> = <description>")
return
channel = find_channel(caller, self.lhs)
if not channel:
@ -716,7 +718,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
if not self.args or not self.rhs:
pages = pages_we_sent + pages_we_got
pages.sort(lambda x, y: cmp(x.date_created, y.date_created))
pages = sorted(pages, key=lambda page: page.date_created)
number = 5
if self.args:
@ -759,7 +761,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
recobjs = []
for receiver in set(receivers):
if isinstance(receiver, basestring):
if isinstance(receiver, str):
pobj = caller.search(receiver)
elif hasattr(receiver, 'character'):
pobj = receiver
@ -802,19 +804,20 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
self.msg("You paged %s with: '%s'." % (", ".join(received), message))
def _list_bots():
def _list_bots(cmd):
"""
Helper function to produce a list of all IRC bots.
Args:
cmd (Command): Instance of the Bot command.
Returns:
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-")]
if ircbots:
from evennia.utils.evtable import EvTable
table = EvTable("|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)
@ -825,11 +828,11 @@ def _list_bots():
class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
"""
link an evennia channel to an external IRC channel
Link an evennia channel to an external IRC channel
Usage:
@irc2chan[/switches] <evennia_channel> = <ircnetwork> <port> <#irchannel> <botname>[:typeclass]
@irc2chan/delete botname|#dbid
irc2chan[/switches] <evennia_channel> = <ircnetwork> <port> <#irchannel> <botname>[:typeclass]
irc2chan/delete botname|#dbid
Switches:
/delete - this will delete the bot and remove the irc connection
@ -840,8 +843,8 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
/ssl - use an SSL-encrypted connection
Example:
@irc2chan myircchan = irc.dalnet.net 6667 #mychannel evennia-bot
@irc2chan public = irc.freenode.net 6667 #evgaming #evbot:accounts.mybot.MyBot
irc2chan myircchan = irc.dalnet.net 6667 #mychannel evennia-bot
irc2chan public = irc.freenode.net 6667 #evgaming #evbot:accounts.mybot.MyBot
This creates an IRC bot that connects to a given IRC network and
channel. If a custom typeclass path is given, this will be used
@ -850,11 +853,11 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
IRC channel and vice versa. The bot will automatically connect at
server start, so this command need only be given once. The
/disconnect switch will permanently delete the bot. To only
temporarily deactivate it, use the |w@services|n command instead.
temporarily deactivate it, use the |wservices|n command instead.
Provide an optional bot class path to use a custom bot.
"""
key = "@irc2chan"
key = "irc2chan"
switch_options = ("delete", "remove", "disconnect", "list", "ssl")
locks = "cmd:serversetting(IRC_ENABLED) and pperm(Developer)"
help_category = "Comms"
@ -869,7 +872,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
if 'list' in self.switches:
# show all connections
self.msg(_list_bots())
self.msg(_list_bots(self))
return
if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches:
@ -887,7 +890,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
return
if not self.args or not self.rhs:
string = "Usage: @irc2chan[/switches] <evennia_channel> =" \
string = "Usage: irc2chan[/switches] <evennia_channel> =" \
" <ircnetwork> <port> <#irchannel> <botname>[:typeclass]"
self.msg(string)
return
@ -920,9 +923,8 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
self.msg("Account '%s' already exists and is not a bot." % botname)
return
else:
password = hashlib.md5(str(time.time())).hexdigest()[:11]
try:
bot = create.create_account(botname, None, password, typeclass=botclass)
bot = create.create_account(botname, None, None, typeclass=botclass)
except Exception as err:
self.msg("|rError, could not create the bot:|n '%s'." % err)
return
@ -939,7 +941,7 @@ class CmdIRCStatus(COMMAND_DEFAULT_CLASS):
ircstatus [#dbref ping||nicklist||reconnect]
If not given arguments, will return a list of all bots (like
@irc2chan/list). The 'ping' argument will ping the IRC network to
irc2chan/list). The 'ping' argument will ping the IRC network to
see if the connection is still responsive. The 'nicklist' argument
(aliases are 'who' and 'users') will return a list of users on the
remote IRC channel. Finally, 'reconnect' will force the client to
@ -949,7 +951,7 @@ class CmdIRCStatus(COMMAND_DEFAULT_CLASS):
messages sent to either channel will be lost.
"""
key = "@ircstatus"
key = "ircstatus"
locks = "cmd:serversetting(IRC_ENABLED) and perm(ircstatus) or perm(Builder))"
help_category = "Comms"
@ -957,12 +959,12 @@ class CmdIRCStatus(COMMAND_DEFAULT_CLASS):
"""Handles the functioning of the command."""
if not self.args:
self.msg(_list_bots())
self.msg(_list_bots(self))
return
# should always be on the form botname option
args = self.args.split()
if len(args) != 2:
self.msg("Usage: @ircstatus [#dbref ping||nicklist||reconnect]")
self.msg("Usage: ircstatus [#dbref ping||nicklist||reconnect]")
return
botname, option = args
if option not in ("ping", "users", "reconnect", "nicklist", "who"):
@ -972,7 +974,7 @@ 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
@ -1002,7 +1004,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
link an evennia channel to an external RSS feed
Usage:
@rss2chan[/switches] <evennia_channel> = <rss_url>
rss2chan[/switches] <evennia_channel> = <rss_url>
Switches:
/disconnect - this will stop the feed and remove the connection to the
@ -1011,7 +1013,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
/list - show all rss->evennia mappings
Example:
@rss2chan rsschan = http://code.google.com/feeds/p/evennia/updates/basic
rss2chan rsschan = http://code.google.com/feeds/p/evennia/updates/basic
This creates an RSS reader that connects to a given RSS feed url. Updates
will be echoed as a title and news link to the given channel. The rate of
@ -1022,7 +1024,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
to identify the connection uniquely.
"""
key = "@rss2chan"
key = "rss2chan"
switch_options = ("disconnect", "remove", "list")
locks = "cmd:serversetting(RSS_ENABLED) and pperm(Developer)"
help_category = "Comms"
@ -1048,9 +1050,8 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
# show all connections
rssbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")]
if rssbots:
from evennia.utils.evtable import EvTable
table = EvTable("|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)
self.msg(table)
@ -1072,14 +1073,13 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
return
if not self.args or not self.rhs:
string = "Usage: @rss2chan[/switches] <evennia_channel> = <rss url>"
string = "Usage: rss2chan[/switches] <evennia_channel> = <rss url>"
self.msg(string)
return
channel = self.lhs
url = self.rhs
botname = "rssbot-%s" % url
# create a new bot
bot = AccountDB.objects.filter(username__iexact=botname)
if bot:
# re-use existing bot
@ -1088,6 +1088,97 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
self.msg("Account '%s' already exists and is not a bot." % botname)
return
else:
# create a new bot
bot = create.create_account(botname, None, None, typeclass=bots.RSSBot)
bot.start(ev_channel=channel, rss_url=url, rss_rate=10)
self.msg("RSS reporter created. Fetching RSS.")
class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
"""
Link an Evennia channel to an exteral Grapevine channel
Usage:
grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>
grapevine2chan/disconnect <connection #id>
Switches:
/list - (or no switch): show existing grapevine <-> Evennia
mappings and available grapevine chans
/remove - alias to disconnect
/delete - alias to disconnect
Example:
grapevine2chan mygrapevine = gossip
This creates a link between an in-game Evennia channel and an external
Grapevine channel. The game must be registered with the Grapevine network
(register at https://grapevine.haus) and the GRAPEVINE_* auth information
must be added to game settings.
"""
key = "grapevine2chan"
switch_options = ("disconnect", "remove", "delete", "list")
locks = "cmd:serversetting(GRAPEVINE_ENABLED) and pperm(Developer)"
help_category = "Comms"
def func(self):
"""Setup the Grapevine channel mapping"""
if not settings.GRAPEVINE_ENABLED:
self.msg("Set GRAPEVINE_ENABLED=True in settings to enable.")
return
if "list" in self.switches:
# show all connections
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)
for gwbot in gwbots:
table.add_row(gwbot.id, gwbot.db.ev_channel, gwbot.db.grapevine_channel)
self.msg(table)
else:
self.msg("No grapevine bots found.")
return
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)
if not matches:
# try dbref match
matches = AccountDB.objects.filter(db_is_bot=True, id=self.args.lstrip("#"))
if matches:
matches[0].delete()
self.msg("Grapevine connection destroyed.")
else:
self.msg("Grapevine connection/bot could not be removed, does it exist?")
return
if not self.args or not self.rhs:
string = "Usage: grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>"
self.msg(string)
return
channel = self.lhs
grapevine_channel = self.rhs
botname = "grapewinebot-%s-%s" % (channel, grapevine_channel)
bot = AccountDB.objects.filter(username__iexact=botname)
if bot:
# re-use existing bot
bot = bot[0]
if not bot.is_bot:
self.msg("Account '%s' already exists and is not a bot." % botname)
return
else:
self.msg("Reusing bot '%s' (%s)" % (botname, bot.dbref))
else:
# create a new bot
bot = create.create_account(botname, None, None, typeclass=bots.GrapevineBot)
bot.start(ev_channel=channel, grapevine_channel=grapevine_channel)
self.msg(f"Grapevine connection created {channel} <-> {grapevine_channel}.")

View file

@ -96,10 +96,10 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
Examples:
nick hi = say Hello, I'm Sarah!
nick/object tom = the tall man
nick build $1 $2 = @create/drop $1;$2
nick tell $1 $2=@page $1=$2
nick tm?$1=@page tallman=$1
nick tm\=$1=@page tallman=$1
nick build $1 $2 = create/drop $1;$2
nick tell $1 $2=page $1=$2
nick tm?$1=page tallman=$1
nick tm\=$1=page tallman=$1
A 'nick' is a personal string replacement. Use $1, $2, ... to catch arguments.
Put the last $-marker without an ending space to catch all remaining text. You
@ -113,7 +113,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
Note that no objects are actually renamed or changed by this command - your nicks
are only available to you. If you want to permanently add keywords to an object
for everyone to use, you need build privileges and the @alias command.
for everyone to use, you need build privileges and the alias command.
"""
key = "nick"
@ -152,12 +152,12 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
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", "@nicks"):
if 'list' in switches or self.cmdstring in ("nicks",):
if not nicklist:
string = "|wNo nicks defined.|n"
else:
table = evtable.EvTable("#", "Type", "Nick match", "Replacement")
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))
@ -338,7 +338,7 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
if not items:
string = "You are not carrying anything."
else:
table = evtable.EvTable(border="header")
table = self.styled_table(border="header")
for item in items:
table.add_row("|C%s|n" % item.name, item.db.desc or "")
string = "|wYou are carrying:\n%s" % table

View file

@ -295,7 +295,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
Edit the help database.
Usage:
@help[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text>]
help[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text>]
Switches:
edit - open a line editor to edit the topic's help text.
@ -305,10 +305,10 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
delete - remove help topic.
Examples:
@sethelp throw = This throws something at ...
@sethelp/append pickpocketing,Thievery = This steals ...
@sethelp/replace pickpocketing, ,attr(is_thief) = This steals ...
@sethelp/edit thievery
sethelp throw = This throws something at ...
sethelp/append pickpocketing,Thievery = This steals ...
sethelp/replace pickpocketing, ,attr(is_thief) = This steals ...
sethelp/edit thievery
This command manipulates the help database. A help entry can be created,
appended/merged to and deleted. If you don't assign a category, the
@ -316,7 +316,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
is to let everyone read the help file.
"""
key = "@sethelp"
key = "sethelp"
switch_options = ("edit", "replace", "append", "extend", "delete")
locks = "cmd:perm(Helper)"
help_category = "Building"
@ -328,7 +328,7 @@ 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)

View file

@ -30,7 +30,7 @@ class MuxCommand(Command):
We just show it here for completeness - we
are satisfied using the default check in Command.
"""
return super(MuxCommand, self).has_perm(srcobj)
return super().has_perm(srcobj)
def at_pre_cmd(self):
"""
@ -200,6 +200,13 @@ 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())
string = f"""
Command {self} has no defined `func()` - showing on-command variables: No child func() defined for {self} - available variables:
{variables}
"""
self.caller.msg(string)
return
# a simple test command to show the available properties
string = "-" * 50
string += "\n|w%s|n - Command variables from evennia:\n" % self.key

View file

@ -20,6 +20,7 @@ the line is just added to the editor buffer).
from evennia.comms.models import ChannelDB
from evennia.utils import create
from evennia.utils.utils import at_search_result
# The command keys the engine is calling
# (the actual names all start with __)
@ -76,57 +77,30 @@ class SystemMultimatch(COMMAND_DEFAULT_CLASS):
The cmdhandler adds a special attribute 'matches' to this
system command.
matches = [(candidate, cmd) , (candidate, cmd), ...],
matches = [(cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname) , (cmdname, ...), ...]
Here, `cmdname` is the command's name and `args` the rest of the incoming string,
without said command name. `cmdobj` is the Command instance, the cmdlen is
the same as len(cmdname) and mratio is a measure of how big a part of the
full input string the cmdname takes up - an exact match would be 1.0. Finally,
the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping.
where candidate is an instance of evennia.commands.cmdparser.CommandCandidate
and cmd is an an instantiated Command object matching the candidate.
"""
key = CMD_MULTIMATCH
locks = "cmd:all()"
def format_multimatches(self, caller, matches):
"""
Format multiple command matches to a useful error.
This is copied directly from the default method in
evennia.commands.cmdhandler.
"""
string = "There were multiple matches:"
for num, match in enumerate(matches):
# each match is a tuple (candidate, cmd)
candidate, cmd = match
is_channel = hasattr(cmd, "is_channel") and cmd.is_channel
if is_channel:
is_channel = " (channel)"
else:
is_channel = ""
is_exit = hasattr(cmd, "is_exit") and cmd.is_exit
if is_exit and cmd.destination:
is_exit = " (exit to %s)" % cmd.destination
else:
is_exit = ""
id1 = ""
id2 = ""
if not (is_channel or is_exit) and (hasattr(cmd, 'obj') and cmd.obj != caller):
# the command is defined on some other object
id1 = "%s-" % cmd.obj.name
id2 = " (%s-%s)" % (num + 1, candidate.cmdname)
else:
id1 = "%s-" % (num + 1)
id2 = ""
string += "\n %s%s%s%s%s" % (id1, candidate.cmdname, id2, is_channel, is_exit)
return string
def func(self):
"""
argument to cmd is a comma-separated string of
all the clashing matches.
Handle multiple-matches by using the at_search_result default handler.
"""
string = self.format_multimatches(self.caller, self.matches)
self.msg(string)
# this was set by the cmdparser and is a tuple
# (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname). See
# 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])
# Command called when the command given at the command line

View file

@ -3,7 +3,7 @@
System commands
"""
from __future__ import division
import traceback
import os
@ -19,10 +19,10 @@ from evennia.server.sessionhandler import SESSIONS
from evennia.scripts.models import ScriptDB
from evennia.objects.models import ObjectDB
from evennia.accounts.models import AccountDB
from evennia.utils import logger, utils, gametime, create
from evennia.utils import logger, utils, gametime, create, search
from evennia.utils.eveditor import EvEditor
from evennia.utils.evtable import EvTable
from evennia.utils.utils import crop, class_from_module, to_unicode
from evennia.utils.utils import crop, class_from_module
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -41,13 +41,14 @@ class CmdReload(COMMAND_DEFAULT_CLASS):
reload the server
Usage:
@reload [reason]
reload [reason]
This restarts the server. The Portal is not
affected. Non-persistent scripts will survive a @reload (use
@reset to purge) and at_reload() hooks will be called.
affected. Non-persistent scripts will survive a reload (use
reset to purge) and at_reload() hooks will be called.
"""
key = "@reload"
key = "reload"
aliases = ['restart']
locks = "cmd:perm(reload) or perm(Developer)"
help_category = "System"
@ -67,23 +68,23 @@ class CmdReset(COMMAND_DEFAULT_CLASS):
reset and reboot the server
Usage:
@reset
reset
Notes:
For normal updating you are recommended to use @reload rather
than this command. Use @shutdown for a complete stop of
For normal updating you are recommended to use reload rather
than this command. Use shutdown for a complete stop of
everything.
This emulates a cold reboot of the Server component of Evennia.
The difference to @shutdown is that the Server will auto-reboot
The difference to shutdown is that the Server will auto-reboot
and that it does not affect the Portal, so no users will be
disconnected. Contrary to @reload however, all shutdown hooks will
disconnected. Contrary to reload however, all shutdown hooks will
be called and any non-database saved scripts, ndb-attributes,
cmdsets etc will be wiped.
"""
key = "@reset"
aliases = ['@reboot']
key = "reset"
aliases = ['reboot']
locks = "cmd:perm(reload) or perm(Developer)"
help_category = "System"
@ -101,11 +102,11 @@ class CmdShutdown(COMMAND_DEFAULT_CLASS):
stop the server completely
Usage:
@shutdown [announcement]
shutdown [announcement]
Gracefully shut down both Server and Portal.
"""
key = "@shutdown"
key = "shutdown"
locks = "cmd:perm(shutdown) or perm(Developer)"
help_category = "System"
@ -132,11 +133,13 @@ def _py_code(caller, buf):
Execute the buffer.
"""
measure_time = caller.db._py_measure_time
client_raw = caller.db._py_clientraw
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)
return True
@ -147,15 +150,16 @@ def _py_quit(caller):
def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
show_input=True):
client_raw=False, show_input=True):
"""
Run code and try to display information to the caller.
Args:
caller (Object): the caller.
pycode (str): the Python code to run.
m_time (bool, optional): should we measure the time of execution?
show_input (bookl, optional): should we display the input?
caller (Object): The caller.
pycode (str): The Python code to run.
measure_time (bool, optional): Should we measure the time of execution?
client_raw (bool, optional): Should we turn off all client-specific escaping?
show_input (bookl, optional): Should we display the input?
"""
# Try to retrieve the session
@ -210,9 +214,11 @@ def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
for session in sessions:
try:
caller.msg(ret, session=session, options={"raw": True})
caller.msg(ret, session=session, options={"raw": True,
"client_raw": client_raw})
except TypeError:
caller.msg(ret, options={"raw": True})
caller.msg(ret, options={"raw": True,
"client_raw": client_raw})
class CmdPy(COMMAND_DEFAULT_CLASS):
@ -220,19 +226,22 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
execute a snippet of python code
Usage:
@py <cmd>
@py/edit
py <cmd>
py/edit
Switches:
time - output an approximate execution time for <cmd>
edit - open a code editor for multi-line code experimentation
clientraw - turn off all client-specific escaping. Note that this may
lead to different output depending on prototocol (such as angular brackets
being parsed as HTML in the webclient but not in telnet clients)
Separate multiple commands by ';' or open the editor using the
/edit switch. A few variables are made available for convenience
in order to offer access to the system (you can import more at
execution time).
Available variables in @py environment:
Available variables in py environment:
self, me : caller
here : caller.location
ev : the evennia API
@ -240,16 +249,16 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
You can explore The evennia API from inside the game by calling
the `__doc__` property on entities:
@py evennia.__doc__
@py evennia.managers.__doc__
py evennia.__doc__
py evennia.managers.__doc__
|rNote: In the wrong hands this command is a severe security risk.
It should only be accessible by trusted server admins/superusers.|n
"""
key = "@py"
key = "py"
aliases = ["!"]
switch_options = ("time", "edit")
switch_options = ("time", "edit", "clientraw")
locks = "cmd:perm(py) or perm(Developer)"
help_category = "System"
@ -261,17 +270,19 @@ 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)
return
if not pycode:
string = "Usage: @py <code>"
string = "Usage: py <code>"
self.msg(string)
return
_run_code_snippet(caller, self.args, measure_time="time" 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.
@ -315,7 +326,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
list and manage all running scripts
Usage:
@scripts[/switches] [#dbref, key, script.path or <obj>]
scripts[/switches] [#dbref, key, script.path or <obj>]
Switches:
start - start a script (must supply a script path)
@ -329,10 +340,10 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
or #dbref. For using the /stop switch, a unique script #dbref is
required since whole classes of scripts often have the same name.
Use @script for managing commands on objects.
Use script for managing commands on objects.
"""
key = "@scripts"
aliases = ["@globalscript", "@listscripts"]
key = "scripts"
aliases = ["globalscript", "listscripts"]
switch_options = ("start", "stop", "kill", "validate")
locks = "cmd:perm(listscripts) or perm(Admin)"
help_category = "System"
@ -408,14 +419,14 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
statistics on objects in the database
Usage:
@objects [<nr>]
objects [<nr>]
Gives statictics on objects in database as well as
a list of <nr> latest objects in database. If not
given, <nr> defaults to 10.
"""
key = "@objects"
aliases = ["@listobjects", "@listobjs", '@stats', '@db']
key = "objects"
aliases = ["listobjects", "listobjs", 'stats', 'db']
locks = "cmd:perm(listobjects) or perm(Builder)"
help_category = "System"
@ -425,24 +436,30 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
caller = self.caller
nlim = int(self.args) if self.args and self.args.isdigit() else 10
nobjs = ObjectDB.objects.count()
base_char_typeclass = settings.BASE_CHARACTER_TYPECLASS
nchars = ObjectDB.objects.filter(db_typeclass_path=base_char_typeclass).count()
nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(
db_typeclass_path=base_char_typeclass).count()
nexits = ObjectDB.objects.filter(db_location__isnull=False, db_destination__isnull=False).count()
Character = class_from_module(settings.BASE_CHARACTER_TYPECLASS)
nchars = Character.objects.all_family().count()
Room = class_from_module(settings.BASE_ROOM_TYPECLASS)
nrooms = Room.objects.all_family().count()
Exit = class_from_module(settings.BASE_EXIT_TYPECLASS)
nexits = Exit.objects.all_family().count()
nother = nobjs - nchars - nrooms - nexits
nobjs = nobjs or 1 # fix zero-div error with empty database
# total object sum table
totaltable = EvTable("|wtype|n", "|wcomment|n", "|wcount|n", "|w%%|n", border="table", align="l")
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)", nchars, "%.2f" % ((float(nchars) / nobjs) * 100))
totaltable.add_row("Rooms", "(location=None)", nrooms, "%.2f" % ((float(nrooms) / nobjs) * 100))
totaltable.add_row("Exits", "(destination!=None)", nexits, "%.2f" % ((float(nexits) / nobjs) * 100))
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.add_row("Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100))
# typeclass table
typetable = EvTable("|wtypeclass|n", "|wcount|n", "|w%%|n", border="table", 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():
@ -450,7 +467,8 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
# last N table
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim):]
latesttable = EvTable("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table")
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),
@ -464,17 +482,22 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
class CmdAccounts(COMMAND_DEFAULT_CLASS):
"""
list all registered accounts
Manage registered accounts
Usage:
@accounts [nr]
accounts [nr]
accounts/delete <name or #id> [: reason]
Lists statistics about the Accounts registered with the game.
Switches:
delete - delete an account from the server
By default, lists statistics about the Accounts registered with the game.
It will list the <nr> amount of latest registered accounts
If not given, <nr> defaults to 10.
"""
key = "@accounts"
aliases = ["@listaccounts"]
key = "accounts"
aliases = ["account", "listaccounts"]
switch_options = ("delete", )
locks = "cmd:perm(listaccounts) or perm(Admin)"
help_category = "System"
@ -482,6 +505,56 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
"""List the accounts"""
caller = self.caller
args = self.args
if "delete" in self.switches:
account = getattr(caller, "account")
if not account or not account.check_permstring("Developer"):
caller.msg("You are not allowed to delete accounts.")
return
if not args:
caller.msg("Usage: accounts/delete <name or #id> [: reason]")
return
reason = ""
if ":" in args:
args, reason = [arg.strip() for arg in args.split(":", 1)]
# We use account_search since we want to be sure to find also accounts
# that lack characters.
accounts = search.account_search(args)
if not accounts:
self.msg("Could not find an account by that name.")
return
if len(accounts) > 1:
string = "There were multiple matches:\n"
string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts)
self.msg(string)
return
account = accounts.first()
if not account.access(caller, "delete"):
self.msg("You don't have the permissions to delete that account.")
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'):
caller.msg("Canceled deletion.")
return
# Boot the account then delete it.
self.msg("Informing and disconnecting account ...")
string = "\nYour account '%s' is being *permanently* deleted.\n" % username
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))
account.delete()
self.msg("Account %s was successfully deleted." % username)
return
# No switches, default to displaying a list of accounts.
if self.args and self.args.isdigit():
nlim = int(self.args)
else:
@ -491,12 +564,12 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
# typeclass table
dbtotals = AccountDB.objects.object_totals()
typetable = EvTable("|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 = EvTable("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", border="cells", align="l")
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)
@ -510,7 +583,7 @@ class CmdService(COMMAND_DEFAULT_CLASS):
manage system services
Usage:
@service[/switch] <service>
service[/switch] <service>
Switches:
list - shows all available services (default)
@ -525,8 +598,8 @@ class CmdService(COMMAND_DEFAULT_CLASS):
in the list.
"""
key = "@service"
aliases = ["@services"]
key = "service"
aliases = ["services"]
switch_options = ("list", "start", "stop", "delete")
locks = "cmd:perm(service) or perm(Developer)"
help_category = "System"
@ -538,7 +611,7 @@ class CmdService(COMMAND_DEFAULT_CLASS):
switches = self.switches
if switches and switches[0] not in ("list", "start", "stop", "delete"):
caller.msg("Usage: @service/<list|start|stop|delete> [servicename]")
caller.msg("Usage: service/<list|start|stop|delete> [servicename]")
return
# get all services
@ -547,10 +620,10 @@ 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 = EvTable("|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(unicode(table))
caller.msg(str(table))
return
# Get the service to start / stop
@ -559,7 +632,7 @@ class CmdService(COMMAND_DEFAULT_CLASS):
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 += 'See service/list for valid service name (enter the full name exactly).'
caller.msg(string)
return
@ -604,13 +677,13 @@ class CmdAbout(COMMAND_DEFAULT_CLASS):
show Evennia info
Usage:
@about
about
Display info about the game engine.
"""
key = "@about"
aliases = "@version"
key = "about"
aliases = "version"
locks = "cmd:all()"
help_category = "System"
@ -645,31 +718,32 @@ class CmdTime(COMMAND_DEFAULT_CLASS):
show server time statistics
Usage:
@time
time
List Server time statistics such as uptime
and the current time stamp.
"""
key = "@time"
aliases = "@uptime"
key = "time"
aliases = "uptime"
locks = "cmd:perm(time) or perm(Player)"
help_category = "System"
def func(self):
"""Show server time data in a table."""
table1 = EvTable("|wServer time", "", align="l", width=78)
table1 = self.styled_table("|wServer time", "", align="l", width=78)
table1.add_row("Current uptime", utils.time_format(gametime.uptime(), 3))
table1.add_row("Portal uptime", utils.time_format(gametime.portal_uptime(), 3))
table1.add_row("Total runtime", utils.time_format(gametime.runtime(), 2))
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 = EvTable("|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.reformat_column(0, width=30)
self.caller.msg(unicode(table1) + "\n" + unicode(table2))
self.caller.msg(str(table1) + "\n" + str(table2))
class CmdServerLoad(COMMAND_DEFAULT_CLASS):
@ -677,7 +751,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
show server load and memory statistics
Usage:
@server[/mem]
server[/mem]
Switches:
mem - return only a string of the current memory usage
@ -708,8 +782,8 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
the released memory will instead be re-used by the program.
"""
key = "@server"
aliases = ["@serverload", "@serverprocess"]
key = "server"
aliases = ["serverload", "serverprocess"]
switch_options = ("mem", "flushmem")
locks = "cmd:perm(list) or perm(Developer)"
help_category = "System"
@ -757,7 +831,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
self.caller.msg(string % (rmem, pmem))
return
# Display table
loadtable = EvTable("property", "statistic", align="l")
loadtable = self.styled_table("property", "statistic", align="l")
loadtable.add_row("Total CPU load", "%g %%" % loadavg)
loadtable.add_row("Total computer memory usage", "%g MB (%g%%)" % (rmem, pmem))
loadtable.add_row("Process ID", "%g" % pid),
@ -783,7 +857,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
self.caller.msg(string % (rmem, pmem, vmem))
return
loadtable = EvTable("property", "statistic", align="l")
loadtable = self.styled_table("property", "statistic", align="l")
loadtable.add_row("Server load (1 min)", "%g" % loadavg)
loadtable.add_row("Process ID", "%g" % pid),
loadtable.add_row("Memory usage", "%g MB (%g%%)" % (rmem, pmem))
@ -808,7 +882,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
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)
memtable = EvTable("entity name", "number", "idmapper %", align="l")
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))
@ -823,14 +897,14 @@ class CmdTickers(COMMAND_DEFAULT_CLASS):
View running tickers
Usage:
@tickers
tickers
Note: Tickers are created, stopped and manipulated in Python code
using the TickerHandler. This is merely a convenience function for
inspecting the current status.
"""
key = "@tickers"
key = "tickers"
help_category = "System"
locks = "cmd:perm(tickers) or perm(Builder)"
@ -840,7 +914,7 @@ class CmdTickers(COMMAND_DEFAULT_CLASS):
if not all_subs:
self.caller.msg("No tickers are currently active.")
return
table = EvTable("interval (s)", "object", "path/methodname", "idstring", "db")
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]",
@ -848,4 +922,4 @@ class CmdTickers(COMMAND_DEFAULT_CLASS):
sub[1] if sub[1] else sub[2],
sub[4] or "[Unset]",
"*" if sub[5] else "-")
self.caller.msg("|wActive tickers|n:\n" + unicode(table))
self.caller.msg("|wActive tickers|n:\n" + str(table))

View file

@ -11,19 +11,23 @@ main test suite started with
> python game/manage.py test.
"""
import re
import types
import datetime
from anything import Anything
from django.conf import settings
from mock import Mock, mock
from evennia import DefaultRoom, DefaultExit
from evennia.commands.default.cmdset_character import CharacterCmdSet
from evennia.utils.test_resources import EvenniaTest
from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms, unloggedin
from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms, unloggedin, syscommands
from evennia.commands.cmdparser import build_matches
from evennia.commands.default.muxcommand import MuxCommand
from evennia.commands.command import Command, InterruptCommand
from evennia.commands import cmdparser
from evennia.commands.cmdset import CmdSet
from evennia.utils import ansi, utils, gametime
from evennia.server.sessionhandler import SESSIONS
from evennia import search_object
@ -46,7 +50,7 @@ class CommandTest(EvenniaTest):
Tests a command
"""
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None,
receiver=None, cmdstring=None, obj=None, inputs=None):
receiver=None, cmdstring=None, obj=None, inputs=None, raw_string=None):
"""
Test a command by assigning all the needed
properties to cmdobj and running
@ -71,7 +75,7 @@ class CommandTest(EvenniaTest):
cmdobj.cmdset = cmdset
cmdobj.session = SESSIONS.session_from_sessid(1)
cmdobj.account = self.account
cmdobj.raw_string = cmdobj.key + " " + args
cmdobj.raw_string = raw_string if raw_string is not None else cmdobj.key + " " + args
cmdobj.obj = obj or (caller if caller else self.char1)
# test
old_msg = receiver.msg
@ -93,10 +97,10 @@ class CommandTest(EvenniaTest):
try:
ret.send(inp)
except TypeError:
ret.next()
next(ret)
ret = ret.send(inp)
else:
ret.next()
next(ret)
except StopIteration:
break
@ -107,7 +111,7 @@ class CommandTest(EvenniaTest):
pass
# clean out evtable sugar. We only operate on text-type
stored_msg = [args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs, force_string=True))
stored_msg = [args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
for name, args, kwargs in receiver.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]
@ -137,7 +141,8 @@ class CommandTest(EvenniaTest):
class TestGeneral(CommandTest):
def test_look(self):
self.call(general.CmdLook(), "here", "Room(#1)\nroom_desc")
rid = self.room1.id
self.call(general.CmdLook(), "here", "Room(#{})\nroom_desc".format(rid))
def test_home(self):
self.call(general.CmdHome(), "", "You are already home")
@ -155,10 +160,10 @@ class TestGeneral(CommandTest):
"Account-nick 'testalias' mapped to 'testaliasedstring2'.")
self.call(general.CmdNick(), "/object testalias = testaliasedstring3",
"Object-nick 'testalias' mapped to 'testaliasedstring3'.")
self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias"))
self.assertEqual(u"testaliasedstring2", self.char1.nicks.get("testalias", category="account"))
self.assertEqual("testaliasedstring1", self.char1.nicks.get("testalias"))
self.assertEqual("testaliasedstring2", self.char1.nicks.get("testalias", category="account"))
self.assertEqual(None, self.char1.account.nicks.get("testalias", category="account"))
self.assertEqual(u"testaliasedstring3", self.char1.nicks.get("testalias", category="object"))
self.assertEqual("testaliasedstring3", self.char1.nicks.get("testalias", category="object"))
def test_get_and_drop(self):
self.call(general.CmdGet(), "Obj", "You pick up Obj.")
@ -215,6 +220,7 @@ class TestSystem(CommandTest):
# we are not testing CmdReload, CmdReset and CmdShutdown, CmdService or CmdTime
# since the server is not running during these tests.
self.call(system.CmdPy(), "1+2", ">>> 1+2|3")
self.call(system.CmdPy(), "/clientraw 1+2", ">>> 1+2|3")
def test_scripts(self):
self.call(system.CmdScripts(), "", "dbref ")
@ -243,6 +249,11 @@ class TestAdmin(CommandTest):
def test_ban(self):
self.call(admin.CmdBan(), "Char", "Name-Ban char was added.")
def test_force(self):
cid = self.char2.id
self.call(admin.CmdForce(), "Char2=say test",
'Char2(#{}) says, "test"|You have forced Char2 to: say test'.format(cid))
class TestAccount(CommandTest):
@ -279,7 +290,33 @@ class TestAccount(CommandTest):
def test_char_create(self):
self.call(account.CmdCharCreate(), "Test1=Test char",
"Created new character Test1. Use @ic Test1 to enter the game", caller=self.account)
"Created new character Test1. Use ic Test1 to enter the game", caller=self.account)
def test_char_delete(self):
# Chardelete requires user input; this test is mainly to confirm
# whether permissions are being checked
# Add char to account playable characters
self.account.db._playable_characters.append(self.char1)
# Try deleting as Developer
self.call(account.CmdCharDelete(), "Char", "This will permanently destroy 'Char'. This cannot be undone. Continue yes/[no]?", caller=self.account)
# Downgrade permissions on account
self.account.permissions.add('Player')
self.account.permissions.remove('Developer')
# Set lock on character object to prevent deletion
self.char1.locks.add('delete:none()')
# Try deleting as Player
self.call(account.CmdCharDelete(), "Char", "You do not have permission to delete this character.", caller=self.account)
# Set lock on character object to allow self-delete
self.char1.locks.add('delete:pid(%i)' % self.account.id)
# Try deleting as Player again
self.call(account.CmdCharDelete(), "Char", "This will permanently destroy 'Char'. This cannot be undone. Continue yes/[no]?", caller=self.account)
def test_quell(self):
self.call(account.CmdQuell(), "", "Quelling to current puppet's permissions (developer).", caller=self.account)
@ -290,108 +327,323 @@ class TestBuilding(CommandTest):
name = settings.BASE_OBJECT_TYPECLASS.rsplit('.', 1)[1]
self.call(building.CmdCreate(), "/d TestObj1", # /d switch is abbreviated form of /drop
"You create a new %s: TestObj1." % name)
self.call(building.CmdCreate(), "", "Usage: ")
self.call(building.CmdCreate(), "TestObj1;foo;bar",
"You create a new %s: TestObj1 (aliases: foo, bar)." % name)
def test_examine(self):
self.call(building.CmdExamine(), "", "Name/key: Room")
self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
self.call(building.CmdExamine(), "*TestAccount", "Name/key: TestAccount")
self.char1.db.test = "testval"
self.call(building.CmdExamine(), "self/test", "Persistent attributes:\n test = testval")
self.call(building.CmdExamine(), "NotFound", "Could not find 'NotFound'.")
self.call(building.CmdExamine(), "out", "Name/key: out")
self.room1.scripts.add(self.script.__class__)
self.call(building.CmdExamine(), "")
self.account.scripts.add(self.script.__class__)
self.call(building.CmdExamine(), "*TestAccount")
def test_set_obj_alias(self):
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj(#4)")
self.call(building.CmdSetObjAlias(), "Obj = TestObj1b", "Alias(es) for 'Obj(#4)' set to 'testobj1b'.")
oid = self.obj1.id
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj")
self.call(building.CmdSetObjAlias(), "Obj = TestObj1b",
"Alias(es) for 'Obj(#{})' set to 'testobj1b'.".format(oid))
self.call(building.CmdSetObjAlias(), "", "Usage: ")
self.call(building.CmdSetObjAlias(), "NotFound =", "Could not find 'NotFound'.")
self.call(building.CmdSetObjAlias(), "Obj",
"Aliases for Obj(#{}): 'testobj1b'".format(oid))
self.call(building.CmdSetObjAlias(), "Obj2 =", "Cleared aliases from Obj2")
self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.")
def test_copy(self):
self.call(building.CmdCopy(), "Obj = TestObj2;TestObj2b, TestObj3;TestObj3b",
"Copied Obj to 'TestObj3' (aliases: ['TestObj3b']")
self.call(building.CmdCopy(), "", "Usage: ")
self.call(building.CmdCopy(), "Obj", "Identical copy of Obj, named 'Obj_copy' was created.")
self.call(building.CmdCopy(), "NotFound = Foo", "Could not find 'NotFound'.")
def test_attribute_commands(self):
self.call(building.CmdSetAttribute(), "", "Usage: ")
self.call(building.CmdSetAttribute(), "Obj/test1=\"value1\"", "Created attribute Obj/test1 = 'value1'")
self.call(building.CmdSetAttribute(), "Obj2/test2=\"value2\"", "Created attribute Obj2/test2 = 'value2'")
self.call(building.CmdSetAttribute(), "Obj2/test2", "Attribute Obj2/test2 = value2")
self.call(building.CmdSetAttribute(), "Obj2/NotFound", "Obj2 has no attribute 'notfound'.")
with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed:
self.call(building.CmdSetAttribute(), "/edit Obj2/test3")
mock_ed.assert_called_with(self.char1, Anything, Anything, key='Obj2/test3')
self.call(building.CmdSetAttribute(), "Obj2/test3=\"value3\"", "Created attribute Obj2/test3 = 'value3'")
self.call(building.CmdSetAttribute(), "Obj2/test3 = ", "Deleted attribute 'test3' (= True) from Obj2.")
self.call(building.CmdCpAttr(), "/copy Obj2/test2 = Obj2/test3",
"cpattr: Extra switch \"/copy\" ignored.|\nCopied Obj2.test2 -> Obj2.test3. "
"(value: 'value2')")
self.call(building.CmdMvAttr(), "", "Usage: ")
self.call(building.CmdMvAttr(), "Obj2/test2 = Obj/test3", "Moved Obj2.test2 -> Obj.test3")
self.call(building.CmdCpAttr(), "", "Usage: ")
self.call(building.CmdCpAttr(), "Obj/test1 = Obj2/test3", "Copied Obj.test1 -> Obj2.test3")
self.call(building.CmdWipe(), "", "Usage: ")
self.call(building.CmdWipe(), "Obj2/test2/test3", "Wiped attributes test2,test3 on Obj2.")
self.call(building.CmdWipe(), "Obj2", "Wiped all attributes on Obj2.")
def test_name(self):
self.call(building.CmdName(), "", "Usage: ")
self.call(building.CmdName(), "Obj2=Obj3", "Object's name changed to 'Obj3'.")
self.call(building.CmdName(), "*TestAccount=TestAccountRenamed",
"Account's name changed to 'TestAccountRenamed'.")
self.call(building.CmdName(), "*NotFound=TestAccountRenamed",
"Could not find '*NotFound'")
self.call(building.CmdName(), "Obj3=Obj4;foo;bar",
"Object's name changed to 'Obj4' (foo, bar).")
self.call(building.CmdName(), "Obj4=", "No names or aliases defined!")
def test_desc(self):
self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#5).")
oid = self.obj2.id
self.call(building.CmdDesc(), "Obj2=TestDesc",
"The description was set on Obj2(#{}).".format(oid))
self.call(building.CmdDesc(), "", "Usage: ")
with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed:
self.call(building.CmdDesc(), "/edit")
mock_ed.assert_called_with(self.char1, key='desc',
loadfunc=building._desc_load,
quitfunc=building._desc_quit,
savefunc=building._desc_save,
persistent=True)
def test_empty_desc(self):
"""
empty desc sets desc as ''
"""
oid = self.obj2.id
o2d = self.obj2.db.desc
r1d = self.room1.db.desc
self.call(building.CmdDesc(), "Obj2=", "The description was set on Obj2(#5).")
self.call(building.CmdDesc(), "Obj2=",
"The description was set on Obj2(#{}).".format(oid))
assert self.obj2.db.desc == '' and self.obj2.db.desc != o2d
assert self.room1.db.desc == r1d
def test_desc_default_to_room(self):
"""no rhs changes room's desc"""
rid = self.room1.id
o2d = self.obj2.db.desc
r1d = self.room1.db.desc
self.call(building.CmdDesc(), "Obj2", "The description was set on Room(#1).")
self.call(building.CmdDesc(), "Obj2",
"The description was set on Room(#{}).".format(rid))
assert self.obj2.db.desc == o2d
assert self.room1.db.desc == 'Obj2' and self.room1.db.desc != r1d
def test_wipe(self):
def test_destroy(self):
confirm = building.CmdDestroy.confirm
building.CmdDestroy.confirm = False
self.call(building.CmdDestroy(), "", "Usage: ")
self.call(building.CmdDestroy(), "Obj", "Obj was destroyed.")
self.call(building.CmdDestroy(), "Obj", "Obj2 was destroyed.")
self.call(building.CmdDestroy(), "Obj", "Could not find 'Obj'.| (Objects to destroy "
"must either be local or specified with a unique #dbref.)")
self.call(building.CmdDestroy(), settings.DEFAULT_HOME,
"You are trying to delete") # DEFAULT_HOME should not be deleted
self.char2.location = self.room2
charid = self.char2.id
room1id = self.room1.id
room2id = self.room2.id
self.call(building.CmdDestroy(), self.room2.dbref,
"Char2(#{}) arrives to Room(#{}) from Room2(#{}).|Room2 was destroyed.".format(
charid, room1id, room2id))
building.CmdDestroy.confirm = confirm
def test_destroy_sequence(self):
confirm = building.CmdDestroy.confirm
building.CmdDestroy.confirm = False
self.call(building.CmdDestroy(),
"{}-{}".format(self.obj1.dbref, self.obj2.dbref),
"Obj was destroyed.\nObj2 was destroyed.")
def test_dig(self):
self.call(building.CmdDig(), "TestRoom1=testroom;tr,back;b", "Created room TestRoom1")
self.call(building.CmdDig(), "", "Usage: ")
def test_tunnel(self):
self.call(building.CmdTunnel(), "n = TestRoom2;test2", "Created room TestRoom2")
self.call(building.CmdTunnel(), "", "Usage: ")
self.call(building.CmdTunnel(), "foo = TestRoom2;test2", "tunnel can only understand the")
self.call(building.CmdTunnel(), "/tel e = TestRoom3;test3", "Created room TestRoom3")
DefaultRoom.objects.get_family(db_key="TestRoom3")
exits = DefaultExit.objects.filter_family(db_key__in=("east", "west"))
self.assertEqual(len(exits), 2)
def test_tunnel_exit_typeclass(self):
self.call(building.CmdTunnel(), "n:evennia.objects.objects.DefaultExit = TestRoom3", "Created room TestRoom3")
self.call(building.CmdTunnel(), "n:evennia.objects.objects.DefaultExit = TestRoom3",
"Created room TestRoom3")
def test_exit_commands(self):
self.call(building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2")
self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 -> Room (one way).")
self.call(building.CmdUnLink(), "", "Usage: ")
self.call(building.CmdLink(), "NotFound", "Could not find 'NotFound'.")
self.call(building.CmdLink(), "TestExit", "TestExit1 is an exit to Room.")
self.call(building.CmdLink(), "Obj", "Obj is not an exit. Its home location is Room.")
self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.")
self.char1.location = self.room2
self.call(building.CmdOpen(), "TestExit2=Room", "Created new Exit 'TestExit2' from Room2 to Room.")
self.call(building.CmdOpen(), "TestExit2=Room", "Exit TestExit2 already exists. It already points to the correct place.")
# ensure it matches locally first
self.call(building.CmdLink(), "TestExit=Room2", "Link created TestExit2 -> Room2 (one way).")
self.call(building.CmdLink(), "/twoway TestExit={}".format(self.exit.dbref),
"Link created TestExit2 (in Room2) <-> out (in Room) (two-way).")
self.call(building.CmdLink(), "/twoway TestExit={}".format(self.room1.dbref),
"To create a two-way link, TestExit2 and Room must both have a location ")
self.call(building.CmdLink(), "/twoway {}={}".format(self.exit.dbref, self.exit.dbref),
"Cannot link an object to itself.")
self.call(building.CmdLink(), "", "Usage: ")
# ensure can still match globally when not a local name
self.call(building.CmdLink(), "TestExit1=Room2", "Note: TestExit1")
self.call(building.CmdLink(), "TestExit1=", "Former exit TestExit1 no longer links anywhere.")
def test_set_home(self):
self.call(building.CmdSetHome(), "Obj = Room2", "Obj's home location was changed from Room")
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was changed from Room")
self.call(building.CmdSetHome(), "", "Usage: ")
self.call(building.CmdSetHome(), "self", "Char's current home is Room")
self.call(building.CmdSetHome(), "Obj", "Obj's current home is Room2")
self.obj1.home = None
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was set to Room")
def test_list_cmdsets(self):
self.call(building.CmdListCmdSets(), "", "<DefaultCharacter (Union, prio 0, perm)>:")
self.call(building.CmdListCmdSets(), "NotFound", "Could not find 'NotFound'")
def test_typeclass(self):
self.call(building.CmdTypeclass(), "", "Usage: ")
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultExit",
"Obj changed typeclass from evennia.objects.objects.DefaultObject "
"to evennia.objects.objects.DefaultExit.")
self.call(building.CmdTypeclass(), "Obj2 = evennia.objects.objects.DefaultExit",
"Obj2 changed typeclass from evennia.objects.objects.DefaultObject "
"to evennia.objects.objects.DefaultExit.", cmdstring="swap")
self.call(building.CmdTypeclass(), "/list Obj", "Core typeclasses")
self.call(building.CmdTypeclass(), "/show Obj", "Obj's current typeclass is 'evennia.objects.objects.DefaultExit'")
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultExit",
"Obj already has the typeclass 'evennia.objects.objects.DefaultExit'. Use /force to override.")
self.call(building.CmdTypeclass(), "/force Obj = evennia.objects.objects.DefaultExit",
"Obj updated its existing typeclass ")
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultObject")
self.call(building.CmdTypeclass(), "/show Obj", "Obj's current typeclass is 'evennia.objects.objects.DefaultObject'")
self.call(building.CmdTypeclass(), "Obj",
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
"Only the at_object_creation hook was run (update mode). Attributes set before swap were not removed.",
cmdstring="update")
self.call(building.CmdTypeclass(), "/reset/force Obj=evennia.objects.objects.DefaultObject",
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
"All object creation hooks were run. All old attributes where deleted before the swap.")
def test_lock(self):
self.call(building.CmdLock(), "Obj = test:perm(Developer)", "Added lock 'test:perm(Developer)' to Obj.")
self.call(building.CmdLock(), "", "Usage: ")
self.call(building.CmdLock(), "Obj = test:all()", "Added lock 'test:all()' to Obj.")
self.call(building.CmdLock(), "*TestAccount = test:all()", "Added lock 'test:all()' to TestAccount")
self.call(building.CmdLock(), "Obj/notfound", "Obj has no lock of access type 'notfound'.")
self.call(building.CmdLock(), "Obj/test", "test:all()")
self.call(building.CmdLock(), "/view Obj = edit:false()",
"Switch(es) view can not be used with a lock assignment. "
"Use e.g. lock/del objname/locktype instead.")
self.call(building.CmdLock(), "Obj = control:false()")
self.call(building.CmdLock(), "Obj = edit:false()")
self.call(building.CmdLock(), "Obj/test", "You are not allowed to do that.")
self.obj1.locks.add("control:true()")
self.call(building.CmdLock(), "Obj", "call:true()") # etc
self.call(building.CmdLock(), "*TestAccount", "boot:perm(Admin)") # etc
def test_find(self):
rid2 = self.room2.id
rmax = rid2 + 100
self.call(building.CmdFind(), "", "Usage: ")
self.call(building.CmdFind(), "oom2", "One Match")
expect = "One Match(#1-#7, loc):\n " +\
"Char2(#7) - evennia.objects.objects.DefaultCharacter (location: Room(#1))"
self.call(building.CmdFind(), "Char2", expect, cmdstring="locate")
self.call(building.CmdFind(), "oom2 = 1-{}".format(rmax), "One Match")
self.call(building.CmdFind(), "oom2 = 1 {}".format(rmax), "One Match") # space works too
self.call(building.CmdFind(), "Char2", "One Match", cmdstring="locate")
self.call(building.CmdFind(), "/ex Char2", # /ex is an ambiguous switch
"locate: Ambiguous switch supplied: Did you mean /exit or /exact?|" + expect,
"locate: Ambiguous switch supplied: Did you mean /exit or /exact?|",
cmdstring="locate")
self.call(building.CmdFind(), "Char2", expect, cmdstring="@locate")
self.call(building.CmdFind(), "/l Char2", expect, cmdstring="find") # /l switch is abbreviated form of /loc
self.call(building.CmdFind(), "Char2", "One Match", cmdstring="@find")
self.call(building.CmdFind(), "Char2", "One Match", cmdstring="locate")
self.call(building.CmdFind(), "/l Char2", "One Match", cmdstring="find") # /l switch is abbreviated form of /loc
self.call(building.CmdFind(), "Char2", "One Match", cmdstring="find")
self.call(building.CmdFind(), "/startswith Room2", "One Match")
self.call(building.CmdFind(), self.char1.dbref, "Exact dbref match")
self.call(building.CmdFind(), "*TestAccount", "Match")
self.call(building.CmdFind(), "/char Obj")
self.call(building.CmdFind(), "/room Obj")
self.call(building.CmdFind(), "/exit Obj")
self.call(building.CmdFind(), "/exact Obj", "One Match")
def test_script(self):
self.call(building.CmdScript(), "Obj = ", "No scripts defined on Obj")
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
self.call(building.CmdScript(), "", "Usage: ")
self.call(building.CmdScript(), "= Obj", "To create a global script you need scripts/add <typeclass>.")
self.call(building.CmdScript(), "Obj = ", "dbref obj")
self.call(building.CmdScript(), "/start Obj", "0 scripts started on Obj") # because it's already started
self.call(building.CmdScript(), "/stop Obj", "Stopping script")
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
self.call(building.CmdScript(), "/start Obj = scripts.Script", "Script scripts.Script could not be (re)started.")
self.call(building.CmdScript(), "/stop Obj = scripts.Script", "Script stopped and removed from object.")
def test_teleport(self):
self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#2)\n|Teleported to Room2.")
oid = self.obj1.id
rid = self.room1.id
rid2 = self.room2.id
self.call(building.CmdTeleport(), "", "Usage: ")
self.call(building.CmdTeleport(), "Obj = Room", "Obj is already at Room.")
self.call(building.CmdTeleport(), "Obj = NotFound", "Could not find 'NotFound'.|Destination not found.")
self.call(building.CmdTeleport(),
"Obj = Room2", "Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2.".format(
oid, rid, rid2))
self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.")
self.call(building.CmdTeleport(), "Obj = Obj", "You can't teleport an object inside of itself!")
self.call(building.CmdTeleport(), "/tonone Obj2", "Teleported Obj2 -> None-location.")
self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#{})".format(rid2))
self.call(building.CmdTeleport(), "/t", # /t switch is abbreviated form of /tonone
"Cannot teleport a puppeted object (Char, puppeted by TestAccount(account 1)) to a None-location.")
"Cannot teleport a puppeted object (Char, puppeted by TestAccount")
self.call(building.CmdTeleport(), "/l Room2", # /l switch is abbreviated form of /loc
"Destination has no location.")
self.call(building.CmdTeleport(), "/q me to Room2", # /q switch is abbreviated form of /quiet
"Char is already at Room2.")
def test_tag(self):
self.call(building.CmdTag(), "", "Usage: ")
self.call(building.CmdTag(), "Obj = testtag")
self.call(building.CmdTag(), "Obj = testtag2")
self.call(building.CmdTag(), "Obj = testtag2:category1")
self.call(building.CmdTag(), "Obj = testtag3")
self.call(building.CmdTag(), "Obj", "Tags on Obj: 'testtag', 'testtag2', "
"'testtag2' (category: category1), 'testtag3'")
self.call(building.CmdTag(), "/search NotFound", "No objects found with tag 'NotFound'.")
self.call(building.CmdTag(), "/search testtag", "Found 1 object with tag 'testtag':")
self.call(building.CmdTag(), "/search testtag2", "Found 1 object with tag 'testtag2':")
self.call(building.CmdTag(), "/search testtag2:category1",
"Found 1 object with tag 'testtag2' (category: 'category1'):")
self.call(building.CmdTag(), "/del Obj = testtag3", "Removed tag 'testtag3' from Obj.")
self.call(building.CmdTag(), "/del Obj",
"Cleared all tags from Obj: testtag, testtag2, testtag2 (category: category1)")
def test_spawn(self):
def getObject(commandTest, objKeyStr):
# A helper function to get a spawned object and
@ -403,16 +655,24 @@ class TestBuilding(CommandTest):
commandTest.assertIsNotNone(obj)
return obj
# Tests "@spawn" without any arguments.
self.call(building.CmdSpawn(), " ", "Usage: @spawn")
# Tests "spawn" without any arguments.
self.call(building.CmdSpawn(), " ", "Usage: spawn")
# Tests "@spawn <prototype_dictionary>" without specifying location.
# Tests "spawn <prototype_dictionary>" without specifying location.
self.call(building.CmdSpawn(),
"/save {'prototype_key': 'testprot', 'key':'Test Char', "
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
"Saved prototype: testprot", inputs=['y'])
self.call(building.CmdSpawn(), "/search ", "Key ")
self.call(building.CmdSpawn(), "/search test;test2", "")
self.call(building.CmdSpawn(),
"/save {'key':'Test Char', "
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
"To save a prototype it must have the 'prototype_key' set.")
self.call(building.CmdSpawn(), "/list", "Key ")
self.call(building.CmdSpawn(), 'testprot', "Spawned Test Char")
@ -422,7 +682,7 @@ class TestBuilding(CommandTest):
self.assertEqual(testchar.location, self.char1.location)
testchar.delete()
# Test "@spawn <prototype_dictionary>" with a location other than the character's.
# Test "spawn <prototype_dictionary>" with a location other than the character's.
spawnLoc = self.room2
if spawnLoc == self.char1.location:
# Just to make sure we use a different location, in case someone changes
@ -440,11 +700,11 @@ class TestBuilding(CommandTest):
goblin.delete()
# create prototype
protlib.create_prototype(**{'key': 'Ball',
'typeclass': 'evennia.objects.objects.DefaultCharacter',
'prototype_key': 'testball'})
protlib.create_prototype({'key': 'Ball',
'typeclass': 'evennia.objects.objects.DefaultCharacter',
'prototype_key': 'testball'})
# Tests "@spawn <prototype_name>"
# Tests "spawn <prototype_name>"
self.call(building.CmdSpawn(), "testball", "Spawned Ball")
ball = getObject(self, "Ball")
@ -452,7 +712,7 @@ class TestBuilding(CommandTest):
self.assertIsInstance(ball, DefaultObject)
ball.delete()
# Tests "@spawn/n ..." without specifying a location.
# Tests "spawn/n ..." without specifying a location.
# Location should be "None".
self.call(building.CmdSpawn(), "/n 'BALL'", "Spawned Ball") # /n switch is abbreviated form of /noloc
ball = getObject(self, "Ball")
@ -463,7 +723,7 @@ class TestBuilding(CommandTest):
"/noloc {'prototype_parent':'TESTBALL', 'prototype_key': 'testball', 'location':'%s'}"
% spawnLoc.dbref, "Error: Prototype testball tries to parent itself.")
# Tests "@spawn/noloc ...", but DO specify a location.
# Tests "spawn/noloc ...", but DO specify a location.
# Location should be the specified location.
self.call(building.CmdSpawn(),
"/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo', 'location':'%s'}"
@ -478,14 +738,14 @@ class TestBuilding(CommandTest):
# Test listing commands
self.call(building.CmdSpawn(), "/list", "Key ")
# @spawn/edit (missing prototype)
# spawn/edit (missing prototype)
# brings up olc menu
msg = self.call(
building.CmdSpawn(),
'/edit')
assert 'Prototype wizard' in msg
# @spawn/edit with valid prototype
# spawn/edit with valid prototype
# brings up olc menu loaded with prototype
msg = self.call(
building.CmdSpawn(),
@ -499,34 +759,34 @@ class TestBuilding(CommandTest):
and 'Ball' == self.char1.ndb._menutree.olc_prototype['key']
assert 'Ball' in msg and 'testball' in msg
# @spawn/edit with valid prototype (synomym)
# spawn/edit with valid prototype (synomym)
msg = self.call(
building.CmdSpawn(),
'/edit BALL')
assert 'Prototype wizard' in msg
assert 'Ball' in msg and 'testball' in msg
# @spawn/edit with invalid prototype
# spawn/edit with invalid prototype
msg = self.call(
building.CmdSpawn(),
'/edit NO_EXISTS',
"No prototype 'NO_EXISTS' was found.")
# @spawn/examine (missing prototype)
# spawn/examine (missing prototype)
# lists all prototypes that exist
msg = self.call(
building.CmdSpawn(),
'/examine')
assert 'testball' in msg and 'testprot' in msg
# @spawn/examine with valid prototype
# spawn/examine with valid prototype
# prints the prototype
msg = self.call(
building.CmdSpawn(),
'/examine BALL')
assert 'Ball' in msg and 'testball' in msg
# @spawn/examine with invalid prototype
# spawn/examine with invalid prototype
# shows error
self.call(
building.CmdSpawn(),
@ -576,7 +836,7 @@ class TestComms(CommandTest):
def test_cboot(self):
# No one else connected to boot
self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] <channel> = <account> [:reason]", receiver=self.account)
self.call(comms.CmdCBoot(), "", "Usage: cboot[/quiet] <channel> = <account> [:reason]", receiver=self.account)
def test_cdestroy(self):
self.call(comms.CmdCdestroy(), "testchan",
@ -615,8 +875,41 @@ class TestInterruptCommand(CommandTest):
class TestUnconnectedCommand(CommandTest):
def test_info_command(self):
# instead of using SERVER_START_TIME (0), we use 86400 because Windows won't let us use anything lower
gametime.SERVER_START_TIME = 86400
expected = "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % (
settings.SERVERNAME,
datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),
SESSIONS.account_count(), utils.get_evennia_version())
self.call(unloggedin.CmdUnconnectedInfo(), "", expected)
del gametime.SERVER_START_TIME
# Test syscommands
class TestSystemCommands(CommandTest):
def test_simple_defaults(self):
self.call(syscommands.SystemNoInput(), "")
self.call(syscommands.SystemNoMatch(), "Huh?")
def test_multimatch(self):
# set up fake matches and store on command instance
cmdset = CmdSet()
cmdset.add(general.CmdLook())
cmdset.add(general.CmdLook())
matches = cmdparser.build_matches("look", cmdset)
multimatch = syscommands.SystemMultimatch()
multimatch.matches = matches
self.call(multimatch, "look", "")
@mock.patch("evennia.commands.default.syscommands.ChannelDB")
def test_channelcommand(self, mock_channeldb):
channel = mock.MagicMock()
channel.msg = mock.MagicMock()
mock_channeldb.objects.get_channel = mock.MagicMock(return_value=channel)
self.call(syscommands.SystemSendToChannel(), "public:Hello")
channel.msg.assert_called()

View file

@ -2,19 +2,13 @@
Commands that are available from the connect screen.
"""
import re
import time
import datetime
from random import getrandbits
from codecs import lookup as codecs_lookup
from django.conf import settings
from django.contrib.auth import authenticate
from evennia.accounts.models import AccountDB
from evennia.objects.models import ObjectDB
from evennia.server.models import ServerConfig
from evennia.server.throttle import Throttle
from evennia.comms.models import ChannelDB
from evennia.server.sessionhandler import SESSIONS
from evennia.utils import create, logger, utils, gametime
from evennia.utils import class_from_module, create, logger, utils, gametime
from evennia.commands.cmdhandler import CMD_LOGINSTART
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -26,11 +20,6 @@ __all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
MULTISESSION_MODE = settings.MULTISESSION_MODE
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
# Create throttles for too many connections, account-creations and login attempts
CONNECTION_THROTTLE = Throttle(limit=5, timeout=1 * 60)
CREATION_THROTTLE = Throttle(limit=2, timeout=10 * 60)
LOGIN_THROTTLE = Throttle(limit=5, timeout=5 * 60)
def create_guest_account(session):
"""
@ -44,49 +33,20 @@ def create_guest_account(session):
the boolean is whether guest accounts are enabled at all.
the Account which was created from an available guest name.
"""
# check if guests are enabled.
if not settings.GUEST_ENABLED:
return False, None
enabled = settings.GUEST_ENABLED
address = session.address
# Check IP bans.
bans = ServerConfig.objects.conf("server_bans")
if bans and any(tup[2].match(session.address) for tup in bans if tup[2]):
# this is a banned IP!
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 True, None
# Get account class
Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)
try:
# Find an available guest name.
accountname = None
for name in settings.GUEST_LIST:
if not AccountDB.objects.filter(username__iexact=accountname).count():
accountname = name
break
if not accountname:
session.msg("All guest accounts are in use. Please try again later.")
return True, None
else:
# build a new account with the found guest accountname
password = "%016x" % getrandbits(64)
home = ObjectDB.objects.get_id(settings.GUEST_HOME)
permissions = settings.PERMISSION_GUEST_DEFAULT
typeclass = settings.BASE_CHARACTER_TYPECLASS
ptypeclass = settings.BASE_GUEST_TYPECLASS
new_account = _create_account(session, accountname, password, permissions, ptypeclass)
if new_account:
_create_character(session, new_account, typeclass, home, permissions)
return True, new_account
except Exception:
# We are in the middle between logged in and -not, so we have
# to handle tracebacks ourselves at this point. If we don't,
# we won't see any errors at all.
session.msg("An error occurred. Please e-mail an admin if the problem persists.")
logger.log_trace()
raise
# Get an available guest account
# authenticate() handles its own throttling
account, errors = Guest.authenticate(ip=address)
if account:
return enabled, account
else:
session.msg("|R%s|n" % '\n'.join(errors))
return enabled, None
def create_normal_account(session, name, password):
@ -101,38 +61,17 @@ def create_normal_account(session, name, password):
Returns:
account (Account): the account which was created from the name and password.
"""
# check for too many login errors too quick.
address = session.address
if isinstance(address, tuple):
address = address[0]
# Get account class
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
if LOGIN_THROTTLE.check(address):
session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n")
return None
address = session.address
# Match account name and check password
account = authenticate(username=name, password=password)
# authenticate() handles all its own throttling
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
if not account:
# No accountname or password match
session.msg("Incorrect login information given.")
# this just updates the throttle
LOGIN_THROTTLE.update(address)
# calls account hook for a failed login if possible.
account = AccountDB.objects.get_account_from_name(name)
if account:
account.at_failed_login(session)
return None
# Check IP and/or name bans
bans = ServerConfig.objects.conf("server_bans")
if bans and (any(tup[0] == account.name.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"
session.msg(string)
session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")
session.msg("|R%s|n" % '\n'.join(errors))
return None
return account
@ -164,15 +103,7 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
there is no object yet before the account has logged in)
"""
session = self.caller
# check for too many login errors too quick.
address = session.address
if isinstance(address, tuple):
address = address[0]
if CONNECTION_THROTTLE.check(address):
# timeout is 5 minutes.
session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n")
return
args = self.args
# extract double quote parts
@ -180,23 +111,33 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
if len(parts) == 1:
# this was (hopefully) due to no double quotes being found, or a guest login
parts = parts[0].split(None, 1)
# Guest login
if len(parts) == 1 and parts[0].lower() == "guest":
enabled, new_account = create_guest_account(session)
if new_account:
session.sessionhandler.login(session, new_account)
if enabled:
# Get Guest typeclass
Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)
account, errors = Guest.authenticate(ip=address)
if account:
session.sessionhandler.login(session, account)
return
else:
session.msg("|R%s|n" % '\n'.join(errors))
return
if len(parts) != 2:
session.msg("\n\r Usage (without <>): connect <name> <password>")
return
CONNECTION_THROTTLE.update(address)
# Get account class
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
name, password = parts
account = create_normal_account(session, name, password)
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))
class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
@ -222,14 +163,10 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
session = self.caller
args = self.args.strip()
# Rate-limit account creation.
address = session.address
if isinstance(address, tuple):
address = address[0]
if CREATION_THROTTLE.check(address):
session.msg("|RYou are creating too many accounts. Try again in a few minutes.|n")
return
# Get account class
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
# extract double quoted parts
parts = [part.strip() for part in re.split(r"\"", args) if part.strip()]
@ -241,77 +178,21 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
"\nIf <name> or <password> contains spaces, enclose it in double quotes."
session.msg(string)
return
accountname, password = parts
# sanity checks
if not re.findall(r"^[\w. @+\-']+$", accountname) or not (0 < len(accountname) <= 30):
# this echoes the restrictions made by django's auth
# module (except not allowing spaces, for convenience of
# logging in).
string = "\n\r Accountname can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_/' only."
session.msg(string)
return
# strip excessive spaces in accountname
accountname = re.sub(r"\s+", " ", accountname).strip()
if AccountDB.objects.filter(username__iexact=accountname):
# account already exists (we also ignore capitalization here)
session.msg("Sorry, there is already an account with the name '%s'." % accountname)
return
# Reserve accountnames found in GUEST_LIST
if settings.GUEST_LIST and accountname.lower() in (guest.lower() for guest in settings.GUEST_LIST):
string = "\n\r That name is reserved. Please choose another Accountname."
session.msg(string)
return
# Validate password
Account = utils.class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
# Have to create a dummy Account object to check username similarity
valid, error = Account.validate_password(password, account=Account(username=accountname))
if error:
errors = [e for suberror in error.messages for e in error.messages]
string = "\n".join(errors)
session.msg(string)
return
# Check IP and/or name bans
bans = ServerConfig.objects.conf("server_bans")
if bans and (any(tup[0] == accountname.lower() for tup in bans) or
any(tup[2].match(session.address) for tup in bans if tup[2])):
# this is a banned IP or name!
string = "|rYou have been banned and cannot continue from here." \
"\nIf you feel this ban is in error, please email an admin.|x"
session.msg(string)
session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")
return
username, password = parts
# everything's ok. Create the new account account.
try:
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
typeclass = settings.BASE_CHARACTER_TYPECLASS
new_account = _create_account(session, accountname, password, permissions)
if new_account:
if MULTISESSION_MODE < 2:
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
_create_character(session, new_account, typeclass, default_home, permissions)
# Update the throttle to indicate a new account was created from this IP
CREATION_THROTTLE.update(address)
# 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>'."
else:
string += "\n\nYou can now log with the command 'connect %s <your password>'."
session.msg(string % (accountname, accountname))
except Exception:
# We are in the middle between logged in and -not, so we have
# to handle tracebacks ourselves at this point. If we don't,
# we won't see any errors at all.
session.msg("An error occurred. Please e-mail an admin if the problem persists.")
logger.log_trace()
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>'."
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))
class CmdUnconnectedQuit(COMMAND_DEFAULT_CLASS):
@ -353,9 +234,14 @@ class CmdUnconnectedLook(COMMAND_DEFAULT_CLASS):
def func(self):
"""Show the connect screen."""
connection_screen = utils.random_string_from_module(CONNECTION_SCREEN_MODULE)
if not connection_screen:
connection_screen = "No connection screen found. Please contact an admin."
callables = utils.callables_from_module(CONNECTION_SCREEN_MODULE)
if "connection_screen" in callables:
connection_screen = callables['connection_screen']()
else:
connection_screen = utils.random_string_from_module(CONNECTION_SCREEN_MODULE)
if not connection_screen:
connection_screen = "No connection screen found. Please contact an admin."
self.caller.msg(connection_screen)
@ -395,6 +281,9 @@ Next you can connect to the game: |wconnect Anna c67jHL8p|n
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
self.caller.msg(string)
@ -422,7 +311,7 @@ class CmdUnconnectedEncoding(COMMAND_DEFAULT_CLASS):
"""
key = "encoding"
aliases = ("@encoding", "@encode")
aliases = ("encode")
locks = "cmd:all()"
def func(self):
@ -448,7 +337,7 @@ class CmdUnconnectedEncoding(COMMAND_DEFAULT_CLASS):
pencoding = self.session.protocol_flags.get("ENCODING", None)
string = ""
if pencoding:
string += "Default encoding: |g%s|n (change with |w@encoding <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)
@ -459,7 +348,7 @@ class CmdUnconnectedEncoding(COMMAND_DEFAULT_CLASS):
old_encoding = self.session.protocol_flags.get("ENCODING", None)
encoding = self.args
try:
utils.to_str(utils.to_unicode("test-string"), encoding=encoding)
codecs_lookup(encoding)
except LookupError:
string = "|rThe encoding '|w%s|r' is invalid. Keeping the previous encoding '|w%s|r'.|n"\
% (encoding, old_encoding)
@ -480,10 +369,9 @@ class CmdUnconnectedScreenreader(COMMAND_DEFAULT_CLASS):
screenreader
Used to flip screenreader mode on and off before logging in (when
logged in, use @option screenreader on).
logged in, use option screenreader on).
"""
key = "screenreader"
aliases = "@screenreader"
def func(self):
"""Flips screenreader setting."""
@ -554,7 +442,7 @@ def _create_character(session, new_account, typeclass, home, permissions):
# If no description is set, set a default description
if not new_character.db.desc:
new_character.db.desc = "This is a character."
# We need to set this to have @ic auto-connect to this character
# We need to set this to have ic auto-connect to this character
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)

View file

@ -14,7 +14,7 @@ class _CmdA(Command):
key = "A"
def __init__(self, cmdset, *args, **kwargs):
super(_CmdA, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.from_cmdset = cmdset
@ -22,7 +22,7 @@ class _CmdB(Command):
key = "B"
def __init__(self, cmdset, *args, **kwargs):
super(_CmdB, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.from_cmdset = cmdset
@ -30,7 +30,7 @@ class _CmdC(Command):
key = "C"
def __init__(self, cmdset, *args, **kwargs):
super(_CmdC, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.from_cmdset = cmdset
@ -38,7 +38,7 @@ class _CmdD(Command):
key = "D"
def __init__(self, cmdset, *args, **kwargs):
super(_CmdD, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.from_cmdset = cmdset
@ -85,7 +85,7 @@ class TestCmdSetMergers(TestCase):
"Test merging of cmdsets"
def setUp(self):
super(TestCmdSetMergers, self).setUp()
super().setUp()
self.cmdset_a = _CmdSetA()
self.cmdset_b = _CmdSetB()
self.cmdset_c = _CmdSetC()
@ -278,7 +278,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
def setUp(self):
self.patch(sys.modules['evennia.server.sessionhandler'], 'delay', _mockdelay)
super(TestGetAndMergeCmdSets, self).setUp()
super().setUp()
self.cmdset_a = _CmdSetA()
self.cmdset_b = _CmdSetB()
self.cmdset_c = _CmdSetC()

View file

@ -91,11 +91,9 @@ class ChannelAdmin(admin.ModelAdmin):
obj.at_init()
def response_add(self, request, obj, post_url_continue=None):
if '_continue' in request.POST:
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
return HttpResponseRedirect(reverse("admin:comms_channeldb_change", args=[obj.id]))
return super(ChannelAdmin, self).response_add(request, obj, post_url_continue)
from django.http import HttpResponseRedirect
from django.urls import reverse
return HttpResponseRedirect(reverse("admin:comms_channeldb_change", args=[obj.id]))
admin.site.register(ChannelDB, ChannelAdmin)

View file

@ -271,7 +271,7 @@ class ChannelHandler(object):
if channelname:
channel = self._cached_channels.get(channelname.lower(), None)
return [channel] if channel else []
return self._cached_channels.values()
return list(self._cached_channels.values())
def get_cmdset(self, source_object):
"""
@ -292,7 +292,7 @@ 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.iteritems()
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:

View file

@ -2,10 +2,14 @@
Base typeclass for in-game Channels.
"""
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.text import slugify
from evennia.typeclasses.models import TypeclassBase
from evennia.comms.models import TempMsg, ChannelDB
from evennia.comms.managers import ChannelManager
from evennia.utils import logger
from evennia.utils import create, logger
from evennia.utils.utils import make_iter
from future.utils import with_metaclass
_CHANNEL_HANDLER = None
@ -220,6 +224,51 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
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):
"""
Creates a basic Channel with default parameters, unless otherwise
specified or extended.
Provides a friendlier interface to the utils.create_channel() function.
Args:
key (str): This must be unique.
account (Account): Account to attribute this object to.
Kwargs:
aliases (list of str): List of alternative (likely shorter) keynames.
description (str): A description of the channel, for use in listings.
locks (str): Lockstring.
keep_log (bool): Log channel throughput.
typeclass (str or class): The typeclass of the Channel (not
often used).
ip (str): IP address of creator (for object auditing).
Returns:
channel (Channel): A newly created Channel.
errors (list): A list of errors in string form, if any.
"""
errors = []
obj = None
ip = kwargs.pop('ip', '')
try:
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
except Exception as exc:
errors.append("An error occurred while creating this '%s' object." % key)
logger.log_err(exc)
return obj, errors
def delete(self):
"""
Deletes channel while also cleaning up channelhandler.
@ -227,7 +276,7 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
"""
self.attributes.clear()
self.aliases.clear()
super(DefaultChannel, self).delete()
super().delete()
from evennia.comms.channelhandler import CHANNELHANDLER
CHANNELHANDLER.update()
@ -332,7 +381,7 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
"""
senders = make_iter(senders) if senders else []
if isinstance(msgobj, basestring):
if isinstance(msgobj, str):
# given msgobj is a string - convert to msgobject (always TempMsg)
msgobj = TempMsg(senders=senders, header=header, message=msgobj, channels=[self])
# we store the logging setting for use in distribute_message()
@ -578,3 +627,151 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
"""
pass
#
# Web/Django methods
#
def web_get_admin_url(self):
"""
Returns the URI path for the Django Admin page for this object.
ex. Account#1 = '/admin/accounts/accountdb/1/change/'
Returns:
path (str): URI path to Django Admin page for object.
"""
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,))
@classmethod
def web_get_create_url(cls):
"""
Returns the URI path for a View that allows users to create new
instances of this object.
ex. Chargen = '/characters/create/'
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format 'modelname-action', so in this case
a named view of 'channel-create' would be referenced by this method.
ex.
url(r'channels/create/', ChannelCreateView.as_view(), name='channel-create')
If no View has been created and defined in urls.py, returns an
HTML anchor.
This method is naive and simply returns a path. Securing access to
the actual view and limiting who can create new objects is the
developer's responsibility.
Returns:
path (str): URI path to object creation page, if defined.
"""
try:
return reverse('%s-create' % slugify(cls._meta.verbose_name))
except:
return '#'
def web_get_detail_url(self):
"""
Returns the URI path for a View that allows users to view details for
this object.
ex. Oscar (Character) = '/characters/oscar/1/'
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format 'modelname-action', so in this case
a named view of 'channel-detail' would be referenced by this method.
ex.
url(r'channels/(?P<slug>[\w\d\-]+)/$',
ChannelDetailView.as_view(), name='channel-detail')
If no View has been created and defined in urls.py, returns an
HTML anchor.
This method is naive and simply returns a path. Securing access to
the actual view and limiting who can view this object is the developer's
responsibility.
Returns:
path (str): URI path to object detail page, if defined.
"""
try:
return reverse('%s-detail' % slugify(self._meta.verbose_name),
kwargs={'slug': slugify(self.db_key)})
except:
return '#'
def web_get_update_url(self):
"""
Returns the URI path for a View that allows users to update this
object.
ex. Oscar (Character) = '/characters/oscar/1/change/'
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format 'modelname-action', so in this case
a named view of 'channel-update' would be referenced by this method.
ex.
url(r'channels/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
ChannelUpdateView.as_view(), name='channel-update')
If no View has been created and defined in urls.py, returns an
HTML anchor.
This method is naive and simply returns a path. Securing access to
the actual view and limiting who can modify objects is the developer's
responsibility.
Returns:
path (str): URI path to object update page, if defined.
"""
try:
return reverse('%s-update' % slugify(self._meta.verbose_name),
kwargs={'slug': slugify(self.db_key)})
except:
return '#'
def web_get_delete_url(self):
"""
Returns the URI path for a View that allows users to delete this object.
ex. Oscar (Character) = '/characters/oscar/1/delete/'
For this to work, the developer must have defined a named view somewhere
in urls.py that follows the format 'modelname-action', so in this case
a named view of 'channel-delete' would be referenced by this method.
ex.
url(r'channels/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
ChannelDeleteView.as_view(), name='channel-delete')
If no View has been created and defined in urls.py, returns an
HTML anchor.
This method is naive and simply returns a path. Securing access to
the actual view and limiting who can delete this object is the developer's
responsibility.
Returns:
path (str): URI path to object deletion page, if defined.
"""
try:
return reverse('%s-delete' % slugify(self._meta.verbose_name),
kwargs={'slug': slugify(self.db_key)})
except:
return '#'
# Used by Django Sites/Admin
get_absolute_url = web_get_detail_url

View file

@ -3,7 +3,7 @@ These managers define helper methods for accessing the database from
Comm system components.
"""
from __future__ import print_function
from django.db.models import Q
from evennia.typeclasses.managers import (TypedObjectManager, TypeclassManager)
@ -43,9 +43,9 @@ def dbref(inp, reqhash=True):
dbref, otherwise `None`.
"""
if reqhash and not (isinstance(inp, basestring) and inp.startswith("#")):
if reqhash and not (isinstance(inp, str) and inp.startswith("#")):
return None
if isinstance(inp, basestring):
if isinstance(inp, str):
inp = inp.lstrip('#')
try:
if int(inp) < 0:
@ -77,7 +77,7 @@ def identify_object(inp):
return inp, "object"
elif clsname == "ChannelDB":
return inp, "channel"
if isinstance(inp, basestring):
if isinstance(inp, str):
return inp, "string"
elif dbref(inp):
return dbref(inp), "dbref"

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
@ -14,10 +14,10 @@ class Migration(migrations.Migration):
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=b'key', db_index=True)),
('db_typeclass_path', models.CharField(help_text=b"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=b'typeclass')),
('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name=b'creation date')),
('db_lock_storage', models.TextField(help_text=b"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=b'locks', blank=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',
@ -29,12 +29,12 @@ class Migration(migrations.Migration):
name='Msg',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('db_sender_external', models.CharField(help_text=b"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=b'external sender', db_index=True)),
('db_header', models.TextField(null=True, verbose_name=b'header', blank=True)),
('db_message', models.TextField(verbose_name=b'messsage')),
('db_date_sent', models.DateTimeField(auto_now_add=True, verbose_name=b'date sent', db_index=True)),
('db_lock_storage', models.TextField(help_text=b'access locks on this message.', verbose_name=b'locks', blank=True)),
('db_hide_from_channels', models.ManyToManyField(related_name=b'hide_from_channels_set', null=True, to='comms.ChannelDB')),
('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',

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='msg',
name='db_hide_from_objects',
field=models.ManyToManyField(related_name=b'hide_from_objects_set', null=True, to='objects.ObjectDB'),
field=models.ManyToManyField(related_name='hide_from_objects_set', null=True, to='objects.ObjectDB'),
preserve_default=True,
),
]

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
@ -18,55 +18,55 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='msg',
name='db_hide_from_accounts',
field=models.ManyToManyField(related_name=b'hide_from_accounts_set', null=True, to=settings.AUTH_USER_MODEL),
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=b'channel recievers', related_name=b'channel_set', null=True, to='comms.ChannelDB'),
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=b'object receivers', related_name=b'receiver_object_set', null=True, to='objects.ObjectDB'),
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=b'account receivers', related_name=b'receiver_account_set', null=True, to=settings.AUTH_USER_MODEL),
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=b'sender_object_set', null=True, verbose_name=b'sender(object)', to='objects.ObjectDB', db_index=True),
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=b'sender_account_set', null=True, verbose_name=b'sender(account)', to=settings.AUTH_USER_MODEL, db_index=True),
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=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute', null=True),
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=b'subscription_set', null=True, verbose_name=b'subscriptions', to=settings.AUTH_USER_MODEL, db_index=True),
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=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag', null=True),
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

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='channeldb',
name='db_object_subscriptions',
field=models.ManyToManyField(related_name='object_subscription_set', null=True, verbose_name=b'subscriptions', to='objects.ObjectDB', db_index=True),
field=models.ManyToManyField(related_name='object_subscription_set', null=True, verbose_name='subscriptions', to='objects.ObjectDB', db_index=True),
preserve_default=True,
),
]

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
@ -15,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='msg',
name='db_tags',
field=models.ManyToManyField(help_text=b'tags on this message. Tags are simple string markers to identify, group and alias messages.', to='typeclasses.Tag', null=True),
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

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.9 on 2016-09-05 09:02
from __future__ import unicode_literals
from django.db import migrations

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.9 on 2016-09-21 17:31
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
@ -31,36 +31,36 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='msg',
name='db_receivers_channels',
field=models.ManyToManyField(blank=True, help_text=b'channel recievers', null=True, related_name='channel_set', to='comms.ChannelDB'),
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=b'object receivers', null=True, related_name='receiver_object_set', to='objects.ObjectDB'),
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=b'account receivers', null=True, related_name='receiver_account_set', to=settings.AUTH_USER_MODEL),
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=b"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=b'external sender'),
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=b'sender(object)'),
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=b'sender(account)'),
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=b'tags on this message. Tags are simple string markers to identify, group and alias messages.', null=True, to='typeclasses.Tag'),
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

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2016-12-06 19:12
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
@ -16,11 +16,11 @@ class Migration(migrations.Migration):
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=b'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=b'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

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2017-02-17 20:39
from __future__ import unicode_literals
from django.db import migrations, models
@ -16,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='msg',
name='db_receivers_scripts',
field=models.ManyToManyField(blank=True, help_text=b'script_receivers', null=True, related_name='receiver_script_set', to='scripts.ScriptDB'),
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=b'sender(script)'),
field=models.ManyToManyField(blank=True, db_index=True, null=True, related_name='sender_script_set', to='scripts.ScriptDB', verbose_name='sender(script)'),
),
]

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-06-06 17:31
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
@ -16,22 +16,22 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='channeldb',
name='db_attributes',
field=models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'),
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=b'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=b'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=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
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',
@ -51,31 +51,31 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='msg',
name='db_receivers_channels',
field=models.ManyToManyField(blank=True, help_text=b'channel recievers', related_name='channel_set', to='comms.ChannelDB'),
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=b'object receivers', related_name='receiver_object_set', to='objects.ObjectDB'),
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=b'account receivers', related_name='receiver_account_set', to=settings.AUTH_USER_MODEL),
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=b'sender(object)'),
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=b'sender(account)'),
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=b'tags on this message. Tags are simple string markers to identify, group and alias messages.', to='typeclasses.Tag'),
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

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-06-17 20:17
from __future__ import unicode_literals
from django.db import migrations

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-05 17:26
from __future__ import unicode_literals
from django.db import migrations, models, connection
@ -23,37 +23,37 @@ class Migration(migrations.Migration):
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=b'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=b'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=b'script_receivers', related_name='receiver_script_set', to='scripts.ScriptDB'),
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=b'sender(script)'),
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=b'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=b'script_receivers', related_name='receiver_script_set', to='scripts.ScriptDB'),
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=b'sender(script)'),
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_script_set', to='scripts.ScriptDB', verbose_name='sender(script)'),
),
]
@ -64,7 +64,7 @@ class Migration(migrations.Migration):
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=b'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',
@ -74,11 +74,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='msg',
name='db_receivers_accounts',
field=models.ManyToManyField(blank=True, help_text=b'account receivers', related_name='receiver_account_set', to='accounts.AccountDB'),
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=b'sender(account)'),
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_account_set', to='accounts.AccountDB', verbose_name='sender(account)'),
),
]

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-05 17:36
from __future__ import unicode_literals
from django.db import migrations

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-06 20:41
from __future__ import unicode_literals
from django.db import migrations, connection

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2018-09-25 17:35
from __future__ import unicode_literals
from django.db import migrations, models
@ -19,6 +19,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='msg',
name='db_message',
field=models.TextField(verbose_name=b'message'),
field=models.TextField(verbose_name='message'),
),
]

View file

@ -0,0 +1,121 @@
# -*- 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
class Migration(migrations.Migration):
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'),
),
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'),
),
migrations.AlterField(
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'),
),
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'),
),
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'),
),
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'),
),
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'),
),
migrations.AlterField(
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'),
),
migrations.AlterField(
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'),
),
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),
),
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'),
),
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'),
),
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'),
),
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)'),
),
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'),
),
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)'),
),
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)'),
),
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'),
),
]

View file

@ -166,7 +166,7 @@ class Msg(SharedMemoryModel):
for sender in make_iter(senders):
if not sender:
continue
if isinstance(sender, basestring):
if isinstance(sender, str):
self.db_sender_external = sender
self.extra_senders.append(sender)
self.save(update_fields=["db_sender_external"])
@ -203,7 +203,7 @@ class Msg(SharedMemoryModel):
for sender in make_iter(senders):
if not sender:
continue
if isinstance(sender, basestring):
if isinstance(sender, str):
self.db_sender_external = ""
self.save(update_fields=["db_sender_external"])
if not hasattr(sender, "__dbclass__"):

13
evennia/comms/tests.py Normal file
View file

@ -0,0 +1,13 @@
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)
self.assertTrue(obj, errors)
self.assertFalse(errors, errors)
self.assertEqual(description, obj.db.desc)

View file

@ -29,6 +29,7 @@ things you want from here into your game folder and change them there.
* Dice (Griatch 2012) - A fully featured dice rolling system.
* Email-login (Griatch 2012) - A variant of the standard login system
that requires an email to login rather then just name+password.
* Evscaperoom (Griatch 2019) - A full engine for making escaperoom puzzles
* Extended Room (Griatch 2012) - An expanded Room typeclass with
multiple descriptions for time and season as well as details.
* Field Fill (FlutterSprite 2018) - A simple system for creating an
@ -38,13 +39,14 @@ things you want from here into your game folder and change them there.
on a character and access it in an emote with a custom marker.
* Health Bar (Tim Ashley Jenkins 2017) - Tool to create colorful bars/meters.
* Mail (grungies1138 2016) - An in-game mail system for communication.
* Menu login (Griatch 2011) - A login system using menus asking
* Menu login (Griatch 2011, 2019, Vincent-lg 2016) - A login system using menus asking
for name/password rather than giving them as one command.
* Map Builder (CloudKeeper 2016) - Build a game area based on a 2D
"graphical" unicode map. Supports assymmetric exits.
* Menu Login (Vincent-lg 2016) - Alternate login system using EvMenu.
* Multidescer (Griatch 2016) - Advanced descriptions combined from
many separate description components, inspired by MUSH.
* Puzzles (Hendher 2019) - Combine objects to create new items, adventure-game style
* Random String Generator (Vincent Le Goff 2017) - Simple pseudo-random
generator of strings with rules, avoiding repetitions.
* RPLanguage (Griatch 2015) - Dynamic obfuscation of emotes when

View file

@ -93,7 +93,7 @@ cmdset. This will make the trade (or barter) command available
in-game.
"""
from __future__ import print_function
from builtins import object
from evennia import Command, DefaultScript, CmdSet

View file

@ -595,7 +595,7 @@ class BuildingMenu(object):
if choice_key == self.joker_key:
continue
if not isinstance(menu_key, basestring) or menu_key != choice_key:
if not isinstance(menu_key, str) or menu_key != choice_key:
common = False
break
@ -631,7 +631,7 @@ class BuildingMenu(object):
if choice_key == self.joker_key:
continue
if not isinstance(menu_key, basestring) or menu_key != choice_key:
if not isinstance(menu_key, str) or menu_key != choice_key:
common = False
break

View file

@ -116,7 +116,7 @@ class CmdOOCLook(default_cmds.CmdLook):
# not ooc mode - leave back to normal look
# we have to put this back for normal look to work.
self.caller = self.character
super(CmdOOCLook, self).func()
super().func()
class CmdOOCCharacterCreate(Command):

View file

@ -187,7 +187,7 @@ def clothing_type_count(clothes_list):
for garment in clothes_list:
if garment.db.clothing_type:
type = garment.db.clothing_type
if type not in types_count.keys():
if type not in list(types_count.keys()):
types_count[type] = 1
else:
types_count[type] += 1
@ -380,7 +380,7 @@ class CmdWear(MuxCommand):
# Apply individual clothing type limits.
if clothing.db.clothing_type and not clothing.db.worn:
type_count = single_type_count(get_worn_clothes(self.caller), clothing.db.clothing_type)
if clothing.db.clothing_type in CLOTHING_TYPE_LIMIT.keys():
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)
return
@ -684,7 +684,7 @@ class ClothedCharacterCmdSet(default_cmds.CharacterCmdSet):
"""
Populates the cmdset
"""
super(ClothedCharacterCmdSet, self).at_cmdset_creation()
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones.
#

View file

@ -1 +0,0 @@
from evennia.contrib.egi_client.service import EvenniaGameIndexService

View file

@ -138,7 +138,7 @@ class CmdUnconnectedCreate(MuxCommand):
name enclosed in quotes:
connect "Long name with many words" my@myserv.com mypassw
"""
super(CmdUnconnectedCreate, self).parse()
super().parse()
self.accountinfo = []
if len(self.arglist) < 3:

View file

@ -0,0 +1,116 @@
# EvscapeRoom
Evennia contrib - Griatch 2019
This 'Evennia escaperoom game engine' was created for the MUD Coders Guild game
Jam, April 14-May 15 2019. The theme for the jam was "One Room". This contains the
utilities and base classes and an empty example room. The code for the full
in-production game 'Evscaperoom' is found at https://github.com/Griatch/evscaperoom
and you can play the full game (for now) at `http://experimental.evennia.com`.
# Introduction
Evscaperoom is, as it sounds, an escaperoom in text form. You start locked into
a room and have to figure out how to get out. This engine contains everything needed
to make a fully-featured puzzle game of this type!
# Installation
The Evscaperoom is installed by adding the `evscaperoom` command to your
character cmdset. When you run that command in-game you're ready to play!
In `mygame/commands/default_cmdsets.py`:
```python
from evennia.contrib.evscaperoom.commands import CmdEvscapeRoomStart
class CharacterCmdSet(...):
# ...
self.add(CmdEvscapeRoomStart())
```
Reload the server and the `evscaperoom` command will be available. The contrib comes
with a small (very small) escape room as an example.
# Making your own evscaperoom
To do this, you need to make your own states. First make sure you can play the
simple example room installed above.
Copy `evennia/contrib/evscaperoom/states` to somewhere in your game folder (let's
assume you put it under `mygame/world/`).
Next you need to re-point Evennia to look for states in this new location. Add
the following to your `mygame/server/conf/settings.py` file:
```python
EVSCAPEROOM_STATE_PACKAGE = "world.states"
```
Reload and the example evscaperoom should still work, but you can now modify and expand
it from your game dir!
## Other useful settings
There are a few other settings that may be useful:
- `EVSCAPEROOM_START_STATE` - default is `state_001_start` and is the name of the
state-module to start from (without `.py`). You can change this if you want some
other naming scheme.
- `HELP_SUMMARY_TEXT` - this is the help blurb shown when entering `help` in
the room without an argument. The original is found at the top of
`evennia/contrib/evscaperoom/commands.py`.
# Playing the game
You should start by `look`ing around and at objects.
The `examine <object>` command allows you to 'focus' on an object. When you do
you'll learn actions you could try for the object you are focusing on, such as
turning it around, read text on it or use it with some other object. Note that
more than one player can focus on the same object, so you won't block anyone
when you focus. Focusing on another object or use `examine` again will remove
focus.
There is also a full hint system.
# Technical
When connecting to the game, the user has the option to join an existing room
(which may already be in some state of ongoing progress), or may create a fresh
room for them to start solving on their own (but anyone may still join them later).
The room will go through a series of 'states' as the players progress through
its challenges. These states are describes as modules in .states/ and the
room will load and execute the State-object within each module to set up
and transition between states as the players progress. This allows for isolating
the states from each other and will hopefully make it easier to track
the logic and (in principle) inject new puzzles later.
Once no players remain in the room, the room and its state will be wiped.
# Design Philosophy
Some basic premises inspired the design of this.
- You should be able to resolve the room alone. So no puzzles should require the
collaboration of multiple players. This is simply because there is no telling
if others will actually be online at a given time (or stay online throughout).
- You should never be held up by the actions/inactions of other players. This
is why you cannot pick up anything (no inventory system) but only
focus/operate on items. This avoids the annoying case of a player picking up
a critical piece of a puzzle and then logging off.
- A room's state changes for everyone at once. My first idea was to have a given
room have different states depending on who looked (so a chest could be open
and closed to two different players at the same time). But not only does this
add a lot of extra complexity, it also defeats the purpose of having multiple
players. This way people can help each other and collaborate like in a 'real'
escape room. For people that want to do it all themselves I instead made it
easy to start "fresh" rooms for them to take on.
All other design decisions flowed from these.

View file

@ -0,0 +1,752 @@
"""
Commands for the Evscaperoom. This contains all in-room commands as well as
admin debug-commands to help development.
Gameplay commands
- `look` - custom look
- `focus` - focus on object (to perform actions on it)
- `<action> <obj>` - arbitrary interaction with focused object
- `stand` - stand on the floor, resetting any position
- `emote` - free-form emote
- `say/whisper/shout` - simple communication
Other commands
- `evscaperoom` - starts the evscaperoom top-level menu
- `help` - custom in-room help command
- `options` - set game/accessibility options
- `who` - show who's in the room with you
- `quit` - leave a room, return to menu
Admin/development commands
- `jumpstate` - jump to specific room state
- `flag` - assign a flag to an object
- `createobj` - create a room-object set up for Evscaperoom
"""
import re
from django.conf import settings
from evennia import SESSION_HANDLER
from evennia import Command, CmdSet, InterruptCommand, default_cmds
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))
_RE_ARGSPLIT = re.compile(r"\s(with|on|to|in|at)\s", re.I + re.U)
_RE_EMOTE_SPEECH = re.compile(r"(\".*?\")|(\'.*?\')")
_RE_EMOTE_NAME = re.compile(r"(/\w+)")
_RE_EMOTE_PROPER_END = re.compile(r"\.$|\.[\'\"]$|\![\'\"]$|\?[\'\"]$")
# configurable help
if hasattr(settings, "EVSCAPEROOM_HELP_SUMMARY_TEXT"):
_HELP_SUMMARY_TEXT = settings.EVSCAPEROOM_HELP_SUMMARY_TEXT
else:
_HELP_SUMMARY_TEXT = """
|yWhat to do ...|n
- Your goal is to |wescape|n the room. To do that you need to |wlook|n at
your surroundings for clues on how to escape. When you find something
interesting, |wexamine|n it for any actions you could take with it.
|yHow to explore ...|n
- |whelp [obj or command]|n - get usage help (never puzzle-related)
- |woptions|n - set game/accessibility options
- |wlook/l [obj]|n - give things a cursory look.
- |wexamine/ex/e [obj]|n - look closer at an object. Use again to
look away.
- |wstand|n - stand up if you were sitting, lying etc.
|yHow to express yourself ...|n
- |wwho [all]|n - show who's in the room or on server.
- |wemote/pose/: <something>|n - free-form emote. Use /me to refer
to yourself and /name to refer to other
things/players. Use quotes "..." to speak.
- |wsay/; <something>|n - quick-speak your mind
- |wwhisper <something>|n - whisper your mind
- |wshout <something>|n - shout your mind
|yHow to quit like a little baby ...|n
- |wquit / give up|n - admit defeat and give up
"""
_HELP_FALLBACK_TEXT = """
There is no help to be had about |y{this}|n. To look away, use |wexamine|n on
its own or with another object you are interested in.
"""
_QUIT_WARNING = """
|rDo you really want to quit?|n
{warning}
Enter |w'quit'|n again to truly give up.
"""
_QUIT_WARNING_CAN_COME_BACK = """
(Since you are not the last person to leave this room, you |gcan|n get back in here
by joining room '|c{roomname}|n' from the menu. Note however that if you leave
now, any personal achievements you may have gathered so far will be |rlost|n!)
"""
_QUIT_WARNING_LAST_CHAR = """
(You are the |rlast|n player to leave this room ('|c{roomname}|n'). This means that when you
leave, this room will go away and you |rwon't|n be able to come back to it!)
"""
class CmdEvscapeRoom(Command):
"""
Base command parent for all Evscaperoom commands.
This operates on the premise of 'focus' - the 'focus'
is set on the caller, then subsequent commands will
operate on that focus. If no focus is set,
the operation will be general or on the room.
Syntax:
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"
# these flags allow child classes to determine how strict parsing for obj1/obj2 should be
# (if they are given at all):
# True - obj1/obj2 must be found as Objects, otherwise it's an error aborting command
# False - obj1/obj2 will remain None, instead self.arg1, arg2 will be stored as strings
# None - if obj1/obj2 are found as Objects, set them, otherwise set arg1/arg2 as strings
obj1_search = None
obj2_search = None
def _search(self, query, required):
"""
This implements the various search modes
Args:
query (str): The search query
required (bool or None): This defines if query *must* be
found to match a single local Object or not. If None,
a non-match means returning the query unchanged. When
False, immediately return the query. If required is False,
don't search at all.
Return:
match (Object or str): The match or the search string depending
on the `required` mode.
Raises:
InterruptCommand: Aborts the command quietly.
Notes:
The _AT_SEARCH_RESULT function will handle all error messaging
for us.
"""
if required is False:
return None, query
matches = self.caller.search(query, quiet=True)
if not matches or len(matches) > 1:
if required:
if not query:
self.caller.msg("You must give an argument.")
else:
_AT_SEARCH_RESULT(matches, self.caller, query=query)
raise InterruptCommand
else:
return None, query
else:
return matches[0], None
def parse(self):
"""
Parse incoming arguments for use in all child classes.
"""
caller = self.caller
self.args = self.args.strip()
# splits to either ['obj'] or e.g. ['obj', 'on', 'obj']
parts = [part.strip() for part in _RE_ARGSPLIT.split(" " + self.args, 1)]
nparts = len(parts)
self.obj1 = None
self.arg1 = None
self.prep = None
self.obj2 = None
self.arg2 = None
if nparts == 1:
self.obj1, self.arg1 = self._search(parts[0], self.obj1_search)
elif nparts == 3:
obj1, self.prep, obj2 = parts
self.obj1, self.arg1 = self._search(obj1, self.obj1_search)
self.obj2, self.arg2 = self._search(obj2, self.obj2_search)
self.room = caller.location
self.roomstate = self.room.db.state
@property
def focus(self):
return self.caller.attributes.get("focus", category=self.room.db.tagcategory)
@focus.setter
def focus(self, obj):
self.caller.attributes.add("focus", obj, category=self.room.tagcategory)
@focus.deleter
def focus(self):
self.caller.attributes.remove("focus", category=self.room.tagcategory)
class CmdGiveUp(CmdEvscapeRoom):
"""
Give up
Usage:
give up
Abandons your attempts at escaping and of ever winning the pie-eating contest.
"""
key = "give up"
aliases = ("abort", "chicken out", "quit", "q")
def func(self):
from .menu import run_evscaperoom_menu
nchars = len(self.room.get_all_characters())
if nchars == 1:
warning = _QUIT_WARNING_LAST_CHAR.format(roomname=self.room.name)
warning = _QUIT_WARNING.format(warning=warning)
else:
warning = _QUIT_WARNING_CAN_COME_BACK.format(roomname=self.room.name)
warning = _QUIT_WARNING.format(warning=warning)
ret = yield(warning)
if ret.upper() == "QUIT":
self.msg("|R ... Oh. Okay then. Off you go.|n\n")
yield(1)
self.room.log(f"QUIT: {self.caller.key} used the quit command")
# manually call move hooks
self.room.msg_room(self.caller, f"|r{self.caller.key} gave up and was whisked away!|n")
self.room.at_object_leave(self.caller, self.caller.home)
self.caller.move_to(self.caller.home, quiet=True, move_hooks=False)
# back to menu
run_evscaperoom_menu(self.caller)
else:
self.msg("|gYou're staying? That's the spirit!|n")
class CmdLook(CmdEvscapeRoom):
"""
Look at the room, an object or the currently focused object
Usage:
look [obj]
"""
key = "look"
aliases = ["l", "ls"]
obj1_search = None
obj2_search = None
def func(self):
caller = self.caller
target = self.obj1 or self.obj2 or self.focus or self.room
# the at_look hook will in turn call return_appearance and
# pass the 'unfocused' kwarg to it
txt = caller.at_look(target, unfocused=(target and target != self.focus))
self.room.msg_char(caller, txt, client_type="look")
class CmdWho(CmdEvscapeRoom, default_cmds.CmdWho):
"""
List other players in the game.
Usage:
who
who all
Show who is in the room with you, or (with who all), who is online on the
server as a whole.
"""
key = "who"
obj1_search = False
obj2_search = False
def func(self):
caller = self.caller
if self.args == 'all':
table = self.style_table("|wName", "|wRoom")
sessions = SESSION_HANDLER.get_sessions()
for session in sessions:
puppet = session.get_puppet()
if puppet:
location = puppet.location
locname = location.key if location else "(Outside somewhere)"
table.add_row(puppet, locname)
else:
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)")
else:
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}")
caller.msg(txt)
class CmdSpeak(Command):
"""
Perform an communication action.
Usage:
say <text>
whisper
shout
"""
key = "say"
aliases = [";", "shout", "whisper"]
arg_regex = r"\w|\s|$"
def func(self):
args = self.args.strip()
caller = self.caller
action = self.cmdname
action = "say" if action == ';' else action
room = self.caller.location
if not self.args:
caller.msg(f"What do you want to {action}?")
return
if action == "shout":
args = f"|c{args.upper()}|n"
elif action == "whisper":
args = f"|C({args})|n"
else:
args = f"|c{args}|n"
message = f"~You ~{action}: {args}"
if hasattr(room, "msg_room"):
room.msg_room(caller, message)
room.log(f"{action} by {caller.key}: {args}")
class CmdEmote(Command):
"""
Perform a free-form emote. Use /me to
include yourself in the emote and /name
to include other objects or characters.
Use "..." to enact speech.
Usage:
emote <emote>
:<emote
Example:
emote /me smiles at /peter
emote /me points to /box and /lever.
"""
key = "emote"
aliases = [":", "pose"]
arg_regex = r"\w|\s|$"
def you_replace(match):
return match
def room_replace(match):
return match
def func(self):
emote = self.args.strip()
if not emote:
self.caller.msg("Usage: emote /me points to /door, saying \"look over there!\"")
return
speech_clr = "|c"
obj_clr = "|y"
self_clr = "|g"
player_clr = "|b"
add_period = not _RE_EMOTE_PROPER_END.search(emote)
emote = _RE_EMOTE_SPEECH.sub(speech_clr + r"\1\2|n", emote)
room = self.caller.location
characters = room.get_all_characters()
logged = False
for target in characters:
txt = []
self_refer = False
for part in _RE_EMOTE_NAME.split(emote):
nameobj = None
if part.startswith("/"):
name = part[1:]
if name == "me":
nameobj = self.caller
self_refer = True
else:
match = self.caller.search(name, quiet=True)
if len(match) == 1:
nameobj = match[0]
if nameobj:
if target == nameobj:
part = f"{self_clr}{nameobj.get_display_name(target)}|n"
elif nameobj in characters:
part = f"{player_clr}{nameobj.get_display_name(target)}|n"
else:
part = f"{obj_clr}{nameobj.get_display_name(target)}|n"
txt.append(part)
if not self_refer:
if target == self.caller:
txt = [f"{self_clr}{self.caller.get_display_name(target)}|n "] + txt
else:
txt = [f"{player_clr}{self.caller.get_display_name(target)}|n "] + txt
txt = "".join(txt).strip() + ("." if add_period else "")
if not logged and hasattr(self.caller.location, "log"):
self.caller.location.log(f"emote: {txt}")
logged = True
target.msg(txt)
class CmdFocus(CmdEvscapeRoom):
"""
Focus your attention on a target.
Usage:
focus <obj>
Once focusing on an object, use look to get more information about how it
looks and what actions is available.
"""
key = "focus"
aliases = ["examine", "e", "ex", "unfocus"]
obj1_search = None
def func(self):
if self.obj1:
old_focus = self.focus
if hasattr(old_focus, "at_unfocus"):
old_focus.at_unfocus(self.caller)
if not hasattr(self.obj1, "at_focus"):
self.caller.msg("Nothing of interest there.")
return
if self.focus != self.obj1:
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:
self.caller.msg("What do you want to focus on?")
else:
old_focus = self.focus
del self.focus
self.caller.msg(f"You no longer focus on |y{old_focus.key}|n.")
class CmdOptions(CmdEvscapeRoom):
"""
Start option menu
Usage:
options
"""
key = "options"
aliases = ["option"]
def func(self):
from .menu import run_option_menu
run_option_menu(self.caller, self.session)
class CmdGet(CmdEvscapeRoom):
"""
Use focus / examine instead.
"""
key = "get"
aliases = ["inventory", "i", "inv", "give"]
def func(self):
self.caller.msg("Use |wfocus|n or |wexamine|n for handling objects.")
class CmdRerouter(default_cmds.MuxCommand):
"""
Interact with an object in focus.
Usage:
<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
# cmdset (we want to keep most of them).
key = "open"
aliases = ["@dig", "@open"]
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)
class CmdFocusInteraction(CmdEvscapeRoom):
"""
Interact with an object in focus.
Usage:
<action> [arg]
This is a special catch-all command which will operate on
the current focus. It will look for a method
`focused_object.at_focus_<action>(caller, **kwargs)` and call
it. This allows objects to just add a new hook to make that
action apply to it. The obj1, prep, obj2, arg1, arg2 are passed
as keys into the method.
"""
# all commands not matching something else goes here.
key = syscmdkeys.CMD_NOMATCH
obj1_search = None
obj2_search = None
def parse(self):
"""
We assume this type of command is always on the form `command [arg]`
"""
self.args = self.args.strip()
parts = self.args.split(None, 1)
if not self.args:
self.action, self.args = "", ""
elif len(parts) == 1:
self.action = parts[0]
self.args = ""
else:
self.action, self.args = parts
self.room = self.caller.location
def func(self):
focused = self.focus
action = self.action
if focused and hasattr(focused, f"at_focus_{action}"):
# there is a suitable hook to call!
getattr(focused, f"at_focus_{action}")(self.caller, args=self.args)
else:
self.caller.msg("Hm?")
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)
if pos:
# we have a position, clean up.
obj, position = pos
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:
self.caller.msg("You are already standing.")
class CmdHelp(CmdEvscapeRoom, default_cmds.CmdHelp):
"""
Get help.
Usage:
help <topic> or <command>
"""
key = 'help'
aliases = ['?']
def func(self):
if self.obj1:
if hasattr(self.obj1, "get_help"):
helptxt = self.obj1.get_help(self.caller)
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.")
elif self.arg1:
# fall back to the normal help command
super().func()
return
else:
helptxt = _HELP_SUMMARY_TEXT
self.caller.msg(helptxt.rstrip())
# Debug/help command
class CmdCreateObj(CmdEvscapeRoom):
"""
Create command, only for Admins during debugging.
Usage:
createobj name[:typeclass]
Here, :typeclass is a class in evscaperoom.commands
"""
key = "createobj"
aliases = ["cobj"]
locks = "cmd:perm(Admin)"
obj1_search = False
obj2_search = False
def func(self):
caller = self.caller
args = self.args
if not args:
caller.msg("Usage: createobj name[:typeclass]")
return
typeclass = "EvscaperoomObject"
if ":" in args:
name, typeclass = (part.strip() for part in args.rsplit(":", 1))
if typeclass.startswith("state_"):
# a state class
typeclass = "evscaperoom.states." + typeclass
else:
name = args.strip()
obj = create_evscaperoom_object(typeclass=typeclass, key=name, location=self.room)
caller.msg(f"Created new object {name} ({obj.typeclass_path}).")
class CmdSetFlag(CmdEvscapeRoom):
"""
Assign a flag to an object. Admin use only
Usage:
flag <obj> with <flagname>
"""
key = "flag"
aliases = ["setflag"]
locks = "cmd:perm(Admin)"
obj1_search = True
obj2_search = False
def func(self):
if not self.arg2:
self.caller.msg("Usage: flag <obj> with <flagname>")
return
if hasattr(self.obj1, "set_flag"):
if self.obj1.check_flag(self.arg2):
self.obj1.unset_flag(self.arg2)
self.caller.msg(f"|rUnset|n flag '{self.arg2}' on {self.obj1}.")
else:
self.obj1.set_flag(self.arg2)
self.caller.msg(f"|gSet|n flag '{self.arg2}' on {self.obj1}.")
else:
self.caller.msg(f"Cannot set flag on {self.obj1}.")
class CmdJumpState(CmdEvscapeRoom):
"""
Jump to a given state.
Args:
jumpstate <statename>
"""
key = "jumpstate"
locks = "cmd:perm(Admin)"
obj1_search = False
obj2_search = False
def func(self):
self.caller.msg(f"Trying to move to state {self.args}")
self.room.next_state(self.args)
# 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
def at_cmdset_creation(self):
self.add(CmdHelp())
self.add(CmdLook())
self.add(CmdGiveUp())
self.add(CmdFocus())
self.add(CmdSpeak())
self.add(CmdEmote())
self.add(CmdFocusInteraction())
self.add(CmdStand())
self.add(CmdWho())
self.add(CmdOptions())
# rerouters
self.add(CmdGet())
self.add(CmdRerouter())
# admin commands
self.add(CmdCreateObj())
self.add(CmdSetFlag())
self.add(CmdJumpState())

View file

@ -0,0 +1,325 @@
"""
Start menu
This is started from the `evscaperoom` command.
Here player user can set their own description as well as select to create a
new room (to start from scratch) or join an existing room (with other players).
"""
from evennia import EvMenu
from evennia.utils.evmenu import list_node
from evennia.utils import create, justify, list_to_string
from evennia.utils import logger
from .room import EvscapeRoom
from .utils import create_fantasy_word
# ------------------------------------------------------------
# Main menu
# ------------------------------------------------------------
_START_TEXT = """
|mEv|rScape|mRoom|n
|x- an escape-room experience using Evennia|n
You are |c{name}|n - {desc}|n.
Make a selection below.
"""
_CREATE_ROOM_TEXT = """
This will create a |ynew, empty room|n to challenge you.
Other players can be thrown in there at any time.
Remember that if you give up and are the last person to leave, that particular
room will be gone!
|yDo you want to create (and automatically join) a new room?|n")
"""
_JOIN_EXISTING_ROOM_TEXT = """
This will have you join an existing room ({roomname}).
This is {percent}% complete and has {nplayers} player(s) in it already:
{players}
|yDo you want to join this room?|n
"""
def _move_to_room(caller, raw_string, **kwargs):
"""
Helper to move a user to a 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.
old_location = caller.location
caller.location = room
room.at_object_receive(caller, old_location)
return "node_quit", {"quiet": True}
def _create_new_room(caller, raw_string, **kwargs):
# create a random name, retrying until we find
# a unique one
key = create_fantasy_word(length=5, capitalize=True)
while EvscapeRoom.objects.filter(db_key=key):
key = create_fantasy_word(length=5, capitalize=True)
room = create.create_object(EvscapeRoom, key=key)
# we must do this once manually for the new room
room.statehandler.init_state()
_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.")
room.log(f"JOIN: {caller.key} created and joined room")
return "node_quit", {"quiet": True}
def _get_all_rooms(caller):
"""
Get a list of all available rooms and store the mapping
between option and room so we get to it later.
"""
room_option_descs = []
room_map = {}
for room in EvscapeRoom.objects.all():
if not room.pk or room.db.deleting:
continue
stats = room.db.stats or {"progress": 0}
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})")
room_map[desc] = room
room_option_descs.append(desc)
caller.ndb._menutree.room_map = room_map
return room_option_descs
def _select_room(caller, menuchoice, **kwargs):
"""
Get a room from the selection using the mapping we created earlier.
"""
room = caller.ndb._menutree.room_map[menuchoice]
return "node_join_room", {"room": room}
@list_node(_get_all_rooms, _select_room)
def node_start(caller, raw_string, **kwargs):
text = _START_TEXT.strip()
text = text.format(name=caller.key, desc=caller.db.desc)
# 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"})
return text, options
def node_set_desc(caller, raw_string, **kwargs):
current_desc = kwargs.get('desc', caller.db.desc)
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()
if 5 < len(desc) < 40:
return None, {"desc": raw_string.strip()}
else:
caller.msg("|rYour description must be 5-40 characters long.|n")
return None
def _set_description(caller, raw_string, **kwargs):
caller.db.desc = kwargs.get("desc")
caller.msg("You set your description!")
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"})
return text, options
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"})
return text, options
def node_join_room(caller, raw_string, **kwargs):
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']),
nplayers=len(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)})
return text, options
def node_quit(caller, raw_string, **kwargs):
quiet = kwargs.get("quiet")
text = ""
if not quiet:
text = "Goodbye for now!\n"
# we check an Attribute on the caller to see if we should
# leave the game entirely when leaving
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")
return text, None # empty options exit the menu
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)
def options_formatter(self, optionlist):
main_options = []
room_choices = []
for key, desc in optionlist:
if key.isdigit():
room_choices.append((key, desc))
else:
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)
# access function
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}
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={})
options["things_style"] = kwargs.get("value", 2)
caller.attributes.add("options", options, category=room.tagcategory)
return None, kwargs # rerun node
def _toggle_screen_reader(caller, raw_string, **kwargs):
session = kwargs['session']
# flip old setting
session.protocol_flags["SCREENREADER"] = not session.protocol_flags.get("SCREENREADER", False)
# sync setting with portal
session.sessionhandler.session_portal_sync(session)
return None, kwargs # rerun node
def node_options(caller, raw_string, **kwargs):
text = "|cOption menu|n\n('|wq|nuit' to return)"
room = caller.location
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
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)})
return text, options
class OptionsMenu(EvMenu):
"""
Custom display of Option menu
"""
def node_formatter(self, nodetext, optionstext):
return f"{nodetext}\n\n{optionstext}"
# access function
def run_option_menu(caller, session):
"""
Run option menu in-game
"""
menutree = {"node_start": node_options}
OptionsMenu(caller, menutree, startnode="node_start",
cmd_on_exit="look", auto_quit=True, startnode_input=("", {"session": session}))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,237 @@
"""
Room class and mechanics for the Evscaperoom.
This is a special room class that not only depicts the evscaperoom itself, it
also acts as a central store for the room state, score etc. When deleting this,
that particular escaperoom challenge should be gone.
"""
from evennia import DefaultRoom, DefaultCharacter, DefaultObject
from evennia import utils
from evennia.utils.ansi import strip_ansi
from evennia import logger
from evennia.locks.lockhandler import check_lockstring
from evennia.utils.utils import lazy_property, list_to_string
from .objects import EvscaperoomObject
from .commands import CmdSetEvScapeRoom
from .state import StateHandler
class EvscapeRoom(EvscaperoomObject, DefaultRoom):
"""
The room to escape from.
"""
def at_object_creation(self):
"""
Called once, when the room is first created.
"""
super().at_object_creation()
# starting state
self.db.state = None # name
self.db.prev_state = None
# this is used for tagging of all objects belonging to this
# particular room instance, so they can be cleaned up later
# this is accessed through the .tagcategory getter.
self.db.tagcategory = "evscaperoom_{}".format(self.key)
# room progress statistics
self.db.stats = {
"progress": 0, # in percent
"score": {}, # reason: score
"max_score": 100,
"hints_used": 0, # total across all states
"hints_total": 41,
"total_achievements": 14
}
self.cmdset.add(CmdSetEvScapeRoom, permanent=True)
self.log("Room created and log started.")
@lazy_property
def statehandler(self):
return StateHandler(self)
@property
def state(self):
return self.statehandler.current_state
def log(self, message, caller=None):
"""
Log to a file specificially for this room.
"""
caller = f"[caller.key]: " if caller else ""
logger.log_file(
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']:
self.log(f"score: {reason} ({new_score}pts)")
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
def achievement(self, caller, achievement, subtext=""):
"""
Give the caller a personal achievment. You will only
ever get one of the same type
Args:
caller (Object): The receiver of the achievement.
achievement (str): The title/name of the achievement.
subtext (str, optional): Eventual subtext/explanation
of the achievement.
"""
achievements = caller.attributes.get(
"achievements", category=self.tagcategory)
if not achievements:
achievements = {}
if achievement not in achievements:
self.log(f"achievement: {caller} earned '{achievement}' - {subtext}")
achievements[achievement] = subtext
caller.attributes.add("achievements", achievements, category=self.tagcategory)
def get_all_characters(self):
"""
Get the player characters in the room.
Returns:
chars (Queryset): The characters.
"""
return DefaultCharacter.objects.filter_family(db_location=self)
def set_flag(self, flagname):
self.db.flags[flagname] = True
def unset_flag(self, flagname):
if flagname in self.db.flags:
del self.db.flags[flagname]
def check_flag(self, flagname):
return self.db.flags.get(flagname, False)
def check_perm(self, caller, permission):
return check_lockstring(caller, f"dummy:perm({permission})")
def tag_character(self, character, tag, category=None):
"""
Tag a given character in this room.
Args:
character (Character): Player character to tag.
tag (str): Tag to set.
category (str, optional): Tag-category. If unset, use room's
tagcategory.
"""
category = category if category else self.db.tagcategory
character.tags.add(tag, category=category)
def tag_all_characters(self, tag, category=None):
"""
Set a given tag on all players in the room.
Args:
room (EvscapeRoom): The room to escape from.
tag (str): The tag to set.
category (str, optional): If unset, will use the room's tagcategory.
"""
category = category if category else self.tagcategory
for char in self.get_all_characters():
char.tags.add(tag, category=category)
def character_cleanup(self, char):
"""
Clean all custom tags/attrs on a character.
"""
if self.tagcategory:
char.tags.remove(category=self.tagcategory)
char.attributes.remove(category=self.tagcategory)
def character_exit(self, char):
"""
Have a character exit the room - return them to the room menu.
"""
self.log(f"EXIT: {char} left room")
from .menu import run_evscaperoom_menu
self.character_cleanup(char)
char.location = char.home
# check if room should be deleted
if len(self.get_all_characters()) < 1:
self.delete()
# we must run menu after deletion so we don't include this room!
run_evscaperoom_menu(char)
# Evennia hooks
def at_object_receive(self, moved_obj, source_location):
"""
Called when an object arrives in the room. This can be used to
sum up the situation, set tags etc.
"""
if utils.inherits_from(moved_obj, "evennia.objects.objects.DefaultCharacter"):
self.log(f"JOIN: {moved_obj} joined room")
self.state.character_enters(moved_obj)
def at_object_leave(self, moved_obj, target_location, **kwargs):
"""
Called when an object leaves the room; if this is a Character we need
to clean them up and move them to the menu state.
"""
if utils.inherits_from(moved_obj, "evennia.objects.objects.DefaultCharacter"):
self.character_cleanup(moved_obj)
if len(self.get_all_characters()) <= 1:
# after this move there'll be no more characters in the room - delete the room!
self.delete()
# logger.log_info("DEBUG: Don't delete room when last player leaving")
def delete(self):
"""
Delete this room and all items related to it. Only move the players.
"""
self.db.deleting = True
for char in self.get_all_characters():
self.character_exit(char)
for obj in self.contents:
obj.delete()
self.log("END: Room cleaned up and deleted")
return super().delete()
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 "")
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])
return f"{self.db.desc}{pos}{admin_only}"

View file

@ -0,0 +1,32 @@
"""
A simple cleanup script to wipe empty rooms
(This can happen if users leave 'uncleanly', such as by closing their browser
window)
Just start this global script manually or at server creation.
"""
from evennia import DefaultScript
from evscaperoom.room import EvscapeRoom
class CleanupScript(DefaultScript):
def at_script_creation(self):
self.key = "evscaperoom_cleanup"
self.desc = "Cleans up empty evscaperooms"
self.interval = 60 * 15
self.persistent = True
def at_repeat(self):
for room in EvscapeRoom.objects.all():
if not room.get_all_characters():
# this room is empty
room.log("END: Room cleaned by garbage collector.")
room.delete()

View file

@ -0,0 +1,293 @@
"""
States represent the sequence of states the room goes through.
This module includes the BaseState class and the StateHandler
for managing states on the room.
The state handler operates on an Evscaperoom and changes
its state from one to another.
A given state is given as a module in states/ package. The
state is identified by its module name.
"""
from django.conf import settings
from functools import wraps
from evennia import utils
from evennia import logger
from .objects import EvscaperoomObject
from .utils import create_evscaperoom_object, msg_cinematic, parse_for_things
# state setup
if hasattr(settings, "EVSCAPEROOM_STATE_PACKAGE"):
_ROOMSTATE_PACKAGE = settings.EVSCAPEROOM_STATE_PACKAGE
else:
_ROOMSTATE_PACKAGE = "evennia.contrib.evscaperoom.states"
if hasattr(settings, "EVSCAPEROOM_START_STATE"):
_FIRST_STATE = settings.EVSCAPEROOM_START_STATE
else:
_FIRST_STATE = "state_001_start"
_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
self.prev_state_name = room.db.prev_state
self.current_state = None
self.current_state = self.load_state(self.current_state_name)
def load_state(self, statename):
"""
Load state without initializing it
"""
try:
mod = utils.mod_import(f"{_ROOMSTATE_PACKAGE}.{statename}")
except Exception as err:
logger.log_trace()
self.room.msg_room(None, f"|rBUG: Could not load state {statename}: {err}!")
self.room.msg_room(None, f"|rBUG: Falling back to {self.current_state_name}")
return
state = mod.State(self, self.room)
return state
def init_state(self):
"""
Initialize a new state
"""
self.current_state.init()
def next_state(self, next_state=None):
"""
Check if the current state is finished. This should be called whenever
the players do actions that may affect the state of the room.
Args:
next_state (str, optional): If given, override the next_state given
by the current state's check() method with this - this allows
for branching paths (but the current state must still first agree
that the check passes).
Returns:
state_changed (bool): True if the state changed, False otherwise.
"""
# allows the state to enforce/customize what the next state should be
next_state_name = self.current_state.next(next_state)
if next_state_name:
# we are ready to move on!
next_state = self.load_state(next_state_name)
if not next_state:
raise RuntimeError(f"Could not load new state {next_state_name}!")
self.prev_state_name = self.current_state_name
self.current_state_name = next_state_name
self.current_state.clean()
self.prev_state = self.current_state
self.current_state = next_state
self.init_state()
self.room.db.prev_state = self.prev_state_name
self.room.db.state = self.current_state_name
return True
return False
# 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 = []
def __init__(self, handler, room):
"""
Initializer.
Args:
room (EvscapeRoom): The room tied to this state.
handler (StateHandler): Back-reference to the handler
storing this state.
"""
self.handler = handler
self.room = room
# the name is derived from the name of the module
self.name = self.__class__.__module__
def __str__(self):
return self.__class__.__module__
def __repr__(self):
return str(self)
def _catch_errors(self, method):
"""
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")
raise # TODO
return decorator
def __getattribute__(self, key):
"""
Always wrap all callables in the error-handler
"""
val = _GA(self, key)
if callable(val):
return _GA(self, "_catch_errors")(val)
return val
def get_hint(self):
"""
Get a hint for how to solve this state.
"""
hint_level = self.room.attributes.get("state_hint_level", default=-1)
next_level = hint_level + 1
if next_level < len(self.hints):
# 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']})")
return self.hints[next_level]
else:
# no more hints for this state
return None
# helpers
def msg(self, message, target=None, borders=False, cinematic=False):
"""
Display messsage to everyone in room, or given target.
"""
if cinematic:
message = msg_cinematic(message, borders=borders)
if target:
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))
else:
self.room.msg_room(None, message)
def cinematic(self, message, target=None):
"""
Display a 'cinematic' sequence - centered, with borders.
"""
self.msg(message, target=target, borders=True, cinematic=True)
def create_object(self, typeclass=None, key='testobj', location=None, **kwargs):
"""
This is a convenience-wrapper for quickly building EvscapeRoom objects.
Kwargs:
typeclass (str): This can take just the class-name in the evscaperoom's
objects.py module. Otherwise, a full path or the actual class
is needed (for custom state objects, just give the class directly).
key (str): Name of object.
location (Object): If not given, this will be the current room.
kwargs (any): Will be passed into create_object.
Returns:
new_obj (Object): The newly created object, if any.
"""
if not location:
location = self.room
return create_evscaperoom_object(
typeclass=typeclass, key=key, location=location,
tags=[("room", self.room.tagcategory.lower())], **kwargs)
def get_object(self, key):
"""
Find a named *non-character* object for this state in this room.
Args:
key (str): Object to search for.
Returns:
obj (Object): Object in the room.
"""
match = EvscaperoomObject.objects.filter_family(
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
return match[0]
# state methods
def init(self):
"""
Initializes the state (usually by modifying the room in some way)
"""
pass
def clean(self):
"""
Any cleanup operations after the state ends.
"""
self.room.db.state_hint_level = -1
def next(self, next_state=None):
"""
Get the next state after this one.
Args:
next_state (str, optional): This allows the calling code
to redirect to a different state than the 'default' one
(creating branching paths in the game). Override this method
to customize (by default the input will always override default
set on the class)
Returns:
state_name (str or None): Name of next state to switch to. None
to remain in this state. By default we check the room for the
"finished" flag be set.
"""
return next_state or self.next_state
def character_enters(self, character):
"""
Called when character enters the room in this state.
"""
pass
def character_leaves(self, character):
"""
Called when character is whisked away (usually because of
quitting). This method cannot influence the move itself; it
happens just before room.character_cleanup()
"""
pass

View file

@ -0,0 +1,23 @@
# Room state modules
The Evscaperoom goes through a series of 'states' as the players solve puzzles
and progress towards escaping. The States are managed by the StateHandler. When
a certain set of criteria (different for each state) have been fulfilled, the
state ends by telling the StateHandler what state should be loaded next.
A 'state' is a series of Python instructions in a module. A state could mean
the room description changing or new objects appearing, or flag being set that
changes the behavior of existing objects.
The states are stored in Python modules, with file names on the form
`state_001_<name_of_state>.py`. The numbers help organize the states in the file
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
methods on this class will be called to initialize the state and clean up etc.
There are no other restrictions on the module.
The first state (*when the room is created) defaults to being `state_001_start.py`,
this can be changed with `settings.EVSCAPEROOM_STATE_STATE`.

View file

@ -0,0 +1,176 @@
"""
First room state
This simple example sets up an empty one-state room with a door and a key.
After unlocking, opening the door and leaving the room, the player is
teleported back to the evscaperoom menu.
"""
from evennia.contrib.evscaperoom.state import BaseState
from evennia.contrib.evscaperoom import objects
GREETING = """
This is the situation, {name}:
You are locked in this room ... get out! Simple as that!
"""
ROOM_DESC = """
This is a featureless room. On one wall is a *door. On the other wall is a
*button marked "GET HELP".
There is a *key lying on the floor.
"""
# Example object
DOOR_DESC = """
This is a simple example door leading out of the room.
"""
class Door(objects.Openable):
"""
The door leads out of the room.
"""
start_open = False
def at_object_creation(self):
super().at_object_creation()
self.set_flag("door") # target for key
def at_open(self, caller):
# only works if the door was unlocked
self.msg_room(caller, f"~You ~open *{self.key}")
def at_focus_leave(self, caller, **kwargs):
if self.check_flag("open"):
self.msg_room(caller, "~You ~leave the room!")
self.msg_char(caller, "Congrats!")
# exit evscaperoom
self.room.character_exit(caller)
else:
self.msg_char(caller, "The door is closed. You cannot leave!")
# key
KEY_DESC = """
A simple room key. A paper label is attached to it.
"""
KEY_READ = """
A paper label is attached to the key. It reads.
|rOPEN THE DOOR WITH ME|n
A little on the nose, but this is an example room after all ...
"""
KEY_APPLY = """
~You insert the *key into the *door, turns it and ... the door unlocks!
"""
class Key(objects.Insertable, objects.Readable):
"A key for opening the door"
# where this key applies (must be flagged as such)
target_flag = "door"
def at_apply(self, caller, action, obj):
obj.set_flag("unlocked") # unlocks the door
self.msg_room(caller, KEY_APPLY.strip())
def at_read(self, caller, *args, **kwargs):
self.msg_char(caller, KEY_READ.strip())
def get_cmd_signatures(self):
return [], "You can *read the label or *insert the key into something."
# help button
BUTTON_DESC = """
On the wall is a button marked
PRESS ME FOR HELP
"""
class HelpButton(objects.EvscaperoomObject):
def at_focus_push(self, caller, **kwargs):
"this adds the 'push' action to the button"
hint = self.room.state.get_hint()
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")
# state
STATE_HINT_LVL1 = """
The door is locked. What is usually used for unlocking doors?
"""
STATE_HINT_LVL2 = """
This is just an example. Do what comes naturally. Examine what's on the floor.
"""
STATE_HINT_LVL3 = """
Insert the *key in the *door. Then open the door and leave! Yeah, it's really
that simple.
"""
class State(BaseState):
"""
This class (with exactly this name) must exist in every state module.
"""
# this makes these hints available to the .get_hint method.
hints = [STATE_HINT_LVL1,
STATE_HINT_LVL2,
STATE_HINT_LVL3]
def character_enters(self, char):
"Called when char enters room at this state"
self.cinematic(GREETING.format(name=char.key))
def init(self):
"Initialize state"
# describe the room
self.room.db.desc = ROOM_DESC
# create the room objects
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.db.desc = KEY_DESC.strip()
button = self.create_object(
HelpButton, key="button", aliases=["help button"])
button.db.desc = BUTTON_DESC.strip()
def clean(self):
"Cleanup operations on the state, when it's over"
super().clean()

View file

@ -0,0 +1,313 @@
"""
Unit tests for the Evscaperoom
"""
import inspect
import pkgutil
from os import path
from evennia.commands.default.tests import CommandTest
from evennia import InterruptCommand
from evennia.utils.test_resources import EvenniaTest
from evennia.utils import mod_import
from . import commands
from . import state as basestate
from . import objects
from . import utils
class TestEvscaperoomCommands(CommandTest):
def setUp(self):
super().setUp()
self.room1 = utils.create_evscaperoom_object(
"evscaperoom.room.EvscapeRoom", key='Testroom')
self.char1.location = self.room1
self.obj1.location = self.room1
def test_base_search(self):
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
self.assertEqual((self.obj1, None), cmd._search("Obj", True))
self.assertEqual((None, "Obj"), cmd._search("Obj", False))
self.assertEqual((None, "Foo"), cmd._search("Foo", False))
self.assertEqual((None, "Foo"), cmd._search("Foo", None))
self.assertRaises(InterruptCommand, cmd._search, "Foo", True)
def test_base_parse(self):
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
cmd.obj1_search = None
cmd.obj2_search = None
cmd.args = "obj"
cmd.parse()
self.assertEqual(cmd.obj1, self.obj1)
self.assertEqual(cmd.room, self.char1.location)
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
cmd.obj1_search = False
cmd.obj2_search = False
cmd.args = "obj"
cmd.parse()
self.assertEqual(cmd.arg1, "obj")
self.assertEqual(cmd.obj1, None)
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
cmd.obj1_search = None
cmd.obj2_search = None
cmd.args = "obj"
cmd.parse()
self.assertEqual(cmd.obj1, self.obj1)
self.assertEqual(cmd.arg1, None)
self.assertEqual(cmd.arg2, None)
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
cmd.obj1_search = True
cmd.obj2_search = True
cmd.args = "obj at obj"
cmd.parse()
self.assertEqual(cmd.obj1, self.obj1)
self.assertEqual(cmd.obj2, self.obj1)
self.assertEqual(cmd.arg1, None)
self.assertEqual(cmd.arg2, None)
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
cmd.obj1_search = False
cmd.obj2_search = False
cmd.args = "obj at obj"
cmd.parse()
self.assertEqual(cmd.obj1, None)
self.assertEqual(cmd.obj2, None)
self.assertEqual(cmd.arg1, "obj")
self.assertEqual(cmd.arg2, "obj")
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
cmd.obj1_search = None
cmd.obj2_search = None
cmd.args = "obj at obj"
cmd.parse()
self.assertEqual(cmd.obj1, self.obj1)
self.assertEqual(cmd.obj2, self.obj1)
self.assertEqual(cmd.arg1, None)
self.assertEqual(cmd.arg2, None)
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
cmd.obj1_search = None
cmd.obj2_search = None
cmd.args = "foo in obj"
cmd.parse()
self.assertEqual(cmd.obj1, None)
self.assertEqual(cmd.obj2, self.obj1)
self.assertEqual(cmd.arg1, 'foo')
self.assertEqual(cmd.arg2, None)
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
cmd.obj1_search = None
cmd.obj2_search = None
cmd.args = "obj on foo"
cmd.parse()
self.assertEqual(cmd.obj1, self.obj1)
self.assertEqual(cmd.obj2, None)
self.assertEqual(cmd.arg1, None)
self.assertEqual(cmd.arg2, 'foo')
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
cmd.obj1_search = None
cmd.obj2_search = True
cmd.args = "obj on foo"
self.assertRaises(InterruptCommand, cmd.parse)
cmd = commands.CmdEvscapeRoom()
cmd.caller = self.char1
cmd.obj1_search = None
cmd.obj2_search = True
cmd.args = "on obj"
cmd.parse()
self.assertEqual(cmd.obj1, None)
self.assertEqual(cmd.obj2, self.obj1)
self.assertEqual(cmd.arg1, "")
self.assertEqual(cmd.arg2, None)
def test_set_focus(self):
cmd = commands.CmdEvscapeRoom()
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)
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)
# should focus correctly
myobj = utils.create_evscaperoom_object(
objects.EvscaperoomObject, "mytestobj", location=self.room1)
self.call(commands.CmdFocus(), "mytestobj")
self.assertEqual(self.char1.attributes.get(
"focus", category=self.room1.tagcategory), myobj)
def test_look(self):
self.call(commands.CmdLook(), "at obj", "Obj")
self.call(commands.CmdLook(), "obj", "Obj")
self.call(commands.CmdLook(), "obj", "Obj")
def test_speech(self):
self.call(commands.CmdSpeak(), "", "What do you want to say?", cmdstring="")
self.call(commands.CmdSpeak(), "Hello!", "You say: Hello!", cmdstring="")
self.call(commands.CmdSpeak(), "", "What do you want to whisper?", cmdstring="whisper")
self.call(commands.CmdSpeak(), "Hi.", "You whisper: Hi.", cmdstring="whisper")
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")
def test_emote(self):
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')
obj1 = utils.create_evscaperoom_object(
objects.EvscaperoomObject, key="testobj", location=room)
id1 = obj1.id
obj2 = utils.create_evscaperoom_object(
objects.EvscaperoomObject, key="testobj", location=room)
id2 = obj2.id
# we should have created a new object, deleting the old same-named one
self.assertTrue(id1 != id2)
self.assertFalse(bool(obj1.pk))
self.assertTrue(bool(obj2.pk))
def test_parse_for_perspectives(self):
second, third = utils.parse_for_perspectives("~You ~look at the nice book", "TestGuy")
self.assertTrue(second, "You look at the nice book")
self.assertTrue(third, "TestGuy looks at the nice book")
# irregular
second, third = utils.parse_for_perspectives("With a smile, ~you ~were gone", "TestGuy")
self.assertTrue(second, "With a smile, you were gone")
self.assertTrue(third, "With a smile, TestGuy was gone")
def test_parse_for_things(self):
string = "Looking at *book and *key."
self.assertEqual(utils.parse_for_things(string, 0), "Looking at book and key.")
self.assertEqual(utils.parse_for_things(string, 1), "Looking at |ybook|n and |ykey|n.")
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)
self.roomtag = "evscaperoom_{}".format(self.room.key)
def tearDown(self):
self.room.delete()
def test_room_methods(self):
room = self.room
self.char1.location = room
self.assertEqual(room.tagcategory, self.roomtag)
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")
room.tag_all_characters("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)
self.roomtag = "evscaperoom_#{}".format(self.room.id)
def tearDown(self):
self.room.delete()
def _get_all_state_modules(self):
dirname = path.join(path.dirname(__file__), "states")
states = []
for imp, module, ispackage in pkgutil.walk_packages(
path=[dirname], prefix="evscaperoom.states."):
mod = mod_import(module)
states.append(mod)
return states
def test_base_state(self):
st = basestate.BaseState(self.room.statehandler, self.room)
st.init()
obj = st.create_object(objects.Edible, key="apple")
self.assertEqual(obj.key, "apple")
self.assertEqual(obj.__class__, objects.Edible)
obj.delete()
def test_all_states(self):
"Tick through all defined states"
for mod in self._get_all_state_modules():
state = mod.State(self.room.statehandler, self.room)
state.init()
for obj in self.room.contents:
if obj.pk:
methods = inspect.getmembers(obj, predicate=inspect.ismethod)
for name, method in methods:
if name.startswith("at_focus_"):
method(self.char1, args="dummy")
next_state = state.next()
self.assertEqual(next_state, mod.State.next_state)

View file

@ -0,0 +1,187 @@
"""
Helper functions and classes for the evscaperoom contrib.
Most of these are available directly from wrappers in state/object/room classes
and does not need to be imported from here.
"""
import re
from random import choice
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)
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
for the object-create admin command.
Note that for the purpose of the Evscaperoom, we only allow one instance
of each *name*, deleting the old version if it already exists.
Kwargs:
typeclass (str): This can take just the class-name in the evscaperoom's
objects.py module. Otherwise, a full path is needed.
key (str): Name of object.
location (Object): The location to create new object.
delete_duplicates (bool): Delete old object with same key.
kwargs (any): Will be passed into create_object.
Returns:
new_obj (Object): The newly created object, if any.
"""
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")]
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)
return new_obj
def create_fantasy_word(length=5, capitalize=True):
"""
Create a random semi-pronouncable 'word'.
Kwargs:
length (int): The desired length of the 'word'.
capitalize (bool): If the return should be capitalized or not
Returns:
word (str): The fictous word of given length.
"""
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()
word = [choice(phonemes)]
while len(word) < length:
word.append(choice(phonemes))
# it's possible to exceed length limit due to double consonants
word = "".join(word)[:length]
return word.capitalize() if capitalize else word
# special word mappings when going from 2nd person to 3rd
irregulars = {
"were": "was",
"are": "is",
"mix": "mixes",
"push": "pushes",
"have": "has",
"focus": "focuses",
}
def parse_for_perspectives(string, you=None):
"""
Parse a string with special markers to produce versions both
intended for the person doing the action ('you') and for those
seeing the person doing that action. Also marks 'things'
according to style. See example below.
Args:
string (str): String on 2nd person form with ~ markers ('~you ~open ...')
you (str): What others should see instead of you (Bob opens)
Returns:
second, third_person (tuple): Strings replace to be shown in 2nd and 3rd person
perspective
Example:
"~You ~open"
-> "You open", "Bob opens"
"""
def _replace_third_person(match):
match = match.group(1)
lmatch = match.lower()
if lmatch == "you":
return "|c{}|n".format(you)
elif lmatch in irregulars:
if match[0].isupper():
return irregulars[lmatch].capitalize()
return irregulars[lmatch]
elif lmatch[-1] == 's':
return match + "es"
else:
return match + "s" # simple, most normal form
you = "They" if you is None else you
first_person = _RE_PERSPECTIVE.sub(r"\1", string)
third_person = _RE_PERSPECTIVE.sub(_replace_third_person, string)
return first_person, third_person
def parse_for_things(string, things_style=2, clr="|y"):
"""
Parse string for special *thing markers and decorate
it.
Args:
string (str): The string to parse.
things_style (int): The style to handle `*things` marked:
0 - no marking (remove `*`)
1 - mark with color
2 - mark with color and [] (default)
clr (str): Which color to use for marker..
Example:
You open *door -> You open [door].
"""
if not things_style:
# hardcore mode - no marking of focus targets
return _RE_THING.sub(r"\1", string)
elif things_style == 1:
# only colors
return _RE_THING.sub(r"{}\1|n".format(clr), string)
else:
# colors and brackets
return _RE_THING.sub(r"{}[\1]|n".format(clr), string)
def add_msg_borders(text):
"Add borders above/below text block"
maxwidth = max(len(line) for line in text.split("\n"))
sep = "|w" + "~" * maxwidth + "|n"
text = f"{sep}\n{text}\n{sep}"
return text
def msg_cinematic(text, borders=True):
"""
Display a text as a 'cinematic' - centered and
surrounded by borders.
Args:
text (str): Text to format.
borders (bool, optional): Put borders above and below text.
Returns:
text (str): Pretty-formatted text.
"""
text = text.strip()
text = justify(text, align='c', indent=1)
if borders:
text = add_msg_borders(text)
return text

View file

@ -1,7 +1,7 @@
"""
Extended Room
Evennia Contribution - Griatch 2012
Evennia Contribution - Griatch 2012, vincent-lg 2019
This is an extended Room typeclass for Evennia. It is supported
by an extended `Look` command and an extended `desc` command, also
@ -45,28 +45,44 @@ at, without there having to be a database object created for it. The
Details are simply stored in a dictionary on the room and if the look
command cannot find an object match for a `look <target>` command it
will also look through the available details at the current location
if applicable. An extended `desc` command is used to set details.
if applicable. The `@detail` command is used to change details.
4) Extra commands
CmdExtendedLook - look command supporting room details
CmdExtendedDesc - desc command allowing to add seasonal descs and details,
CmdExtendedRoomLook - look command supporting room details
CmdExtendedRoomDesc - desc command allowing to add seasonal descs,
CmdExtendedRoomDetail - command allowing to manipulate details in this room
as well as listing them
CmdGameTime - A simple `time` command, displaying the current
CmdExtendedRoomGameTime - A simple `time` command, displaying the current
time and season.
Installation/testing:
1) Add `CmdExtendedLook`, `CmdExtendedDesc` and `CmdGameTime` to the default `cmdset`
(see Wiki for how to do this).
2) `@dig` a room of type `contrib.extended_room.ExtendedRoom` (or make it the
default room type)
3) Use `desc` and `detail` to customize the room, then play around!
Adding the `ExtendedRoomCmdset` to the default character cmdset will add all
new commands for use.
In more detail, in mygame/commands/default_cmdsets.py:
```
...
from evennia.contrib import extended_room # <-new
class CharacterCmdset(default_cmds.Character_CmdSet):
...
def at_cmdset_creation(self):
...
self.add(extended_room.ExtendedRoomCmdSet) # <-new
```
Then reload to make the bew commands available. Note that they only work
on rooms with the typeclass `ExtendedRoom`. Create new rooms with the right
typeclass or use the `typeclass` command to swap existing rooms.
"""
from __future__ import division
import datetime
import re
@ -75,6 +91,7 @@ from evennia import DefaultRoom
from evennia import gametime
from evennia import default_cmds
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))
@ -213,6 +230,42 @@ class ExtendedRoom(DefaultRoom):
return detail
return None
def set_detail(self, detailkey, description):
"""
This sets a new detail, using an Attribute "details".
Args:
detailkey (str): The detail identifier to add (for
aliases you need to add multiple keys to the
same description). Case-insensitive.
description (str): The text to return when looking
at the given detailkey.
"""
if self.db.details:
self.db.details[detailkey.lower()] = description
else:
self.db.details = {detailkey.lower(): description}
def del_detail(self, detailkey, description):
"""
Delete a detail.
The description is ignored.
Args:
detailkey (str): the detail to remove (case-insensitive).
description (str, ignored): the description.
The description is only included for compliance but is completely
ignored. Note that this method doesn't raise any exception if
the detail doesn't exist in this room.
"""
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
@ -268,7 +321,7 @@ class ExtendedRoom(DefaultRoom):
# Custom Look command supporting Room details. Add this to
# the Default cmdset to use.
class CmdExtendedLook(default_cmds.CmdLook):
class CmdExtendedRoomLook(default_cmds.CmdLook):
"""
look
@ -329,14 +382,12 @@ class CmdExtendedLook(default_cmds.CmdLook):
# Custom build commands for setting seasonal descriptions
# and detailing extended rooms.
class CmdExtendedDesc(default_cmds.CmdDesc):
class CmdExtendedRoomDesc(default_cmds.CmdDesc):
"""
`desc` - describe an object or room.
Usage:
desc[/switch] [<obj> =] <description>
detail[/del] [<key> = <description>]
Switches for `desc`:
spring - set description for <season> in current room.
@ -344,15 +395,9 @@ class CmdExtendedDesc(default_cmds.CmdDesc):
autumn
winter
Switch for `detail`:
del - delete a named detail.
Sets the "desc" attribute on an object. If an object is not given,
describe the current room.
The alias `detail` allows to assign a "detail" (a non-object
target for the `look` command) to the current room (only).
You can also embed special time markers in your room description, like this:
```
@ -362,11 +407,11 @@ class CmdExtendedDesc(default_cmds.CmdDesc):
Text marked this way will only display when the server is truly at the given
timeslot. The available times are night, morning, afternoon and evening.
Note that `detail`, seasons and time-of-day slots only work on rooms in this
Note that seasons and time-of-day slots only work on rooms in this
version of the `desc` command.
"""
aliases = ["describe", "detail"]
aliases = ["describe"]
switch_options = () # Inherits from default_cmds.CmdDesc, but unused here
def reset_times(self, obj):
@ -378,97 +423,121 @@ class CmdExtendedDesc(default_cmds.CmdDesc):
"""Define extended command"""
caller = self.caller
location = caller.location
if self.cmdname == 'detail':
# switch to detailing mode. This operates only on current location
if not self.args:
if location:
string = "|wDescriptions on %s|n:\n" % location.key
string += " |wspring:|n %s\n" % location.db.spring_desc
string += " |wsummer:|n %s\n" % location.db.summer_desc
string += " |wautumn:|n %s\n" % location.db.autumn_desc
string += " |wwinter:|n %s\n" % location.db.winter_desc
string += " |wgeneral:|n %s" % location.db.general_desc
caller.msg(string)
return
if self.switches and self.switches[0] in ("spring", "summer", "autumn", "winter"):
# a seasonal switch was given
if self.rhs:
caller.msg("Seasonal descs only work with rooms, not objects.")
return
switch = self.switches[0]
if not location:
caller.msg("No location to detail!")
caller.msg("No location was found!")
return
if location.db.details is None:
caller.msg("|rThis location does not support details.|n")
return
if self.switches and self.switches[0] in 'del':
# removing a detail.
if self.lhs in location.db.details:
del location.db.details[self.lhs]
caller.msg("Detail %s deleted, if it existed." % self.lhs)
self.reset_times(location)
return
if not self.args:
# No args given. Return all details on location
string = "|wDetails on %s|n:" % location
details = "\n".join(" |w%s|n: %s"
% (key, utils.crop(text)) for key, text in location.db.details.items())
caller.msg("%s\n%s" % (string, details) if details else "%s None." % string)
return
if not self.rhs:
# no '=' used - list content of given detail
if self.args in location.db.details:
string = "|wDetail '%s' on %s:\n|n" % (self.args, location)
string += str(location.db.details[self.args])
caller.msg(string)
else:
caller.msg("Detail '%s' not found." % self.args)
return
# setting a detail
location.db.details[self.lhs] = self.rhs
caller.msg("Set Detail %s to '%s'." % (self.lhs, self.rhs))
if switch == 'spring':
location.db.spring_desc = self.args
elif switch == 'summer':
location.db.summer_desc = self.args
elif switch == 'autumn':
location.db.autumn_desc = self.args
elif switch == 'winter':
location.db.winter_desc = self.args
# clear flag to force an update
self.reset_times(location)
return
caller.msg("Seasonal description was set on %s." % location.key)
else:
# we are doing a desc call
if not self.args:
if location:
string = "|wDescriptions on %s|n:\n" % location.key
string += " |wspring:|n %s\n" % location.db.spring_desc
string += " |wsummer:|n %s\n" % location.db.summer_desc
string += " |wautumn:|n %s\n" % location.db.autumn_desc
string += " |wwinter:|n %s\n" % location.db.winter_desc
string += " |wgeneral:|n %s" % location.db.general_desc
caller.msg(string)
# No seasonal desc set, maybe this is not an extended room
if self.rhs:
text = self.rhs
obj = caller.search(self.lhs)
if not obj:
return
if self.switches and self.switches[0] in ("spring", "summer", "autumn", "winter"):
# a seasonal switch was given
if self.rhs:
caller.msg("Seasonal descs only work with rooms, not objects.")
return
switch = self.switches[0]
if not location:
caller.msg("No location was found!")
return
if switch == 'spring':
location.db.spring_desc = self.args
elif switch == 'summer':
location.db.summer_desc = self.args
elif switch == 'autumn':
location.db.autumn_desc = self.args
elif switch == 'winter':
location.db.winter_desc = self.args
# clear flag to force an update
self.reset_times(location)
caller.msg("Seasonal description was set on %s." % location.key)
else:
# No seasonal desc set, maybe this is not an extended room
if self.rhs:
text = self.rhs
obj = caller.search(self.lhs)
if not obj:
return
else:
text = self.args
obj = location
obj.db.desc = text # a compatibility fallback
if obj.attributes.has("general_desc"):
obj.db.general_desc = text
self.reset_times(obj)
caller.msg("General description was set on %s." % obj.key)
else:
# this is not an ExtendedRoom.
caller.msg("The description was set on %s." % obj.key)
text = self.args
obj = location
obj.db.desc = text # a compatibility fallback
if obj.attributes.has("general_desc"):
obj.db.general_desc = text
self.reset_times(obj)
caller.msg("General description was set on %s." % obj.key)
else:
# this is not an ExtendedRoom.
caller.msg("The description was set on %s." % obj.key)
class CmdExtendedRoomDetail(default_cmds.MuxCommand):
"""
sets a detail on a room
Usage:
@detail[/del] <key> [= <description>]
@detail <key>;<alias>;... = description
Example:
@detail
@detail walls = The walls are covered in ...
@detail castle;ruin;tower = The distant ruin ...
@detail/del wall
@detail/del castle;ruin;tower
This command allows to show the current room details if you enter it
without any argument. Otherwise, sets or deletes a detail on the current
room, if this room supports details like an extended room. To add new
detail, just use the @detail command, specifying the key, an equal sign
and the description. You can assign the same description to several
details using the alias syntax (replace key by alias1;alias2;alias3;...).
To remove one or several details, use the @detail/del switch.
"""
key = "@detail"
locks = "cmd:perm(Builder)"
help_category = "Building"
def func(self):
location = self.caller.location
if not self.args:
details = location.db.details
if not details:
self.msg("|rThe room {} doesn't have any detail set.|n".format(location))
else:
details = sorted(["|y{}|n: {}".format(key, desc) for key, desc in details.items()])
self.msg("Details on Room:\n" + "\n".join(details))
return
if not self.rhs and "del" not in self.switches:
detail = location.return_detail(self.lhs)
if detail:
self.msg("Detail '|y{}|n' on Room:\n{}".format(self.lhs, detail))
else:
self.msg("Detail '{}' not found.".format(self.lhs))
return
method = "set_detail" if "del" not in self.switches else "del_detail"
if not hasattr(location, method):
self.caller.msg("Details cannot be set on %s." % location)
return
for key in self.lhs.split(";"):
# loop over all aliases, if any (if not, this will just be
# the one key to loop over)
getattr(location, method)(key, self.rhs)
if "del" in self.switches:
self.caller.msg("Detail %s deleted, if it existed." % self.lhs)
else:
self.caller.msg("Detail set '%s': '%s'" % (self.lhs, self.rhs))
# Simple command to view the current time and season
class CmdGameTime(default_cmds.MuxCommand):
class CmdExtendedRoomGameTime(default_cmds.MuxCommand):
"""
Check the game time
@ -492,3 +561,17 @@ class CmdGameTime(default_cmds.MuxCommand):
if season == "autumn":
prep = "an"
self.caller.msg("It's %s %s day, in the %s." % (prep, season, timeslot))
# 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)
self.add(CmdExtendedRoomDetail)
self.add(CmdExtendedRoomGameTime)

View file

@ -92,7 +92,7 @@ class GenderCharacter(DefaultCharacter):
"""
Called once when the object is created.
"""
super(GenderCharacter, self).at_object_creation()
super().at_object_creation()
self.db.gender = "ambiguous"
def _get_pronoun(self, regex_match):
@ -139,4 +139,4 @@ class GenderCharacter(DefaultCharacter):
text = _RE_GENDER_PRONOUN.sub(self._get_pronoun, text)
except TypeError:
pass
super(GenderCharacter, self).msg(text, from_obj=from_obj, session=session, **kwargs)
super().msg(text, from_obj=from_obj, session=session, **kwargs)

View file

@ -253,7 +253,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
row.append("Yes" if callback.get("valid") else "No")
table.add_row(*row)
self.msg(unicode(table))
self.msg(str(table))
else:
names = list(set(list(types.keys()) + list(callbacks.keys())))
table = EvTable("Callback name", "Number", "Description",
@ -269,7 +269,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
description = description.strip("\n").splitlines()[0]
table.add_row(name, no, description)
self.msg(unicode(table))
self.msg(str(table))
def add_callback(self):
"""Add a callback."""
@ -457,7 +457,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
updated_on = "|gUnknown|n"
table.add_row(obj.id, type_name, obj, name, by, updated_on)
self.msg(unicode(table))
self.msg(str(table))
return
# An object was specified
@ -518,7 +518,7 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
delta = time_format((future - now).total_seconds(), 1)
table.add_row(task_id, key, callback_name, delta)
self.msg(unicode(table))
self.msg(str(table))
# Private functions to handle editing

View file

@ -3,7 +3,7 @@ Scripts for the in-game Python system.
"""
from datetime import datetime, timedelta
from Queue import Queue
from queue import Queue
import re
import sys
import traceback
@ -362,7 +362,7 @@ class EventHandler(DefaultScript):
self.db.locked[i] = (t_obj, t_callback_name, t_number - 1)
# Delete time-related callbacks associated with this object
for script in list(obj.scripts.all()):
for script in obj.scripts.all():
if isinstance(script, TimecallbackScript):
if script.obj is obj and script.db.callback_name == callback_name:
if script.db.number == number:

View file

@ -30,7 +30,7 @@ class TestEventHandler(EvenniaTest):
def setUp(self):
"""Create the event handler."""
super(TestEventHandler, self).setUp()
super().setUp()
self.handler = create_script(
"evennia.contrib.ingame_python.scripts.EventHandler")
@ -51,7 +51,7 @@ class TestEventHandler(EvenniaTest):
OLD_EVENTS.update(self.handler.ndb.events)
self.handler.stop()
CallbackHandler.script = None
super(TestEventHandler, self).tearDown()
super().tearDown()
def test_start(self):
"""Simply make sure the handler runs with proper initial values."""
@ -224,13 +224,13 @@ class TestEventHandler(EvenniaTest):
self.assertEqual(callback.code, "pass")
self.assertEqual(callback.author, self.char1)
self.assertEqual(callback.valid, True)
self.assertIn([callback], self.room1.callbacks.all().values())
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)
self.assertIn([new], self.room1.callbacks.all().values())
self.assertNotIn([callback], self.room1.callbacks.all().values())
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",
@ -248,7 +248,7 @@ class TestCmdCallback(CommandTest):
def setUp(self):
"""Create the callback handler."""
super(TestCmdCallback, self).setUp()
super().setUp()
self.handler = create_script(
"evennia.contrib.ingame_python.scripts.EventHandler")
@ -273,7 +273,7 @@ class TestCmdCallback(CommandTest):
script.stop()
CallbackHandler.script = None
super(TestCmdCallback, self).tearDown()
super().tearDown()
def test_list(self):
"""Test listing callbacks with different rights."""
@ -413,7 +413,7 @@ class TestDefaultCallbacks(CommandTest):
def setUp(self):
"""Create the callback handler."""
super(TestDefaultCallbacks, self).setUp()
super().setUp()
self.handler = create_script(
"evennia.contrib.ingame_python.scripts.EventHandler")
@ -434,7 +434,7 @@ class TestDefaultCallbacks(CommandTest):
OLD_EVENTS.update(self.handler.ndb.events)
self.handler.stop()
CallbackHandler.script = None
super(TestDefaultCallbacks, self).tearDown()
super().tearDown()
def test_exit(self):
"""Test the callbacks of an exit."""
@ -486,7 +486,7 @@ class TestDefaultCallbacks(CommandTest):
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, force_string=True))
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]
@ -507,7 +507,7 @@ class TestDefaultCallbacks(CommandTest):
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, force_string=True))
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]

View file

@ -229,7 +229,7 @@ class EventCharacter(DefaultCharacter):
if not string:
return
super(EventCharacter, self).announce_move_from(destination, msg=string, mapping=mapping)
super().announce_move_from(destination, msg=string, mapping=mapping)
def announce_move_to(self, source_location, msg=None, mapping=None):
"""
@ -284,7 +284,7 @@ class EventCharacter(DefaultCharacter):
if not string:
return
super(EventCharacter, self).announce_move_to(source_location, msg=string, mapping=mapping)
super().announce_move_to(source_location, msg=string, mapping=mapping)
def at_before_move(self, destination):
"""
@ -334,7 +334,7 @@ class EventCharacter(DefaultCharacter):
source_location (Object): Wwhere we came from. This may be `None`.
"""
super(EventCharacter, self).at_after_move(source_location)
super().at_after_move(source_location)
origin = source_location
destination = self.location
@ -373,7 +373,7 @@ class EventCharacter(DefaultCharacter):
puppeting this Object.
"""
super(EventCharacter, self).at_post_puppet()
super().at_post_puppet()
self.callbacks.call("puppeted", self)
@ -401,7 +401,7 @@ class EventCharacter(DefaultCharacter):
if location and isinstance(location, DefaultRoom):
location.callbacks.call("unpuppeted_in", self, location)
super(EventCharacter, self).at_pre_unpuppet()
super().at_pre_unpuppet()
def at_before_say(self, message, **kwargs):
"""
@ -488,7 +488,7 @@ class EventCharacter(DefaultCharacter):
"""
super(EventCharacter, self).at_say(message, **kwargs)
super().at_say(message, **kwargs)
location = getattr(self, "location", None)
location = location if location and inherits_from(location, "evennia.objects.objects.DefaultRoom") else None
@ -635,7 +635,7 @@ class EventExit(DefaultExit):
if not allow:
return
super(EventExit, self).at_traverse(traversing_object, target_location)
super().at_traverse(traversing_object, target_location)
# After traversing
if is_character:
@ -714,7 +714,7 @@ class EventObject(DefaultObject):
permissions for that.
"""
super(EventObject, self).at_get(getter)
super().at_get(getter)
self.callbacks.call("get", getter, self)
def at_drop(self, dropper):
@ -730,7 +730,7 @@ class EventObject(DefaultObject):
permissions from that.
"""
super(EventObject, self).at_drop(dropper)
super().at_drop(dropper)
self.callbacks.call("drop", dropper, self)

View file

@ -50,7 +50,7 @@ def register_events(path_or_typeclass):
temporary storage, waiting for the script to be initialized.
"""
if isinstance(path_or_typeclass, basestring):
if isinstance(path_or_typeclass, str):
typeclass = class_from_module(path_or_typeclass)
else:
typeclass = path_or_typeclass

View file

@ -3,18 +3,47 @@ In-Game Mail system
Evennia Contribution - grungies1138 2016
A simple Brandymail style @mail system that uses the Msg class from Evennia Core.
A simple Brandymail style @mail system that uses the Msg class from Evennia
Core. It has two Commands, both of which can be used on their own:
- CmdMail - this should sit on the Account cmdset and makes the @mail command
available both IC and OOC. Mails will always go to Accounts (other players).
- CmdMailCharacter - this should sit on the Character cmdset and makes the @mail
command ONLY available when puppeting a character. Mails will be sent to other
Characters only and will not be available when OOC.
- If adding *both* commands to their respective cmdsets, you'll get two separate
IC and OOC mailing systems, with different lists of mail for IC and OOC modes.
Installation:
import CmdMail from this module (from evennia.contrib.mail import CmdMail),
and add into the default Account or Character command set (self.add(CmdMail)).
Install one or both of the following (see above):
- CmdMail (IC + OOC mail, sent between players)
# mygame/commands/default_cmds.py
from evennia.contrib import mail
# in AccountCmdSet.at_cmdset_creation:
self.add(mail.CmdMail())
- CmdMailCharacter (optional, IC only mail, sent between characters)
# mygame/commands/default_cmds.py
from evennia.contrib import mail
# in CharacterCmdSet.at_cmdset_creation:
self.add(mail.CmdMailCharacter())
Once installed, use `help mail` in game for help with the mail command. Use
@ic/@ooc to switch in and out of IC/OOC modes.
"""
import re
from evennia import ObjectDB, AccountDB
from evennia import default_cmds
from evennia.utils import create, evtable, make_iter
from evennia.utils import create, evtable, make_iter, inherits_from, datetime_format
from evennia.comms.models import Msg
@ -23,38 +52,32 @@ _SUB_HEAD_CHAR = "-"
_WIDTH = 78
class CmdMail(default_cmds.MuxCommand):
class CmdMail(default_cmds.MuxAccountCommand):
"""
Commands that allow either IC or OOC communications
Communicate with others by sending mail.
Usage:
@mail - Displays all the mail an account has in their mailbox
@mail <#> - Displays a specific message
@mail <accounts>=<subject>/<message>
- Sends a message to the comma separated list of accounts.
@mail/delete <#> - Deletes a specific message
@mail/forward <account list>=<#>[/<Message>]
- Forwards an existing message to the specified list of accounts,
original message is delivered with optional Message prepended.
@mail/reply <#>=<message>
- Replies to a message #. Prepends message to the original
message text.
@mail - Displays all the mail an account has in their mailbox
@mail <#> - Displays a specific message
@mail <accounts>=<subject>/<message>
- Sends a message to the comma separated list of accounts.
@mail/delete <#> - Deletes a specific message
@mail/forward <account list>=<#>[/<Message>]
- Forwards an existing message to the specified list of accounts,
original message is delivered with optional Message prepended.
@mail/reply <#>=<message>
- Replies to a message #. Prepends message to the original
message text.
Switches:
delete - deletes a message
forward - forward a received message to another object with an optional message attached.
reply - Replies to a received message, appending the original message to the bottom.
delete - deletes a message
forward - forward a received message to another object with an optional message attached.
reply - Replies to a received message, appending the original message to the bottom.
Examples:
@mail 2
@mail Griatch=New mail/Hey man, I am sending you a message!
@mail/delete 6
@mail/forward feend78 Griatch=4/You guys should read this.
@mail/reply 9=Thanks for the info!
@mail 2
@mail Griatch=New mail/Hey man, I am sending you a message!
@mail/delete 6
@mail/forward feend78 Griatch=4/You guys should read this.
@mail/reply 9=Thanks for the info!
"""
key = "@mail"
@ -62,6 +85,16 @@ class CmdMail(default_cmds.MuxCommand):
lock = "cmd:all()"
help_category = "General"
def parse(self):
"""
Add convenience check to know if caller is an Account or not since this cmd
will be able to add to either Object- or Account level.
"""
super().parse()
self.caller_is_account = bool(inherits_from(self.caller,
"evennia.accounts.accounts.DefaultAccount"))
def search_targets(self, namelist):
"""
Search a list of targets of the same type as caller.
@ -71,39 +104,38 @@ class CmdMail(default_cmds.MuxCommand):
namelist (list): List of strings for objects to search for.
Returns:
targetlist (list): List of matches, if any.
targetlist (Queryset): Any target matches.
"""
nameregex = r"|".join(r"^%s$" % re.escape(name) for name in make_iter(namelist))
if hasattr(self.caller, "account") and self.caller.account:
matches = list(ObjectDB.objects.filter(db_key__iregex=nameregex))
if self.caller_is_account:
matches = AccountDB.objects.filter(username__iregex=nameregex)
else:
matches = list(AccountDB.objects.filter(username__iregex=nameregex))
matches = ObjectDB.objects.filter(db_key__iregex=nameregex)
return matches
def get_all_mail(self):
"""
Returns a list of all the messages where the caller is a recipient.
Returns a list of all the messages where the caller is a recipient. These
are all messages tagged with tags of the `mail` category.
Returns:
messages (list): list of Msg objects.
messages (QuerySet): Matching Msg objects.
"""
# mail_messages = Msg.objects.get_by_tag(category="mail")
# messages = []
try:
account = self.caller.account
except AttributeError:
account = self.caller
messages = Msg.objects.get_by_tag(category="mail").filter(db_receivers_accounts=account)
return messages
if self.caller_is_account:
return Msg.objects.get_by_tag(category="mail").filter(db_receivers_accounts=self.caller)
else:
return Msg.objects.get_by_tag(category="mail").filter(db_receivers_objects=self.caller)
def send_mail(self, recipients, subject, message, caller):
"""
Function for sending new mail. Also useful for sending notifications from objects or systems.
Function for sending new mail. Also useful for sending notifications
from objects or systems.
Args:
recipients (list): list of Account or character objects to receive the newly created mails.
recipients (list): list of Account or Character objects to receive
the newly created mails.
subject (str): The header or subject of the message to be delivered.
message (str): The body of the message being sent.
caller (obj): The object (or Account or Character) that is sending the message.
@ -111,43 +143,57 @@ class CmdMail(default_cmds.MuxCommand):
"""
for recipient in recipients:
recipient.msg("You have received a new @mail from %s" % caller)
new_message = create.create_message(self.caller, message, receivers=recipient, header=subject)
new_message.tags.add("U", category="mail")
new_message = create.create_message(self.caller, message,
receivers=recipient,
header=subject)
new_message.tags.add("new", category="mail")
if recipients:
caller.msg("You sent your message.")
return
else:
caller.msg("No valid accounts found. Cannot send message.")
caller.msg("No valid target(s) found. Cannot send message.")
return
def func(self):
"""
Do the main command functionality
"""
subject = ""
body = ""
if self.switches or self.args:
if "delete" in self.switches:
if "delete" in self.switches or "del" in self.switches:
try:
if not self.lhs:
self.caller.msg("No Message ID given. Unable to delete.")
self.caller.msg("No Message ID given. Unable to delete.")
return
else:
all_mail = self.get_all_mail()
mind_max = max(0, all_mail.count() - 1)
mind = max(0, min(mind_max, int(self.lhs) - 1))
if all_mail[mind]:
all_mail[mind].delete()
self.caller.msg("Message %s deleted" % self.lhs)
mail = all_mail[mind]
question = "Delete message {} ({}) [Y]/N?".format(mind + 1, mail.header)
ret = yield(question)
# handle not ret, it will be None during unit testing
if not ret or ret.strip().upper() not in ("N", "No"):
all_mail[mind].delete()
self.caller.msg("Message %s deleted" % (mind + 1,))
else:
self.caller.msg("Message not deleted.")
else:
raise IndexError
except IndexError:
self.caller.msg("That message does not exist.")
except ValueError:
self.caller.msg("Usage: @mail/delete <message ID>")
elif "forward" in self.switches:
elif "forward" in self.switches or "fwd" in self.switches:
try:
if not self.rhs:
self.caller.msg("Cannot forward a message without an account list. Please try again.")
self.caller.msg("Cannot forward a message without a target list. "
"Please try again.")
return
elif not self.lhs:
self.caller.msg("You must define a message to forward.")
@ -175,15 +221,15 @@ class CmdMail(default_cmds.MuxCommand):
self.send_mail(self.search_targets(self.lhslist), "FWD: " + old_message.header,
"\n---- Original Message ----\n" + old_message.message, self.caller)
self.caller.msg("Message forwarded.")
old_message.tags.remove("u", category="mail")
old_message.tags.add("f", category="mail")
old_message.tags.remove("new", category="mail")
old_message.tags.add("fwd", category="mail")
else:
raise IndexError
except IndexError:
self.caller.msg("Message does not exixt.")
except ValueError:
self.caller.msg("Usage: @mail/forward <account list>=<#>[/<Message>]")
elif "reply" in self.switches:
elif "reply" in self.switches or "rep" in self.switches:
try:
if not self.rhs:
self.caller.msg("You must define a message to reply to.")
@ -199,8 +245,8 @@ class CmdMail(default_cmds.MuxCommand):
old_message = all_mail[mind]
self.send_mail(old_message.senders, "RE: " + old_message.header,
self.rhs + "\n---- Original Message ----\n" + old_message.message, self.caller)
old_message.tags.remove("u", category="mail")
old_message.tags.add("r", category="mail")
old_message.tags.remove("new", category="mail")
old_message.tags.add("-", category="mail")
return
else:
raise IndexError
@ -229,27 +275,35 @@ class CmdMail(default_cmds.MuxCommand):
messageForm = []
if message:
messageForm.append(_HEAD_CHAR * _WIDTH)
messageForm.append("|wFrom:|n %s" % (message.senders[0].key))
messageForm.append("|wSent:|n %s" % message.db_date_created.strftime("%m/%d/%Y %H:%M:%S"))
messageForm.append("|wFrom:|n %s" % (message.senders[0].get_display_name(self.caller)))
messageForm.append("|wSent:|n %s" % message.db_date_created.strftime("%b %-d, %Y - %H:%M:%S"))
messageForm.append("|wSubject:|n %s" % message.header)
messageForm.append(_SUB_HEAD_CHAR * _WIDTH)
messageForm.append(message.message)
messageForm.append(_HEAD_CHAR * _WIDTH)
self.caller.msg("\n".join(messageForm))
message.tags.remove("u", category="mail")
message.tags.add("o", category="mail")
message.tags.remove("new", category="mail")
message.tags.add("-", category="mail")
else:
# list messages
messages = self.get_all_mail()
if messages:
table = evtable.EvTable("|wID:|n", "|wFrom:|n", "|wSubject:|n", "|wDate:|n", "|wSta:|n",
table=None, border="header", header_line_char=_SUB_HEAD_CHAR, width=_WIDTH)
table = evtable.EvTable("|wID|n", "|wFrom|n", "|wSubject|n",
"|wArrived|n", "",
table=None, border="header",
header_line_char=_SUB_HEAD_CHAR, width=_WIDTH)
index = 1
for message in messages:
table.add_row(index, message.senders[0], message.header,
message.db_date_created.strftime("%m/%d/%Y"),
str(message.db_tags.last().db_key.upper()))
status = str(message.db_tags.last().db_key.upper())
if status == "NEW":
status = "|gNEW|n"
table.add_row(index, message.senders[0].get_display_name(self.caller),
message.header,
datetime_format(message.db_date_created),
status)
index += 1
table.reformat_column(0, width=6)
@ -259,7 +313,13 @@ class CmdMail(default_cmds.MuxCommand):
table.reformat_column(4, width=7)
self.caller.msg(_HEAD_CHAR * _WIDTH)
self.caller.msg(unicode(table))
self.caller.msg(str(table))
self.caller.msg(_HEAD_CHAR * _WIDTH)
else:
self.caller.msg("There are no messages in your inbox.")
# character - level version of the command
class CmdMailCharacter(CmdMail):
account_caller = False

View file

@ -139,7 +139,7 @@ def example1_build_mountains(x, y, **kwargs):
room.db.desc = random.choice(room_desc)
# Create a random number of objects to populate the room.
for i in xrange(randint(0, 3)):
for i in range(randint(0, 3)):
rock = create_object(key="Rock", location=room)
rock.db.desc = "An ordinary rock."
@ -276,7 +276,7 @@ COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
# Helper function for readability.
def _map_to_list(game_map):
"""
Splits multi line map string into list of rows, treats for UTF-8 encoding.
Splits multi line map string into list of rows.
Args:
game_map (str): An ASCII map
@ -285,9 +285,7 @@ def _map_to_list(game_map):
list (list): The map split into rows
"""
list_map = game_map.split('\n')
return [character.decode('UTF-8') if isinstance(character, basestring)
else character for character in list_map]
return game_map.split('\n')
def build_map(caller, game_map, legend, iterations=1, build_exits=True):
@ -321,12 +319,12 @@ def build_map(caller, game_map, legend, iterations=1, build_exits=True):
room_dict = {}
caller.msg("Creating Landmass...")
for iteration in xrange(iterations):
for y in xrange(len(game_map)):
for x in xrange(len(game_map[y])):
for iteration in range(iterations):
for y in range(len(game_map)):
for x in range(len(game_map[y])):
for key in legend:
# obs - we must use == for unicode
if utils.to_unicode(game_map[y][x]) == utils.to_unicode(key):
# obs - we must use == for strings
if game_map[y][x] == key:
room = legend[key](x, y, iteration=iteration,
room_dict=room_dict,
caller=caller)
@ -336,7 +334,7 @@ def build_map(caller, game_map, legend, iterations=1, build_exits=True):
if build_exits:
# Creating exits. Assumes single room object in dict entry
caller.msg("Connecting Areas...")
for loc_key, location in room_dict.iteritems():
for loc_key, location in room_dict.items():
x = loc_key[0]
y = loc_key[1]

View file

@ -1,320 +1,209 @@
"""
A login menu using EvMenu.
Contribution - Vincent-lg 2016
Contribution - Vincent-lg 2016, Griatch 2019 (rework for modern EvMenu)
This module contains the functions (nodes) of the EvMenu, with the
CmdSet and UnloggedCommand called when a user logs in. In other
words, instead of using the 'connect' or 'create' commands once on the
login screen, players navigates through a simple menu asking them to
enter their username followed by password, or to type 'new' to create
a new one. You will need to change update your login screen if you use
this system.
This changes the Evennia login to ask for the account name and password in
sequence instead of requiring you to enter both at once.
In order to install, to your settings file, add/edit the line:
To install, add this line to the settings file (`mygame/server/conf/settings.py`):
CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedinCmdSet"
CMDSET_UNLOGGEDIN = "evennia.contrib.menu_login.UnloggedinCmdSet"
When you'll reload the server, new sessions will connect to the new
login system, where they will be able to:
Reload the server and the new connection method will be active. Note that you must
independently change the connection screen to match this login style, by editing
`mygame/server/conf/connection_screens.py`.
* Enter their username, assuming they have an existing account.
* Enter 'NEW' to create a new account.
The top-level functions in this file are menu nodes (as described in
evennia.utils.evmenu.py). Each one of these functions is responsible
for prompting the user for a specific piece of information (username,
password and so on). At the bottom of the file is defined the CmdSet
for Unlogging users. This adds a new command that is called just after
a new session has been created, in order to create the menu. See the
specific documentation on functions (nodes) to see what each one
should do.
This uses Evennia's menu system EvMenu and is triggered by a command that is
called automatically when a new user connects.
"""
import re
from textwrap import dedent
from django.conf import settings
from evennia import Command, CmdSet
from evennia import logger
from evennia import managers
from evennia import ObjectDB
from evennia.server.models import ServerConfig
from evennia import syscmdkeys
from evennia.utils.evmenu import EvMenu
from evennia.utils.utils import random_string_from_module
from evennia.utils.utils import (
random_string_from_module, class_from_module, callables_from_module)
# Constants
RE_VALID_USERNAME = re.compile(r"^[a-z]{3,}$", re.I)
LEN_PASSWD = 6
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
_CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
_GUEST_ENABLED = settings.GUEST_ENABLED
_ACCOUNT = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
_GUEST = class_from_module(settings.BASE_GUEST_TYPECLASS)
# Menu notes (top-level functions)
_ACCOUNT_HELP = ("Enter the name you used to log into the game before, "
"or a new account-name if you are new.")
_PASSWORD_HELP = ("Password should be a minimum of 8 characters (preferably longer) and "
"can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only.")
# Menu nodes
def start(caller):
"""The user should enter his/her username or NEW to create one.
def _show_help(caller, raw_string, **kwargs):
"""Echo help message, then re-run node that triggered it"""
help_entry = kwargs['help_entry']
caller.msg(help_entry)
return None # re-run calling node
This node is called at the very beginning of the menu, when
a session has been created OR if an error occurs further
down the menu tree. From there, users can either enter a
username (if this username exists) or type NEW (capitalized
or not) to create a new account.
def node_enter_username(caller, raw_text, **kwargs):
"""
Start node of menu
Start login by displaying the connection screen and ask for a user name.
"""
text = random_string_from_module(CONNECTION_SCREEN_MODULE)
text += "\n\nEnter your username or |yNEW|n to create a new account."
options = (
{"key": "",
"goto": "start"},
{"key": "new",
"goto": "create_account"},
{"key": "quit",
"goto": "quit"},
{"key": "_default",
"goto": "username"})
return text, options
def _check_input(caller, username, **kwargs):
"""
'Goto-callable', set up to be called from the _default option below.
Called when user enters a username string. Check if this username already exists and set the flag
'new_user' if not. Will also directly login if the username is 'guest'
and GUEST_ENABLED is True.
def username(caller, string_input):
"""Check that the username leads to an existing account.
The return from this goto-callable determines which node we go to next
and what kwarg it will be called with.
Check that the specified username exists. If the username doesn't
exist, display an error message and ask the user to try again. If
entering an empty string, return to start node. If user exists,
move to the next node (enter password).
"""
username = username.rstrip('\n')
"""
string_input = string_input.strip()
account = managers.accounts.get_account_from_name(string_input)
if account is None:
text = dedent("""
|rThe username '{}' doesn't exist. Have you created it?|n
Try another name or leave empty to go back.
""".strip("\n")).format(string_input)
options = (
{"key": "",
"goto": "start"},
{"key": "_default",
"goto": "username"})
else:
caller.ndb._menutree.account = account
text = "Enter the password for the {} account.".format(account.name)
# Disables echo for the password
caller.msg("", options={"echo": False})
options = (
{"key": "",
"exec": lambda caller: caller.msg("", options={"echo": True}),
"goto": "start"},
{"key": "_default",
"goto": "ask_password"})
if username == 'guest' and _GUEST_ENABLED:
# do an immediate guest login
session = caller
address = session.address
account, errors = _GUEST.authenticate(ip=address)
if account:
return "node_quit_or_login", {"login": True, "account": account}
else:
session.msg("|R{}|n".format("\n".join(errors)))
return None # re-run the username node
return text, options
def ask_password(caller, string_input):
"""Ask the user to enter the password to this account.
This is assuming the user exists (see 'create_username' and
'create_password'). This node "loops" if needed: if the
user specifies a wrong password, offers the user to try
again or to go back by entering 'b'.
If the password is correct, then login.
"""
menutree = caller.ndb._menutree
string_input = string_input.strip()
# Check the password and login is correct; also check for bans
account = menutree.account
password_attempts = menutree.password_attempts \
if hasattr(menutree, "password_attempts") else 0
bans = ServerConfig.objects.conf("server_bans")
banned = bans and (any(tup[0] == account.name.lower() for tup in bans) or
any(tup[2].match(caller.address) for tup in bans if tup[2]))
if not account.check_password(string_input):
# Didn't enter a correct password
password_attempts += 1
if password_attempts > 2:
# Too many tries
caller.sessionhandler.disconnect(
caller, "|rToo many failed attempts. Disconnecting.|n")
text = ""
options = {}
else:
menutree.password_attempts = password_attempts
text = dedent("""
|rIncorrect password.|n
Try again or leave empty to go back.
""".strip("\n"))
# Loops on the same node
options = (
{"key": "",
"exec": lambda caller: caller.msg("", options={"echo": True}),
"goto": "start"},
{"key": "_default",
"goto": "ask_password"})
elif banned:
# This is a banned IP or name!
string = dedent("""
|rYou have been banned and cannot continue from here.
If you feel this ban is in error, please email an admin.|n
Disconnecting.
""".strip("\n"))
caller.sessionhandler.disconnect(caller, string)
text = ""
options = {}
else:
# We are OK, log us in.
text = ""
options = {}
caller.msg("", options={"echo": True})
caller.sessionhandler.login(caller, account)
return text, options
def create_account(caller):
"""Create a new account.
This node simply prompts the user to entere a username.
The input is redirected to 'create_username'.
"""
text = "Enter your new account name."
options = (
{"key": "_default",
"goto": "create_username"},)
return text, options
def create_username(caller, string_input):
"""Prompt to enter a valid username (one that doesnt exist).
'string_input' contains the new username. If it exists, prompt
the username to retry or go back to the login screen.
"""
menutree = caller.ndb._menutree
string_input = string_input.strip()
account = managers.accounts.get_account_from_name(string_input)
# If an account with that name exists, a new one will not be created
if account:
text = dedent("""
|rThe account {} already exists.|n
Enter another username or leave blank to go back.
""".strip("\n")).format(string_input)
# Loops on the same node
options = (
{"key": "",
"goto": "start"},
{"key": "_default",
"goto": "create_username"})
elif not RE_VALID_USERNAME.search(string_input):
text = dedent("""
|rThis username isn't valid.|n
Only letters are accepted, without special characters.
The username must be at least 3 characters long.
Enter another username or leave blank to go back.
""".strip("\n"))
options = (
{"key": "",
"goto": "start"},
{"key": "_default",
"goto": "create_username"})
else:
# a valid username - continue getting the password
menutree.accountname = string_input
# Disables echo for entering password
caller.msg("", options={"echo": False})
# Redirects to the creation of a password
text = "Enter this account's new password."
options = (
{"key": "_default",
"goto": "create_password"},)
return text, options
def create_password(caller, string_input):
"""Ask the user to create a password.
This node is at the end of the menu for account creation. If
a proper MULTI_SESSION is configured, a character is also
created with the same name (we try to login into it).
"""
menutree = caller.ndb._menutree
text = ""
options = (
{"key": "",
"exec": lambda caller: caller.msg("", options={"echo": True}),
"goto": "start"},
{"key": "_default",
"goto": "create_password"})
password = string_input.strip()
accountname = menutree.accountname
if len(password) < LEN_PASSWD:
# The password is too short
text = dedent("""
|rYour password must be at least {} characters long.|n
Enter another password or leave it empty to go back.
""".strip("\n")).format(LEN_PASSWD)
else:
# Everything's OK. Create the new player account and
# possibly the character, depending on the multisession mode
from evennia.commands.default import unloggedin
# We make use of the helper functions from the default set here.
try:
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
typeclass = settings.BASE_CHARACTER_TYPECLASS
new_account = unloggedin._create_account(caller, accountname,
password, permissions)
if new_account:
if settings.MULTISESSION_MODE < 2:
default_home = ObjectDB.objects.get_id(
settings.DEFAULT_HOME)
unloggedin._create_character(caller, new_account,
typeclass, default_home, permissions)
except Exception:
# We are in the middle between logged in and -not, so we have
# to handle tracebacks ourselves at this point. If we don't, we
# won't see any errors at all.
caller.msg(dedent("""
|rAn error occurred.|n Please e-mail an admin if
the problem persists. Try another password or leave
it empty to go back to the login screen.
""".strip("\n")))
logger.log_trace()
_ACCOUNT.objects.get(username__iexact=username)
except _ACCOUNT.DoesNotExist:
new_user = True
else:
text = ""
caller.msg("|gWelcome, your new account has been created!|n")
caller.msg("", options={"echo": True})
caller.sessionhandler.login(caller, new_account)
new_user = False
# pass username/new_user into next node as kwargs
return "node_enter_password", {'new_user': new_user, 'username': username}
callables = callables_from_module(_CONNECTION_SCREEN_MODULE)
if "connection_screen" in callables:
connection_screen = callables['connection_screen']()
else:
connection_screen = random_string_from_module(_CONNECTION_SCREEN_MODULE)
if _GUEST_ENABLED:
text = "Enter a new or existing user name to login (write 'guest' for a guest login):"
else:
text = "Enter a new or existing user name to login:"
text = "{}\n\n{}".format(connection_screen, text)
options = ({"key": "",
"goto": "node_enter_username"},
{"key": ("quit", "q"),
"goto": "node_quit_or_login"},
{"key": ("help", "h"),
"goto": (_show_help, {"help_entry": _ACCOUNT_HELP, **kwargs})},
{"key": "_default",
"goto": _check_input})
return text, options
def quit(caller):
caller.sessionhandler.disconnect(caller, "Goodbye! Logging off.")
def node_enter_password(caller, raw_string, **kwargs):
"""
Handle password input.
"""
def _check_input(caller, password, **kwargs):
"""
'Goto-callable', set up to be called from the _default option below.
Called when user enters a password string. Check username + password
viability. If it passes, the account will have been created and login
will be initiated.
The return from this goto-callable determines which node we go to next
and what kwarg it will be called with.
"""
# these flags were set by the goto-callable
username = kwargs['username']
new_user = kwargs['new_user']
password = password.rstrip('\n')
session = caller
address = session.address
if new_user:
# create a new account
account, errors = _ACCOUNT.create(username=username, password=password,
ip=address, session=session)
else:
# check password against existing account
account, errors = _ACCOUNT.authenticate(username=username, password=password,
ip=address, session=session)
if account:
if new_user:
session.msg("|gA new account |c{}|g was created. Welcome!|n".format(username))
# pass login info to login node
return "node_quit_or_login", {"login": True, "account": account}
else:
# restart due to errors
session.msg("|R{}".format("\n".join(errors)))
kwargs['retry_password'] = True
return "node_enter_password", kwargs
def _restart_login(caller, *args, **kwargs):
caller.msg("|yCancelled login.|n")
return "node_enter_username"
username = kwargs['username']
if kwargs["new_user"]:
if kwargs.get('retry_password'):
# Attempting to fix password
text = "Enter a new password:"
else:
text = ("Creating a new account |c{}|n. "
"Enter a password (empty to abort):".format(username))
else:
text = "Enter the password for account |c{}|n (empty to abort):".format(username)
options = ({"key": "",
"goto": _restart_login},
{"key": ("quit", "q"),
"goto": "node_quit_or_login"},
{"key": ("help", "h"),
"goto": (_show_help, {"help_entry": _PASSWORD_HELP, **kwargs})},
{"key": "_default",
"goto": (_check_input, kwargs)})
return text, options
def node_quit_or_login(caller, raw_text, **kwargs):
"""
Exit menu, either by disconnecting or logging in.
"""
session = caller
if kwargs.get("login"):
account = kwargs.get("account")
session.msg("|gLogging in ...|n")
session.sessionhandler.login(session, account)
else:
session.sessionhandler.disconnect(session, "Goodbye! Logging off.")
return "", {}
# Other functions
# EvMenu helper function
def _formatter(nodetext, optionstext, caller=None):
def _node_formatter(nodetext, optionstext, caller=None):
"""Do not display the options, only the text.
This function is used by EvMenu to format the text of nodes.
Options are not displayed for this menu, where it doesn't often
make much sense to do so. Thus, only the node text is displayed.
This function is used by EvMenu to format the text of nodes. The menu login
is just a series of prompts so we disable all automatic display decoration
and let the nodes handle everything on their own.
"""
return nodetext
@ -337,13 +226,17 @@ class CmdUnloggedinLook(Command):
An unloggedin version of the look command. This is called by the server
when the account first connects. It sets up the menu before handing off
to the menu's own look command.
"""
key = syscmdkeys.CMD_LOGINSTART
locks = "cmd:all()"
arg_regex = r"^$"
def func(self):
"Execute the menu"
"""
Run the menu using the nodes in this module.
"""
EvMenu(self.caller, "evennia.contrib.menu_login",
startnode="start", auto_look=False, auto_quit=False,
cmd_on_exit=None, node_formatter=_formatter)
startnode="node_enter_username", auto_look=False, auto_quit=False,
cmd_on_exit=None, node_formatter=_node_formatter)

800
evennia/contrib/puzzles.py Normal file
View file

@ -0,0 +1,800 @@
"""
Puzzles System - Provides a typeclass and commands for
objects that can be combined (i.e. 'use'd) to produce
new objects.
Evennia contribution - Henddher 2018
A Puzzle is a recipe of what objects (aka parts) must
be combined by a player so a new set of objects
(aka results) are automatically created.
Consider this simple Puzzle:
orange, mango, yogurt, blender = fruit smoothie
As a Builder:
@create/drop orange
@create/drop mango
@create/drop yogurt
@create/drop blender
@create/drop fruit smoothie
@puzzle smoothie, orange, mango, yogurt, blender = fruit smoothie
...
Puzzle smoothie(#1234) created successfuly.
@destroy/force orange, mango, yogurt, blender, fruit smoothie
@armpuzzle #1234
Part orange is spawned at ...
Part mango is spawned at ...
....
Puzzle smoothie(#1234) has been armed successfully
As Player:
use orange, mango, yogurt, blender
...
Genius, you blended all fruits to create a fruit smoothie!
Details:
Puzzles are created from existing objects. The given
objects are introspected to create prototypes for the
puzzle parts and results. These prototypes become the
puzzle recipe. (See PuzzleRecipe and @puzzle
command). Once the recipe is created, all parts and result
can be disposed (i.e. destroyed).
At a later time, a Builder or a Script can arm the puzzle
and spawn all puzzle parts in their respective
locations (See @armpuzzle).
A regular player can collect the puzzle parts and combine
them (See use command). If player has specified
all pieces, the puzzle is considered solved and all
its puzzle parts are destroyed while the puzzle results
are spawened on their corresponding location.
Installation:
Add the PuzzleSystemCmdSet to all players.
Alternatively:
@py self.cmdset.add('evennia.contrib.puzzles.PuzzleSystemCmdSet')
"""
import itertools
from random import choice
from evennia import create_script
from evennia import CmdSet
from evennia import DefaultScript
from evennia import DefaultCharacter
from evennia import DefaultRoom
from evennia import DefaultExit
from evennia.commands.default.muxcommand import MuxCommand
from evennia.utils.utils import inherits_from
from evennia.utils import search, utils, logger
from evennia.prototypes.spawner import spawn
# Tag used by puzzles
_PUZZLES_TAG_CATEGORY = 'puzzles'
_PUZZLES_TAG_RECIPE = 'puzzle_recipe'
# puzzle part and puzzle result
_PUZZLES_TAG_MEMBER = 'puzzle_member'
_PUZZLE_DEFAULT_FAIL_USE_MESSAGE = 'You try to utilize %s but nothing happens ... something amiss?'
_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = 'You are a Genius!!!'
_PUZZLE_DEFAULT_SUCCESS_USE_LOCATION_MESSAGE = "|c{caller}|n performs some kind of tribal dance and |y{result_names}|n seems to appear from thin air"
# ----------- UTILITY FUNCTIONS ------------
def proto_def(obj, with_tags=True):
"""
Basic properties needed to spawn
and compare recipe with candidate part
"""
protodef = {
# TODO: Don't we need to honor ALL properties? attributes, contents, etc.
'prototype_key': '%s(%s)' % (obj.key, obj.dbref),
'key': obj.key,
'typeclass': obj.typeclass_path,
'desc': obj.db.desc,
'location': obj.location,
'home': obj.home,
'locks': ';'.join(obj.locks.all()),
'permissions': obj.permissions.all()[:],
}
if with_tags:
tags = obj.tags.all(return_key_and_category=True)
tags = [(t[0], t[1], None) for t in tags]
tags.append((_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY, None))
protodef['tags'] = tags
return protodef
def maskout_protodef(protodef, mask):
"""
Returns a new protodef after removing protodef values based on mask
"""
protodef = dict(protodef)
for m in mask:
if m in protodef:
protodef.pop(m)
return protodef
# Colorize the default success message
def _colorize_message(msg):
_i = 0
_colors = ['|r', '|g', '|y']
_msg = []
for l in msg:
_msg += _colors[_i] + l
_i = (_i + 1) % len(_colors)
msg = ''.join(_msg) + '|n'
return msg
_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = _colorize_message(_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE)
# ------------------------------------------
class PuzzleRecipe(DefaultScript):
"""
Definition of a Puzzle Recipe
"""
def save_recipe(self, puzzle_name, parts, results):
self.db.puzzle_name = str(puzzle_name)
self.db.parts = tuple(parts)
self.db.results = tuple(results)
self.db.mask = tuple()
self.tags.add(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY)
self.db.use_success_message = _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE
self.db.use_success_location_message = _PUZZLE_DEFAULT_SUCCESS_USE_LOCATION_MESSAGE
class CmdCreatePuzzleRecipe(MuxCommand):
"""
Creates a puzzle recipe. A puzzle consists of puzzle-parts that
the player can 'use' together to create a specified result.
Usage:
@puzzle name,<part1[,part2,...>] = <result1[,result2,...]>
Example:
create/drop balloon
create/drop glass of water
create/drop water balloon
@puzzle waterballon,balloon,glass of water = water balloon
@del ballon, glass of water, water balloon
@armpuzzle #1
Notes:
Each part and result are objects that must (temporarily) exist and be placed in their
corresponding location in order to create the puzzle. After the creation of the puzzle,
these objects are not needed anymore and can be deleted. Components of the puzzle
will be re-created by use of the `@armpuzzle` command later.
"""
key = '@puzzle'
aliases = '@puzzlerecipe'
locks = 'cmd:perm(puzzle) or perm(Builder)'
help_category = 'Puzzles'
confirm = True
default_confirm = 'no'
def func(self):
caller = self.caller
if len(self.lhslist) < 2 or not self.rhs:
string = "Usage: @puzzle name,<part1[,...]> = <result1[,...]>"
caller.msg(string)
return
puzzle_name = self.lhslist[0]
if len(puzzle_name) == 0:
caller.msg('Invalid puzzle name %r.' % puzzle_name)
return
# if there is another puzzle with same name
# warn user that parts and results will be
# interchangable
_puzzles = search.search_script_attribute(
key='puzzle_name',
value=puzzle_name
)
_puzzles = list(filter(lambda p: isinstance(p, PuzzleRecipe), _puzzles))
if _puzzles:
confirm = 'There are %d puzzles with the same name.\n' % len(_puzzles) \
+ 'Its parts and results will be interchangeable.\n' \
+ 'Continue yes/[no]? '
answer = ''
while answer.strip().lower() not in ('y', 'yes', 'n', 'no'):
answer = yield(confirm)
answer = self.default_confirm if answer == '' else answer
if answer.strip().lower() in ('n', 'no'):
caller.msg('Cancelled: no puzzle created.')
return
def is_valid_obj_location(obj):
valid = True
# Rooms are the only valid locations.
# TODO: other valid locations could be added here.
# Certain locations can be handled accordingly: e.g,
# a part is located in a character's inventory,
# perhaps will translate into the player character
# having the part in his/her inventory while being
# located in the same room where the builder was
# located.
# Parts and results may have different valid locations
if not inherits_from(obj.location, DefaultRoom):
caller.msg('Invalid location for %s' % (obj.key))
valid = False
return valid
def is_valid_part_location(part):
return is_valid_obj_location(part)
def is_valid_result_location(part):
return is_valid_obj_location(part)
def is_valid_inheritance(obj):
valid = not inherits_from(obj, DefaultCharacter) \
and not inherits_from(obj, DefaultRoom) \
and not inherits_from(obj, DefaultExit)
if not valid:
caller.msg('Invalid typeclass for %s' % (obj))
return valid
def is_valid_part(part):
return is_valid_inheritance(part) \
and is_valid_part_location(part)
def is_valid_result(result):
return is_valid_inheritance(result) \
and is_valid_result_location(result)
parts = []
for objname in self.lhslist[1:]:
obj = caller.search(objname)
if not obj:
return
if not is_valid_part(obj):
return
parts.append(obj)
results = []
for objname in self.rhslist:
obj = caller.search(objname)
if not obj:
return
if not is_valid_result(obj):
return
results.append(obj)
for part in parts:
caller.msg('Part %s(%s)' % (part.name, part.dbref))
for result in results:
caller.msg('Result %s(%s)' % (result.name, result.dbref))
proto_parts = [proto_def(obj) for obj in parts]
proto_results = [proto_def(obj) for obj in results]
puzzle = create_script(PuzzleRecipe, key=puzzle_name)
puzzle.save_recipe(puzzle_name, proto_parts, proto_results)
puzzle.locks.add('control:id(%s) or perm(Builder)' % caller.dbref[1:])
caller.msg(
"Puzzle |y'%s' |w%s(%s)|n has been created |gsuccessfully|n."
% (puzzle.db.puzzle_name, puzzle.name, puzzle.dbref))
caller.msg(
'You may now dispose of all parts and results. \n'
'Use @puzzleedit #{dbref} to customize this puzzle further. \n'
'Use @armpuzzle #{dbref} to arm a new puzzle instance.'.format(dbref=puzzle.dbref))
class CmdEditPuzzle(MuxCommand):
"""
Edits puzzle properties
Usage:
@puzzleedit[/delete] <#dbref>
@puzzleedit <#dbref>/use_success_message = <Custom message>
@puzzleedit <#dbref>/use_success_location_message = <Custom message from {caller} producing {result_names}>
@puzzleedit <#dbref>/mask = attr1[,attr2,...]>
@puzzleedit[/addpart] <#dbref> = <obj[,obj2,...]>
@puzzleedit[/delpart] <#dbref> = <obj[,obj2,...]>
@puzzleedit[/addresult] <#dbref> = <obj[,obj2,...]>
@puzzleedit[/delresult] <#dbref> = <obj[,obj2,...]>
Switches:
addpart - adds parts to the puzzle
delpart - removes parts from the puzzle
addresult - adds results to the puzzle
delresult - removes results from the puzzle
delete - deletes the recipe. Existing parts and results aren't modified
mask - attributes to exclude during matching (e.g. location, desc, etc.)
use_success_location_message containing {result_names} and {caller} will
automatically be replaced with correct values. Both are optional.
When removing parts/results, it's possible to remove all.
"""
key = '@puzzleedit'
locks = 'cmd:perm(puzzleedit) or perm(Builder)'
help_category = 'Puzzles'
def func(self):
self._USAGE = "Usage: @puzzleedit[/switches] <dbref>[/attribute = <value>]"
caller = self.caller
if not self.lhslist:
caller.msg(self._USAGE)
return
if '/' in self.lhslist[0]:
recipe_dbref, attr = self.lhslist[0].split('/')
else:
recipe_dbref = self.lhslist[0]
if not utils.dbref(recipe_dbref):
caller.msg("A puzzle recipe's #dbref must be specified.\n" + self._USAGE)
return
puzzle = search.search_script(recipe_dbref)
if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe):
caller.msg('%s(%s) is not a puzzle' % (puzzle[0].name, recipe_dbref))
return
puzzle = puzzle[0]
puzzle_name_id = '%s(%s)' % (puzzle.name, puzzle.dbref)
if 'delete' in self.switches:
if not (puzzle.access(caller, 'control') or puzzle.access(caller, 'delete')):
caller.msg("You don't have permission to delete %s." % puzzle_name_id)
return
puzzle.delete()
caller.msg('%s was deleted' % puzzle_name_id)
return
elif 'addpart' in self.switches:
objs = self._get_objs()
if objs:
added = self._add_parts(objs, puzzle)
caller.msg('%s were added to parts' % (', '.join(added)))
return
elif 'delpart' in self.switches:
objs = self._get_objs()
if objs:
removed = self._remove_parts(objs, puzzle)
caller.msg('%s were removed from parts' % (', '.join(removed)))
return
elif 'addresult' in self.switches:
objs = self._get_objs()
if objs:
added = self._add_results(objs, puzzle)
caller.msg('%s were added to results' % (', '.join(added)))
return
elif 'delresult' in self.switches:
objs = self._get_objs()
if objs:
removed = self._remove_results(objs, puzzle)
caller.msg('%s were removed from results' % (', '.join(removed)))
return
else:
# edit attributes
if not (puzzle.access(caller, 'control') or puzzle.access(caller, 'edit')):
caller.msg("You don't have permission to edit %s." % puzzle_name_id)
return
if attr == 'use_success_message':
puzzle.db.use_success_message = self.rhs
caller.msg(
"%s use_success_message = %s\n" % (
puzzle_name_id, puzzle.db.use_success_message)
)
return
elif attr == 'use_success_location_message':
puzzle.db.use_success_location_message = self.rhs
caller.msg(
"%s use_success_location_message = %s\n" % (
puzzle_name_id, puzzle.db.use_success_location_message)
)
return
elif attr == 'mask':
puzzle.db.mask = tuple(self.rhslist)
caller.msg(
"%s mask = %r\n" % (puzzle_name_id, puzzle.db.mask)
)
return
def _get_objs(self):
if not self.rhslist:
self.caller.msg(self._USAGE)
return
objs = []
for o in self.rhslist:
obj = self.caller.search(o)
if obj:
objs.append(obj)
return objs
def _add_objs_to(self, objs, to):
"""Adds propto objs to the given set (parts or results)"""
added = []
toobjs = list(to[:])
for obj in objs:
protoobj = proto_def(obj)
toobjs.append(protoobj)
added.append(obj.key)
return added, toobjs
def _remove_objs_from(self, objs, frm):
"""Removes propto objs from the given set (parts or results)"""
removed = []
fromobjs = list(frm[:])
for obj in objs:
protoobj = proto_def(obj)
if protoobj in fromobjs:
fromobjs.remove(protoobj)
removed.append(obj.key)
return removed, fromobjs
def _add_parts(self, objs, puzzle):
added, toobjs = self._add_objs_to(objs, puzzle.db.parts)
puzzle.db.parts = tuple(toobjs)
return added
def _remove_parts(self, objs, puzzle):
removed, fromobjs = self._remove_objs_from(objs, puzzle.db.parts)
puzzle.db.parts = tuple(fromobjs)
return removed
def _add_results(self, objs, puzzle):
added, toobjs = self._add_objs_to(objs, puzzle.db.results)
puzzle.db.results = tuple(toobjs)
return added
def _remove_results(self, objs, puzzle):
removed, fromobjs = self._remove_objs_from(objs, puzzle.db.results)
puzzle.db.results = tuple(fromobjs)
return removed
class CmdArmPuzzle(MuxCommand):
"""
Arms a puzzle by spawning all its parts.
Usage:
@armpuzzle <puzzle #dbref>
Notes:
Create puzzles with `@puzzle`; get list of
defined puzzles using `@lspuzlerecipies`.
"""
key = '@armpuzzle'
locks = 'cmd:perm(armpuzzle) or perm(Builder)'
help_category = 'Puzzles'
def func(self):
caller = self.caller
if self.args is None or not utils.dbref(self.args):
caller.msg("A puzzle recipe's #dbref must be specified")
return
puzzle = search.search_script(self.args)
if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe):
caller.msg('Invalid puzzle %r' % (self.args))
return
puzzle = puzzle[0]
caller.msg(
"Puzzle Recipe %s(%s) '%s' found.\nSpawning %d parts ..." % (
puzzle.name, puzzle.dbref, puzzle.db.puzzle_name, len(puzzle.db.parts)))
for proto_part in puzzle.db.parts:
part = spawn(proto_part)[0]
caller.msg("Part %s(%s) spawned and placed at %s(%s)" % (
part.name, part.dbref, part.location, part.location.dbref))
part.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY)
part.db.puzzle_name = puzzle.db.puzzle_name
caller.msg("Puzzle armed |gsuccessfully|n.")
def _lookups_parts_puzzlenames_protodefs(parts):
# Create lookup dicts by part's dbref and by puzzle_name(tags)
parts_dict = dict()
puzzlename_tags_dict = dict()
puzzle_ingredients = dict()
for part in parts:
parts_dict[part.dbref] = part
protodef = proto_def(part, with_tags=False)
# remove 'prototype_key' as it will prevent equality
del(protodef['prototype_key'])
puzzle_ingredients[part.dbref] = protodef
tags_categories = part.tags.all(return_key_and_category=True)
for tag, category in tags_categories:
if category != _PUZZLES_TAG_CATEGORY:
continue
if tag not in puzzlename_tags_dict:
puzzlename_tags_dict[tag] = []
puzzlename_tags_dict[tag].append(part.dbref)
return parts_dict, puzzlename_tags_dict, puzzle_ingredients
def _puzzles_by_names(names):
# Find all puzzles by puzzle name (i.e. tag name)
puzzles = []
for puzzle_name in names:
_puzzles = search.search_script_attribute(
key='puzzle_name',
value=puzzle_name
)
_puzzles = list(filter(lambda p: isinstance(p, PuzzleRecipe), _puzzles))
if not _puzzles:
continue
else:
puzzles.extend(_puzzles)
return puzzles
def _matching_puzzles(puzzles, puzzlename_tags_dict, puzzle_ingredients):
# Check if parts can be combined to solve a puzzle
matched_puzzles = dict()
for puzzle in puzzles:
puzzle_protoparts = list(puzzle.db.parts[:])
puzzle_mask = puzzle.db.mask[:]
# remove tags and prototype_key as they prevent equality
for i, puzzle_protopart in enumerate(puzzle_protoparts[:]):
del(puzzle_protopart['tags'])
del(puzzle_protopart['prototype_key'])
puzzle_protopart = maskout_protodef(puzzle_protopart, puzzle_mask)
puzzle_protoparts[i] = puzzle_protopart
matched_dbrefparts = []
parts_dbrefs = puzzlename_tags_dict[puzzle.db.puzzle_name]
for part_dbref in parts_dbrefs:
protopart = puzzle_ingredients[part_dbref]
protopart = maskout_protodef(protopart, puzzle_mask)
if protopart in puzzle_protoparts:
puzzle_protoparts.remove(protopart)
matched_dbrefparts.append(part_dbref)
else:
if len(puzzle_protoparts) == 0:
matched_puzzles[puzzle.dbref] = matched_dbrefparts
return matched_puzzles
class CmdUsePuzzleParts(MuxCommand):
"""
Searches for all puzzles whose parts match the given set of objects. If there are matching
puzzles, the result objects are spawned in their corresponding location if all parts have been
passed in.
Usage:
use <part1[,part2,...>]
"""
key = 'use'
aliases = 'combine'
locks = 'cmd:pperm(use) or pperm(Player)'
help_category = 'Puzzles'
def func(self):
caller = self.caller
if not self.lhs:
caller.msg('Use what?')
return
many = 'these' if len(self.lhslist) > 1 else 'this'
# either all are parts, or abort finding matching puzzles
parts = []
partnames = self.lhslist[:]
for partname in partnames:
part = caller.search(
partname,
multimatch_string='Which %s. There are many.\n' % (partname),
nofound_string='There is no %s around.' % (partname)
)
if not part:
return
if not part.tags.get(_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY):
# not a puzzle part ... abort
caller.msg('You have no idea how %s can be used' % (many))
return
# a valid part
parts.append(part)
# Create lookup dicts by part's dbref and by puzzle_name(tags)
parts_dict, puzzlename_tags_dict, puzzle_ingredients = \
_lookups_parts_puzzlenames_protodefs(parts)
# Find all puzzles by puzzle name (i.e. tag name)
puzzles = _puzzles_by_names(puzzlename_tags_dict.keys())
logger.log_info("PUZZLES %r" % ([(p.dbref, p.db.puzzle_name) for p in puzzles]))
# Create lookup dict of puzzles by dbref
puzzles_dict = dict((puzzle.dbref, puzzle) for puzzle in puzzles)
# Check if parts can be combined to solve a puzzle
matched_puzzles = _matching_puzzles(
puzzles, puzzlename_tags_dict, puzzle_ingredients)
if len(matched_puzzles) == 0:
# TODO: we could use part.fail_message instead, if there was one
# random part falls and lands on your feet
# random part hits you square on the face
caller.msg(_PUZZLE_DEFAULT_FAIL_USE_MESSAGE % (many))
return
puzzletuples = sorted(matched_puzzles.items(), key=lambda t: len(t[1]), reverse=True)
logger.log_info("MATCHED PUZZLES %r" % (puzzletuples))
# sort all matched puzzles and pick largest one(s)
puzzledbref, matched_dbrefparts = puzzletuples[0]
nparts = len(matched_dbrefparts)
puzzle = puzzles_dict[puzzledbref]
largest_puzzles = list(itertools.takewhile(lambda t: len(t[1]) == nparts, puzzletuples))
# if there are more than one, choose one at random.
# we could show the names of all those that can be resolved
# but that would give away that there are other puzzles that
# can be resolved with the same parts.
# just hint how many.
if len(largest_puzzles) > 1:
caller.msg(
'Your gears start turning and %d different ideas come to your mind ...\n'
% (len(largest_puzzles))
)
puzzletuple = choice(largest_puzzles)
puzzle = puzzles_dict[puzzletuple[0]]
caller.msg("You try %s ..." % (puzzle.db.puzzle_name))
# got one, spawn its results
result_names = []
for proto_result in puzzle.db.results:
result = spawn(proto_result)[0]
result.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY)
result.db.puzzle_name = puzzle.db.puzzle_name
result_names.append(result.name)
# Destroy all parts used
for dbref in matched_dbrefparts:
parts_dict[dbref].delete()
result_names = ', '.join(result_names)
caller.msg(puzzle.db.use_success_message)
caller.location.msg_contents(
puzzle.db.use_success_location_message.format(
caller=caller, result_names=result_names),
exclude=(caller,)
)
class CmdListPuzzleRecipes(MuxCommand):
"""
Searches for all puzzle recipes
Usage:
@lspuzzlerecipes
"""
key = '@lspuzzlerecipes'
locks = 'cmd:perm(lspuzzlerecipes) or perm(Builder)'
help_category = 'Puzzles'
def func(self):
caller = self.caller
recipes = search.search_script_tag(
_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY)
div = "-" * 60
text = [div]
msgf_recipe = "Puzzle |y'%s' %s(%s)|n"
msgf_item = "%2s|c%15s|n: |w%s|n"
for recipe in recipes:
text.append(msgf_recipe % (recipe.db.puzzle_name, recipe.name, recipe.dbref))
text.append('Success Caller message:\n' + recipe.db.use_success_message + '\n')
text.append('Success Location message:\n' + recipe.db.use_success_location_message + '\n')
text.append('Mask:\n' + str(recipe.db.mask) + '\n')
text.append('Parts')
for protopart in recipe.db.parts[:]:
mark = '-'
for k, v in protopart.items():
text.append(msgf_item % (mark, k, v))
mark = ''
text.append('Results')
for protoresult in recipe.db.results[:]:
mark = '-'
for k, v in protoresult.items():
text.append(msgf_item % (mark, k, v))
mark = ''
else:
text.append(div)
text.append('Found |r%d|n puzzle(s).' % (len(recipes)))
text.append(div)
caller.msg('\n'.join(text))
class CmdListArmedPuzzles(MuxCommand):
"""
Searches for all armed puzzles
Usage:
@lsarmedpuzzles
"""
key = '@lsarmedpuzzles'
locks = 'cmd:perm(lsarmedpuzzles) or perm(Builder)'
help_category = 'Puzzles'
def func(self):
caller = self.caller
armed_puzzles = search.search_tag(
_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY)
armed_puzzles = dict((k, list(g)) for k, g in itertools.groupby(
armed_puzzles,
lambda ap: ap.db.puzzle_name))
div = '-' * 60
msgf_pznm = "Puzzle name: |y%s|n"
msgf_item = "|m%25s|w(%s)|n at |c%25s|w(%s)|n"
text = [div]
for pzname, items in armed_puzzles.items():
text.append(msgf_pznm % (pzname))
for item in items:
text.append(msgf_item % (
item.name, item.dbref,
item.location.name, item.location.dbref))
else:
text.append(div)
text.append('Found |r%d|n armed puzzle(s).' % (len(armed_puzzles)))
text.append(div)
caller.msg('\n'.join(text))
class PuzzleSystemCmdSet(CmdSet):
"""
CmdSet to create, arm and resolve Puzzles
"""
def at_cmdset_creation(self):
super(PuzzleSystemCmdSet, self).at_cmdset_creation()
self.add(CmdCreatePuzzleRecipe())
self.add(CmdEditPuzzle())
self.add(CmdArmPuzzle())
self.add(CmdListPuzzleRecipes())
self.add(CmdListArmedPuzzles())
self.add(CmdUsePuzzleParts())

View file

@ -185,8 +185,8 @@ class RandomStringGenerator(object):
tree = re.sre_parse.parse(regex).data
# `tree` contains a list of elements in the regular expression
for element in tree:
# `eleemnt` is also a list, the first element is a string
name = element[0]
# `element` is also a list, the first element is a string
name = str(element[0]).lower()
desc = {"min": 1, "max": 1}
# If `.`, break here
@ -213,10 +213,11 @@ class RandomStringGenerator(object):
def _find_literal(self, element):
"""Find the literal corresponding to a piece of regular expression."""
name = str(element[0]).lower()
chars = []
if element[0] == "literal":
if name == "literal":
chars.append(chr(element[1]))
elif element[0] == "in":
elif name == "in":
negate = False
if element[1][0][0] == "negate":
negate = True
@ -233,10 +234,10 @@ class RandomStringGenerator(object):
chars.remove(char)
else:
chars.append(char)
elif element[0] == "range":
elif name == "range":
chars = [chr(i) for i in range(element[1][0], element[1][1] + 1)]
elif element[0] == "category":
category = element[1]
elif name == "category":
category = str(element[1]).lower()
if category == "category_digit":
chars = list(string.digits)
elif category == "category_word":

View file

@ -114,7 +114,7 @@ _VOWELS = "eaoiuy"
# vowel phoneme defined above)
_GRAMMAR = "v cv vc cvv vcc vcv cvcc vccv cvccv cvcvcc cvccvcv vccvccvc cvcvccvv cvcvcvcvv"
_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.UNICODE
_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.DOTALL + re.UNICODE
_RE_GRAMMAR = re.compile(r"vv|cc|v|c", _RE_FLAGS)
_RE_WORD = re.compile(r'\w+', _RE_FLAGS)
_RE_EXTRA_CHARS = re.compile(r'\s+(?=\W)|[,.?;](?=[,.?;]|\s+[,.?;])', _RE_FLAGS)
@ -255,7 +255,7 @@ class LanguageHandler(DefaultScript):
translation = {}
if auto_translations:
if isinstance(auto_translations, basestring):
if isinstance(auto_translations, str):
# path to a file rather than a list
with open(auto_translations, 'r') as f:
auto_translations = f.readlines()
@ -333,7 +333,7 @@ class LanguageHandler(DefaultScript):
new_word = ''
else:
# use random word length
wlen = choice(grammar.keys())
wlen = choice(list(grammar.keys()))
if wlen:
structure = choice(grammar[wlen])
@ -516,4 +516,7 @@ def obfuscate_whisper(whisper, level=0.0):
"""
level = min(max(0.0, level), 1.0)
olevel = int(13.0 * level)
return _RE_WHISPER_OBSCURE[olevel].sub('...' if olevel == 13.0 else '-', whisper)
if olevel == 13:
return "..."
else:
return _RE_WHISPER_OBSCURE[olevel].sub('-', whisper)

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