mirror of
https://github.com/evennia/evennia.git
synced 2026-03-27 18:26:32 +01:00
Merge branch 'develop'
This commit is contained in:
commit
dc6ac210f4
318 changed files with 20372 additions and 4579 deletions
48
.travis.yml
48
.travis.yml
|
|
@ -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
1
.travis/my.conf
Normal file
|
|
@ -0,0 +1 @@
|
|||
init_connect='SET collation_connection = utf8_general_ci; SET NAMES utf8;'
|
||||
71
.travis/mysql_settings.py
Normal file
71
.travis/mysql_settings.py
Normal 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.")
|
||||
62
.travis/postgresql_settings.py
Normal file
62
.travis/postgresql_settings.py
Normal 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.")
|
||||
47
.travis/sqlite3_settings.py
Normal file
47
.travis/sqlite3_settings.py
Normal 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.")
|
||||
214
CHANGELOG.md
214
CHANGELOG.md
|
|
@ -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:
|
||||
|
|
|
|||
14
Dockerfile
14
Dockerfile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.8.0
|
||||
0.9.0-dev
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
73
evennia/accounts/migrations/0008_auto_20190128_1820.py
Normal file
73
evennia/accounts/migrations/0008_auto_20190128_1820.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')]
|
||||
|
|
|
|||
|
|
@ -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)):
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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}.")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
121
evennia/comms/migrations/0017_auto_20190128_1820.py
Normal file
121
evennia/comms/migrations/0017_auto_20190128_1820.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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
13
evennia/comms/tests.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
from evennia.contrib.egi_client.service import EvenniaGameIndexService
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
116
evennia/contrib/evscaperoom/README.md
Normal file
116
evennia/contrib/evscaperoom/README.md
Normal 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.
|
||||
752
evennia/contrib/evscaperoom/commands.py
Normal file
752
evennia/contrib/evscaperoom/commands.py
Normal 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())
|
||||
325
evennia/contrib/evscaperoom/menu.py
Normal file
325
evennia/contrib/evscaperoom/menu.py
Normal 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}))
|
||||
1052
evennia/contrib/evscaperoom/objects.py
Normal file
1052
evennia/contrib/evscaperoom/objects.py
Normal file
File diff suppressed because it is too large
Load diff
237
evennia/contrib/evscaperoom/room.py
Normal file
237
evennia/contrib/evscaperoom/room.py
Normal 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}"
|
||||
32
evennia/contrib/evscaperoom/scripts.py
Normal file
32
evennia/contrib/evscaperoom/scripts.py
Normal 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()
|
||||
293
evennia/contrib/evscaperoom/state.py
Normal file
293
evennia/contrib/evscaperoom/state.py
Normal 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
|
||||
23
evennia/contrib/evscaperoom/states/README.md
Normal file
23
evennia/contrib/evscaperoom/states/README.md
Normal 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`.
|
||||
176
evennia/contrib/evscaperoom/states/state_001_start.py
Normal file
176
evennia/contrib/evscaperoom/states/state_001_start.py
Normal 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()
|
||||
313
evennia/contrib/evscaperoom/tests.py
Normal file
313
evennia/contrib/evscaperoom/tests.py
Normal 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)
|
||||
187
evennia/contrib/evscaperoom/utils.py
Normal file
187
evennia/contrib/evscaperoom/utils.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
800
evennia/contrib/puzzles.py
Normal 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())
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue