Merge branch 'master' into latin_il8n

Added Latin translation of Evennia's core messages as per the directions
in evennia/locale/README.
This commit is contained in:
jamalainm 2021-02-05 15:52:14 -06:00
commit ce469f2765
590 changed files with 51168 additions and 4796 deletions

12
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: griatch
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://www.paypal.me/GriatchEvennia

View file

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

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Questions
url: https://github.com/evennia/evennia/discussions?discussions_q=category%3A%22Evennia+Questions%22
about: Ask usage questions here (Github Discussions)
- name: Feature discussions
url: https://github.com/evennia/evennia/discussions?discussions_q=category%3A%22Evennia+Engine+Development%22
about: If you want to discuss before making a formal feature request (Github Discussions)

View file

@ -1,14 +1,17 @@
---
name: Documentation issue
about: Documentation problems and suggestions
title: '[Documentation] Enter a brief description here'
title: '[Documentation] (Enter a brief description here)'
labels: documentation
assignees: ''
---
#### Existing page / new
(Link to existing documentation page or proposed name of new page)
#### Documentation issue
Describe what the issue is and where it can/should be found.
(Replace with the description of what the issue is or motivate a changes/additions)
#### Suggested change
The suggested change.
(Enter the suggested change here)

View file

@ -0,0 +1,59 @@
# This github-Evennia workflow will build the docs.
name: documentation
on:
push:
branches: [ master, develop ]
paths:
- 'docs/**'
pull_request:
branches: [ master, develop ]
paths:
- 'docs/**'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install doc-building dependencies
run: |
python -m pip install --upgrade pip
cd docs/
make install
# fail early here, run quickstrict with aborts also on warnings
- name: Quick-test docs (no autodocs)
run: |
cd docs/
make quickstrict
# full game dir needed for mv-local
- name: Set up evennia game dir
run: |
pip install -e .
cd ..
evennia --init gamedir
cd gamedir
evennia migrate
- name: Deploy docs (only from master/develop branch)
if: ${{ github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master'}}
run: |
git config --global user.email "docbuilder@evennia.com"
git config --global user.name "Doc builder mechanism"
cd docs
make mv-local
echo "Would deploy here!"

View file

@ -0,0 +1,109 @@
# This Evennia workflow will install Python dependencies, run tests with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: test-suite
on:
push:
branches: [ master, develop ]
paths-ignore:
- 'docs/**'
pull_request:
branches: [ master, develop ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [3.7, 3.8]
TESTING_DB: ['sqlite3', 'postgresql', 'mysql']
steps:
- uses: actions/checkout@v2
- name: Set up PostgreSQL server
uses: harmon758/postgresql-action@v1
if: ${{ matrix.TESTING_DB == 'postgresql' }}
with:
postgresql version: '10.7'
postgresql db: 'evennia'
postgresql user: 'evennia'
postgresql password: 'password'
- name: Set up MySQL server
uses: mirromutth/mysql-action@v1.1
if: ${{ matrix.TESTING_DB == 'mysql'}}
with:
host port: 3306
character set server: 'utf8mb4'
collation server: 'utf8mb4_unicode_ci'
mysql database: 'evennia'
mysql user: 'evennia'
mysql password: 'password'
# wait for db to activage, get logs from their start
- name: Wait / sleep
uses: jakejarvis/wait-action@v0.1.0
if: ${{ matrix.TESTING_DB == 'postgresql' || matrix.TESTING_DB == 'mysql' }}
with:
time: '10s'
- name: Database container logs
uses: jwalton/gh-docker-logs@v1.0.0
- name: Check running containers
run: docker ps -a
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install package dependencies
run: |
python -m pip install --upgrade pip
pip install wheel
pip install psycopg2-binary
pip install mysqlclient
pip install coveralls
pip install codacy-coverage
pip install -e .
- name: Install extra dependencies # Only develop branch right now
if: ${{ github.ref == 'refs/heads/develop' }}
run: pip install -r requirements_extra.txt
- name: Initialize evennia
run: |
evennia --init testing_mygame
cp .github/workflows/${{ matrix.TESTING_DB }}_settings.py testing_mygame/server/conf/settings.py
cd testing_mygame
evennia migrate
evennia collectstatic --noinput
- name: Run test suite
run: |
cd testing_mygame
coverage run --source=../evennia --omit=*/migrations/*,*/urls.py,*/test*.py,*.sh,*.txt,*.md,*.pyc,*.service ../bin/unix/evennia test --settings=settings --keepdb evennia
coverage xml
# we only want to run coverall/codacy once, so we only do it for one of the matrix combinations
# it's also not critical if pushing to either service fails (happens for PRs since env is not
# available outside of the evennia org)
- name: Send data to Coveralls
if: ${{ matrix.TESTING_DB == 'sqlite3' && matrix.python-version == 3.7 }}
continue-on-error: true
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
run: |
cd testing_mygame
coveralls
- name: Send data to Codacy
if: ${{ matrix.TESTING_DB == 'sqlite3' && matrix.python-version == 3.7 }}
continue-on-error: true
uses: codacy/codacy-coverage-reporter-action@master
with:
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
coverage-reports: ./testing_mygame/coverage.xml

View file

@ -39,25 +39,24 @@ 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'
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "evennia",
"USER": "evennia",
"PASSWORD": "password",
"HOST": "127.0.0.1",
"PORT": "", # use default port
"OPTIONS": {
"charset": "utf8mb4",
"init_command": "set collation_connection=utf8mb4_unicode_ci",
},
"TEST": {
"NAME": "evennia",
"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'"
}
}
}
}

View file

@ -39,16 +39,14 @@ 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'
}
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "evennia",
"USER": "evennia",
"PASSWORD": "password",
"HOST": "localhost",
"PORT": "", # use default
"TEST": {"NAME": "default"},
}
}

3
.gitignore vendored
View file

@ -47,3 +47,6 @@ nosetests.xml
# Windows files generated during setup
evennia.bat
twistd.bat
# never commit docs/build
docs/build

View file

@ -1,50 +0,0 @@
dist: xenial
language: python
cache: pip
services:
- postgresql
- mysql
python:
- "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:
- make format
- 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 --settings=settings --keepdb evennia
after_success:
- coveralls
- coverage xml
- python-codacy-coverage -r coverage.xml

View file

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

View file

@ -1,12 +1,16 @@
# Changelog
## Evennia 1.0 (2019-) (WIP)
## Evennia 1.0-dev (2019-) (WIP)
- new `drop:holds()` lock default to limit dropping nonsensical things. Access check
- New `drop:holds()` lock default to limit dropping nonsensical things. Access check
defaults to True for backwards-compatibility in 0.9, will be False in 1.0
- Add `tags.has()` method for checking if an object has a tag or tags (PR by ChrisLR)
### Already in master
### Evennia 0.9.5 (Nov 2020)
A transitional release, including new doc system.
- `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False
- `py` command now reroutes stdout to output results in-game client. `py`
without arguments starts a full interactive Python console.
- Webclient default to a single input pane instead of two. Now defaults to no help-popup.
@ -27,13 +31,63 @@ without arguments starts a full interactive Python console.
- `AttributeHandler.get(return_list=True)` will return `[]` if there are no
Attributes instead of `[None]`.
- Remove `pillow` requirement (install especially if using imagefield)
- Add Simplified Korean translation (user aceamro)
- Add Simplified Korean translation (aceamro)
- Show warning on `start -l` if settings contains values unsafe for production.
- Make code auto-formatted with Black.
- Make default `set` command able to edit nested structures (PR by Aaron McMillan)
- Allow running Evennia test suite from core repo with `make test`.
- Return `store_key` from `TickerHandler.add` and add `store_key` as a kwarg to
the `TickerHandler.remove` method. This makes it easier to manage tickers.
- Return `store_key` from `TickerHandler.add` and add `store_key` as a kwarg to
the `TickerHandler.remove` method. This makes it easier to manage tickers.
- EvMore auto-justify now defaults to False since this works better with all types
of texts (such as tables). New `justify` bool. Old `justify_kwargs` remains
but is now only used to pass extra kwargs into the justify function.
- EvMore `text` argument can now also be a list or a queryset. Querysets will be
sliced to only return the required data per page.
- Improve performance of `find` and `objects` commands on large data sets (strikaco)
- New `CHANNEL_HANDLER_CLASS` setting allows for replacing the ChannelHandler entirely.
- Made `py` interactive mode support regular quit() and more verbose.
- Made `Account.options.get` accept `default=None` kwarg to mimic other uses of get. Set
the new `raise_exception` boolean if ranting to raise KeyError on a missing key.
- Moved behavior of unmodified `Command` and `MuxCommand` `.func()` to new
`.get_command_info()` method for easier overloading and access. (Volund)
- Removed unused `CYCLE_LOGFILES` setting. Added `SERVER_LOG_DAY_ROTATION`
and `SERVER_LOG_MAX_SIZE` (and equivalent for PORTAL) to control log rotation.
- Addded `inside_rec` lockfunc - if room is locked, the normal `inside()` lockfunc will
fail e.g. for your inventory objs (since their loc is you), whereas this will pass.
- RPSystem contrib's CmdRecog will now list all recogs if no arg is given. Also multiple
bugfixes.
- Remove `dummy@example.com` as a default account email when unset, a string is no longer
required by Django.
- Fixes to `spawn`, make updating an existing prototype/object work better. Add `/raw` switch
to `spawn` command to extract the raw prototype dict for manual editing.
- `list_to_string` is now `iter_to_string` (but old name still works as legacy alias). It will
now accept any input, including generators and single values.
- EvTable should now correctly handle columns with wider asian-characters in them.
- Update Twisted requirement to >=2.3.0 to close security vulnerability
- Add `$random` inlinefunc, supports minval,maxval arguments that can be ints and floats.
- Add `evennia.utils.inlinefuncs.raw(<str>)` as a helper to escape inlinefuncs in a string.
- Make CmdGet/Drop/Give give proper error if `obj.move_to` returns `False`.
- Make `Object/Room/Exit.create`'s `account` argument optional. If not given, will set perms
to that of the object itself (along with normal Admin/Dev permission).
- Make `INLINEFUNC_STACK_MAXSIZE` default visible in `settings_default.py`.
- Change how `ic` finds puppets; non-priveleged users will use `_playable_characters` list as
candidates, Builders+ will use list, local search and only global search if no match found.
- Make `cmd.at_post_cmd()` always run after `cmd.func()`, even when the latter uses delays
with yield.
- `EvMore` support for db queries and django paginators as well as easier to override for custom
pagination (e.g. to create EvTables for every page instead of splitting one table).
- New `EvMore` methods `.init_pages`, `paginator` and `page_formatter` for easily customize pagination.
- Using `EvMore pagination`, dramatically improves performance of `spawn/list` and `scripts` listings
(100x speed increase for displaying 1000+ prototypes/scripts).
- `EvMenu` now uses the more logically named `.ndb._evmenu` instead of `.ndb._menutree` to store itself.
Both still work for backward compatibility, but `_menutree` is deprecated.
- `EvMenu.msg(txt)` added as a central place to send text to the user, makes it easier to override.
Default `EvMenu.msg` sends with OOB type="menu" for use with OOB and webclient pane-redirects.
- New EvMenu templating system for quickly building simpler EvMenus without as much code.
- Add `Command.client_height()` method to match existing `.client_width` (stricako)
- Include more Web-client info in `session.protocol_flags`.
- Fixes in multi-match situations - don't allow finding/listing multimatches for 3-box when
only two boxes in location.
## Evennia 0.9 (2018-2019)

View file

@ -101,7 +101,7 @@ def funcname(a, b, c, d=False, **kwargs):
c (list): A list argument.
d (bool, optional): An optional keyword argument.
Kwargs:
Keyword Args:
test (list): A test keyword.
Returns:
@ -165,13 +165,13 @@ Args:
and
```
Kwargs:
Keyword Args:
argname (type): text
```
mean the same thing! Which one is used depends on the function or
method documented, but there are no hard rules; If there is a large
`**kwargs` block in the function, using the `Kwargs:` block may be a
`**kwargs` block in the function, using the `Keyword Args:` block may be a
good idea, for a small number of arguments though, just using `Args:`
and marking keywords as `optional` will shorten the docstring and make
it easier to read.

View file

@ -1,137 +1,5 @@
# Evennia installation
The latest and more detailed installation instructions can be found
[here](https://github.com/evennia/evennia/wiki/Getting-Started).
## Installing Python
First install [Python](https://www.python.org/). Linux users should
have it in their repositories, Windows/Mac users can get it from the
Python homepage. You need the 2.7.x version (Python 3 is not yet
supported). Windows users, make sure to select the option to make
Python available in your path - this is so you can call it everywhere
as `python`. Python 2.7.9 and later also includes the
[pip](https://pypi.python.org/pypi/pip/) installer out of the box,
otherwise install this separately (in linux it's usually found as the
`python-pip` package).
### installing virtualenv
This step is optional, but *highly* recommended. For installing
up-to-date Python packages we recommend using
[virtualenv](https://pypi.python.org/pypi/virtualenv), this makes it
easy to keep your Python packages up-to-date without interfering with
the defaults for your system.
```
pip install virtualenv
```
Go to the place where you want to make your virtual python library
storage. This does not need to be near where you plan to install
Evennia. Then do
```
virtualenv vienv
```
A new folder `vienv` will be created (you could also name it something
else if you prefer). Activate the virtual environment like this:
```
# for Linux/Unix/Mac:
source vienv/bin/activate
# for Windows:
vienv\Scripts\activate.bat
```
You should see `(vienv)` next to your prompt to show you the
environment is active. You need to activate it whenever you open a new
terminal, but you *don't* have to be inside the `vienv` folder henceforth.
## Get the developer's version of Evennia
This is currently the only Evennia version available. First download
and install [Git](http://git-scm.com/) from the homepage or via the
package manager in Linux. Next, go to the place where you want the
`evennia` folder to be created and run
```
git clone https://github.com/evennia/evennia.git
```
If you have a github account and have [set up SSH
keys](https://help.github.com/articles/generating-ssh-keys/), you want
to use this instead:
```
git clone git@github.com:evennia/evennia.git
```
In the future you just enter the new `evennia` folder and do
```
git pull
```
to get the latest Evennia updates.
## Evennia package install
Stand at the root of your new `evennia` directory and run
```
pip install -e .
```
(note the period "." at the end, this tells pip to install from the
current directory). This will install Evennia and all its dependencies
(into your virtualenv if you are using that) and make the `evennia`
command available on the command line. You can find Evennia's
dependencies in `evennia/requirements.txt`.
## Creating your game project
To create your new game you need to initialize a new game project.
This should be done somewhere *outside* of your `evennia` folder.
```
evennia --init mygame
```
This will create a new game project named "mygame" in a folder of the
same name. If you want to change the settings for your project, you
will need to edit `mygame/server/conf/settings.py`.
## Starting Evennia
Enter your new game directory and run
```
evennia migrate
evennia start
```
Follow the instructions to create your superuser account. A lot of
information will scroll past as the database is created and the server
initializes. After this Evennia will be running. Use
```
evennia -h
```
for help with starting, stopping and other operations.
Start up your MUD client of choice and point it to your server and
port *4000*. If you are just running locally the server name is
*localhost*.
Alternatively, you can find the web interface and webclient by
pointing your web browser to *http://localhost:4001*.
Finally, login with the superuser account and password you provided
earlier. Welcome to Evennia!
You can find the latest updated installation instructions and
requirements [here](https://github.com/evennia/evennia/wiki/Getting-Started).

View file

@ -1,9 +1,9 @@
# This is used with `make <option>` and is used for running various
# administration operations on the code.
BLACK_FORMAT_CONFIGS = --target-version py37 --line-length 100
BLACK_FORMAT_CONFIGS = --target-version py37 --line-length 100 --exclude=/docs
TEST_GAME_DIR = .test_game_dir
tests?=evennia
TESTS ?= evennia
default:
@echo " Usage: "
@ -29,10 +29,10 @@ test:
evennia --init $(TEST_GAME_DIR);\
cd $(TEST_GAME_DIR);\
evennia migrate;\
evennia test --keepdb $(tests);\
evennia test --keepdb $(TESTS);\
testp:
evennia --init $(TEST_GAME_DIR);\
cd $(TEST_GAME_DIR);\
evennia migrate;\
evennia test --keepdb --parallel 4 $(tests);\
evennia test --keepdb --parallel 4 $(TESTS);\

View file

@ -1,5 +1,6 @@
# Evennia MUD/MU\* Creation System ![evennia logo][logo]
[![Build Status][travisimg]][travislink] [![Coverage Status][coverimg]][coverlink]
[![Build Status][unittestciimg]][unittestcilink] [![Coverage Status][coverimg]][coverlink]
*Evennia* is a modern library for creating [online multiplayer text
games][wikimudpage] (MUD, MUSH, MUX, MUCK, MOO etc) in pure Python. It
@ -69,8 +70,8 @@ Welcome!
[wiki]: https://github.com/evennia/evennia/wiki
[screenshot]: https://user-images.githubusercontent.com/294267/30773728-ea45afb6-a076-11e7-8820-49be2168a6b8.png
[logo]: https://github.com/evennia/evennia/blob/master/evennia/web/website/static/website/images/evennia_logo.png
[travisimg]: https://travis-ci.org/evennia/evennia.svg?branch=master
[travislink]: https://travis-ci.org/evennia/evennia
[unittestciimg]: https://github.com/evennia/evennia/workflows/test-suite/badge.svg
[unittestcilink]: https://github.com/evennia/evennia/actions?query=workflow%3Atest-suite
[coverimg]: https://coveralls.io/repos/github/evennia/evennia/badge.svg?branch=master
[coverlink]: https://coveralls.io/github/evennia/evennia?branch=master
[introduction]: https://github.com/evennia/evennia/wiki/Evennia-Introduction

View file

@ -23,7 +23,7 @@ USE_COLOR = True
FAKE_MODE = False
# if these words are longer than output word, retain given case
CASE_WORD_EXCEPTIONS = ('an', )
CASE_WORD_EXCEPTIONS = ("an",)
_HELP_TEXT = """This program interactively renames words in all files of your project. It's
currently renaming {sources} to {targets}.
@ -80,6 +80,7 @@ def _case_sensitive_replace(string, old, new):
`old` has been replaced with `new`, retaining case.
"""
def repl(match):
current = match.group()
# treat multi-word sentences word-by-word
@ -99,12 +100,12 @@ def _case_sensitive_replace(string, old, new):
all_upper = False
# special cases - keep remaing case)
if new_word.lower() in CASE_WORD_EXCEPTIONS:
result.append(new_word[ind + 1:])
result.append(new_word[ind + 1 :])
# append any remaining characters from new
elif all_upper:
result.append(new_word[ind + 1:].upper())
result.append(new_word[ind + 1 :].upper())
else:
result.append(new_word[ind + 1:].lower())
result.append(new_word[ind + 1 :].lower())
out.append("".join(result))
# if we have more new words than old ones, just add them verbatim
out.extend([new_word for ind, new_word in enumerate(new_words) if ind >= len(old_words)])
@ -156,7 +157,7 @@ def rename_in_tree(path, in_list, out_list, excl_list, fileend_list, is_interact
new_file = _case_sensitive_replace(new_file, src, dst)
if new_file != file:
inp = input(_green("Rename %s\n -> %s\n Y/[N]? > " % (file, new_file)))
if inp.upper() == 'Y':
if inp.upper() == "Y":
new_full_path = os.path.join(root, new_file)
try:
os.rename(full_path, new_full_path)
@ -172,7 +173,7 @@ def rename_in_tree(path, in_list, out_list, excl_list, fileend_list, is_interact
new_root = _case_sensitive_replace(new_root, src, dst)
if new_root != root:
inp = input(_green("Dir Rename %s\n -> %s\n Y/[N]? > " % (root, new_root)))
if inp.upper() == 'Y':
if inp.upper() == "Y":
try:
os.rename(root, new_root)
except OSError as err:
@ -201,7 +202,7 @@ def rename_in_file(path, in_list, out_list, is_interactive):
print("%s is a directory. You should use the --recursive option." % path)
sys.exit()
with open(path, 'r') as fil:
with open(path, "r") as fil:
org_text = fil.read()
repl_mapping = list(zip(in_list, out_list))
@ -215,7 +216,7 @@ def rename_in_file(path, in_list, out_list, is_interactive):
if FAKE_MODE:
print(" ... Saved changes to %s. (faked)" % path)
else:
with open(path, 'w') as fil:
with open(path, "w") as fil:
fil.write(new_text)
print(" ... Saved changes to %s." % path)
else:
@ -244,13 +245,17 @@ def rename_in_file(path, in_list, out_list, is_interactive):
print(" new : %s" % (_yellow(renamed_line)))
print(_green("%s (%i lines changed)" % (path, len(renamed))))
ret = input(_green("Choose: "
"[q]uit, "
"[h]elp, "
"[s]kip file, "
"[i]gnore lines, "
"[c]lear ignores, "
"[a]ccept/save file: ".lower()))
ret = input(
_green(
"Choose: "
"[q]uit, "
"[h]elp, "
"[s]kip file, "
"[i]gnore lines, "
"[c]lear ignores, "
"[a]ccept/save file: ".lower()
)
)
if ret == "s":
# skip file entirely
@ -267,7 +272,7 @@ def rename_in_file(path, in_list, out_list, is_interactive):
if FAKE_MODE:
print(" ... Saved file %s (faked)" % path)
return
with open(path, 'w') as fil:
with open(path, "w") as fil:
fil.writelines("\n".join(org_lines))
print(" ... Saved file %s" % path)
return
@ -278,7 +283,7 @@ def rename_in_file(path, in_list, out_list, is_interactive):
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()]
ignores = [int(ind) - 1 for ind in ret[1:].split(",") if ind.strip().isdigit()]
if not ignores:
input("Ignore example: i 2,7,34,133\n (return to continue)")
continue
@ -290,37 +295,46 @@ def rename_in_file(path, in_list, out_list, is_interactive):
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Rename text in a source tree, or a single file")
parser = argparse.ArgumentParser(description="Rename text in a source tree, or a single file")
parser.add_argument('-i', '--input', action='append',
help="Source word to rename (quote around multiple words)")
parser.add_argument('-o', '--output', action='append',
help="Word to rename a matching src-word to")
parser.add_argument('-x', '--exc', action='append',
help="File path patterns to exclude")
parser.add_argument('-a', '--auto', action='store_true',
help="Automatic mode, don't ask to rename")
parser.add_argument('-r', '--recursive', action='store_true',
help="Recurse subdirs")
parser.add_argument('-f', '--fileending', action='append',
help="Change which file endings to allow (default .py and .html)")
parser.add_argument('--nocolor', action='store_true',
help="Turn off in-program color")
parser.add_argument('--fake', action='store_true',
help="Simulate run but don't actually save")
parser.add_argument('path',
help="File or directory in which to rename text")
parser.add_argument(
"-i",
"--input",
action="append",
help="Source word to rename (quote around multiple words)",
)
parser.add_argument(
"-o", "--output", action="append", help="Word to rename a matching src-word to"
)
parser.add_argument("-x", "--exc", action="append", help="File path patterns to exclude")
parser.add_argument(
"-a", "--auto", action="store_true", help="Automatic mode, don't ask to rename"
)
parser.add_argument("-r", "--recursive", action="store_true", help="Recurse subdirs")
parser.add_argument(
"-f",
"--fileending",
action="append",
help="Change which file endings to allow (default .py and .html)",
)
parser.add_argument("--nocolor", action="store_true", help="Turn off in-program color")
parser.add_argument("--fake", action="store_true", help="Simulate run but don't actually save")
parser.add_argument("path", help="File or directory in which to rename text")
args = parser.parse_args()
in_list, out_list, exc_list, fileend_list = args.input, args.output, args.exc, args.fileending
in_list, out_list, exc_list, fileend_list = (
args.input,
args.output,
args.exc,
args.fileending,
)
if not (in_list and out_list):
print('At least one source- and destination word must be given.')
print("At least one source- and destination word must be given.")
sys.exit()
if len(in_list) != len(out_list):
print('Number of sources must be identical to the number of destination arguments.')
print("Number of sources must be identical to the number of destination arguments.")
sys.exit()
exc_list = exc_list or []

View file

@ -14,4 +14,5 @@ sys.path.insert(0, os.path.abspath(os.getcwd()))
sys.path.insert(0, os.path.join(sys.prefix, "Lib", "site-packages"))
from evennia.server.evennia_launcher import main
main()

145
docs/Makefile Normal file
View file

@ -0,0 +1,145 @@
# Makefile to control Evennia documentation building.
# Most common commands are `make help`, `make quick` and `make local`.
SOURCEDIR = source
BUILDDIR = build
AUTODOCDIR = $(SOURCEDIR)/api
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SPHINXMULTIVERSION ?= sphinx-multiversion
SPHINXAPIDOC ?= sphinx-apidoc
SPHINXAPIDOCOPTS = --tocfile evennia-api --module-first --force --maxdepth 6 --separate --templatedir=$(SOURCEDIR)/_templates/
SPHINXAPIDOCENV = members,undoc-members,show-inheritance
SPHINXAPIDOCEXCLUDE = ../*/migrations/* ../evennia/game_template/* ../evennia/*/tests/* ../evennia/*/tests.py
EVDIR ?= $(realpath ../evennia)
EVGAMEDIR ?= $(realpath ../../gamedir)
cblue = $(shell echo "\033[1m\033[34m")
cnorm = $(shell echo "\033[0m")
# set specific files to only run for quick run (even if unset, will still not build new api docs)
QUICKFILES=
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@echo "Evennia-specific: "
@echo " $(cblue)install$(cnorm) to get build requirements"
@echo " $(cblue)clean$(cnorm) to remove remnants of a previous build"
@echo " $(cblue)local$(cnorm) to build local html docs of the current branch (no multiversion)."
@echo " $(cblue)mv-local$(cnorm) to build multiversion html docs, without deploying (req: local git commit first)"
@echo " $(cblue)deploy$(cnorm) to deploy previously built multiversion docs online (req: commit and github push access)"
@echo " $(cblue)release$(cnorm) to build + deploy multiversion docs online (req: commit and github push access)"
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# Evennia - custom commands
# helper targets
_check-env:
@EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) bash -e checkenv.sh
_multiversion-check-env:
@EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) bash -e checkenv.sh multiversion
_clean_api_index:
rm source/api/*
# remove superfluos 'module' and 'package' text from api headers
_reformat_apidoc_headers:
for f in source/api/*.rst; do\
perl -pi -e 's/(module|package)$$// if $$. == 1' $$f ;\
done
_autodoc-index:
make _clean_api_index
@EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) SPHINX_APIDOC_OPTIONS=$(SPHINXAPIDOCENV) $(SPHINXAPIDOC) $(SPHINXAPIDOCOPTS) -o $(SOURCEDIR)/api/ $(EVDIR) $(SPHINXAPIDOCEXCLUDE)
make _reformat_apidoc_headers
_multiversion-autodoc-index:
make _clean_api_index
@EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) SPHINX_APIDOC_OPTIONS=$(SPHINXAPIDOCENV) $(SPHINXAPIDOC) $(SPHINXAPIDOCOPTS) -o $(SOURCEDIR)/api/ $(EVDIR) $(SPHINXAPIDOCEXCLUDE)
make _reformat_apidoc_headers
git diff-index --quiet HEAD || git commit -a -m "Updated API autodoc index." | :
_html-build:
@EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) $(SPHINXBUILD) $(SPHINXOPTS) "$(SOURCEDIR)" "$(BUILDDIR)/html"
_quick-html-build:
@NOAUTODOC=1 EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) $(SPHINXBUILD) $(SPHINXOPTS) "$(SOURCEDIR)" "$(BUILDDIR)/html" $(QUICKFILES)
_multiversion-build:
@EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) $(SPHINXMULTIVERSION) $(SPHINXOPTS) "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS)
_multiversion-deploy:
@bash -e deploy.sh
@EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) $(SPHINXMULTIVERSION) $(SPHINXOPTS) "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS)
_latex-build:
@NOAUTODOC=1 EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) $(SPHINXBUILD) -M latexpdf "$(SOURCEDIR)" "$(BUILDDIR)/latex" $(QUICKFILES)
# main targets
install:
@pip install -r requirements.txt
clean:
@rm -Rf $(BUILDDIR)
@git clean -f -d docs/
@echo "Cleaned old build dir and leftover files."
# not fully working at this time
pdf:
make _latex-build
@echo ""
@echo "Documentation built (single version, no autodocs)."
@echo "To see result, open evennia/docs/build/latex/evennia.pdf in a PDF reader."
quick:
make _quick-html-build $(FILES)
@echo ""
@echo "Documentation built (single version, no autodocs)."
@echo "To see result, open evennia/docs/build/html/index.html in a browser."
# abort on warnings too
quickstrict:
SPHINXOPTS=-W make quick
local:
make _check-env
make clean
make _autodoc-index
make _html-build
@echo ""
@echo "Documentation built (single version)."
@echo "To see result, open evennia/docs/build/html/index.html in a browser."
mv-local:
make _multiversion-check-env
make clean
make _multiversion-autodoc-index
make _multiversion-build
@echo ""
@echo "Documentation built (multiversion + autodocs)."
@echo "To see result, open evennia/docs/build/html/<version>/index.html in a browser."
deploy:
make _multiversion-deploy
@echo "Documentation deployed."
# build and prepare the docs for release
release:
make mv-local
make deploy
@echo "Release complete."

496
docs/README.md Normal file
View file

@ -0,0 +1,496 @@
# Evennia docs
Documentation for the Evennia MUD creation system.
> WARNING: This system is still WIP and many things are bound to change!
> Contributing is still primarily to be done in the wiki.
The live documentation is (will in the future be) available at `https://evennia.github.io/evennia/`.
# Editing the docs
The documentation source files are `*.md` (Markdown) files found in `evennia/docs/source/`.
Markdown files are simple text files that can be edited with a normal text editor. They primaroly use
the [Markdown][commonmark] syntax. See [the syntax section below](#Editing-syntax) for more help.
Don't edit the files in `source/api/`. These are auto-generated and your changes
will be lost.
## Contributing
Contributing to the docs is is like [contributing to the rest of Evennia][contributing]:
Check out the branch of Evennia you want to edit the documentation for. Create your
own work-branch, make your changes to files in `evennia/docs/source/` and make a PR for it!
# Building the docs locally
The sources in `evennia/docs/source/` are built into a pretty documentation using
the [Sphinx][sphinx] static generator system. To do so locally you need to either
use a system with `make` (Linux/Unix/Mac or [Windows-WSL][Windows-WSL]). Lacking that, you could
in principle also run the sphinx build-commands manually - read the `evennia/docs/Makefile` to see
which commands are run by `make`.
You don't necessarily _have_ to build the docs locally to contribute. But
building them allows you to check for yourself that syntax is correct and that
your change comes out looking as you expected.
## Building only the main documentation
If you only want to build the main documentation pages (not the API autodocs),
you don't need to install Evennia itself, only the documentation resources.
All is done in your terminal/console.
- (Optional, but recommended): Activate a virtualenv with Python 3.7.
- `cd` to into the `evennia/docs` folder (where this README is).
- Install the documentation-build requirements:
```
make install
or
pip install -r requirements.txt
```
- Next, build the html-based documentation.
```
make quick
```
- The html-based documentation will appear in the new
folder `evennia/docs/build/html/`. Note any errors from files you have edited.
- Use a web browser to open `evennia/docs/build/html/index.html` and view the docs.
Note that you will get errors if clicking a link to the auto-docs, because you didn't build them!
## Building the main documentation and API docs
The full documentation includes both the doc pages and the API documentation
generated from the Evennia source. For this you must install Evennia and
initialize a new game with a default database (you don't need to have it
running)
- Follow the normal [Evennia Getting-Started instructions][getting-started]
to install Evennia. Use a virtualenv.
- Make sure you `cd` to the folder _containing_ your `evennia/` repo (so two levels up from `docs/`).
- Create a new game folder called `gamedir` at the same level as your `evennia`
repo with
```
evennia --init gamedir
```
- Then `cd` into it and create a new, empty database. You don't need to start the game
or do any further changes.
```
evennia migrate
```
- This is how the structure should look at this point:
```
(top)
|
----- evennia/ (the top-level folder, containing docs/)
|
----- gamedir/
```
- Make sure you are still in your virtualenv, then go to `evennia/docs/` and
install the doc-building requirements:
```
make install
or
pip install -r requirements.txt
```
- Finally, build the full documentation, including the auto-docs:
```
make local
```
- The rendered files will appear in a new folder `evennia/docs/build/html`.
Note any errors from files you have edited.
- Point your web browser to `evennia/docs/build/html/index.html` to view the full docs.
### Building with another gamedir
If you for some reason want to use another location of your `gamedir/`, or want it
named something else (maybe you already use the name 'gamedir' for your development ...),
you can do so by setting the `EVGAMEDIR` environment variable to the absolute path
of your alternative game dir. For example:
```
EVGAMEDIR=/my/path/to/mygamedir make local
```
## Building for release
The full Evennia documentation also tracks documentation from older Evennia
versions. This is done by pulling documentation from Evennia's old release
branches and building them all so readers can choose which one to view. Only
specific official Evennia branches will be built, so you can't use this to
build your own testing branch.
- All local changes must have been committed to git first, since the versioned
docs are built by looking at the git tree.
- To build for local checking, run (`mv` stands for "multi-version"):
```
make mv-local
```
- The different versions will be found under `evennia/docs/build/versions/`.
- If you have git-push access to the Evennia `gh-pages` branch on `github`, you
can now deploy.
```
make deploy
```
- If you know what you are doing you can also do build + deploy in one step:
```
make release
```
- After deployment finishes, the updated live documentation will be
available at `https://evennia.github.io/evennia/`.
# Editing syntax
The format is [Markdown][commonmark-help] (Commonmark). While markdown supports a few alternative
forms for some of these, we try to stick to the below forms for consistency.
## Italic/Bold
We generally use underscores for italics and double-asterisks for bold:
- `_Italic text_`
- `**Bold Text**`
## Headings
We use `#` to indicate sections/headings. The more `#` the more of a sub-heading it is (will get smaller
and smaller font).
- `# Heading`
- `## SubHeading`
- `## SubSubHeading`
> Don't reuse the same heading/subheading name over and over in the same document. While Markdown does not prevent
it, it makes it impossible to link to those duplicates properly (see next section).
## Lists
One can create both bullet-point lists and numbered lists:
```markdown
- first bulletpoint
- second bulletpoint
- third bulletpoint
```
```markdown
1. Numbered point one
2. Numbered point two
3. Numbered point three
```
## Notes
A note can be used to enphasise important things. It's added by starting one or more lines with `>`.
```
> Note: This is an important
> thing to remember.
```
## Links
- `[linktext](url_or_ref)` - gives a clickable link `linktext`.
The `url_or_ref` can either be a full `http://...` url or an internal _reference_. For example, use
`[my document](My-Document)` to link to the document `evennia/docs/source/My-Document.md`. Avoid using
full `http://` linking unless really referring to an external resource.
- `[linktext](ref#heading-name)`
You can point to sub-sections (headings) in a document by using a single `#` and the name of the
heading, replacing spaces with dashes. So to refer to a heading `## Cool Stuff` inside `My-Document`
would be a link `[cool stuff](My-Document#Cool-Stuff)`.
- `[linktext][linkref]` - refer to a reference defined later in the document.
Urls can get long and if you are using the same url in many places it can get a little cluttered. So you can also put
the url as a 'footnote' at the end of your document
and refer to it by putting your reference within square brackets `[ ]`. Here's an example:
```
This is a [clickable link][mylink]. This is [another link][1].
...
[mylink]: http://...
[1]: My-Document
```
### Special references
The Evennia documentation supports some special reference shortcuts in links:
#### Github online repository
- `github:` - a shortcut for the full path to the Evennia repository on github. This will refer to
the `master` branch by default:
[link to objects.py](github:evennia/objects/objects.py)
This will remap to https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py.
- To refer to the `develop` branch, start the url with `develop/`:
[link to objects.py](github:develop/evennia/objects/objects.py)
#### API
- `api:` - references a path in the api documentation. This is specified as a Python-path:
[link to api for objects.py](api:evennia.objects)
This will create a link to the auto-generated `evennia/source/api/evennia.objects.rst` document.
Since api-docs are generated alongside the documentation, this will always be the api docs for the
current version/branch of the docs.
#### Bug reports/feature request
- `issue`, `bug-report`, `feature-request` - links to the same github issue select page.
If you find a problem, make a [bug report](issue)!
This will generate a link to https://github.com/evennia/evennia/issues/new/choose.
> For some reason these particular shortcuts gives a warning during documentation compilation. This
> can be ignored.
## Verbatim text
It's common to want to mark something to be displayed verbatim - just as written - without any
Markdown parsing. In running text, this is done using backticks (\`), like \`verbatim text\` becomes `verbatim text`.
If you want to put the verbatim text on its own line, you can do so easily by simply indenting
it 4 spaces (add empty lines on each side for readability too):
```
This is normal text
This is verbatim text
This is normal text
```
Another way is to use triple-backticks:
````
```
Everything within these backticks will be verbatim.
```
````
## Code blocks
A special case is code examples - we want them to get code-highlighting for readability. This is done by using
the triple-backticks and specify which language we use:
````
```python
def a_python_func(x):
return x * x
```
````
## ReST blocks
Markdown is easy to read and use. But while it does most of what we need, there are some things it's
not quite as expressive as it needs to be. For this we need to fall back to the [ReST][ReST] markup
language which the documentation system uses under the hood. This is done by specifying `eval_rst` as
the name of the `language` of a literal block:
````
```eval_rst
This will be evaluated as ReST.
```
````
There is also a short-hand form for starting a [ReST directive][ReST-directives] without need for `eval_rst`:
````
```directive:: possible-option
Content that *must* be indented for it to be included in the directive.
New lines are ignored except if separated by an empty line.
```
````
See below for examples of this.
#### Important
This will display a one-line note that will pop even more than a normal `> note`.
````
```important::
This is important because it is!
```
````
#### Warning
A warning block is used to draw attention to particularly dangerous things, or features easy to
mess up.
````
```warning::
Be careful about this ...
````
#### Version changes and deprecations
These will show up as one-line warnings that suggest an added, changed or deprecated
feature beginning with particular version.
````
```versionadded:: 1.0
```
````
````
```versionchanged:: 1.0
How the feature changed with this version.
```
````
````
```deprecated:: 1.0
```
````
#### Sidebar
This will display an informative sidebar that floats to the side of regular content. This is useful
for example to remind the reader of some concept relevant to the text.
````
```sidebar:: Things to remember
- There can be bullet lists
- in here.
Headers with indented blocks:
like this
Will end up as full sub-headings:
in the sidebar.
```
````
> Remember that for ReST-directives, the content within the triple-backticks _must_ be indented to
>some degree or the content will just appear outside of the directive as regular text.
#### Tables
A table is specified using [ReST table syntax][ReST-tables]:
````
```eval_rst
===== ===== =======
A B A and B
===== ===== =======
False False False
True False False
False True False
True True True
===== ===== =======
```
````
or the more flexible but verbose
````
```eval_rst
+------------------------+------------+----------+----------+
| Header row, column 3 | Header 2 | Header 3 | Header 4 |
| (header rows optional) | | | |
+========================+============+==========+==========+
| body row 1, column 1 | column 2 | column 3 | column 4 |
+------------------------+------------+----------+----------+
| body row 2 | ... | ... | |
+------------------------+------------+----------+----------+
```
````
#### A more flexible code block
The regular Markdown codeblock is usually enough but for more direct control over the style, one
can also specify the code block explicitly in `ReST`.
for more flexibility. It also provides a link to the code block, identified by its name.
````
```code-block:: python
:linenos:
:emphasize-lines: 6-7,12
:caption: An example code block
:name: A full code block example
from evennia import Command
class CmdEcho(Command):
"""
Usage: echo <arg>
"""
key = "echo"
def func(self):
self.caller.msg(self.args.strip())
```
````
Here, `:linenos:` turns on line-numbers and `:emphasize-lines:` allows for emphasizing certain lines
in a different color. The `:caption:` shows an instructive text and `:name:` is used to reference this
block through the link that will appear (so it should be unique for a give document).
> The default markdown syntax will actually generate a code-block ReST instruction like this
> automatically for us behind the scenes. The automatic generation can't know things like emphasize-lines
> or caption since that's not a part of the Markdown specification.
# Technical
Evennia leverages [Sphinx][sphinx] with the [recommonmark][recommonmark] extension, which allows us to write our
docs in light-weight Markdown (more specifically [CommonMark][commonmark], like on github) rather than ReST.
The recommonmark extension however also allows us to use ReST selectively in the places were it is more
expressive than the simpler (but much easier) Markdown.
For [autodoc-generation][sphinx-autodoc] generation, we use the sphinx-[napoleon][sphinx-napoleon] extension
to understand our friendly Google-style docstrings used in classes and functions etc.
[sphinx]: https://www.sphinx-doc.org/en/master/
[recommonmark]: https://recommonmark.readthedocs.io/en/latest/index.html
[commonmark]: https://spec.commonmark.org/current/
[commonmark-help]: https://commonmark.org/help/
[sphinx-autodoc]: http://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#module-sphinx.ext.autodoc
[sphinx-napoleon]: http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
[getting-started]: https://github.com/evennia/evennia/wiki/Getting-Started
[contributing]: https://github.com/evennia/evennia/wiki/Contributing
[ReST]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
[ReST-tables]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#tables
[ReST-directives]: https://www.sphinx-doc.org/en/master/usage/restruturedtext/directives.html
[Windows-WSL]: https://docs.microsoft.com/en-us/windows/wsl/install-win10

30
docs/checkenv.sh Normal file
View file

@ -0,0 +1,30 @@
# check environment
# common checks
if [ ! -d "$EVDIR" ]; then
echo "The evennia dir is not found at $EVDIR.";
exit 1
fi
if [ ! -d "$EVGAMEDIR" ]; then
echo "The gamedir is not found at $EVGAMEDIR";
exit 1
fi
if [ $# -ne 0 ]
# a multi-version build
then
if [ -n "$(git status --untracked-files=no --porcelain)" ]; then
echo "There are uncommitted changes. Make sure to commit everything in your current branch before doing a multiversion build."
exit 1
fi
fi

37
docs/deploy.sh Normal file
View file

@ -0,0 +1,37 @@
#
# deploy to github
#
# This copies the recently built files from build/html into the github-gh branch. Note that
# it's important that build/ must not be committed to git!
#
if [ -n "$(git status --untracked-files=no --porcelain)" ]; then
echo "There are uncommitted changes. Make sure to commit everything in your current branch first."
exit 1
fi
# get the deployment branch
git checkout gh-pages
# at this point we should be inside the docs/ folder of the gh-pages branch,
# with the build/ directory available since this is not in git
# remove all but the build dir
ls -Q | grep -v build | xargs rm -Rf
cp -Rf build/html/* .
# TODO automate this?
ln -s 0.9.5 latest
ln -s 0.9.5/index.html .
# docs/build is in .gitignore so will not be included
git add .
git commit -a -m "Updated HTML docs"
echo "Skipping deployment"
git push origin gh-pages
# get back to previous branch
git checkout -
echo "Deployed to https://evennia.github.io/evennia-docs."

35
docs/make.bat Normal file
View file

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View file

@ -0,0 +1,224 @@
"""
Build a TOC-tree; Sphinx requires it and this makes it easy to just
add/build/link new files without needing to explicitly add it to a toctree
directive somewhere.
"""
import re
from collections import defaultdict
from sphinx.errors import DocumentError
from pathlib import Path
from os.path import abspath, dirname, join as pathjoin, sep, relpath
_IGNORE_FILES = []
_SOURCEDIR_NAME = "source"
_SOURCE_DIR = pathjoin(dirname(dirname(abspath(__file__))), _SOURCEDIR_NAME)
_TOC_FILE = pathjoin(_SOURCE_DIR, "toc.md")
_NO_REMAP_STARTSWITH = [
"http://",
"https://",
"github:",
"api:",
"feature-request",
"report-bug",
"issue",
"bug-report",
]
TXT_REMAPS = {}
URL_REMAPS = {}
_USED_REFS = {}
_CURRFILE = None
def auto_link_remapper(no_autodoc=False):
"""
- Auto-Remaps links to fit with the actual document file structure. Requires
all doc files to have a unique name.
- Creates source/toc.md file
"""
global _CURRFILE
print(" -- Auto-Remapper starting.")
def _get_rel_source_ref(path):
"""Get the path relative the source/ dir"""
pathparts = path.split("/")
# we allow a max of 4 levels of nesting in the source dir
ind = pathparts[-5:].index(_SOURCEDIR_NAME)
# get the part after source/
pathparts = pathparts[-5 + 1 + ind :]
url = "/".join(pathparts)
# get the reference, without .md
url = url.rsplit(".", 1)[0]
return url
toc_map = {}
docref_map = defaultdict(dict)
for path in Path(_SOURCE_DIR).rglob("*.md"):
# find the source/ part of the path and strip it out
if path.name in _IGNORE_FILES:
# this is the name including .md
continue
sourcepath = path.as_posix()
# get name and url relative to source/
fname = path.name.rsplit(".", 1)[0]
src_url = _get_rel_source_ref(sourcepath)
# check for duplicate files
if fname in toc_map:
duplicate_src_url = toc_map[fname]
raise DocumentError(
f" Tried to add {src_url}.md, but a file {duplicate_src_url}.md already exists.\n"
" Evennia's auto-link-corrector does not accept doc-files with the same \n"
" name, even in different folders. Rename one.\n"
)
toc_map[fname] = src_url
# find relative links to all other files
for targetpath in Path(_SOURCE_DIR).rglob("*.md"):
targetname = targetpath.name.rsplit(".", 1)[0]
targetpath = targetpath.as_posix()
url = relpath(targetpath, dirname(sourcepath))
if not "/" in url:
# need to be explicit or there will be link ref collisions between
# e.g. TickerHandler page and TickerHandle api node
url = "./" + url
docref_map[sourcepath][targetname] = url.rsplit(".", 1)[0]
# normal reference-links [txt](urls)
ref_regex = re.compile(
r"\[(?P<txt>[\w -\[\]\`]+?)\]\((?P<url>.+?)\)", re.I + re.S + re.U + re.M
)
# in document references
ref_doc_regex = re.compile(
r"\[(?P<txt>[\w -\`]+?)\]:\s+?(?P<url>.+?)(?=$|\n)", re.I + re.S + re.U + re.M
)
def _sub(match):
# inline reference links
global _USED_REFS
grpdict = match.groupdict()
txt, url = grpdict["txt"], grpdict["url"]
txt = TXT_REMAPS.get(txt, txt)
url = URL_REMAPS.get(url, url)
if any(url.startswith(noremap) for noremap in _NO_REMAP_STARTSWITH):
return f"[{txt}]({url})"
if "http" in url and "://" in url:
urlout = url
else:
fname, *part = url.rsplit("/", 1)
fname = part[0] if part else fname
fname = fname.rsplit(".", 1)[0]
fname, *anchor = fname.rsplit("#", 1)
if not _CURRFILE.endswith("toc.md"):
_USED_REFS[fname] = url
if _CURRFILE in docref_map and fname in docref_map[_CURRFILE]:
cfilename = _CURRFILE.rsplit("/", 1)[-1]
urlout = docref_map[_CURRFILE][fname] + ("#" + anchor[0] if anchor else "")
if urlout != url:
print(f" {cfilename}: [{txt}]({url}) -> [{txt}]({urlout})")
else:
urlout = url
return f"[{txt}]({urlout})"
def _sub_doc(match):
# reference links set at the bottom of the page
global _USED_REFS
grpdict = match.groupdict()
txt, url = grpdict["txt"], grpdict["url"]
txt = TXT_REMAPS.get(txt, txt)
url = URL_REMAPS.get(url, url)
if any(url.startswith(noremap) for noremap in _NO_REMAP_STARTSWITH):
return f"[{txt}]: {url}"
if "http" in url and "://" in url:
urlout = url
else:
fname, *part = url.rsplit("/", 1)
fname = part[0] if part else fname
fname = fname.rsplit(".", 1)[0]
fname, *anchor = fname.rsplit("#", 1)
if not _CURRFILE.endswith("toc.md"):
_USED_REFS[fname] = url
if _CURRFILE in docref_map and fname in docref_map[_CURRFILE]:
cfilename = _CURRFILE.rsplit("/", 1)[-1]
urlout = docref_map[_CURRFILE][fname] + ("#" + anchor[0] if anchor else "")
if urlout != url:
print(f" {cfilename}: [{txt}]: {url} -> [{txt}]: {urlout}")
else:
urlout = url
return f"[{txt}]: {urlout}"
# replace / correct links in all files
count = 0
for path in sorted(Path(_SOURCE_DIR).rglob("*.md"), key=lambda p: p.name):
# from pudb import debugger;debugger.Debugger().set_trace()
_CURRFILE = path.as_posix()
with open(path, "r") as fil:
intxt = fil.read()
outtxt = ref_regex.sub(_sub, intxt)
outtxt = ref_doc_regex.sub(_sub_doc, outtxt)
if intxt != outtxt:
with open(path, "w") as fil:
fil.write(outtxt)
count += 1
print(f" -- Auto-relinked links in {path.name}")
if count > 0:
print(f" -- Auto-corrected links in {count} documents.")
for (fname, src_url) in sorted(toc_map.items(), key=lambda tup: tup[0]):
if fname not in _USED_REFS:
print(f" ORPHANED DOC: no refs found to {src_url}.md")
# write tocfile
with open(_TOC_FILE, "w") as fil:
fil.write("# Toc\n")
if not no_autodoc:
fil.write("- [API root](api/evennia-api.rst)")
for ref in sorted(toc_map.values()):
if ref == "toc":
continue
if "Part1/" in ref:
continue
if not "/" in ref:
ref = "./" + ref
linkname = ref.replace("-", " ")
fil.write(f"\n- [{linkname}]({ref})")
# we add a self-reference so the toc itself is also a part of a toctree
fil.write("\n\n```toctree::\n :hidden:\n\n toc\n```")
print(" -- Auto-Remapper finished.")
if __name__ == "__main__":
auto_link_remapper()

View file

@ -0,0 +1,92 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Builds a lunr static search index for optimized search
"""
import os
import json
import glob
from argparse import ArgumentParser
from os.path import sep, abspath, dirname, join as joinpath
from lunr import lunr
_DOCS_PATH = dirname(dirname(abspath(__file__)))
_DEFAULT_BUILD_DIR = joinpath(_DOCS_PATH, "build", "html")
_DEFAULT_URL_BASE = f"file://{_DEFAULT_BUILD_DIR}"
_INDEX_PATH = joinpath("_static", "js", "lunr", "search_index.json")
DEFAULT_SOURCE_DIR = joinpath(_DOCS_PATH, "source")
DEFAULT_OUTFILE = joinpath(DEFAULT_SOURCE_DIR, _INDEX_PATH)
URL_BASE = os.environ.get("SEARCH_URL_BASE", _DEFAULT_URL_BASE)
def create_search_index(sourcedir, outfile):
"""
Create the index.
Args:
sourcedir (str): Path to the source directory. This will be searched
for both .md and .rst files.
outfile (str): Path to the index file to create.
"""
markdown_files = glob.glob(f"{sourcedir}{sep}*.md")
markdown_files.extend(glob.glob(f"{sourcedir}{sep}*{sep}*.md"))
rest_files = glob.glob(f"{sourcedir}{sep}*.rst")
rest_files.extend(glob.glob(f"{sourcedir}{sep}*{sep}*.rst"))
filepaths = markdown_files + rest_files
outlist = []
print(f"Building Search index from {len(filepaths)} files ... ", end="")
for filepath in filepaths:
with open(filepath, "r") as fil:
filename = filepath.rsplit(sep, 1)[1].split(".", 1)[0]
url = f"{URL_BASE}{sep}{filename}.html".strip()
title = filename.replace("-", " ").strip()
body = fil.read()
data = {
"url": url,
"title": title,
"text": body,
}
outlist.append(data)
idx = lunr(
ref="url",
documents=outlist,
fields=[{"field_name": "title", "boost": 10}, {"field_name": "text", "boost": 1}],
)
with open(outfile, "w") as fil:
fil.write(json.dumps(idx.serialize()))
print(f"wrote to source{sep}{_INDEX_PATH}.")
if __name__ == "__main__":
parser = ArgumentParser(description="Build a static search index.")
parser.add_argument(
"-i",
dest="sourcedir",
default=DEFAULT_SOURCE_DIR,
help="Absolute path to the documentation source dir",
)
parser.add_argument(
"-o",
dest="outfile",
default=DEFAULT_OUTFILE,
help="Absolute path to the index file to output.",
)
args = parser.parse_args()
create_search_index(args.sourcedir, args.outfile)

View file

@ -0,0 +1,290 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Copy data from old Evennia github Wiki to static files.
Prepare files for mkdoc. This assumes evennia.wiki is cloned
to a folder at the same level as the evennia repo.
Just run this to update everything.
We also need to build the toc-tree and should do so automatically for now.
"""
import glob
import re
import datetime
import textwrap
_RE_MD_LINK = re.compile(r"\[(?P<txt>[\w -\[\]]+?)\]\((?P<url>.+?)\)", re.I + re.S + re.U)
_RE_REF_LINK = re.compile(r"\[[\w -\[\]]*?\]\(.+?\)", re.I + re.S + re.U)
_RE_CLEAN = re.compile(r"\|-+?|-+\|", re.I + re.S + re.U)
_IGNORE_FILES = (
"_Sidebar.md",
# "Wiki-Index.md"
)
_INDEX_PREFIX = f"""
# VERSION WARNING
> This is the experimental static v0.95 documentation of Evennia, _automatically_ generated from the
> [evennia wiki](https://github.com/evennia/evennia/wiki/) at {datetime.datetime.now()}.
> There are known conversion issues which will _not_ be addressed in this version - refer to
> the original wiki if you have trouble.
>
> Manual conversion and cleanup will instead happen during development of the upcoming v1.0
> version of this static documentation.
"""
_WIKI_DIR = "../../../evennia.wiki/"
_INFILES = [
path
for path in sorted(glob.glob(_WIKI_DIR + "/*.md"))
if path.rsplit("/", 1)[-1] not in _IGNORE_FILES
]
_FILENAMES = [path.rsplit("/", 1)[-1] for path in _INFILES]
_FILENAMES = [path.split(".", 1)[0] for path in _FILENAMES]
_FILENAMESLOW = [path.lower() for path in _FILENAMES]
_OUTDIR = "../source/"
_OLD_WIKI_URL = "https://github.com/evennia/evennia/wiki/"
_OLD_WIKI_URL_LEN = len(_OLD_WIKI_URL)
_CODE_PREFIX = "github:"
_API_PREFIX = "api:"
_CUSTOM_LINK_REMAP = {
"CmdSets": "Command-Sets",
"CmdSet": "Command-Sets",
"Cmdsets": "Command-Sets",
"CommandSet": "Command-Sets",
"batch-code-processor": "Batch-Code-Processor",
"Batch-code-processor": "Batch-Code-Processor",
"batch-command-processor": "Batch-Command-Processor",
"Batch-command-processor": "Batch-Command-Processor",
"evennia-API": "Evennia-API",
"Channels": "Communications#Channels",
"Comms": "Communications",
"typeclass": "Typeclasses",
"Home": "index",
"Help-system": "Help-System",
"Using-Mux-as-a-Standard": "Using-MUX-as-a-Standard",
"Building-quickstart": "Building-Quickstart",
"Adding-Object-Typeclass-tutorial": "Adding-Object-Typeclass-Tutorial",
"EvTable": _API_PREFIX + "evennia.utils#module-evennia.utils.evtable",
}
# complete reference remaps
_REF_REMAP = {
"[![Getting Started][icon_new]](Getting-Started)": "![Getting Started][icon_new]",
"[![Admin Docs][icon_admin]](Administrative-Docs)": "![Admin Docs][icon_admin]",
"[![Builder Docs][icon_builder]](Builder-Docs)": "![Builder Docs][icon_builder]",
"[![Developer-Central][icon_devel]](Developer-Central)": "![Developer-Central][icon_devel]",
"[![tutorial][icon_tutorial]](Tutorials)": "![Tutorials][icon_tutorial]",
"[![API][icon_api]](evennia)": "![API][icon_api]",
"[](Wiki-front-page.)": "",
}
# absolute links (mainly github links) that should not be converted. This
# should be given without any #anchor.
_ABSOLUTE_LINK_SKIP = (
# "https://github.com/evennia/evennia/wiki/feature-request",
)
# specific references tokens that should be ignored. Should be given
# without any #anchor.
_REF_SKIP = (
"[5](Win)",
"[6](Win)",
"[7](Win)",
"[10](Win)",
"[11](Mac)",
"[13](Win)",
"[14](IOS)",
"[15](IOS)",
"[16](Andr)",
"[17](Andr)",
"[18](Unix)",
"[21](Chrome)",
# these should be checked
"[EvTable](EvTable)",
"[styled](OptionStyles)",
"[Inputfunc](Inputfunc)",
"[online documentation wiki](index)",
"[online documentation](index)",
"[Accounts](Account)",
"[Session](Session)",
"[Inputfuncs](Inputfunc)",
)
_CURRENT_TITLE = ""
def _sub_remap(match):
"""Total remaps"""
ref = match.group(0)
if ref in _REF_REMAP:
new_ref = _REF_REMAP[ref]
print(f" Replacing reference {ref} -> {new_ref}")
return new_ref
return ref
def _sub_link(match):
mdict = match.groupdict()
txt, url_orig = mdict["txt"], mdict["url"]
url = url_orig
# if not txt:
# # the 'comment' is not supported by Mkdocs
# return ""
print(f" [{txt}]({url})")
url = _CUSTOM_LINK_REMAP.get(url, url)
url, *anchor = url.rsplit("#", 1)
if url in _ABSOLUTE_LINK_SKIP:
url += ("#" + anchor[0]) if anchor else ""
return f"[{txt}]({url})"
if url.startswith("evennia"):
print(f" Convert evennia url {url} -> {_CODE_PREFIX + url}")
url = _API_PREFIX + url
if url.startswith(_OLD_WIKI_URL):
# old wiki is an url on the form https://<wikiurl>/wiki/TextTags#header
# we don't refer to the old wiki but use internal mapping.
if len(url) != len(_OLD_WIKI_URL):
url_conv = url[_OLD_WIKI_URL_LEN:]
url_conv = re.sub(r"%20", "-", url_conv)
if url_conv.endswith("/_edit"):
# this is actually a bug in the wiki format
url_conv = url_conv[:-6]
if url_conv.startswith("evennia"):
# this is an api link
url_conv = _CODE_PREFIX + url_conv
print(f" Converting wiki-url: {url} -> {url_conv}")
url = url_conv
if not url and anchor:
# this happens on same-file #labels in wiki
url = _CURRENT_TITLE
if url not in _FILENAMES and not url.startswith("http") and not url.startswith(_CODE_PREFIX):
url_cap = url.capitalize()
url_plur = url[:-3] + "s" + ".md"
url_cap_plur = url_plur.capitalize()
link = f"[{txt}]({url})"
if link in _REF_SKIP:
url = link
elif url_cap in _FILENAMES:
print(f" Replacing (capitalized): {url.capitalize()}")
url = url_cap
elif url_plur in _FILENAMES:
print(f" Replacing (pluralized): {url + 's'}")
url = url_plur
elif url_cap_plur in _FILENAMES:
print(f" Replacing (capitalized, pluralized): {url.capitalize() + 's'}")
url = url_cap_plur
elif url.lower() in _FILENAMESLOW:
ind = _FILENAMESLOW.index(url.lower())
alt = _FILENAMES[ind]
print(f" Replacing {url} with different cap: {alt}")
url = alt
# print(f"\nlink {link} (orig: [{txt}]({url_orig})) found no file match")
# inp = input("Enter alternate url (return to keep old): ")
# if inp.strip():
# url = inp.strip()
if anchor:
url += "#" + anchor[0]
return f"[{txt}]({url})"
def create_toctree(files):
with open("../source/toc.md", "w") as fil:
fil.write("# Toc\n")
for path in files:
filename = path.rsplit("/", 1)[-1]
ref = filename.rsplit(".", 1)[0]
linkname = ref.replace("-", " ")
if ref == "Home":
ref = "index"
fil.write(f"\n* [{linkname}]({ref}.md)")
def convert_links(files, outdir):
global _CURRENT_TITLE
for inpath in files:
is_index = False
outfile = inpath.rsplit("/", 1)[-1]
if outfile == "Home.md":
outfile = "index.md"
is_index = True
outfile = _OUTDIR + outfile
title = inpath.rsplit("/", 1)[-1].split(".", 1)[0].replace("-", " ")
print(f"Converting links in {inpath} -> {outfile} ...")
with open(inpath) as fil:
text = fil.read()
if is_index:
text = _INDEX_PREFIX + text
lines = text.split("\n")
lines = (
lines[:-11]
+ [" - The [TOC](toc) lists all regular documentation pages.\n\n"]
+ lines[-11:]
)
text = "\n".join(lines)
_CURRENT_TITLE = title.replace(" ", "-")
text = _RE_CLEAN.sub("", text)
text = _RE_REF_LINK.sub(_sub_remap, text)
text = _RE_MD_LINK.sub(_sub_link, text)
text = (
text.split("\n")[1:]
if text.split("\n")[0].strip().startswith("[]")
else text.split("\n")
)
# wrap text
formatted_lines = []
for line in text:
if line.strip():
formatted_lines.append(textwrap.fill(line, width=100))
else:
formatted_lines.append(line)
text = "\n".join(formatted_lines)
if not is_index:
text = f"# {title}\n\n{text}"
with open(outfile, "w") as fil:
fil.write(text)
if __name__ == "__main__":
create_toctree(_INFILES)
convert_links(_INFILES, _OUTDIR)

11
docs/requirements.txt Normal file
View file

@ -0,0 +1,11 @@
# requirements for building the docs
sphinx==3.2.1
sphinx-multiversion==0.2.4
# lunr==0.5.8
# recommonmark custom branch with evennia-specific fixes
git+https://github.com/evennia/recommonmark.git@evennia-mods#egg=recommonmark
# sphinxcontrib-lunrsearch custom branch with evennia-specific fixes
git+https://github.com/evennia/sphinxcontrib-lunrsearch.git@evennia-mods#egg=sphinxcontrib-lunrsearch

View file

@ -0,0 +1,436 @@
# A voice operated elevator using events
- Previous tutorial: [Adding dialogues in events](./Dialogues-in-events)
This tutorial will walk you through the steps to create a voice-operated elevator, using the [in-
game Python
system](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md).
This tutorial assumes the in-game Python system is installed in your game. If it isn't, you can
follow the installation steps given in [the documentation on in-game
Python](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md), and
come back on this tutorial once the system is installed. **You do not need to read** the entire
documentation, it's a good reference, but not the easiest way to learn about it. Hence these
tutorials.
The in-game Python system allows to run code on individual objects in some situations. You don't
have to modify the source code to add these features, past the installation. The entire system
makes it easy to add specific features to some objects, but not all.
> What will we try to do?
In this tutorial, we are going to create a simple voice-operated elevator. In terms of features, we
will:
- Explore events with parameters.
- Work on more interesting callbacks.
- Learn about chained events.
- Play with variable modification in callbacks.
## Our study case
Let's summarize what we want to achieve first. We would like to create a room that will represent
the inside of our elevator. In this room, a character could just say "1", "2" or "3", and the
elevator will start moving. The doors will close and open on the new floor (the exits leading in
and out of the elevator will be modified).
We will work on basic features first, and then will adjust some, showing you how easy and powerfully
independent actions can be configured through the in-game Python system.
## Creating the rooms and exits we need
We'll create an elevator right in our room (generally called "Limbo", of ID 2). You could easily
adapt the following instructions if you already have some rooms and exits, of course, just remember
to check the IDs.
> Note: the in-game Python system uses IDs for a lot of things. While it is not mandatory, it is
good practice to know the IDs you have for your callbacks, because it will make manipulation much
quicker. There are other ways to identify objects, but as they depend on many factors, IDs are
usually the safest path in our callbacks.
Let's go into limbo (`#2`) to add our elevator. We'll add it to the north. To create this room,
in-game you could type:
tunnel n = Inside of an elevator
The game should respond by telling you:
Created room Inside of an elevator(#3) of type typeclasses.rooms.Room.
Created Exit from Limbo to Inside of an elevator: north(#4) (n).
Created Exit back from Inside of an elevator to Limbo: south(#5) (s).
Note the given IDs:
- `#2` is limbo, the first room the system created.
- `#3` is our room inside of an elevator.
- `#4` is the north exit from Limbo to our elevator.
- `#5` is the south exit from an elevator to Limbo.
Keep these IDs somewhere for the demonstration. You will shortly see why they are important.
> Why have we created exits to our elevator and back to Limbo? Isn't the elevator supposed to move?
It is. But we need to have exits that will represent the way inside the elevator and out. What we
will do, at every floor, will be to change these exits so they become connected to the right room.
You'll see this process a bit later.
We have two more rooms to create: our floor 2 and 3. This time, we'll use `dig`, because we don't
need exits leading there, not yet anyway.
dig The second floor
dig The third floor
Evennia should answer with:
Created room The second floor(#6) of type typeclasses.rooms.Room.
Created room The third floor(#7) of type typeclasses.rooms.Room.
Add these IDs to your list, we will use them too.
## Our first callback in the elevator
Let's go to the elevator (you could use `tel #3` if you have the same IDs I have).
This is our elevator room. It looks a bit empty, feel free to add a prettier description or other
things to decorate it a bit.
But what we want now is to be able to say "1", "2" or "3" and have the elevator move in that
direction.
If you have read [the previous tutorial about adding dialogues in events](./Dialogues-in-events), you
may remember what we need to do. If not, here's a summary: we need to run some code when somebody
speaks in the room. So we need to create a callback (the callback will contain our lines of code).
We just need to know on which event this should be set. You can enter `call here` to see the
possible events in this room.
In the table, you should see the "say" event, which is called when somebody says something in the
room. So we'll need to add a callback to this event. Don't worry if you're a bit lost, just follow
the following steps, the way they connect together will become more obvious.
call/add here = say 1, 2, 3
1. We need to add a callback. A callback contains the code that will be executed at a given time.
So we use the `call/add` command and switch.
2. `here` is our object, the room in which we are.
3. An equal sign.
4. The name of the event to which the callback should be connected. Here, the event is "say".
Meaning this callback will be executed every time somebody says something in the room.
5. But we add an event parameter to indicate the keywords said in the room that should execute our
callback. Otherwise, our callback would be called every time somebody speaks, no matter what. Here
we limit, indicating our callback should be executed only if the spoken message contains "1", "2" or
"3".
An editor should open, inviting you to enter the Python code that should be executed. The first
thing to remember is to read the text provided (it can contain important information) and, most of
all, the list of variables that are available in this callback:
```
Variables you can use in this event:
character: the character having spoken in this room.
room: the room connected to this event.
message: the text having been spoken by the character.
----------Line Editor [Callback say of Inside of an elevator]---------------------
01|
----------[l:01 w:000 c:0000]------------(:h for help)----------------------------
```
This is important, in order to know what variables we can use in our callback out-of-the-box. Let's
write a single line to be sure our callback is called when we expect it to:
```python
character.msg("You just said {}.".format(message))
```
You can paste this line in-game, then type the `:wq` command to exit the editor and save your
modifications.
Let's check. Try to say "hello" in the room. You should see the standard message, but nothing
more. Now try to say "1". Below the standard message, you should see:
You just said 1.
You can try it. Our callback is only called when we say "1", "2" or "3". Which is just what we
want.
Let's go back in our code editor and add something more useful.
call/edit here = say
> Notice that we used the "edit" switch this time, since the callback exists, we just want to edit
it.
The editor opens again. Let's empty it first:
:DD
And turn off automatic indentation, which will help us:
:=
> Auto-indentation is an interesting feature of the code editor, but we'd better not use it at this
point, it will make copy/pasting more complicated.
## Our entire callback in the elevator
So here's the time to truly code our callback in-game. Here's a little reminder:
1. We have all the IDs of our three rooms and two exits.
2. When we say "1", "2" or "3", the elevator should move to the right room, that is change the
exits. Remember, we already have the exits, we just need to change their location and destination.
It's a good idea to try to write this callback yourself, but don't feel bad about checking the
solution right now. Here's a possible code that you could paste in the code editor:
```python
# First let's have some constants
ELEVATOR = get(id=3)
FLOORS = {
"1": get(id=2),
"2": get(id=6),
"3": get(id=7),
}
TO_EXIT = get(id=4)
BACK_EXIT = get(id=5)
# Now we check that the elevator isn't already at this floor
floor = FLOORS.get(message)
if floor is None:
character.msg("Which floor do you want?")
elif TO_EXIT.location is floor:
character.msg("The elevator already is at this floor.")
else:
# 'floor' contains the new room where the elevator should be
room.msg_contents("The doors of the elevator close with a clank.")
TO_EXIT.location = floor
BACK_EXIT.destination = floor
room.msg_contents("The doors of the elevator open to {floor}.",
mapping=dict(floor=floor))
```
Let's review this longer callback:
1. We first obtain the objects of both exits and our three floors. We use the `get()` eventfunc,
which is a shortcut to obtaining objects. We usually use it to retrieve specific objects with an
ID. We put the floors in a dictionary. The keys of the dictionary are the floor number (as str),
the values are room objects.
2. Remember, the `message` variable contains the message spoken in the room. So either "1", "2", or
"3". We still need to check it, however, because if the character says something like "1 2" in the
room, our callback will be executed. Let's be sure what she says is a floor number.
3. We then check if the elevator is already at this floor. Notice that we use `TO_EXIT.location`.
`TO_EXIT` contains our "north" exit, leading inside of our elevator. Therefore, its `location` will
be the room where the elevator currently is.
4. If the floor is a different one, have the elevator "move", changing just the location and
destination of both exits.
- The `BACK_EXIT` (that is "north") should change its location. The elevator shouldn't be
accessible through our old floor.
- The `TO_EXIT` (that is "south", the exit leading out of the elevator) should have a different
destination. When we go out of the elevator, we should find ourselves in the new floor, not the old
one.
Feel free to expand on this example, changing messages, making further checks. Usage and practice
are keys.
You can quit the editor as usual with `:wq` and test it out.
## Adding a pause in our callback
Let's improve our callback. One thing that's worth adding would be a pause: for the time being,
when we say the floor number in the elevator, the doors close and open right away. It would be
better to have a pause of several seconds. More logical.
This is a great opportunity to learn about chained events. Chained events are very useful to create
pauses. Contrary to the events we have seen so far, chained events aren't called automatically.
They must be called by you, and can be called after some time.
- Chained events always have the name "chain_X". Usually, X is a number, but you can give the
chained event a more explicit name.
- In our original callback, we will call our chained events in, say, 15 seconds.
- We'll also have to make sure the elevator isn't already moving.
Other than that, a chained event can be connected to a callback as usual. We'll create a chained
event in our elevator, that will only contain the code necessary to open the doors to the new floor.
call/add here = chain_1
The callback is added to the "chain_1" event, an event that will not be automatically called by the
system when something happens. Inside this event, you can paste the code to open the doors at the
new floor. You can notice a few differences:
```python
TO_EXIT.location = floor
TO_EXIT.destination = ELEVATOR
BACK_EXIT.location = ELEVATOR
BACK_EXIT.destination = floor
room.msg_contents("The doors of the elevator open to {floor}.",
mapping=dict(floor=floor))
```
Paste this code into the editor, then use `:wq` to save and quit the editor.
Now let's edit our callback in the "say" event. We'll have to change it a bit:
- The callback will have to check the elevator isn't already moving.
- It must change the exits when the elevator move.
- It has to call the "chain_1" event we have defined. It should call it 15 seconds later.
Let's see the code in our callback.
call/edit here = say
Remove the current code and disable auto-indentation again:
:DD
:=
And you can paste instead the following code. Notice the differences with our first attempt:
```python
# First let's have some constants
ELEVATOR = get(id=3)
FLOORS = {
"1": get(id=2),
"2": get(id=6),
"3": get(id=7),
}
TO_EXIT = get(id=4)
BACK_EXIT = get(id=5)
# Now we check that the elevator isn't already at this floor
floor = FLOORS.get(message)
if floor is None:
character.msg("Which floor do you want?")
elif BACK_EXIT.location is None:
character.msg("The elevator is between floors.")
elif TO_EXIT.location is floor:
character.msg("The elevator already is at this floor.")
else:
# 'floor' contains the new room where the elevator should be
room.msg_contents("The doors of the elevator close with a clank.")
TO_EXIT.location = None
BACK_EXIT.location = None
call_event(room, "chain_1", 15)
```
What changed?
1. We added a little test to make sure the elevator wasn't already moving. If it is, the
`BACK_EXIT.location` (the "south" exit leading out of the elevator) should be `None`. We'll remove
the exit while the elevator is moving.
2. When the doors close, we set both exits' `location` to `None`. Which "removes" them from their
room but doesn't destroy them. The exits still exist but they don't connect anything. If you say
"2" in the elevator and look around while the elevator is moving, you won't see any exits.
3. Instead of opening the doors immediately, we call `call_event`. We give it the object containing
the event to be called (here, our elevator), the name of the event to be called (here, "chain_1")
and the number of seconds from now when the event should be called (here, `15`).
4. The `chain_1` callback we have created contains the code to "re-open" the elevator doors. That
is, besides displaying a message, it reset the exits' `location` and `destination`.
If you try to say "3" in the elevator, you should see the doors closing. Look around you and you
won't see any exit. Then, 15 seconds later, the doors should open, and you can leave the elevator
to go to the third floor. While the elevator is moving, the exit leading to it will be
inaccessible.
> Note: we don't define the variables again in our chained event, we just call them. When we
execute `call_event`, a copy of our current variables is placed in the database. These variables
will be restored and accessible again when the chained event is called.
You can use the `call/tasks` command to see the tasks waiting to be executed. For instance, say "2"
in the room, notice the doors closing, and then type the `call/tasks` command. You will see a task
in the elevator, waiting to call the `chain_1` event.
## Changing exit messages
Here's another nice little feature of events: you can modify the message of a single exit without
altering the others. In this case, when someone goes north into our elevator, we'd like to see
something like: "someone walks into the elevator." Something similar for the back exit would be
great too.
Inside of the elevator, you can look at the available events on the exit leading outside (south).
call south
You should see two interesting rows in this table:
```
| msg_arrive | 0 (0) | Customize the message when a character |
| | | arrives through this exit. |
| msg_leave | 0 (0) | Customize the message when a character leaves |
| | | through this exit. |
```
So we can change the message others see when a character leaves, by editing the "msg_leave" event.
Let's do that:
call/add south = msg_leave
Take the time to read the help. It gives you all the information you should need. We'll need to
change the "message" variable, and use custom mapping (between braces) to alter the message. We're
given an example, let's use it. In the code editor, you can paste the following line:
```python
message = "{character} walks out of the elevator."
```
Again, save and quit the editor by entering `:wq`. You can create a new character to see it leave.
charcreate A beggar
tel #8 = here
(Obviously, adapt the ID if necessary.)
py self.search("beggar").move_to(self.search("south"))
This is a crude way to force our beggar out of the elevator, but it allows us to test. You should
see:
A beggar(#8) walks out of the elevator.
Great! Let's do the same thing for the exit leading inside of the elevator. Follow the beggar,
then edit "msg_leave" of "north":
call/add north = msg_leave
```python
message = "{character} walks into the elevator."
```
Again, you can force our beggar to move and see the message we have just set. This modification
applies to these two exits, obviously: the custom message won't be used for other exits. Since we
use the same exits for every floor, this will be available no matter at what floor the elevator is,
which is pretty neat!
## Tutorial F.A.Q.
- **Q:** what happens if the game reloads or shuts down while a task is waiting to happen?
- **A:** if your game reloads while a task is in pause (like our elevator between floors), when the
game is accessible again, the task will be called (if necessary, with a new time difference to take
into account the reload). If the server shuts down, obviously, the task will not be called, but
will be stored and executed when the server is up again.
- **Q:** can I use all kinds of variables in my callback? Whether chained or not?
- **A:** you can use every variable type you like in your original callback. However, if you
execute `call_event`, since your variables are stored in the database, they will need to respect the
constraints on persistent attributes. A callback will not be stored in this way, for instance.
This variable will not be available in your chained event.
- **Q:** when you say I can call my chained events something else than "chain_1", "chain_2" and
such, what is the naming convention?
- **A:** chained events have names beginning by "chain_". This is useful for you and for the
system. But after the underscore, you can give a more useful name, like "chain_open_doors" in our
case.
- **Q:** do I have to pause several seconds to call a chained event?
- **A:** no, you can call it right away. Just leave the third parameter of `call_event` out (it
will default to 0, meaning the chained event will be called right away). This will not create a
task.
- **Q:** can I have chained events calling themselves?
- **A:** you can. There's no limitation. Just be careful, a callback that calls itself,
particularly without delay, might be a good recipe for an infinite loop. However, in some cases, it
is useful to have chained events calling themselves, to do the same repeated action every X seconds
for instance.
- **Q:** what if I need several elevators, do I need to copy/paste these callbacks each time?
- **A:** not advisable. There are definitely better ways to handle this situation. One of them is
to consider adding the code in the source itself. Another possibility is to call chained events
with the expected behavior, which makes porting code very easy. This side of chained events will be
shown in the next tutorial.
- Previous tutorial: [Adding dialogues in events](./Dialogues-in-events)

View file

@ -0,0 +1,46 @@
# API refactoring
Building up to Evennia 1.0 and beyond, it's time to comb through the Evennia API for old cruft. This
whitepage is for anyone interested to contribute with their views on what part of the API needs
refactoring, cleanup or clarification (or extension!)
Note that this is not a forum. To keep things clean, each opinion text should ideally present a
clear argument or lay out a suggestion. Asking for clarification and any side-discussions should be
held in chat or forum.
---
### Griatch (Aug 13, 2019)
This is how to enter an opinion. Use any markdown needed but stay within your section. Also remember
to copy your text to the clipboard before saving since if someone else edited the wiki in the
meantime you'll have to start over.
### Griatch (Sept 2, 2019)
I don't agree with removing explicit keywords as suggested by [Johnny on Aug 29 below](API-
refactoring#reduce-usage-of-optionalpositional-arguments-aug-29-2019). Overriding such a method can
still be done by `get(self, **kwargs)` if so desired, making the kwargs explicit helps IMO
readability of the API. If just giving a generic `**kwargs`, one must read the docstring or even the
code to see which keywords are valid.
On the other hand, I think it makes sense to as a standard offer an extra `**kwargs` at the end of
arg-lists for common methods that are expected to be over-ridden. This make the API more flexible by
hinting to the dev that they could expand their own over-ridden implementation with their own
keyword arguments if so desired.
---
### Johnny
#### Reduce usage of optional/positional arguments (Aug 29, 2019)
```
# AttributeHandler
def get(self, key=None, default=None, category=None, return_obj=False,
strattr=False, raise_exception=False, accessing_obj=None,
default_access=True, return_list=False):
```
Many classes have methods requiring lengthy positional argument lists, which are tedious and error-
prone to extend and override especially in cases where not all arguments are even required. It would
be useful if arguments were reserved for required inputs and anything else relegated to kwargs for
easier passthrough on extension.

108
docs/source/Accounts.md Normal file
View file

@ -0,0 +1,108 @@
# Accounts
All *users* (real people) that starts a game [Session](./Sessions) on Evennia are doing so through an
object called *Account*. The Account object has no in-game representation, it represents a unique
game account. In order to actually get on the game the Account must *puppet* an [Object](./Objects)
(normally a [Character](./Objects#Character)).
Exactly how many Sessions can interact with an Account and its Puppets at once is determined by
Evennia's [MULTISESSION_MODE](./Sessions#Multisession-mode) setting.
Apart from storing login information and other account-specific data, the Account object is what is
chatting on [Channels](./Communications). It is also a good place to store [Permissions](./Locks) to be
consistent between different in-game characters as well as configuration options. The Account
object also has its own [CmdSet](./Command-Sets), the `AccountCmdSet`.
Logged into default evennia, you can use the `ooc` command to leave your current
[character](./Objects) and go into OOC mode. You are quite limited in this mode, basically it works
like a simple chat program. It acts as a staging area for switching between Characters (if your
game supports that) or as a safety mode if your Character gets deleted. Use `ic` to attempt to
(re)puppet a Character.
Note that the Account object can have, and often does have, a different set of
[Permissions](./Locks#Permissions) from the Character they control. Normally you should put your
permissions on the Account level - this will overrule permissions set on the Character level. For
the permissions of the Character to come into play the default `quell` command can be used. This
allows for exploring the game using a different permission set (but you can't escalate your
permissions this way - for hierarchical permissions like `Builder`, `Admin` etc, the *lower* of the
permissions on the Character/Account will always be used).
## How to create your own Account types
You will usually not want more than one Account typeclass for all new accounts (but you could in
principle create a system that changes an account's typeclass dynamically).
An Evennia Account is, per definition, a Python class that includes `evennia.DefaultAccount` among
its parents. In `mygame/typeclasses/accounts.py` there is an empty class ready for you to modify.
Evennia defaults to using this (it inherits directly from `DefaultAccount`).
Here's an example of modifying the default Account class in code:
```python
# in mygame/typeclasses/accounts.py
from evennia import DefaultAccount
class Account(DefaultAccount): # [...]
at_account_creation(self): "this is called only once, when account is first created"
self.db.real_name = None # this is set later self.db.real_address = None #
"
self.db.config_1 = True # default config self.db.config_2 = False # "
self.db.config_3 = 1 # "
# ... whatever else our game needs to know ``` Reload the server with `reload`.
```
... However, if you use `examine *self` (the asterisk makes you examine your Account object rather
than your Character), you won't see your new Attributes yet. This is because `at_account_creation`
is only called the very *first* time the Account is called and your Account object already exists
(any new Accounts that connect will see them though). To update yourself you need to make sure to
re-fire the hook on all the Accounts you have already created. Here is an example of how to do this
using `py`:
``` py [account.at_account_creation() for account in evennia.managers.accounts.all()] ```
You should now see the Attributes on yourself.
> If you wanted Evennia to default to a completely *different* Account class located elsewhere, you
> must point Evennia to it. Add `BASE_ACCOUNT_TYPECLASS` to your settings file, and give the python
> path to your custom class as its value. By default this points to `typeclasses.accounts.Account`,
> the empty template we used above.
## Properties on Accounts
Beyond those properties assigned to all typeclassed objects (see [Typeclasses](./Typeclasses)), the
Account also has the following custom properties:
- `user` - a unique link to a `User` Django object, representing the logged-in user.
- `obj` - an alias for `character`.
- `name` - an alias for `user.username`
- `sessions` - an instance of
[ObjectSessionHandler](github:evennia.objects.objects#objectsessionhandler)
managing all connected Sessions (physical connections) this object listens to (Note: In older
versions of Evennia, this was a list). The so-called `session-id` (used in many places) is found
as
a property `sessid` on each Session instance.
- `is_superuser` (bool: True/False) - if this account is a superuser.
Special handlers:
- `cmdset` - This holds all the current [Commands](./Commands) of this Account. By default these are
the commands found in the cmdset defined by `settings.CMDSET_ACCOUNT`.
- `nicks` - This stores and handles [Nicks](./Nicks), in the same way as nicks it works on Objects.
For Accounts, nicks are primarily used to store custom aliases for
[Channels](./Communications#Channels).
Selection of special methods (see `evennia.DefaultAccount` for details):
- `get_puppet` - get a currently puppeted object connected to the Account and a given session id, if
any.
- `puppet_object` - connect a session to a puppetable Object.
- `unpuppet_object` - disconnect a session from a puppetable Object.
- `msg` - send text to the Account
- `execute_cmd` - runs a command as if this Account did it.
- `search` - search for Accounts.

View file

@ -0,0 +1,101 @@
# Add a simple new web page
Evennia leverages [Django](https://docs.djangoproject.com) which is a web development framework.
Huge professional websites are made in Django and there is extensive documentation (and books) on it
. You are encouraged to at least look at the Django basic tutorials. Here we will just give a brief
introduction for how things hang together, to get you started.
We assume you have installed and set up Evennia to run. A webserver and website comes out of the
box. You can get to that by entering `http://localhost:4001` in your web browser - you should see a
welcome page with some game statistics and a link to the web client. Let us add a new page that you
can get to by going to `http://localhost:4001/story`.
### Create the view
A django "view" is a normal Python function that django calls to render the HTML page you will see
in the web browser. Here we will just have it spit back the raw html, but Django can do all sorts of
cool stuff with the page in the view, like adding dynamic content or change it on the fly. Open
`mygame/web` folder and add a new module there named `story.py` (you could also put it in its own
folder if you wanted to be neat. Don't forget to add an empty `__init__.py` file if you do, to tell
Python you can import from the new folder). Here's how it looks:
```python
# in mygame/web/story.py
from django.shortcuts import render
def storypage(request):
return render(request, "story.html")
```
This view takes advantage of a shortcut provided to use by Django, _render_. This shortcut gives the
template some information from the request, for instance, the game name, and then renders it.
### The HTML page
We need to find a place where Evennia (and Django) looks for html files (called *templates* in
Django parlance). You can specify such places in your settings (see the `TEMPLATES` variable in
`default_settings.py` for more info), but here we'll use an existing one. Go to
`mygame/template/overrides/website/` and create a page `story.html` there.
This is not a HTML tutorial, so we'll go simple:
```html
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col">
<h1>A story about a tree</h1>
<p>
This is a story about a tree, a classic tale ...
</p>
</div>
</div>
{% endblock %}
```
Since we've used the _render_ shortcut, Django will allow us to extend our base styles easily.
If you'd rather not take advantage of Evennia's base styles, you can do something like this instead:
```html
<html>
<body>
<h1>A story about a tree</h1>
<p>
This is a story about a tree, a classic tale ...
</body>
</html>
```
### The URL
When you enter the address `http://localhost:4001/story` in your web browser, Django will parse that
field to figure out which page you want to go to. You tell it which patterns are relevant in the
file
[mygame/web/urls.py](https://github.com/evennia/evennia/blob/master/evennia/game_template/web/urls.p
y).
Open it now.
Django looks for the variable `urlpatterns` in this file. You want to add your new pattern to the
`custom_patterns` list we have prepared - that is then merged with the default `urlpatterns`. Here's
how it could look:
```python
from web import story
# ...
custom_patterns = [
url(r'story', story.storypage, name='Story'),
]
```
That is, we import our story view module from where we created it earlier and then create an `url`
instance. The first argument to `url` is the pattern of the url we want to find (`"story"`) (this is
a regular expression if you are familiar with those) and then our view function we want to direct
to.
That should be it. Reload Evennia and you should be able to browse to your new story page!

View file

@ -0,0 +1,232 @@
# Add a wiki on your website
**Before doing this tutorial you will probably want to read the intro in
[Basic Web tutorial](./Web-Tutorial).** Reading the three first parts of the
[Django tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/) might help as well.
This tutorial will provide a step-by-step process to installing a wiki on your website.
Fortunately, you don't have to create the features manually, since it has been done by others, and
we can integrate their work quite easily with Django. I have decided to focus on
the [Django-wiki](http://django-wiki.readthedocs.io/).
> Note: this article has been updated for Evennia 0.9. If you're not yet using this version, be
careful, as the django wiki doesn't support Python 2 anymore. (Remove this note when enough time
has passed.)
The [Django-wiki](http://django-wiki.readthedocs.io/) offers a lot of features associated with
wikis, is
actively maintained (at this time, anyway), and isn't too difficult to install in Evennia. You can
see a [demonstration of Django-wiki here](https://demo.django.wiki).
## Basic installation
You should begin by shutting down the Evennia server if it is running. We will run migrations and
alter the virtual environment just a bit. Open a terminal and activate your Python environment, the
one you use to run the `evennia` command.
* On Linux:
```
source evenv/bin/activate
```
* Or Windows:
```
evenv\bin\activate
```
### Installing with pip
Install the wiki using pip:
pip install wiki
> Note: this will install the last version of Django wiki. Version >0.4 doesn't support Python 2, so
install wiki 0.3 if you haven't updated to Python 3 yet.
It might take some time, the Django-wiki having some dependencies.
### Adding the wiki in the settings
You will need to add a few settings to have the wiki app on your website. Open your
`server/conf/settings.py` file and add the following at the bottom (but before importing
`secret_settings`). Here's what you'll find in my own setting file (add the whole Django-wiki
section):
```python
r"""
Evennia settings file.
...
"""
# 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 = "demowiki"
######################################################################
# Django-wiki settings
######################################################################
INSTALLED_APPS += (
'django.contrib.humanize.apps.HumanizeConfig',
'django_nyt.apps.DjangoNytConfig',
'mptt',
'sorl.thumbnail',
'wiki.apps.WikiConfig',
'wiki.plugins.attachments.apps.AttachmentsConfig',
'wiki.plugins.notifications.apps.NotificationsConfig',
'wiki.plugins.images.apps.ImagesConfig',
'wiki.plugins.macros.apps.MacrosConfig',
)
# Disable wiki handling of login/signup
WIKI_ACCOUNT_HANDLING = False
WIKI_ACCOUNT_SIGNUP_ALLOWED = False
######################################################################
# 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.")
```
### Adding the new URLs
Next we need to add two URLs in our `web/urls.py` file. Open it and compare the following output:
you will need to add two URLs in `custom_patterns` and add one import line:
```python
from django.conf.urls import url, include
from django.urls import path # NEW!
# default evenni a patterns
from evennia.web.urls import urlpatterns
# eventual custom patterns
custom_patterns = [
# url(r'/desired/url/', view, name='example'),
url('notifications/', include('django_nyt.urls')), # NEW!
url('wiki/', include('wiki.urls')), # NEW!
]
# this is required by Django.
urlpatterns = custom_patterns + urlpatterns
```
You will probably need to copy line 2, 10, and 11. Be sure to place them correctly, as shown in
the example above.
### Running migrations
It's time to run the new migrations. The wiki app adds a few tables in our database. We'll need to
run:
evennia migrate
And that's it, you can start the server. If you go to http://localhost:4001/wiki , you should see
the wiki. Use your account's username and password to connect to it. That's how simple it is.
## Customizing privileges
A wiki can be a great collaborative tool, but who can see it? Who can modify it? Django-wiki comes
with a privilege system centered around four values per wiki page. The owner of an article can
always read and write in it (which is somewhat logical). The group of the article defines who can
read and who can write, if the user seeing the page belongs to this group. The topic of groups in
wiki pages will not be discussed here. A last setting determines which other user (that is, these
who aren't in the groups, and aren't the article's owner) can read and write. Each article has
these four settings (group read, group write, other read, other write). Depending on your purpose,
it might not be a good default choice, particularly if you have to remind every builder to keep the
pages private. Fortunately, Django-wiki gives us additional settings to customize who can read, and
who can write, a specific article.
These settings must be placed, as usual, in your `server/conf/settings.py` file. They take a
function as argument, said function (or callback) will be called with the article and the user.
Remember, a Django user, for us, is an account. So we could check lockstrings on them if needed.
Here is a default setting to restrict the wiki: only builders can write in it, but anyone (including
non-logged in users) can read it. The superuser has some additional privileges.
```python
# In server/conf/settings.py
# ...
def is_superuser(article, user):
"""Return True if user is a superuser, False otherwise."""
return not user.is_anonymous() and user.is_superuser
def is_builder(article, user):
"""Return True if user is a builder, False otherwise."""
return not user.is_anonymous() and user.locks.check_lockstring(user, "perm(Builders)")
def is_anyone(article, user):
"""Return True even if the user is anonymous."""
return True
# Who can create new groups and users from the wiki?
WIKI_CAN_ADMIN = is_superuser
# Who can change owner and group membership?
WIKI_CAN_ASSIGN = is_superuser
# Who can change group membership?
WIKI_CAN_ASSIGN_OWNER = is_superuser
# Who can change read/write access to groups or others?
WIKI_CAN_CHANGE_PERMISSIONS = is_superuser
# Who can soft-delete an article?
WIKI_CAN_DELETE = is_builder
# Who can lock an article and permanently delete it?
WIKI_CAN_MODERATE = is_superuser
# Who can edit articles?
WIKI_CAN_WRITE = is_builder
# Who can read articles?
WIKI_CAN_READ = is_anyone
```
Here, we have created three functions: one to return `True` if the user is the superuser, one to
return `True` if the user is a builder, one to return `True` no matter what (this includes if the
user is anonymous, E.G. if it's not logged-in). We then change settings to allow either the
superuser or
each builder to moderate, read, write, delete, and more. You can, of course, add more functions,
adapting them to your need. This is just a demonstration.
Providing the `WIKI_CAN*...` settings will bypass the original permission system. The superuser
could change permissions of an article, but still, only builders would be able to write it. If you
need something more custom, you will have to expand on the functions you use.
### Managing wiki pages from Evennia
Unfortunately, Django wiki doesn't provide a clear and clean entry point to read and write articles
from Evennia and it doesn't seem to be a very high priority. If you really need to keep Django wiki
and to create and manage wiki pages from your code, you can do so, but this article won't elaborate,
as this is somewhat more technical.
However, it is a good opportunity to present a small project that has been created more recently:
[evennia-wiki](https://github.com/vincent-lg/evennia-wiki) has been created to provide a simple
wiki, more tailored to Evennia and easier to connect. It doesn't, as yet, provide as many options
as does Django wiki, but it's perfectly usable:
- Pages have an inherent and much-easier to understand hierarchy based on URLs.
- Article permissions are connected to Evennia groups and are much easier to accommodate specific
requirements.
- Articles can easily be created, read or updated from the Evennia code itself.
- Markdown is fully-supported with a default integration to Bootstrap to look good on an Evennia
website. Tables and table of contents are supported as well as wiki links.
- The process to override wiki templates makes full use of the `template_overrides` directory.
However evennia-wiki doesn't yet support:
- Images in markdown and the uploading schema. If images are important to you, please consider
contributing to this new project.
- Modifying permissions on a per page/setting basis.
- Moving pages to new locations.
- Viewing page history.
Considering the list of features in Django wiki, obviously other things could be added to the list.
However, these features may be the most important and useful. Additional ones might not be that
necessary. If you're interested in supporting this little project, you are more than welcome to
[contribute to it](https://github.com/vincent-lg/evennia-wiki). Thanks!

View file

@ -0,0 +1,171 @@
# Adding Command Tutorial
This is a quick first-time tutorial expanding on the [Commands](./Commands) documentation.
Let's assume you have just downloaded Evennia, installed it and created your game folder (let's call
it just `mygame` here). Now you want to try to add a new command. This is the fastest way to do it.
## Step 1: Creating a custom command
1. Open `mygame/commands/command.py` in a text editor. This is just one place commands could be
placed but you get it setup from the onset as an easy place to start. It also already contains some
example code.
1. Create a new class in `command.py` inheriting from `default_cmds.MuxCommand`. Let's call it
`CmdEcho` in this example.
1. Set the class variable `key` to a good command name, like `echo`.
1. Give your class a useful _docstring_. A docstring is the string at the very top of a class or
function/method. The docstring at the top of the command class is read by Evennia to become the help
entry for the Command (see
[Command Auto-help](./Help-System#command-auto-help-system)).
1. Define a class method `func(self)` that echoes your input back to you.
Below is an example how this all could look for the echo command:
```python
# file mygame/commands/command.py
#[...]
from evennia import default_cmds
class CmdEcho(default_cmds.MuxCommand):
"""
Simple command example
Usage:
echo [text]
This command simply echoes text back to the caller.
"""
key = "echo"
def func(self):
"This actually does things"
if not self.args:
self.caller.msg("You didn't enter anything!")
else:
self.caller.msg("You gave the string: '%s'" % self.args)
```
## Step 2: Adding the Command to a default Cmdset
The command is not available to use until it is part of a [Command Set](./Command-Sets). In this
example we will go the easiest route and add it to the default Character commandset that already
exists.
1. Edit `mygame/commands/default_cmdsets.py`
1. Import your new command with `from commands.command import CmdEcho`.
1. Add a line `self.add(CmdEcho())` to `CharacterCmdSet`, in the `at_cmdset_creation` method (the
template tells you where).
This is approximately how it should look at this point:
```python
# file mygame/commands/default_cmdsets.py
#[...]
from commands.command import CmdEcho
#[...]
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
# this first adds all default commands
super().at_cmdset_creation()
# all commands added after this point will extend or
# overwrite the default commands.
self.add(CmdEcho())
```
Next, run the `@reload` command. You should now be able to use your new `echo` command from inside
the game. Use `help echo` to see the documentation for the command.
If you have trouble, make sure to check the log for error messages (probably due to syntax errors in
your command definition).
> Note: Typing `echotest` will also work. It will be handled as the command `echo` directly followed
by
its argument `test` (which will end up in `self.args). To change this behavior, you can add the
`arg_regex` property alongside `key`, `help_category` etc. [See the arg_regex
documentation](Commands#on-arg_regex) for more info.
If you want to overload existing default commands (such as `look` or `get`), just add your new
command with the same key as the old one - it will then replace it. Just remember that you must use
`@reload` to see any changes.
See [Commands](./Commands) for many more details and possibilities when defining Commands and using
Cmdsets in various ways.
## Adding the command to specific object types
Adding your Command to the `CharacterCmdSet` is just one easy exapmple. The cmdset system is very
generic. You can create your own cmdsets (let's say in a module `mycmdsets.py`) and add them to
objects as you please (how to control their merging is described in detail in the [Command Set
documentation](Command-Sets)).
```python
# file mygame/commands/mycmdsets.py
#[...]
from commands.command import CmdEcho
from evennia import CmdSet
#[...]
class MyCmdSet(CmdSet):
key = "MyCmdSet"
def at_cmdset_creation(self):
self.add(CmdEcho())
```
Now you just need to add this to an object. To test things (as superuser) you can do
@py self.cmdset.add("mycmdsets.MyCmdSet")
This will add this cmdset (along with its echo command) to yourself so you can test it. Note that
you cannot add a single Command to an object on its own, it must be part of a CommandSet in order to
do so.
The Command you added is not there permanently at this point. If you do a `@reload` the merger will
be gone. You *could* add the `permanent=True` keyword to the `cmdset.add` call. This will however
only make the new merged cmdset permanent on that *single* object. Often you want *all* objects of
this particular class to have this cmdset.
To make sure all new created objects get your new merged set, put the `cmdset.add` call in your
custom [Typeclasses](./Typeclasses)' `at_object_creation` method:
```python
# e.g. in mygame/typeclasses/objects.py
from evennia import DefaultObject
class MyObject(DefaultObject):
def at_object_creation(self):
"called when the object is first created"
self.cmdset.add("mycmdset.MyCmdSet", permanent=True)
```
All new objects of this typeclass will now start with this cmdset and it will survive a `@reload`.
*Note:* An important caveat with this is that `at_object_creation` is only called *once*, when the
object is first created. This means that if you already have existing objects in your databases
using that typeclass, they will not have been initiated the same way. There are many ways to update
them; since it's a one-time update you can usually just simply loop through them. As superuser, try
the following:
@py from typeclasses.objects import MyObject; [o.cmdset.add("mycmdset.MyCmdSet") for o in
MyObject.objects.all()]
This goes through all objects in your database having the right typeclass, adding the new cmdset to
each. The good news is that you only have to do this if you want to post-add *cmdsets*. If you just
want to add a new *command*, you can simply add that command to the cmdset's `at_cmdset_creation`
and `@reload` to make the Command immediately available.
## Change where Evennia looks for command sets
Evennia uses settings variables to know where to look for its default command sets. These are
normally not changed unless you want to re-organize your game folder in some way. For example, the
default character cmdset defaults to being defined as
CMDSET_CHARACTER="commands.default_cmdset.CharacterCmdSet"
See `evennia/settings_default.py` for the other settings.

View file

@ -0,0 +1,109 @@
# Adding Object Typeclass Tutorial
Evennia comes with a few very basic classes of in-game entities:
DefaultObject
|
DefaultCharacter
DefaultRoom
DefaultExit
DefaultChannel
When you create a new Evennia game (with for example `evennia --init mygame`) Evennia will
automatically create empty child classes `Object`, `Character`, `Room` and `Exit` respectively. They
are found `mygame/typeclasses/objects.py`, `mygame/typeclasses/rooms.py` etc.
> Technically these are all [Typeclassed](./Typeclasses), which can be ignored for now. In
> `mygame/typeclasses` are also base typeclasses for out-of-character things, notably
> [Channels](./Communications), [Accounts](./Accounts) and [Scripts](./Scripts). We don't cover those in
> this tutorial.
For your own game you will most likely want to expand on these very simple beginnings. It's normal
to want your Characters to have various attributes, for example. Maybe Rooms should hold extra
information or even *all* Objects in your game should have properties not included in basic Evennia.
## Change Default Rooms, Exits, Character Typeclass
This is the simplest case.
The default build commands of a new Evennia game is set up to use the `Room`, `Exit` and `Character`
classes found in the same-named modules under `mygame/typeclasses/`. By default these are empty and
just implements the default parents from the Evennia library (`DefaultRoom`etc). Just add the
changes you want to these classes and run `@reload` to add your new functionality.
## Create a new type of object
Say you want to create a new "Heavy" object-type that characters should not have the ability to pick
up.
1. Edit `mygame/typeclasses/objects.py` (you could also create a new module there, named something
like `heavy.py`, that's up to how you want to organize things).
1. Create a new class inheriting at any distance from `DefaultObject`. It could look something like
this:
```python
# end of file mygame/typeclasses/objects.py
from evennia import DefaultObject
class Heavy(DefaultObject):
"Heavy object"
def at_object_creation(self):
"Called whenever a new object is created"
# lock the object down by default
self.locks.add("get:false()")
# the default "get" command looks for this Attribute in order
# to return a customized error message (we just happen to know
# this, you'd have to look at the code of the 'get' command to
# find out).
self.db.get_err_msg = "This is too heavy to pick up."
```
1. Once you are done, log into the game with a build-capable account and do `@create/drop
rock:objects.Heavy` to drop a new heavy "rock" object in your location. Next try to pick it up
(`@quell` yourself first if you are a superuser). If you get errors, look at your log files where
you will find the traceback. The most common error is that you have some sort of syntax error in
your class.
Note that the [Locks](./Locks) and [Attribute](./Attributes) which are set in the typeclass could just
as well have been set using commands in-game, so this is a *very* simple example.
## Storing data on initialization
The `at_object_creation` is only called once, when the object is first created. This makes it ideal
for database-bound things like [Attributes](./Attributes). But sometimes you want to create temporary
properties (things that are not to be stored in the database but still always exist every time the
object is created). Such properties can be initialized in the `at_init` method on the object.
`at_init` is called every time the object is loaded into memory.
> Note: It's usually pointless and wasteful to assign database data in `at_init`, since this will
> hit the database with the same value over and over. Put those in `at_object_creation` instead.
You are wise to use `ndb` (non-database Attributes) to store these non-persistent properties, since
ndb-properties are protected against being cached out in various ways and also allows you to list
them using various in-game tools:
```python
def at_init(self):
self.ndb.counter = 0
self.ndb.mylist = []
```
> Note: As mentioned in the [Typeclasses](./Typeclasses) documentation, `at_init` replaces the use of
> the standard `__init__` method of typeclasses due to how the latter may be called in situations
> other than you'd expect. So use `at_init` where you would normally use `__init__`.
## Updating existing objects
If you already have some `Heavy` objects created and you add a new `Attribute` in
`at_object_creation`, you will find that those existing objects will not have this Attribute. This
is not so strange, since `at_object_creation` is only called once, it will not be called again just
because you update it. You need to update existing objects manually.
If the number of objects is limited, you can use `@typeclass/force/reload objectname` to force a
re-load of the `at_object_creation` method (only) on the object. This case is common enough that
there is an alias `@update objectname` you can use to get the same effect. If there are multiple
objects you can use `@py` to loop over the objects you need:
```
@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()]
```

View file

@ -0,0 +1,76 @@
# Administrative Docs
The following pages are aimed at game administrators -- the higher-ups that possess shell access and
are responsible for managing the game.
### Installation and Early Life
- [Choosing (and installing) an SQL Server](./Choosing-An-SQL-Server)
- [Getting Started - Installing Evennia](./Getting-Started)
- [Running Evennia in Docker Containers](./Running-Evennia-in-Docker)
- [Starting, stopping, reloading and resetting Evennia](./Start-Stop-Reload)
- [Keeping your game up to date](./Updating-Your-Game)
- [Resetting your database](./Updating-Your-Game#resetting-your-database)
- [Making your game available online](./Online-Setup)
- [Hosting options](./Online-Setup#hosting-options)
- [Securing your server with SSL/Let's Encrypt](./Online-Setup#ssl)
- [Listing your game](./Evennia-Game-Index) at the online [Evennia game
index](http://games.evennia.com)
### Customizing the server
- [Changing the Settings](./Server-Conf#Settings-file)
- [Available Master
Settings](https://github.com/evennia/evennia/blob/master/evennia/settings_default.py)
- [Change Evennia's language](./Internationalization) (internationalization)
- [Apache webserver configuration](./Apache-Config) (optional)
- [Changing text encodings used by the server](./Text-Encodings)
- [The Connection Screen](./Connection-Screen)
- [Guest Logins](./Guest-Logins)
- [How to connect Evennia to IRC channels](./IRC)
- [How to connect Evennia to RSS feeds](./RSS)
- [How to connect Evennia to Grapevine](./Grapevine)
- [How to connect Evennia to Twitter](./How-to-connect-Evennia-to-Twitter)
### Administrating the running game
- [Supported clients](./Client-Support-Grid) (grid of known client issues)
- [Changing Permissions](./Building-Permissions) of users
- [Banning](./Banning) and deleting users
- [Summary of abuse-handling tools](./Banning#summary-of-abuse-handling-tools) in the default cmdset
### Working with Evennia
- [Setting up your work environment with version control](./Version-Control)
- [First steps coding with Evennia](./First-Steps-Coding)
- [Setting up a continuous integration build environment](./Continuous-Integration)
```toctree::
:hidden:
Choosing-An-SQL-Server
Getting-Started
Running-Evennia-in-Docker
Start-Stop-Reload
Updating-Your-Game
Online-Setup
Evennia-Game-Index
Server-Conf
Internationalization
Apache-Config
Text-Encodings
Connection-Screen
Guest-Logins
IRC
RSS
Grapevine
How-to-connect-Evennia-to-Twitter
Client-Support-Grid
Building-Permissions
Banning
Version-Control
First-Steps-Coding
Continuous-Integration
```

View file

@ -0,0 +1,171 @@
# Apache Config
**Warning**: This information is presented as a convenience, using another webserver than Evennia's
own is not directly supported and you are on your own if you want to do so. Evennia's webserver
works out of the box without any extra configuration and also runs in-process making sure to avoid
caching race conditions. The browser web client will most likely not work (at least not without
tweaking) on a third-party web server.
One reason for wanting to use an external webserver like Apache would be to act as a *proxy* in
front of the Evennia webserver. Getting this working with TLS (encryption) requires some extra work
covered at the end of this page.
Note that the Apache instructions below might be outdated. If something is not working right, or you
use Evennia with a different server, please let us know. Also, if there is a particular Linux distro
you would like covered, please let us know.
## `mod_wsgi` Setup
### Install `mod_wsgi`
- *Fedora/RHEL* - Apache HTTP Server and `mod_wsgi` are available in the standard package
repositories for Fedora and RHEL:
```
$ dnf install httpd mod_wsgi
or
$ yum install httpd mod_wsgi
```
- *Ubuntu/Debian* - Apache HTTP Server and `mod_wsgi` are available in the standard package
repositories for Ubuntu and Debian:
```
$ apt-get update
$ apt-get install apache2 libapache2-mod-wsgi
```
### Copy and modify the VHOST
After `mod_wsgi` is installed, copy the `evennia/web/utils/evennia_wsgi_apache.conf` file to your
apache2 vhosts/sites folder. On Debian/Ubuntu, this is `/etc/apache2/sites-enabled/`. Make your
modifications **after** copying the file there.
Read the comments and change the paths to point to the appropriate locations within your setup.
### Restart/Reload Apache
You'll then want to reload or restart apache2 after changing the configurations.
- *Fedora/RHEL/Ubuntu*
```
$ systemctl restart httpd
```
- *Ubuntu/Debian*
```
$ systemctl restart apache2
```
### Enjoy
With any luck, you'll be able to point your browser at your domain or subdomain that you set up in
your vhost and see the nifty default Evennia webpage. If not, read the hopefully informative error
message and work from there. Questions may be directed to our [Evennia Community
site](http://evennia.com).
### A note on code reloading
If your `mod_wsgi` is set up to run on daemon mode (as will be the case by default on Debian and
Ubuntu), you may tell `mod_wsgi` to reload by using the `touch` command on
`evennia/game/web/utils/apache_wsgi.conf`. When `mod_wsgi` sees that the file modification time has
changed, it will force a code reload. Any modifications to the code will not be propagated to the
live instance of your site until reloaded.
If you are not running in daemon mode or want to force the issue, simply restart or reload apache2
to apply your changes.
### Further notes and hints:
If you get strange (and usually uninformative) `Permission denied` errors from Apache, make sure
that your `evennia` directory is located in a place the webserver may actually access. For example,
some Linux distributions may default to very restrictive access permissions on a user's `/home`
directory.
One user commented that they had to add the following to their Apache config to get things to work.
Not confirmed, but worth trying if there are trouble.
<Directory "/home/<yourname>/evennia/game/web">
Options +ExecCGI
Allow from all
</Directory>
## `mod_proxy` and `mod_ssl` setup
Below are steps on running Evennia using a front-end proxy (Apache HTTP), `mod_proxy_http`,
`mod_proxy_wstunnel`, and `mod_ssl`. `mod_proxy_http` and `mod_proxy_wstunnel` will simply be
referred to as
`mod_proxy` below.
### Install `mod_ssl`
- *Fedora/RHEL* - Apache HTTP Server and `mod_ssl` are available in the standard package
repositories for Fedora and RHEL:
```
$ dnf install httpd mod_ssl
or
$ yum install httpd mod_ssl
```
- *Ubuntu/Debian* - Apache HTTP Server and `mod_sslj`kl are installed together in the `apache2`
package and available in the
standard package repositories for Ubuntu and Debian. `mod_ssl` needs to be enabled after
installation:
```
$ apt-get update
$ apt-get install apache2
$ a2enmod ssl
```
### TLS proxy+websocket configuration
Below is a sample configuration for Evennia with a TLS-enabled http and websocket proxy.
#### Apache HTTP Server Configuration
```
<VirtualHost *:80>
# Always redirect to https/443
ServerName mud.example.com
Redirect / https://mud.example.com
</VirtualHost>
<VirtualHost *:443>
ServerName mud.example.com
SSLEngine On
# Location of certificate and key
SSLCertificateFile /etc/pki/tls/certs/mud.example.com.crt
SSLCertificateKeyFile /etc/pki/tls/private/mud.example.com.key
# Use a tool https://www.ssllabs.com/ssltest/ to scan your set after setting up.
SSLProtocol TLSv1.2
SSLCipherSuite HIGH:!eNULL:!NULL:!aNULL
# Proxy all websocket traffic to port 4002 in Evennia
ProxyPass /ws ws://127.0.0.1:4002/
ProxyPassReverse /ws ws://127.0.0.1:4002/
# Proxy all HTTP traffic to port 4001 in Evennia
ProxyPass / http://127.0.0.1:4001/
ProxyPassReverse / http://127.0.0.1:4001/
# Configure separate logging for this Evennia proxy
ErrorLog logs/evennia_error.log
CustomLog logs/evennia_access.log combined
</VirtualHost>
```
#### Evennia secure websocket configuration
There is a slight trick in setting up Evennia so websocket traffic is handled correctly by the
proxy. You must set the `WEBSOCKET_CLIENT_URL` setting in your `mymud/server/conf/settings.py` file:
```
WEBSOCKET_CLIENT_URL = "wss://external.example.com/ws"
```
The setting above is what the client's browser will actually use. Note the use of `wss://` is
because our client will be communicating over an encrypted connection ("wss" indicates websocket
over SSL/TLS). Also, especially note the additional path `/ws` at the end of the URL. This is how
Apache HTTP Server identifies that a particular request should be proxied to Evennia's websocket
port but this should be applicable also to other types of proxies (like nginx).

View file

@ -0,0 +1,272 @@
# Arxcode installing help
## Introduction
[Arx - After the Reckoning](http://play.arxmush.org/) is a big and very popular
[Evennia](http://www.evennia.com)-based game. Arx is heavily roleplaying-centric, relying on game
masters to drive the story. Technically it's maybe best described as "a MUSH, but with more coded
systems". In August of 2018, the game's developer, Tehom, generously released the [source code of
Arx on github](https://github.com/Arx-Game/arxcode). This is a treasure-trove for developers wanting
to pick ideas or even get a starting game to build on. These instructions are based on the Arx-code
released as of *Aug 12, 2018*.
If you are not familiar with what Evennia is, you can read
[an introduction here](./Evennia-Introduction).
It's not too hard to run Arx from the sources (of course you'll start with an empty database) but
since part of Arx has grown organically, it doesn't follow standard Evennia paradigms everywhere.
This page covers one take on installing and setting things up while making your new Arx-based game
better match with the vanilla Evennia install.
## Installing Evennia
Firstly, set aside a folder/directory on your drive for everything to follow.
You need to start by installing [Evennia](http://www.evennia.com) by following most of the [Getting
Started
Instructions](Getting-Started) for your OS. The difference is that you need to `git clone
https://github.com/TehomCD/evennia.git` instead of Evennia's repo because Arx uses TehomCD's older
Evennia 0.8 [fork](https://github.com/TehomCD/evennia), notably still using Python2. This detail is
important if referring to newer Evennia documentation.
If you are new to Evennia it's *highly* recommended that you run through the
instructions in full - including initializing and starting a new empty game and connecting to it.
That way you can be sure Evennia works correctly as a base line. If you have trouble, make sure to
read the [Troubleshooting instructions](./Getting-Started#troubleshooting) for your
operating system. You can also drop into our
[forums](https://groups.google.com/forum/#%21forum/evennia), join `#evennia` on `irc.freenode.net`
or chat from the linked [Discord Server](https://discord.gg/NecFePw).
After installing you should have a `virtualenv` running and you should have the following file
structure in your set-aside folder:
```
vienv/
evennia/
mygame/
```
Here `mygame` is the empty game you created during the Evennia install, with `evennia --init`. Go to
that and run `evennia stop` to make sure your empty game is not running. We'll instead let Evenna
run Arx, so in principle you could erase `mygame` - but it could also be good to have a clean game
to compare to.
## Installing Arxcode
### Clone the arxcode repo
Cd to the root of your directory and clone the released source code from github:
git clone https://github.com/Arx-Game/arxcode.git myarx
A new folder `myarx` should appear next to the ones you already had. You could rename this to
something else if you want.
Cd into `myarx`. If you wonder about the structure of the game dir, you can [read more about it
here](Directory-Overview).
### Clean up settings
Arx has split evennia's normal settings into `base_settings.py` and `production_settings.py`. It
also has its own solution for managing 'secret' parts of the settings file. We'll keep most of Arx
way but remove the secret-handling and replace it with the normal Evennia method.
Cd into `myarx/server/conf/` and open the file `settings.py` in a text editor. The top part (within
`"""..."""`) is just help text. Wipe everything underneath that and make it look like this instead
(don't forget to save):
```
from base_settings import *
TELNET_PORTS = [4000]
SERVERNAME = "MyArx"
GAME_SLOGAN = "The cool game"
try:
from server.conf.secret_settings import *
except ImportError:
print("secret_settings.py file not found or failed to import.")
```
> Note: Indents and capitalization matter in Python. Make indents 4 spaces (not tabs) for your own
> sanity. If you want a starter on Python in Evennia, [you can look here](Python-basic-
introduction).
This will import Arx' base settings and override them with the Evennia-default telnet port and give
the game a name. The slogan changes the sub-text shown under the name of your game in the website
header. You can tweak these to your own liking later.
Next, create a new, empty file `secret_settings.py` in the same location as the `settings.py` file.
This can just contain the following:
```python
SECRET_KEY = "sefsefiwwj3 jnwidufhjw4545_oifej whewiu hwejfpoiwjrpw09&4er43233fwefwfw"
```
Replace the long random string with random ASCII characters of your own. The secret key should not
be shared.
Next, open `myarx/server/conf/base_settings.py` in your text editor. We want to remove/comment out
all mentions of the `decouple` package, which Evennia doesn't use (we use `private_settings.py` to
hide away settings that should not be shared).
Comment out `from decouple import config` by adding a `#` to the start of the line: `# from decouple
import config`. Then search for `config(` in the file and comment out all lines where this is used.
Many of these are specific to the server environment where the original Arx runs, so is not that
relevant to us.
### Install Arx dependencies
Arx has some further dependencies beyond vanilla Evennia. Start by `cd`:ing to the root of your
`myarx` folder.
> If you run *Linux* or *Mac*: Edit `myarx/requirements.txt` and comment out the line
> `pypiwin32==219` - it's only needed on Windows and will give an error on other platforms.
Make sure your `virtualenv` is active, then run
pip install -r requirements.txt
The needed Python packages will be installed for you.
### Adding logs/ folder
The Arx repo does not contain the `myarx/server/logs/` folder Evennia expects for storing server
logs. This is simple to add:
# linux/mac
mkdir server/logs
# windows
mkdir server\logs
### Setting up the database and starting
From the `myarx` folder, run
evennia migrate
This creates the database and will step through all database migrations needed.
evennia start
If all goes well Evennia will now start up, running Arx! You can connect to it on `localhost` (or
`127.0.0.1` if your platform doesn't alias `localhost`), port `4000` using a Telnet client.
Alternatively, you can use your web browser to browse to `http://localhost:4001` to see the game's
website and get to the web client.
When you log in you'll get the standard Evennia greeting (since the database is empty), but you can
try `help` to see that it's indeed Arx that is running.
### Additional Setup Steps
The first time you start Evennia after creating the database with the `evennia migrate` step above,
it should create a few starting objects for you - your superuser account, which it will prompt you
to enter, a starting room (Limbo), and a character object for you. If for some reason this does not
occur, you may have to follow the steps below. For the first time Superuser login you may have to
run steps 7-8 and 10 to create and connect to your in-came Character.
1. Login to the game website with your Superuser account.
2. Press the `Admin` button to get into the (Django-) Admin Interface.
3. Navigate to the `Accounts` section.
4. Add a new Account named for the new staffer. Use a place holder password and dummy e-mail
address.
5. Flag account as `Staff` and apply the `Admin` permission group (This assumes you have already set
up an Admin Group in Django).
6. Add Tags named `player` and `developer`.
7. Log into the game using the web client (or a third-party telnet client) using your superuser
account. Move to where you want the new staffer character to appear.
8. In the game client, run `@create/drop <staffername>:typeclasses.characters.Character`, where
`<staffername>` is usually the same name you used for the Staffer account you created in the
Admin earlier (if you are creating a Character for your superuser, use your superuser account
name).
This creates a new in-game Character and places it in your current location.
9. Have the new Admin player log into the game.
10. Have the new Admin puppet the character with `@ic StafferName`.
11. Have the new Admin change their password - `@password <old password> = <new password>`.
Now that you have a Character and an Account object, there's a few additional things you may need to
do in order for some commands to function properly. You can either execute these as in-game commands
while `@ic` (controlling your character object).
1. `@py from web.character.models import RosterEntry;RosterEntry.objects.create(player=self.player,
character=self)`
2. `@py from world.dominion.models import PlayerOrNpc, AssetOwner;dompc =
PlayerOrNpc.objects.create(player = self.player);AssetOwner.objects.create(player=dompc)`
Those steps will give you a 'RosterEntry', 'PlayerOrNpc', and 'AssetOwner' objects. RosterEntry
explicitly connects a character and account object together, even while offline, and contains
additional information about a character's current presence in game (such as which 'roster' they're
in, if you choose to use an active roster of characters). PlayerOrNpc are more character extensions,
as well as support for npcs with no in-game presence and just represented by a name which can be
offscreen members of a character's family. It also allows for membership in Organizations.
AssetOwner holds information about a character or organization's money and resources.
## Alternate guide by Pax for installing on Windows
If for some reason you cannot use the Windows Subsystem for Linux (which would use instructions
identical to the ones above), it's possible to get Evennia running under Anaconda for Windows. The
process is a little bit trickier.
Make sure you have:
* Git for Windows https://git-scm.com/download/win
* Anaconda for Windows https://www.anaconda.com/distribution/
* VC++ Compiler for Python 2.7 http://aka.ms/vcpython27
conda update conda
conda create -n arx python=2.7
source activate arx
Set up a convenient repository place for things.
cd ~
mkdir Source
cd Source
mkdir Arx
cd Arx
Replace the SSH git clone links below with your own github forks.
If you don't plan to change Evennia at all, you can use the
evennia/evennia.git repo instead of a forked one.
git clone git@github.com:<youruser>/evennia.git
git clone git@github.com:<youruser>/arxcode.git
Evennia is a package itself, so we want to install it and all of its
prerequisites, after switching to the appropriately-tagged branch for
Arxcode.
cd evennia
git checkout tags/v0.7 -b arx-master
pip install -e .
Arx has some dependencies of its own, so now we'll go install them
As it is not a package, we'll use the normal requirements file.
cd ../arxcode
pip install -r requirements.txt
The git repo doesn't include the empty log directory and Evennia is unhappy if you
don't have it, so while still in the arxcode directory...
mkdir server/logs
Now hit https://github.com/evennia/evennia/wiki/Arxcode-installing-help and
change the setup stuff as in the 'Clean up settings' section.
Then we will create our default database...
../evennia/bin/windows/evennia.bat migrate
...and do the first run. You need winpty because Windows does not have a TTY/PTY
by default, and so the Python console input commands (used for prompts on first
run) will fail and you will end up in an unhappy place. Future runs, you should
not need winpty.
winpty ../evennia/bin/windows/evennia.bat start
Once this is done, you should have your Evennia server running Arxcode up
on localhost at port 4000, and the webserver at http://localhost:4001/
And you are done! Huzzah!

View file

@ -0,0 +1,234 @@
# Async Process
*This is considered an advanced topic.*
## Synchronous versus Asynchronous
Most program code operates *synchronously*. This means that each statement in your code gets
processed and finishes before the next can begin. This makes for easy-to-understand code. It is also
a *requirement* in many cases - a subsequent piece of code often depend on something calculated or
defined in a previous statement.
Consider this piece of code in a traditional Python program:
```python
print("before call ...")
long_running_function()
print("after call ...")
```
When run, this will print `"before call ..."`, after which the `long_running_function` gets to work
for however long time. Only once that is done, the system prints `"after call ..."`. Easy and
logical to follow. Most of Evennia work in this way and often it's important that commands get
executed in the same strict order they were coded.
Evennia, via Twisted, is a single-process multi-user server. In simple terms this means that it
swiftly switches between dealing with player input so quickly that each player feels like they do
things at the same time. This is a clever illusion however: If one user, say, runs a command
containing that `long_running_function`, *all* other players are effectively forced to wait until it
finishes.
Now, it should be said that on a modern computer system this is rarely an issue. Very few commands
run so long that other users notice it. And as mentioned, most of the time you *want* to enforce
all commands to occur in strict sequence.
When delays do become noticeable and you don't care in which order the command actually completes,
you can run it *asynchronously*. This makes use of the `run_async()` function in
`src/utils/utils.py`:
```python
run_async(function, *args, **kwargs)
```
Where `function` will be called asynchronously with `*args` and `**kwargs`. Example:
```python
from evennia import utils
print("before call ...")
utils.run_async(long_running_function)
print("after call ...")
```
Now, when running this you will find that the program will not wait around for
`long_running_function` to finish. In fact you will see `"before call ..."` and `"after call ..."`
printed out right away. The long-running function will run in the background and you (and other
users) can go on as normal.
## Customizing asynchronous operation
A complication with using asynchronous calls is what to do with the result from that call. What if
`long_running_function` returns a value that you need? It makes no real sense to put any lines of
code after the call to try to deal with the result from `long_running_function` above - as we saw
the `"after call ..."` got printed long before `long_running_function` was finished, making that
line quite pointless for processing any data from the function. Instead one has to use *callbacks*.
`utils.run_async` takes reserved kwargs that won't be passed into the long-running function:
- `at_return(r)` (the *callback*) is called when the asynchronous function (`long_running_function`
above) finishes successfully. The argument `r` will then be the return value of that function (or
`None`).
```python
def at_return(r):
print(r)
```
- `at_return_kwargs` - an optional dictionary that will be fed as keyword arguments to the
`at_return` callback.
- `at_err(e)` (the *errback*) is called if the asynchronous function fails and raises an exception.
This exception is passed to the errback wrapped in a *Failure* object `e`. If you do not supply an
errback of your own, Evennia will automatically add one that silently writes errors to the evennia
log. An example of an errback is found below:
```python
def at_err(e):
print("There was an error:", str(e))
```
- `at_err_kwargs` - an optional dictionary that will be fed as keyword arguments to the `at_err`
errback.
An example of making an asynchronous call from inside a [Command](./Commands) definition:
```python
from evennia import utils, Command
class CmdAsync(Command):
key = "asynccommand"
def func(self):
def long_running_function():
#[... lots of time-consuming code ...]
return final_value
def at_return_function(r):
self.caller.msg("The final value is %s" % r)
def at_err_function(e):
self.caller.msg("There was an error: %s" % e)
# do the async call, setting all callbacks
utils.run_async(long_running_function, at_return=at_return_function,
at_err=at_err_function)
```
That's it - from here on we can forget about `long_running_function` and go on with what else need
to be done. *Whenever* it finishes, the `at_return_function` function will be called and the final
value will
pop up for us to see. If not we will see an error message.
## delay
The `delay` function is a much simpler sibling to `run_async`. It is in fact just a way to delay the
execution of a command until a future time. This is equivalent to something like `time.sleep()`
except delay is asynchronous while `sleep` would lock the entire server for the duration of the
sleep.
```python
from evennia.utils import delay
# [...]
# e.g. inside a Command, where `self.caller` is available
def callback(obj):
obj.msg("Returning!")
delay(10, callback, self.caller)
```
This will delay the execution of the callback for 10 seconds. This function is explored much more in
the [Command Duration Tutorial](./Command-Duration).
You can also try the following snippet just see how it works:
@py from evennia.utils import delay; delay(10, lambda who: who.msg("Test!"), self)
Wait 10 seconds and 'Test!' should be echoed back to you.
## The @interactive decorator
As of Evennia 0.9, the `@interactive` [decorator](https://realpython.com/primer-on-python-
decorators/)
is available. This makes any function or method possible to 'pause' and/or await player input
in an interactive way.
```python
from evennia.utils import interactive
@interactive
def myfunc(caller):
while True:
caller.msg("Getting ready to wait ...")
yield(5)
caller.msg("Now 5 seconds have passed.")
response = yield("Do you want to wait another 5 secs?")
if response.lower() not in ("yes", "y"):
break
```
The `@interactive` decorator gives the function the ability to pause. The use
of `yield(seconds)` will do just that - it will asynchronously pause for the
number of seconds given before continuing. This is technically equivalent to
using `call_async` with a callback that continues after 5 secs. But the code
with `@interactive` is a little easier to follow.
Within the `@interactive` function, the `response = yield("question")` question
allows you to ask the user for input. You can then process the input, just like
you would if you used the Python `input` function. There is one caveat to this
functionality though - _it will only work if the function/method has an
argument named exactly `caller`_. This is because internally Evennia will look
for the `caller` argument and treat that as the source of input.
All of this makes the `@interactive` decorator very useful. But it comes with a
few caveats. Notably, decorating a function/method with `@interactive` turns it
into a Python [generator](https://wiki.python.org/moin/Generators). The most
common issue is that you cannot use `return <value>` from a generator (just an
empty `return` works). To return a value from a function/method you have decorated
with `@interactive`, you must instead use a special Twisted function
`twisted.internet.defer.returnValue`. Evennia also makes this function
conveniently available from `evennia.utils`:
```python
from evennia.utils import interactive, returnValue
@interactive
def myfunc():
# ...
result = 10
# this must be used instead of `return result`
returnValue(result)
```
## Assorted notes
Overall, be careful with choosing when to use asynchronous calls. It is mainly useful for large
administration operations that have no direct influence on the game world (imports and backup
operations come to mind). Since there is no telling exactly when an asynchronous call actually ends,
using them for in-game commands is to potentially invite confusion and inconsistencies (and very
hard-to-reproduce bugs).
The very first synchronous example above is not *really* correct in the case of Twisted, which is
inherently an asynchronous server. Notably you might find that you will *not* see the first `before
call ...` text being printed out right away. Instead all texts could end up being delayed until
after the long-running process finishes. So all commands will retain their relative order as
expected, but they may appear with delays or in groups.
## Further reading
Technically, `run_async` is just a very thin and simplified wrapper around a
[Twisted Deferred](http://twistedmatrix.com/documents/9.0.0/core/howto/defer.html) object; the
wrapper sets
up a default errback also if none is supplied. If you know what you are doing there is nothing
stopping you from bypassing the utility function, building a more sophisticated callback chain after
your own liking.

394
docs/source/Attributes.md Normal file
View file

@ -0,0 +1,394 @@
# Attributes
When performing actions in Evennia it is often important that you store data for later. If you write
a menu system, you have to keep track of the current location in the menu tree so that the player
can give correct subsequent commands. If you are writing a combat system, you might have a
combattant's next roll get easier dependent on if their opponent failed. Your characters will
probably need to store roleplaying-attributes like strength and agility. And so on.
[Typeclassed](./Typeclasses) game entities ([Accounts](./Accounts), [Objects](./Objects),
[Scripts](./Scripts) and [Channels](./Communications)) always have *Attributes* associated with them.
Attributes are used to store any type of data 'on' such entities. This is different from storing
data in properties already defined on entities (such as `key` or `location`) - these have very
specific names and require very specific types of data (for example you couldn't assign a python
*list* to the `key` property no matter how hard you tried). `Attributes` come into play when you
want to assign arbitrary data to arbitrary names.
**Attributes are _not_ secure by default and any player may be able to change them unless you
[prevent this behavior](./Attributes#locking-and-checking-attributes).**
## The .db and .ndb shortcuts
To save persistent data on a Typeclassed object you normally use the `db` (DataBase) operator. Let's
try to save some data to a *Rose* (an [Object](./Objects)):
```python
# saving
rose.db.has_thorns = True
# getting it back
is_ouch = rose.db.has_thorns
```
This looks like any normal Python assignment, but that `db` makes sure that an *Attribute* is
created behind the scenes and is stored in the database. Your rose will continue to have thorns
throughout the life of the server now, until you deliberately remove them.
To be sure to save **non-persistently**, i.e. to make sure NOT to create a database entry, you use
`ndb` (NonDataBase). It works in the same way:
```python
# saving
rose.ndb.has_thorns = True
# getting it back
is_ouch = rose.ndb.has_thorns
```
Technically, `ndb` has nothing to do with `Attributes`, despite how similar they look. No
`Attribute` object is created behind the scenes when using `ndb`. In fact the database is not
invoked at all since we are not interested in persistence. There is however an important reason to
use `ndb` to store data rather than to just store variables direct on entities - `ndb`-stored data
is tracked by the server and will not be purged in various cache-cleanup operations Evennia may do
while it runs. Data stored on `ndb` (as well as `db`) will also be easily listed by example the
`@examine` command.
You can also `del` properties on `db` and `ndb` as normal. This will for example delete an
`Attribute`:
```python
del rose.db.has_thorns
```
Both `db` and `ndb` defaults to offering an `all` property on themselves. This returns all
associated attributes or non-persistent properties.
```python
list_of_all_rose_attributes = rose.db.all
list_of_all_rose_ndb_attrs = rose.ndb.all
```
If you use `all` as the name of an attribute, this will be used instead. Later deleting your custom
`all` will return the default behaviour.
## The AttributeHandler
The `.db` and `.ndb` properties are very convenient but if you don't know the name of the Attribute
beforehand they cannot be used. Behind the scenes `.db` actually accesses the `AttributeHandler`
which sits on typeclassed entities as the `.attributes` property. `.ndb` does the same for the
`.nattributes` property.
The handlers have normal access methods that allow you to manage and retrieve `Attributes` and
`NAttributes`:
- `has('attrname')` - this checks if the object has an Attribute with this key. This is equivalent
to doing `obj.db.attrname`.
- `get(...)` - this retrieves the given Attribute. Normally the `value` property of the Attribute is
returned, but the method takes keywords for returning the Attribute object itself. By supplying an
`accessing_object` to the call one can also make sure to check permissions before modifying
anything.
- `add(...)` - this adds a new Attribute to the object. An optional [lockstring](./Locks) can be
supplied here to restrict future access and also the call itself may be checked against locks.
- `remove(...)` - Remove the given Attribute. This can optionally be made to check for permission
before performing the deletion. - `clear(...)` - removes all Attributes from object.
- `all(...)` - returns all Attributes (of the given category) attached to this object.
See [this section](./Attributes#locking-and-checking-attributes) for more about locking down Attribute
access and editing. The `Nattribute` offers no concept of access control.
Some examples:
```python
import evennia
obj = evennia.search_object("MyObject")
obj.attributes.add("test", "testvalue")
print(obj.db.test) # prints "testvalue"
print(obj.attributes.get("test")) # "
print(obj.attributes.all()) # prints [<AttributeObject>]
obj.attributes.remove("test")
```
## Properties of Attributes
An Attribute object is stored in the database. It has the following properties:
- `key` - the name of the Attribute. When doing e.g. `obj.db.attrname = value`, this property is set
to `attrname`.
- `value` - this is the value of the Attribute. This value can be anything which can be pickled -
objects, lists, numbers or what have you (see
[this section](./Attributes#What_types_of_data_can_I_save_in_an_Attribute) for more info). In the
example
`obj.db.attrname = value`, the `value` is stored here.
- `category` - this is an optional property that is set to None for most Attributes. Setting this
allows to use Attributes for different functionality. This is usually not needed unless you want
to use Attributes for very different functionality ([Nicks](./Nicks) is an example of using
Attributes
in this way). To modify this property you need to use the [Attribute
Handler](Attributes#The_Attribute_Handler).
- `strvalue` - this is a separate value field that only accepts strings. This severely limits the
data possible to store, but allows for easier database lookups. This property is usually not used
except when re-using Attributes for some other purpose ([Nicks](./Nicks) use it). It is only
accessible via the [Attribute Handler](./Attributes#The_Attribute_Handler).
There are also two special properties:
- `attrtype` - this is used internally by Evennia to separate [Nicks](./Nicks), from Attributes (Nicks
use Attributes behind the scenes).
- `model` - this is a *natural-key* describing the model this Attribute is attached to. This is on
the form *appname.modelclass*, like `objects.objectdb`. It is used by the Attribute and
NickHandler to quickly sort matches in the database. Neither this nor `attrtype` should normally
need to be modified.
Non-database attributes have no equivalence to `category` nor `strvalue`, `attrtype` or `model`.
## Persistent vs non-persistent
So *persistent* data means that your data will survive a server reboot, whereas with
*non-persistent* data it will not ...
... So why would you ever want to use non-persistent data? The answer is, you don't have to. Most of
the time you really want to save as much as you possibly can. Non-persistent data is potentially
useful in a few situations though.
- You are worried about database performance. Since Evennia caches Attributes very aggressively,
this is not an issue unless you are reading *and* writing to your Attribute very often (like many
times per second). Reading from an already cached Attribute is as fast as reading any Python
property. But even then this is not likely something to worry about: Apart from Evennia's own
caching, modern database systems themselves also cache data very efficiently for speed. Our
default
database even runs completely in RAM if possible, alleviating much of the need to write to disk
during heavy loads.
- A more valid reason for using non-persistent data is if you *want* to lose your state when logging
off. Maybe you are storing throw-away data that are re-initialized at server startup. Maybe you
are implementing some caching of your own. Or maybe you are testing a buggy [Script](./Scripts) that
does potentially harmful stuff to your character object. With non-persistent storage you can be
sure
that whatever is messed up, it's nothing a server reboot can't clear up.
- NAttributes have no restrictions at all on what they can store (see next section), since they
don't need to worry about being saved to the database - they work very well for temporary storage.
- You want to implement a fully or partly *non-persistent world*. Who are we to argue with your
grand vision!
## What types of data can I save in an Attribute?
> None of the following affects NAttributes, which does not invoke the database at all. There are no
> restrictions to what can be stored in a NAttribute.
The database doesn't know anything about Python objects, so Evennia must *serialize* Attribute
values into a string representation in order to store it to the database. This is done using the
`pickle` module of Python (the only exception is if you use the `strattr` keyword of the
AttributeHandler to save to the `strvalue` field of the Attribute. In that case you can only save
*strings* which will not be pickled).
It's important to note that when you access the data in an Attribute you are *always* de-serializing
it from the database representation every time. This is because we allow for storing
database-entities in Attributes too. If we cached it as its Python form, we might end up with
situations where the database entity was deleted since we last accessed the Attribute.
De-serializing data with a database-entity in it means querying the database for that object and
making sure it still exists (otherwise it will be set to `None`). Performance-wise this is usually
not a big deal. But if you are accessing the Attribute as part of some big loop or doing a large
amount of reads/writes you should first extract it to a temporary variable, operate on *that* and
then save the result back to the Attribute. If you are storing a more complex structure like a
`dict` or a `list` you should make sure to "disconnect" it from the database before looping over it,
as mentioned in the [Retrieving Mutable Objects](./Attributes#retrieving-mutable-objects) section
below.
### Storing single objects
With a single object, we mean anything that is *not iterable*, like numbers, strings or custom class
instances without the `__iter__` method.
* You can generally store any non-iterable Python entity that can be
[pickled](http://docs.python.org/library/pickle.html).
* Single database objects/typeclasses can be stored as any other in the Attribute. These can
normally *not* be pickled, but Evennia will behind the scenes convert them to an internal
representation using their classname, database-id and creation-date with a microsecond precision,
guaranteeing you get the same object back when you access the Attribute later.
* If you *hide* a database object inside a non-iterable custom class (like stored as a variable
inside it), Evennia will not know it's there and won't convert it safely. Storing classes with
such hidden database objects is *not* supported and will lead to errors!
```python
# Examples of valid single-value attribute data:
obj.db.test1 = 23
obj.db.test1 = False
# a database object (will be stored as an internal representation)
obj.db.test2 = myobj
# example of an invalid, "hidden" dbobject
class Invalid(object):
def __init__(self, dbobj):
# no way for Evennia to know this is a dbobj
self.dbobj = dbobj
invalid = Invalid(myobj)
obj.db.invalid = invalid # will cause error!
```
### Storing multiple objects
This means storing objects in a collection of some kind and are examples of *iterables*, pickle-able
entities you can loop over in a for-loop. Attribute-saving supports the following iterables:
* [Tuples](https://docs.python.org/2/library/functions.html#tuple), like `(1,2,"test", <dbobj>)`.
* [Lists](https://docs.python.org/2/tutorial/datastructures.html#more-on-lists), like `[1,2,"test",
<dbobj>]`.
* [Dicts](https://docs.python.org/2/tutorial/datastructures.html#dictionaries), like `{1:2,
"test":<dbobj>]`.
* [Sets](https://docs.python.org/2/tutorial/datastructures.html#sets), like `{1,2,"test",<dbobj>}`.
* [collections.OrderedDict](https://docs.python.org/2/library/collections.html#collections.OrderedDi
ct), like `OrderedDict((1,2), ("test", <dbobj>))`.
* [collections.Deque](https://docs.python.org/2/library/collections.html#collections.deque), like
`deque((1,2,"test",<dbobj>))`.
* *Nestings* of any combinations of the above, like lists in dicts or an OrderedDict of tuples, each
containing dicts, etc.
* All other iterables (i.e. entities with the `__iter__` method) will be converted to a *list*.
Since you can use any combination of the above iterables, this is generally not much of a
limitation.
Any entity listed in the [Single object](./Attributes#Storing-Single-Objects) section above can be
stored in the iterable.
> As mentioned in the previous section, database entities (aka typeclasses) are not possible to
> pickle. So when storing an iterable, Evennia must recursively traverse the iterable *and all its
> nested sub-iterables* in order to find eventual database objects to convert. This is a very fast
> process but for efficiency you may want to avoid too deeply nested structures if you can.
```python
# examples of valid iterables to store
obj.db.test3 = [obj1, 45, obj2, 67]
# a dictionary
obj.db.test4 = {'str':34, 'dex':56, 'agi':22, 'int':77}
# a mixed dictionary/list
obj.db.test5 = {'members': [obj1,obj2,obj3], 'enemies':[obj4,obj5]}
# a tuple with a list in it
obj.db.test6 = (1,3,4,8, ["test", "test2"], 9)
# a set
obj.db.test7 = set([1,2,3,4,5])
# in-situ manipulation
obj.db.test8 = [1,2,{"test":1}]
obj.db.test8[0] = 4
obj.db.test8[2]["test"] = 5
# test8 is now [4,2,{"test":5}]
```
### Retrieving Mutable objects
A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can
be modified in-place after they were created, which is everything except tuples) are handled by
custom objects called `_SaverList`, `_SaverDict` etc. These `_Saver...` classes behave just like the
normal variant except that they are aware of the database and saves to it whenever new data gets
assigned to them. This is what allows you to do things like `self.db.mylist[7] = val` and be sure
that the new version of list is saved. Without this you would have to load the list into a temporary
variable, change it and then re-assign it to the Attribute in order for it to save.
There is however an important thing to remember. If you retrieve your mutable iterable into another
variable, e.g. `mylist2 = obj.db.mylist`, your new variable (`mylist2`) will *still* be a
`_SaverList`. This means it will continue to save itself to the database whenever it is updated!
```python
obj.db.mylist = [1,2,3,4]
mylist = obj.db.mylist
mylist[3] = 5 # this will also update database
print(mylist) # this is now [1,2,3,5]
print(obj.db.mylist) # this is also [1,2,3,5]
```
To "disconnect" your extracted mutable variable from the database you simply need to convert the
`_Saver...` iterable to a normal Python structure. So to convert a `_SaverList`, you use the
`list()` function, for a `_SaverDict` you use `dict()` and so on.
```python
obj.db.mylist = [1,2,3,4]
mylist = list(obj.db.mylist) # convert to normal list
mylist[3] = 5
print(mylist) # this is now [1,2,3,5]
print(obj.db.mylist) # this is still [1,2,3,4]
```
A further problem comes with *nested mutables*, like a dict containing lists of dicts or something
like that. Each of these nested mutables would be `_Saver*` structures connected to the database and
disconnecting the outermost one of them would not disconnect those nested within. To make really
sure you disonnect a nested structure entirely from the database, Evennia provides a special
function `evennia.utils.dbserialize.deserialize`:
```
from evennia.utils.dbserialize import deserialize
decoupled_mutables = deserialize(nested_mutables)
```
The result of this operation will be a structure only consisting of normal Python mutables (`list`
instead of `_SaverList` and so on).
Remember, this is only valid for *mutable* iterables.
[Immutable](http://en.wikipedia.org/wiki/Immutable) objects (strings, numbers, tuples etc) are
already disconnected from the database from the onset.
```python
obj.db.mytup = (1,2,[3,4])
obj.db.mytup[0] = 5 # this fails since tuples are immutable
# this works but will NOT update database since outermost is a tuple
obj.db.mytup[2][1] = 5
print(obj.db.mytup[2][1]) # this still returns 4, not 5
mytup1 = obj.db.mytup # mytup1 is already disconnected from database since outermost
# iterable is a tuple, so we can edit the internal list as we want
# without affecting the database.
```
> Attributes will fetch data fresh from the database whenever you read them, so
> if you are performing big operations on a mutable Attribute property (such as looping over a list
> or dict) you should make sure to "disconnect" the Attribute's value first and operate on this
> rather than on the Attribute. You can gain dramatic speed improvements to big loops this
> way.
## Locking and checking Attributes
Attributes are normally not locked down by default, but you can easily change that for individual
Attributes (like those that may be game-sensitive in games with user-level building).
First you need to set a *lock string* on your Attribute. Lock strings are specified [Locks](./Locks).
The relevant lock types are
- `attrread` - limits who may read the value of the Attribute
- `attredit` - limits who may set/change this Attribute
You cannot use the `db` handler to modify Attribute object (such as setting a lock on them) - The
`db` handler will return the Attribute's *value*, not the Attribute object itself. Instead you use
the AttributeHandler and set it to return the object instead of the value:
```python
lockstring = "attread:all();attredit:perm(Admins)"
obj.attributes.get("myattr", return_obj=True).locks.add(lockstring)
```
Note the `return_obj` keyword which makes sure to return the `Attribute` object so its LockHandler
could be accessed.
A lock is no good if nothing checks it -- and by default Evennia does not check locks on Attributes.
You have to add a check to your commands/code wherever it fits (such as before setting an
Attribute).
```python
# in some command code where we want to limit
# setting of a given attribute name on an object
attr = obj.attributes.get(attrname,
return_obj=True,
accessing_obj=caller,
default=None,
default_access=False)
if not attr:
caller.msg("You cannot edit that Attribute!")
return
# edit the Attribute here
```
The same keywords are available to use with `obj.attributes.set()` and `obj.attributes.remove()`,
those will check for the `attredit` lock type.

148
docs/source/Banning.md Normal file
View file

@ -0,0 +1,148 @@
# Banning
Whether due to abuse, blatant breaking of your rules, or some other reason, you will eventually find
no other recourse but to kick out a particularly troublesome player. The default command set has
admin tools to handle this, primarily `@ban`, `@unban`, and `@boot`.
## Creating a ban
Say we have a troublesome player "YouSuck" - this is a person that refuses common courtesy - an
abusive
and spammy account that is clearly created by some bored internet hooligan only to cause grief. You
have tried to be nice. Now you just want this troll gone.
### Name ban
The easiest recourse is to block the account YouSuck from ever connecting again.
@ban YouSuck
This will lock the name YouSuck (as well as 'yousuck' and any other capitalization combination), and
next time they try to log in with this name the server will not let them!
You can also give a reason so you remember later why this was a good thing (the banned account will
never see this)
@ban YouSuck:This is just a troll.
If you are sure this is just a spam account, you might even consider deleting the player account
outright:
@delaccount YouSuck
Generally, banning the name is the easier and safer way to stop the use of an account -- if you
change your mind you can always remove the block later whereas a deletion is permanent.
### IP ban
Just because you block YouSuck's name might not mean the trolling human behind that account gives
up. They can just create a new account YouSuckMore and be back at it. One way to make things harder
for them is to tell the server to not allow connections from their particular IP address.
First, when the offending account is online, check which IP address they use. This you can do with
the `who` command, which will show you something like this:
Account Name On for Idle Room Cmds Host
YouSuckMore 01:12 2m 22 212 237.333.0.223
The "Host" bit is the IP address from which the account is connecting. Use this to define the ban
instead of the name:
@ban 237.333.0.223
This will stop YouSuckMore connecting from their computer. Note however that IP address might change
easily - either due to how the player's Internet Service Provider operates or by the user simply
changing computers. You can make a more general ban by putting asterisks `*` as wildcards for the
groups of three digits in the address. So if you figure out that !YouSuckMore mainly connects from
237.333.0.223, 237.333.0.225, and 237.333.0.256 (only changes in their subnet), it might be an idea
to put down a ban like this to include any number in that subnet:
@ban 237.333.0.*
You should combine the IP ban with a name-ban too of course, so the account YouSuckMore is truly
locked regardless of where they connect from.
Be careful with too general IP bans however (more asterisks above). If you are unlucky you could be
blocking out innocent players who just happen to connect from the same subnet as the offender.
## Booting
YouSuck is not really noticing all this banning yet though - and won't until having logged out and
trying to log back in again. Let's help the troll along.
@boot YouSuck
Good riddance. You can give a reason for booting too (to be echoed to the player before getting
kicked out).
@boot YouSuck:Go troll somewhere else.
### Lifting a ban
Use the `@unban` (or `@ban`) command without any arguments and you will see a list of all currently
active bans:
Active bans
id name/ip date reason
1 yousuck Fri Jan 3 23:00:22 2020 This is just a Troll.
2 237.333.0.* Fri Jan 3 23:01:03 2020 YouSuck's IP.
Use the `id` from this list to find out which ban to lift.
@unban 2
Cleared ban 2: 237.333.0.*
## Summary of abuse-handling tools
Below are other useful commands for dealing with annoying players.
- **who** -- (as admin) Find the IP of a account. Note that one account can be connected to from
multiple IPs depending on what you allow in your settings.
- **examine/account thomas** -- Get all details about an account. You can also use `*thomas` to get
the account. If not given, you will get the *Object* thomas if it exists in the same location, which
is not what you want in this case.
- **boot thomas** -- Boot all sessions of the given account name.
- **boot 23** -- Boot one specific client session/IP by its unique id.
- **ban** -- List all bans (listed with ids)
- **ban thomas** -- Ban the user with the given account name
- **ban/ip `134.233.2.111`** -- Ban by IP
- **ban/ip `134.233.2.*`** -- Widen IP ban
- **ban/ip `134.233.*.*`** -- Even wider IP ban
- **unban 34** -- Remove ban with id #34
- **cboot mychannel = thomas** -- Boot a subscriber from a channel you control
- **clock mychannel = control:perm(Admin);listen:all();send:all()** -- Fine control of access to
your channel using [lock definitions](./Locks).
Locking a specific command (like `page`) is accomplished like so:
1. Examine the source of the command. [The default `page` command class](
https://github.com/evennia/evennia/blob/master/evennia/commands/default/comms.py#L686) has the lock
string **"cmd:not pperm(page_banned)"**. This means that unless the player has the 'permission'
"page_banned" they can use this command. You can assign any lock string to allow finer customization
in your commands. You might look for the value of an [Attribute](./Attributes) or [Tag](./Tags), your
current location etc.
2. **perm/account thomas = page_banned** -- Give the account the 'permission' which causes (in this
case) the lock to fail.
- **perm/del/account thomas = page_banned** -- Remove the given permission
- **tel thomas = jail** -- Teleport a player to a specified location or #dbref
- **type thomas = FlowerPot** -- Turn an annoying player into a flower pot (assuming you have a
`FlowerPot` typeclass ready)
- **userpassword thomas = fooBarFoo** -- Change a user's password
- **delaccount thomas** -- Delete a player account (not recommended, use **ban** instead)
- **server** -- Show server statistics, such as CPU load, memory usage, and how many objects are
cached
- **time** -- Gives server uptime, runtime, etc
- **reload** -- Reloads the server without disconnecting anyone
- **reset** -- Restarts the server, kicking all connections
- **shutdown** -- Stops the server cold without it auto-starting again
- **py** -- Executes raw Python code, allows for direct inspection of the database and account
objects on the fly. For advanced users.
**Useful Tip:** `evennia changepassword <username>` entered into the command prompt will reset the
password of any account, including the superuser or admin accounts. This is a feature of Django.

View file

@ -0,0 +1,229 @@
# Batch Code Processor
For an introduction and motivation to using batch processors, see [here](./Batch-Processors). This
page describes the Batch-*code* processor. The Batch-*command* one is covered [here](Batch-Command-
Processor).
## Basic Usage
The batch-code processor is a superuser-only function, invoked by
> @batchcode path.to.batchcodefile
Where `path.to.batchcodefile` is the path to a *batch-code file*. Such a file should have a name
ending in "`.py`" (but you shouldn't include that in the path). The path is given like a python path
relative to a folder you define to hold your batch files, set by `BATCH_IMPORT_PATH` in your
settings. Default folder is (assuming your game is called "mygame") `mygame/world/`. So if you want
to run the example batch file in `mygame/world/batch_code.py`, you could simply use
> @batchcode batch_code
This will try to run through the entire batch file in one go. For more gradual, *interactive*
control you can use the `/interactive` switch. The switch `/debug` will put the processor in
*debug* mode. Read below for more info.
## The batch file
A batch-code file is a normal Python file. The difference is that since the batch processor loads
and executes the file rather than importing it, you can reliably update the file, then call it
again, over and over and see your changes without needing to `@reload` the server. This makes for
easy testing. In the batch-code file you have also access to the following global variables:
- `caller` - This is a reference to the object running the batchprocessor.
- `DEBUG` - This is a boolean that lets you determine if this file is currently being run in debug-
mode or not. See below how this can be useful.
Running a plain Python file through the processor will just execute the file from beginning to end.
If you want to get more control over the execution you can use the processor's *interactive* mode.
This runs certain code blocks on their own, rerunning only that part until you are happy with it. In
order to do this you need to add special markers to your file to divide it up into smaller chunks.
These take the form of comments, so the file remains valid Python.
Here are the rules of syntax of the batch-code `*.py` file.
- `#CODE` as the first on a line marks the start of a *code* block. It will last until the beginning
of another marker or the end of the file. Code blocks contain functional python code. Each `#CODE`
block will be run in complete isolation from other parts of the file, so make sure it's self-
contained.
- `#HEADER` as the first on a line marks the start of a *header* block. It lasts until the next
marker or the end of the file. This is intended to hold imports and variables you will need for all
other blocks .All python code defined in a header block will always be inserted at the top of every
`#CODE` blocks in the file. You may have more than one `#HEADER` block, but that is equivalent to
having one big one. Note that you can't exchange data between code blocks, so editing a header-
variable in one code block won't affect that variable in any other code block!
- `#INSERT path.to.file` will insert another batchcode (Python) file at that position.
- A `#` that is not starting a `#HEADER`, `#CODE` or `#INSERT` instruction is considered a comment.
- Inside a block, normal Python syntax rules apply. For the sake of indentation, each block acts as
a separate python module.
Below is a version of the example file found in `evennia/contrib/tutorial_examples/`.
```python
#
# This is an example batch-code build file for Evennia.
#
#HEADER
# This will be included in all other #CODE blocks
from evennia import create_object, search_object
from evennia.contrib.tutorial_examples import red_button
from typeclasses.objects import Object
limbo = search_object('Limbo')[0]
#CODE
red_button = create_object(red_button.RedButton, key="Red button",
location=limbo, aliases=["button"])
# caller points to the one running the script
caller.msg("A red button was created.")
# importing more code from another batch-code file
#INSERT batch_code_insert
#CODE
table = create_object(Object, key="Blue Table", location=limbo)
chair = create_object(Object, key="Blue Chair", location=limbo)
string = "A %s and %s were created."
if DEBUG:
table.delete()
chair.delete()
string += " Since debug was active, " \
"they were deleted again."
caller.msg(string % (table, chair))
```
This uses Evennia's Python API to create three objects in sequence.
## Debug mode
Try to run the example script with
> @batchcode/debug tutorial_examples.example_batch_code
The batch script will run to the end and tell you it completed. You will also get messages that the
button and the two pieces of furniture were created. Look around and you should see the button
there. But you won't see any chair nor a table! This is because we ran this with the `/debug`
switch, which is directly visible as `DEBUG==True` inside the script. In the above example we
handled this state by deleting the chair and table again.
The debug mode is intended to be used when you test out a batchscript. Maybe you are looking for
bugs in your code or try to see if things behave as they should. Running the script over and over
would then create an ever-growing stack of chairs and tables, all with the same name. You would have
to go back and painstakingly delete them later.
## Interactive mode
Interactive mode works very similar to the [batch-command processor counterpart](Batch-Command-
Processor). It allows you more step-wise control over how the batch file is executed. This is useful
for debugging or for picking and choosing only particular blocks to run. Use `@batchcode` with the
`/interactive` flag to enter interactive mode.
> @batchcode/interactive tutorial_examples.example_batch_code
You should see the following:
01/02: red_button = create_object(red_button.RedButton, [...] (hh for help)
This shows that you are on the first `#CODE` block, the first of only two commands in this batch
file. Observe that the block has *not* actually been executed at this point!
To take a look at the full code snippet you are about to run, use `ll` (a batch-processor version of
`look`).
```python
from evennia.utils import create, search
from evennia.contrib.tutorial_examples import red_button
from typeclasses.objects import Object
limbo = search.objects(caller, 'Limbo', global_search=True)[0]
red_button = create.create_object(red_button.RedButton, key="Red button",
location=limbo, aliases=["button"])
# caller points to the one running the script
caller.msg("A red button was created.")
```
Compare with the example code given earlier. Notice how the content of `#HEADER` has been pasted at
the top of the `#CODE` block. Use `pp` to actually execute this block (this will create the button
and give you a message). Use `nn` (next) to go to the next command. Use `hh` for a list of commands.
If there are tracebacks, fix them in the batch file, then use `rr` to reload the file. You will
still be at the same code block and can rerun it easily with `pp` as needed. This makes for a simple
debug cycle. It also allows you to rerun individual troublesome blocks - as mentioned, in a large
batch file this can be very useful (don't forget the `/debug` mode either).
Use `nn` and `bb` (next and back) to step through the file; e.g. `nn 12` will jump 12 steps forward
(without processing any blocks in between). All normal commands of Evennia should work too while
working in interactive mode.
## Limitations and Caveats
The batch-code processor is by far the most flexible way to build a world in Evennia. There are
however some caveats you need to keep in mind.
### Safety
Or rather the lack of it. There is a reason only *superusers* are allowed to run the batch-code
processor by default. The code-processor runs **without any Evennia security checks** and allows
full access to Python. If an untrusted party could run the code-processor they could execute
arbitrary python code on your machine, which is potentially a very dangerous thing. If you want to
allow other users to access the batch-code processor you should make sure to run Evennia as a
separate and very limited-access user on your machine (i.e. in a 'jail'). By comparison, the batch-
command processor is much safer since the user running it is still 'inside' the game and can't
really do anything outside what the game commands allow them to.
### No communication between code blocks
Global variables won't work in code batch files, each block is executed as stand-alone environments.
`#HEADER` blocks are literally pasted on top of each `#CODE` block so updating some header-variable
in your block will not make that change available in another block. Whereas a python execution
limitation, allowing this would also lead to very hard-to-debug code when using the interactive mode
- this would be a classical example of "spaghetti code".
The main practical issue with this is when building e.g. a room in one code block and later want to
connect that room with a room you built in the current block. There are two ways to do this:
- Perform a database search for the name of the room you created (since you cannot know in advance
which dbref it got assigned). The problem is that a name may not be unique (you may have a lot of "A
dark forest" rooms). There is an easy way to handle this though - use [Tags](./Tags) or *Aliases*. You
can assign any number of tags and/or aliases to any object. Make sure that one of those tags or
aliases is unique to the room (like "room56") and you will henceforth be able to always uniquely
search and find it later.
- Use the `caller` global property as an inter-block storage. For example, you could have a
dictionary of room references in an `ndb`:
```python
#HEADER
if caller.ndb.all_rooms is None:
caller.ndb.all_rooms = {}
#CODE
# create and store the castle
castle = create_object("rooms.Room", key="Castle")
caller.ndb.all_rooms["castle"] = castle
#CODE
# in another node we want to access the castle
castle = caller.ndb.all_rooms.get("castle")
```
Note how we check in `#HEADER` if `caller.ndb.all_rooms` doesn't already exist before creating the
dict. Remember that `#HEADER` is copied in front of every `#CODE` block. Without that `if` statement
we'd be wiping the dict every block!
### Don't treat a batchcode file like any Python file
Despite being a valid Python file, a batchcode file should *only* be run by the batchcode processor.
You should not do things like define Typeclasses or Commands in them, or import them into other
code. Importing a module in Python will execute base level of the module, which in the case of your
average batchcode file could mean creating a lot of new objects every time.
### Don't let code rely on the batch-file's real file path
When you import things into your batchcode file, don't use relative imports but always import with
paths starting from the root of your game directory or evennia library. Code that relies on the
batch file's "actual" location *will fail*. Batch code files are read as text and the strings
executed. When the code runs it has no knowledge of what file those strings where once a part of.

View file

@ -0,0 +1,182 @@
# Batch Command Processor
For an introduction and motivation to using batch processors, see [here](./Batch-Processors). This
page describes the Batch-*command* processor. The Batch-*code* one is covered [here](Batch-Code-
Processor).
## Basic Usage
The batch-command processor is a superuser-only function, invoked by
> @batchcommand path.to.batchcmdfile
Where `path.to.batchcmdfile` is the path to a *batch-command file* with the "`.ev`" file ending.
This path is given like a python path relative to a folder you define to hold your batch files, set
with `BATCH_IMPORT_PATH` in your settings. Default folder is (assuming your game is in the `mygame`
folder) `mygame/world`. So if you want to run the example batch file in
`mygame/world/batch_cmds.ev`, you could use
> @batchcommand batch_cmds
A batch-command file contains a list of Evennia in-game commands separated by comments. The
processor will run the batch file from beginning to end. Note that *it will not stop if commands in
it fail* (there is no universal way for the processor to know what a failure looks like for all
different commands). So keep a close watch on the output, or use *Interactive mode* (see below) to
run the file in a more controlled, gradual manner.
## The batch file
The batch file is a simple plain-text file containing Evennia commands. Just like you would write
them in-game, except you have more freedom with line breaks.
Here are the rules of syntax of an `*.ev` file. You'll find it's really, really simple:
- All lines having the `#` (hash)-symbol *as the first one on the line* are considered *comments*.
All non-comment lines are treated as a command and/or their arguments.
- Comment lines have an actual function -- they mark the *end of the previous command definition*.
So never put two commands directly after one another in the file - separate them with a comment, or
the second of the two will be considered an argument to the first one. Besides, using plenty of
comments is good practice anyway.
- A line that starts with the word `#INSERT` is a comment line but also signifies a special
instruction. The syntax is `#INSERT <path.batchfile>` and tries to import a given batch-cmd file
into this one. The inserted batch file (file ending `.ev`) will run normally from the point of the
`#INSERT` instruction.
- Extra whitespace in a command definition is *ignored*. - A completely empty line translates in to
a line break in texts. Two empty lines thus means a new paragraph (this is obviously only relevant
for commands accepting such formatting, such as the `@desc` command).
- The very last command in the file is not required to end with a comment.
- You *cannot* nest another `@batchcommand` statement into your batch file. If you want to link many
batch-files together, use the `#INSERT` batch instruction instead. You also cannot launch the
`@batchcode` command from your batch file, the two batch processors are not compatible.
Below is a version of the example file found in `evennia/contrib/tutorial_examples/batch_cmds.ev`.
```bash
#
# This is an example batch build file for Evennia.
#
# This creates a red button
@create button:tutorial_examples.red_button.RedButton
# (This comment ends input for @create)
# Next command. Let's create something.
@set button/desc =
This is a large red button. Now and then
it flashes in an evil, yet strangely tantalizing way.
A big sign sits next to it. It says:
-----------
Press me!
-----------
... It really begs to be pressed! You
know you want to!
# This inserts the commands from another batch-cmd file named
# batch_insert_file.ev.
#INSERT examples.batch_insert_file
# (This ends the @set command). Note that single line breaks
# and extra whitespace in the argument are ignored. Empty lines
# translate into line breaks in the output.
# Now let's place the button where it belongs (let's say limbo #2 is
# the evil lair in our example)
@teleport #2
# (This comments ends the @teleport command.)
# Now we drop it so others can see it.
# The very last command in the file needs not be ended with #.
drop button
```
To test this, run `@batchcommand` on the file:
> @batchcommand contrib.tutorial_examples.batch_cmds
A button will be created, described and dropped in Limbo. All commands will be executed by the user
calling the command.
> Note that if you interact with the button, you might find that its description changes, loosing
your custom-set description above. This is just the way this particular object works.
## Interactive mode
Interactive mode allows you to more step-wise control over how the batch file is executed. This is
useful for debugging and also if you have a large batch file and is only updating a small part of it
-- running the entire file again would be a waste of time (and in the case of `@create`-ing objects
you would to end up with multiple copies of same-named objects, for example). Use `@batchcommand`
with the `/interactive` flag to enter interactive mode.
> @batchcommand/interactive tutorial_examples.batch_cmds
You will see this:
01/04: @create button:tutorial_examples.red_button.RedButton (hh for help)
This shows that you are on the `@create` command, the first out of only four commands in this batch
file. Observe that the command `@create` has *not* been actually processed at this point!
To take a look at the full command you are about to run, use `ll` (a batch-processor version of
`look`). Use `pp` to actually process the current command (this will actually `@create` the button)
-- and make sure it worked as planned. Use `nn` (next) to go to the next command. Use `hh` for a
list of commands.
If there are errors, fix them in the batch file, then use `rr` to reload the file. You will still be
at the same command and can rerun it easily with `pp` as needed. This makes for a simple debug
cycle. It also allows you to rerun individual troublesome commands - as mentioned, in a large batch
file this can be very useful. Do note that in many cases, commands depend on the previous ones (e.g.
if `@create` in the example above had failed, the following commands would have had nothing to
operate on).
Use `nn` and `bb` (next and back) to step through the file; e.g. `nn 12` will jump 12 steps forward
(without processing any command in between). All normal commands of Evennia should work too while
working in interactive mode.
## Limitations and Caveats
The batch-command processor is great for automating smaller builds or for testing new commands and
objects repeatedly without having to write so much. There are several caveats you have to be aware
of when using the batch-command processor for building larger, complex worlds though.
The main issue is that when you run a batch-command script you (*you*, as in your superuser
character) are actually moving around in the game creating and building rooms in sequence, just as
if you had been entering those commands manually, one by one. You have to take this into account
when creating the file, so that you can 'walk' (or teleport) to the right places in order.
This also means there are several pitfalls when designing and adding certain types of objects. Here
are some examples:
- *Rooms that change your [Command Set](./Command-Sets)*: Imagine that you build a 'dark' room, which
severely limits the cmdsets of those entering it (maybe you have to find the light switch to
proceed). In your batch script you would create this room, then teleport to it - and promptly be
shifted into the dark state where none of your normal build commands work ...
- *Auto-teleportation*: Rooms that automatically teleport those that enter them to another place
(like a trap room, for example). You would be teleported away too.
- *Mobiles*: If you add aggressive mobs, they might attack you, drawing you into combat. If they
have AI they might even follow you around when building - or they might move away from you before
you've had time to finish describing and equipping them!
The solution to all these is to plan ahead. Make sure that superusers are never affected by whatever
effects are in play. Add an on/off switch to objects and make sure it's always set to *off* upon
creation. It's all doable, one just needs to keep it in mind.
## Assorted notes
The fact that you build as 'yourself' can also be considered an advantage however, should you ever
decide to change the default command to allow others than superusers to call the processor. Since
normal access-checks are still performed, a malevolent builder with access to the processor should
not be able to do all that much damage (this is the main drawback of the [Batch Code
Processor](Batch-Code-Processor))
- [GNU Emacs](https://www.gnu.org/software/emacs/) users might find it interesting to use emacs'
*evennia mode*. This is an Emacs major mode found in `evennia/utils/evennia-mode.el`. It offers
correct syntax highlighting and indentation with `<tab>` when editing `.ev` files in Emacs. See the
header of that file for installation instructions.
- [VIM](http://www.vim.org/) users can use amfl's [vim-evennia](https://github.com/amfl/vim-evennia)
mode instead, see its readme for install instructions.

View file

@ -0,0 +1,82 @@
# Batch Processors
Building a game world is a lot of work, especially when starting out. Rooms should be created,
descriptions have to be written, objects must be detailed and placed in their proper places. In many
traditional MUD setups you had to do all this online, line by line, over a telnet session.
Evennia already moves away from much of this by shifting the main coding work to external Python
modules. But also building would be helped if one could do some or all of it externally. Enter
Evennia's *batch processors* (there are two of them). The processors allows you, as a game admin, to
build your game completely offline in normal text files (*batch files*) that the processors
understands. Then, when you are ready, you use the processors to read it all into Evennia (and into
the database) in one go.
You can of course still build completely online should you want to - this is certainly the easiest
way to go when learning and for small build projects. But for major building work, the advantages of
using the batch-processors are many:
- It's hard to compete with the comfort of a modern desktop text editor; Compared to a traditional
MUD line input, you can get much better overview and many more features. Also, accidentally pressing
Return won't immediately commit things to the database.
- You might run external spell checkers on your batch files. In the case of one of the batch-
processors (the one that deals with Python code), you could also run external debuggers and code
analyzers on your file to catch problems before feeding it to Evennia.
- The batch files (as long as you keep them) are records of your work. They make a natural starting
point for quickly re-building your world should you ever decide to start over.
- If you are an Evennia developer, using a batch file is a fast way to setup a test-game after
having reset the database.
- The batch files might come in useful should you ever decide to distribute all or part of your
world to others.
There are two batch processors, the Batch-*command* processor and the Batch-*code* processor. The
first one is the simpler of the two. It doesn't require any programming knowledge - you basically
just list in-game commands in a text file. The code-processor on the other hand is much more
powerful but also more complex - it lets you use Evennia's API to code your world in full-fledged
Python code.
- The [Batch Command Processor](./Batch-Command-Processor)
- The [Batch Code Processor](./Batch-Code-Processor)
If you plan to use international characters in your batchfiles you are wise to read about *file
encodings* below.
## A note on File Encodings
As mentioned, both the processors take text files as input and then proceed to process them. As long
as you stick to the standard [ASCII](http://en.wikipedia.org/wiki/Ascii) character set (which means
the normal English characters, basically) you should not have to worry much about this section.
Many languages however use characters outside the simple `ASCII` table. Common examples are various
apostrophes and umlauts but also completely different symbols like those of the greek or cyrillic
alphabets.
First, we should make it clear that Evennia itself handles international characters just fine. It
(and Django) uses [unicode](http://en.wikipedia.org/wiki/Unicode) strings internally.
The problem is that when reading a text file like the batchfile, we need to know how to decode the
byte-data stored therein to universal unicode. That means we need an *encoding* (a mapping) for how
the file stores its data. There are many, many byte-encodings used around the world, with opaque
names such as `Latin-1`, `ISO-8859-3` or `ARMSCII-8` to pick just a few examples. Problem is that
it's practially impossible to determine which encoding was used to save a file just by looking at it
(it's just a bunch of bytes!). You have to *know*.
With this little introduction it should be clear that Evennia can't guess but has to *assume* an
encoding when trying to load a batchfile. The text editor and Evennia must speak the same "language"
so to speak. Evennia will by default first try the international `UTF-8` encoding, but you can have
Evennia try any sequence of different encodings by customizing the `ENCODINGS` list in your settings
file. Evennia will use the first encoding in the list that do not raise any errors. Only if none
work will the server give up and return an error message.
You can often change the text editor encoding (this depends on your editor though), otherwise you
need to add the editor's encoding to Evennia's `ENCODINGS` list. If you are unsure, write a test
file with lots of non-ASCII letters in the editor of your choice, then import to make sure it works
as it should.
More help with encodings can be found in the entry [Text Encodings](./Text-Encodings) and also in the
Wikipedia article [here](http://en.wikipedia.org/wiki/Text_encodings).
**A footnote for the batch-code processor**: Just because *Evennia* can parse your file and your
fancy special characters, doesn't mean that *Python* allows their use. Python syntax only allows
international characters inside *strings*. In all other source code only `ASCII` set characters are
allowed.

View file

@ -0,0 +1,101 @@
# Bootstrap & Evennia
# What is Bootstrap?
Evennia's new default web page uses a framework called [Bootstrap](https://getbootstrap.com/). This
framework is in use across the internet - you'll probably start to recognize its influence once you
learn some of the common design patterns. This switch is great for web developers, perhaps like
yourself, because instead of wondering about setting up different grid systems or what custom class
another designer used, we have a base, a bootstrap, to work from. Bootstrap is responsive by
default, and comes with some default styles that Evennia has lightly overrode to keep some of the
same colors and styles you're used to from the previous design.
For your reading pleasure, a brief overview of Bootstrap follows. For more in-depth info, please
read [the documentation](https://getbootstrap.com/docs/4.0/getting-started/introduction/).
***
## The Layout System
Other than the basic styling Bootstrap includes, it also includes [a built in layout and grid
system](https://getbootstrap.com/docs/4.0/layout/overview/).
The first part of this system is [the
container](https://getbootstrap.com/docs/4.0/layout/overview/#containers).
The container is meant to hold all your page content. Bootstrap provides two types: fixed-width and
full-width.
Fixed-width containers take up a certain max-width of the page - they're useful for limiting the
width on Desktop or Tablet platforms, instead of making the content span the width of the page.
```
<div class="container">
<!--- Your content here -->
</div>
```
Full width containers take up the maximum width available to them - they'll span across a wide-
screen desktop or a smaller screen phone, edge-to-edge.
```
<div class="container-fluid">
<!--- This content will span the whole page -->
</div>
```
The second part of the layout system is [the grid](https://getbootstrap.com/docs/4.0/layout/grid/).
This is the bread-and-butter of the layout of Bootstrap - it allows you to change the size of
elements depending on the size of the screen, without writing any media queries. We'll briefly go
over it - to learn more, please read the docs or look at the source code for Evennia's home page in
your browser.
> Important! Grid elements should be in a .container or .container-fluid. This will center the
contents of your site.
Bootstrap's grid system allows you to create rows and columns by applying classes based on
breakpoints. The default breakpoints are extra small, small, medium, large, and extra-large. If
you'd like to know more about these breakpoints, please [take a look at the documentation for
them.](https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints)
To use the grid system, first create a container for your content, then add your rows and columns
like so:
```
<div class="container">
<div class="row">
<div class="col">
1 of 3
</div>
<div class="col">
2 of 3
</div>
<div class="col">
3 of 3
</div>
</div>
</div>
```
This layout would create three equal-width columns.
To specify your sizes - for instance, Evennia's default site has three columns on desktop and
tablet, but reflows to single-column on smaller screens. Try it out!
```
<div class="container">
<div class="row">
<div class="col col-md-6 col-lg-3">
1 of 4
</div>
<div class="col col-md-6 col-lg-3">
2 of 4
</div>
<div class="col col-md-6 col-lg-3">
3 of 4
</div>
<div class="col col-md-6 col-lg-3">
4 of 4
</div>
</div>
</div>
```
This layout would be 4 columns on large screens, 2 columns on medium screens, and 1 column on
anything smaller.
To learn more about Bootstrap's grid, please [take a look at the
docs](https://getbootstrap.com/docs/4.0/layout/grid/)
***
## More Bootstrap
Bootstrap also provides a huge amount of utilities, as well as styling and content elements. To
learn more about them, please [read the Bootstrap docs](https://getbootstrap.com/docs/4.0/getting-
started/introduction/) or read one of our other web tutorials.

View file

@ -0,0 +1,82 @@
# Bootstrap Components and Utilities
Bootstrap provides many utilities and components you can use when customizing Evennia's web
presence. We'll go over a few examples here that you might find useful.
> Please take a look at either [the basic web tutorial](./Add-a-simple-new-web-page) or [the web
character view tutorial](Web-Character-View-Tutorial)
> to get a feel for how to add pages to Evennia's website to test these examples.
## General Styling
Bootstrap provides base styles for your site. These can be customized through CSS, but the default
styles are intended to provide a consistent, clean look for sites.
### Color
Most elements can be styled with default colors. [Take a look at the
documentation](https://getbootstrap.com/docs/4.0/utilities/colors/) to learn more about these colors
- suffice to say, adding a class of text-* or bg-*, for instance, text-primary, sets the text color
or background color.
### Borders
Simply adding a class of 'border' to an element adds a border to the element. For more in-depth
info, please [read the documentation on
borders.](https://getbootstrap.com/docs/4.0/utilities/borders/).
```
<span class="border border-dark"></span>
```
You can also easily round corners just by adding a class.
```
<img src="..." class="rounded" />
```
### Spacing
Bootstrap provides classes to easily add responsive margin and padding. Most of the time, you might
like to add margins or padding through CSS itself - however these classes are used in the default
Evennia site. [Take a look at the docs](https://getbootstrap.com/docs/4.0/utilities/spacing/) to
learn more.
***
## Components
### Buttons
[Buttons](https://getbootstrap.com/docs/4.0/components/buttons/) in Bootstrap are very easy to use -
button styling can be added to `<button>`, `<a>`, and `<input>` elements.
```
<a class="btn btn-primary" href="#" role="button">I'm a Button</a>
<button class="btn btn-primary" type="submit">Me too!</button>
<input class="btn btn-primary" type="button" value="Button">
<input class="btn btn-primary" type="submit" value="Also a Button">
<input class="btn btn-primary" type="reset" value="Button as Well">
```
### Cards
[Cards](https://getbootstrap.com/docs/4.0/components/card/) provide a container for other elements
that stands out from the rest of the page. The "Accounts", "Recently Connected", and "Database
Stats" on the default webpage are all in cards. Cards provide quite a bit of formatting options -
the following is a simple example, but read the documentation or look at the site's source for more.
```
<div class="card">
<div class="card-body">
<h4 class="card-title">Card title</h4>
<h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
<p class="card-text">Fancy, isn't it?</p>
<a href="#" class="card-link">Card link</a>
</div>
</div>
```
### Jumbotron
[Jumbotrons](https://getbootstrap.com/docs/4.0/components/jumbotron/) are useful for featuring an
image or tagline for your game. They can flow with the rest of your content or take up the full
width of the page - Evennia's base site uses the former.
```
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-3">Full Width Jumbotron</h1>
<p class="lead">Look at the source of the default Evennia page for a regular Jumbotron</p>
</div>
</div>
```
### Forms
[Forms](https://getbootstrap.com/docs/4.0/components/forms/) are highly customizable with Bootstrap.
For a more in-depth look at how to use forms and their styles in your own Evennia site, please read
over [the web character gen tutorial.](./Web-Character-Generation)

View file

@ -0,0 +1,43 @@
# Builder Docs
This section contains information useful to world builders.
### Building basics
- [Default in-game commands](api:evennia.commands.default)
- [Building Quick-start](./Building-Quickstart)
- [Giving build permissions to others](./Building-Permissions)
- [Adding text tags](./TextTags)
- [Colored text](./TextTags#coloured-text)
- [Clickable links](./TextTags#clickable-links)
- [Inline functions](./TextTags#inline-functions)
- [Customizing the connection screen](./Connection-Screen)
### Advanced building and World building
- [Overview of batch processors](./Batch-Processors)
- [Batch-command processor](./Batch-Command-Processor)
- [Batch-code processor](./Batch-Code-Processor)
- [Using the Spawner for individualizing objects](./Spawner-and-Prototypes)
- [Adding Zones](./Zones)
### The Tutorial world
- [Introduction and setup](./Tutorial-World-Introduction)
```toctree::
:hidden:
Building-Quickstart
Building-Permissions
TextTags
Connection-Screen
Batch-Processors
Batch-Command-Processor
Batch-Code-Processor
Spawner-and-Prototypes
Zones
Tutorial-World-Introduction
```

View file

@ -0,0 +1,72 @@
# Building Permissions
*OBS: This gives only a brief introduction to the access system. Locks and permissions are fully
detailed* [here](./Locks).
## The super user
There are strictly speaking two types of users in Evennia, the *super user* and everyone else. The
superuser is the first user you create, object `#1`. This is the all-powerful server-owner account.
Technically the superuser not only has access to everything, it *bypasses* the permission checks
entirely. This makes the superuser impossible to lock out, but makes it unsuitable to actually play-
test the game's locks and restrictions with (see `@quell` below). Usually there is no need to have
but one superuser.
## Assigning permissions
Whereas permissions can be used for anything, those put in `settings.PERMISSION_HIERARCHY` will have
a ranking relative each other as well. We refer to these types of permissions as *hierarchical
permissions*. When building locks to check these permissions, the `perm()` [lock function](./Locks) is
used. By default Evennia creates the following hierarchy (spelled exactly like this):
1. **Developers** basically have the same access as superusers except that they do *not* sidestep
the Permission system. Assign only to really trusted server-admin staff since this level gives
access both to server reload/shutdown functionality as well as (and this may be more critical) gives
access to the all-powerful `@py` command that allows the execution of arbitrary Python code on the
command line.
1. **Admins** can do everything *except* affecting the server functions themselves. So an Admin
couldn't reload or shutdown the server for example. They also cannot execute arbitrary Python code
on the console or import files from the hard drive.
1. **Builders** - have all the build commands, but cannot affect other accounts or mess with the
server.
1. **Helpers** are almost like a normal *Player*, but they can also add help files to the database.
1. **Players** is the default group that new players end up in. A new player have permission to use
tells and to use and create new channels.
A user having a certain level of permission automatically have access to locks specifying access of
a lower level.
To assign a new permission from inside the game, you need to be able to use the `@perm` command.
This is an *Developer*-level command, but it could in principle be made lower-access since it only
allows assignments equal or lower to your current level (so you cannot use it to escalate your own
permission level). So, assuming you yourself have *Developer* access (or is superuser), you assign
a new account "Tommy" to your core staff with the command
@perm/account Tommy = Developer
or
@perm *Tommy = Developer
We use a switch or the `*name` format to make sure to put the permission on the *Account* and not on
any eventual *Character* that may also be named "Tommy". This is usually what you want since the
Account will then remain an Developer regardless of which Character they are currently controlling.
To limit permission to a per-Character level you should instead use *quelling* (see below). Normally
permissions can be any string, but for these special hierarchical permissions you can also use
plural ("Developer" and "Developers" both grant the same powers).
## Quelling your permissions
When developing it can be useful to check just how things would look had your permission-level been
lower. For this you can use *quelling*. Normally, when you puppet a Character you are using your
Account-level permission. So even if your Character only has *Accounts* level permissions, your
*Developer*-level Account will take precedence. With the `@quell` command you can change so that the
Character's permission takes precedence instead:
@quell
This will allow you to test out the game using the current Character's permission level. A developer
or builder can thus in principle maintain several test characters, all using different permission
levels. Note that you cannot escalate your permissions this way; If the Character happens to have a
*higher* permission level than the Account, the *Account's* (lower) permission will still be used.

View file

@ -0,0 +1,274 @@
# Building Quickstart
The [default command](./Default-Command-Help) definitions coming with Evennia
follows a style [similar](./Using-MUX-as-a-Standard) to that of MUX, so the
commands should be familiar if you used any such code bases before.
> Throughout the larger documentation you may come across commands prefixed
> with `@`. This is just an optional marker used in some places to make a
> command stand out. Evennia defaults to ignoring the use of `@` in front of
> your command (so entering `dig` is the same as entering `@dig`).
The default commands have the following style (where `[...]` marks optional parts):
command[/switch/switch...] [arguments ...]
A _switch_ is a special, optional flag to the command to make it behave differently. It is always
put directly after the command name, and begins with a forward slash (`/`). The _arguments_ are one
or more inputs to the commands. It's common to use an equal sign (`=`) when assigning something to
an object.
Below are some examples of commands you can try when logged in to the game. Use `help <command>` for
learning more about each command and their detailed options.
## Stepping Down From Godhood
If you just installed Evennia, your very first player account is called user #1, also known as the
_superuser_ or _god user_. This user is very powerful, so powerful that it will override many game
restrictions such as locks. This can be useful, but it also hides some functionality that you might
want to test.
To temporarily step down from your superuser position you can use the `quell` command in-game:
quell
This will make you start using the permission of your current character's level instead of your
superuser level. If you didn't change any settings your game Character should have an _Developer_
level permission - high as can be without bypassing locks like the superuser does. This will work
fine for the examples on this page. Use `unquell` to get back to superuser status again afterwards.
## Creating an Object
Basic objects can be anything -- swords, flowers and non-player characters. They are created using
the `create` command:
create box
This created a new 'box' (of the default object type) in your inventory. Use the command `inventory`
(or `i`) to see it. Now, 'box' is a rather short name, let's rename it and tack on a few aliases.
name box = very large box;box;very;crate
We now renamed the box to _very large box_ (and this is what we will see when looking at it), but we
will also recognize it by any of the other names we give - like _crate_ or simply _box_ as before.
We could have given these aliases directly after the name in the `create` command, this is true for
all creation commands - you can always tag on a list of `;`-separated aliases to the name of your
new object. If you had wanted to not change the name itself, but to only add aliases, you could have
used the `alias` command.
We are currently carrying the box. Let's drop it (there is also a short cut to create and drop in
one go by using the `/drop` switch, for example `create/drop box`).
drop box
Hey presto - there it is on the ground, in all its normality.
examine box
This will show some technical details about the box object. For now we will ignore what this
information means.
Try to `look` at the box to see the (default) description.
look box
You see nothing special.
The description you get is not very exciting. Let's add some flavor.
describe box = This is a large and very heavy box.
If you try the `get` command we will pick up the box. So far so good, but if we really want this to
be a large and heavy box, people should _not_ be able to run off with it that easily. To prevent
this we need to lock it down. This is done by assigning a _Lock_ to it. Make sure the box was
dropped in the room, then try this:
lock box = get:false()
Locks represent a rather [big topic](./Locks), but for now that will do what we want. This will lock
the box so noone can lift it. The exception is superusers, they override all locks and will pick it
up anyway. Make sure you are quelling your superuser powers and try to get the box now:
> get box
You can't get that.
Think thís default error message looks dull? The `get` command looks for an [Attribute](./Attributes)
named `get_err_msg` for returning a nicer error message (we just happen to know this, you would need
to peek into the
[code](https://github.com/evennia/evennia/blob/master/evennia/commands/default/general.py#L235) for
the `get` command to find out.). You set attributes using the `set` command:
set box/get_err_msg = It's way too heavy for you to lift.
Try to get it now and you should see a nicer error message echoed back to you. To see what this
message string is in the future, you can use 'examine.'
examine box/get_err_msg
Examine will return the value of attributes, including color codes. `examine here/desc` would return
the raw description of your current room (including color codes), so that you can copy-and-paste to
set its description to something else.
You create new Commands (or modify existing ones) in Python outside the game. See the [Adding
Commands tutorial](Adding-Command-Tutorial) for help with creating your first own Command.
## Get a Personality
[Scripts](./Scripts) are powerful out-of-character objects useful for many "under the hood" things.
One of their optional abilities is to do things on a timer. To try out a first script, let's put one
on ourselves. There is an example script in `evennia/contrib/tutorial_examples/bodyfunctions.py`
that is called `BodyFunctions`. To add this to us we will use the `script` command:
script self = tutorial_examples.bodyfunctions.BodyFunctions
(note that you don't have to give the full path as long as you are pointing to a place inside the
`contrib` directory, it's one of the places Evennia looks for Scripts). Wait a while and you will
notice yourself starting making random observations.
script self
This will show details about scripts on yourself (also `examine` works). You will see how long it is
until it "fires" next. Don't be alarmed if nothing happens when the countdown reaches zero - this
particular script has a randomizer to determine if it will say something or not. So you will not see
output every time it fires.
When you are tired of your character's "insights", kill the script with
script/stop self = tutorial_examples.bodyfunctions.BodyFunctions
You create your own scripts in Python, outside the game; the path you give to `script` is literally
the Python path to your script file. The [Scripts](./Scripts) page explains more details.
## Pushing Your Buttons
If we get back to the box we made, there is only so much fun you can do with it at this point. It's
just a dumb generic object. If you renamed it to `stone` and changed its description noone would be
the wiser. However, with the combined use of custom [Typeclasses](./Typeclasses), [Scripts](./Scripts)
and object-based [Commands](./Commands), you could expand it and other items to be as unique, complex
and interactive as you want.
Let's take an example. So far we have only created objects that use the default object typeclass
named simply `Object`. Let's create an object that is a little more interesting. Under
`evennia/contrib/tutorial_examples` there is a module `red_button.py`. It contains the enigmatic
`RedButton` typeclass.
Let's make us one of _those_!
create/drop button:tutorial_examples.red_button.RedButton
We import the RedButton python class the same way you would import it in Python except Evennia makes
sure to look in`evennia/contrib/` so you don't have to write the full path every time. There you go
- one red button.
The RedButton is an example object intended to show off a few of Evennia's features. You will find
that the [Typeclass](./Typeclasses) and [Commands](./Commands) controlling it are inside
`evennia/contrib/tutorial_examples/`.
If you wait for a while (make sure you dropped it!) the button will blink invitingly. Why don't you
try to push it ...? Surely a big red button is meant to be pushed. You know you want to.
## Making Yourself a House
The main command for shaping the game world is `dig`. For example, if you are standing in Limbo you
can dig a route to your new house location like this:
dig house = large red door;door;in,to the outside;out
This will create a new room named 'house'. Spaces at the start/end of names and aliases are ignored
so you could put more air if you wanted. This call will directly create an exit from your current
location named 'large red door' and a corresponding exit named 'to the outside' in the house room
leading back to Limbo. We also define a few aliases to those exits, so people don't have to write
the full thing all the time.
If you wanted to use normal compass directions (north, west, southwest etc), you could do that with
`dig` too. But Evennia also has a limited version of `dig` that helps for compass directions (and
also up/down and in/out). It's called `tunnel`:
tunnel sw = cliff
This will create a new room "cliff" with an exit "southwest" leading there and a path "northeast"
leading back from the cliff to your current location.
You can create new exits from where you are using the `open` command:
open north;n = house
This opens an exit `north` (with an alias `n`) to the previously created room `house`.
If you have many rooms named `house` you will get a list of matches and have to select which one you
want to link to. You can also give its database (#dbref) number, which is unique to every object.
This can be found with the `examine` command or by looking at the latest constructions with
`objects`.
Follow the north exit to your 'house' or `teleport` to it:
north
or:
teleport house
To manually open an exit back to Limbo (if you didn't do so with the `dig` command):
open door = limbo
(or give limbo's dbref which is #2)
## Reshuffling the World
You can find things using the `find` command. Assuming you are back at `Limbo`, let's teleport the
_large box to our house_.
> teleport box = house
very large box is leaving Limbo, heading for house.
Teleported very large box -> house.
We can still find the box by using find:
> find box
One Match(#1-#8):
very large box(#8) - src.objects.objects.Object
Knowing the `#dbref` of the box (#8 in this example), you can grab the box and get it back here
without actually yourself going to `house` first:
teleport #8 = here
(You can usually use `here` to refer to your current location. To refer to yourself you can use
`self` or `me`). The box should now be back in Limbo with you.
We are getting tired of the box. Let's destroy it.
destroy box
You can destroy many objects in one go by giving a comma-separated list of objects (or their
#dbrefs, if they are not in the same location) to the command.
## Adding a Help Entry
An important part of building is keeping the help files updated. You can add, delete and append to
existing help entries using the `sethelp` command.
sethelp/add MyTopic = This help topic is about ...
## Adding a World
After this brief introduction to building you may be ready to see a more fleshed-out example.
Evennia comes with a tutorial world for you to explore.
First you need to switch back to _superuser_ by using the `unquell` command. Next, place yourself in
`Limbo` and run the following command:
batchcommand tutorial_world.build
This will take a while (be patient and don't re-run the command). You will see all the commands used
to build the world scroll by as the world is built for you.
You will end up with a new exit from Limbo named _tutorial_. Apart from being a little solo-
adventure in its own right, the tutorial world is a good source for learning Evennia building (and
coding).
Read [the batch
file](https://github.com/evennia/evennia/blob/master/evennia/contrib/tutorial_world/build.ev) to see
exactly how it's built, step by step. See also more info about the tutorial world [here](Tutorial-
World-Introduction).

View file

@ -0,0 +1,236 @@
# Building a mech tutorial
> This page was adapted from the article "Building a Giant Mech in Evennia" by Griatch, published in
Imaginary Realities Volume 6, issue 1, 2014. The original article is no longer available online,
this is a version adopted to be compatible with the latest Evennia.
## Creating the Mech
Let us create a functioning giant mech using the Python MUD-creation system Evennia. Everyone likes
a giant mech, right? Start in-game as a character with build privileges (or the superuser).
@create/drop Giant Mech ; mech
Boom. We created a Giant Mech Object and dropped it in the room. We also gave it an alias *mech*.
Lets describe it.
@desc mech = This is a huge mech. It has missiles and stuff.
Next we define who can “puppet” the mech object.
@lock mech = puppet:all()
This makes it so that everyone can control the mech. More mechs to the people! (Note that whereas
Evennias default commands may look vaguely MUX-like, you can change the syntax to look like
whatever interface style you prefer.)
Before we continue, lets make a brief detour. Evennia is very flexible about its objects and even
more flexible about using and adding commands to those objects. Here are some ground rules well
worth remembering for the remainder of this article:
- The [Account](./Accounts) represents the real person logging in and has no game-world existence.
- Any [Object](./Objects) can be puppeted by an Account (with proper permissions).
- [Characters](./Objects#characters), [Rooms](./Objects#rooms), and [Exits](./Objects#exits) are just
children of normal Objects.
- Any Object can be inside another (except if it creates a loop).
- Any Object can store custom sets of commands on it. Those commands can:
- be made available to the puppeteer (Account),
- be made available to anyone in the same location as the Object, and
- be made available to anyone “inside” the Object
- Also Accounts can store commands on themselves. Account commands are always available unless
commands on a puppeted Object explicitly override them.
In Evennia, using the `@ic` command will allow you to puppet a given Object (assuming you have
puppet-access to do so). As mentioned above, the bog-standard Character class is in fact like any
Object: it is auto-puppeted when logging in and just has a command set on it containing the normal
in-game commands, like look, inventory, get and so on.
@ic mech
You just jumped out of your Character and *are* now the mech! If people look at you in-game, they
will look at a mech. The problem at this point is that the mech Object has no commands of its own.
The usual things like look, inventory and get sat on the Character object, remember? So at the
moment the mech is not quite as cool as it could be.
@ic <Your old Character>
You just jumped back to puppeting your normal, mundane Character again. All is well.
> (But, you ask, where did that `@ic` command come from, if the mech had no commands on it? The
answer is that it came from the Account's command set. This is important. Without the Account being
the one with the `@ic` command, we would not have been able to get back out of our mech again.)
### Arming the Mech
Let us make the mech a little more interesting. In our favorite text editor, we will create some new
mech-suitable commands. In Evennia, commands are defined as Python classes.
```python
# in a new file mygame/commands/mechcommands.py
from evennia import Command
class CmdShoot(Command):
"""
Firing the mechs gun
Usage:
shoot [target]
This will fire your mechs main gun. If no
target is given, you will shoot in the air.
"""
key = "shoot"
aliases = ["fire", "fire!"]
def func(self):
"This actually does the shooting"
caller = self.caller
location = caller.location
if not self.args:
# no argument given to command - shoot in the air
message = "BOOM! The mech fires its gun in the air!"
location.msg_contents(message)
return
# we have an argument, search for target
target = caller.search(self.args.strip())
if target:
message = "BOOM! The mech fires its gun at %s" % target.key
location.msg_contents(message)
class CmdLaunch(Command):
# make your own 'launch'-command here as an exercise!
# (it's very similar to the 'shoot' command above).
```
This is saved as a normal Python module (lets call it `mechcommands.py`), in a place Evennia looks
for such modules (`mygame/commands/`). This command will trigger when the player gives the command
“shoot”, “fire,” or even “fire!” with an exclamation mark. The mech can shoot in the air or at a
target if you give one. In a real game the gun would probably be given a chance to hit and give
damage to the target, but this is enough for now.
We also make a second command for launching missiles (`CmdLaunch`). To save
space we wont describe it here; it looks the same except it returns a text
about the missiles being fired and has different `key` and `aliases`. We leave
that up to you to create as an exercise. You could have it print "WOOSH! The
mech launches missiles against <target>!", for example.
Now we shove our commands into a command set. A [Command Set](./Command-Sets) (CmdSet) is a container
holding any number of commands. The command set is what we will store on the mech.
```python
# in the same file mygame/commands/mechcommands.py
from evennia import CmdSet
from evennia import default_cmds
class MechCmdSet(CmdSet):
"""
This allows mechs to do do mech stuff.
"""
key = "mechcmdset"
def at_cmdset_creation(self):
"Called once, when cmdset is first created"
self.add(CmdShoot())
self.add(CmdLaunch())
```
This simply groups all the commands we want. We add our new shoot/launch commands. Lets head back
into the game. For testing we will manually attach our new CmdSet to the mech.
@py self.search("mech").cmdset.add("commands.mechcommands.MechCmdSet")
This is a little Python snippet (run from the command line as an admin) that searches for the mech
in our current location and attaches our new MechCmdSet to it. What we add is actually the Python
path to our cmdset class. Evennia will import and initialize it behind the scenes.
@ic mech
We are back as the mech! Lets do some shooting!
fire!
BOOM! The mech fires its gun in the air!
There we go, one functioning mech. Try your own `launch` command and see that it works too. We can
not only walk around as the mech — since the CharacterCmdSet is included in our MechCmdSet, the mech
can also do everything a Character could do, like look around, pick up stuff, and have an inventory.
We could now shoot the gun at a target or try the missile launch command. Once you have your own
mech, what else do you need?
> Note: You'll find that the mech's commands are available to you by just standing in the same
location (not just by puppeting it). We'll solve this with a *lock* in the next section.
## Making a Mech production line
What weve done so far is just to make a normal Object, describe it and put some commands on it.
This is great for testing. The way we added it, the MechCmdSet will even go away if we reload the
server. Now we want to make the mech an actual object “type” so we can create mechs without those
extra steps. For this we need to create a new Typeclass.
A [Typeclass](./Typeclasses) is a near-normal Python class that stores its existence to the database
behind the scenes. A Typeclass is created in a normal Python source file:
```python
# in the new file mygame/typeclasses/mech.py
from typeclasses.objects import Object
from commands.mechcommands import MechCmdSet
from evennia import default_cmds
class Mech(Object):
"""
This typeclass describes an armed Mech.
"""
def at_object_creation(self):
"This is called only when object is first created"
self.cmdset.add_default(default_cmds.CharacterCmdSet)
self.cmdset.add(MechCmdSet, permanent=True)
self.locks.add("puppet:all();call:false()")
self.db.desc = "This is a huge mech. It has missiles and stuff."
```
For convenience we include the full contents of the default `CharacterCmdSet` in there. This will
make a Characters normal commands available to the mech. We also add the mech-commands from before,
making sure they are stored persistently in the database. The locks specify that anyone can puppet
the meck and no-one can "call" the mech's Commands from 'outside' it - you have to puppet it to be
able to shoot.
Thats it. When Objects of this type are created, they will always start out with the mechs command
set and the correct lock. We set a default description, but you would probably change this with
`@desc` to individualize your mechs as you build them.
Back in the game, just exit the old mech (`@ic` back to your old character) then do
@create/drop The Bigger Mech ; bigmech : mech.Mech
We create a new, bigger mech with an alias bigmech. Note how we give the python-path to our
Typeclass at the end — this tells Evennia to create the new object based on that class (we don't
have to give the full path in our game dir `typeclasses.mech.Mech` because Evennia knows to look in
the `typeclasses` folder already). A shining new mech will appear in the room! Just use
@ic bigmech
to take it on a test drive.
## Future Mechs
To expand on this you could add more commands to the mech and remove others. Maybe the mech
shouldnt work just like a Character after all. Maybe it makes loud noises every time it passes from
room to room. Maybe it cannot pick up things without crushing them. Maybe it needs fuel, ammo and
repairs. Maybe youll lock it down so it can only be puppeted by emo teenagers.
Having you puppet the mech-object directly is also just one way to implement a giant mech in
Evennia.
For example, you could instead picture a mech as a “vehicle” that you “enter” as your normal
Character (since any Object can move inside another). In that case the “insides” of the mech Object
could be the “cockpit”. The cockpit would have the `MechCommandSet` stored on itself and all the
shooting goodness would be made available to you only when you enter it.
And of course you could put more guns on it. And make it fly.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,249 @@
# Choosing An SQL Server
This page gives an overview of the supported SQL databases as well as instructions on install:
- SQLite3 (default)
- PostgreSQL
- MySQL / MariaDB
Since Evennia uses [Django](http://djangoproject.com), most of our notes are based off of what we
know from the community and their documentation. While the information below may be useful, you can
always find the most up-to-date and "correct" information at Django's [Notes about supported
Databases](http://docs.djangoproject.com/en/dev/ref/databases/#ref-databases) page.
## SQLite3
[SQLite3](https://sqlite.org/) is a light weight single-file database. It is our default database
and Evennia will set this up for you automatically if you give no other options. SQLite stores the
database in a single file (`mygame/server/evennia.db3`). This means it's very easy to reset this
database - just delete (or move) that `evennia.db3` file and run `evennia migrate` again! No server
process is needed and the administrative overhead and resource consumption is tiny. It is also very
fast since it's run in-memory. For the vast majority of Evennia installs it will probably be all
that's ever needed.
SQLite will generally be much faster than MySQL/PostgreSQL but its performance comes with two
drawbacks:
* SQLite [ignores length constraints by design](https://www.sqlite.org/faq.html#q9); it is possible
to store very large strings and numbers in fields that technically should not accept them. This is
not something you will notice; your game will read and write them and function normally, but this
*can* create some data migration problems requiring careful thought if you do need to change
databases later.
* SQLite can scale well to storage of millions of objects, but if you end up with a thundering herd
of users trying to access your MUD and web site at the same time, or you find yourself writing long-
running functions to update large numbers of objects on a live game, either will yield errors and
interference. SQLite does not work reliably with multiple concurrent threads or processes accessing
its records. This has to do with file-locking clashes of the database file. So for a production
server making heavy use of process- or thread pools (or when using a third-party webserver like
Apache), a proper database is a more appropriate choice.
### Install of SQlite3
This is installed and configured as part of Evennia. The database file is created as
`mygame/server/evennia.db3` when you run
evennia migrate
without changing any database options. An optional requirement is the `sqlite3` client program -
this is required if you want to inspect the database data manually. A shortcut for using it with the
evennia database is `evennia dbshell`. Linux users should look for the `sqlite3` package for their
distro while Mac/Windows should get the [sqlite-tools package from this
page](https://sqlite.org/download.html).
To inspect the default Evennia database (once it's been created), go to your game dir and do
```bash
sqlite3 server/evennia.db3
# or
evennia dbshell
```
This will bring you into the sqlite command line. Use `.help` for instructions and `.quit` to exit.
See [here](https://gist.github.com/vincent178/10889334) for a cheat-sheet of commands.
## PostgreSQL
[PostgreSQL](https://www.postgresql.org/) is an open-source database engine, recommended by Django.
While not as fast as SQLite for normal usage, it will scale better than SQLite, especially if your
game has an very large database and/or extensive web presence through a separate server process.
### Install and initial setup of PostgreSQL
First, install the posgresql server. Version `9.6` is tested with Evennia. Packages are readily
available for all distributions. You need to also get the `psql` client (this is called `postgresql-
client` on debian-derived systems). Windows/Mac users can [find what they need on the postgresql
download page](https://www.postgresql.org/download/). You should be setting up a password for your
database-superuser (always called `postgres`) when you install.
For interaction with Evennia you need to also install `psycopg2` to your Evennia install (`pip
install psycopg2-binary` in your virtualenv). This acts as the python bridge to the database server.
Next, start the postgres client:
```bash
psql -U postgres --password
```
> :warning: **Warning:** With the `--password` argument, Postgres should prompt you for a password.
If it won't, replace that with `-p yourpassword` instead. Do not use the `-p` argument unless you
have to since the resulting command, and your password, will be logged in the shell history.
This will open a console to the postgres service using the psql client.
On the psql command line:
```sql
CREATE USER evennia WITH PASSWORD 'somepassword';
CREATE DATABASE evennia;
-- Postgres-specific optimizations
-- https://docs.djangoproject.com/en/dev/ref/databases/#optimizing-postgresql-s-configuration
ALTER ROLE evennia SET client_encoding TO 'utf8';
ALTER ROLE evennia SET default_transaction_isolation TO 'read committed';
ALTER ROLE evennia SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE evennia TO evennia;
-- Other useful commands:
-- \l (list all databases and permissions)
-- \q (exit)
```
[Here](https://gist.github.com/Kartones/dd3ff5ec5ea238d4c546) is a cheat-sheet for psql commands.
We create a database user 'evennia' and a new database named `evennia` (you can call them whatever
you want though). We then grant the 'evennia' user full privileges to the new database so it can
read/write etc to it.
If you in the future wanted to completely wipe the database, an easy way to do is to log in as the
`postgres` superuser again, then do `DROP DATABASE evennia;`, then `CREATE` and `GRANT` steps above
again to recreate the database and grant privileges.
### Evennia PostgreSQL configuration
Edit `mygame/server/conf/secret_settings.py and add the following section:
```python
#
# PostgreSQL Database Configuration
#
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'evennia',
'USER': 'evennia',
'PASSWORD': 'somepassword',
'HOST': 'localhost',
'PORT': '' # use default
}}
```
If you used some other name for the database and user, enter those instead. Run
evennia migrate
to populate your database. Should you ever want to inspect the database directly you can from now on
also use
evennia dbshell
as a shortcut to get into the postgres command line for the right database and user.
With the database setup you should now be able to start start Evennia normally with your new
database.
## MySQL / MariaDB
[MySQL](https://www.mysql.com/) is a commonly used proprietary database system, on par with
PostgreSQL. There is an open-source alternative called [MariaDB](https://mariadb.org/) that mimics
all functionality and command syntax of the former. So this section covers both.
### Installing and initial setup of MySQL/MariaDB
First, install and setup MariaDB or MySQL for your specific server. Linux users should look for the
`mysql-server` or `mariadb-server` packages for their respective distributions. Windows/Mac users
will find what they need from the [MySQL downloads](https://www.mysql.com/downloads/) or [MariaDB
downloads](https://mariadb.org/download/) pages. You also need the respective database clients
(`mysql`, `mariadb-client`), so you can setup the database itself. When you install the server you
should usually be asked to set up the database root user and password.
You will finally also need a Python interface to allow Evennia to talk to the database. Django
recommends the `mysqlclient` one. Install this into the evennia virtualenv with `pip install
mysqlclient`.
Start the database client (this is named the same for both mysql and mariadb):
```bash
mysql -u root -p
```
You should get to enter your database root password (set this up when you installed the database
server).
Inside the database client interface:
```sql
CREATE USER 'evennia'@'localhost' IDENTIFIED BY 'somepassword';
CREATE DATABASE evennia;
ALTER DATABASE `evennia` CHARACTER SET utf8; -- note that it's `evennia` with back-ticks, not
quotes!
GRANT ALL PRIVILEGES ON evennia.* TO 'evennia'@'localhost';
FLUSH PRIVILEGES;
-- use 'exit' to quit client
```
[Here](https://gist.github.com/hofmannsven/9164408) is a mysql command cheat sheet.
Above we created a new local user and database (we called both 'evennia' here, you can name them
what you prefer). We set the character set to `utf8` to avoid an issue with prefix character length
that can pop up on some installs otherwise. Next we grant the 'evennia' user all privileges on the
`evennia` database and make sure the privileges are applied. Exiting the client brings us back to
the normal terminal/console.
> Note: If you are not using MySQL for anything else you might consider granting the 'evennia' user
full privileges with `GRANT ALL PRIVILEGES ON *.* TO 'evennia'@'localhost';`. If you do, it means
you can use `evennia dbshell` later to connect to mysql, drop your database and re-create it as a
way of easy reset. Without this extra privilege you will be able to drop the database but not re-
create it without first switching to the database-root user.
## Add MySQL configuration to Evennia
To tell Evennia to use your new database you need to edit `mygame/server/conf/settings.py` (or
`secret_settings.py` if you don't want your db info passed around on git repositories).
> Note: The Django documentation suggests using an external `db.cnf` or other external conf-
formatted file. Evennia users have however found that this leads to problems (see e.g. [issue
#1184](https://git.io/vQdiN)). To avoid trouble we recommend you simply put the configuration in
your settings as below.
```python
#
# MySQL Database Configuration
#
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'evennia',
'USER': 'evennia',
'PASSWORD': 'somepassword',
'HOST': 'localhost', # or an IP Address that your DB is hosted on
'PORT': '', # use default port
}
}
```
Change this to fit your database setup. Next, run:
evennia migrate
to populate your database. Should you ever want to inspect the database directly you can from now on
also use
evennia dbshell
as a shortcut to get into the postgres command line for the right database and user.
With the database setup you should now be able to start start Evennia normally with your new
database.
## Others
No testing has been performed with Oracle, but it is also supported through Django. There are
community maintained drivers for [MS SQL](http://code.google.com/p/django-mssql/) and possibly a few
others. If you try other databases out, consider expanding this page with instructions.

View file

@ -0,0 +1,109 @@
# Client Support Grid
This grid tries to gather info about different MU clients when used with Evennia.
If you want to report a problem, update an entry or add a client, make a
new [documentation issue](github:issue) for it. Everyone's encouraged to report their findings.
##### Legend:
- **Name**: The name of the client. Also note if it's OS-specific.
- **Version**: Which version or range of client versions were tested.
- **Comments**: Any quirks on using this client with Evennia should be added here.
## Client Grid
```eval_rst
+----------------------------+-----------+----------------------------------------------------------------+
| Name | Version | Comments |
+============================+===========+================================================================+
| `Evennia Webclient`_ | 0.9 | Evennia-specific |
+----------------------------+-----------+----------------------------------------------------------------+
| `tintin++`_ | 2.0+ | No MXP support |
+----------------------------+-----------+----------------------------------------------------------------+
| tinyfugue_ | 5.0+ | No UTF-8 support |
+----------------------------+-----------+----------------------------------------------------------------+
| MUSHclient_ (Win) | 4.94 | NAWS reports full text area |
+----------------------------+-----------+----------------------------------------------------------------+
| Zmud_ (Win) | 7.21 | *UNTESTED* |
+----------------------------+-----------+----------------------------------------------------------------+
| Cmud_ (Win) | v3 | *UNTESTED* |
+----------------------------+-----------+----------------------------------------------------------------+
| Potato_ | 2.0.0b16 | No MXP, MCCP support. Win 32bit does not understand |
| | | "localhost", must use `127.0.0.1`. |
+----------------------------+-----------+----------------------------------------------------------------+
| Mudlet_ | 3.4+ | No known issues. Some older versions showed <> as html |
| | | under MXP. |
+----------------------------+-----------+----------------------------------------------------------------+
| SimpleMU_ (Win) | full | Discontinued. NAWS reports pixel size. |
+----------------------------+-----------+----------------------------------------------------------------+
| Atlantis_ (Mac) | 0.9.9.4 | No known issues. |
+----------------------------+-----------+----------------------------------------------------------------+
| GMUD_ | 0.0.1 | Can't handle any telnet handshakes. Not recommended. |
+----------------------------+-----------+----------------------------------------------------------------+
| BeipMU_ (Win) | 3.0.255 | No MXP support. Best to enable "MUD prompt handling", disable |
| | | "Handle HTML tags". |
+----------------------------+-----------+----------------------------------------------------------------+
| MudRammer_ (IOS) | 1.8.7 | Bad Telnet Protocol compliance: displays spurious characters. |
+----------------------------+-----------+----------------------------------------------------------------+
| MUDMaster_ | 1.3.1 | *UNTESTED* |
+----------------------------+-----------+----------------------------------------------------------------+
| BlowTorch_ (Andr) | 1.1.3 | Telnet NOP displays as spurious character. |
+----------------------------+-----------+----------------------------------------------------------------+
| Mukluk_ (Andr) | 2015.11.20| Telnet NOP displays as spurious character. Has UTF-8/Emoji |
| | | support. |
+----------------------------+-----------+----------------------------------------------------------------+
| Gnome-MUD_ (Unix) | 0.11.2 | Telnet handshake errors. First (only) attempt at logging in |
| | | fails. |
+----------------------------+-----------+----------------------------------------------------------------+
| Spyrit_ | 0.4 | No MXP, OOB support. |
+----------------------------+-----------+----------------------------------------------------------------+
| JamochaMUD_ | 5.2 | Does not support ANSI within MXP text. |
+----------------------------+-----------+----------------------------------------------------------------+
| DuckClient_ (Chrome) | 4.2 | No MXP support. Displays Telnet Go-Ahead and |
| | | WILL SUPPRESS-GO-AHEAD as ù character. Also seems to run |
| | | the `version` command on connection, which will not work in |
| | | `MULTISESSION_MODES` above 1. |
+----------------------------+-----------+----------------------------------------------------------------+
| KildClient_ | 2.11.1 | No known issues. |
+----------------------------+-----------+----------------------------------------------------------------+
.. _Evennia Webclient: ../Components/Webclient.html
.. _tintin++: http://tintin.sourceforge.net/
.. _tinyfugue: http://tinyfugue.sourceforge.net/
.. _MUSHclient: http://mushclient.com/
.. _Zmud: http://forums.zuggsoft.com/index.php?page=4&action=file&file_id=65
.. _Cmud: http://forums.zuggsoft.com/index.php?page=4&action=category&cat_id=11
.. _Potato: http://www.potatomushclient.com/
.. _Mudlet: http://www.mudlet.org/
.. _SimpleMU: https://archive.org/details/tucows_196173_SimpleMU_MU_Client
.. _Atlantis: http://www.riverdark.net/atlantis/
.. _GMUD: https://sourceforge.net/projects/g-mud/
.. _BeipMU: http://www.beipmu.com/
.. _MudRammer: https://itunes.apple.com/us/app/mudrammer-a-modern-mud-client/id597157072
.. _MUDMaster: https://itunes.apple.com/us/app/mudmaster/id341160033
.. _BlowTorch: http://bt.happygoatstudios.com/
.. _Mukluk: https://play.google.com/store/apps/details?id=com.crap.mukluk
.. _Gnome-MUD: https://github.com/GNOME/gnome-mud
.. _Spyrit: https://spyrit.ierne.eu.org/
.. _JamochaMUD: http://jamochamud.org/
.. _DuckClient: http://duckclient.com/
.. _KildClient: https://www.kildclient.org/
```
## Workarounds for client issues:
### Issue: Telnet NOP displays as spurious character.
Known clients:
* BlowTorch (Andr)
* Mukluk (Andr)
Workaround:
* In-game: Use `@option NOPKEEPALIVE=off` for the session, or use the `/save`
parameter to disable it for that Evennia account permanently.
* Client-side: Set a gag-type trigger on the NOP character to make it invisible to the client.

389
docs/source/Coding-FAQ.md Normal file
View file

@ -0,0 +1,389 @@
# Coding FAQ
*This FAQ page is for users to share their solutions to coding problems. Keep it brief and link to
the docs if you can rather than too lengthy explanations. Don't forget to check if an answer already
exists before answering - maybe you can clarify that answer rather than to make a new Q&A section.*
## Table of Contents
- [Will I run out of dbrefs?](./Coding-FAQ#will-i-run-out-of-dbrefs)
- [Removing default commands](./Coding-FAQ#removing-default-commands)
- [Preventing character from moving based on a condition](./Coding-FAQ#preventing-character-from-
moving-based-on-a-condition)
- [Reference initiating object in an EvMenu command](./Coding-FAQ#reference-initiating-object-in-an-
evmenu-command)
- [Adding color to default Evennia Channels](./Coding-FAQ#adding-color-to-default-evennia-channels)
- [Selectively turn off commands in a room](./Coding-FAQ#selectively-turn-off-commands-in-a-room)
- [Select Command based on a condition](./Coding-FAQ#select-command-based-on-a-condition)
- [Automatically updating code when reloading](./Coding-FAQ#automatically-updating-code-when-
reloading)
- [Changing all exit messages](./Coding-FAQ#changing-all-exit-messages)
- [Add parsing with the "to" delimiter](./Coding-FAQ#add-parsing-with-the-to-delimiter)
- [Store last used session IP address](./Coding-FAQ#store-last-used-session-ip-address)
- [Use wide characters with EvTable](./Coding-FAQ#non-latin-characters-in-evtable)
## Will I run out of dbrefs?
**Q:** The `#dbref` of a database object is ever-increasing. Evennia doesn't allow you to change or
reuse them. Will not a big/old game run out of dbref integers eventually?
**A:** No. For example, the default sqlite3 database's max dbref is `2**64`. If you created `10 000`
objects every second every minute and every day of the year it would take ~60 million years for you
to run out of dbref numbers. That's a database of 140 TeraBytes, if every row was empty. If you are
still using Evennia at that point and has this concern, get back to us and we can discuss adding
dbref reuse then.
## Removing default commands
**Q:** How does one *remove* (not replace) e.g. the default `get` [Command](./Commands) from the
Character [Command Set](./Command-Sets)?
**A:** Go to `mygame/commands/default_cmdsets.py`. Find the `CharacterCmdSet` class. It has one
method named `at_cmdset_creation`. At the end of that method, add the following line:
`self.remove(default_cmds.CmdGet())`. See the [Adding Commands Tutorial](./Adding-Command-Tutorial)
for more info.
## Preventing character from moving based on a condition
**Q:** How does one keep a character from using any exit, if they meet a certain condition? (I.E. in
combat, immobilized, etc.)
**A:** The `at_before_move` hook is called by Evennia just before performing any move. If it returns
`False`, the move is aborted. Let's say we want to check for an [Attribute](./Attributes) `cantmove`.
Add the following code to the `Character` class:
```python
def at_before_move(self, destination):
"Called just before trying to move"
if self.db.cantmove: # replace with condition you want to test
self.msg("Something is preventing you from moving!")
return False
return True
```
## Reference initiating object in an EvMenu command.
**Q:** An object has a Command on it starts up an EvMenu instance. How do I capture a reference to
that object for use in the menu?
**A:** When an [EvMenu](./EvMenu) is started, the menu object is stored as `caller.ndb._menutree`.
This is a good place to store menu-specific things since it will clean itself up when the menu
closes. When initiating the menu, any additional keywords you give will be available for you as
properties on this menu object:
```python
class MyObjectCommand(Command):
# A Command stored on an object (the object is always accessible from
# the Command as self.obj)
def func(self):
# add the object as the stored_obj menu property
EvMenu(caller, ..., stored_obj=self.obj)
```
Inside the menu you can now access the object through `caller.ndb._menutree.stored_obj`.
## Adding color to default Evennia Channels
**Q:** How do I add colors to the names of Evennia channels?
**A:** The Channel typeclass' `channel_prefix` method decides what is shown at the beginning of a
channel send. Edit `mygame/typeclasses/channels.py` (and then `@reload`):
```python
# define our custom color names
CHANNEL_COLORS = {'public': '|015Public|n',
'newbie': '|550N|n|551e|n|552w|n|553b|n|554i|n|555e|n',
'staff': '|010S|n|020t|n|030a|n|040f|n|050f|n'}
# Add to the Channel class
# ...
def channel_prefix(self, msg, emit=False):
prefix_string = ""
if self.key in COLORS:
prefix_string = "[%s] " % CHANNEL_COLORS.get(self.key.lower())
else:
prefix_string = "[%s] " % self.key.capitalize()
return prefix_string
```
Additional hint: To make colors easier to change from one place you could instead put the
`CHANNEL_COLORS` dict in your settings file and import it as `from django.conf.settings import
CHANNEL_COLORS`.
## Selectively turn off commands in a room
**Q:** I want certain commands to turn off in a given room. They should still work normally for
staff.
**A:** This is done using a custom cmdset on a room [locked with the 'call' lock type](./Locks). Only
if this lock is passed will the commands on the room be made available to an object inside it. Here
is an example of a room where certain commands are disabled for non-staff:
```python
# in mygame/typeclasses/rooms.py
from evennia import default_commands, CmdSet
class CmdBlocking(default_commands.MuxCommand):
# block commands give, get, inventory and drop
key = "give"
aliases = ["get", "inventory", "drop"]
def func(self):
self.caller.msg("You cannot do that in this room.")
class BlockingCmdSet(CmdSet):
key = "blocking_cmdset"
# default commands have prio 0
priority = 1
def at_cmdset_creation(self):
self.add(CmdBlocking())
class BlockingRoom(Room):
def at_object_creation(self):
self.cmdset.add(BlockingCmdSet, permanent=True)
# only share commands with players in the room that
# are NOT Builders or higher
self.locks.add("call:not perm(Builders)")
```
After `@reload`, make some `BlockingRooms` (or switch a room to it with `@typeclass`). Entering one
will now replace the given commands for anyone that does not have the `Builders` or higher
permission. Note that the 'call' lock is special in that even the superuser will be affected by it
(otherwise superusers would always see other player's cmdsets and a game would be unplayable for
superusers).
## Select Command based on a condition
**Q:** I want a command to be available only based on a condition. For example I want the "werewolf"
command to only be available on a full moon, from midnight to three in-game time.
**A:** This is easiest accomplished by putting the "werewolf" command on the Character as normal,
but to [lock](./Locks) it with the "cmd" type lock. Only if the "cmd" lock type is passed will the
command be available.
```python
# in mygame/commands/command.py
from evennia import Command
class CmdWerewolf(Command):
key = "werewolf"
# lock full moon, between 00:00 (midnight) and 03:00.
locks = "cmd:is_full_moon(0, 3)"
def func(self):
# ...
```
Add this to the [default cmdset as usual](./Adding-Command-Tutorial). The `is_full_moon` [lock
function](Locks#lock-functions) does not yet exist. We must create that:
```python
# in mygame/server/conf/lockfuncs.py
def is_full_moon(accessing_obj, accessed_obj,
starthour, endhour, *args, **kwargs):
# calculate if the moon is full here and
# if current game time is between starthour and endhour
# return True or False
```
After a `@reload`, the `werewolf` command will be available only at the right time, that is when the
`is_full_moon` lock function returns True.
## Automatically updating code when reloading
**Q:** I have a development server running Evennia. Can I have the server update its code-base when
I reload?
**A:** Having a development server that pulls updated code whenever you reload it can be really
useful if you have limited shell access to your server, or want to have it done automatically. If
you have your project in a configured Git environment, it's a matter of automatically calling `git
pull` when you reload. And that's pretty straightforward:
In `/server/conf/at_server_startstop.py`:
```python
import subprocess
# ... other hooks ...
def at_server_reload_stop():
"""
This is called only time the server stops before a reload.
"""
print("Pulling from the game repository...")
process = subprocess.call(["git", "pull"], shell=False)
```
That's all. We call `subprocess` to execute a shell command (that code works on Windows and Linux,
assuming the current directory is your game directory, which is probably the case when you run
Evennia). `call` waits for the process to complete, because otherwise, Evennia would reload on
partially-modified code, which would be problematic.
Now, when you enter `@reload` on your development server, the game repository is updated from the
configured remote repository (Github, for instance). Your development cycle could resemble
something like:
1. Coding on the local machine.
2. Testing modifications.
3. Committing once, twice or more (being sure the code is still working, unittests are pretty useful
here).
4. When the time comes, login to the development server and run `@reload`.
The reloading might take one or two additional seconds, since Evennia will pull from your remote Git
repository. But it will reload on it and you will have your modifications ready, without needing
connecting to your server using SSH or something similar.
## Changing all exit messages
**Q:** How can I change the default exit messages to something like "XXX leaves east" or "XXX
arrives from the west"?
**A:** the default exit messages are stored in two hooks, namely `announce_move_from` and
`announce_move_to`, on the `Character` typeclass (if what you want to change is the message other
characters will see when a character exits).
These two hooks provide some useful features to easily update the message to be displayed. They
take both the default message and mapping as argument. You can easily call the parent hook with
these information:
* The message represents the string of characters sent to characters in the room when a character
leaves.
* The mapping is a dictionary containing additional mappings (you will probably not need it for
simple customization).
It is advisable to look in the [code of both
hooks](https://github.com/evennia/evennia/tree/master/evennia/objects/objects.py), and read the
hooks' documentation. The explanations on how to quickly update the message are shown below:
```python
# In typeclasses/characters.py
"""
Characters
"""
from evennia import DefaultCharacter
class Character(DefaultCharacter):
"""
The default character class.
...
"""
def announce_move_from(self, destination, msg=None, mapping=None):
"""
Called if the move is to be announced. This is
called while we are still standing in the old
location.
Args:
destination (Object): The place we are going to.
msg (str, optional): a replacement message.
mapping (dict, optional): additional mapping objects.
You can override this method and call its parent with a
message to simply change the default message. In the string,
you can use the following as mappings (between braces):
object: the object which is moving.
exit: the exit from which the object is moving (if found).
origin: the location of the object before the move.
destination: the location of the object after moving.
"""
super().announce_move_from(destination, msg="{object} leaves {exit}.")
def announce_move_to(self, source_location, msg=None, mapping=None):
"""
Called after the move if the move was not quiet. At this point
we are standing in the new location.
Args:
source_location (Object): The place we came from
msg (str, optional): the replacement message if location.
mapping (dict, optional): additional mapping objects.
You can override this method and call its parent with a
message to simply change the default message. In the string,
you can use the following as mappings (between braces):
object: the object which is moving.
exit: the exit from which the object is moving (if found).
origin: the location of the object before the move.
destination: the location of the object after moving.
"""
super().announce_move_to(source_location, msg="{object} arrives from the {exit}.")
```
We override both hooks, but call the parent hook to display a different message. If you read the
provided docstrings, you will better understand why and how we use mappings (information between
braces). You can provide additional mappings as well, if you want to set a verb to move, for
instance, or other, extra information.
## Add parsing with the "to" delimiter
**Q:** How do I change commands to undestand say `give obj to target` as well as the default `give
obj = target`?
**A:** You can make change the default `MuxCommand` parent with your own class making a small change
in its `parse` method:
```python
# in mygame/commands/command.py
from evennia import default_cmds
class MuxCommand(default_cmds.MuxCommand):
def parse(self):
"""Implement an additional parsing of 'to'"""
super().parse()
if " to " in self.args:
self.lhs, self.rhs = self.args.split(" to ", 1)
```
Next you change the parent of the default commands in settings:
```python
COMMAND_DEFAULT_CLASS = "commands.command.MuxCommand"
```
Do a `@reload` and all default commands will now use your new tweaked parent class. A copy of the
MuxCommand class is also found commented-out in the `mygame/commands/command.py` file.
## Store last used session IP address
**Q:** If a user has already logged out of an Evennia account, their IP is no longer visible to
staff that wants to ban-by-ip (instead of the user) with `@ban/ip`?
**A:** One approach is to write the IP from the last session onto the "account" account object.
`typeclasses/accounts.py`
```python
def at_post_login(self, session=None, **kwargs):
super().at_post_login(session=session, **kwargs)
self.db.lastsite = self.sessions.all()[-1].address
```
Adding timestamp for login time and appending to a list to keep the last N login IP addresses and
timestamps is possible, also. Additionally, if you don't want the list to grow beyond a
`do_not_exceed` length, conditionally pop a value after you've added it, if the length has grown too
long.
**NOTE:** You'll need to add `import time` to generate the login timestamp.
```python
def at_post_login(self, session=None, **kwargs):
super().at_post_login(session=session, **kwargs)
do_not_exceed = 24 # Keep the last two dozen entries
session = self.sessions.all()[-1] # Most recent session
if not self.db.lastsite:
self.db.lastsite = []
self.db.lastsite.insert(0, (session.address, int(time.time())))
if len(self.db.lastsite) > do_not_exceed:
self.db.lastsite.pop()
```
This only stores the data. You may want to interface the `@ban` command or make a menu-driven viewer
for staff to browse the list and display how long ago the login occurred.
## Non-latin characters in EvTable
**Q:** When using e.g. Chinese characters in EvTable, some lines appear to be too wide, for example
```
+------+------+
| | |
| 测试 | 测试 |
| | |
+~~~~~~+~~~~~~+
```
**A:** The reason for this is because certain non-latin characters are *visually* much wider than
their len() suggests. There is little Evennia can (reliably) do about this. If you are using such
characters, you need to make sure to use a suitable mono-spaced font where are width are equal. You
can set this in your web client and need to recommend it for telnet-client users. See [this
discussion](https://github.com/evennia/evennia/issues/1522) where some suitable fonts are suggested.

View file

@ -0,0 +1,99 @@
# Coding Introduction
Evennia allows for a lot of freedom when designing your game - but to code efficiently you still
need to adopt some best practices as well as find a good place to start to learn.
Here are some pointers to get you going.
### Python
Evennia is developed using Python. Even if you are more of a designer than a coder, it is wise to
learn how to read and understand basic Python code. If you are new to Python, or need a refresher,
take a look at our two-part [Python introduction](./Python-basic-introduction).
### Explore Evennia interactively
When new to Evennia it can be hard to find things or figure out what is available. Evennia offers a
special interactive python shell that allows you to experiment and try out things. It's recommended
to use [ipython](http://ipython.org/) for this since the vanilla python prompt is very limited. Here
are some simple commands to get started:
# [open a new console/terminal]
# [activate your evennia virtualenv in this console/terminal]
pip install ipython # [only needed the first time]
cd mygame
evennia shell
This will open an Evennia-aware python shell (using ipython). From within this shell, try
import evennia
evennia.<TAB>
That is, enter `evennia.` and press the `<TAB>` key. This will show you all the resources made
available at the top level of Evennia's "flat API". See the [flat API](./Evennia-API) page for more
info on how to explore it efficiently.
You can complement your exploration by peeking at the sections of the much more detailed
[Developer Central](./Developer-Central). The [Tutorials](./Tutorials) section also contains a growing collection
of system- or implementation-specific help.
### Use a python syntax checker
Evennia works by importing your own modules and running them as part of the server. Whereas Evennia
should just gracefully tell you what errors it finds, it can nevertheless be a good idea for you to
check your code for simple syntax errors *before* you load it into the running server. There are
many python syntax checkers out there. A fast and easy one is
[pyflakes](https://pypi.python.org/pypi/pyflakes), a more verbose one is
[pylint](http://www.pylint.org/). You can also check so that your code looks up to snuff using
[pep8](https://pypi.python.org/pypi/pep8). Even with a syntax checker you will not be able to catch
every possible problem - some bugs or problems will only appear when you actually run the code. But
using such a checker can be a good start to weed out the simple problems.
### Plan before you code
Before you start coding away at your dream game, take a look at our [Game Planning](./Game-Planning)
page. It might hopefully help you avoid some common pitfalls and time sinks.
### Code in your game folder, not in the evennia/ repository
As part of the Evennia setup you will create a game folder to host your game code. This is your
home. You should *never* need to modify anything in the `evennia` library (anything you download
from us, really). You import useful functionality from here and if you see code you like, copy&paste
it out into your game folder and edit it there.
If you find that Evennia doesn't support some functionality you need, make a
[Feature Request](github:issue) about it. Same goes for [bugs](github:issue). If you add features or fix bugs
yourself, please consider [Contributing](./Contributing) your changes upstream!
### Learn to read tracebacks
Python is very good at reporting when and where things go wrong. A *traceback* shows everything you
need to know about crashing code. The text can be pretty long, but you usually are only interested
in the last bit, where it says what the error is and at which module and line number it happened -
armed with this info you can resolve most problems.
Evennia will usually not show the full traceback in-game though. Instead the server outputs errors
to the terminal/console from which you started Evennia in the first place. If you want more to show
in-game you can add `IN_GAME_ERRORS = True` to your settings file. This will echo most (but not all)
tracebacks both in-game as well as to the terminal/console. This is a potential security problem
though, so don't keep this active when your game goes into production.
> A common confusing error is finding that objects in-game are suddenly of the type `DefaultObject`
rather than your custom typeclass. This happens when you introduce a critical Syntax error to the
module holding your custom class. Since such a module is not valid Python, Evennia can't load it at
all. Instead of crashing, Evennia will then print the full traceback to the terminal/console and
temporarily fall back to the safe `DefaultObject` until you fix the problem and reload.
### Docs are here to help you
Some people find reading documentation extremely dull and shun it out of principle. That's your
call, but reading docs really *does* help you, promise! Evennia's documentation is pretty thorough
and knowing what is possible can often give you a lot of new cool game ideas. That said, if you
can't find the answer in the docs, don't be shy to ask questions! The
[discussion group](https://sites.google.com/site/evenniaserver/discussions) and the
[irc chat](http://webchat.freenode.net/?channels=evennia) are also there for you.
### The most important point
And finally, of course, have fun!

297
docs/source/Coding-Utils.md Normal file
View file

@ -0,0 +1,297 @@
# Coding Utils
Evennia comes with many utilities to help with common coding tasks. Most are accessible directly
from the flat API, otherwise you can find them in the `evennia/utils/` folder.
## Searching
A common thing to do is to search for objects. There it's easiest to use the `search` method defined
on all objects. This will search for objects in the same location and inside the self object:
```python
obj = self.search(objname)
```
The most common time one needs to do this is inside a command body. `obj =
self.caller.search(objname)` will search inside the caller's (typically, the character that typed
the command) `.contents` (their "inventory") and `.location` (their "room").
Give the keyword `global_search=True` to extend search to encompass entire database. Aliases will
also be matched by this search. You will find multiple examples of this functionality in the default
command set.
If you need to search for objects in a code module you can use the functions in
`evennia.utils.search`. You can access these as shortcuts `evennia.search_*`.
```python
from evennia import search_object
obj = search_object(objname)
```
- [evennia.search_account](../wiki/evennia.accounts.manager#accountdbmanagersearch_account)
- [evennia.search_object](../wiki/evennia.objects.manager#objectdbmanagersearch_object)
- [evennia.search_object_by_tag](../wiki/evennia.utils.search#search_object_by_tag)
- [evennia.search_script](../wiki/evennia.scripts.manager#scriptdbmanagersearch_script)
- [evennia.search_channel](../wiki/evennia.comms.managers#channeldbmanagersearch_channel)
- [evennia.search_message](../wiki/evennia.comms.managers#msgmanagersearch_message)
- [evennia.search_help](../wiki/evennia.help.manager#helpentrymanagersearch_help)
Note that these latter methods will always return a `list` of results, even if the list has one or
zero entries.
## Create
Apart from the in-game build commands (`@create` etc), you can also build all of Evennia's game
entities directly in code (for example when defining new create commands).
```python
import evennia
myobj = evennia.create_objects("game.gamesrc.objects.myobj.MyObj", key="MyObj")
```
- [evennia.create_account](../wiki/evennia.utils.create#create_account)
- [evennia.create_object](../wiki/evennia.utils.create#create_object)
- [evennia.create_script](../wiki/evennia.utils.create#create_script)
- [evennia.create_channel](../wiki/evennia.utils.create#create_channel)
- [evennia.create_help_entry](../wiki/evennia.utils.create#create_help_entry)
- [evennia.create_message](../wiki/evennia.utils.create#create_message)
Each of these create-functions have a host of arguments to further customize the created entity. See
`evennia/utils/create.py` for more information.
## Logging
Normally you can use Python `print` statements to see output to the terminal/log. The `print`
statement should only be used for debugging though. For producion output, use the `logger` which
will create proper logs either to terminal or to file.
```python
from evennia import logger
#
logger.log_err("This is an Error!")
logger.log_warn("This is a Warning!")
logger.log_info("This is normal information")
logger.log_dep("This feature is deprecated")
```
There is a special log-message type, `log_trace()` that is intended to be called from inside a
traceback - this can be very useful for relaying the traceback message back to log without having it
kill the server.
```python
try:
# [some code that may fail...]
except Exception:
logger.log_trace("This text will show beneath the traceback itself.")
```
The `log_file` logger, finally, is a very useful logger for outputting arbitrary log messages. This
is a heavily optimized asynchronous log mechanism using
[threads](https://en.wikipedia.org/wiki/Thread_%28computing%29) to avoid overhead. You should be
able to use it for very heavy custom logging without fearing disk-write delays.
```python
logger.log_file(message, filename="mylog.log")
```
If not an absolute path is given, the log file will appear in the `mygame/server/logs/` directory.
If the file already exists, it will be appended to. Timestamps on the same format as the normal
Evennia logs will be automatically added to each entry. If a filename is not specified, output will
be written to a file `game/logs/game.log`.
## Time Utilities
### Game time
Evennia tracks the current server time. You can access this time via the `evennia.gametime`
shortcut:
```python
from evennia import gametime
# all the functions below return times in seconds).
# total running time of the server
runtime = gametime.runtime()
# time since latest hard reboot (not including reloads)
uptime = gametime.uptime()
# server epoch (its start time)
server_epoch = gametime.server_epoch()
# in-game epoch (this can be set by `settings.TIME_GAME_EPOCH`.
# If not, the server epoch is used.
game_epoch = gametime.game_epoch()
# in-game time passed since time started running
gametime = gametime.gametime()
# in-game time plus game epoch (i.e. the current in-game
# time stamp)
gametime = gametime.gametime(absolute=True)
# reset the game time (back to game epoch)
gametime.reset_gametime()
```
The setting `TIME_FACTOR` determines how fast/slow in-game time runs compared to the real world. The
setting `TIME_GAME_EPOCH` sets the starting game epoch (in seconds). The functions from the
`gametime` module all return their times in seconds. You can convert this to whatever units of time
you desire for your game. You can use the `@time` command to view the server time info.
You can also *schedule* things to happen at specific in-game times using the
[gametime.schedule](github:evennia.utils.gametime#schedule) function:
```python
import evennia
def church_clock:
limbo = evennia.search_object(key="Limbo")
limbo.msg_contents("The church clock chimes two.")
gametime.schedule(church_clock, hour=2)
```
### utils.time_format()
This function takes a number of seconds as input (e.g. from the `gametime` module above) and
converts it to a nice text output in days, hours etc. It's useful when you want to show how old
something is. It converts to four different styles of output using the *style* keyword:
- style 0 - `5d:45m:12s` (standard colon output)
- style 1 - `5d` (shows only the longest time unit)
- style 2 - `5 days, 45 minutes` (full format, ignores seconds)
- style 3 - `5 days, 45 minutes, 12 seconds` (full format, with seconds)
### utils.delay()
```python
from evennia import utils
def _callback(obj, text):
obj.msg(text)
# wait 10 seconds before sending "Echo!" to obj (which we assume is defined)
deferred = utils.delay(10, _callback, obj, "Echo!", persistent=False)
# code here will run immediately, not waiting for the delay to fire!
```
This creates an asynchronous delayed call. It will fire the given callback function after the given
number of seconds. This is a very light wrapper over a Twisted
[Deferred](https://twistedmatrix.com/documents/current/core/howto/defer.html). Normally this is run
non-persistently, which means that if the server is `@reload`ed before the delay is over, the
callback will never run (the server forgets it). If setting `persistent` to True, the delay will be
stored in the database and survive a `@reload` - but for this to work it is susceptible to the same
limitations incurred when saving to an [Attribute](./Attributes).
The `deferred` return object can usually be ignored, but calling its `.cancel()` method will abort
the delay prematurely.
`utils.delay` is the lightest form of delayed call in Evennia. For other way to create time-bound
tasks, see the [TickerHandler](./TickerHandler) and [Scripts](./Scripts).
> Note that many delayed effects can be achieved without any need for an active timer. For example
if you have a trait that should recover a point every 5 seconds you might just need its value when
it's needed, but checking the current time and calculating on the fly what value it should have.
## Object Classes
### utils.inherits_from()
This useful function takes two arguments - an object to check and a parent. It returns `True` if
object inherits from parent *at any distance* (as opposed to Python's in-built `is_instance()` that
will only catch immediate dependence). This function also accepts as input any combination of
classes, instances or python-paths-to-classes.
Note that Python code should usually work with [duck
typing](http://en.wikipedia.org/wiki/Duck_typing). But in Evennia's case it can sometimes be useful
to check if an object inherits from a given [Typeclass](./Typeclasses) as a way of identification. Say
for example that we have a typeclass *Animal*. This has a subclass *Felines* which in turn has a
subclass *HouseCat*. Maybe there are a bunch of other animal types too, like horses and dogs. Using
`inherits_from` will allow you to check for all animals in one go:
```python
from evennia import utils
if (utils.inherits_from(obj, "typeclasses.objects.animals.Animal"):
obj.msg("The bouncer stops you in the door. He says: 'No talking animals allowed.'")
```
## Text utilities
In a text game, you are naturally doing a lot of work shuffling text back and forth. Here is a *non-
complete* selection of text utilities found in `evennia/utils/utils.py` (shortcut `evennia.utils`).
If nothing else it can be good to look here before starting to develop a solution of your own.
### utils.fill()
This flood-fills a text to a given width (shuffles the words to make each line evenly wide). It also
indents as needed.
```python
outtxt = fill(intxt, width=78, indent=4)
```
### utils.crop()
This function will crop a very long line, adding a suffix to show the line actually continues. This
can be useful in listings when showing multiple lines would mess up things.
```python
intxt = "This is a long text that we want to crop."
outtxt = crop(intxt, width=19, suffix="[...]")
# outtxt is now "This is a long text[...]"
```
### utils.dedent()
This solves what may at first glance appear to be a trivial problem with text - removing
indentations. It is used to shift entire paragraphs to the left, without disturbing any further
formatting they may have. A common case for this is when using Python triple-quoted strings in code
- they will retain whichever indentation they have in the code, and to make easily-readable source
code one usually don't want to shift the string to the left edge.
```python
#python code is entered at a given indentation
intxt = """
This is an example text that will end
up with a lot of whitespace on the left.
It also has indentations of
its own."""
outtxt = dedent(intxt)
# outtxt will now retain all internal indentation
# but be shifted all the way to the left.
```
Normally you do the dedent in the display code (this is for example how the help system homogenizes
help entries).
### to_str() and to_bytes()
Evennia supplies two utility functions for converting text to the correct
encodings. `to_str()` and `to_bytes()`. Unless you are adding a custom protocol and
need to send byte-data over the wire, `to_str` is the only one you'll need.
The difference from Python's in-built `str()` and `bytes()` operators are that
the Evennia ones makes use of the `ENCODINGS` setting and will try very hard to
never raise a traceback but instead echo errors through logging. See
[here](./Text-Encodings) for more info.
### Ansi Coloring Tools
- [evennia.ansi](api:evennia.utils.ansi)
## Display utilities
### Making ascii tables
The [EvTable](github:evennia.utils.evtable#evtable) class (`evennia/utils/evtable.py`) can be used
to create correctly formatted text tables. There is also
[EvForm](github:evennia.utils.evform#evform) (`evennia/utils/evform.py`). This reads a fixed-format
text template from a file in order to create any level of sophisticated ascii layout. Both evtable
and evform have lots of options and inputs so see the header of each module for help.
The third-party [PrettyTable](https://code.google.com/p/prettytable/) module is also included in
Evennia. PrettyTable is considered deprecated in favor of EvTable since PrettyTable cannot handle
ANSI colour. PrettyTable can be found in `evennia/utils/prettytable/`. See its homepage above for
instructions.
### Menus
- [evennia.EvMenu](github:evennia.utils.evmenu#evmenu)

View file

@ -0,0 +1,98 @@
# Command Cooldown
Some types of games want to limit how often a command can be run. If a
character casts the spell *Firestorm*, you might not want them to spam that
command over and over. Or in an advanced combat system, a massive swing may
offer a chance of lots of damage at the cost of not being able to re-do it for
a while. Such effects are called *cooldowns*.
This page exemplifies a very resource-efficient way to do cooldowns. A more
'active' way is to use asynchronous delays as in the [command duration
tutorial](Command-Duration#Blocking-Commands), the two might be useful to
combine if you want to echo some message to the user after the cooldown ends.
## Non-persistent cooldown
This little recipe will limit how often a particular command can be run. Since
Commands are class instances, and those are cached in memory, a command
instance will remember things you store on it. So just store the current time
of execution! Next time the command is run, it just needs to check if it has
that time stored, and compare it with the current time to see if a desired
delay has passed.
```python
import time
from evennia import default_cmds
class CmdSpellFirestorm(default_cmds.MuxCommand):
"""
Spell - Firestorm
Usage:
cast firestorm <target>
This will unleash a storm of flame. You can only release one
firestorm every five minutes (assuming you have the mana).
"""
key = "cast firestorm"
locks = "cmd:isFireMage()"
def func(self):
"Implement the spell"
# check cooldown (5 minute cooldown)
now = time.time()
if hasattr(self, "lastcast") and \
now - self.lastcast < 5 * 60:
message = "You cannot cast this spell again yet."
self.caller.msg(message)
return
#[the spell effect is implemented]
# if the spell was successfully cast, store the casting time
self.lastcast = now
```
We just check the `lastcast` flag, and update it if everything works out.
Simple and very effective since everything is just stored in memory. The
drawback of this simple scheme is that it's non-persistent. If you do
`@reload`, the cache is cleaned and all such ongoing cooldowns will be
forgotten. It is also limited only to this one command, other commands cannot
(easily) check for this value.
## Persistent cooldown
This is essentially the same mechanism as the simple one above, except we use
the database to store the information which means the cooldown will survive a
server reload/reboot. Since commands themselves have no representation in the
database, you need to use the caster for the storage.
```python
# inside the func() of CmdSpellFirestorm as above
# check cooldown (5 minute cooldown)
now = time.time()
lastcast = self.caller.db.firestorm_lastcast
if lastcast and now - lastcast < 5 * 60:
message = "You need to wait before casting this spell again."
self.caller.msg(message)
return
#[the spell effect is implemented]
# if the spell was successfully cast, store the casting time
self.caller.db.firestorm_lastcast = now
```
Since we are storing as an [Attribute](./Attributes), we need to identify the
variable as `firestorm_lastcast` so we are sure we get the right one (we'll
likely have other skills with cooldowns after all). But this method of
using cooldowns also has the advantage of working *between* commands - you can
for example let all fire-related spells check the same cooldown to make sure
the casting of *Firestorm* blocks all fire-related spells for a while. Or, in
the case of taking that big swing with the sword, this could now block all
other types of attacks for a while before the warrior can recover.

View file

@ -0,0 +1,403 @@
# Command Duration
Before reading this tutorial, if you haven't done so already, you might want to
read [the documentation on commands](./Commands) to get a basic understanding of
how commands work in Evennia.
In some types of games a command should not start and finish immediately.
Loading a crossbow might take a bit of time to do - time you don't have when
the enemy comes rushing at you. Crafting that armour will not be immediate
either. For some types of games the very act of moving or changing pose all
comes with a certain time associated with it.
## The simple way to pause commands with yield
Evennia allows a shortcut in syntax to create simple pauses in commands. This
syntax uses the `yield` keyword. The `yield` keyword is used in Python to
create generators, although you don't need to know what generators are to use
this syntax. A short example will probably make it clear:
```python
class CmdTest(Command):
"""
A test command just to test waiting.
Usage:
test
"""
key = "test"
locks = "cmd:all()"
def func(self):
self.msg("Before ten seconds...")
yield 10
self.msg("Afterwards.")
```
> Important: The `yield` functionality will *only* work in the `func` method of
> Commands. It only works because Evennia has especially
> catered for it in Commands. If you want the same functionality elsewhere you
> must use the [interactive decorator](./Async-Process#The-@interactive-decorator).
The important line is the `yield 10`. It tells Evennia to "pause" the command
and to wait for 10 seconds to execute the rest. If you add this command and
run it, you'll see the first message, then, after a pause of ten seconds, the
next message. You can use `yield` several times in your command.
This syntax will not "freeze" all commands. While the command is "pausing",
you can execute other commands (or even call the same command again). And
other players aren't frozen either.
> Note: this will not save anything in the database. If you reload the game
> while a command is "paused", it will not resume after the server has
> reloaded.
## The more advanced way with utils.delay
The `yield` syntax is easy to read, easy to understand, easy to use. But it's not that flexible if
you want more advanced options. Learning to use alternatives might be much worth it in the end.
Below is a simple command example for adding a duration for a command to finish.
```python
from evennia import default_cmds, utils
class CmdEcho(default_cmds.MuxCommand):
"""
wait for an echo
Usage:
echo <string>
Calls and waits for an echo
"""
key = "echo"
locks = "cmd:all()"
def func(self):
"""
This is called at the initial shout.
"""
self.caller.msg("You shout '%s' and wait for an echo ..." % self.args)
# this waits non-blocking for 10 seconds, then calls self.echo
utils.delay(10, self.echo) # call echo after 10 seconds
def echo(self):
"Called after 10 seconds."
shout = self.args
string = "You hear an echo: %s ... %s ... %s"
string = string % (shout.upper(), shout.capitalize(), shout.lower())
self.caller.msg(string)
```
Import this new echo command into the default command set and reload the server. You will find that
it will take 10 seconds before you see your shout coming back. You will also find that this is a
*non-blocking* effect; you can issue other commands in the interim and the game will go on as usual.
The echo will come back to you in its own time.
### About utils.delay()
`utils.delay(timedelay, callback, persistent=False, *args, **kwargs)` is a useful function. It will
wait `timedelay` seconds, then call the `callback` function, optionally passing to it the arguments
provided to utils.delay by way of *args and/or **kwargs`.
> Note: The callback argument should be provided with a python path to the desired function, for
instance `my_object.my_function` instead of `my_object.my_function()`. Otherwise my_function would
get called and run immediately upon attempting to pass it to the delay function.
If you want to provide arguments for utils.delay to use, when calling your callback function, you
have to do it separatly, for instance using the utils.delay *args and/or **kwargs, as mentioned
above.
> If you are not familiar with the syntax `*args` and `**kwargs`, [see the Python documentation
here](https://docs.python.org/2/tutorial/controlflow.html#arbitrary-argument-lists).
Looking at it you might think that `utils.delay(10, callback)` in the code above is just an
alternative to some more familiar thing like `time.sleep(10)`. This is *not* the case. If you do
`time.sleep(10)` you will in fact freeze the *entire server* for ten seconds! The `utils.delay()`is
a thin wrapper around a Twisted
[Deferred](http://twistedmatrix.com/documents/11.0.0/core/howto/defer.html) that will delay
execution until 10 seconds have passed, but will do so asynchronously, without bothering anyone else
(not even you - you can continue to do stuff normally while it waits to continue).
The point to remember here is that the `delay()` call will not "pause" at that point when it is
called (the way `yield` does in the previous section). The lines after the `delay()` call will
actually execute *right away*. What you must do is to tell it which function to call *after the time
has passed* (its "callback"). This may sound strange at first, but it is normal practice in
asynchronous systems. You can also link such calls together as seen below:
```python
from evennia import default_cmds, utils
class CmdEcho(default_cmds.MuxCommand):
"""
waits for an echo
Usage:
echo <string>
Calls and waits for an echo
"""
key = "echo"
locks = "cmd:all()"
def func(self):
"This sets off a chain of delayed calls"
self.caller.msg("You shout '%s', waiting for an echo ..." % self.args)
# wait 2 seconds before calling self.echo1
utils.delay(2, self.echo1)
# callback chain, started above
def echo1(self):
"First echo"
self.caller.msg("... %s" % self.args.upper())
# wait 2 seconds for the next one
utils.delay(2, self.echo2)
def echo2(self):
"Second echo"
self.caller.msg("... %s" % self.args.capitalize())
# wait another 2 seconds
utils.delay(2, callback=self.echo3)
def echo3(self):
"Last echo"
self.caller.msg("... %s ..." % self.args.lower())
```
The above version will have the echoes arrive one after another, each separated by a two second
delay.
> echo Hello!
... HELLO!
... Hello!
... hello! ...
## Blocking commands
As mentioned, a great thing about the delay introduced by `yield` or `utils.delay()` is that it does
not block. It just goes on in the background and you are free to play normally in the interim. In
some cases this is not what you want however. Some commands should simply "block" other commands
while they are running. If you are in the process of crafting a helmet you shouldn't be able to also
start crafting a shield at the same time, or if you just did a huge power-swing with your weapon you
should not be able to do it again immediately.
The simplest way of implementing blocking is to use the technique covered in the [Command
Cooldown](Command-Cooldown) tutorial. In that tutorial we implemented cooldowns by having the
Command store the current time. Next time the Command was called, we compared the current time to
the stored time to determine if enough time had passed for a renewed use. This is a *very*
efficient, reliable and passive solution. The drawback is that there is nothing to tell the Player
when enough time has passed unless they keep trying.
Here is an example where we will use `utils.delay` to tell the player when the cooldown has passed:
```python
from evennia import utils, default_cmds
class CmdBigSwing(default_cmds.MuxCommand):
"""
swing your weapon in a big way
Usage:
swing <target>
Makes a mighty swing. Doing so will make you vulnerable
to counter-attacks before you can recover.
"""
key = "bigswing"
locks = "cmd:all()"
def func(self):
"Makes the swing"
if self.caller.ndb.off_balance:
# we are still off-balance.
self.caller.msg("You are off balance and need time to recover!")
return
# [attack/hit code goes here ...]
self.caller.msg("You swing big! You are off balance now.")
# set the off-balance flag
self.caller.ndb.off_balance = True
# wait 8 seconds before we can recover. During this time
# we won't be able to swing again due to the check at the top.
utils.delay(8, self.recover)
def recover(self):
"This will be called after 8 secs"
del self.caller.ndb.off_balance
self.caller.msg("You regain your balance.")
```
Note how, after the cooldown, the user will get a message telling them they are now ready for
another swing.
By storing the `off_balance` flag on the character (rather than on, say, the Command instance
itself) it can be accessed by other Commands too. Other attacks may also not work when you are off
balance. You could also have an enemy Command check your `off_balance` status to gain bonuses, to
take another example.
## Abortable commands
One can imagine that you will want to abort a long-running command before it has a time to finish.
If you are in the middle of crafting your armor you will probably want to stop doing that when a
monster enters your smithy.
You can implement this in the same way as you do the "blocking" command above, just in reverse.
Below is an example of a crafting command that can be aborted by starting a fight:
```python
from evennia import utils, default_cmds
class CmdCraftArmour(default_cmds.MuxCommand):
"""
Craft armour
Usage:
craft <name of armour>
This will craft a suit of armour, assuming you
have all the components and tools. Doing some
other action (such as attacking someone) will
abort the crafting process.
"""
key = "craft"
locks = "cmd:all()"
def func(self):
"starts crafting"
if self.caller.ndb.is_crafting:
self.caller.msg("You are already crafting!")
return
if self._is_fighting():
self.caller.msg("You can't start to craft "
"in the middle of a fight!")
return
# [Crafting code, checking of components, skills etc]
# Start crafting
self.caller.ndb.is_crafting = True
self.caller.msg("You start crafting ...")
utils.delay(60, self.step1)
def _is_fighting(self):
"checks if we are in a fight."
if self.caller.ndb.is_fighting:
del self.caller.ndb.is_crafting
return True
def step1(self):
"first step of armour construction"
if self._is_fighting():
return
self.msg("You create the first part of the armour.")
utils.delay(60, callback=self.step2)
def step2(self):
"second step of armour construction"
if self._is_fighting():
return
self.msg("You create the second part of the armour.")
utils.delay(60, step3)
def step3(self):
"last step of armour construction"
if self._is_fighting():
return
# [code for creating the armour object etc]
del self.caller.ndb.is_crafting
self.msg("You finalize your armour.")
# example of a command that aborts crafting
class CmdAttack(default_cmds.MuxCommand):
"""
attack someone
Usage:
attack <target>
Try to cause harm to someone. This will abort
eventual crafting you may be currently doing.
"""
key = "attack"
aliases = ["hit", "stab"]
locks = "cmd:all()"
def func(self):
"Implements the command"
self.caller.ndb.is_fighting = True
# [...]
```
The above code creates a delayed crafting command that will gradually create the armour. If the
`attack` command is issued during this process it will set a flag that causes the crafting to be
quietly canceled next time it tries to update.
## Persistent delays
In the latter examples above we used `.ndb` storage. This is fast and easy but it will reset all
cooldowns/blocks/crafting etc if you reload the server. If you don't want that you can replace
`.ndb` with `.db`. But even this won't help because the `yield` keyword is not persisent and nor is
the use of `delay` shown above. To resolve this you can use `delay` with the `persistent=True`
keyword. But wait! Making something persistent will add some extra complications, because now you
must make sure Evennia can properly store things to the database.
Here is the original echo-command reworked to function with persistence:
```python
from evennia import default_cmds, utils
# this is now in the outermost scope and takes two args!
def echo(caller, args):
"Called after 10 seconds."
shout = args
string = "You hear an echo: %s ... %s ... %s"
string = string % (shout.upper(), shout.capitalize(), shout.lower())
caller.msg(string)
class CmdEcho(default_cmds.MuxCommand):
"""
wait for an echo
Usage:
echo <string>
Calls and waits for an echo
"""
key = "echo"
locks = "cmd:all()"
def func(self):
"""
This is called at the initial shout.
"""
self.caller.msg("You shout '%s' and wait for an echo ..." % self.args)
# this waits non-blocking for 10 seconds, then calls echo(self.caller, self.args)
utils.delay(10, echo, self.caller, self.args, persistent=True) # changes!
```
Above you notice two changes:
- The callback (`echo`) was moved out of the class and became its own stand-alone function in the
outermost scope of the module. It also now takes `caller` and `args` as arguments (it doesn't have
access to them directly since this is now a stand-alone function).
- `utils.delay` specifies the `echo` function (not `self.echo` - it's no longer a method!) and sends
`self.caller` and `self.args` as arguments for it to use. We also set `persistent=True`.
The reason for this change is because Evennia needs to `pickle` the callback into storage and it
cannot do this correctly when the method sits on the command class. Now this behave the same as the
first version except if you reload (or even shut down) the server mid-delay it will still fire the
callback when the server comes back up (it will resume the countdown and ignore the downtime).

View file

@ -0,0 +1,129 @@
# Command Prompt
A *prompt* is quite common in MUDs. The prompt display useful details about your character that you
are likely to want to keep tabs on at all times, such as health, magical power etc. It might also
show things like in-game time, weather and so on. Many modern MUD clients (including Evennia's own
webclient) allows for identifying the prompt and have it appear in a correct location (usually just
above the input line). Usually it will remain like that until it is explicitly updated.
## Sending a prompt
A prompt is sent using the `prompt` keyword to the `msg()` method on objects. The prompt will be
sent without any line breaks.
```python
self.msg(prompt="HP: 5, MP: 2, SP: 8")
```
You can combine the sending of normal text with the sending (updating of the prompt):
```python
self.msg("This is a text", prompt="This is a prompt")
```
You can update the prompt on demand, this is normally done using [OOB](./OOB)-tracking of the relevant
Attributes (like the character's health). You could also make sure that attacking commands update
the prompt when they cause a change in health, for example.
Here is a simple example of the prompt sent/updated from a command class:
```python
from evennia import Command
class CmdDiagnose(Command):
"""
see how hurt your are
Usage:
diagnose [target]
This will give an estimate of the target's health. Also
the target's prompt will be updated.
"""
key = "diagnose"
def func(self):
if not self.args:
target = self.caller
else:
target = self.search(self.args)
if not target:
return
# try to get health, mana and stamina
hp = target.db.hp
mp = target.db.mp
sp = target.db.sp
if None in (hp, mp, sp):
# Attributes not defined
self.caller.msg("Not a valid target!")
return
text = "You diagnose %s as having " \
"%i health, %i mana and %i stamina." \
% (hp, mp, sp)
prompt = "%i HP, %i MP, %i SP" % (hp, mp, sp)
self.caller.msg(text, prompt=prompt)
```
## A prompt sent with every command
The prompt sent as described above uses a standard telnet instruction (the Evennia web client gets a
special flag). Most MUD telnet clients will understand and allow users to catch this and keep the
prompt in place until it updates. So *in principle* you'd not need to update the prompt every
command.
However, with a varying user base it can be unclear which clients are used and which skill level the
users have. So sending a prompt with every command is a safe catch-all. You don't need to manually
go in and edit every command you have though. Instead you edit the base command class for your
custom commands (like `MuxCommand` in your `mygame/commands/command.py` folder) and overload the
`at_post_cmd()` hook. This hook is always called *after* the main `func()` method of the Command.
```python
from evennia import default_cmds
class MuxCommand(default_cmds.MuxCommand):
# ...
def at_post_cmd(self):
"called after self.func()."
caller = self.caller
prompt = "%i HP, %i MP, %i SP" % (caller.db.hp,
caller.db.mp,
caller.db.sp)
caller.msg(prompt=prompt)
```
### Modifying default commands
If you want to add something small like this to Evennia's default commands without modifying them
directly the easiest way is to just wrap those with a multiple inheritance to your own base class:
```python
# in (for example) mygame/commands/mycommands.py
from evennia import default_cmds
# our custom MuxCommand with at_post_cmd hook
from commands.command import MuxCommand
# overloading the look command
class CmdLook(default_cmds.CmdLook, MuxCommand):
pass
```
The result of this is that the hooks from your custom `MuxCommand` will be mixed into the default
`CmdLook` through multiple inheritance. Next you just add this to your default command set:
```python
# in mygame/commands/default_cmdsets.py
from evennia import default_cmds
from commands import mycommands
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# ...
def at_cmdset_creation(self):
# ...
self.add(mycommands.CmdLook())
```
This will automatically replace the default `look` command in your game with your own version.

376
docs/source/Command-Sets.md Normal file
View file

@ -0,0 +1,376 @@
# Command Sets
Command Sets are intimately linked with [Commands](./Commands) and you should be familiar with
Commands before reading this page. The two pages were split for ease of reading.
A *Command Set* (often referred to as a CmdSet or cmdset) is the basic unit for storing one or more
*Commands*. A given Command can go into any number of different command sets. Storing Command
classes in a command set is the way to make commands available to use in your game.
When storing a CmdSet on an object, you will make the commands in that command set available to the
object. An example is the default command set stored on new Characters. This command set contains
all the useful commands, from `look` and `inventory` to `@dig` and `@reload`
([permissions](./Locks#Permissions) then limit which players may use them, but that's a separate
topic).
When an account enters a command, cmdsets from the Account, Character, its location, and elsewhere
are pulled together into a *merge stack*. This stack is merged together in a specific order to
create a single "merged" cmdset, representing the pool of commands available at that very moment.
An example would be a `Window` object that has a cmdset with two commands in it: `look through
window` and `open window`. The command set would be visible to players in the room with the window,
allowing them to use those commands only there. You could imagine all sorts of clever uses of this,
like a `Television` object which had multiple commands for looking at it, switching channels and so
on. The tutorial world included with Evennia showcases a dark room that replaces certain critical
commands with its own versions because the Character cannot see.
If you want a quick start into defining your first commands and using them with command sets, you
can head over to the [Adding Command Tutorial](./Adding-Command-Tutorial) which steps through things
without the explanations.
## Defining Command Sets
A CmdSet is, as most things in Evennia, defined as a Python class inheriting from the correct parent
(`evennia.CmdSet`, which is a shortcut to `evennia.commands.cmdset.CmdSet`). The CmdSet class only
needs to define one method, called `at_cmdset_creation()`. All other class parameters are optional,
but are used for more advanced set manipulation and coding (see the [merge rules](Command-
Sets#merge-rules) section).
```python
# file mygame/commands/mycmdset.py
from evennia import CmdSet
# this is a theoretical custom module with commands we
# created previously: mygame/commands/mycommands.py
from commands import mycommands
class MyCmdSet(CmdSet):
def at_cmdset_creation(self):
"""
The only thing this method should need
to do is to add commands to the set.
"""
self.add(mycommands.MyCommand1())
self.add(mycommands.MyCommand2())
self.add(mycommands.MyCommand3())
```
The CmdSet's `add()` method can also take another CmdSet as input. In this case all the commands
from that CmdSet will be appended to this one as if you added them line by line:
```python
def at_cmdset_creation():
...
self.add(AdditionalCmdSet) # adds all command from this set
...
```
If you added your command to an existing cmdset (like to the default cmdset), that set is already
loaded into memory. You need to make the server aware of the code changes:
```
@reload
```
You should now be able to use the command.
If you created a new, fresh cmdset, this must be added to an object in order to make the commands
within available. A simple way to temporarily test a cmdset on yourself is use the `@py` command to
execute a python snippet:
```python
@py self.cmdset.add('commands.mycmdset.MyCmdSet')
```
This will stay with you until you `@reset` or `@shutdown` the server, or you run
```python
@py self.cmdset.delete('commands.mycmdset.MyCmdSet')
```
In the example above, a specific Cmdset class is removed. Calling `delete` without arguments will
remove the latest added cmdset.
> Note: Command sets added using `cmdset.add` are, by default, *not* persistent in the database.
If you want the cmdset to survive a reload, you can do:
```
@py self.cmdset.add(commands.mycmdset.MyCmdSet, permanent=True)
```
Or you could add the cmdset as the *default* cmdset:
```
@py self.cmdset.add_default(commands.mycmdset.MyCmdSet)
```
An object can only have one "default" cmdset (but can also have none). This is meant as a safe fall-
back even if all other cmdsets fail or are removed. It is always persistent and will not be affected
by `cmdset.delete()`. To remove a default cmdset you must explicitly call `cmdset.remove_default()`.
Command sets are often added to an object in its `at_object_creation` method. For more examples of
adding commands, read the [Step by step tutorial](./Adding-Command-Tutorial). Generally you can
customize which command sets are added to your objects by using `self.cmdset.add()` or
`self.cmdset.add_default()`.
> Important: Commands are identified uniquely by key *or* alias (see [Commands](./Commands)). If any
overlap exists, two commands are considered identical. Adding a Command to a command set that
already has an identical command will *replace* the previous command. This is very important. You
must take this behavior into account when attempting to overload any default Evennia commands with
your own. Otherwise, you may accidentally "hide" your own command in your command set when adding a
new one that has a matching alias.
### Properties on Command Sets
There are several extra flags that you can set on CmdSets in order to modify how they work. All are
optional and will be set to defaults otherwise. Since many of these relate to *merging* cmdsets,
you might want to read the [Adding and Merging Command Sets](./Command-Sets#adding-and-merging-
command-sets) section for some of these to make sense.
- `key` (string) - an identifier for the cmdset. This is optional, but should be unique. It is used
for display in lists, but also to identify special merging behaviours using the `key_mergetype`
dictionary below.
- `mergetype` (string) - allows for one of the following string values: "*Union*", "*Intersect*",
"*Replace*", or "*Remove*".
- `priority` (int) - This defines the merge order of the merge stack - cmdsets will merge in rising
order of priority with the highest priority set merging last. During a merger, the commands from the
set with the higher priority will have precedence (just what happens depends on the [merge
type](Command-Sets#adding-and-merging-command-sets)). If priority is identical, the order in the
merge stack determines preference. The priority value must be greater or equal to `-100`. Most in-
game sets should usually have priorities between `0` and `100`. Evennia default sets have priorities
as follows (these can be changed if you want a different distribution):
- EmptySet: `-101` (should be lower than all other sets)
- SessionCmdSet: `-20`
- AccountCmdSet: `-10`
- CharacterCmdSet: `0`
- ExitCmdSet: ` 101` (generally should always be available)
- ChannelCmdSet: `101` (should usually always be available) - since exits never accept
arguments, there is no collision between exits named the same as a channel even though the commands
"collide".
- `key_mergetype` (dict) - a dict of `key:mergetype` pairs. This allows this cmdset to merge
differently with certain named cmdsets. If the cmdset to merge with has a `key` matching an entry in
`key_mergetype`, it will not be merged according to the setting in `mergetype` but according to the
mode in this dict. Please note that this is more complex than it may seem due to the [merge
order](Command-Sets#adding-and-merging-command-sets) of command sets. Please review that section
before using `key_mergetype`.
- `duplicates` (bool/None default `None`) - this determines what happens when merging same-priority
cmdsets containing same-key commands together. The`dupicate` option will *only* apply when merging
the cmdset with this option onto one other cmdset with the same priority. The resulting cmdset will
*not* retain this `duplicate` setting.
- `None` (default): No duplicates are allowed and the cmdset being merged "onto" the old one
will take precedence. The result will be unique commands. *However*, the system will assume this
value to be `True` for cmdsets on Objects, to avoid dangerous clashes. This is usually the safe bet.
- `False`: Like `None` except the system will not auto-assume any value for cmdsets defined on
Objects.
- `True`: Same-named, same-prio commands will merge into the same cmdset. This will lead to a
multimatch error (the user will get a list of possibilities in order to specify which command they
meant). This is is useful e.g. for on-object cmdsets (example: There is a `red button` and a `green
button` in the room. Both have a `press button` command, in cmdsets with the same priority. This
flag makes sure that just writing `press button` will force the Player to define just which object's
command was intended).
- `no_objs` this is a flag for the cmdhandler that builds the set of commands available at every
moment. It tells the handler not to include cmdsets from objects around the account (nor from rooms
or inventory) when building the merged set. Exit commands will still be included. This option can
have three values:
- `None` (default): Passthrough of any value set explicitly earlier in the merge stack. If never
set explicitly, this acts as `False`.
- `True`/`False`: Explicitly turn on/off. If two sets with explicit `no_objs` are merged,
priority determines what is used.
- `no_exits` - this is a flag for the cmdhandler that builds the set of commands available at every
moment. It tells the handler not to include cmdsets from exits. This flag can have three values:
- `None` (default): Passthrough of any value set explicitly earlier in the merge stack. If
never set explicitly, this acts as `False`.
- `True`/`False`: Explicitly turn on/off. If two sets with explicit `no_exits` are merged,
priority determines what is used.
- `no_channels` (bool) - this is a flag for the cmdhandler that builds the set of commands available
at every moment. It tells the handler not to include cmdsets from available in-game channels. This
flag can have three values:
- `None` (default): Passthrough of any value set explicitly earlier in the merge stack. If
never set explicitly, this acts as `False`.
- `True`/`False`: Explicitly turn on/off. If two sets with explicit `no_channels` are merged,
priority determines what is used.
## Command Sets Searched
When a user issues a command, it is matched against the [merged](./Command-Sets#adding-and-merging-
command-sets) command sets available to the player at the moment. Which those are may change at any
time (such as when the player walks into the room with the `Window` object described earlier).
The currently valid command sets are collected from the following sources:
- The cmdsets stored on the currently active [Session](./Sessions). Default is the empty
`SessionCmdSet` with merge priority `-20`.
- The cmdsets defined on the [Account](./Accounts). Default is the AccountCmdSet with merge priority
`-10`.
- All cmdsets on the Character/Object (assuming the Account is currently puppeting such a
Character/Object). Merge priority `0`.
- The cmdsets of all objects carried by the puppeted Character (checks the `call` lock). Will not be
included if `no_objs` option is active in the merge stack.
- The cmdsets of the Character's current location (checks the `call` lock). Will not be included if
`no_objs` option is active in the merge stack.
- The cmdsets of objects in the current location (checks the `call` lock). Will not be included if
`no_objs` option is active in the merge stack.
- The cmdsets of Exits in the location. Merge priority `+101`. Will not be included if `no_exits`
*or* `no_objs` option is active in the merge stack.
- The [channel](./Communications) cmdset containing commands for posting to all channels the account
or character is currently connected to. Merge priority `+101`. Will not be included if `no_channels`
option is active in the merge stack.
Note that an object does not *have* to share its commands with its surroundings. A Character's
cmdsets should not be shared for example, or all other Characters would get multi-match errors just
by being in the same room. The ability of an object to share its cmdsets is managed by its `call`
[lock](./Locks). For example, [Character objects](./Objects) defaults to `call:false()` so that any
cmdsets on them can only be accessed by themselves, not by other objects around them. Another
example might be to lock an object with `call:inside()` to only make their commands available to
objects inside them, or `cmd:holds()` to make their commands available only if they are held.
## Adding and Merging Command Sets
*Note: This is an advanced topic. It's very useful to know about, but you might want to skip it if
this is your first time learning about commands.*
CmdSets have the special ability that they can be *merged* together into new sets. Which of the
ingoing commands end up in the merged set is defined by the *merge rule* and the relative
*priorities* of the two sets. Removing the latest added set will restore things back to the way it
was before the addition.
CmdSets are non-destructively stored in a stack inside the cmdset handler on the object. This stack
is parsed to create the "combined" cmdset active at the moment. CmdSets from other sources are also
included in the merger such as those on objects in the same room (like buttons to press) or those
introduced by state changes (such as when entering a menu). The cmdsets are all ordered after
priority and then merged together in *reverse order*. That is, the higher priority will be merged
"onto" lower-prio ones. By defining a cmdset with a merge-priority between that of two other sets,
you will make sure it will be merged in between them.
The very first cmdset in this stack is called the *Default cmdset* and is protected from accidental
deletion. Running `obj.cmdset.delete()` will never delete the default set. Instead one should add
new cmdsets on top of the default to "hide" it, as described below. Use the special
`obj.cmdset.delete_default()` only if you really know what you are doing.
CmdSet merging is an advanced feature useful for implementing powerful game effects. Imagine for
example a player entering a dark room. You don't want the player to be able to find everything in
the room at a glance - maybe you even want them to have a hard time to find stuff in their backpack!
You can then define a different CmdSet with commands that override the normal ones. While they are
in the dark room, maybe the `look` and `inv` commands now just tell the player they cannot see
anything! Another example would be to offer special combat commands only when the player is in
combat. Or when being on a boat. Or when having taken the super power-up. All this can be done on
the fly by merging command sets.
### Merge Rules
Basic rule is that command sets are merged in *reverse priority order*. That is, lower-prio sets are
merged first and higher prio sets are merged "on top" of them. Think of it like a layered cake with
the highest priority on top.
To further understand how sets merge, we need to define some examples. Let's call the first command
set **A** and the second **B**. We assume **B** is the command set already active on our object and
we will merge **A** onto **B**. In code terms this would be done by `object.cdmset.add(A)`.
Remember, B is already active on `object` from before.
We let the **A** set have higher priority than **B**. A priority is simply an integer number. As
seen in the list above, Evennia's default cmdsets have priorities in the range `-101` to `120`. You
are usually safe to use a priority of `0` or `1` for most game effects.
In our examples, both sets contain a number of commands which we'll identify by numbers, like `A1,
A2` for set **A** and `B1, B2, B3, B4` for **B**. So for that example both sets contain commands
with the same keys (or aliases) "1" and "2" (this could for example be "look" and "get" in the real
game), whereas commands 3 and 4 are unique to **B**. To describe a merge between these sets, we
would write `A1,A2 + B1,B2,B3,B4 = ?` where `?` is a list of commands that depend on which merge
type **A** has, and which relative priorities the two sets have. By convention, we read this
statement as "New command set **A** is merged onto the old command set **B** to form **?**".
Below are the available merge types and how they work. Names are partly borrowed from [Set
theory](http://en.wikipedia.org/wiki/Set_theory).
- **Union** (default) - The two cmdsets are merged so that as many commands as possible from each
cmdset ends up in the merged cmdset. Same-key commands are merged by priority.
# Union
A1,A2 + B1,B2,B3,B4 = A1,A2,B3,B4
- **Intersect** - Only commands found in *both* cmdsets (i.e. which have the same keys) end up in
the merged cmdset, with the higher-priority cmdset replacing the lower one's commands.
# Intersect
A1,A3,A5 + B1,B2,B4,B5 = A1,A5
- **Replace** - The commands of the higher-prio cmdset completely replaces the lower-priority
cmdset's commands, regardless of if same-key commands exist or not.
# Replace
A1,A3 + B1,B2,B4,B5 = A1,A3
- **Remove** - The high-priority command sets removes same-key commands from the lower-priority
cmdset. They are not replaced with anything, so this is a sort of filter that prunes the low-prio
set using the high-prio one as a template.
# Remove
A1,A3 + B1,B2,B3,B4,B5 = B2,B4,B5
Besides `priority` and `mergetype`, a command-set also takes a few other variables to control how
they merge:
- `duplicates` (bool) - determines what happens when two sets of equal priority merge. Default is
that the new set in the merger (i.e. **A** above) automatically takes precedence. But if
*duplicates* is true, the result will be a merger with more than one of each name match. This will
usually lead to the player receiving a multiple-match error higher up the road, but can be good for
things like cmdsets on non-player objects in a room, to allow the system to warn that more than one
'ball' in the room has the same 'kick' command defined on it and offer a chance to select which
ball to kick ... Allowing duplicates only makes sense for *Union* and *Intersect*, the setting is
ignored for the other mergetypes.
- `key_mergetypes` (dict) - allows the cmdset to define a unique mergetype for particular cmdsets,
identified by their cmdset `key`. Format is `{CmdSetkey:mergetype}`. Example:
`{'Myevilcmdset','Replace'}` which would make sure for this set to always use 'Replace' on the
cmdset with the key `Myevilcmdset` only, no matter what the main `mergetype` is set to.
> Warning: The `key_mergetypes` dictionary *can only work on the cmdset we merge onto*. When using
`key_mergetypes` it is thus important to consider the merge priorities - you must make sure that you
pick a priority *between* the cmdset you want to detect and the next higher one, if any. That is, if
we define a cmdset with a high priority and set it to affect a cmdset that is far down in the merge
stack, we would not "see" that set when it's time for us to merge. Example: Merge stack is
`A(prio=-10), B(prio=-5), C(prio=0), D(prio=5)`. We now merge a cmdset `E(prio=10)` onto this stack,
with a `key_mergetype={"B":"Replace"}`. But priorities dictate that we won't be merged onto B, we
will be merged onto E (which is a merger of the lower-prio sets at this point). Since we are merging
onto E and not B, our `key_mergetype` directive won't trigger. To make sure it works we must make
sure we merge onto B. Setting E's priority to, say, -4 will make sure to merge it onto B and affect
it appropriately.
More advanced cmdset example:
```python
from commands import mycommands
class MyCmdSet(CmdSet):
key = "MyCmdSet"
priority = 4
mergetype = "Replace"
key_mergetypes = {'MyOtherCmdSet':'Union'}
def at_cmdset_creation(self):
"""
The only thing this method should need
to do is to add commands to the set.
"""
self.add(mycommands.MyCommand1())
self.add(mycommands.MyCommand2())
self.add(mycommands.MyCommand3())
```
### Assorted Notes
It is very important to remember that two commands are compared *both* by their `key` properties
*and* by their `aliases` properties. If either keys or one of their aliases match, the two commands
are considered the *same*. So consider these two Commands:
- A Command with key "kick" and alias "fight"
- A Command with key "punch" also with an alias "fight"
During the cmdset merging (which happens all the time since also things like channel commands and
exits are merged in), these two commands will be considered *identical* since they share alias. It
means only one of them will remain after the merger. Each will also be compared with all other
commands having any combination of the keys and/or aliases "kick", "punch" or "fight".
... So avoid duplicate aliases, it will only cause confusion.

View file

@ -0,0 +1,9 @@
# Command System
- [Commands](./Commands)
- [Command Sets](./Command-Sets)
- [Command Auto-help](./Help-System#command-auto-help-system)
See also:
- [Default Command Help](./Default-Command-Help)
- [Adding Command Tutorial](./Adding-Command-Tutorial)

663
docs/source/Commands.md Normal file
View file

@ -0,0 +1,663 @@
# Commands
Commands are intimately linked to [Command Sets](./Command-Sets) and you need to read that page too to
be familiar with how the command system works. The two pages were split for easy reading.
The basic way for users to communicate with the game is through *Commands*. These can be commands
directly related to the game world such as *look*, *get*, *drop* and so on, or administrative
commands such as *examine* or *@dig*.
The [default commands](./Default-Command-Help) coming with Evennia are 'MUX-like' in that they use @
for admin commands, support things like switches, syntax with the '=' symbol etc, but there is
nothing that prevents you from implementing a completely different command scheme for your game. You
can find the default commands in `evennia/commands/default`. You should not edit these directly -
they will be updated by the Evennia team as new features are added. Rather you should look to them
for inspiration and inherit your own designs from them.
There are two components to having a command running - the *Command* class and the [Command
Set](Command-Sets) (command sets were split into a separate wiki page for ease of reading).
1. A *Command* is a python class containing all the functioning code for what a command does - for
example, a *get* command would contain code for picking up objects.
1. A *Command Set* (often referred to as a CmdSet or cmdset) is like a container for one or more
Commands. A given Command can go into any number of different command sets. Only by putting the
command set on a character object you will make all the commands therein available to use by that
character. You can also store command sets on normal objects if you want users to be able to use the
object in various ways. Consider a "Tree" object with a cmdset defining the commands *climb* and
*chop down*. Or a "Clock" with a cmdset containing the single command *check time*.
This page goes into full detail about how to use Commands. To fully use them you must also read the
page detailing [Command Sets](./Command-Sets). There is also a step-by-step [Adding Command
Tutorial](Adding-Command-Tutorial) that will get you started quickly without the extra explanations.
## Defining Commands
All commands are implemented as normal Python classes inheriting from the base class `Command`
(`evennia.Command`). You will find that this base class is very "bare". The default commands of
Evennia actually inherit from a child of `Command` called `MuxCommand` - this is the class that
knows all the mux-like syntax like `/switches`, splitting by "=" etc. Below we'll avoid mux-
specifics and use the base `Command` class directly.
```python
# basic Command definition
from evennia import Command
class MyCmd(Command):
"""
This is the help-text for the command
"""
key = "mycommand"
def parse(self):
# parsing the command line here
def func(self):
# executing the command here
```
Here is a minimalistic command with no custom parsing:
```python
from evennia import Command
class CmdEcho(Command):
key = "echo"
def func(self):
# echo the caller's input back to the caller
self.caller.msg("Echo: {}".format(self.args)
```
You define a new command by assigning a few class-global properties on your inherited class and
overloading one or two hook functions. The full gritty mechanic behind how commands work are found
towards the end of this page; for now you only need to know that the command handler creates an
instance of this class and uses that instance whenever you use this command - it also dynamically
assigns the new command instance a few useful properties that you can assume to always be available.
### Who is calling the command?
In Evennia there are three types of objects that may call the command. It is important to be aware
of this since this will also assign appropriate `caller`, `session`, `sessid` and `account`
properties on the command body at runtime. Most often the calling type is `Session`.
* A [Session](./Sessions). This is by far the most common case when a user is entering a command in
their client.
* `caller` - this is set to the puppeted [Object](./Objects) if such an object exists. If no
puppet is found, `caller` is set equal to `account`. Only if an Account is not found either (such as
before being logged in) will this be set to the Session object itself.
* `session` - a reference to the [Session](./Sessions) object itself.
* `sessid` - `sessid.id`, a unique integer identifier of the session.
* `account` - the [Account](./Accounts) object connected to this Session. None if not logged in.
* An [Account](./Accounts). This only happens if `account.execute_cmd()` was used. No Session
information can be obtained in this case.
* `caller` - this is set to the puppeted Object if such an object can be determined (without
Session info this can only be determined in `MULTISESSION_MODE=0` or `1`). If no puppet is found,
this is equal to `account`.
* `session` - `None*`
* `sessid` - `None*`
* `account` - Set to the Account object.
* An [Object](./Objects). This only happens if `object.execute_cmd()` was used (for example by an
NPC).
* `caller` - This is set to the calling Object in question.
* `session` - `None*`
* `sessid` - `None*`
* `account` - `None`
> `*)`: There is a way to make the Session available also inside tests run directly on Accounts and
Objects, and that is to pass it to `execute_cmd` like so: `account.execute_cmd("...",
session=<Session>)`. Doing so *will* make the `.session` and `.sessid` properties available in the
command.
### Properties assigned to the command instance at run-time
Let's say account *Bob* with a character *BigGuy* enters the command *look at sword*. After the
system having successfully identified this as the "look" command and determined that BigGuy really
has access to a command named `look`, it chugs the `look` command class out of storage and either
loads an existing Command instance from cache or creates one. After some more checks it then assigns
it the following properties:
- `caller` - The character BigGuy, in this example. This is a reference to the object executing the
command. The value of this depends on what type of object is calling the command; see the previous
section.
- `session` - the [Session](./Sessions) Bob uses to connect to the game and control BigGuy (see also
previous section).
- `sessid` - the unique id of `self.session`, for quick lookup.
- `account` - the [Account](./Accounts) Bob (see previous section).
- `cmdstring` - the matched key for the command. This would be *look* in our example.
- `args` - this is the rest of the string, except the command name. So if the string entered was
*look at sword*, `args` would be " *at sword*". Note the space kept - Evennia would correctly
interpret `lookat sword` too. This is useful for things like `/switches` that should not use space.
In the `MuxCommand` class used for default commands, this space is stripped. Also see the
`arg_regex` property if you want to enforce a space to make `lookat sword` give a command-not-found
error.
- `obj` - the game [Object](./Objects) on which this command is defined. This need not be the caller,
but since `look` is a common (default) command, this is probably defined directly on *BigGuy* - so
`obj` will point to BigGuy. Otherwise `obj` could be an Account or any interactive object with
commands defined on it, like in the example of the "check time" command defined on a "Clock" object.
- `cmdset` - this is a reference to the merged CmdSet (see below) from which this command was
matched. This variable is rarely used, it's main use is for the [auto-help system](Help-
System#command-auto-help-system) (*Advanced note: the merged cmdset need NOT be the same as
`BigGuy.cmdset`. The merged set can be a combination of the cmdsets from other objects in the room,
for example*).
- `raw_string` - this is the raw input coming from the user, without stripping any surrounding
whitespace. The only thing that is stripped is the ending newline marker.
#### Other useful utility methods:
- `.get_help(caller, cmdset)` - Get the help entry for this command. By default the arguments are
not
used, but they could be used to implement alternate help-display systems.
- `.client_width()` - Shortcut for getting the client's screen-width. Note that not all clients will
truthfully report this value - that case the `settings.DEFAULT_SCREEN_WIDTH` will be returned.
- `.styled_table(*args, **kwargs)` - This returns an [EvTable](api:evennia.utils#module-
evennia.utils.evtable) styled based on the
session calling this command. The args/kwargs are the same as for EvTable, except styling defaults
are set.
- `.styled_header`, `_footer`, `separator` - These will produce styled decorations for
display to the user. They are useful for creating listings and forms with colors adjustable per-
user.
### Defining your own command classes
Beyond the properties Evennia always assigns to the command at run-time (listed above), your job is
to define the following class properties:
- `key` (string) - the identifier for the command, like `look`. This should (ideally) be unique. A
key can consist of more than one word, like "press button" or "pull left lever". Note that *both*
`key` and `aliases` below determine the identity of a command. So two commands are considered if
either matches. This is important for merging cmdsets described below.
- `aliases` (optional list) - a list of alternate names for the command (`["glance", "see", "l"]`).
Same name rules as for `key` applies.
- `locks` (string) - a [lock definition](./Locks), usually on the form `cmd:<lockfuncs>`. Locks is a
rather big topic, so until you learn more about locks, stick to giving the lockstring `"cmd:all()"`
to make the command available to everyone (if you don't provide a lock string, this will be assigned
for you).
- `help_category` (optional string) - setting this helps to structure the auto-help into categories.
If none is set, this will be set to *General*.
- `save_for_next` (optional boolean). This defaults to `False`. If `True`, a copy of this command
object (along with any changes you have done to it) will be stored by the system and can be accessed
by the next command by retrieving `self.caller.ndb.last_cmd`. The next run command will either clear
or replace the storage.
- `arg_regex` (optional raw string): Used to force the parser to limit itself and tell it when the
command-name ends and arguments begin (such as requiring this to be a space or a /switch). This is
done with a regular expression. [See the arg_regex section](./Commands#on-arg_regex) for the details.
- `auto_help` (optional boolean). Defaults to `True`. This allows for turning off the [auto-help
system](Help-System#command-auto-help-system) on a per-command basis. This could be useful if you
either want to write your help entries manually or hide the existence of a command from `help`'s
generated list.
- `is_exit` (bool) - this marks the command as being used for an in-game exit. This is, by default,
set by all Exit objects and you should not need to set it manually unless you make your own Exit
system. It is used for optimization and allows the cmdhandler to easily disregard this command when
the cmdset has its `no_exits` flag set.
- `is_channel` (bool)- this marks the command as being used for an in-game channel. This is, by
default, set by all Channel objects and you should not need to set it manually unless you make your
own Channel system. is used for optimization and allows the cmdhandler to easily disregard this
command when its cmdset has its `no_channels` flag set.
- `msg_all_sessions` (bool): This affects the behavior of the `Command.msg` method. If unset
(default), calling `self.msg(text)` from the Command will always only send text to the Session that
actually triggered this Command. If set however, `self.msg(text)` will send to all Sessions relevant
to the object this Command sits on. Just which Sessions receives the text depends on the object and
the server's `MULTISESSION_MODE`.
You should also implement at least two methods, `parse()` and `func()` (You could also implement
`perm()`, but that's not needed unless you want to fundamentally change how access checks work).
- `at_pre_cmd()` is called very first on the command. If this function returns anything that
evaluates to `True` the command execution is aborted at this point.
- `parse()` is intended to parse the arguments (`self.args`) of the function. You can do this in any
way you like, then store the result(s) in variable(s) on the command object itself (i.e. on `self`).
To take an example, the default mux-like system uses this method to detect "command switches" and
store them as a list in `self.switches`. Since the parsing is usually quite similar inside a command
scheme you should make `parse()` as generic as possible and then inherit from it rather than re-
implementing it over and over. In this way, the default `MuxCommand` class implements a `parse()`
for all child commands to use.
- `func()` is called right after `parse()` and should make use of the pre-parsed input to actually
do whatever the command is supposed to do. This is the main body of the command. The return value
from this method will be returned from the execution as a Twisted Deferred.
- `at_post_cmd()` is called after `func()` to handle eventual cleanup.
Finally, you should always make an informative [doc
string](http://www.python.org/dev/peps/pep-0257/#what-is-a-docstring) (`__doc__`) at the top of your
class. This string is dynamically read by the [Help System](./Help-System) to create the help entry
for this command. You should decide on a way to format your help and stick to that.
Below is how you define a simple alternative "`smile`" command:
```python
from evennia import Command
class CmdSmile(Command):
"""
A smile command
Usage:
smile [at] [<someone>]
grin [at] [<someone>]
Smiles to someone in your vicinity or to the room
in general.
(This initial string (the __doc__ string)
is also used to auto-generate the help
for this command)
"""
key = "smile"
aliases = ["smile at", "grin", "grin at"]
locks = "cmd:all()"
help_category = "General"
def parse(self):
"Very trivial parser"
self.target = self.args.strip()
def func(self):
"This actually does things"
caller = self.caller
if not self.target or self.target == "here":
string = f"{caller.key} smiles"
else:
target = caller.search(self.target)
if not target:
return
string = f"{caller.key} smiles at {target.key}"
caller.location.msg_contents(string)
```
The power of having commands as classes and to separate `parse()` and `func()`
lies in the ability to inherit functionality without having to parse every
command individually. For example, as mentioned the default commands all
inherit from `MuxCommand`. `MuxCommand` implements its own version of `parse()`
that understands all the specifics of MUX-like commands. Almost none of the
default commands thus need to implement `parse()` at all, but can assume the
incoming string is already split up and parsed in suitable ways by its parent.
Before you can actually use the command in your game, you must now store it
within a *command set*. See the [Command Sets](./Command-Sets) page.
### On arg_regex
The command parser is very general and does not require a space to end your command name. This means
that the alias `:` to `emote` can be used like `:smiles` without modification. It also means
`getstone` will get you the stone (unless there is a command specifically named `getstone`, then
that will be used). If you want to tell the parser to require a certain separator between the
command name and its arguments (so that `get stone` works but `getstone` gives you a 'command not
found' error) you can do so with the `arg_regex` property.
The `arg_regex` is a [raw regular expression string](http://docs.python.org/library/re.html). The
regex will be compiled by the system at runtime. This allows you to customize how the part
*immediately following* the command name (or alias) must look in order for the parser to match for
this command. Some examples:
- `commandname argument` (`arg_regex = r"\s.+"`): This forces the parser to require the command name
to be followed by one or more spaces. Whatever is entered after the space will be treated as an
argument. However, if you'd forget the space (like a command having no arguments), this would *not*
match `commandname`.
- `commandname` or `commandname argument` (`arg_regex = r"\s.+|$"`): This makes both `look` and
`look me` work but `lookme` will not.
- `commandname/switches arguments` (`arg_regex = r"(?:^(?:\s+|\/).*$)|^$"`. If you are using
Evennia's `MuxCommand` Command parent, you may wish to use this since it will allow `/switche`s to
work as well as having or not having a space.
The `arg_regex` allows you to customize the behavior of your commands. You can put it in the parent
class of your command to customize all children of your Commands. However, you can also change the
base default behavior for all Commands by modifying `settings.COMMAND_DEFAULT_ARG_REGEX`.
## Exiting a command
Normally you just use `return` in one of your Command class' hook methods to exit that method. That
will however still fire the other hook methods of the Command in sequence. That's usually what you
want but sometimes it may be useful to just abort the command, for example if you find some
unacceptable input in your parse method. To exit the command this way you can raise
`evennia.InterruptCommand`:
```python
from evennia import InterruptCommand
class MyCommand(Command):
# ...
def parse(self):
# ...
# if this fires, `func()` and `at_post_cmd` will not
# be called at all
raise InterruptCommand()
```
## Pauses in commands
Sometimes you want to pause the execution of your command for a little while before continuing -
maybe you want to simulate a heavy swing taking some time to finish, maybe you want the echo of your
voice to return to you with an ever-longer delay. Since Evennia is running asynchronously, you
cannot use `time.sleep()` in your commands (or anywhere, really). If you do, the *entire game* will
be frozen for everyone! So don't do that. Fortunately, Evennia offers a really quick syntax for
making pauses in commands.
In your `func()` method, you can use the `yield` keyword. This is a Python keyword that will freeze
the current execution of your command and wait for more before processing.
> Note that you *cannot* just drop `yield` into any code and expect it to pause. Evennia will only
pause for you if you `yield` inside the Command's `func()` method. Don't expect it to work anywhere
else.
Here's an example of a command using a small pause of five seconds between messages:
```python
from evennia import Command
class CmdWait(Command):
"""
A dummy command to show how to wait
Usage:
wait
"""
key = "wait"
locks = "cmd:all()"
help_category = "General"
def func(self):
"""Command execution."""
self.msg("Starting to wait ...")
yield 5
self.msg("... This shows after 5 seconds. Waiting ...")
yield 2
self.msg("... And now another 2 seconds have passed.")
```
The important line is the `yield 5` and `yield 2` lines. It will tell Evennia to pause execution
here and not continue until the number of seconds given has passed.
There are two things to remember when using `yield` in your Command's `func` method:
1. The paused state produced by the `yield` is not saved anywhere. So if the server reloads in the
middle of your command pausing, it will *not* resume when the server comes back up - the remainder
of the command will never fire. So be careful that you are not freezing the character or account in
a way that will not be cleared on reload.
2. If you use `yield` you may not also use `return <values>` in your `func` method. You'll get an
error explaining this. This is due to how Python generators work. You can however use a "naked"
`return` just fine. Usually there is no need for `func` to return a value, but if you ever do need
to mix `yield` with a final return value in the same `func`, look at [twisted.internet.defer.returnV
alue](https://twistedmatrix.com/documents/current/api/twisted.internet.defer.html#returnValue).
## Asking for user input
The `yield` keyword can also be used to ask for user input. Again you can't
use Python's `input` in your command, for it would freeze Evennia for
everyone while waiting for that user to input their text. Inside a Command's
`func` method, the following syntax can also be used:
```python
answer = yield("Your question")
```
Here's a very simple example:
```python
class CmdConfirm(Command):
"""
A dummy command to show confirmation.
Usage:
confirm
"""
key = "confirm"
def func(self):
answer = yield("Are you sure you want to go on?")
if answer.strip().lower() in ("yes", "y"):
self.msg("Yes!")
else:
self.msg("No!")
```
This time, when the user enters the 'confirm' command, she will be asked if she wants to go on.
Entering 'yes' or "y" (regardless of case) will give the first reply, otherwise the second reply
will show.
> Note again that the `yield` keyword does not store state. If the game reloads while waiting for
the user to answer, the user will have to start over. It is not a good idea to use `yield` for
important or complex choices, a persistent [EvMenu](./EvMenu) might be more appropriate in this case.
## System commands
*Note: This is an advanced topic. Skip it if this is your first time learning about commands.*
There are several command-situations that are exceptional in the eyes of the server. What happens if
the account enters an empty string? What if the 'command' given is infact the name of a channel the
user wants to send a message to? Or if there are multiple command possibilities?
Such 'special cases' are handled by what's called *system commands*. A system command is defined
in the same way as other commands, except that their name (key) must be set to one reserved by the
engine (the names are defined at the top of `evennia/commands/cmdhandler.py`). You can find (unused)
implementations of the system commands in `evennia/commands/default/system_commands.py`. Since these
are not (by default) included in any `CmdSet` they are not actually used, they are just there for
show. When the special situation occurs, Evennia will look through all valid `CmdSet`s for your
custom system command. Only after that will it resort to its own, hard-coded implementation.
Here are the exceptional situations that triggers system commands. You can find the command keys
they use as properties on `evennia.syscmdkeys`:
- No input (`syscmdkeys.CMD_NOINPUT`) - the account just pressed return without any input. Default
is to do nothing, but it can be useful to do something here for certain implementations such as line
editors that interpret non-commands as text input (an empty line in the editing buffer).
- Command not found (`syscmdkeys.CMD_NOMATCH`) - No matching command was found. Default is to
display the "Huh?" error message.
- Several matching commands where found (`syscmdkeys.CMD_MULTIMATCH`) - Default is to show a list of
matches.
- User is not allowed to execute the command (`syscmdkeys.CMD_NOPERM`) - Default is to display the
"Huh?" error message.
- Channel (`syscmdkeys.CMD_CHANNEL`) - This is a [Channel](./Communications) name of a channel you are
subscribing to - Default is to relay the command's argument to that channel. Such commands are
created by the Comm system on the fly depending on your subscriptions.
- New session connection (`syscmdkeys.CMD_LOGINSTART`). This command name should be put in the
`settings.CMDSET_UNLOGGEDIN`. Whenever a new connection is established, this command is always
called on the server (default is to show the login screen).
Below is an example of redefining what happens when the account doesn't provide any input (e.g. just
presses return). Of course the new system command must be added to a cmdset as well before it will
work.
```python
from evennia import syscmdkeys, Command
class MyNoInputCommand(Command):
"Usage: Just press return, I dare you"
key = syscmdkeys.CMD_NOINPUT
def func(self):
self.caller.msg("Don't just press return like that, talk to me!")
```
## Dynamic Commands
*Note: This is an advanced topic.*
Normally Commands are created as fixed classes and used without modification. There are however
situations when the exact key, alias or other properties is not possible (or impractical) to pre-
code ([Exits](./Commands#Exits) is an example of this).
To create a command with a dynamic call signature, first define the command body normally in a class
(set your `key`, `aliases` to default values), then use the following call (assuming the command
class you created is named `MyCommand`):
```python
cmd = MyCommand(key="newname",
aliases=["test", "test2"],
locks="cmd:all()",
...)
```
*All* keyword arguments you give to the Command constructor will be stored as a property on the
command object. This will overload existing properties defined on the parent class.
Normally you would define your class and only overload things like `key` and `aliases` at run-time.
But you could in principle also send method objects (like `func`) as keyword arguments in order to
make your command completely customized at run-time.
## Exits
*Note: This is an advanced topic.*
Exits are examples of the use of a [Dynamic Command](./Commands#Dynamic_Commands).
The functionality of [Exit](./Objects) objects in Evennia is not hard-coded in the engine. Instead
Exits are normal [typeclassed](./Typeclasses) objects that auto-create a [CmdSet](./Commands#CmdSets) on
themselves when they load. This cmdset has a single dynamically created Command with the same
properties (key, aliases and locks) as the Exit object itself. When entering the name of the exit,
this dynamic exit-command is triggered and (after access checks) moves the Character to the exit's
destination.
Whereas you could customize the Exit object and its command to achieve completely different
behaviour, you will usually be fine just using the appropriate `traverse_*` hooks on the Exit
object. But if you are interested in really changing how things work under the hood, check out
`evennia/objects/objects.py` for how the `Exit` typeclass is set up.
## Command instances are re-used
*Note: This is an advanced topic that can be skipped when first learning about Commands.*
A Command class sitting on an object is instantiated once and then re-used. So if you run a command
from object1 over and over you are in fact running the same command instance over and over (if you
run the same command but sitting on object2 however, it will be a different instance). This is
usually not something you'll notice, since every time the Command-instance is used, all the relevant
properties on it will be overwritten. But armed with this knowledge you can implement some of the
more exotic command mechanism out there, like the command having a 'memory' of what you last entered
so that you can back-reference the previous arguments etc.
> Note: On a server reload, all Commands are rebuilt and memory is flushed.
To show this in practice, consider this command:
```python
class CmdTestID(Command):
key = "testid"
def func(self):
if not hasattr(self, "xval"):
self.xval = 0
self.xval += 1
self.caller.msg("Command memory ID: {} (xval={})".format(id(self), self.xval))
```
Adding this to the default character cmdset gives a result like this in-game:
```
> testid
Command memory ID: 140313967648552 (xval=1)
> testid
Command memory ID: 140313967648552 (xval=2)
> testid
Command memory ID: 140313967648552 (xval=3)
```
Note how the in-memory address of the `testid` command never changes, but `xval` keeps ticking up.
## Dynamically created commands
*This is also an advanced topic.*
Commands can also be created and added to a cmdset on the fly. Creating a class instance with a
keyword argument, will assign that keyword argument as a property on this paricular command:
```
class MyCmdSet(CmdSet):
def at_cmdset_creation(self):
self.add(MyCommand(myvar=1, foo="test")
```
This will start the `MyCommand` with `myvar` and `foo` set as properties (accessable as `self.myvar`
and `self.foo`). How they are used is up to the Command. Remember however the discussion from the
previous section - since the Command instance is re-used, those properties will *remain* on the
command as long as this cmdset and the object it sits is in memory (i.e. until the next reload).
Unless `myvar` and `foo` are somehow reset when the command runs, they can be modified and that
change will be remembered for subsequent uses of the command.
## How commands actually work
*Note: This is an advanced topic mainly of interest to server developers.*
Any time the user sends text to Evennia, the server tries to figure out if the text entered
corresponds to a known command. This is how the command handler sequence looks for a logged-in user:
1. A user enters a string of text and presses enter.
2. The user's Session determines the text is not some protocol-specific control sequence or OOB
command, but sends it on to the command handler.
3. Evennia's *command handler* analyzes the Session and grabs eventual references to Account and
eventual puppeted Characters (these will be stored on the command object later). The *caller*
property is set appropriately.
4. If input is an empty string, resend command as `CMD_NOINPUT`. If no such command is found in
cmdset, ignore.
5. If command.key matches `settings.IDLE_COMMAND`, update timers but don't do anything more.
6. The command handler gathers the CmdSets available to *caller* at this time:
- The caller's own currently active CmdSet.
- CmdSets defined on the current account, if caller is a puppeted object.
- CmdSets defined on the Session itself.
- The active CmdSets of eventual objects in the same location (if any). This includes commands
on [Exits](./Objects#Exits).
- Sets of dynamically created *System commands* representing available
[Communications](./Communications#Channels).
7. All CmdSets *of the same priority* are merged together in groups. Grouping avoids order-
dependent issues of merging multiple same-prio sets onto lower ones.
8. All the grouped CmdSets are *merged* in reverse priority into one combined CmdSet according to
each set's merge rules.
9. Evennia's *command parser* takes the merged cmdset and matches each of its commands (using its
key and aliases) against the beginning of the string entered by *caller*. This produces a set of
candidates.
10. The *cmd parser* next rates the matches by how many characters they have and how many percent
matches the respective known command. Only if candidates cannot be separated will it return multiple
matches.
- If multiple matches were returned, resend as `CMD_MULTIMATCH`. If no such command is found in
cmdset, return hard-coded list of matches.
- If no match was found, resend as `CMD_NOMATCH`. If no such command is found in cmdset, give
hard-coded error message.
11. If a single command was found by the parser, the correct command object is plucked out of
storage. This usually doesn't mean a re-initialization.
12. It is checked that the caller actually has access to the command by validating the *lockstring*
of the command. If not, it is not considered as a suitable match and `CMD_NOMATCH` is triggered.
13. If the new command is tagged as a channel-command, resend as `CMD_CHANNEL`. If no such command
is found in cmdset, use hard-coded implementation.
14. Assign several useful variables to the command instance (see previous sections).
15. Call `at_pre_command()` on the command instance.
16. Call `parse()` on the command instance. This is fed the remainder of the string, after the name
of the command. It's intended to pre-parse the string into a form useful for the `func()` method.
17. Call `func()` on the command instance. This is the functional body of the command, actually
doing useful things.
18. Call `at_post_command()` on the command instance.
## Assorted notes
The return value of `Command.func()` is a Twisted
[deferred](http://twistedmatrix.com/documents/current/core/howto/defer.html).
Evennia does not use this return value at all by default. If you do, you must
thus do so asynchronously, using callbacks.
```python
# in command class func()
def callback(ret, caller):
caller.msg("Returned is %s" % ret)
deferred = self.execute_command("longrunning")
deferred.addCallback(callback, self.caller)
```
This is probably not relevant to any but the most advanced/exotic designs (one might use it to
create a "nested" command structure for example).
The `save_for_next` class variable can be used to implement state-persistent commands. For example
it can make a command operate on "it", where it is determined by what the previous command operated
on.

View file

@ -0,0 +1,113 @@
# Communications
Apart from moving around in the game world and talking, players might need other forms of
communication. This is offered by Evennia's `Comm` system. Stock evennia implements a 'MUX-like'
system of channels, but there is nothing stopping you from changing things to better suit your
taste.
Comms rely on two main database objects - `Msg` and `Channel`. There is also the `TempMsg` which
mimics the API of a `Msg` but has no connection to the database.
## Msg
The `Msg` object is the basic unit of communication in Evennia. A message works a little like an
e-mail; it always has a sender (a [Account](./Accounts)) and one or more recipients. The recipients
may be either other Accounts, or a *Channel* (see below). You can mix recipients to send the message
to both Channels and Accounts if you like.
Once created, a `Msg` is normally not changed. It is peristently saved in the database. This allows
for comprehensive logging of communications. This could be useful for allowing senders/receivers to
have 'mailboxes' with the messages they want to keep.
### Properties defined on `Msg`
- `senders` - this is a reference to one or many [Account](./Accounts) or [Objects](./Objects) (normally
*Characters*) sending the message. This could also be an *External Connection* such as a message
coming in over IRC/IMC2 (see below). There is usually only one sender, but the types can also be
mixed in any combination.
- `receivers` - a list of target [Accounts](./Accounts), [Objects](./Objects) (usually *Characters*) or
*Channels* to send the message to. The types of receivers can be mixed in any combination.
- `header` - this is a text field for storing a title or header for the message.
- `message` - the actual text being sent.
- `date_sent` - when message was sent (auto-created).
- `locks` - a [lock definition](./Locks).
- `hide_from` - this can optionally hold a list of objects, accounts or channels to hide this `Msg`
from. This relationship is stored in the database primarily for optimization reasons, allowing for
quickly post-filter out messages not intended for a given target. There is no in-game methods for
setting this, it's intended to be done in code.
You create new messages in code using `evennia.create_message` (or
`evennia.utils.create.create_message.`)
## TempMsg
`evennia.comms.models` also has `TempMsg` which mimics the API of `Msg` but is not connected to the
database. TempMsgs are used by Evennia for channel messages by default. They can be used for any
system expecting a `Msg` but when you don't actually want to save anything.
## Channels
Channels are [Typeclassed](./Typeclasses) entities, which mean they can be easily extended and their
functionality modified. To change which channel typeclass Evennia uses, change
settings.BASE_CHANNEL_TYPECLASS.
Channels act as generic distributors of messages. Think of them as "switch boards" redistributing
`Msg` or `TempMsg` objects. Internally they hold a list of "listening" objects and any `Msg` (or
`TempMsg`) sent to the channel will be distributed out to all channel listeners. Channels have
[Locks](./Locks) to limit who may listen and/or send messages through them.
The *sending* of text to a channel is handled by a dynamically created [Command](./Commands) that
always have the same name as the channel. This is created for each channel by the global
`ChannelHandler`. The Channel command is added to the Account's cmdset and normal command locks are
used to determine which channels are possible to write to. When subscribing to a channel, you can
then just write the channel name and the text to send.
The default ChannelCommand (which can be customized by pointing `settings.CHANNEL_COMMAND_CLASS` to
your own command), implements a few convenient features:
- It only sends `TempMsg` objects. Instead of storing individual entries in the database it instead
dumps channel output a file log in `server/logs/channel_<channelname>.log`. This is mainly for
practical reasons - we find one rarely need to query individual Msg objects at a later date. Just
stupidly dumping the log to a file also means a lot less database overhead.
- It adds a `/history` switch to view the 20 last messages in the channel. These are read from the
end of the log file. One can also supply a line number to start further back in the file (but always
20 entries at a time). It's used like this:
> public/history
> public/history 35
There are two default channels created in stock Evennia - `MudInfo` and `Public`. `MudInfo`
receives server-related messages meant for Admins whereas `Public` is open to everyone to chat on
(all new accounts are automatically joined to it when logging in, it is useful for asking
questions). The default channels are defined by the `DEFAULT_CHANNELS` list (see
`evennia/settings_default.py` for more details).
You create new channels with `evennia.create_channel` (or `evennia.utils.create.create_channel`).
In code, messages are sent to a channel using the `msg` or `tempmsg` methods of channels:
channel.msg(msgobj, header=None, senders=None, persistent=True)
The argument `msgobj` can be either a string, a previously constructed `Msg` or a `TempMsg` - in the
latter cases all the following keywords are ignored since the message objects already contains all
this information. If `msgobj` is a string, the other keywords are used for creating a new `Msg` or
`TempMsg` on the fly, depending on if `persistent` is set or not. By default, a `TempMsg` is emitted
for channel communication (since the default ChannelCommand instead logs to a file).
```python
# assume we have a 'sender' object and a channel named 'mychan'
# manually sending a message to a channel
mychan.msg("Hello!", senders=[sender])
```
### Properties defined on `Channel`
- `key` - main name for channel
- `aliases` - alternative native names for channels
- `desc` - optional description of channel (seen in listings)
- `keep_log` (bool) - if the channel should store messages (default)
- `locks` - A [lock definition](./Locks). Channels normally use the access_types `send, control` and
`listen`.

View file

@ -0,0 +1,36 @@
# Connection Screen
When you first connect to your game you are greeted by Evennia's default connection screen.
==============================================================
Welcome to Evennia, version Beta-ra4d24e8a3cab+!
If you have an existing account, connect to it by typing:
connect <username> <password>
If you need to create an account, type (without the <>'s):
create <username> <password>
If you have spaces in your username, enclose it in quotes.
Enter help for more info. look will re-show this screen.
==============================================================
Effective, but not very exciting. You will most likely want to change this to be more unique for
your game. This is simple:
1. Edit `mygame/server/conf/connection_screens.py`.
1. [Reload](./Start-Stop-Reload) Evennia.
Evennia will look into this module and locate all *globally defined strings* in it. These strings
are used as the text in your connection screen and are shown to the user at startup. If more than
one such string/screen is defined in the module, a *random* screen will be picked from among those
available.
### Commands available at the Connection Screen
You can also customize the [Commands](./Commands) available to use while the connection screen is
shown (`connect`, `create` etc). These commands are a bit special since when the screen is running
the account is not yet logged in. A command is made available at the login screen by adding them to
`UnloggedinCmdSet` in `mygame/commands/default_cmdset.py`. See [Commands](./Commands) and the
tutorial section on how to add new commands to a default command set.

View file

@ -0,0 +1,222 @@
# Continuous Integration
One of the advantages of Evennia over traditional MUSH development systems is that Evennia is
capable of integrating into enterprise level integration environments and source control. Because of
this, it can also be the subject of automation for additional convenience, allowing a more
streamlined development environment.
## What is Continuous Integration?
[Continuous Integration (CI)](https://www.thoughtworks.com/continuous-integration) is a development
practice that requires developers to integrate code into a shared repository several times a day.
Each check-in is then verified by an automated build, allowing teams to detect problems early.
For Evennia, continuous integration allows an automated build process to:
* Pull down a latest build from Source Control.
* Run migrations on the backing SQL database.
* Automate additional unique tasks for that project.
* Run unit tests.
* Publish those files to the server directory
* Reload the game.
## Preparation
To prepare a CI environment for your `MU*`, it will be necessary to set up some prerequisite
software for your server.
Among those you will need:
* A Continuous Integration Environment.
* I recommend [TeamCity](https://www.jetbrains.com/teamcity/) which has an in-depth [Setup
Guide](https://confluence.jetbrains.com/display/TCD8/Installing+and+Configuring+the+TeamCity+Server)
* [Source Control](./Version-Control)
* This could be Git or SVN or any other available SC.
## Linux TeamCity Setup
For this part of the guide, an example setup will be provided for administrators running a TeamCity
build integration environment on Linux.
After meeting the preparation steps for your specific environment, log on to your teamcity interface
at `http://<your server>:8111/`.
Create a new project named "Evennia" and in it construct a new template called continuous-
integration.
### A Quick Overview
Templates are fancy objects in TeamCity that allow an administrator to define build steps that are
shared between one or more build projects. Assigning a VCS Root (Source Control) is unnecessary at
this stage, primarily you'll be worrying about the build steps and your default parameters (both
visible on the tabs to the left.)
### Template Setup
In this template, you'll be outlining the steps necessary to build your specific game. (A number of
sample scripts are provided under this section below!) Click Build Steps and prepare your general
flow. For this example, we will be doing a few basic example steps:
* Transforming the Settings.py file
* We do this to update ports or other information that make your production environment unique
from your development environment.
* Making migrations and migrating the game database.
* Publishing the game files.
* Reloading the server.
For each step we'll being use the "Command Line Runner" (a fancy name for a shell script executor).
* Create a build step with the name: Transform Configuration
* For the script add:
```bash
#!/bin/bash
# Replaces the game configuration with one
# appropriate for this deployment.
CONFIG="%system.teamcity.build.checkoutDir%/server/conf/settings.py"
MYCONF="%system.teamcity.build.checkoutDir%/server/conf/my.cnf"
sed -e 's/TELNET_PORTS = [4000]/TELNET_PORTS = [%game.ports%]/g' "$CONFIG" > "$CONFIG".tmp && mv
"$CONFIG".tmp "$CONFIG"
sed -e 's/WEBSERVER_PORTS = [(4001, 4002)]/WEBSERVER_PORTS = [%game.webports%]/g' "$CONFIG" >
"$CONFIG".tmp && mv "$CONFIG".tmp "$CONFIG"
# settings.py MySQL DB configuration
echo Configuring Game Database...
echo "" >> "$CONFIG"
echo "######################################################################" >> "$CONFIG"
echo "# MySQL Database Configuration" >> "$CONFIG"
echo "######################################################################" >> "$CONFIG"
echo "DATABASES = {" >> "$CONFIG"
echo " 'default': {" >> "$CONFIG"
echo " 'ENGINE': 'django.db.backends.mysql'," >> "$CONFIG"
echo " 'OPTIONS': {" >> "$CONFIG"
echo " 'read_default_file': 'server/conf/my.cnf'," >> "$CONFIG"
echo " }," >> "$CONFIG"
echo " }" >> "$CONFIG"
echo "}" >> "$CONFIG"
# Create the My.CNF file.
echo "[client]" >> "$MYCONF"
echo "database = %mysql.db%" >> "$MYCONF"
echo "user = %mysql.user%" >> "$MYCONF"
echo "password = %mysql.pass%" >> "$MYCONF"
echo "default-character-set = utf8" >> "$MYCONF"
```
If you look at the parameters side of the page after saving this script, you'll notice that some new
parameters have been populated for you. This is because we've included new teamcity configuration
parameters that are populated when the build itself is ran. When creating projects that inherit this
template, we'll be able to fill in or override those parameters for project-specific configuration.
* Go ahead and create another build step called "Make Database Migration"
* If you're using SQLLite on your game, it will be prudent to change working directory on this
step to: %game.dir%
* In this script include:
```bash
#!/bin/bash
# Update the DB migration
LOGDIR="server/logs"
. %evenv.dir%/bin/activate
# Check that the logs directory exists.
if [ ! -d "$LOGDIR" ]; then
# Control will enter here if $LOGDIR doesn't exist.
mkdir "$LOGDIR"
fi
evennia makemigrations
```
* Create yet another build step, this time named: "Execute Database Migration":
* If you're using SQLLite on your game, it will be prudent to change working directory on this
step to: %game.dir%
```bash
#!/bin/bash
# Apply the database migration.
LOGDIR="server/logs"
. %evenv.dir%/bin/activate
# Check that the logs directory exists.
if [ ! -d "$LOGDIR" ]; then
# Control will enter here if $LOGDIR doesn't exist.
mkdir "$LOGDIR"
fi
evennia migrate
```
Our next build step is where we actually publish our build. Up until now, all work on game has been
done in a 'work' directory on TeamCity's build agent. From that directory we will now copy our files
to where our game actually exists on the local server.
* Create a new build step called "Publish Build":
* If you're using SQLLite on your game, be sure to order this step ABOVE the Database Migration
steps. The build order will matter!
```bash
#!/bin/bash
# Publishes the build to the proper build directory.
DIRECTORY="%game.dir%"
if [ ! -d "$DIRECTORY" ]; then
# Control will enter here if $DIRECTORY doesn't exist.
mkdir "$DIRECTORY"
fi
# Copy all the files.
cp -ruv %teamcity.build.checkoutDir%/* "$DIRECTORY"
chmod -R 775 "$DIRECTORY"
```
Finally the last script will reload our game for us.
* Create a new script called "Reload Game":
* The working directory on this build step will be: %game.dir%
```bash
#!/bin/bash
# Apply the database migration.
LOGDIR="server/logs"
PIDDIR="server/server.pid"
. %evenv.dir%/bin/activate
# Check that the logs directory exists.
if [ ! -d "$LOGDIR" ]; then
# Control will enter here if $LOGDIR doesn't exist.
mkdir "$LOGDIR"
fi
# Check that the server is running.
if [ -d "$PIDDIR" ]; then
# Control will enter here if the game is running.
evennia reload
fi
```
Now the template is ready for use! It would be useful this time to revisit the parameters page and
set the evenv parameter to the directory where your virtualenv exists: IE "/srv/mush/evenv".
### Creating the Project
Now it's time for the last few steps to set up a CI environment.
* Return to the Evennia Project overview/administration page.
* Create a new Sub-Project called "Production"
* This will be the category that holds our actual game.
* Create a new Build Configuration in Production with the name of your MUSH.
* Base this configuration off of the continuous-integration template we made earlier.
* In the build configuration, enter VCS roots and create a new VCS root that points to the
branch/version control that you are using.
* Go to the parameters page and fill in the undefined parameters for your specific configuration.
* If you wish for the CI to run every time a commit is made, go to the VCS triggers and add one for
"On Every Commit".
And you're done! At this point, you can return to the project overview page and queue a new build
for your game. If everything was set up correctly, the build will complete successfully. Additional
build steps could be added or removed at this point, adding some features like Unit Testing or more!

View file

@ -0,0 +1,679 @@
# Contributing to Evennia Docs
```warning::
The creation of docs pages is still WIP and we are still figuring things out here and there.
```
Contributing to the docs is is like [contributing to the rest of Evennia][contributing]: Check out the branch of Evennia you want to edit the documentation for. Create your
own work-branch, make your changes to files in `evennia/docs/source/` and make a PR for it!
The documentation source files are `*.md` (Markdown) files found in `evennia/docs/source/`.
Markdown files are simple text files that can be edited with a normal text editor. They can also
contain raw HTML directives (but that is very rarely needed). They primarly use
the [Markdown][commonmark] syntax. See [the syntax section below](#Editing-syntax) for more help.
## Source file structure
For v 0.9.5, the sources are all together under `evennia/docs/source/`. The main files are all
Markdown (`.md`) files.
Other files and folders:
- `source/api/` contains the auto-generated API documentation as `.rst` files. Don't edit these
files manually, your changes will be lost. To refer to these files, use `api:` followed by
the Python path, for example `[rpsystem contrib](api:evennia.contrib.rpsystem)`.
- `source/_templates` and `source/_static` should not be modified unless adding a new doc-page
feature or changing the look of the HTML documentation.
- `conf.py` holds the Sphinx configuration. It should usually not be modified except to update
the Evennia version on a new branch.
## Building the docs locally
The sources in `evennia/docs/source/` are built into a documentation using the
[Sphinx][sphinx] static generator system. To do this locally you need to use a
system with `make` (Linux/Unix/Mac or [Windows-WSL][Windows-WSL]). Lacking
that, you could in principle also run the sphinx build-commands manually - read
the `evennia/docs/Makefile` to see which commands are run by the `make`-commands
referred to in this document.
You don't necessarily _have_ to build the docs locally to contribute. Markdown is
not hard and is very readable on its raw text-form.
You can furthermore get a good feel for how things will look using a
Markdown-viewer like [Grip][grip]. Editors like [ReText][retext] or IDE's like
[PyCharm][pycharm] also have native Markdown previews. Building the docs locally is
however the only way to make sure the outcome is exactly as you expect. The process
will also find any mistakes you made, like making a typo in a link.
### Building only the main documentation
This is the fastest way to compile and view your changes. It will only build
the main documentation pages and not the API auto-docs or versions. All is
done in your terminal/console.
- (Optional, but recommended): Activate a virtualenv with Python 3.7.
- `cd` to into the `evennia/docs` folder.
- Install the documentation-build requirements:
```
make install
or
pip install -r requirements.txt
```
- Next, build the html-based documentation (re-run this in the future to build your changes):
```
make quick
```
- Note any errors from files you have edited.
- The html-based documentation will appear in the new
folder `evennia/docs/build/html/`.
- Use a web browser to open `file://<path-to-folder>/evennia/docs/build/html/index.html` and view
the docs. Note that you will get errors if clicking a link to the auto-docs, because you didn't
build them!
### Building the main documentation and API docs
The full documentation includes both the doc pages and the API documentation
generated from the Evennia source. For this you must install Evennia and
initialize a new game with a default database (you don't need to have any server
running)
- It's recommended that you use a virtualenv. Install your cloned version of Evennia into
by pointing to the repo folder (the one containing `/docs`):
```
pip install -e evennia
```
- Make sure you are in the parent folder _containing_ your `evennia/` repo (so _two_ levels
up from `evennia/docs/`).
- Create a new game folder called exactly `gamedir` at the same level as your `evennia`
repo with
```
evennia --init gamedir
```
- Then `cd` into it and create a new, empty database. You don't need to start the
game or do any further changes after this.
```
evennia migrate
```
- This is how the structure should look at this point:
```
(top)
|
----- evennia/ (the top-level folder, containing docs/)
|
----- gamedir/
```
(If you are already working on a game, you may of course have your 'real' game folder there as
well. We won't touch that.)
- Go to `evennia/docs/` and install the doc-building requirements (you only need to do this once):
```
make install
or
pip install -r requirements.txt
```
- Finally, build the full documentation, including the auto-docs:
```
make local
```
- The rendered files will appear in a new folder `evennia/docs/build/html/`.
Note any errors from files you have edited.
- Point your web browser to `file://<path-to-folder>/evennia/docs/build/html/index.html` to
view the full docs.
#### Building with another gamedir
If you for some reason want to use another location of your `gamedir/`, or want it
named something else (maybe you already use the name 'gamedir' for your development ...),
you can do so by setting the `EVGAMEDIR` environment variable to the absolute path
of your alternative game dir. For example (bash):
```
EVGAMEDIR=/my/path/to/mygamedir make local
```
### Building for release
The full Evennia documentation contains docs from many Evennia
versions, old and new. This is done by pulling documentation from Evennia's old release
branches and building them all so readers can choose which one to view. Only
specific official Evennia branches will be built, so you can't use this to
build your own testing branch.
- All local changes must have been committed to git first, since the versioned
docs are built by looking at the git tree.
- To build for local checking, run (`mv` stands for "multi-version"):
```
make mv-local
```
This is as close to the 'real' version of the docs as you can get locally. The different versions
will be found under `evennia/docs/build/versions/`. During deploy a symlink `latest` will point
to the latest version of the docs.
#### Release
Releasing the official docs requires git-push access the the Evennia `gh-pages` branch
on `github`. So there is no risk of you releasing your local changes accidentally.
- To deploy docs in two steps
```
make mv-local
make deploy
```
- If you know what you are doing you can also do build + deploy in one step:
```
make release
```
After deployment finishes, the updated live documentation will be
available at https://evennia.github.io/evennia/latest/.
# Editing syntax
The format used for Evennia's docs is [Markdown][commonmark-help] (Commonmark). While markdown
supports a
few alternative forms for some of these, we try to stick to the below forms for consistency.
### Italic/Bold
We generally use underscores for italics and double-asterisks for bold:
- `_Italic text_` - _Italic text_
- `**Bold Text**` - **Bold text**
### Headings
We use `#` to indicate sections/headings. The more `#` the more of a sub-heading it is (will get
smaller and smaller font).
- `# Heading`
- `## SubHeading`
- `### SubSubHeading`
- `#### SubSubSubHeading`
> Don't use the same heading/subheading name more than once in one page. While Markdown
does not prevent it, it will make it impossible to refer to that heading uniquely.
The Evennia documentation preparser will detect this and give you an error.
### Lists
One can create both bullet-point lists and numbered lists:
```
- first bulletpoint
- second bulletpoint
- third bulletpoint
```
- first bulletpoint
- second bulletpoint
- third bulletpoint
```
1. Numbered point one
2. Numbered point two
3. Numbered point three
```
1. Numbered point one
2. Numbered point two
3. Numbered point three
### Blockquotes
A blockquote will create an indented block. It's useful for emphasis and is
added by starting one or more lines with `>`. For 'notes' you can also use
an explicit [Note](#Note).
```
> This is an important
> thing to remember.
```
> Note: This is an important
> thing to remember.
### Links
- `[linktext](url_or_ref)` - gives a clickable link [linktext][linkdemo].
The `url_or_ref` can either be a full `http://...` url or an internal _reference_. For example, use
`[my document](My-Document)` to link to the document `evennia/docs/source/My-Document.md`. Avoid
using
full `http://` linking unless really referring to an external resource.
- `[linktext](ref#heading-name)`
You can point to sub-sections (headings) in a document by using a single `#` and the name of the
heading, replacing spaces with dashes. So to refer to a heading `## Cool Stuff` inside `My-Document`
would be a link `[cool stuff](My-Document#Cool-Stuff)`. This is why headings must be uniquely named
within on document.
- `[linktext][linkref]` - refer to a reference defined later in the document.
Urls can get long and if you are using the same url in many places it can get a little cluttered. So
you can also put the url as a 'footnote' at the end of your document
and refer to it by putting your reference within square brackets `[ ]`. Here's an example:
```
This is a [clickable link][mylink]. This is [another link][1].
...
[mylink]: http://...
[1]: My-Document
```
#### Special references
The Evennia documentation supports some special reference shortcuts in links:
##### Github online repository
- `github:` - a shortcut for the full path to the Evennia repository on github. This must be given
with forward-slashes and include the `.py` file ending. It will refer to the `master` branch by default:
[link to objects.py](github:evennia/objects/objects.py)
This will remap to https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py.
- To refer to the `develop` branch, start the url with `develop/`:
[link to objects.py](github:develop/evennia/objects/objects.py)
##### API
- `api:` - references a path in the api documentation. This is specified as a Python-path:
[link to api for objects.py](api:evennia.objects)
This will create a link to the auto-generated `evennia/source/api/evennia.objects.rst` document.
Since api-docs are generated alongside the documentation, this will always be the api docs for
the current version/branch of the docs.
##### Bug reports/feature request
- `github:issue` - links to the github issue selection page, where the user can choose which type of
issue to create.
If you find a problem, make a [bug report](github:issue)!
This will generate a link to https://github.com/evennia/evennia/issues/new/choose.
### Verbatim text
It's common to want to mark something to be displayed verbatim - just as written - without any
Markdown parsing. In running text, this is done using backticks (\`), like \`verbatim text\` becomes
`verbatim text`.
If you want to put the verbatim text on its own line, you can do so easily by simply indenting
it 4 spaces (add empty lines on each side for readability too):
```
This is normal text
This is verbatim text
This is normal text
```
Another way is to use triple-backticks:
````
```
Everything within these backticks will be verbatim.
```
````
### Code blocks
A special case is code examples - we want them to get code-highlighting for readability. This is
done by using
the triple-backticks and specify which language we use:
````
```python
def a_python_func(x):
return x * x
```
````
```python
def a_python_func(x):
return x * x
```
### ReST blocks
Markdown is easy to read and use. But while it does most of what we need, there are some things it's
not quite as expressive as it needs to be. For this we need to fall back to the [ReST][ReST] markup
language which the documentation system uses under the hood. This is done by specifying `eval_rst`
as the name of the `language` of a literal block:
````
```eval_rst
This will be evaluated as ReST.
All content must be indented.
```
````
There is also a short-hand form for starting a [ReST directive][ReST-directives] without need for
`eval_rst`:
````
```directive:: possible-option
Content *must* be indented for it to be included in the directive.
New lines are ignored, empty lines starts a new paragraph.
```
````
Within a ReST block, one must use Restructured Text syntax, which is not the same as Markdown.
- Single backticks around text makes it _italic_.
- Double backticks around text makes it `verbatim`.
- A link is written within back-ticks, with an underscore at the end:
`python <www.python.org>`_
[Here is a ReST formatting cheat sheet](https://thomas-cokelaer.info/tutorials/sphinx/rest_syntax.html).
Below are examples of ReST-block structures.
#### Note
This kind of note may pop more than doing a `> Note: ...`. Contrary to a
[blockquote](#Blockquotes), the end result will not be indented.
````
```note::
Remember that you have to indent this content for it to be part of the note.
```
````
```note::
Remember that you have to indent this content for it to be part of the note.
```
#### Important
This is for particularly important and visible notes.
````
```important::
This is important because it is!
```
````
```important::
This is important because it is!
```
#### Warning
A warning block is used to draw attention to particularly dangerous things, or features easy to
mess up.
````
```warning::
Be careful about this ...
```
````
```warning::
Be careful about this ...
```
#### Version changes and deprecations
These will show up as one-line warnings that suggest an added, changed or deprecated
feature beginning with particular version.
````
```versionadded:: 1.0
```
````
```versionadded:: 1.0
```
````
```versionchanged:: 1.0
How the feature changed with this version.
```
````
```versionchanged:: 1.0
How the feature changed with this version.
```
````
```deprecated:: 1.0
```
````
```deprecated:: 1.0
```
#### Sidebar
This will display an informative sidebar that floats to the side of regular content. This is useful
for example to remind the reader of some concept relevant to the text.
````
```sidebar:: Things to remember
- There can be bullet lists
- in here.
Headers:
with indented blocks like this
Will end up:
as full sub-headings in the sidebar.
```
````
```sidebar:: Things to remember
- There can be bullet lists
- in here.
Headers:
with indented blocks like this
Will end up:
as full sub-headings in the sidebar.
```
Remember that for ReST-directives, the content within the triple-backticks _must_ be indented to
some degree or the content will just appear outside of the directive as regular text.
If wanting to make sure to have the next header appear on a row of its own, one can embed
a plain HTML string in the markdown like so:
```html
<div style="clear: right;"></div>
```
<div style="clear: right;"></div>
#### Tables
A table is specified using [ReST table syntax][ReST-tables] (they don't need to be indented):
````
```eval_rst
===== ===== =======
A B A and B
===== ===== =======
False False False
True False False
False True False
True True True
===== ===== =======
```
````
```eval_rst
===== ===== =======
A B A and B
===== ===== =======
False False False
True False False
False True False
True True True
===== ===== =======
```
or the more flexible but verbose
````
```eval_rst
+------------------------+------------+----------+----------+
| Header row, column 3 | Header 2 | Header 3 | Header 4 |
| (header rows optional) | | | |
+========================+============+==========+==========+
| body row 1, column 1 | column 2 | column 3 | column 4 |
+------------------------+------------+----------+----------+
| body row 2 | ... | ... | |
+------------------------+------------+----------+----------+
```
````
```eval_rst
+------------------------+------------+----------+----------+
| Header row, column 3 | Header 2 | Header 3 | Header 4 |
| (header rows optional) | | | |
+========================+============+==========+==========+
| body row 1, column 1 | column 2 | column 3 | column 4 |
+------------------------+------------+----------+----------+
| body row 2 | ... | ... | |
+------------------------+------------+----------+----------+
```
#### A more flexible code block
The regular Markdown Python codeblock is usually enough but for more direct control over the style, one
can also specify the code block explicitly in `ReST` for more flexibility.
It also provides a link to the code block, identified by its name.
````
```code-block:: python
:linenos:
:emphasize-lines: 1-2,8
:caption: An example code block
:name: A full code block example
from evennia import Command
class CmdEcho(Command):
"""
Usage: echo <arg>
"""
key = "echo"
def func(self):
self.caller.msg(self.args.strip())
```
````
```code-block:: python
:linenos:
:emphasize-lines: 1-2,8
:caption: An example code block
:name: A full code block example
from evennia import Command
class CmdEcho(Command):
"""
Usage: echo <arg>
"""
key = "echo"
def func(self):
self.caller.msg(self.args.strip())
```
Here, `:linenos:` turns on line-numbers and `:emphasize-lines:` allows for emphasizing certain lines
in a different color. The `:caption:` shows an instructive text and `:name:` is used to reference
this
block through the link that will appear (so it should be unique for a give document).
> The default markdown syntax will actually generate a code-block ReST instruction like this
> automatically for us behind the scenes. But the automatic generation can't know things like emphasize-
lines or captions since that's not a part of the Markdown specification.
## Code documentation
The source code docstrings will be parsed as Markdown. When writing a module docstring, you can use Markdown formatting,
including header levels down to 4th level (`#### SubSubSubHeader`). After the module documentation it's
a good idea to end with four dashes `----`. This will create a visible line between the documentation and the
class/function docs to follow. See for example [the Traits docs](api:evennia.contrib.traits).
All non-private classes, methods and functions must have a Google-style docstring, as per the
[Evennia coding style guidelines](github:evennia/CODING_STYLE.md). This will then be correctly formatted
into pretty api docs.
## Technical
Evennia leverages [Sphinx][sphinx] with the [recommonmark][recommonmark] extension, which allows us
to write our
docs in light-weight Markdown (more specifically [CommonMark][commonmark], like on github) rather
than ReST.
The recommonmark extension however also allows us to use ReST selectively in the places were it is
more
expressive than the simpler (but much easier) Markdown.
For [autodoc-generation][sphinx-autodoc] generation, we use the sphinx-[napoleon][sphinx-napoleon]
extension
to understand our friendly Google-style docstrings used in classes and functions etc.
[sphinx]: https://www.sphinx-doc.org/en/master/
[recommonmark]: https://recommonmark.readthedocs.io/en/latest/index.html
[commonmark]: https://spec.commonmark.org/current/
[commonmark-help]: https://commonmark.org/help/
[sphinx-autodoc]: http://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#module-sphinx.ext.autodoc
[sphinx-napoleon]: http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
[getting-started]: Setup/Setup-Quickstart
[contributing]: ./Contributing
[ReST]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
[ReST-tables]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#tables
[ReST-directives]: https://www.sphinx-doc.org/en/master/usage/restruturedtext/directives.html
[Windows-WSL]: https://docs.microsoft.com/en-us/windows/wsl/install-win10
[linkdemo]: #Links
[retext]: https://github.com/retext-project/retext
[grip]: https://github.com/joeyespo/grip
[pycharm]: https://www.jetbrains.com/pycharm/

118
docs/source/Contributing.md Normal file
View file

@ -0,0 +1,118 @@
# Contributing
Wanna help out? Great! Here's how.
## Spreading the word
Even if you are not keen on working on the server code yourself, just spreading the word is a big
help - it will help attract more people which leads to more feedback, motivation and interest.
Consider writing about Evennia on your blog or in your favorite (relevant) forum. Write a review
somewhere (good or bad, we like feedback either way). Rate it on places like [ohloh][ohloh]. Talk
about it to your friends ... that kind of thing.
## Donations
The best way to support Evennia is to become an [Evennia patron][patron]. Evennia is a free,
open-source project and any monetary donations you want to offer are completely voluntary. See it as
a way of announcing that you appreciate the work done - a tip of the hat! A patron donates a
(usually small) sum every month to show continued support. If this is not your thing you can also
show your appreciation via a [one-time donation][donate] (this is a PayPal link but you don't need
PayPal yourself).
## Help with Documentation
Evennia depends heavily on good documentation and we are always looking for extra eyes and hands to
improve it. Even small things such as fixing typos are a great help!
The documentation is a wiki and as long as you have a GitHub account you can edit it. It can be a
good idea to discuss in the chat or forums if you want to add new pages/tutorials. Otherwise, it
goes a long way just pointing out wiki errors so we can fix them (in an Issue or just over
chat/forum).
## Contributing through a forked repository
We always need more eyes and hands on the code. Even if you don't feel confident with tackling a
[bug or feature][issues], just correcting typos, adjusting formatting or simply *using* the thing
and reporting when stuff doesn't make sense helps us a lot.
The most elegant way to contribute code to Evennia is to use GitHub to create a *fork* of the
Evennia repository and make your changes to that. Refer to the [Forking Evennia](Version-
Control#forking-evennia) version
control instructions for detailed instructions.
Once you have a fork set up, you can not only work on your own game in a separate branch, you can
also commit your fixes to Evennia itself. Make separate branches for all Evennia additions you do -
don't edit your local `master` or `develop` branches directly. It will make your life a lot easier.
If you have a change that you think is suitable for the main Evennia repository, you issue a [Pull
Request][pullrequest]. This will let Evennia devs know you have stuff to share. Bug fixes should
generally be done against the `master` branch of Evennia, while new features/contribs should go into
the `develop` branch. If you are unsure, just pick one and we'll figure it out.
## Contributing with Patches
To help with Evennia development it's recommended to do so using a fork repository as described
above. But for small, well isolated fixes you are also welcome to submit your suggested Evennia
fixes/addendums as a [patch][patch].
You can include your patch in an Issue or a Mailing list post. Please avoid pasting the full patch
text directly in your post though, best is to use a site like [Pastebin](http://pastebin.com/) and
just supply the link.
## Contributing with Contribs
While Evennia's core is pretty much game-agnostic, it also has a `contrib/` directory. The `contrib`
directory contains game systems that are specialized or useful only to certain types of games. Users
are welcome to contribute to the `contrib/` directory. Such contributions should always happen via a
Forked repository as described above.
* If you are unsure if your idea/code is suitable as a contrib, *ask the devs before putting any
work into it*. This can also be a good idea in order to not duplicate efforts. This can also act as
a check that your implementation idea is sound. We are, for example, unlikely to accept contribs
that require large modifications of the game directory structure.
* If your code is intended *primarily* as an example or shows a concept/principle rather than a
working system, it is probably not suitable for `contrib/`. You are instead welcome to use it as
part of a [new tutorial][tutorials]!
* The code should ideally be contained within a single Python module. But if the contribution is
large this may not be practical and it should instead be grouped in its own subdirectory (not as
loose modules).
* The contribution should preferably be isolated (only make use of core Evennia) so it can easily be
dropped into use. If it does depend on other contribs or third-party modules, these must be clearly
documented and part of the installation instructions.
* The code itself should follow Evennia's [Code style guidelines][codestyle].
* The code must be well documented as described in our [documentation style
guide](https://github.com/evennia/evennia/blob/master/CODING_STYLE.md#doc-strings). Expect that your
code will be read and should be possible to understand by others. Include comments as well as a
header in all modules. If a single file, the header should include info about how to include the
contrib in a game (installation instructions). If stored in a subdirectory, this info should go into
a new `README.md` file within that directory.
* Within reason, your contribution should be designed as genre-agnostic as possible. Limit the
amount of game-style-specific code. Assume your code will be applied to a very different game than
you had in mind when creating it.
* To make the licensing situation clear we assume all contributions are released with the same
[license as Evennia](./Licensing). If this is not possible for some reason, talk to us and we'll
handle it on a case-by-case basis.
* Your contribution must be covered by [unit tests](./Unit-Testing). Having unit tests will both help
make your code more stable and make sure small changes does not break it without it being noticed,
it will also help us test its functionality and merge it quicker. If your contribution is a single
module, you can add your unit tests to `evennia/contribs/tests.py`. If your contribution is bigger
and in its own sub-directory you could just put the tests in your own `tests.py` file (Evennia will
find it automatically).
* Merging of your code into Evennia is not guaranteed. Be ready to receive feedback and to be asked
to make corrections or fix bugs. Furthermore, merging a contrib means the Evennia project takes on
the responsibility of maintaining and supporting it. For various reasons this may be deemed to be
beyond our manpower. However, if your code were to *not* be accepted for merger for some reason, we
will instead add a link to your online repository so people can still find and use your work if they
want.
[ohloh]: http://www.ohloh.net/p/evennia
[patron]: https://www.patreon.com/griatch
[donate]: https://www.paypal.com/en/cgi-bin/webscr?cmd=_flow&SESSION=TWy_epDPSWqNr4UJCOtVWxl-
pO1X1jbKiv_-
UBBFWIuVDEZxC0M_2pM6ywO&dispatch=5885d80a13c0db1f8e263663d3faee8d66f31424b43e9a70645c907a6cbd8fb4
[forking]: https://github.com/evennia/evennia/wiki/Version-Control#wiki-forking-from-evennia
[pullrequest]: https://github.com/evennia/evennia/pulls
[issues]: https://github.com/evennia/evennia/issues
[patch]: https://secure.wikimedia.org/wikipedia/en/wiki/Patch_%28computing%29
[codestyle]: https://github.com/evennia/evennia/blob/master/CODING_STYLE.md
[tutorials]: https://github.com/evennia/evennia/wiki/Tutorials

349
docs/source/Coordinates.md Normal file
View file

@ -0,0 +1,349 @@
# Coordinates
# Adding room coordinates in your game
This tutorial is moderately difficult in content. You might want to be familiar and at ease with
some Python concepts (like properties) and possibly Django concepts (like queries), although this
tutorial will try to walk you through the process and give enough explanations each time. If you
don't feel very confident with math, don't hesitate to pause, go to the example section, which shows
a tiny map, and try to walk around the code or read the explanation.
Evennia doesn't have a coordinate system by default. Rooms and other objects are linked by location
and content:
- An object can be in a location, that is, another object. Like an exit in a room.
- An object can access its content. A room can see what objects uses it as location (that would
include exits, rooms, characters and so on).
This system allows for a lot of flexibility and, fortunately, can be extended by other systems.
Here, I offer you a way to add coordinates to every room in a way most compliant with Evennia
design. This will also show you how to use coordinates, find rooms around a given point for
instance.
## Coordinates as tags
The first concept might be the most surprising at first glance: we will create coordinates as
[tags](./Tags).
> Why not attributes, wouldn't that be easier?
It would. We could just do something like `room.db.x = 3`. The advantage of using tags is that it
will be easy and effective to search. Although this might not seem like a huge advantage right now,
with a database of thousands of rooms, it might make a difference, particularly if you have a lot of
things based on coordinates.
Rather than giving you a step-by-step process, I'll show you the code. Notice that we use
properties to easily access and update coordinates. This is a Pythonic approach. Here's our first
`Room` class, that you can modify in `typeclasses/rooms.py`:
```python
# in typeclasses/rooms.py
from evennia import DefaultRoom
class Room(DefaultRoom):
"""
Rooms are like any Object, except their location is None
(which is default). They also use basetype_setup() to
add locks so they cannot be puppeted or picked up.
(to change that, use at_object_creation instead)
See examples/object.py for a list of
properties and methods available on all Objects.
"""
@property
def x(self):
"""Return the X coordinate or None."""
x = self.tags.get(category="coordx")
return int(x) if isinstance(x, str) else None
@x.setter
def x(self, x):
"""Change the X coordinate."""
old = self.tags.get(category="coordx")
if old is not None:
self.tags.remove(old, category="coordx")
if x is not None:
self.tags.add(str(x), category="coordx")
@property
def y(self):
"""Return the Y coordinate or None."""
y = self.tags.get(category="coordy")
return int(y) if isinstance(y, str) else None
@y.setter
def y(self, y):
"""Change the Y coordinate."""
old = self.tags.get(category="coordy")
if old is not None:
self.tags.remove(old, category="coordy")
if y is not None:
self.tags.add(str(y), category="coordy")
@property
def z(self):
"""Return the Z coordinate or None."""
z = self.tags.get(category="coordz")
return int(z) if isinstance(z, str) else None
@z.setter
def z(self, z):
"""Change the Z coordinate."""
old = self.tags.get(category="coordz")
if old is not None:
self.tags.remove(old, category="coordz")
if z is not None:
self.tags.add(str(z), category="coordz")
```
If you aren't familiar with the concept of properties in Python, I encourage you to read a good
tutorial on the subject. [This article on Python properties](https://www.programiz.com/python-
programming/property)
is well-explained and should help you understand the idea.
Let's look at our properties for `x`. First of all is the read property.
```python
@property
def x(self):
"""Return the X coordinate or None."""
x = self.tags.get(category="coordx")
return int(x) if isinstance(x, str) else None
```
What it does is pretty simple:
1. It gets the tag of category `"coordx"`. It's the tag category where we store our X coordinate.
The `tags.get` method will return `None` if the tag can't be found.
2. We convert the value to an integer, if it's a `str`. Remember that tags can only contain `str`,
so we'll need to convert it.
> I thought tags couldn't contain values?
Well, technically, they can't: they're either here or not. But using tag categories, as we have
done, we get a tag, knowing only its category. That's the basic approach to coordinates in this
tutorial.
Now, let's look at the method that will be called when we wish to set `x` in our room:
```python
@x.setter
def x(self, x):
"""Change the X coordinate."""
old = self.tags.get(category="coordx")
if old is not None:
self.tags.remove(old, category="coordx")
if x is not None:
self.tags.add(str(x), category="coordx")
```
1. First, we remove the old X coordinate, if it exists. Otherwise, we'd end up with two tags in our
room with "coordx" as their category, which wouldn't do at all.
2. Then we add the new tag, giving it the proper category.
> Now what?
If you add this code and reload your game, once you're logged in with a character in a room as its
location, you can play around:
```
@py here.x
@py here.x = 0
@py here.y = 3
@py here.z = -2
@py here.z = None
```
The code might not be that easy to read, but you have to admit it's fairly easy to use.
## Some additional searches
Having coordinates is useful for several reasons:
1. It can help in shaping a truly logical world, in its geography, at least.
2. It can allow to look for specific rooms at given coordinates.
3. It can be good in order to quickly find the rooms around a location.
4. It can even be great in path-finding (finding the shortest path between two rooms).
So far, our coordinate system can help with 1., but not much else. Here are some methods that we
could add to the `Room` typeclass. These methods will just be search methods. Notice that they are
class methods, since we want to get rooms.
### Finding one room
First, a simple one: how to find a room at a given coordinate? Say, what is the room at X=0, Y=0,
Z=0?
```python
class Room(DefaultRoom):
# ...
@classmethod
def get_room_at(cls, x, y, z):
"""
Return the room at the given location or None if not found.
Args:
x (int): the X coord.
y (int): the Y coord.
z (int): the Z coord.
Return:
The room at this location (Room) or None if not found.
"""
rooms = cls.objects.filter(
db_tags__db_key=str(x), db_tags__db_category="coordx").filter(
db_tags__db_key=str(y), db_tags__db_category="coordy").filter(
db_tags__db_key=str(z), db_tags__db_category="coordz")
if rooms:
return rooms[0]
return None
```
This solution includes a bit of [Django
queries](https://docs.djangoproject.com/en/1.11/topics/db/queries/).
Basically, what we do is reach for the object manager and search for objects with the matching tags.
Again, don't spend too much time worrying about the mechanism, the method is quite easy to use:
```
Room.get_room_at(5, 2, -3)
```
Notice that this is a class method: you will call it from `Room` (the class), not an instance.
Though you still can:
@py here.get_room_at(3, 8, 0)
### Finding several rooms
Here's another useful method that allows us to look for rooms around a given coordinate. This is
more advanced search and doing some calculation, beware! Look at the following section if you're
lost.
```python
from math import sqrt
class Room(DefaultRoom):
# ...
@classmethod
def get_rooms_around(cls, x, y, z, distance):
"""
Return the list of rooms around the given coordinates.
This method returns a list of tuples (distance, room) that
can easily be browsed. This list is sorted by distance (the
closest room to the specified position is always at the top
of the list).
Args:
x (int): the X coord.
y (int): the Y coord.
z (int): the Z coord.
distance (int): the maximum distance to the specified position.
Returns:
A list of tuples containing the distance to the specified
position and the room at this distance. Several rooms
can be at equal distance from the position.
"""
# Performs a quick search to only get rooms in a square
x_r = list(reversed([str(x - i) for i in range(0, distance + 1)]))
x_r += [str(x + i) for i in range(1, distance + 1)]
y_r = list(reversed([str(y - i) for i in range(0, distance + 1)]))
y_r += [str(y + i) for i in range(1, distance + 1)]
z_r = list(reversed([str(z - i) for i in range(0, distance + 1)]))
z_r += [str(z + i) for i in range(1, distance + 1)]
wide = cls.objects.filter(
db_tags__db_key__in=x_r, db_tags__db_category="coordx").filter(
db_tags__db_key__in=y_r, db_tags__db_category="coordy").filter(
db_tags__db_key__in=z_r, db_tags__db_category="coordz")
# We now need to filter down this list to find out whether
# these rooms are really close enough, and at what distance
# In short: we change the square to a circle.
rooms = []
for room in wide:
x2 = int(room.tags.get(category="coordx"))
y2 = int(room.tags.get(category="coordy"))
z2 = int(room.tags.get(category="coordz"))
distance_to_room = sqrt(
(x2 - x) ** 2 + (y2 - y) ** 2 + (z2 - z) ** 2)
if distance_to_room <= distance:
rooms.append((distance_to_room, room))
# Finally sort the rooms by distance
rooms.sort(key=lambda tup: tup[0])
return rooms
```
This gets more serious.
1. We have specified coordinates as parameters. We determine a broad range using the distance.
That is, for each coordinate, we create a list of possible matches. See the example below.
2. We then search for the rooms within this broader range. It gives us a square
around our location. Some rooms are definitely outside the range. Again, see the example below
to follow the logic.
3. We filter down the list and sort it by distance from the specified coordinates.
Notice that we only search starting at step 2. Thus, the Django search doesn't look and cache all
objects, just a wider range than what would be really necessary. This method returns a circle of
coordinates around a specified point. Django looks for a square. What wouldn't fit in the circle
is removed at step 3, which is the only part that includes systematic calculation. This method is
optimized to be quick and efficient.
### An example
An example might help. Consider this very simple map (a textual description follows):
```
4 A B C D
3 E F G H
2 I J K L
1 M N O P
1 2 3 4
```
The X coordinates are given below. The Y coordinates are given on the left. This is a simple
square with 16 rooms: 4 on each line, 4 lines of them. All the rooms are identified by letters in
this example: the first line at the top has rooms A to D, the second E to H, the third I to L and
the fourth M to P. The bottom-left room, X=1 and Y=1, is M. The upper-right room X=4 and Y=4 is D.
So let's say we want to find all the neighbors, distance 1, from the room J. J is at X=2, Y=2.
So we use:
Room.get_rooms_around(x=2, y=2, z=0, distance=1)
# we'll assume a z coordinate of 0 for simplicity
1. First, this method gets all the rooms in a square around J. So it gets E F G, I J K, M N O. If
you want, draw the square around these coordinates to see what's happening.
2. Next, we browse over this list and check the real distance between J (X=2, Y=2) and the room.
The four corners of the square are not in this circle. For instance, the distance between J and M
is not 1. If you draw a circle of center J and radius 1, you'll notice that the four corners of our
square (E, G, M and O) are not in this circle. So we remove them.
3. We sort by distance from J.
So in the end we might obtain something like this:
```
[
(0, J), # yes, J is part of this circle after all, with a distance of 0
(1, F),
(1, I),
(1, K),
(1, N),
]
```
You can try with more examples if you want to see this in action.
### To conclude
You can definitely use this system to map other objects, not just rooms. You can easily remove the
`Z coordinate too, if you simply need X and Y.

View file

@ -0,0 +1,239 @@
# Custom Protocols
*Note: This is considered an advanced topic and is mostly of interest to users planning to implement
their own custom client protocol.*
A [PortalSession](./Sessions#Portal-and-Server-Sessions) is the basic data object representing an
external
connection to the Evennia [Portal](./Portal-And-Server) -- usually a human player running a mud client
of some kind. The way they connect (the language the player's client and Evennia use to talk to
each other) is called the connection *Protocol*. The most common such protocol for MUD:s is the
*Telnet* protocol. All Portal Sessions are stored and managed by the Portal's *sessionhandler*.
It's technically sometimes hard to separate the concept of *PortalSession* from the concept of
*Protocol* since both depend heavily on the other (they are often created as the same class). When
data flows through this part of the system, this is how it goes
```
# In the Portal
You <->
Protocol + PortalSession <->
PortalSessionHandler <->
(AMP) <->
ServerSessionHandler <->
ServerSession <->
InputFunc
```
(See the [Message Path](./Messagepath) for the bigger picture of how data flows through Evennia). The
parts that needs to be customized to make your own custom protocol is the `Protocol + PortalSession`
(which translates between data coming in/out over the wire to/from Evennia internal representation)
as well as the `InputFunc` (which handles incoming data).
## Adding custom Protocols
Evennia has a plugin-system that add the protocol as a new "service" to the application.
Take a look at `evennia/server/portal/portal.py`, notably the sections towards the end of that file.
These are where the various in-built services like telnet, ssh, webclient etc are added to the
Portal (there is an equivalent but shorter list in `evennia/server/server.py`).
To add a new service of your own (for example your own custom client protocol) to the Portal or
Server, look at `mygame/server/conf/server_services_plugins` and `portal_services_plugins`. By
default Evennia will look into these modules to find plugins. If you wanted to have it look for more
modules, you could do the following:
```python
# add to the Server
SERVER_SERVICES_PLUGIN_MODULES.append('server.conf.my_server_plugins')
# or, if you want to add to the Portal
PORTAL_SERVICES_PLUGIN_MODULES.append('server.conf.my_portal_plugins')
```
When adding a new connection you'll most likely only need to add new things to the
`PORTAL_SERVICES_PLUGIN_MODULES`.
This module can contain whatever you need to define your protocol, but it *must* contain a function
`start_plugin_services(app)`. This is called by the Portal as part of its upstart. The function
`start_plugin_services` must contain all startup code the server need. The `app` argument is a
reference to the Portal/Server application itself so the custom service can be added to it. The
function should not return anything.
This is how it looks:
```python
# mygame/server/conf/portal_services_plugins.py
# here the new Portal Twisted protocol is defined
class MyOwnFactory( ... ):
[...]
# some configs
MYPROC_ENABLED = True # convenient off-flag to avoid having to edit settings all the time
MY_PORT = 6666
def start_plugin_services(portal):
"This is called by the Portal during startup"
if not MYPROC_ENABLED:
return
# output to list this with the other services at startup
print(" myproc: %s" % MY_PORT)
# some setup (simple example)
factory = MyOwnFactory()
my_service = internet.TCPServer(MY_PORT, factory)
# all Evennia services must be uniquely named
my_service.setName("MyService")
# add to the main portal application
portal.services.addService(my_service)
```
Once the module is defined and targeted in settings, just reload the server and your new
protocol/services should start with the others.
## Writing your own Protocol
Writing a stable communication protocol from scratch is not something we'll cover here, it's no
trivial task. The good news is that Twisted offers implementations of many common protocols, ready
for adapting.
Writing a protocol implementation in Twisted usually involves creating a class inheriting from an
already existing Twisted protocol class and from `evennia.server.session.Session` (multiple
inheritance), then overloading the methods that particular protocol uses to link them to the
Evennia-specific inputs.
Here's a example to show the concept:
```python
# In module that we'll later add to the system through PORTAL_SERVICE_PLUGIN_MODULES
# pseudo code
from twisted.something import TwistedClient
# this class is used both for Portal- and Server Sessions
from evennia.server.session import Session
from evennia.server.portal.portalsessionhandler import PORTAL_SESSIONS
class MyCustomClient(TwistedClient, Session):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sessionhandler = PORTAL_SESSIONS
# these are methods we must know that TwistedClient uses for
# communication. Name and arguments could vary for different Twisted protocols
def onOpen(self, *args, **kwargs):
# let's say this is called when the client first connects
# we need to init the session and connect to the sessionhandler. The .factory
# is available through the Twisted parents
client_address = self.getClientAddress() # get client address somehow
self.init_session("mycustom_protocol", client_address, self.factory.sessionhandler)
self.sessionhandler.connect(self)
def onClose(self, reason, *args, **kwargs):
# called when the client connection is dropped
# link to the Evennia equivalent
self.disconnect(reason)
def onMessage(self, indata, *args, **kwargs):
# called with incoming data
# convert as needed here
self.data_in(data=indata)
def sendMessage(self, outdata, *args, **kwargs):
# called to send data out
# modify if needed
super().sendMessage(self, outdata, *args, **kwargs)
# these are Evennia methods. They must all exist and look exactly like this
# The above twisted-methods call them and vice-versa. This connects the protocol
# the Evennia internals.
def disconnect(self, reason=None):
"""
Called when connection closes.
This can also be called directly by Evennia when manually closing the connection.
Do any cleanups here.
"""
self.sessionhandler.disconnect(self)
def at_login(self):
"""
Called when this session authenticates by the server (if applicable)
"""
def data_in(self, **kwargs):
"""
Data going into the server should go through this method. It
should pass data into `sessionhandler.data_in`. THis will be called
by the sessionhandler with the data it gets from the approrpriate
send_* method found later in this protocol.
"""
self.sessionhandler.data_in(self, text=kwargs['data'])
def data_out(self, **kwargs):
"""
Data going out from the server should go through this method. It should
hand off to the protocol's send method, whatever it's called.
"""
# we assume we have a 'text' outputfunc
self.onMessage(kwargs['text'])
# 'outputfuncs' are defined as `send_<outputfunc_name>`. From in-code, they are called
# with `msg(outfunc_name=<data>)`.
def send_text(self, txt, *args, **kwargs):
"""
Send text, used with e.g. `session.msg(text="foo")`
"""
# we make use of the
self.data_out(text=txt)
def send_default(self, cmdname, *args, **kwargs):
"""
Handles all outputfuncs without an explicit `send_*` method to handle them.
"""
self.data_out(**{cmdname: str(args)})
```
The principle here is that the Twisted-specific methods are overridden to redirect inputs/outputs to
the Evennia-specific methods.
### Sending data out
To send data out through this protocol, you'd need to get its Session and then you could e.g.
```python
session.msg(text="foo")
```
The message will pass through the system such that the sessionhandler will dig out the session and
check if it has a `send_text` method (it has). It will then pass the "foo" into that method, which
in our case means sending "foo" across the network.
### Receiving data
Just because the protocol is there, does not mean Evennia knows what to do with it. An
[Inputfunc](./Inputfuncs) must exist to receive it. In the case of the `text` input exemplified above,
Evennia alredy handles this input - it will parse it as a Command name followed by its inputs. So
handle that you need to simply add a cmdset with commands on your receiving Session (and/or the
Object/Character it is puppeting). If not you may need to add your own Inputfunc (see the
[Inputfunc](./Inputfuncs) page for how to do this.
These might not be as clear-cut in all protocols, but the principle is there. These four basic
components - however they are accessed - links to the *Portal Session*, which is the actual common
interface between the different low-level protocols and Evennia.
## Assorted notes
To take two examples, Evennia supports the *telnet* protocol as well as *webclient*, via ajax or
websockets. You'll find that whereas telnet is a textbook example of a Twisted protocol as seen
above, the ajax protocol looks quite different due to how it interacts with the
webserver through long-polling (comet) style requests. All the necessary parts
mentioned above are still there, but by necessity implemented in very different
ways.

View file

@ -0,0 +1,484 @@
# Customize channels
# Channel commands in Evennia
By default, Evennia's default channel commands are inspired by MUX. They all
begin with "c" followed by the action to perform (like "ccreate" or "cdesc").
If this default seems strange to you compared to other Evennia commands that
rely on switches, you might want to check this tutorial out.
This tutorial will also give you insight into the workings of the channel system.
So it may be useful even if you don't plan to make the exact changes shown here.
## What we will try to do
Our mission: change the default channel commands to have a different syntax.
This tutorial will do the following changes:
- Remove all the default commands to handle channels.
- Add a `+` and `-` command to join and leave a channel. So, assuming there is
a `public` channel on your game (most often the case), you could type `+public`
to join it and `-public` to leave it.
- Group the commands to manipulate channels under the channel name, after a
switch. For instance, instead of writing `cdesc public = My public channel`,
you would write `public/desc My public channel`.
> I listed removing the default Evennia commands as a first step in the
> process. Actually, we'll move it at the very bottom of the list, since we
> still want to use them, we might get it wrong and rely on Evennia commands
> for a while longer.
## A command to join, another to leave
We'll do the most simple task at first: create two commands, one to join a
channel, one to leave.
> Why not have them as switches? `public/join` and `public/leave` for instance?
For security reasons, I will hide channels to which the caller is not
connected. It means that if the caller is not connected to the "public"
channel, he won't be able to use the "public" command. This is somewhat
standard: if we create an administrator-only channel, we don't want players to
try (or even know) the channel command. Again, you could design it a different
way should you want to.
First create a file named `comms.py` in your `commands` package. It's
a rather logical place, since we'll write different commands to handle
communication.
Okay, let's add the first command to join a channel:
```python
# in commands/comms.py
from evennia.utils.search import search_channel
from commands.command import Command
class CmdConnect(Command):
"""
Connect to a channel.
"""
key = "+"
help_category = "Comms"
locks = "cmd:not pperm(channel_banned)"
auto_help = False
def func(self):
"""Implement the command"""
caller = self.caller
args = self.args
if not args:
self.msg("Which channel do you want to connect to?")
return
channelname = self.args
channel = search_channel(channelname)
if not channel:
return
# Check permissions
if not channel.access(caller, 'listen'):
self.msg("%s: You are not allowed to listen to this channel." % channel.key)
return
# If not connected to the channel, try to connect
if not channel.has_connection(caller):
if not channel.connect(caller):
self.msg("%s: You are not allowed to join this channel." % channel.key)
return
else:
self.msg("You now are connected to the %s channel. " % channel.key.lower())
else:
self.msg("You already are connected to the %s channel. " % channel.key.lower())
```
Okay, let's review this code, but if you're used to Evennia commands, it shouldn't be too strange:
1. We import `search_channel`. This is a little helper function that we will use to search for
channels by name and aliases, found in `evennia.utils.search`. It's just more convenient.
2. Our class `CmdConnect` contains the body of our command to join a channel.
3. Notice the key of this command is simply `"+"`. When you enter `+something` in the game, it will
try to find a command key `+something`. Failing that, it will look at other potential matches.
Evennia is smart enough to understand that when we type `+something`, `+` is the command key and
`something` is the command argument. This will, of course, fail if you have a command beginning by
`+` conflicting with the `CmdConnect` key.
4. We have altered some class attributes, like `auto_help`. If you want to know what they do and
why they have changed here, you can check the [documentation on commands](./Commands).
5. In the command body, we begin by extracting the channel name. Remember that this name should be
in the command arguments (that is, in `self.args`). Following the same example, if a player enters
`+something`, `self.args` should contain `"something"`. We use `search_channel` to see if this
channel exists.
6. We then check the access level of the channel, to see if the caller can listen to it (not
necessarily use it to speak, mind you, just listen to others speak, as these are two different locks
on Evennia).
7. Finally, we connect the caller if he's not already connected to the channel. We use the
channel's `connect` method to do this. Pretty straightforward eh?
Now we'll add a command to leave a channel. It's almost the same, turned upside down:
```python
class CmdDisconnect(Command):
"""
Disconnect from a channel.
"""
key = "-"
help_category = "Comms"
locks = "cmd:not pperm(channel_banned)"
auto_help = False
def func(self):
"""Implement the command"""
caller = self.caller
args = self.args
if not args:
self.msg("Which channel do you want to disconnect from?")
return
channelname = self.args
channel = search_channel(channelname)
if not channel:
return
# If connected to the channel, try to disconnect
if channel.has_connection(caller):
if not channel.disconnect(caller):
self.msg("%s: You are not allowed to disconnect from this channel." % channel.key)
return
else:
self.msg("You stop listening to the %s channel. " % channel.key.lower())
else:
self.msg("You are not connected to the %s channel. " % channel.key.lower())
```
So far, you shouldn't have trouble following what this command does: it's
pretty much the same as the `CmdConnect` class in logic, though it accomplishes
the opposite. If you are connected to the channel `public` you could
disconnect from it using `-public`. Remember, you can use channel aliases too
(`+pub` and `-pub` will also work, assuming you have the alias `pub` on the
`public` channel).
It's time to test this code, and to do so, you will need to add these two
commands. Here is a good time to say it: by default, Evennia connects accounts
to channels. Some other games (usually with a higher multisession mode) will
want to connect characters instead of accounts, so that several characters in
the same account can be connected to various channels. You can definitely add
these commands either in the `AccountCmdSet` or `CharacterCmdSet`, the caller
will be different and the command will add or remove accounts of characters.
If you decide to install these commands on the `CharacterCmdSet`, you might
have to disconnect your superuser account (account #1) from the channel before
joining it with your characters, as Evennia tends to subscribe all accounts
automatically if you don't tell it otherwise.
So here's an example of how to add these commands into your `AccountCmdSet`.
Edit the file `commands/default_cmdsets.py` to change a few things:
```python
# In commands/default_cmdsets.py
from evennia import default_cmds
from commands.comms import CmdConnect, CmdDisconnect
# ... Skip to the AccountCmdSet class ...
class AccountCmdSet(default_cmds.AccountCmdSet):
"""
This is the cmdset available to the Account at all times. It is
combined with the `CharacterCmdSet` when the Account puppets a
Character. It holds game-account-specific commands, channel
commands, etc.
"""
key = "DefaultAccount"
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
# Channel commands
self.add(CmdConnect())
self.add(CmdDisconnect())
```
Save, reload your game, and you should be able to use `+public` and `-public`
now!
## A generic channel command with switches
It's time to dive a little deeper into channel processing. What happens in
Evennia when a player enters `public Hello everybody!`?
Like exits, channels are a particular command that Evennia automatically
creates and attaches to individual channels. So when you enter `public
message` in your game, Evennia calls the `public` command.
> But I didn't add any public command...
Evennia will just create these commands automatically based on the existing
channels. The base command is the command we'll need to edit.
> Why edit it? It works just fine to talk.
Unfortunately, if we want to add switches to our channel names, we'll have to
edit this command. It's not too hard, however, we'll just start writing a
standard command with minor twitches.
### Some additional imports
You'll need to add a line of import in your `commands/comms.py` file. We'll
see why this import is important when diving in the command itself:
```python
from evennia.comms.models import ChannelDB
```
### The class layout
```python
# In commands/comms.py
class ChannelCommand(Command):
"""
{channelkey} channel
{channeldesc}
Usage:
{lower_channelkey} <message>
{lower_channelkey}/history [start]
{lower_channelkey}/me <message>
{lower_channelkey}/who
Switch:
history: View 20 previous messages, either from the end or
from <start> number of messages from the end.
me: Perform an emote on this channel.
who: View who is connected to this channel.
Example:
{lower_channelkey} Hello World!
{lower_channelkey}/history
{lower_channelkey}/history 30
{lower_channelkey}/me grins.
{lower_channelkey}/who
"""
# note that channeldesc and lower_channelkey will be filled
# automatically by ChannelHandler
# this flag is what identifies this cmd as a channel cmd
# and branches off to the system send-to-channel command
# (which is customizable by admin)
is_channel = True
key = "general"
help_category = "Channel Names"
obj = None
arg_regex = ""
```
There are some differences here compared to most common commands.
- There is something disconcerting in the class docstring. Some information is
between curly braces. This is a format-style which is only used for channel
commands. `{channelkey}` will be replaced by the actual channel key (like
public). `{channeldesc}` will be replaced by the channel description (like
"public channel"). And `{lower_channelkey}`.
- We have set `is_channel` to `True` in the command class variables. You
shouldn't worry too much about that: it just tells Evennia this is a special
command just for channels.
- `key` is a bit misleading because it will be replaced eventually. So we
could set it to virtually anything.
- The `obj` class variable is another one we won't detail right now.
- `arg_regex` is important: the default `arg_regex` in the channel command will
forbid to use switches (a slash just after the channel name is not allowed).
That's why we enforce it here, we allow any syntax.
> What will become of this command?
Well, when we'll be through with it, and once we'll add it as the default
command to handle channels, Evennia will create one per existing channel. For
instance, the public channel will receive one command of this class, with `key`
set to `public` and `aliases` set to the channel aliases (like `['pub']`).
> Can I see it work?
Not just yet, there's still a lot of code needed.
Okay we have the command structure but it's rather empty.
### The parse method
The `parse` method is called before `func` in every command. Its job is to
parse arguments and in our case, we will analyze switches here.
```python
# ...
def parse(self):
"""
Simple parser
"""
# channel-handler sends channame:msg here.
channelname, msg = self.args.split(":", 1)
self.switch = None
if msg.startswith("/"):
try:
switch, msg = msg[1:].split(" ", 1)
except ValueError:
switch = msg[1:]
msg = ""
self.switch = switch.lower().strip()
self.args = (channelname.strip(), msg.strip())
```
Reading the comments we see that the channel handler will send the command in a
strange way: a string with the channel name, a colon and the actual message
entered by the player. So if the player enters "public hello", the command
`args` will contain `"public:hello"`. You can look at the way the channel name
and message are parsed, this can be used in a lot of different commands.
Next we check if there's any switch, that is, if the message starts with a
slash. This would be the case if a player entered `public/me jumps up and
down`, for instance. If there is a switch, we save it in `self.switch`. We
alter `self.args` at the end to contain a tuple with two values: the channel
name, and the message (if a switch was used, notice that the switch will be
stored in `self.switch`, not in the second element of `self.args`).
### The command func
Finally, let's see the `func` method in the command class. It will have to
handle switches and also the raw message to send if no switch was used.
```python
# ...
def func(self):
"""
Create a new message and send it to channel, using
the already formatted input.
"""
channelkey, msg = self.args
caller = self.caller
channel = ChannelDB.objects.get_channel(channelkey)
# Check that the channel exists
if not channel:
self.msg(_("Channel '%s' not found.") % channelkey)
return
# Check that the caller is connected
if not channel.has_connection(caller):
string = "You are not connected to channel '%s'."
self.msg(string % channelkey)
return
# Check that the caller has send access
if not channel.access(caller, 'send'):
string = "You are not permitted to send to channel '%s'."
self.msg(string % channelkey)
return
# Handle the various switches
if self.switch == "me":
if not msg:
self.msg("What do you want to do on this channel?")
else:
msg = "{} {}".format(caller.key, msg)
channel.msg(msg, online=True)
elif self.switch:
self.msg("{}: Invalid switch {}.".format(channel.key, self.switch))
elif not msg:
self.msg("Say what?")
else:
if caller in channel.mutelist:
self.msg("You currently have %s muted." % channel)
return
channel.msg(msg, senders=self.caller, online=True)
```
- First of all, we try to get the channel object from the channel name we have
in the `self.args` tuple. We use `ChannelDB.objects.get_channel` this time
because we know the channel name isn't an alias (that was part of the deal,
`channelname` in the `parse` method contains a command key).
- We check that the channel does exist.
- We then check that the caller is connected to the channel. Remember, if the
caller isn't connected, we shouldn't allow him to use this command (that
includes the switches on channels).
- We then check that the caller has access to the channel's `send` lock. This
time, we make sure the caller can send messages to the channel, no matter what
operation he's trying to perform.
- Finally we handle switches. We try only one switch: `me`. This switch would
be used if a player entered `public/me jumps up and down` (to do a channel
emote).
- We handle the case where the switch is unknown and where there's no switch
(the player simply wants to talk on this channel).
The good news: The code is not too complicated by itself. The bad news is that
this is just an abridged version of the code. If you want to handle all the
switches mentioned in the command help, you will have more code to write. This
is left as an exercise.
### End of class
It's almost done, but we need to add a method in this command class that isn't
often used. I won't detail it's usage too much, just know that Evennia will use
it and will get angry if you don't add it. So at the end of your class, just
add:
```python
# ...
def get_extra_info(self, caller, **kwargs):
"""
Let users know that this command is for communicating on a channel.
Args:
caller (TypedObject): A Character or Account who has entered an ambiguous command.
Returns:
A string with identifying information to disambiguate the object, conventionally with a
preceding space.
"""
return " (channel)"
```
### Adding this channel command
Contrary to most Evennia commands, we won't add our `ChannelCommand` to a
`CmdSet`. Instead we need to tell Evennia that it should use the command we
just created instead of its default channel-command.
In your `server/conf/settings.py` file, add a new setting:
```python
# Channel options
CHANNEL_COMMAND_CLASS = "commands.comms.ChannelCommand"
```
Then you can reload your game. Try to type `public hello` and `public/me jumps
up and down`. Don't forget to enter `help public` to see if your command has
truly been added.
## Conclusion and full code
That was some adventure! And there's still things to do! But hopefully, this
tutorial will have helped you in designing your own channel system. Here are a
few things to do:
- Add more switches to handle various actions, like changing the description of
a channel for instance, or listing the connected participants.
- Remove the default Evennia commands to handle channels.
- Alter the behavior of the channel system so it better aligns with what you
want to do.
As a special bonus, you can find a full, working example of a communication
system similar to the one I've shown you: this is a working example, it
integrates all switches and does ever some extra checking, but it's also very
close from the code I've provided here. Notice, however, that this resource is
external to Evennia and not maintained by anyone but the original author of
this article.
[Read the full example on Github](https://github.com/vincent-
lg/avenew/blob/master/commands/comms.py)

296
docs/source/Debugging.md Normal file
View file

@ -0,0 +1,296 @@
# Debugging
Sometimes, an error is not trivial to resolve. A few simple `print` statements is not enough to find
the cause of the issue. Running a *debugger* can then be very helpful and save a lot of time.
Debugging
means running Evennia under control of a special *debugger* program. This allows you to stop the
action at a given point, view the current state and step forward through the program to see how its
logic works.
Evennia natively supports these debuggers:
- [Pdb](https://docs.python.org/2/library/pdb.html) is a part of the Python distribution and
available out-of-the-box.
- [PuDB](https://pypi.org/project/pudb/) is a third-party debugger that has a slightly more
'graphical', curses-based user interface than pdb. It is installed with `pip install pudb`.
## Debugging Evennia
To run Evennia with the debugger, follow these steps:
1. Find the point in the code where you want to have more insight. Add the following line at that
point.
```python
from evennia import set_trace;set_trace()
```
2. (Re-)start Evennia in interactive (foreground) mode with `evennia istart`. This is important -
without this step the debugger will not start correctly - it will start in this interactive
terminal.
3. Perform the steps that will trigger the line where you added the `set_trace()` call. The debugger
will start in the terminal from which Evennia was interactively started.
The `evennia.set_trace` function takes the following arguments:
```python
evennia.set_trace(debugger='auto', term_size=(140, 40))
```
Here, `debugger` is one of `pdb`, `pudb` or `auto`. If `auto`, use `pudb` if available, otherwise
use `pdb`. The `term_size` tuple sets the viewport size for `pudb` only (it's ignored by `pdb`).
## A simple example using pdb
The debugger is useful in different cases, but to begin with, let's see it working in a command.
Add the following test command (which has a range of deliberate errors) and also add it to your
default cmdset. Then restart Evennia in interactive mode with `evennia istart`.
```python
# In file commands/command.py
class CmdTest(Command):
"""
A test command just to test pdb.
Usage:
test
"""
key = "test"
def func(self):
from evennia import set_trace; set_trace() # <--- start of debugger
obj = self.search(self.args)
self.msg("You've found {}.".format(obj.get_display_name()))
```
If you type `test` in your game, everything will freeze. You won't get any feedback from the game,
and you won't be able to enter any command (nor anyone else). It's because the debugger has started
in your console, and you will find it here. Below is an example with `pdb`.
```
...
> .../mygame/commands/command.py(79)func()
-> obj = self.search(self.args)
(Pdb)
```
`pdb` notes where it has stopped execution and, what line is about to be executed (in our case, `obj
= self.search(self.args)`), and ask what you would like to do.
### Listing surrounding lines of code
When you have the `pdb` prompt `(Pdb)`, you can type in different commands to explore the code. The
first one you should know is `list` (you can type `l` for short):
```
(Pdb) l
43
44 key = "test"
45
46 def func(self):
47 from evennia import set_trace; set_trace() # <--- start of debugger
48 -> obj = self.search(self.args)
49 self.msg("You've found {}.".format(obj.get_display_name()))
50
51 # -------------------------------------------------------------
52 #
53 # The default commands inherit from
(Pdb)
```
Okay, this didn't do anything spectacular, but when you become more confident with `pdb` and find
yourself in lots of different files, you sometimes need to see what's around in code. Notice that
there is a little arrow (`->`) before the line that is about to be executed.
This is important: **about to be**, not **has just been**. You need to tell `pdb` to go on (we'll
soon see how).
### Examining variables
`pdb` allows you to examine variables (or really, to run any Python instruction). It is very useful
to know the values of variables at a specific line. To see a variable, just type its name (as if
you were in the Python interpreter:
```
(Pdb) self
<commands.command.CmdTest object at 0x045A0990>
(Pdb) self.args
u''
(Pdb) self.caller
<Character: XXX>
(Pdb)
```
If you try to see the variable `obj`, you'll get an error:
```
(Pdb) obj
*** NameError: name 'obj' is not defined
(Pdb)
```
That figures, since at this point, we haven't created the variable yet.
> Examining variable in this way is quite powerful. You can even run Python code and keep on
> executing, which can help to check that your fix is actually working when you have identified an
> error. If you have variable names that will conflict with `pdb` commands (like a `list`
> variable), you can prefix your variable with `!`, to tell `pdb` that what follows is Python code.
### Executing the current line
It's time we asked `pdb` to execute the current line. To do so, use the `next` command. You can
shorten it by just typing `n`:
```
(Pdb) n
AttributeError: "'CmdTest' object has no attribute 'search'"
> .../mygame/commands/command.py(79)func()
-> obj = self.search(self.args)
(Pdb)
```
`Pdb` is complaining that you try to call the `search` method on a command... whereas there's no
`search` method on commands. The character executing the command is in `self.caller`, so we might
change our line:
```python
obj = self.caller.search(self.args)
```
### Letting the program run
`pdb` is waiting to execute the same instruction... it provoked an error but it's ready to try
again, just in case. We have fixed it in theory, but we need to reload, so we need to enter a
command. To tell `pdb` to terminate and keep on running the program, use the `continue` (or `c`)
command:
```
(Pdb) c
...
```
You see an error being caught, that's the error we have fixed... or hope to have. Let's reload the
game and try again. You need to run `evennia istart` again and then run `test` to get into the
command again.
```
> .../mygame/commands/command.py(79)func()
-> obj = self.caller.search(self.args)
(Pdb)
```
`pdb` is about to run the line again.
```
(Pdb) n
> .../mygame/commands/command.py(80)func()
-> self.msg("You've found {}.".format(obj.get_display_name()))
(Pdb)
```
This time the line ran without error. Let's see what is in the `obj` variable:
```
(Pdb) obj
(Pdb) print obj
None
(Pdb)
```
We have entered the `test` command without parameter, so no object could be found in the search
(`self.args` is an empty string).
Let's allow the command to continue and try to use an object name as parameter (although, we should
fix that bug too, it would be better):
```
(Pdb) c
...
```
Notice that you'll have an error in the game this time. Let's try with a valid parameter. I have
another character, `barkeep`, in this room:
```test barkeep```
And again, the command freezes, and we have the debugger opened in the console.
Let's execute this line right away:
```
> .../mygame/commands/command.py(79)func()
-> obj = self.caller.search(self.args)
(Pdb) n
> .../mygame/commands/command.py(80)func()
-> self.msg("You've found {}.".format(obj.get_display_name()))
(Pdb) obj
<Character: barkeep>
(Pdb)
```
At least this time we have found the object. Let's process...
```
(Pdb) n
TypeError: 'get_display_name() takes exactly 2 arguments (1 given)'
> .../mygame/commands/command.py(80)func()
-> self.msg("You've found {}.".format(obj.get_display_name()))
(Pdb)
```
As an exercise, fix this error, reload and run the debugger again. Nothing better than some
experimenting!
Your debugging will often follow the same strategy:
1. Receive an error you don't understand.
2. Put a breaking point **BEFORE** the error occurs.
3. Run the code again and see the debugger open.
4. Run the program line by line,examining variables, checking the logic of instructions.
5. Continue and try again, each step a bit further toward the truth and the working feature.
### Stepping through a function
`n` is useful, but it will avoid stepping inside of functions if it can. But most of the time, when
we have an error we don't understand, it's because we use functions or methods in a way that wasn't
intended by the developer of the API. Perhaps using wrong arguments, or calling the function in a
situation that would cause a bug. When we have a line in the debugger that calls a function or
method, we can "step" to examine it further. For instance, in the previous example, when `pdb` was
about to execute `obj = self.caller.search(self.args)`, we may want to see what happens inside of
the `search` method.
To do so, use the `step` (or `s`) command. This command will show you the definition of the
function/method and you can then use `n` as before to see it line-by-line. In our little example,
stepping through a function or method isn't that useful, but when you have an impressive set of
commands, functions and so on, it might really be handy to examine some feature and make sure they
operate as planned.
## Cheat-sheet of pdb/pudb commands
PuDB and Pdb share the same commands. The only real difference is how it's presented. The `look`
command is not needed much in `pudb` since it displays the code directly in its user interface.
| Pdb/PuDB command | To do what |
| ----------- | ---------- |
| list (or l) | List the lines around the point of execution (not needed for `pudb`, it will show
this directly). |
| print (or p) | Display one or several variables. |
| `!` | Run Python code (using a `!` is often optional). |
| continue (or c) | Continue execution and terminate the debugger for this time. |
| next (or n) | Execute the current line and goes to the next one. |
| step (or s) | Step inside of a function or method to examine it. |
| `<RETURN>` | Repeat the last command (don't type `n` repeatedly, just type it once and then press
`<RETURN>` to repeat it). |
If you want to learn more about debugging with Pdb, you will find an [interesting tutorial on that
topic here](https://pymotw.com/3/pdb/).

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,122 @@
# Default Exit Errors
Evennia allows for exits to have any name. The command "kitchen" is a valid exit name as well as
"jump out the window" or "north". An exit actually consists of two parts: an [Exit Object](./Objects)
and an [Exit Command](./Commands) stored on said exit object. The command has the same key and aliases
as the object, which is why you can see the exit in the room and just write its name to traverse it.
If you try to enter the name of a non-existing exit, it is thus the same as trying a non-exising
command; Evennia doesn't care about the difference:
> jump out the window
Command 'jump out the window' is not available. Type "help" for help.
Many games don't need this type of freedom however. They define only the cardinal directions as
valid exit names (Evennia's `@tunnel` command also offers this functionality). In this case, the
error starts to look less logical:
> west
Command 'west' is not available. Maybe you meant "@set" or "@reset"?
Since we for our particular game *know* that west is an exit direction, it would be better if the
error message just told us that we couldn't go there.
## Adding default error commands
To solve this you need to be aware of how to [write and add new commands](./Adding-Command-Tutorial).
What you need to do is to create new commands for all directions you want to support in your game.
In this example all we'll do is echo an error message, but you could certainly consider more
advanced uses. You add these commands to the default command set. Here is an example of such a set
of commands:
```python
# for example in a file mygame/commands/movecommands.py
from evennia import default_cmds
class CmdExitError(default_cmds.MuxCommand):
"Parent class for all exit-errors."
locks = "cmd:all()"
arg_regex = r"\s|$"
auto_help = False
def func(self):
"returns the error"
self.caller.msg("You cannot move %s." % self.key)
class CmdExitErrorNorth(CmdExitError):
key = "north"
aliases = ["n"]
class CmdExitErrorEast(CmdExitError):
key = "east"
aliases = ["e"]
class CmdExitErrorSouth(CmdExitError):
key = "south"
aliases = ["s"]
class CmdExitErrorWest(CmdExitError):
key = "west"
aliases = ["w"]
```
Make sure to add the directional commands (not their parent) to the `CharacterCmdSet` class in
`mygame/commands/default_cmdsets.py`:
```python
# in mygame/commands/default_cmdsets.py
from commands import movecommands
# [...]
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# [...]
def at_cmdset_creation(self):
# [...]
self.add(movecommands.CmdExitErrorNorth())
self.add(movecommands.CmdExitErrorEast())
self.add(movecommands.CmdExitErrorSouth())
self.add(movecommands.CmdExitErrorWest())
```
After a `@reload` these commands (assuming you don't get any errors - check your log) will be
loaded. What happens henceforth is that if you are in a room with an Exitobject (let's say it's
"north"), the proper Exit-command will overload your error command (also named "north"). But if you
enter an direction without having a matching exit for it, you will fallback to your default error
commands:
> east
You cannot move east.
Further expansions by the exit system (including manipulating the way the Exit command itself is
created) can be done by modifying the [Exit typeclass](./Typeclasses) directly.
## Additional Comments
So why didn't we create a single error command above? Something like this:
```python
class CmdExitError(default_cmds.MuxCommand):
"Handles all exit-errors."
key = "error_cmd"
aliases = ["north", "n",
"east", "e",
"south", "s",
"west", "w"]
#[...]
```
The anwer is that this would *not* work and understanding why is important in order to not be
confused when working with commands and command sets.
The reason it doesn't work is because Evennia's [command system](./Commands) compares commands *both*
by `key` and by `aliases`. If *either* of those match, the two commands are considered *identical*
as far as cmdset merging system is concerned.
So the above example would work fine as long as there were no Exits at all in the room. But what
happens when we enter a room with an exit "north"? The Exit's cmdset is merged onto the default one,
and since there is an alias match, the system determines our `CmdExitError` to be identical. It is
thus overloaded by the Exit command (which also correctly defaults to a higher priority). The result
is that you can go through the north exit normally but none of the error messages for the other
directions are available since the single error command was completely overloaded by the single
matching "north" exit-command.

View file

@ -0,0 +1,170 @@
# Developer Central
This page serves as a central nexus for information on using Evennia as well as developing the
library itself.
### General Evennia development information
- [Introduction to coding with Evennia](./Coding-Introduction)
- [Evennia Licensing FAQ](./Licensing)
- [Contributing to Evennia](./Contributing)
- [Code Style Guide](https://github.com/evennia/evennia/blob/master/CODING_STYLE.md) (Important!)
- [Policy for 'MUX-like' default commands](./Using-MUX-as-a-Standard)
- [Setting up a Git environment for coding](./Version-Control)
- [Getting started with Travis and Github for continuous integration testing](./Using-Travis)
- [Planning your own Evennia game](./Game-Planning)
- [First steps coding Evennia](./First-Steps-Coding)
- [Translating Evennia](./Internationalization#translating-evennia)
- [Evennia Quirks](./Quirks) to keep in mind.
- [Directions for configuring PyCharm with Evennia on Windows](./Setting-up-PyCharm)
### Evennia API
- [Directory Overview](./Directory-Overview)
- [evennia - the flat API](./Evennia-API)
- [Running and Testing Python code](./Execute-Python-Code)
#### Core components and protocols
- [Server and Portal](./Portal-And-Server)
- [Sessions](./Sessions)
- [Configuration and module plugins](./Server-Conf)
- [The message path](./Messagepath)
- [OOB](./OOB) - Out-of-band communication
- [Inputfuncs](./Inputfuncs)
- [Adding new protocols (client APIs) and services](./Custom-Protocols)
- [Adding new database models](./New-Models)
- [Unit Testing](./Unit-Testing)
- [Running profiling](./Profiling)
- [Debugging your code](./Debugging)
#### In-game Commands
- [Command System overview](./Command-System)
- [Commands](./Commands)
- [Command Sets](./Command-Sets)
- [Command Auto-help](./Help-System#command-auto-help-system)
#### Typeclasses and related concepts
- [General about Typeclasses](./Typeclasses)
- [Objects](./Objects)
- [Characters](./Objects#characters)
- [Rooms](./Objects#rooms)
- [Exits](./Objects#exits)
- [Accounts](./Accounts)
- [Communications](./Communications)
- [Channels](./Communications#channels)
- [Scripts](./Scripts)
- [Global Scripts](./Scripts#Global-Scripts)
- [TickerHandler](./TickerHandler)
- [utils.delay](./Coding-Utils#utilsdelay)
- [MonitorHandler](./MonitorHandler)
- [Attributes](./Attributes)
- [Nicks](./Nicks)
- [Tags](./Tags)
- [Tags for Aliases and Permissions](./Tags#using-aliases-and-permissions)
#### Web
- [Web features overview](./Web-Features)
- [The Webclient](./Webclient)
- [Web tutorials](./Web-Tutorial)
#### Other systems
- [Locks](./Locks)
- [Permissions](./Locks#permissions)
- [Help System](./Help-System)
- [Signals](./Signals)
- [General coding utilities](./Coding-Utils)
- [Utils in evennia.utils.utils](api:evennia.utils.utils)
- [Game time](./Coding-Utils#game-time)
- [Game Menus](./EvMenu) (EvMenu)
- [Text paging/scrolling](./EvMore) (EvMore)
- [Text Line Editor](./EvEditor) (EvEditor)
- [Text Tables](github:evennia.utils.evtable) (EvTable)
- [Text Form generation](github:evennia.utils.evform) (EvForm)
- [Spawner and Prototypes](./Spawner-and-Prototypes)
- [Inlinefuncs](./TextTags#inline-functions)
- [Asynchronous execution](./Async-Process)
### Developer brainstorms and whitepages
- [API refactoring](./API-refactoring), discussing what parts of the Evennia API needs a
refactoring/cleanup/simplification
- [Docs refactoring](./Docs-refactoring), discussing how to reorganize and structure this wiki/docs
better going forward
- [Webclient brainstorm](./Webclient-brainstorm), some ideas for a future webclient gui
- [Roadmap](./Roadmap), a tentative list of future major features
- [Change log](https://github.com/evennia/evennia/blob/master/CHANGELOG.md) of big Evennia updates
over time
[group]: https://groups.google.com/forum/#!forum/evennia
[online-form]: https://docs.google.com/spreadsheet/viewform?hl=en_US&formkey=dGN0VlJXMWpCT3VHaHpscDE
zY1RoZGc6MQ#gid=0
[issues]: https://github.com/evennia/evennia/issues
```toctree::
:hidden:
Coding-Introduction
Licensing
Contributing
Using-MUX-as-a-Standard
Version-Control
Using-Travis
Game-Planning
First-Steps-Coding
Internationalization
Quirks
Setting-up-PyCharm
Directory-Overview
Evennia-API
Execute-Python-Code
Portal-And-Server
Sessions
Server-Conf
Messagepath
OOB
Inputfuncs
Custom-Protocols
New-Models
Unit-Testing
Profiling
Debugging
Command-System
Commands
Command-Sets
Help-System
Typeclasses
Objects
Accounts
Communications
Scripts
TickerHandler
Coding-Utils
MonitorHandler
Attributes
Nicks
Tags
Web-Features
Webclient
Web-Tutorial
Locks
Signals
Coding-Utils
EvMenu
EvMore
EvEditor
Spawner-and-Prototypes
TextTags
Async-Process
API-refactoring
Docs-refactoring
Webclient-brainstorm
```

View file

@ -0,0 +1,248 @@
# Dialogues in events
- Next tutorial: [adding a voice-operated elevator with events](A-voice-operated-elevator-using-
events).
This tutorial will walk you through the steps to create several dialogues with characters, using the
[in-game Python
system](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md).
This tutorial assumes the in-game Python system is installed in your game. If it isn't, you can
follow the installation steps given in [the documentation on in-game
Python](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md), and
come back on this tutorial once the system is installed. **You do not need to read** the entire
documentation, it's a good reference, but not the easiest way to learn about it. Hence these
tutorials.
The in-game Python system allows to run code on individual objects in some situations. You don't
have to modify the source code to add these features, past the installation. The entire system
makes it easy to add specific features to some objects, but not all. This is why it can be very
useful to create a dialogue system taking advantage of the in-game Python system.
> What will we try to do?
In this tutorial, we are going to create a basic dialogue to have several characters automatically
respond to specific messages said by others.
## A first example with a first character
Let's create a character to begin with.
@charcreate a merchant
This will create a merchant in the room where you currently are. It doesn't have anything, like a
description, you can decorate it a bit if you like.
As said above, the in-game Python system consists in linking objects with arbitrary code. This code
will be executed in some circumstances. Here, the circumstance is "when someone says something in
the same room", and might be more specific like "when someone says hello". We'll decide what code
to run (we'll actually type the code in-game). Using the vocabulary of the in-game Python system,
we'll create a callback: a callback is just a set of lines of code that will run under some
conditions.
You can have an overview of every "conditions" in which callbacks can be created using the `@call`
command (short for `@callback`). You need to give it an object as argument. Here for instance, we
could do:
@call a merchant
You should see a table with three columns, showing the list of events existing on our newly-created
merchant. There are quite a lot of them, as it is, althougn no line of code has been set yet. For
our system, you might be more interested by the line describing the `say` event:
| say | 0 (0) | After another character has said something in |
| | | the character's room. |
We'll create a callback on the `say` event, called when we say "hello" in the merchant's room:
@call/add a merchant = say hello
Before seeing what this command displays, let's see the command syntax itself:
- `@call` is the command name, `/add` is a switch. You can read the help of the command to get the
help of available switches and a brief overview of syntax.
- We then enter the object's name, here "a merchant". You can enter the ID too ("#3" in my case),
which is useful to edit the object when you're not in the same room. You can even enter part of the
name, as usual.
- An equal sign, a simple separator.
- The event's name. Here, it's "say". The available events are displayed when you use `@call`
without switch.
- After a space, we enter the conditions in which this callback should be called. Here, the
conditions represent what the other character should say. We enter "hello". Meaning that if
someone says something containing "hello" in the room, the callback we are now creating will be
called.
When you enter this command, you should see something like this:
```
After another character has said something in the character's room.
This event is called right after another character has said
something in the same location. The action cannot be prevented
at this moment. Instead, this event is ideal to create keywords
that would trigger a character (like a NPC) in doing something
if a specific phrase is spoken in the same location.
To use this event, you have to specify a list of keywords as
parameters that should be present, as separate words, in the
spoken phrase. For instance, you can set a callback that would
fire if the phrase spoken by the character contains "menu" or
"dinner" or "lunch":
@call/add ... = say menu, dinner, lunch
Then if one of the words is present in what the character says,
this callback will fire.
Variables you can use in this event:
speaker: the character speaking in this room.
character: the character connected to this event.
message: the text having been spoken by the character.
```
That's some list of information. What's most important to us now is:
- The "say" event is called whenever someone else speaks in the room.
- We can set callbacks to fire when specific keywords are present in the phrase by putting them as
additional parameters. Here we have set this parameter to "hello". We can have several keywords
separated by a comma (we'll see this in more details later).
- We have three default variables we can use in this callback: `speaker` which contains the
character who speaks, `character` which contains the character who's modified by the in-game Python
system (here, or merchant), and `message` which contains the spoken phrase.
This concept of variables is important. If it makes things more simple to you, think of them as
parameters in a function: they can be used inside of the function body because they have been set
when the function was called.
This command has opened an editor where we can type our Python code.
```
----------Line Editor [Callback say of a merchant]--------------------------------
01|
----------[l:01 w:000 c:0000]------------(:h for help)----------------------------
```
For our first test, let's type something like:
```python
character.location.msg_contents("{character} shrugs and says: 'well, yes, hello to you!'",
mapping=dict(character=character))
```
Once you have entered this line, you can type `:wq` to save the editor and quit it.
And now if you use the "say" command with a message containing "hello":
```
You say, "Hello sir merchant!"
a merchant(#3) shrugs and says: 'well, yes, hello to you!'
```
If you say something that doesn't contain "hello", our callback won't execute.
**In summary**:
1. When we say something in the room, using the "say" command, the "say" event of all characters
(except us) is called.
2. The in-game Python system looks at what we have said, and checks whether one of our callbacks in
the "say" event contains a keyword that we have spoken.
3. If so, call it, defining the event variables as we have seen.
4. The callback is then executed as normal Python code. Here we have called the `msg_contents`
method on the character's location (probably a room) to display a message to the entire room. We
have also used mapping to easily display the character's name. This is not specific to the in-game
Python system. If you feel overwhelmed by the code we've used, just shorten it and use something
more simple, for instance:
```python
speaker.msg("You have said something to me.")
```
## The same callback for several keywords
It's easy to create a callback that will be triggered if the sentence contains one of several
keywords.
@call/add merchant = say trade, trader, goods
And in the editor that opens:
```python
character.location.msg_contents("{character} says: 'Ho well, trade's fine as long as roads are
safe.'", mapping=dict(character=character))
```
Then you can say something with either "trade", "trader" or "goods" in your sentence, which should
call the callback:
```
You say, "and how is your trade going?"
a merchant(#3) says: 'Ho well, trade's fine as long as roads are safe.'
```
We can set several keywords when adding the callback. We just need to separate them with commas.
## A longer callback
So far, we have only set one line in our callbacks. Which is useful, but we often need more. For
an entire dialogue, you might want to do a bit more than that.
@call/add merchant = say bandit, bandits
And in the editor you can paste the following lines:
```python
character.location.msg_contents("{character} says: 'Bandits he?'",
mapping=dict(character=character))
character.location.msg_contents("{character} scratches his head, considering.",
mapping=dict(character=character))
character.location.msg_contents("{character} whispers: 'Aye, saw some of them, north from here. No
trouble o' mine, but...'", mapping=dict(character=character))
speaker.msg("{character} looks at you more
closely.".format(character=character.get_display_name(speaker)))
speaker.msg("{character} continues in a low voice: 'Ain't my place to say, but if you need to find
'em, they're encamped some distance away from the road, I guess near a cave or
something.'.".format(character=character.get_display_name(speaker)))
```
Now try to ask the merchant about bandits:
```
You say, "have you seen bandits?"
a merchant(#3) says: 'Bandits he?'
a merchant(#3) scratches his head, considering.
a merchant(#3) whispers: 'Aye, saw some of them, north from here. No trouble o' mine, but...'
a merchant(#3) looks at you more closely.
a merchant(#3) continues in a low voice: 'Ain't my place to say, but if you need to find 'em,
they're encamped some distance away from the road, I guess near a cave or something.'.
```
Notice here that the first lines of dialogue are spoken to the entire room, but then the merchant is
talking directly to the speaker, and only the speaker hears it. There's no real limit to what you
can do with this.
- You can set a mood system, storing attributes in the NPC itself to tell you in what mood he is,
which will influence the information he will give... perhaps the accuracy of it as well.
- You can add random phrases spoken in some context.
- You can use other actions (you're not limited to having the merchant say something, you can ask
him to move, gives you something, attack if you have a combat system, or whatever else).
- The callbacks are in pure Python, so you can write conditions or loops.
- You can add in "pauses" between some instructions using chained events. This tutorial won't
describe how to do that however. You already have a lot to play with.
## Tutorial F.A.Q.
- **Q:** can I create several characters who would answer to specific dialogue?
- **A:** of course. Te in-game Python system is so powerful because you can set unique code for
various objects. You can have several characters answering to different things. You can even have
different characters in the room answering to greetings. All callbacks will be executed one after
another.
- **Q:** can I have two characters answering to the same dialogue in exactly the same way?
- **A:** It's possible but not so easy to do. Usually, event grouping is set in code, and depends
on different games. However, if it is for some infrequent occurrences, it's easy to do using
[chained events](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README
.md#chained-events).
- **Q:** is it possible to deploy callbacks on all characters sharing the same prototype?
- **A:** not out of the box. This depends on individual settings in code. One can imagine that all
characters of some type would share some events, but this is game-specific. Rooms of the same zone
could share the same events as well. It is possible to do but requires modification of the source
code.
- Next tutorial: [adding a voice-operated elevator with events](A-voice-operated-elevator-using-
events).

View file

@ -0,0 +1,68 @@
# Directory Overview
This is an overview of the directories relevant to Evennia coding.
## The Game directory
The game directory is created with `evennia --init <name>`. In the Evennia documentation we always
assume it's called `mygame`. Apart from the `server/` subfolder within, you could reorganize this
folder if you preferred a different code structure for your game.
- `mygame/`
- `commands/` - Overload default [Commands](./Commands) or add your own Commands/[Command
sets](Command-Sets) here.
- `server`/ - The structure of this folder should not change since Evennia expects it.
- [`conf/`](https://github.com/evennia/evennia/tree/master/evennia/game_template/server) - All
server configuration files sits here. The most important file is `settings.py`.
- `logs/` - Portal log files are stored here (Server is logging to the terminal by default)
- `typeclasses/` - this folder contains empty templates for overloading default game entities of
Evennia. Evennia will automatically use the changes in those templates for the game entities it
creates.
- `web/` - This holds the [Web features](./Web-Features) of your game.
- `world/` - this is a "miscellaneous" folder holding everything related to the world you are
building, such as build scripts and rules modules that don't fit with one of the other folders.
## Evennia library layout:
If you cloned the GIT repo following the instructions, you will have a folder named `evennia`. The
top level of it contains Python package specific stuff such as a readme file, `setup.py` etc. It
also has two subfolders`bin/` and `evennia/` (again).
The `bin/` directory holds OS-specific binaries that will be used when installing Evennia with `pip`
as per the [Getting started](./Getting-Started) instructions. The library itself is in the `evennia`
subfolder. From your code you will access this subfolder simply by `import evennia`.
- evennia
- [`__init__.py`](./Evennia-API) - The "flat API" of Evennia resides here.
- [`commands/`](./Commands) - The command parser and handler.
- `default/` - The [default commands](./Default-Command-Help) and cmdsets.
- [`comms/`](./Communications) - Systems for communicating in-game.
- `contrib/` - Optional plugins too game-specific for core Evennia.
- `game_template/` - Copied to become the "game directory" when using `evennia --init`.
- [`help/`](./Help-System) - Handles the storage and creation of help entries.
- `locale/` - Language files ([i18n](./Internationalization)).
- [`locks/`](./Locks) - Lock system for restricting access to in-game entities.
- [`objects/`](./Objects) - In-game entities (all types of items and Characters).
- [`prototypes/`](./Spawner-and-Prototypes) - Object Prototype/spawning system and OLC menu
- [`accounts/`](./Accounts) - Out-of-game Session-controlled entities (accounts, bots etc)
- [`scripts/`](./Scripts) - Out-of-game entities equivalence to Objects, also with timer support.
- [`server/`](./Portal-And-Server) - Core server code and Session handling.
- `portal/` - Portal proxy and connection protocols.
- [`settings_default.py`](./Server-Conf#Settings-file) - Root settings of Evennia. Copy settings
from here to `mygame/server/settings.py` file.
- [`typeclasses/`](./Typeclasses) - Abstract classes for the typeclass storage and database system.
- [`utils/`](./Coding-Utils) - Various miscellaneous useful coding resources.
- [`web/`](./Web-Features) - Web resources and webserver. Partly copied into game directory on
initialization.
All directories contain files ending in `.py`. These are Python *modules* and are the basic units of
Python code. The roots of directories also have (usually empty) files named `__init__.py`. These are
required by Python so as to be able to find and import modules in other directories. When you have
run Evennia at least once you will find that there will also be `.pyc` files appearing, these are
pre-compiled binary versions of the `.py` files to speed up execution.
The root of the `evennia` folder has an `__init__.py` file containing the "[flat API](./Evennia-API)".
This holds shortcuts to various subfolders in the evennia library. It is provided to make it easier
to find things; it allows you to just import `evennia` and access things from that rather than
having to import from their actual locations inside the source tree.

View file

@ -0,0 +1,117 @@
# Docs refactoring
This is a whitepage for free discussion about the wiki docs and refactorings needed.
Note that this is not a forum. To keep things clean, each opinion text should ideally present a
clear argument or lay out a suggestion. Asking for clarification and any side-discussions should be
held in chat or forum.
### Griatch (Aug 13, 2019)
This is how to make a discussion entry for the whitepage. Use any markdown formatting you need. Also
remember to copy your work to the clipboard before saving the page since if someone else edited the
page since you started, you'll have to reload and write again.
#### (Sept 23, 2019)
[This (now closed) issue by DamnedScholar](https://github.com/evennia/evennia/issues/1431) gives the
following suggestion:
> I think it would be useful for the pages that explain how to use various features of Evennia to
have explicit and easily visible links to the respective API entry or entries. Some pages do, but
not all. I imagine this as a single entry at the top of the page [...].
[This (now closed) issue by taladan](https://github.com/evennia/evennia/issues/1578) gives the
following suggestion:
> It would help me (and probably a couple of others) if there is a way to show the file path where a
particular thing exists. Maybe up under the 'last edited' line we could have a line like:
evennia/locks/lockhandler.py
This would help in development to quickly refer to where a resource is located.
### Kovitikus (Sept. 11, 2019)
[Batch Code](./Batch-Code-Processor) should have a link in the developer area. It is currently only
listed in the tutorials section as an afterthought to a tutorial title.
***
In regards to the general structure of each wiki page: I'd like to see a table of contents at the
top of each one, so that it can be quickly navigated and is immediately apparent what sections are
covered on the page. Similar to the current [Getting Started](./Getting-Started) page.
***
The structuring of the page should also include a quick reference cheatsheet for certain aspects.
Such as [Tags](./Tags) including a quick reference section at the top that lists an example of every
available method you can use in a clear and consistent format, along with a comment. Readers
shouldn't have to decipher the article to gather such basic information and it should instead be
available at first glance.
Example of a quick reference:
**Tags**
```
# Add a tag.
obj.tags.add("label")
# Remove a tag.
obj.tags.remove("label")
# Remove all tags.
obj.tags.clear()
# Search for a tag. Evennia must be imported first.
store_result = evennia.search_tag("label")
# Return a list of all tags.
obj.tags.all()
```
**Aliases**
```
# Add an alias.
obj.aliases.add("label")
ETC...
```
***
In regards to comment structure, I often find that smushing together lines with comments to be too
obscure. White space should be used to clearly delineate what information the comment is for. I
understand that the current format is that a comment references whatever is below it, but newbies
may not know that until they realize it.
Example of poor formatting:
```
#comment
command/code
#comment
command/code
```
Example of good formatting:
```
# Comment.
command/code
# Comment.
command/code
```
### Sage (3/28/20)
If I want to find information on the correct syntax for is_typeclass(), here's what I do:
* Pop over to the wiki. Okay, this is a developer functionality. Let's try that.
* Ctrl+F on Developer page. No results.
* Ctrl+F on API page. No results. Ctrl+F on Flat API page. No results
* Ctrl+F on utils page. No results.
* Ctrl+F on utils.utils page. No results.
* Ctrl+F in my IDE. Results.
* Fortunately, there's only one result for def is_typeclass. If this was at_look, there would be
several results, and I'd have to go through each of those individually, and most of them would just
call return_appearance
An important part of a refactor, in my opinion, is separating out the "Tutorials" from the
"Reference" documentation.

View file

@ -0,0 +1,495 @@
# Dynamic In Game Map
## Introduction
An often desired feature in a MUD is to show an in-game map to help navigation. The [Static in-game
map](Static-In-Game-Map) tutorial solves this by creating a *static* map, meaning the map is pre-
drawn once and for all - the rooms are then created to match that map. When walking around, parts of
the static map is then cut out and displayed next to the room description.
In this tutorial we'll instead do it the other way around; We will dynamically draw the map based on
the relationships we find between already existing rooms.
## The Grid of Rooms
There are at least two requirements needed for this tutorial to work.
1. The structure of your mud has to follow a logical layout. Evennia supports the layout of your
world to be 'logically' impossible with rooms looping to themselves or exits leading to the other
side of the map. Exits can also be named anything, from "jumping out the window" to "into the fifth
dimension". This tutorial assumes you can only move in the cardinal directions (N, E, S and W).
2. Rooms must be connected and linked together for the map to be generated correctly. Vanilla
Evennia comes with a admin command [@tunnel](./Default-Command-Help#tunnel-cmdtunnel) that allows a
user to create rooms in the cardinal directions, but additional work is needed to assure that rooms
are connected. For example, if you `@tunnel east` and then immediately do `@tunnel west` you'll find
that you have created two completely stand-alone rooms. So care is needed if you want to create a
"logical" layout. In this tutorial we assume you have such a grid of rooms that we can generate the
map from.
## Concept
Before getting into the code, it is beneficial to understand and conceptualize how this is going to
work. The idea is analogous to a worm that starts at your current position. It chooses a direction
and 'walks' outward from it, mapping its route as it goes. Once it has traveled a pre-set distance
it stops and starts over in another direction. An important note is that we want a system which is
easily callable and not too complicated. Therefore we will wrap this entire code into a custom
Python class (not a typeclass as this doesn't use any core objects from evennia itself).
We are going to create something that displays like this when you type 'look':
```
Hallway
[.] [.]
[@][.][.][.][.]
[.] [.] [.]
The distant echoes of the forgotten
wail throughout the empty halls.
Exits: North, East, South
```
Your current location is defined by `[@]` while the `[.]`s are other rooms that the "worm" has seen
since departing from your location.
## Setting up the Map Display
First we must define the components for displaying the map. For the "worm" to know what symbol to
draw on the map we will have it check an Attribute on the room it visits called `sector_type`. For
this tutorial we understand two symbols - a normal room and the room with us in it. We also define a
fallback symbol for rooms without said Attribute - that way the map will still work even if we
didn't prepare the room correctly. Assuming your game folder is named `mygame`, we create this code
in `mygame/world/map.py.`
```python
# in mygame/world/map.py
# the symbol is identified with a key "sector_type" on the
# Room. Keys None and "you" must always exist.
SYMBOLS = { None : ' . ', # for rooms without sector_type Attribute
'you' : '[@]',
'SECT_INSIDE': '[.]' }
```
Since trying to access an unset Attribute returns `None`, this means rooms without the `sector_type`
Atttribute will show as ` . `. Next we start building the custom class `Map`. It will hold all
methods we need.
```python
# in mygame/world/map.py
class Map(object):
def __init__(self, caller, max_width=9, max_length=9):
self.caller = caller
self.max_width = max_width
self.max_length = max_length
self.worm_has_mapped = {}
self.curX = None
self.curY = None
```
- `self.caller` is normally your Character object, the one using the map.
- `self.max_width/length` determine the max width and length of the map that will be generated. Note
that it's important that these variables are set to *odd* numbers to make sure the display area has
a center point.
- ` self.worm_has_mapped` is building off the worm analogy above. This dictionary will store all
rooms the "worm" has mapped as well as its relative position within the grid. This is the most
important variable as it acts as a 'checker' and 'address book' that is able to tell us where the
worm has been and what it has mapped so far.
- `self.curX/Y` are coordinates representing the worm's current location on the grid.
Before any sort of mapping can actually be done we need to create an empty display area and do some
sanity checks on it by using the following methods.
```python
# in mygame/world/map.py
class Map(object):
# [... continued]
def create_grid(self):
# This method simply creates an empty grid/display area
# with the specified variables from __init__(self):
board = []
for row in range(self.max_width):
board.append([])
for column in range(self.max_length):
board[row].append(' ')
return board
def check_grid(self):
# this method simply checks the grid to make sure
# that both max_l and max_w are odd numbers.
return True if self.max_length % 2 != 0 or self.max_width % 2 != 0\
else False
```
Before we can set our worm on its way, we need to know some of the computer science behind all this
called 'Graph Traversing'. In Pseudo code what we are trying to accomplish is this:
```python
# pseudo code
def draw_room_on_map(room, max_distance):
self.draw(room)
if max_distance == 0:
return
for exit in room.exits:
if self.has_drawn(exit.destination):
# skip drawing if we already visited the destination
continue
else:
# first time here!
self.draw_room_on_map(exit.destination, max_distance - 1)
```
The beauty of Python is that our actual code of doing this doesn't differ much if at all from this
Pseudo code example.
- `max_distance` is a variable indicating to our Worm how many rooms AWAY from your current location
will it map. Obviously the larger the number the more time it will take if your current location has
many many rooms around you.
The first hurdle here is what value to use for 'max_distance'. There is no reason for the worm to
travel further than what is actually displayed to you. For example, if your current location is
placed in the center of a display area of size `max_length = max_width = 9`, then the worm need only
go `4` spaces in either direction:
```
[.][.][.][.][@][.][.][.][.]
4 3 2 1 0 1 2 3 4
```
The max_distance can be set dynamically based on the size of the display area. As your width/length
changes it becomes a simple algebraic linear relationship which is simply `max_distance =
(min(max_width, max_length) -1) / 2`.
## Building the Mapper
Now we can start to fill our Map object with some methods. We are still missing a few methods that
are very important:
* `self.draw(self, room)` - responsible for actually drawing room to grid.
* `self.has_drawn(self, room)` - checks to see if the room has been mapped and worm has already been
here.
* `self.median(self, number)` - a simple utility method that finds the median (middle point) from 0,
n
* `self.update_pos(self, room, exit_name)` - updates the worm's physical position by reassigning
self.curX/Y. .accordingly
* `self.start_loc_on_grid(self)` - the very first initial draw on the grid representing your
location in the middle of the grid
* 'self.show_map` - after everything is done convert the map into a readable string`
* `self.draw_room_on_map(self, room, max_distance)` - the main method that ties it all together.`
Now that we know which methods we need, let's refine our initial `__init__(self)` to pass some
conditional statements and set it up to start building the display.
```python
#mygame/world/map.py
class Map(object):
def __init__(self, caller, max_width=9, max_length=9):
self.caller = caller
self.max_width = max_width
self.max_length = max_length
self.worm_has_mapped = {}
self.curX = None
self.curY = None
if self.check_grid():
# we have to store the grid into a variable
self.grid = self.create_grid()
# we use the algebraic relationship
self.draw_room_on_map(caller.location,
((min(max_width, max_length) -1 ) / 2)
```
Here we check to see if the parameters for the grid are okay, then we create an empty canvas and map
our initial location as the first room!
As mentioned above, the code for the `self.draw_room_on_map()` is not much different than the Pseudo
code. The method is shown below:
```python
# in mygame/world/map.py, in the Map class
def draw_room_on_map(self, room, max_distance):
self.draw(room)
if max_distance == 0:
return
for exit in room.exits:
if exit.name not in ("north", "east", "west", "south"):
# we only map in the cardinal directions. Mapping up/down would be
# an interesting learning project for someone who wanted to try it.
continue
if self.has_drawn(exit.destination):
# we've been to the destination already, skip ahead.
continue
self.update_pos(room, exit.name.lower())
self.draw_room_on_map(exit.destination, max_distance - 1)
```
The first thing the "worm" does is to draw your current location in `self.draw`. Lets define that...
```python
#in mygame/word/map.py, in the Map class
def draw(self, room):
# draw initial ch location on map first!
if room == self.caller.location:
self.start_loc_on_grid()
self.worm_has_mapped[room] = [self.curX, self.curY]
else:
# map all other rooms
self.worm_has_mapped[room] = [self.curX, self.curY]
# this will use the sector_type Attribute or None if not set.
self.grid[self.curX][self.curY] = SYMBOLS[room.db.sector_type]
```
In `self.start_loc_on_grid()`:
```python
def median(self, num):
lst = sorted(range(0, num))
n = len(lst)
m = n -1
return (lst[n//2] + lst[m//2]) / 2.0
def start_loc_on_grid(self):
x = self.median(self.max_width)
y = self.median(self.max_length)
# x and y are floats by default, can't index lists with float types
x, y = int(x), int(y)
self.grid[x][y] = SYMBOLS['you']
self.curX, self.curY = x, y # updating worms current location
```
After the system has drawn the current map it checks to see if the `max_distance` is `0` (since this
is the inital start phase it is not). Now we handle the iteration once we have each individual exit
in the room. The first thing it does is check if the room the Worm is in has been mapped already..
lets define that...
```python
def has_drawn(self, room):
return True if room in self.worm_has_mapped.keys() else False
```
If `has_drawn` returns `False` that means the worm has found a room that hasn't been mapped yet. It
will then 'move' there. The self.curX/Y sort of lags behind, so we have to make sure to track the
position of the worm; we do this in `self.update_pos()` below.
```python
def update_pos(self, room, exit_name):
# this ensures the coordinates stays up to date
# to where the worm is currently at.
self.curX, self.curY = \
self.worm_has_mapped[room][0], self.worm_has_mapped[room][1]
# now we have to actually move the pointer
# variables depending on which 'exit' it found
if exit_name == 'east':
self.curY += 1
elif exit_name == 'west':
self.curY -= 1
elif exit_name == 'north':
self.curX -= 1
elif exit_name == 'south':
self.curX += 1
```
Once the system updates the position of the worm it feeds the new room back into the original
`draw_room_on_map()` and starts the process all over again..
That is essentially the entire thing. The final method is to bring it all together and make a nice
presentational string out of it using the `self.show_map()` method.
```python
def show_map(self):
map_string = ""
for row in self.grid:
map_string += " ".join(row)
map_string += "\n"
return map_string
```
## Using the Map
In order for the map to get triggered we store it on the Room typeclass. If we put it in
`return_appearance` we will get the map back every time we look at the room.
> `return_appearance` is a default Evennia hook available on all objects; it is called e.g. by the
`look` command to get the description of something (the room in this case).
```python
# in mygame/typeclasses/rooms.py
from evennia import DefaultRoom
from world.map import Map
class Room(DefaultRoom):
def return_appearance(self, looker):
# [...]
string = "%s\n" % Map(looker).show_map()
# Add all the normal stuff like room description,
# contents, exits etc.
string += "\n" + super().return_appearance(looker)
return string
```
Obviously this method of generating maps doesn't take into account of any doors or exits that are
hidden.. etc.. but hopefully it serves as a good base to start with. Like previously mentioned, it
is very important to have a solid foundation on rooms before implementing this. You can try this on
vanilla evennia by using @tunnel and essentially you can just create a long straight/edgy non-
looping rooms that will show on your in-game map.
The above example will display the map above the room description. You could also use an
[EvTable](github:evennia.utils.evtable) to place description and map next to each other. Some other
things you can do is to have a [Command](./Commands) that displays with a larger radius, maybe with a
legend and other features.
Below is the whole `map.py` for your reference. You need to update your `Room` typeclass (see above)
to actually call it. Remember that to see different symbols for a location you also need to set the
`sector_type` Attribute on the room to one of the keys in the `SYMBOLS` dictionary. So in this
example, to make a room be mapped as `[.]` you would set the room's `sector_type` to
`"SECT_INSIDE"`. Try it out with `@set here/sector_type = "SECT_INSIDE"`. If you wanted all new
rooms to have a given sector symbol, you could change the default in the `SYMBOLS´ dictionary below,
or you could add the Attribute in the Room's `at_object_creation` method.
```python
#mygame/world/map.py
# These are keys set with the Attribute sector_type on the room.
# The keys None and "you" must always exist.
SYMBOLS = { None : ' . ', # for rooms without a sector_type attr
'you' : '[@]',
'SECT_INSIDE': '[.]' }
class Map(object):
def __init__(self, caller, max_width=9, max_length=9):
self.caller = caller
self.max_width = max_width
self.max_length = max_length
self.worm_has_mapped = {}
self.curX = None
self.curY = None
if self.check_grid():
# we actually have to store the grid into a variable
self.grid = self.create_grid()
self.draw_room_on_map(caller.location,
((min(max_width, max_length) -1 ) / 2))
def update_pos(self, room, exit_name):
# this ensures the pointer variables always
# stays up to date to where the worm is currently at.
self.curX, self.curY = \
self.worm_has_mapped[room][0], self.worm_has_mapped[room][1]
# now we have to actually move the pointer
# variables depending on which 'exit' it found
if exit_name == 'east':
self.curY += 1
elif exit_name == 'west':
self.curY -= 1
elif exit_name == 'north':
self.curX -= 1
elif exit_name == 'south':
self.curX += 1
def draw_room_on_map(self, room, max_distance):
self.draw(room)
if max_distance == 0:
return
for exit in room.exits:
if exit.name not in ("north", "east", "west", "south"):
# we only map in the cardinal directions. Mapping up/down would be
# an interesting learning project for someone who wanted to try it.
continue
if self.has_drawn(exit.destination):
# we've been to the destination already, skip ahead.
continue
self.update_pos(room, exit.name.lower())
self.draw_room_on_map(exit.destination, max_distance - 1)
def draw(self, room):
# draw initial caller location on map first!
if room == self.caller.location:
self.start_loc_on_grid()
self.worm_has_mapped[room] = [self.curX, self.curY]
else:
# map all other rooms
self.worm_has_mapped[room] = [self.curX, self.curY]
# this will use the sector_type Attribute or None if not set.
self.grid[self.curX][self.curY] = SYMBOLS[room.db.sector_type]
def median(self, num):
lst = sorted(range(0, num))
n = len(lst)
m = n -1
return (lst[n//2] + lst[m//2]) / 2.0
def start_loc_on_grid(self):
x = self.median(self.max_width)
y = self.median(self.max_length)
# x and y are floats by default, can't index lists with float types
x, y = int(x), int(y)
self.grid[x][y] = SYMBOLS['you']
self.curX, self.curY = x, y # updating worms current location
def has_drawn(self, room):
return True if room in self.worm_has_mapped.keys() else False
def create_grid(self):
# This method simply creates an empty grid
# with the specified variables from __init__(self):
board = []
for row in range(self.max_width):
board.append([])
for column in range(self.max_length):
board[row].append(' ')
return board
def check_grid(self):
# this method simply checks the grid to make sure
# both max_l and max_w are odd numbers
return True if self.max_length % 2 != 0 or \
self.max_width % 2 != 0 else False
def show_map(self):
map_string = ""
for row in self.grid:
map_string += " ".join(row)
map_string += "\n"
return map_string
```
## Final Comments
The Dynamic map could be expanded with further capabilities. For example, it could mark exits or
allow NE, SE etc directions as well. It could have colors for different terrain types. One could
also look into up/down directions and figure out how to display that in a good way.

181
docs/source/EvEditor.md Normal file
View file

@ -0,0 +1,181 @@
# EvEditor
Evennia offers a powerful in-game line editor in `evennia.utils.eveditor.EvEditor`. This editor,
mimicking the well-known VI line editor. It offers line-by-line editing, undo/redo, line deletes,
search/replace, fill, dedent and more.
### Launching the editor
The editor is created as follows:
```python
from evennia.utils.eveditor import EvEditor
EvEditor(caller,
loadfunc=None, savefunc=None, quitfunc=None,
key="")
```
- `caller` (Object or Account): The user of the editor.
- `loadfunc` (callable, optional): This is a function called when the editor is first started. It
is called with `caller` as its only argument. The return value from this function is used as the
starting text in the editor buffer.
- `savefunc` (callable, optional): This is called when the user saves their buffer in the editor is
called with two arguments, `caller` and `buffer`, where `buffer` is the current buffer.
- `quitfunc` (callable, optional): This is called when the user quits the editor. If given, all
cleanup and exit messages to the user must be handled by this function.
- `key` (str, optional): This text will be displayed as an identifier and reminder while editing.
It has no other mechanical function.
- `persistent` (default `False`): if set to `True`, the editor will survive a reboot.
### Example of usage
This is an example command for setting a specific Attribute using the editor.
```python
from evennia import Command
from evennia.utils import eveditor
class CmdSetTestAttr(Command):
"""
Set the "test" Attribute using
the line editor.
Usage:
settestattr
"""
key = "settestattr"
def func(self):
"Set up the callbacks and launch the editor"
def load(caller):
"get the current value"
return caller.attributes.get("test")
def save(caller, buffer):
"save the buffer"
caller.attributes.set("test", buffer)
def quit(caller):
"Since we define it, we must handle messages"
caller.msg("Editor exited")
key = "%s/test" % self.caller
# launch the editor
eveditor.EvEditor(self.caller,
loadfunc=load, savefunc=save, quitfunc=quit,
key=key)
```
### Persistent editor
If you set the `persistent` keyword to `True` when creating the editor, it will remain open even
when reloading the game. In order to be persistent, an editor needs to have its callback functions
(`loadfunc`, `savefunc` and `quitfunc`) as top-level functions defined in the module. Since these
functions will be stored, Python will need to find them.
```python
from evennia import Command
from evennia.utils import eveditor
def load(caller):
"get the current value"
return caller.attributes.get("test")
def save(caller, buffer):
"save the buffer"
caller.attributes.set("test", buffer)
def quit(caller):
"Since we define it, we must handle messages"
caller.msg("Editor exited")
class CmdSetTestAttr(Command):
"""
Set the "test" Attribute using
the line editor.
Usage:
settestattr
"""
key = "settestattr"
def func(self):
"Set up the callbacks and launch the editor"
key = "%s/test" % self.caller
# launch the editor
eveditor.EvEditor(self.caller,
loadfunc=load, savefunc=save, quitfunc=quit,
key=key, persistent=True)
```
### Line editor usage
The editor mimics the `VIM` editor as best as possible. The below is an excerpt of the return from
the in-editor help command (`:h`).
```
<txt> - any non-command is appended to the end of the buffer.
: <l> - view buffer or only line <l>
:: <l> - view buffer without line numbers or other parsing
::: - print a ':' as the only character on the line...
:h - this help.
:w - save the buffer (don't quit)
:wq - save buffer and quit
:q - quit (will be asked to save if buffer was changed)
:q! - quit without saving, no questions asked
:u - (undo) step backwards in undo history
:uu - (redo) step forward in undo history
:UU - reset all changes back to initial state
:dd <l> - delete line <n>
:dw <l> <w> - delete word or regex <w> in entire buffer or on line <l>
:DD - clear buffer
:y <l> - yank (copy) line <l> to the copy buffer
:x <l> - cut line <l> and store it in the copy buffer
:p <l> - put (paste) previously copied line directly after <l>
:i <l> <txt> - insert new text <txt> at line <l>. Old line will move down
:r <l> <txt> - replace line <l> with text <txt>
:I <l> <txt> - insert text at the beginning of line <l>
:A <l> <txt> - append text after the end of line <l>
:s <l> <w> <txt> - search/replace word or regex <w> in buffer or on line <l>
:f <l> - flood-fill entire buffer or line <l>
:fi <l> - indent entire buffer or line <l>
:fd <l> - de-indent entire buffer or line <l>
:echo - turn echoing of the input on/off (helpful for some clients)
Legend:
<l> - line numbers, or range lstart:lend, e.g. '3:7'.
<w> - one word or several enclosed in quotes.
<txt> - longer string, usually not needed to be enclosed in quotes.
```
### The EvEditor to edit code
The `EvEditor` is also used to edit some Python code in Evennia. The `@py` command supports an
`/edit` switch that will open the EvEditor in code mode. This mode isn't significantly different
from the standard one, except it handles automatic indentation of blocks and a few options to
control this behavior.
- `:<` to remove a level of indentation for the future lines.
- `:+` to add a level of indentation for the future lines.
- `:=` to disable automatic indentation altogether.
Automatic indentation is there to make code editing more simple. Python needs correct indentation,
not as an aesthetic addition, but as a requirement to determine beginning and ending of blocks. The
EvEditor will try to guess the next level of indentation. If you type a block "if", for instance,
the EvEditor will propose you an additional level of indentation at the next line. This feature
cannot be perfect, however, and sometimes, you will have to use the above options to handle
indentation.
`:=` can be used to turn automatic indentation off completely. This can be very useful when trying
to paste several lines of code that are already correctly indented, for instance.
To see the EvEditor in code mode, you can use the `@py/edit` command. Type in your code (on one or
several lines). You can then use the `:w` option (save without quitting) and the code you have
typed will be executed. The `:!` will do the same thing. Executing code while not closing the
editor can be useful if you want to test the code you have typed but add new lines after your test.

1312
docs/source/EvMenu.md Normal file

File diff suppressed because it is too large Load diff

38
docs/source/EvMore.md Normal file
View file

@ -0,0 +1,38 @@
# EvMore
When sending a very long text to a user client, it might scroll beyond of the height of the client
window. The `evennia.utils.evmore.EvMore` class gives the user the in-game ability to only view one
page of text at a time. It is usually used via its access function, `evmore.msg`.
The name comes from the famous unix pager utility *more* which performs just this function.
### Using EvMore
To use the pager, just pass the long text through it:
```python
from evennia.utils import evmore
evmore.msg(receiver, long_text)
```
Where receiver is an [Object](./Objects) or a [Account](./Accounts). If the text is longer than the
client's screen height (as determined by the NAWS handshake or by `settings.CLIENT_DEFAULT_HEIGHT`)
the pager will show up, something like this:
>[...]
aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.
>(**more** [1/6] retur**n**|**b**ack|**t**op|**e**nd|**a**bort)
where the user will be able to hit the return key to move to the next page, or use the suggested
commands to jump to previous pages, to the top or bottom of the document as well as abort the
paging.
The pager takes several more keyword arguments for controlling the message output. See the
[evmore-API](github:evennia.utils.evmore) for more info.

View file

@ -0,0 +1,97 @@
# API Summary
[evennia](api:evennia) - library root
- [evennia.accounts](api:evennia.accounts) - the out-of-character entities representing players
- [evennia.commands](api:evennia.commands) - handle all inputs. Also includes default commands
- [evennia.comms](api:evennia.comms) - in-game channels and messaging
- [evennia.contrib](api:evennia.contrib) - game-specific tools and code contributed by the community
- [evennia.help](api:evennia.help) - in-game help system
- [evennia.locks](api:evennia.locks) - limiting access to various systems and resources
- [evennia.objects](api:evennia.objects) - all in-game entities, like Rooms, Characters, Exits etc
- [evennia.prototypes](api:evennia.prototypes) - customize entities using dicts
- [evennia.scripts](api:evennia.scripts) - all out-of-character game objects
- [evennia.server](api:evennia.server) - core Server and Portal programs, also network protocols
- [evennia.typeclasses](api:evennia.typeclasses) - core database-python bridge
- [evennia.utils](api:evennia.utils) - lots of useful coding tools and utilities
- [evennia.web](api:evennia.web) - webclient, website and other web resources
## Shortcuts
Evennia's 'flat API' has shortcuts to common tools, available by only importing `evennia`.
The flat API is defined in `__init__.py` [viewable here](github:evennia/__init__.py)
### Main config
- [evennia.settings_default](github:evennia/settings_default.py) - all settings (modify/override in `mygame/server/settings.py`)
### Search functions
- [evennia.search_account](api:evennia.utils.search#evennia.utils.search.search_account)
- [evennia.search_object](api:evennia.utils.search#evennia.utils.search.search_object)
- [evennia.search_object_by_tag](api:evennia.utils.search#evennia.utils.search_object_by_tag)
- [evennia.search_script](api:evennia.utils.search#evennia.utils.search_script)
- [evennia.search_channel](api:evennia.utils.search#evennia.utils.search_channel)
- [evennia.search_message](api:evennia.utils.search#evennia.utils.search_message)
- [evennia.search_help](api:evennia.utils.search#evennia.utils.search.search_help)
### Create functions
- [evennia.create_account](api:evennia.utils.create#evennia.utils.create.create_account)
- [evennia.create_object](api:evennia.utils.create#evennia.utils.create.create_object)
- [evennia.create_script](api:evennia.utils.create#evennia.utils.create.create_script)
- [evennia.create_channel](api:evennia.utils.create#evennia.utils.create.create_channel)
- [evennia.create_help_entry](api:evennia.utils.create#evennia.utils.create.create_help_entry)
- [evennia.create_message](api:evennia.utils.create#evennia.utils.create.create_message)
### Typeclasses
- [evennia.Defaultaccount](api:evennia.accounts.accounts#evennia.accounts.accounts.DefaultAccount) - player account class ([docs](./Accounts))
- [evennia.DefaultGuest](api:evennia.accounts.accounts#evennia.accounts.accounts.DefaultGuest) - base guest account class
- [evennia.DefaultObject](api:evennia.objects.objects#evennia.objects.objects.DefaultObject) - base class for all objects ([docs](./Objects))
- [evennia.DefaultCharacter](api:evennia.objects.objects#evennia.objects.objects.DefaultCharacter) - base class for in-game characters ([docs](./Objects#Character))
- [evennia.DefaultRoom](api:evennia.objects.objects#evennia.objects.objects.DefaultRoom) - base class for rooms ([docs](./Objects#Room))
- [evennia.DefaultExit](api:evennia.objects.objects#evennia.objects.objects.DefaultExit) - base class for exits ([docs](./Objects#Exit))
- [evennia.DefaultScript](api:evennia.scripts.scripts#evennia.scripts.scripts.DefaultScript) - base class for OOC-objects ([docs](./Scripts))
- [evennia.DefaultChannel](api:evennia.comms.comms#evennia.comms.comms.DefaultChannel) - base class for in-game channels ([docs](./Communications))
### Commands
- [evennia.Command](api:evennia.commands.command#evennia.commands.command.Command) - base [Command](./Commands) class. See also `evennia.default_cmds.MuxCommand`
- [evennia.CmdSet](api:evennia.commands.cmdset#evennia.commands.cmdset.CmdSet) - base [Cmdset](./Command-Sets) class
- evennia.default_cmds - access to all [default command classes](api:evennia.commands.default) as properties
- evennia.syscmdkeys - access to all [system command](./Commands#system-commands) names as properties
### Utilities
- [evennia.utils.utils](api:evennia.utils.utils) - mixed useful utilities
- [evennia.gametime](api:evennia.utils.gametime) - server run- and game time ([docs](./Coding-Utils#gametime))
- [evennia.logger](api:evennia.utils.logger) - logging tools
- [evennia.ansi](api:evennia.utils.ansi) - ansi coloring tools
- [evennia.spawn](api:evennia.prototypes.spawner#evennia.prototypes.spawner.Spawn) - spawn/prototype system ([docs](./Spawner-and-Prototypes))
- [evennia.lockfuncs](api:evennia.locks.lockfuncs) - default lock functions for access control ([docs](./Locks))
- [evennia.EvMenu](api:evennia.utils.evmenu#evennia.utils.evmenu.EvMenu) - menu system ([docs](./EvMenu))
- [evennia.EvTable](api:evennia.utils.evtable#evennia.utils.evtable.EvTable) - text table creater
- [evennia.EvForm](api:evennia.utils.evform#evennia.utils.evform.EvForm) - text form creator
- [evennia.EvEditor](api:evennia.utils.eveditor#evennia.utils.eveditor.EvEditor) - in game text line editor ([docs](./EvEditor))
### Global singleton handlers
- [evennia.TICKER_HANDLER](api:evennia.scripts.tickerhandler) - allow objects subscribe to tickers ([docs](./TickerHandler))
- [evennia.MONITOR_HANDLER](api:evennia.scripts.monitorhandler) - monitor changes ([docs](./MonitorHandler))
- [evennia.CHANNEL_HANDLER](api:evennia.comms.channelhandler) - maintains channels
- [evennia.SESSION_HANDLER](api:evennia.server.sessionhandler) - manages all sessions
### Database core models (for more advanced lookups)
- [evennia.ObjectDB](api:evennia.objects.models#evennia.objects.models.ObjectDB)
- [evennia.accountDB](api:evennia.accounts.models#evennia.accounts.models.AccountDB)
- [evennia.ScriptDB](api:evennia.scripts.models#evennia.scripts.models.ScriptDB)
- [evennia.ChannelDB](api:evennia.comms.models#evennia.comms.models.ChannelDB)
- [evennia.Msg](api:evennia.comms.models#evennia.comms.models.Msg)
- evennia.managers - contains shortcuts to all database managers
### Contributions
- [evennia.contrib](api:evennia.contrib) - game-specific contributions and plugins ([README](github:evennia/contrib/README.md))

View file

@ -0,0 +1,71 @@
# Evennia Game Index
The [Evennia game index](http://games.evennia.com) is a list of games built or
being built with Evennia. Anyone is allowed to add their game to the index
- also if you have just started development and don't yet accept external
players. It's a chance for us to know you are out there and for you to make us
intrigued about or excited for your upcoming game!
All we ask is that you check so your game-name does not collide with one
already in the list - be nice!
## Connect with the wizard
From your game dir, run
evennia connections
This will start the Evennia _Connection wizard_. From the menu, select to add
your game to the Evennia Game Index. Follow the prompts and don't forget to
save your new settings in the end. Use `quit` at any time if you change your
mind.
> The wizard will create a new file `mygame/server/conf/connection_settings.py`
> with the settings you chose. This is imported from the end of your main
> settings file and will thus override it. You can edit this new file if you
> want, but remember that if you run the wizard again, your changes may get
> over-written.
## Manual Settings
If you don't want to use the wizard (maybe because you already have the client installed from an
earlier version), you can also configure your index entry in your settings file
(`mygame/server/conf/settings.py`). Add the following:
```python
GAME_INDEX_ENABLED = True
GAME_INDEX_LISTING = {
# required
'game_status': 'pre-alpha', # pre-alpha, alpha, beta, launched
'listing_contact': "dummy@dummy.com", # not publicly shown.
'short_description': 'Short blurb',
# optional
'long_description':
"Longer description that can use Markdown like *bold*, _italic_"
"and [linkname](http://link.com). Use \n for line breaks."
'telnet_hostname': 'dummy.com',
'telnet_port': '1234',
'web_client_url': 'dummy.com/webclient',
'game_website': 'dummy.com',
# 'game_name': 'MyGame', # set only if different than settings.SERVERNAME
}
```
Of these, the `game_status`, `short_description` and `listing_contact` are
required. The `listing_contact` is not publicly visible and is only meant as a
last resort if we need to get in touch with you over any listing issue/bug (so
far this has never happened).
If `game_name` is not set, the `settings.SERVERNAME` will be used. Use empty strings
(`''`) for optional fields you don't want to specify at this time.
## Non-public games
If you don't specify neither `telnet_hostname + port` nor
`web_client_url`, the Game index will list your game as _Not yet public_.
Non-public games are moved to the bottom of the index since there is no way
for people to try them out. But it's a good way to show you are out there, even
if you are not ready for players yet.

View file

@ -0,0 +1,178 @@
# Evennia Introduction
> *A MUD (originally Multi-User Dungeon, with later variants Multi-User Dimension and Multi-User
Domain) is a multiplayer real-time virtual world described primarily in text. MUDs combine elements
of role-playing games, hack and slash, player versus player, interactive fiction and online chat.
Players can read or view descriptions of rooms, objects, other players, non-player characters, and
actions performed in the virtual world. Players typically interact with each other and the world by
typing commands that resemble a natural language.* - [Wikipedia](http://en.wikipedia.org/wiki/MUD)
If you are reading this, it's quite likely you are dreaming of creating and running a text-based
massively-multiplayer game ([MUD/MUX/MUSH](http://tinyurl.com/c5sc4bm) etc) of your very own. You
might just be starting to think about it, or you might have lugged around that *perfect* game in
your mind for years ... you know *just* how good it would be, if you could only make it come to
reality. We know how you feel. That is, after all, why Evennia came to be.
Evennia is in principle a MUD-building system: a bare-bones Python codebase and server intended to
be highly extendable for any style of game. "Bare-bones" in this context means that we try to impose
as few game-specific things on you as possible. So whereas we for convenience offer basic building
blocks like objects, characters, rooms, default commands for building and administration etc, we
don't prescribe any combat rules, mob AI, races, skills, character classes or other things that will
be different from game to game anyway. It is possible that we will offer some such systems as
contributions in the future, but these will in that case all be optional.
What we *do* however, is to provide a solid foundation for all the boring database, networking, and
behind-the-scenes administration stuff that all online games need whether they like it or not.
Evennia is *fully persistent*, that means things you drop on the ground somewhere will still be
there a dozen server reboots later. Through Django we support a large variety of different database
systems (a database is created for you automatically if you use the defaults).
Using the full power of Python throughout the server offers some distinct advantages. All your
coding, from object definitions and custom commands to AI scripts and economic systems is done in
normal Python modules rather than some ad-hoc scripting language. The fact that you script the game
in the same high-level language that you code it in allows for very powerful and custom game
implementations indeed.
The server ships with a default set of player commands that are similar to the MUX command set. We
*do not* aim specifically to be a MUX server, but we had to pick some default to go with (see
[this](./Soft-Code) for more about our original motivations). It's easy to remove or add commands, or
to have the command syntax mimic other systems, like Diku, LP, MOO and so on. Or why not create a
new and better command system of your own design.
## Can I test it somewhere?
Evennia's demo server can be found at [demo.evennia.com](http://demo.evennia.com). If you prefer to
connect to the demo via your own telnet client you can do so at `silvren.com`, port `4280`. Here is
a [screenshot](./Screenshot).
Once you installed Evennia yourself it comes with its own tutorial - this shows off some of the
possibilities _and_ gives you a small single-player quest to play. The tutorial takes only one
single in-game command to install as explained [here](./Tutorial-World-Introduction).
## Brief summary of features
### Technical
- Game development is done by the server importing your normal Python modules. Specific server
features are implemented by overloading hooks that the engine calls appropriately.
- All game entities are simply Python classes that handle database negotiations behind the scenes
without you needing to worry.
- Command sets are stored on individual objects (including characters) to offer unique functionality
and object-specific commands. Sets can be updated and modified on the fly to expand/limit player
input options during play.
- Scripts are used to offer asynchronous/timed execution abilities. Scripts can also be persistent.
There are easy mechanisms to thread particularly long-running processes and built-in ways to start
"tickers" for games that wants them.
- In-game communication channels are modular and can be modified to any functionality, including
mailing systems and full logging of all messages.
- Server can be fully rebooted/reloaded without users disconnecting.
- An Account can freely connect/disconnect from game-objects, offering an easy way to implement
multi-character systems and puppeting.
- Each Account can optionally control multiple Characters/Objects at the same time using the same
login information.
- Spawning of individual objects via a prototypes-like system.
- Tagging can be used to implement zones and object groupings.
- All source code is extensively documented.
- Unit-testing suite, including tests of default commands and plugins.
### Default content
- Basic classes for Objects, Characters, Rooms and Exits
- Basic login system, using the Account's login name as their in-game Character's name for
simplicity
- "MUX-like" command set with administration, building, puppeting, channels and social commands
- In-game Tutorial
- Contributions folder with working, but optional, code such as alternative login, menus, character
generation and more
### Standards/Protocols supported
- TCP/websocket HTML5 browser web client, with ajax/comet fallback for older browsers
- Telnet and Telnet + SSL with mud-specific extensions ([MCCP](http://tintin.sourceforge.net/mccp/),
[MSSP](http://tintin.sourceforge.net/mssp/), [TTYPE](http://tintin.sourceforge.net/mtts/),
[MSDP](http://tintin.sourceforge.net/msdp/),
[GMCP](https://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html),
[MXP](https://www.zuggsoft.com/zmud/mxp.htm) links)
- ANSI and xterm256 colours
- SSH
- HTTP - Website served by in-built webserver and connected to same database as game.
- IRC - external IRC channels can be connected to in-game chat channels
- RSS feeds can be echoed to in-game channels (things like Twitter can easily be added)
- Several different databases supported (SQLite3, MySQL, PostgreSQL, ...)
For more extensive feature information, see the [Developer Central](./Developer-Central).
## What you need to know to work with Evennia
Assuming you have Evennia working (see the [quick start instructions](./Getting-Started)) and have
gotten as far as to start the server and connect to it with the client of your choice, here's what
you need to know depending on your skills and needs.
### I don't know (or don't want to do) any programming - I just want to run a game!
Evennia comes with a default set of commands for the Python newbies and for those who need to get a
game running *now*. Stock Evennia is enough for running a simple 'Talker'-type game - you can build
and describe rooms and basic objects, have chat channels, do emotes and other things suitable for a
social or free-form MU\*. Combat, mobs and other game elements are not included, so you'll have a
very basic game indeed if you are not willing to do at least *some* coding.
### I know basic Python, or I am willing to learn
Evennia's source code is extensively documented and is [viewable online](https://github.com/evennia/evennia).
We also have a comprehensive [online manual](https://github.com/evennia/evennia/wiki) with lots of examples.
But while Python is
considered a very easy programming language to get into, you do have a learning curve to climb if
you are new to programming. You should probably sit down
with a Python beginner's [tutorial](http://docs.python.org/tutorial/) (there are plenty of them on
the web if you look around) so you at least know what you are seeing. See also our
[link page](./Links#wiki-litterature) for some reading suggestions. To efficiently code your dream game in
Evennia you don't need to be a Python guru, but you do need to be able to read example code
containing at least these basic Python features:
- Importing and using python [modules](http://docs.python.org/3.7/tutorial/modules.html)
- Using [variables](http://www.tutorialspoint.com/python/python_variable_types.htm),
[conditional statements](http://docs.python.org/tutorial/controlflow.html#if-statements),
[loops](http://docs.python.org/tutorial/controlflow.html#for-statements) and
[functions](http://docs.python.org/tutorial/controlflow.html#defining-functions)
- Using [lists, dictionaries and list comprehensions](http://docs.python.org/tutorial/datastructures.html)
- Doing [string handling and formatting](http://docs.python.org/tutorial/introduction.html#strings)
- Have a basic understanding of [object-oriented programming](http://www.tutorialspoint.com/python/python_classes_objects.htm), using
[Classes](http://docs.python.org/tutorial/classes.html), their methods and properties
Obviously, the more things you feel comfortable with, the easier time you'll have to find your way.
With just basic knowledge you should be able to define your own [Commands](./Commands), create custom
[Objects](./Objects) as well as make your world come alive with basic [Scripts](./Scripts). You can
definitely build a whole advanced and customized game from extending Evennia's examples only.
### I know my Python stuff and I am willing to use it!
Even if you started out as a Python beginner, you will likely get to this point after working on
your game for a while. With more general knowledge in Python the full power of Evennia opens up for
you. Apart from modifying commands, objects and scripts, you can develop everything from advanced
mob AI and economic systems, through sophisticated combat and social mini games, to redefining how
commands, players, rooms or channels themselves work. Since you code your game by importing normal
Python modules, there are few limits to what you can accomplish.
If you *also* happen to know some web programming (HTML, CSS, Javascript) there is also a web
presence (a website and a mud web client) to play around with ...
### Where to from here?
From here you can continue browsing the [online documentation](./index) to
find more info about Evennia. Or you can jump into the [Tutorials](./Tutorials) and get your hands
dirty with code right away. You can also read the developer's [dev blog](https://evennia.blogspot.com/) for many tidbits and snippets about Evennia's development and
structure.
Some more hints:
1. Get engaged in the community. Make an introductory post to our [mailing list/forum](https://groups.google.com/forum/#!forum/evennia) and get to know people. It's also
highly recommended you hop onto our [Developer chat](http://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb)
on IRC. This allows you to chat directly with other developers new and old as well as with the devs
of Evennia itself. This chat is logged (you can find links on http://www.evennia.com) and can also
be searched from the same place for discussion topics you are interested in.
2. Read the [Game Planning](./Game-Planning) wiki page. It gives some ideas for your work flow and the
state of mind you should aim for - including cutting down the scope of your game for its first
release.
3. Do the [Tutorial for basic MUSH-like game](./Tutorial-for-basic-MUSH-like-game) carefully from
beginning to end and try to understand what does what. Even if you are not interested in a MUSH for
your own game, you will end up with a small (very small) game that you can build or learn from.

View file

@ -0,0 +1,200 @@
# Evennia for Diku Users
Evennia represents a learning curve for those who used to code on
[Diku](https://en.wikipedia.org/wiki/DikuMUD) type MUDs. While coding in Python is easy if you
already know C, the main effort is to get rid of old C programming habits. Trying to code Python the
way you code C will not only look ugly, it will lead to less optimal and harder to maintain code.
Reading Evennia example code is a good way to get a feel for how different problems are approached
in Python.
Overall, Python offers an extensive library of resources, safe memory management and excellent
handling of errors. While Python code does not run as fast as raw C code does, the difference is not
all that important for a text-based game. The main advantage of Python is an extremely fast
development cycle with and easy ways to create game systems that would take many times more code and
be much harder to make stable and maintainable in C.
### Core Differences
- As mentioned, the main difference between Evennia and a Diku-derived codebase is that Evennia is
written purely in Python. Since Python is an interpreted language there is no compile stage. It is
modified and extended by the server loading Python modules at run-time. It also runs on all computer
platforms Python runs on (which is basically everywhere).
- Vanilla Diku type engines save their data in custom *flat file* type storage solutions. By
contrast, Evennia stores all game data in one of several supported SQL databases. Whereas flat files
have the advantage of being easier to implement, they (normally) lack many expected safety features
and ways to effectively extract subsets of the stored data. For example, if the server loses power
while writing to a flatfile it may become corrupt and the data lost. A proper database solution is
not susceptible to this - at no point is the data in a state where it cannot be recovered. Databases
are also highly optimized for querying large data sets efficiently.
### Some Familiar Things
Diku expresses the character object referenced normally by:
`struct char ch*` then all character-related fields can be accessed by `ch->`. In Evennia, one must
pay attention to what object you are using, and when you are accessing another through back-
handling, that you are accessing the right object. In Diku C, accessing character object is normally
done by:
```c
/* creating pointer of both character and room struct */
void(struct char ch*, struct room room*){
int dam;
if (ROOM_FLAGGED(room, ROOM_LAVA)){
dam = 100
ch->damage_taken = dam
};
};
```
As an example for creating Commands in Evennia via the `from evennia import Command` the character
object that calls the command is denoted by a class property as `self.caller`. In this example
`self.caller` is essentially the 'object' that has called the Command, but most of the time it is an
Account object. For a more familiar Diku feel, create a variable that becomes the account object as:
```python
#mygame/commands/command.py
from evennia import Command
class CmdMyCmd(Command):
"""
This is a Command Evennia Object
"""
[...]
def func(self):
ch = self.caller
# then you can access the account object directly by using the familiar ch.
ch.msg("...")
account_name = ch.name
race = ch.db.race
```
As mentioned above, care must be taken what specific object you are working with. If focused on a
room object and you need to access the account object:
```python
#mygame/typeclasses/room.py
from evennia import DefaultRoom
class MyRoom(DefaultRoom):
[...]
def is_account_object(self, object):
# a test to see if object is an account
[...]
def myMethod(self):
#self.caller would not make any sense, since self refers to the
# object of 'DefaultRoom', you must find the character obj first:
for ch in self.contents:
if self.is_account_object(ch):
# now you can access the account object with ch:
account_name = ch.name
race = ch.db.race
```
## Emulating Evennia to Look and Feel Like A Diku/ROM
To emulate a Diku Mud on Evennia some work has to be done before hand. If there is anything that all
coders and builders remember from Diku/Rom days is the presence of VNUMs. Essentially all data was
saved in flat files and indexed by VNUMs for easy access. Evennia has the ability to emulate VNUMS
to the extent of categorising rooms/mobs/objs/trigger/zones[...] into vnum ranges.
Evennia has objects that are called Scripts. As defined, they are the 'out of game' instances that
exist within the mud, but never directly interacted with. Scripts can be used for timers, mob AI,
and even a stand alone databases.
Because of their wonderful structure all mob, room, zone, triggers, etc.. data can be saved in
independently created global scripts.
Here is a sample mob file from a Diku Derived flat file.
```text
#0
mob0~
mob0~
mob0
~
Mob0
~
10 0 0 0 0 0 0 0 0 E
1 20 9 0d0+10 1d2+0
10 100
8 8 0
E
#1
Puff dragon fractal~
Puff~
Puff the Fractal Dragon is here, contemplating a higher reality.
~
Is that some type of differential curve involving some strange, and unknown
calculus that she seems to be made out of?
~
516106 0 0 0 2128 0 0 0 1000 E
34 9 -10 6d6+340 5d5+5
340 115600
8 8 2
BareHandAttack: 12
E
T 95
```
Each line represents something that the MUD reads in and does something with it. This isn't easy to
read, but let's see if we can emulate this as a dictionary to be stored on a database script created
in Evennia.
First, let's create a global script that does absolutely nothing and isn't attached to anything. You
can either create this directly in-game with the @py command or create it in another file to do some
checks and balances if for whatever reason the script needs to be created again. Progmatically it
can be done like so:
```python
from evennia import create_script
mob_db = create_script("typeclasses.scripts.DefaultScript", key="mobdb",
persistent=True, obj=None)
mob_db.db.vnums = {}
```
Just by creating a simple script object and assigning it a 'vnums' attribute as a type dictionary.
Next we have to create the mob layout..
```python
# vnum : mob_data
mob_vnum_1 = {
'key' : 'puff',
'sdesc' : 'puff the fractal dragon',
'ldesc' : 'Puff the Fractal Dragon is here, ' \
'contemplating a higher reality.',
'ddesc' : ' Is that some type of differential curve ' \
'involving some strange, and unknown calculus ' \
'that she seems to be made out of?',
[...]
}
# Then saving it to the data, assuming you have the script obj stored in a variable.
mob_db.db.vnums[1] = mob_vnum_1
```
This is a very 'caveman' example, but it gets the idea across. You can use the keys in the
`mob_db.vnums` to act as the mob vnum while the rest contains the data..
Much simpler to read and edit. If you plan on taking this route, you must keep in mind that by
default evennia 'looks' at different properties when using the `look` command for instance. If you
create an instance of this mob and make its `self.key = 1`, by default evennia will say
`Here is : 1`
You must restructure all default commands so that the mud looks at different properties defined on
your mob.

View file

@ -0,0 +1,222 @@
# Evennia for MUSH Users
*This page is adopted from an article originally posted for the MUSH community [here on
musoapbox.net](http://musoapbox.net/topic/1150/evennia-for-mushers).*
[MUSH](https://en.wikipedia.org/wiki/MUSH)es are text multiplayer games traditionally used for
heavily roleplay-focused game styles. They are often (but not always) utilizing game masters and
human oversight over code automation. MUSHes are traditionally built on the TinyMUSH-family of game
servers, like PennMUSH, TinyMUSH, TinyMUX and RhostMUSH. Also their siblings
[MUCK](https://en.wikipedia.org/wiki/TinyMUCK) and [MOO](https://en.wikipedia.org/wiki/MOO) are
often mentioned together with MUSH since they all inherit from the same
[TinyMUD](https://en.wikipedia.org/wiki/MUD_trees#TinyMUD_family_tree) base. A major feature is the
ability to modify and program the game world from inside the game by using a custom scripting
language. We will refer to this online scripting as *softcode* here.
Evennia works quite differently from a MUSH both in its overall design and under the hood. The same
things are achievable, just in a different way. Here are some fundamental differences to keep in
mind if you are coming from the MUSH world.
## Developers vs Players
In MUSH, users tend to code and expand all aspects of the game from inside it using softcode. A MUSH
can thus be said to be managed solely by *Players* with different levels of access. Evennia on the
other hand, differentiates between the role of the *Player* and the *Developer*.
- An Evennia *Developer* works in Python from *outside* the game, in what MUSH would consider
“hardcode”. Developers implement larger-scale code changes and can fundamentally change how the game
works. They then load their changes into the running Evennia server. Such changes will usually not
drop any connected players.
- An Evennia *Player* operates from *inside* the game. Some staff-level players are likely to double
as developers. Depending on access level, players can modify and expand the game's world by digging
new rooms, creating new objects, alias commands, customize their experience and so on. Trusted staff
may get access to Python via the `@py` command, but this would be a security risk for normal Players
to use. So the *Player* usually operates by making use of the tools prepared for them by the
*Developer* - tools that can be as rigid or flexible as the developer desires.
## Collaborating on a game - Python vs Softcode
For a *Player*, collaborating on a game need not be too different between MUSH and Evennia. The
building and description of the game world can still happen mostly in-game using build commands,
using text tags and [inline functions](./TextTags#inline-functions) to prettify and customize the
experience. Evennia offers external ways to build a world but those are optional. There is also
nothing *in principle* stopping a Developer from offering a softcode-like language to Players if
that is deemed necessary.
For *Developers* of the game, the difference is larger: Code is mainly written outside the game in
Python modules rather than in-game on the command line. Python is a very popular and well-supported
language with tons of documentation and help to be found. The Python standard library is also a
great help for not having to reinvent the wheel. But that said, while Python is considered one of
the easier languages to learn and use it is undoubtedly very different from MUSH softcode.
While softcode allows collaboration in-game, Evennia's external coding instead opens up the
possibility for collaboration using professional version control tools and bug tracking using
websites like github (or bitbucket for a free private repo). Source code can be written in proper
text editors and IDEs with refactoring, syntax highlighting and all other conveniences. In short,
collaborative development of an Evennia game is done in the same way most professional collaborative
development is done in the world, meaning all the best tools can be used.
## `@parent` vs `@typeclass` and `@spawn`
Inheritance works differently in Python than in softcode. Evennia has no concept of a "master
object" that other objects inherit from. There is in fact no reason at all to introduce "virtual
objects" in the game world - code and data are kept separate from one another.
In Python (which is an [object oriented](https://en.wikipedia.org/wiki/Object-oriented_programming)
language) one instead creates *classes* - these are like blueprints from which you spawn any number
of *object instances*. Evennia also adds the extra feature that every instance is persistent in the
database (this means no SQL is ever needed). To take one example, a unique character in Evennia is
an instances of the class `Character`.
One parallel to MUSH's `@parent` command may be Evennia's `@typeclass` command, which changes which
class an already existing object is an instance of. This way you can literally turn a `Character`
into a `Flowerpot` on the spot.
if you are new to object oriented design it's important to note that all object instances of a class
does *not* have to be identical. If they did, all Characters would be named the same. Evennia allows
to customize individual objects in many different ways. One way is through *Attributes*, which are
database-bound properties that can be linked to any object. For example, you could have an `Orc`
class that defines all the stuff an Orc should be able to do (probably in turn inheriting from some
`Monster` class shared by all monsters). Setting different Attributes on different instances
(different strength, equipment, looks etc) would make each Orc unique despite all sharing the same
class.
The `@spawn` command allows one to conveniently choose between different "sets" of Attributes to
put on each new Orc (like the "warrior" set or "shaman" set) . Such sets can even inherit one
another which is again somewhat remniscent at least of the *effect* of `@parent` and the object-
based inheritance of MUSH.
There are other differences for sure, but that should give some feel for things. Enough with the
theory. Let's get down to more practical matters next. To install, see the [Getting Started
instructions](Getting-Started).
## A first step making things more familiar
We will here give two examples of customizing Evennia to be more familiar to a MUSH *Player*.
### Activating a multi-descer
By default Evennias `desc` command updates your description and thats it. There is a more feature-
rich optional “multi-descer” in `evennia/contrib/multidesc.py` though. This alternative allows for
managing and combining a multitude of keyed descriptions.
To activate the multi-descer, `cd` to your game folder and into the `commands` sub-folder. There
youll find the file `default_cmdsets.py`. In Python lingo all `*.py` files are called *modules*.
Open the module in a text editor. We wont go into Evennia in-game *Commands* and *Command sets*
further here, but suffice to say Evennia allows you to change which commands (or versions of
commands) are available to the player from moment to moment depending on circumstance.
Add two new lines to the module as seen below:
```python
# the file mygame/commands/default_cmdsets.py
# [...]
from evennia.contrib import multidescer # <- added now
class CharacterCmdSet(default_cmds.CharacterCmdSet):
"""
The CharacterCmdSet contains general in-game commands like look,
get etc available on in-game Character objects. It is merged with
the AccountCmdSet when an Account puppets a Character.
"""
key = "DefaultCharacter"
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones.
#
self.add(multidescer.CmdMultiDesc()) # <- added now
# [...]
```
Note that Python cares about indentation, so make sure to indent with the same number of spaces as
shown above!
So what happens above? We [import the module](http://www.linuxtopia.org/online_books/programming_boo
ks/python_programming/python_ch28s03.html) `evennia/contrib/multidescer.py` at the top. Once
imported we can access stuff inside that module using full stop (`.`). The multidescer is defined as
a class `CmdMultiDesc` (we could find this out by opening said module in a text editor). At the
bottom we create a new instance of this class and add it to the `CharacterCmdSet` class. For the
sake of this tutorial we only need to know that `CharacterCmdSet` contains all commands that should
be be available to the `Character` by default.
This whole thing will be triggered when the command set is first created, which happens on server
start. So we need to reload Evennia with `@reload` - no one will be disconnected by doing this. If
all went well you should now be able to use `desc` (or `+desc`) and find that you have more
possibilities:
```text
> help +desc # get help on the command
> +desc eyes = His eyes are blue.
> +desc basic = A big guy.
> +desc/set basic + + eyes # we add an extra space between
> look me
A big guy. His eyes are blue.
```
If there are errors, a *traceback* will show in the server log - several lines of text showing
where the error occurred. Find where the error is by locating the line number related to the
`default_cmdsets.py` file (it's the only one you've changed so far). Most likely you mis-spelled
something or missed the indentation. Fix it and either `@reload` again or run `evennia start` as
needed.
### Customizing the multidescer syntax
As seen above the multidescer uses syntax like this (where `|/` are Evennia's tags for line breaks)
:
```text
> +desc/set basic + |/|/ + cape + footwear + |/|/ + attitude
```
This use of `+ ` was prescribed by the *Developer* that coded this `+desc` command. What if the
*Player* doesnt like this syntax though? Do players need to pester the dev to change it? Not
necessarily. While Evennia does not allow the player to build their own multi-descer on the command
line, it does allow for *re-mapping* the command syntax to one they prefer. This is done using the
`nick` command.
Heres a nick that changes how to input the command above:
```text
> nick setdesc $1 $2 $3 $4 = +desc/set $1 + |/|/ + $2 + $3 + |/|/ + $4
```
The string on the left will be matched against your input and if matching, it will be replaced with
the string on the right. The `$`-type tags will store space-separated arguments and put them into
the replacement. The nick allows [shell-like wildcards](http://www.linfo.org/wildcard.html), so you
can use `*`, `?`, `[...]`, `[!...]` etc to match parts of the input.
The same description as before can now be set as
```text
> setdesc basic cape footwear attitude
```
With the `nick` functionality players can mitigate a lot of syntax dislikes even without the
developer changing the underlying Python code.
## Next steps
If you are a *Developer* and are interested in making a more MUSH-like Evennia game, a good start is
to look into the Evennia [Tutorial for a first MUSH-like game](./Tutorial-for-basic-MUSH-like-game).
That steps through building a simple little game from scratch and helps to acquaint you with the
various corners of Evennia. There is also the [Tutorial for running roleplaying sessions](Evennia-
for-roleplaying-sessions) that can be of interest.
An important aspect of making things more familiar for *Players* is adding new and tweaking existing
commands. How this is done is covered by the [Tutorial on adding new commands](Adding-Command-
Tutorial). You may also find it useful to shop through the `evennia/contrib/` folder. The [Tutorial
world](Tutorial-World-Introduction) is a small single-player quest you can try (its not very MUSH-
like but it does show many Evennia concepts in action). Beyond that there are [many more
tutorials](Tutorials) to try out. If you feel you want a more visual overview you can also look at
[Evennia in pictures](https://evennia.blogspot.se/2016/05/evennia-in-pictures.html).
… And of course, if you need further help you can always drop into the [Evennia chatroom](http://web
chat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb) or post a
question in our [forum/mailing list](https://groups.google.com/forum/#%21forum/evennia)!

View file

@ -0,0 +1,732 @@
# Evennia for roleplaying sessions
This tutorial will explain how to set up a realtime or play-by-post tabletop style game using a
fresh Evennia server.
The scenario is thus: You and a bunch of friends want to play a tabletop role playing game online.
One of you will be the game master and you are all okay with playing using written text. You want
both the ability to role play in real-time (when people happen to be online at the same time) as
well as the ability for people to post when they can and catch up on what happened since they were
last online.
This is the functionality we will be needing and using:
* The ability to make one of you the *GM* (game master), with special abilities.
* A *Character sheet* that players can create, view and fill in. It can also be locked so only the
GM can modify it.
* A *dice roller* mechanism, for whatever type of dice the RPG rules require.
* *Rooms*, to give a sense of location and to compartmentalize play going on- This means both
Character movements from location to location and GM explicitly moving them around.
* *Channels*, for easily sending text to all subscribing accounts, regardless of location.
* Account-to-Account *messaging* capability, including sending to multiple recipients
simultaneously, regardless of location.
We will find most of these things are already part of vanilla Evennia, but that we can expand on the
defaults for our particular use-case. Below we will flesh out these components from start to finish.
## Starting out
We will assume you start from scratch. You need Evennia installed, as per the [Getting
Started](Getting-Started) instructions. Initialize a new game directory with `evennia init
<gamedirname>`. In this tutorial we assume your game dir is simply named `mygame`. You can use the
default database and keep all other settings to default for now. Familiarize yourself with the
`mygame` folder before continuing. You might want to browse the [First Steps Coding](First-Steps-
Coding) tutorial, just to see roughly where things are modified.
## The Game Master role
In brief:
* Simplest way: Being an admin, just give one account `Admins` permission using the standard `@perm`
command.
* Better but more work: Make a custom command to set/unset the above, while tweaking the Character
to show your renewed GM status to the other accounts.
### The permission hierarchy
Evennia has the following [permission hierarchy](./Building-Permissions#assigning-permissions) out of
the box: *Players, Helpers, Builders, Admins* and finally *Developers*. We could change these but
then we'd need to update our Default commands to use the changes. We want to keep this simple, so
instead we map our different roles on top of this permission ladder.
1. `Players` is the permission set on normal players. This is the default for anyone creating a new
account on the server.
2. `Helpers` are like `Players` except they also have the ability to create/edit new help entries.
This could be granted to players who are willing to help with writing lore or custom logs for
everyone.
3. `Builders` is not used in our case since the GM should be the only world-builder.
4. `Admins` is the permission level the GM should have. Admins can do everything builders can
(create/describe rooms etc) but also kick accounts, rename them and things like that.
5. `Developers`-level permission are the server administrators, the ones with the ability to
restart/shutdown the server as well as changing the permission levels.
> The [superuser](./Building-Permissions#the-super-user) is not part of the hierarchy and actually
completely bypasses it. We'll assume server admin(s) will "just" be Developers.
### How to grant permissions
Only `Developers` can (by default) change permission level. Only they have access to the `@perm`
command:
```
> @perm Yvonne
Permissions on Yvonne: accounts
> @perm Yvonne = Admins
> @perm Yvonne
Permissions on Yvonne: accounts, admins
> @perm/del Yvonne = Admins
> @perm Yvonne
Permissions on Yvonne: accounts
```
There is no need to remove the basic `Players` permission when adding the higher permission: the
highest will be used. Permission level names are *not* case sensitive. You can also use both plural
and singular, so "Admins" gives the same powers as "Admin".
### Optional: Making a GM-granting command
Use of `@perm` works out of the box, but it's really the bare minimum. Would it not be nice if other
accounts could tell at a glance who the GM is? Also, we shouldn't really need to remember that the
permission level is called "Admins". It would be easier if we could just do `@gm <account>` and
`@notgm <account>` and at the same time change something make the new GM status apparent.
So let's make this possible. This is what we'll do:
1. We'll customize the default Character class. If an object of this class has a particular flag,
its name will have the string`(GM)` added to the end.
2. We'll add a new command, for the server admin to assign the GM-flag properly.
#### Character modification
Let's first start by customizing the Character. We recommend you browse the beginning of the
[Account](./Accounts) page to make sure you know how Evennia differentiates between the OOC "Account
objects" (not to be confused with the `Accounts` permission, which is just a string specifying your
access) and the IC "Character objects".
Open `mygame/typeclasses/characters.py` and modify the default `Character` class:
```python
# in mygame/typeclasses/characters.py
# [...]
class Character(DefaultCharacter):
# [...]
def get_display_name(self, looker, **kwargs):
"""
This method customizes how character names are displayed. We assume
only permissions of types "Developers" and "Admins" require
special attention.
"""
name = self.key
selfaccount = self.account # will be None if we are not puppeted
lookaccount = looker.account # - " -
if selfaccount and selfaccount.db.is_gm:
# A GM. Show name as name(GM)
name = "%s(GM)" % name
if lookaccount and \
(lookaccount.permissions.get("Developers") or lookaccount.db.is_gm):
# Developers/GMs see name(#dbref) or name(GM)(#dbref)
return "%s(#%s)" % (name, self.id)
else:
return name
```
Above, we change how the Character's name is displayed: If the account controlling this Character is
a GM, we attach the string `(GM)` to the Character's name so everyone can tell who's the boss. If we
ourselves are Developers or GM's we will see database ids attached to Characters names, which can
help if doing database searches against Characters of exactly the same name. We base the "gm-
ingness" on having an flag (an [Attribute](./Attributes)) named `is_gm`. We'll make sure new GM's
actually get this flag below.
> **Extra exercise:** This will only show the `(GM)` text on *Characters* puppeted by a GM account,
that is, it will show only to those in the same location. If we wanted it to also pop up in, say,
`who` listings and channels, we'd need to make a similar change to the `Account` typeclass in
`mygame/typeclasses/accounts.py`. We leave this as an exercise to the reader.
#### New @gm/@ungm command
We will describe in some detail how to create and add an Evennia [command](./Commands) here with the
hope that we don't need to be as detailed when adding commands in the future. We will build on
Evennia's default "mux-like" commands here.
Open `mygame/commands/command.py` and add a new Command class at the bottom:
```python
# in mygame/commands/command.py
from evennia import default_cmds
# [...]
import evennia
class CmdMakeGM(default_cmds.MuxCommand):
"""
Change an account's GM status
Usage:
@gm <account>
@ungm <account>
"""
# note using the key without @ means both @gm !gm etc will work
key = "gm"
aliases = "ungm"
locks = "cmd:perm(Developers)"
help_category = "RP"
def func(self):
"Implement the command"
caller = self.caller
if not self.args:
caller.msg("Usage: @gm account or @ungm account")
return
accountlist = evennia.search_account(self.args) # returns a list
if not accountlist:
caller.msg("Could not find account '%s'" % self.args)
return
elif len(accountlist) > 1:
caller.msg("Multiple matches for '%s': %s" % (self.args, accountlist))
return
else:
account = accountlist[0]
if self.cmdstring == "gm":
# turn someone into a GM
if account.permissions.get("Admins"):
caller.msg("Account %s is already a GM." % account)
else:
account.permissions.add("Admins")
caller.msg("Account %s is now a GM." % account)
account.msg("You are now a GM (changed by %s)." % caller)
account.character.db.is_gm = True
else:
# @ungm was entered - revoke GM status from someone
if not account.permissions.get("Admins"):
caller.msg("Account %s is not a GM." % account)
else:
account.permissions.remove("Admins")
caller.msg("Account %s is no longer a GM." % account)
account.msg("You are no longer a GM (changed by %s)." % caller)
del account.character.db.is_gm
```
All the command does is to locate the account target and assign it the `Admins` permission if we
used `@gm` or revoke it if using the `@ungm` alias. We also set/unset the `is_gm` Attribute that is
expected by our new `Character.get_display_name` method from earlier.
> We could have made this into two separate commands or opted for a syntax like `@gm/revoke
<accountname>`. Instead we examine how this command was called (stored in `self.cmdstring`) in order
to act accordingly. Either way works, practicality and coding style decides which to go with.
To actually make this command available (only to Developers, due to the lock on it), we add it to
the default Account command set. Open the file `mygame/commands/default_cmdsets.py` and find the
`AccountCmdSet` class:
```python
# mygame/commands/default_cmdsets.py
# [...]
from commands.command import CmdMakeGM
class AccountCmdSet(default_cmds.AccountCmdSet):
# [...]
def at_cmdset_creation(self):
# [...]
self.add(CmdMakeGM())
```
Finally, issue the `@reload` command to update the server to your changes. Developer-level players
(or the superuser) should now have the `@gm/@ungm` command available.
## Character sheet
In brief:
* Use Evennia's EvTable/EvForm to build a Character sheet
* Tie individual sheets to a given Character.
* Add new commands to modify the Character sheet, both by Accounts and GMs.
* Make the Character sheet lockable by a GM, so the Player can no longer modify it.
### Building a Character sheet
There are many ways to build a Character sheet in text, from manually pasting strings together to
more automated ways. Exactly what is the best/easiest way depends on the sheet one tries to create.
We will here show two examples using the *EvTable* and *EvForm* utilities.Later we will create
Commands to edit and display the output from those utilities.
> Note that due to the limitations of the wiki, no color is used in any of the examples. See [the
text tag documentation](TextTags) for how to add color to the tables and forms.
#### Making a sheet with EvTable
[EvTable](github:evennia.utils.evtable) is a text-table generator. It helps with displaying text in
ordered rows and columns. This is an example of using it in code:
````python
# this can be tried out in a Python shell like iPython
from evennia.utils import evtable
# we hardcode these for now, we'll get them as input later
STR, CON, DEX, INT, WIS, CHA = 12, 13, 8, 10, 9, 13
table = evtable.EvTable("Attr", "Value",
table = [
["STR", "CON", "DEX", "INT", "WIS", "CHA"],
[STR, CON, DEX, INT, WIS, CHA]
], align='r', border="incols")
````
Above, we create a two-column table by supplying the two columns directly. We also tell the table to
be right-aligned and to use the "incols" border type (borders drawns only in between columns). The
`EvTable` class takes a lot of arguments for customizing its look, you can see [some of the possible
keyword arguments here](github:evennia.utils.evtable#evtable__init__). Once you have the `table` you
could also retroactively add new columns and rows to it with `table.add_row()` and
`table.add_column()`: if necessary the table will expand with empty rows/columns to always remain
rectangular.
The result from printing the above table will be
```python
table_string = str(table)
print(table_string)
Attr | Value
~~~~~~+~~~~~~~
STR | 12
CON | 13
DEX | 8
INT | 10
WIS | 9
CHA | 13
```
This is a minimalistic but effective Character sheet. By combining the `table_string` with other
strings one could build up a reasonably full graphical representation of a Character. For more
advanced layouts we'll look into EvForm next.
#### Making a sheet with EvForm
[EvForm](github:evennia.utils.evform) allows the creation of a two-dimensional "graphic" made by
text characters. On this surface, one marks and tags rectangular regions ("cells") to be filled with
content. This content can be either normal strings or `EvTable` instances (see the previous section,
one such instance would be the `table` variable in that example).
In the case of a Character sheet, these cells would be comparable to a line or box where you could
enter the name of your character or their strength score. EvMenu also easily allows to update the
content of those fields in code (it use EvTables so you rebuild the table first before re-sending it
to EvForm).
The drawback of EvForm is that its shape is static; if you try to put more text in a region than it
was sized for, the text will be cropped. Similarly, if you try to put an EvTable instance in a field
too small for it, the EvTable will do its best to try to resize to fit, but will eventually resort
to cropping its data or even give an error if too small to fit any data.
An EvForm is defined in a Python module. Create a new file `mygame/world/charsheetform.py` and
modify it thus:
````python
#coding=utf-8
# in mygame/world/charsheetform.py
FORMCHAR = "x"
TABLECHAR = "c"
FORM = """
.--------------------------------------.
| |
| Name: xxxxxxxxxxxxxx1xxxxxxxxxxxxxxx |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| |
>------------------------------------<
| |
| ccccccccccc Advantages: |
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
| ccccccccccc xxxxxxxxxx3xxxxxxxxxxx |
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
| ccccc2ccccc Disadvantages: |
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
| ccccccccccc xxxxxxxxxx4xxxxxxxxxxx |
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
| |
+--------------------------------------+
"""
````
The `#coding` statement (which must be put on the very first line to work) tells Python to use the
utf-8 encoding for the file. Using the `FORMCHAR` and `TABLECHAR` we define what single-character we
want to use to "mark" the regions of the character sheet holding cells and tables respectively.
Within each block (which must be separated from one another by at least one non-marking character)
we embed identifiers 1-4 to identify each block. The identifier could be any single character except
for the `FORMCHAR` and `TABLECHAR`
> You can still use `FORMCHAR` and `TABLECHAR` elsewhere in your sheet, but not in a way that it
would identify a cell/table. The smallest identifiable cell/table area is 3 characters wide
including the identifier (for example `x2x`).
Now we will map content to this form.
````python
# again, this can be tested in a Python shell
# hard-code this info here, later we'll ask the
# account for this info. We will re-use the 'table'
# variable from the EvTable example.
NAME = "John, the wise old admin with a chip on his shoulder"
ADVANTAGES = "Language-wiz, Intimidation, Firebreathing"
DISADVANTAGES = "Bad body odor, Poor eyesight, Troubled history"
from evennia.utils import evform
# load the form from the module
form = evform.EvForm("world/charsheetform.py")
# map the data to the form
form.map(cells={"1":NAME, "3": ADVANTAGES, "4": DISADVANTAGES},
tables={"2":table})
````
We create some RP-sounding input and re-use the `table` variable from the previous `EvTable`
example.
> Note, that if you didn't want to create the form in a separate module you *could* also load it
directly into the `EvForm` call like this: `EvForm(form={"FORMCHAR":"x", "TABLECHAR":"c", "FORM":
formstring})` where `FORM` specifies the form as a string in the same way as listed in the module
above. Note however that the very first line of the `FORM` string is ignored, so start with a `\n`.
We then map those to the cells of the form:
````python
print(form)
````
````
.--------------------------------------.
| |
| Name: John, the wise old admin with |
| a chip on his shoulder |
| |
>------------------------------------<
| |
| Attr|Value Advantages: |
| ~~~~~+~~~~~ Language-wiz, |
| STR| 12 Intimidation, |
| CON| 13 Firebreathing |
| DEX| 8 Disadvantages: |
| INT| 10 Bad body odor, Poor |
| WIS| 9 eyesight, Troubled |
| CHA| 13 history |
| |
+--------------------------------------+
````
As seen, the texts and tables have been slotted into the text areas and line breaks have been added
where needed. We chose to just enter the Advantages/Disadvantages as plain strings here, meaning
long names ended up split between rows. If we wanted more control over the display we could have
inserted `\n` line breaks after each line or used a borderless `EvTable` to display those as well.
### Tie a Character sheet to a Character
We will assume we go with the `EvForm` example above. We now need to attach this to a Character so
it can be modified. For this we will modify our `Character` class a little more:
```python
# mygame/typeclasses/character.py
from evennia.utils import evform, evtable
[...]
class Character(DefaultCharacter):
[...]
def at_object_creation(self):
"called only once, when object is first created"
# we will use this to stop account from changing sheet
self.db.sheet_locked = False
# we store these so we can build these on demand
self.db.chardata = {"str": 0,
"con": 0,
"dex": 0,
"int": 0,
"wis": 0,
"cha": 0,
"advantages": "",
"disadvantages": ""}
self.db.charsheet = evform.EvForm("world/charsheetform.py")
self.update_charsheet()
def update_charsheet(self):
"""
Call this to update the sheet after any of the ingoing data
has changed.
"""
data = self.db.chardata
table = evtable.EvTable("Attr", "Value",
table = [
["STR", "CON", "DEX", "INT", "WIS", "CHA"],
[data["str"], data["con"], data["dex"],
data["int"], data["wis"], data["cha"]]],
align='r', border="incols")
self.db.charsheet.map(tables={"2": table},
cells={"1":self.key,
"3":data["advantages"],
"4":data["disadvantages"]})
```
Use `@reload` to make this change available to all *newly created* Characters. *Already existing*
Characters will *not* have the charsheet defined, since `at_object_creation` is only called once.
The easiest to force an existing Character to re-fire its `at_object_creation` is to use the
`@typeclass` command in-game:
```
@typeclass/force <Character Name>
```
### Command for Account to change Character sheet
We will add a command to edit the sections of our Character sheet. Open
`mygame/commands/command.py`.
```python
# at the end of mygame/commands/command.py
ALLOWED_ATTRS = ("str", "con", "dex", "int", "wis", "cha")
ALLOWED_FIELDNAMES = ALLOWED_ATTRS + \
("name", "advantages", "disadvantages")
def _validate_fieldname(caller, fieldname):
"Helper function to validate field names."
if fieldname not in ALLOWED_FIELDNAMES:
err = "Allowed field names: %s" % (", ".join(ALLOWED_FIELDNAMES))
caller.msg(err)
return False
if fieldname in ALLOWED_ATTRS and not value.isdigit():
caller.msg("%s must receive a number." % fieldname)
return False
return True
class CmdSheet(MuxCommand):
"""
Edit a field on the character sheet
Usage:
@sheet field value
Examples:
@sheet name Ulrik the Warrior
@sheet dex 12
@sheet advantages Super strength, Night vision
If given without arguments, will view the current character sheet.
Allowed field names are:
name,
str, con, dex, int, wis, cha,
advantages, disadvantages
"""
key = "sheet"
aliases = "editsheet"
locks = "cmd: perm(Players)"
help_category = "RP"
def func(self):
caller = self.caller
if not self.args or len(self.args) < 2:
# not enough arguments. Display the sheet
if sheet:
caller.msg(caller.db.charsheet)
else:
caller.msg("You have no character sheet.")
return
# if caller.db.sheet_locked:
caller.msg("Your character sheet is locked.")
return
# split input by whitespace, once
fieldname, value = self.args.split(None, 1)
fieldname = fieldname.lower() # ignore case
if not _validate_fieldnames(caller, fieldname):
return
if fieldname == "name":
self.key = value
else:
caller.chardata[fieldname] = value
caller.update_charsheet()
caller.msg("%s was set to %s." % (fieldname, value))
```
Most of this command is error-checking to make sure the right type of data was input. Note how the
`sheet_locked` Attribute is checked and will return if not set.
This command you import into `mygame/commands/default_cmdsets.py` and add to the `CharacterCmdSet`,
in the same way the `@gm` command was added to the `AccountCmdSet` earlier.
### Commands for GM to change Character sheet
Game masters use basically the same input as Players do to edit a character sheet, except they can
do it on other players than themselves. They are also not stopped by any `sheet_locked` flags.
```python
# continuing in mygame/commands/command.py
class CmdGMsheet(MuxCommand):
"""
GM-modification of char sheets
Usage:
@gmsheet character [= fieldname value]
Switches:
lock - lock the character sheet so the account
can no longer edit it (GM's still can)
unlock - unlock character sheet for Account
editing.
Examples:
@gmsheet Tom
@gmsheet Anna = str 12
@gmsheet/lock Tom
"""
key = "gmsheet"
locks = "cmd: perm(Admins)"
help_category = "RP"
def func(self):
caller = self.caller
if not self.args:
caller.msg("Usage: @gmsheet character [= fieldname value]")
if self.rhs:
# rhs (right-hand-side) is set only if a '='
# was given.
if len(self.rhs) < 2:
caller.msg("You must specify both a fieldname and value.")
return
fieldname, value = self.rhs.split(None, 1)
fieldname = fieldname.lower()
if not _validate_fieldname(caller, fieldname):
return
charname = self.lhs
else:
# no '=', so we must be aiming to look at a charsheet
fieldname, value = None, None
charname = self.args.strip()
character = caller.search(charname, global_search=True)
if not character:
return
if "lock" in self.switches:
if character.db.sheet_locked:
caller.msg("The character sheet is already locked.")
else:
character.db.sheet_locked = True
caller.msg("%s can no longer edit their character sheet." % character.key)
elif "unlock" in self.switches:
if not character.db.sheet_locked:
caller.msg("The character sheet is already unlocked.")
else:
character.db.sheet_locked = False
caller.msg("%s can now edit their character sheet." % character.key)
if fieldname:
if fieldname == "name":
character.key = value
else:
character.db.chardata[fieldname] = value
character.update_charsheet()
caller.msg("You set %s's %s to %s." % (character.key, fieldname, value)
else:
# just display
caller.msg(character.db.charsheet)
```
The `@gmsheet` command takes an additional argument to specify which Character's character sheet to
edit. It also takes `/lock` and `/unlock` switches to block the Player from tweaking their sheet.
Before this can be used, it should be added to the default `CharacterCmdSet` in the same way as the
normal `@sheet`. Due to the lock set on it, this command will only be available to `Admins` (i.e.
GMs) or higher permission levels.
## Dice roller
Evennia's *contrib* folder already comes with a full dice roller. To add it to the game, simply
import `contrib.dice.CmdDice` into `mygame/commands/default_cmdsets.py` and add `CmdDice` to the
`CharacterCmdset` as done with other commands in this tutorial. After a `@reload` you will be able
to roll dice using normal RPG-style format:
```
roll 2d6 + 3
7
```
Use `help dice` to see what syntax is supported or look at `evennia/contrib/dice.py` to see how it's
implemented.
## Rooms
Evennia comes with rooms out of the box, so no extra work needed. A GM will automatically have all
needed building commands available. A fuller go-through is found in the [Building
tutorial](Building-Quickstart). Here are some useful highlights:
* `@dig roomname;alias = exit_there;alias, exit_back;alias` - this is the basic command for digging
a new room. You can specify any exit-names and just enter the name of that exit to go there.
* `@tunnel direction = roomname` - this is a specialized command that only accepts directions in the
cardinal directions (n,ne,e,se,s,sw,w,nw) as well as in/out and up/down. It also automatically
builds "matching" exits back in the opposite direction.
* `@create/drop objectname` - this creates and drops a new simple object in the current location.
* `@desc obj` - change the look-description of the object.
* `@tel object = location` - teleport an object to a named location.
* `@search objectname` - locate an object in the database.
> TODO: Describe how to add a logging room, that logs says and poses to a log file that people can
access after the fact.
## Channels
Evennia comes with [Channels](./Communications#Channels) in-built and they are described fully in the
documentation. For brevity, here are the relevant commands for normal use:
* `@ccreate new_channel;alias;alias = short description` - Creates a new channel.
* `addcom channel` - join an existing channel. Use `addcom alias = channel` to add a new alias you
can use to talk to the channel, as many as desired.
* `delcom alias or channel` - remove an alias from a channel or, if the real channel name is given,
unsubscribe completely.
* `@channels` lists all available channels, including your subscriptions and any aliases you have
set up for them.
You can read channel history: if you for example are chatting on the `public` channel you can do
`public/history` to see the 20 last posts to that channel or `public/history 32` to view twenty
posts backwards, starting with the 32nd from the end.
## PMs
To send PMs to one another, players can use the `@page` (or `tell`) command:
```
page recipient = message
page recipient, recipient, ... = message
```
Players can use `page` alone to see the latest messages. This also works if they were not online
when the message was sent.

View file

@ -0,0 +1,120 @@
# Execute Python Code
The `@py` command supplied with the default command set of Evennia allows you to execute Python
commands directly from inside the game. An alias to `@py` is simply "`!`". *Access to the `@py`
command should be severely restricted*. This is no joke - being able to execute arbitrary Python
code on the server is not something you should entrust to just anybody.
@py 1+2
<<< 3
## Available variables
A few local variables are made available when running `@py`. These offer entry into the running
system.
- **self** / **me** - the calling object (i.e. you)
- **here** - the current caller's location
- **obj** - a dummy [Object](./Objects) instance
- **evennia** - Evennia's [flat API](./Evennia-API) - through this you can access all of Evennia.
For accessing other objects in the same room you need to use `self.search(name)`. For objects in
other locations, use one of the `evennia.search_*` methods. See [below](./Execute-Python-Code#finding-
objects).
## Returning output
This is an example where we import and test one of Evennia's utilities found in
`src/utils/utils.py`, but also accessible through `ev.utils`:
@py from ev import utils; utils.time_format(33333)
<<< Done.
Note that we didn't get any return value, all we where told is that the code finished executing
without error. This is often the case in more complex pieces of code which has no single obvious
return value. To see the output from the `time_format()` function we need to tell the system to
echo it to us explicitly with `self.msg()`.
@py from ev import utils; self.msg(str(utils.time_format(33333)))
09:15
<<< Done.
> Warning: When using the `msg` function wrap our argument in `str()` to convert it into a string
above. This is not strictly necessary for most types of data (Evennia will usually convert to a
string behind the scenes for you). But for *lists* and *tuples* you will be confused by the output
if you don't wrap them in `str()`: only the first item of the iterable will be returned. This is
because doing `msg(text)` is actually just a convenience shortcut; the full argument that `msg`
accepts is something called an *outputfunc* on the form `(cmdname, (args), {kwargs})` (see [the
message path](Messagepath) for more info). Sending a list/tuple confuses Evennia to think you are
sending such a structure. Converting it to a string however makes it clear it should just be
displayed as-is.
If you were to use Python's standard `print`, you will see the result in your current `stdout` (your
terminal by default, otherwise your log file).
## Finding objects
A common use for `@py` is to explore objects in the database, for debugging and performing specific
operations that are not covered by a particular command.
Locating an object is best done using `self.search()`:
@py self.search("red_ball")
<<< Ball
@py self.search("red_ball").db.color = "red"
<<< Done.
@py self.search("red_ball").db.color
<<< red
`self.search()` is by far the most used case, but you can also search other database tables for
other Evennia entities like scripts or configuration entities. To do this you can use the generic
search entries found in `ev.search_*`.
@py evennia.search_script("sys_game_time")
<<< [<src.utils.gametime.GameTime object at 0x852be2c>]
(Note that since this becomes a simple statement, we don't have to wrap it in `self.msg()` to get
the output). You can also use the database model managers directly (accessible through the `objects`
properties of database models or as `evennia.managers.*`). This is a bit more flexible since it
gives you access to the full range of database search methods defined in each manager.
@py evennia.managers.scripts.script_search("sys_game_time")
<<< [<src.utils.gametime.GameTime object at 0x852be2c>]
The managers are useful for all sorts of database studies.
@py ev.managers.configvalues.all()
<<< [<ConfigValue: default_home]>, <ConfigValue:site_name>, ...]
## Testing code outside the game
`@py` has the advantage of operating inside a running server (sharing the same process), where you
can test things in real time. Much of this *can* be done from the outside too though.
In a terminal, cd to the top of your game directory (this bit is important since we need access to
your config file) and run
evennia shell
Your default Python interpreter will start up, configured to be able to work with and import all
modules of your Evennia installation. From here you can explore the database and test-run individual
modules as desired.
It's recommended that you get a more fully featured Python interpreter like
[iPython](http://ipython.scipy.org/moin/). If you use a virtual environment, you can just get it
with `pip install ipython`. IPython allows you to better work over several lines, and also has a lot
of other editing features, such as tab-completion and `__doc__`-string reading.
$ evennia shell
IPython 0.10 -- An enhanced Interactive Python
...
In [1]: import evennia
In [2]: evennia.managers.objects.all()
Out[3]: [<ObjectDB: Harry>, <ObjectDB: Limbo>, ...]
See the page about the [Evennia-API](./Evennia-API) for more things to explore.

View file

@ -0,0 +1,292 @@
# First Steps Coding
This section gives a brief step-by-step introduction on how to set up Evennia for the first time so
you can modify and overload the defaults easily. You should only need to do these steps once. It
also walks through you making your first few tweaks.
Before continuing, make sure you have Evennia installed and running by following the [Getting
Started](Getting-Started) instructions. You should have initialized a new game folder with the
`evennia --init foldername` command. We will in the following assume this folder is called
"mygame".
It might be a good idea to eye through the brief [Coding Introduction](./Coding-Introduction) too
(especially the recommendations in the section about the evennia "flat" API and about using `evennia
shell` will help you here and in the future).
To follow this tutorial you also need to know the basics of operating your computer's
terminal/command line. You also need to have a text editor to edit and create source text files.
There are plenty of online tutorials on how to use the terminal and plenty of good free text
editors. We will assume these things are already familiar to you henceforth.
## Your First Changes
Below are some first things to try with your new custom modules. You can test these to get a feel
for the system. See also [Tutorials](./Tutorials) for more step-by-step help and special cases.
### Tweak Default Character
We will add some simple rpg attributes to our default Character. In the next section we will follow
up with a new command to view those attributes.
1. Edit `mygame/typeclasses/characters.py` and modify the `Character` class. The
`at_object_creation` method also exists on the `DefaultCharacter` parent and will overload it. The
`get_abilities` method is unique to our version of `Character`.
```python
class Character(DefaultCharacter):
# [...]
def at_object_creation(self):
"""
Called only at initial creation. This is a rather silly
example since ability scores should vary from Character to
Character and is usually set during some character
generation step instead.
"""
#set persistent attributes
self.db.strength = 5
self.db.agility = 4
self.db.magic = 2
def get_abilities(self):
"""
Simple access method to return ability
scores as a tuple (str,agi,mag)
"""
return self.db.strength, self.db.agility, self.db.magic
```
1. [Reload](./Start-Stop-Reload) the server (you will still be connected to the game after doing
this). Note that if you examine *yourself* you will *not* see any new Attributes appear yet. Read
the next section to understand why.
#### Updating Yourself
It's important to note that the new [Attributes](./Attributes) we added above will only be stored on
*newly* created characters. The reason for this is simple: The `at_object_creation` method, where we
added those Attributes, is per definition only called when the object is *first created*, then never
again. This is usually a good thing since those Attributes may change over time - calling that hook
would reset them back to start values. But it also means that your existing character doesn't have
them yet. You can see this by calling the `get_abilities` hook on yourself at this point:
```
# (you have to be superuser to use @py)
@py self.get_abilities()
<<< (None, None, None)
```
This is easily remedied.
```
@update self
```
This will (only) re-run `at_object_creation` on yourself. You should henceforth be able to get the
abilities successfully:
```
@py self.get_abilities()
<<< (5, 4, 2)
```
This is something to keep in mind if you start building your world before your code is stable -
startup-hooks will not (and should not) automatically run on *existing* objects - you have to update
your existing objects manually. Luckily this is a one-time thing and pretty simple to do. If the
typeclass you want to update is in `typeclasses.myclass.MyClass`, you can do the following (e.g.
from `evennia shell`):
```python
from typeclasses.myclass import MyClass
# loop over all MyClass instances in the database
# and call .swap_typeclass on them
for obj in MyClass.objects.all():
obj.swap_typeclass(MyClass, run_start_hooks="at_object_creation")
```
Using `swap_typeclass` to the same typeclass we already have will re-run the creation hooks (this is
what the `@update` command does under the hood). From in-game you can do the same with `@py`:
```
@py typeclasses.myclass import MyClass;[obj.swap_typeclass(MyClass) for obj in
MyClass.objects.all()]
```
See the [Object Typeclass tutorial](./Adding-Object-Typeclass-Tutorial) for more help and the
[Typeclasses](./Typeclasses) and [Attributes](./Attributes) page for detailed documentation about
Typeclasses and Attributes.
#### Troubleshooting: Updating Yourself
One may experience errors for a number of reasons. Common beginner errors are spelling mistakes,
wrong indentations or code omissions leading to a `SyntaxError`. Let's say you leave out a colon
from the end of a class function like so: ```def at_object_creation(self)```. The client will reload
without issue. *However*, if you look at the terminal/console (i.e. not in-game), you will see
Evennia complaining (this is called a *traceback*):
```
Traceback (most recent call last):
File "C:\mygame\typeclasses\characters.py", line 33
def at_object_creation(self)
^
SyntaxError: invalid syntax
```
Evennia will still be restarting and following the tutorial, doing `@py self.get_abilities()` will
return the right response `(None, None, None)`. But when attempting to `@typeclass/force self` you
will get this response:
```python
AttributeError: 'DefaultObject' object has no attribute 'get_abilities'
```
The full error will show in the terminal/console but this is confusing since you did add
`get_abilities` before. Note however what the error says - you (`self`) should be a `Character` but
the error talks about `DefaultObject`. What has happened is that due to your unhandled `SyntaxError`
earlier, Evennia could not load the `character.py` module at all (it's not valid Python). Rather
than crashing, Evennia handles this by temporarily falling back to a safe default - `DefaultObject`
- in order to keep your MUD running. Fix the original `SyntaxError` and reload the server. Evennia
will then be able to use your modified `Character` class again and things should work.
> Note: Learning how to interpret an error traceback is a critical skill for anyone learning Python.
Full tracebacks will appear in the terminal/Console you started Evennia from. The traceback text can
sometimes be quite long, but you are usually just looking for the last few lines: The description of
the error and the filename + line number for where the error occurred. In the example above, we see
it's a `SyntaxError` happening at `line 33` of `mygame\typeclasses\characters.py`. In this case it
even points out *where* on the line it encountered the error (the missing colon). Learn to read
tracebacks and you'll be able to resolve the vast majority of common errors easily.
### Add a New Default Command
The `@py` command used above is only available to privileged users. We want any player to be able to
see their stats. Let's add a new [command](./Commands) to list the abilities we added in the previous
section.
1. Open `mygame/commands/command.py`. You could in principle put your command anywhere but this
module has all the imports already set up along with some useful documentation. Make a new class at
the bottom of this file:
```python
class CmdAbilities(Command):
"""
List abilities
Usage:
abilities
Displays a list of your current ability values.
"""
key = "abilities"
aliases = ["abi"]
lock = "cmd:all()"
help_category = "General"
def func(self):
"implements the actual functionality"
str, agi, mag = self.caller.get_abilities()
string = "STR: %s, AGI: %s, MAG: %s" % (str, agi, mag)
self.caller.msg(string)
```
1. Next you edit `mygame/commands/default_cmdsets.py` and add a new import to it near the top:
```python
from commands.command import CmdAbilities
```
1. In the `CharacterCmdSet` class, add the following near the bottom (it says where):
```python
self.add(CmdAbilities())
```
1. [Reload](./Start-Stop-Reload) the server (noone will be disconnected by doing this).
You (and anyone else) should now be able to use `abilities` (or its alias `abi`) as part of your
normal commands in-game:
```
abilities
STR: 5, AGI: 4, MAG: 2
```
See the [Adding a Command tutorial](./Adding-Command-Tutorial) for more examples and the
[Commands](./Commands) section for detailed documentation about the Command system.
### Make a New Type of Object
Let's test to make a new type of object. This example is an "wise stone" object that returns some
random comment when you look at it, like this:
> look stone
A very wise stone
This is a very wise old stone.
It grumbles and says: 'The world is like a rock of chocolate.'
1. Create a new module in `mygame/typeclasses/`. Name it `wiseobject.py` for this example.
1. In the module import the base `Object` (`typeclasses.objects.Object`). This is empty by default,
meaning it is just a proxy for the default `evennia.DefaultObject`.
1. Make a new class in your module inheriting from `Object`. Overload hooks on it to add new
functionality. Here is an example of how the file could look:
```python
from random import choice
from typeclasses.objects import Object
class WiseObject(Object):
"""
An object speaking when someone looks at it. We
assume it looks like a stone in this example.
"""
def at_object_creation(self):
"Called when object is first created"
self.db.wise_texts = \
["Stones have feelings too.",
"To live like a stone is to not have lived at all.",
"The world is like a rock of chocolate."]
def return_appearance(self, looker):
"""
Called by the look command. We want to return
a wisdom when we get looked at.
"""
# first get the base string from the
# parent's return_appearance.
string = super().return_appearance(looker)
wisewords = "\n\nIt grumbles and says: '%s'"
wisewords = wisewords % choice(self.db.wise_texts)
return string + wisewords
```
1. Check your code for bugs. Tracebacks will appear on your command line or log. If you have a grave
Syntax Error in your code, the source file itself will fail to load which can cause issues with the
entire cmdset. If so, fix your bug and [reload the server from the command line](./Start-Stop-Reload)
(noone will be disconnected by doing this).
1. Use `@create/drop stone:wiseobject.WiseObject` to create a talkative stone. If the `@create`
command spits out a warning or cannot find the typeclass (it will tell you which paths it searched),
re-check your code for bugs and that you gave the correct path. The `@create` command starts looking
for Typeclasses in `mygame/typeclasses/`.
1. Use `look stone` to test. You will see the default description ("You see nothing special")
followed by a random message of stony wisdom. Use `@desc stone = This is a wise old stone.` to make
it look nicer. See the [Builder Docs](./Builder-Docs) for more information.
Note that `at_object_creation` is only called once, when the stone is first created. If you make
changes to this method later, already existing stones will not see those changes. As with the
`Character` example above you can use `@typeclass/force` to tell the stone to re-run its
initialization.
The `at_object_creation` is a special case though. Changing most other aspects of the typeclass does
*not* require manual updating like this - you just need to `@reload` to have all changes applied
automatically to all existing objects.
## Where to Go From Here?
There are more [Tutorials](./Tutorials), including one for building a [whole little MUSH-like
game](Tutorial-for-basic-MUSH-like-game) - that is instructive also if you have no interest in
MUSHes per se. A good idea is to also get onto the [IRC
chat](http://webchat.freenode.net/?channels=evennia) and the [mailing
list](https://groups.google.com/forum/#!forum/evennia) to get in touch with the community and other
developers.

View file

@ -0,0 +1,218 @@
# Game Planning
So you have Evennia up and running. You have a great game idea in mind. Now it's time to start
cracking! But where to start? Here are some ideas for a workflow. Note that the suggestions on this
page are just that - suggestions. Also, they are primarily aimed at a lone hobby designer or a small
team developing a game in their free time. There is an article in the Imaginary Realities e-zine
which was written by the Evennia lead dev. It focuses more on you finding out your motivations for
making a game - you can [read the article here](http://journal.imaginary-
realities.com/volume-07/issue-03/where-do-i-begin/index.html).
Below are some minimal steps for getting the first version of a new game world going with players.
It's worth to at least make the attempt to do these steps in order even if you are itching to jump
ahead in the development cycle. On the other hand, you should also make sure to keep your work fun
for you, or motivation will falter. Making a full game is a lot of work as it is, you'll need all
your motivation to make it a reality.
Remember that *99.99999% of all great game ideas never lead to a game*. Especially not to an online
game that people can actually play and enjoy. So our first all overshadowing goal is to beat those
odds and get *something* out the door! Even if it's a scaled-down version of your dream game,
lacking many "must-have" features! It's better to get it out there and expand on it later than to
code in isolation forever until you burn out, lose interest or your hard drive crashes.
Like is common with online games, getting a game out the door does not mean you are going to be
"finished" with the game - most MUDs add features gradually over the course of years - it's often
part of the fun!
## Planning (step 1)
This is what you do before having coded a single line or built a single room. Many prospective game
developers are very good at *parts* of this process, namely in defining what their world is "about":
The theme, the world concept, cool monsters and so on. It is by all means very important to define
what is the unique appeal of your game. But it's unfortunately not enough to make your game a
reality. To do that you must also have an idea of how to actually map those great ideas onto
Evennia.
A good start is to begin by planning out the basic primitives of the game and what they need to be
able to do. Below are a far-from-complete list of examples (and for your first version you should
definitely try for a much shorter list):
### Systems
These are the behind-the-scenes features that exist in your game, often without being represented by
a specific in-game object.
- Should your game rules be enforced by coded systems or are you planning for human game masters to
run and arbitrate rules?
- What are the actual mechanical game rules? How do you decide if an action succeeds or fails? What
"rolls" does the game need to be able to do? Do you base your game off an existing system or make up
your own?
- Does the flow of time matter in your game - does night and day change? What about seasons? Maybe
your magic system is affected by the phase of the moon?
- Do you want changing, global weather? This might need to operate in tandem over a large number of
rooms.
- Do you want a game-wide economy or just a simple barter system? Or no formal economy at all?
- Should characters be able to send mail to each other in-game?
- Should players be able to post on Bulletin boards?
- What is the staff hierarchy in your game? What powers do you want your staff to have?
- What should a Builder be able to build and what commands do they need in order to do that?
- etc.
### Rooms
Consider the most basic room in your game.
- Is a simple description enough or should the description be able to change (such as with time, by
light conditions, weather or season)?
- Should the room have different statuses? Can it have smells, sounds? Can it be affected by
dramatic weather, fire or magical effects? If so, how would this affect things in the room? Or are
these things something admins/game masters should handle manually?
- Can objects be hidden in the room? Can a person hide in the room? How does the room display this?
- etc.
### Objects
Consider the most basic (non-player-controlled) object in your game.
- How numerous are your objects? Do you want large loot-lists or are objects just role playing props
created on demand?
- Does the game use money? If so, is each coin a separate object or do you just store a bank account
value?
- What about multiple identical objects? Do they form stacks and how are those stacks handled in
that case?
- Does an object have weight or volume (so you cannot carry an infinite amount of them)?
- Can objects be broken? If so, does it have a health value? Is burning it causing the same damage
as smashing it? Can it be repaired?
- Is a weapon a specific type of object or are you supposed to be able to fight with a chair too?
Can you fight with a flower or piece of paper as well?
- NPCs/mobs are also objects. Should they just stand around or should they have some sort of AI?
- Are NPCs/mobs differet entities? How is an Orc different from a Kobold, in code - are they the
same object with different names or completely different types of objects, with custom code?
- Should there be NPCs giving quests? If so, how would you track quest status and what happens when
multiple players try to do the same quest? Do you use instances or some other mechanism?
- etc.
### Characters
These are the objects controlled directly by Players.
- Can players have more than one Character active at a time or are they allowed to multi-play?
- How does a Player create their Character? A Character-creation screen? Answering questions?
Filling in a form?
- Do you want to use classes (like "Thief", "Warrior" etc) or some other system, like Skill-based?
- How do you implement different "classes" or "races"? Are they separate types of objects or do you
simply load different stats on a basic object depending on what the Player wants?
- If a Character can hide in a room, what skill will decide if they are detected?
- What skill allows a Character to wield a weapon and hit? Do they need a special skill to wield a
chair rather than a sword?
- Does a Character need a Strength attribute to tell how much they can carry or which objects they
can smash?
- What does the skill tree look like? Can a Character gain experience to improve? By killing
enemies? Solving quests? By roleplaying?
- etc.
A MUD's a lot more involved than you would think and these things hang together in a complex web. It
can easily become overwhelming and it's tempting to want *all* functionality right out of the door.
Try to identify the basic things that "make" your game and focus *only* on them for your first
release. Make a list. Keep future expansions in mind but limit yourself.
## Coding (step 2)
This is the actual work of creating the "game" part of your game. Many "game-designer" types tend to
gloss over this bit and jump directly to **World Building**. Vice versa, many "game-coder" types
tend to jump directly to this part without doing the **Planning** first. Neither way is good and
*will* lead to you having to redo all your hard work at least once, probably more.
Evennia's [Developer Central](./Developer-Central) tries to help you with this bit of development. We
also have a slew of [Tutorials](./Tutorials) with worked examples. Evennia tries hard to make this
part easier for you, but there is no way around the fact that if you want anything but a very basic
Talker-type game you *will* have to bite the bullet and code your game (or find a coder willing to
do it for you).
Even if you won't code anything yourself, as a designer you need to at least understand the basic
paradigms of Evennia, such as [Objects](./Objects), [Commands](./Commands) and [Scripts](./Scripts) and
how they hang together. We recommend you go through the [Tutorial World](Tutorial-World-
Introduction) in detail (as well as glancing at its code) to get at least a feel for what is
involved behind the scenes. You could also look through the tutorial for [building a game from
scratch](Tutorial-for-basic-MUSH-like-game).
During Coding you look back at the things you wanted during the **Planning** phase and try to
implement them. Don't be shy to update your plans if you find things easier/harder than you thought.
The earlier you revise problems, the easier they will be to fix.
A good idea is to host your code online (publicly or privately) using version control. Not only will
this make it easy for multiple coders to collaborate (and have a bug-tracker etc), it also means
your work is backed up at all times. The [Version Control](./Version-Control) tutorial has
instructions for setting up a sane developer environment with proper version control.
### "Tech Demo" Building
This is an integral part of your Coding. It might seem obvious to experienced coders, but it cannot
be emphasized enough that you should *test things on a small scale* before putting your untested
code into a large game-world. The earlier you test, the easier and cheaper it will be to fix bugs
and even rework things that didn't work out the way you thought they would. You might even have to
go back to the **Planning** phase if your ideas can't handle their meet with reality.
This means building singular in-game examples. Make one room and one object of each important type
and test so they work correctly in isolation. Then add more if they are supposed to interact with
each other in some way. Build a small series of rooms to test how mobs move around ... and so on. In
short, a test-bed for your growing code. It should be done gradually until you have a fully
functioning (if not guaranteed bug-free) miniature tech demo that shows *all* the features you want
in the first release of your game. There does not need to be any game play or even a theme to your
tests, this is only for you and your co-coders to see. The more testing you do on this small scale,
the less headaches you will have in the next phase.
## World Building (step 3)
Up until this point we've only had a few tech-demo objects in the database. This step is the act of
populating the database with a larger, thematic world. Too many would-be developers jump to this
stage too soon (skipping the **Coding** or even **Planning** stages). What if the rooms you build
now doesn't include all the nice weather messages the code grows to support? Or the way you store
data changes under the hood? Your building work would at best require some rework and at worst you
would have to redo the whole thing. And whereas Evennia's typeclass system does allow you to edit
the properties of existing objects, some hooks are only called at object creation ... Suffice to
say you are in for a *lot* of unnecessary work if you build stuff en masse without having the
underlying code systems in some reasonable shape first.
So before starting to build, the "game" bit (**Coding** + **Testing**) should be more or less
**complete**, *at least to the level of your initial release*.
Before starting to build, you should also plan ahead again. Make sure it is clear to yourself and
your eventual builders just which parts of the world you want for your initial release. Establish
for everyone which style, quality and level of detail you expect. Your goal should *not* be to
complete your entire world in one go. You want just enough to make the game's "feel" come across.
You want a minimal but functioning world where the intended game play can be tested and roughly
balanced. You can always add new areas later.
During building you get free and extensive testing of whatever custom build commands and systems you
have made at this point. Since Building often involves different people than those Coding, you also
get a chance to hear if some things are hard to understand or non-intuitive. Make sure to respond
to this feedback.
## Alpha Release
As mentioned, don't hold onto your world more than necessary. *Get it out there* with a huge *Alpha*
flag and let people try it! Call upon your alpha-players to try everything - they *will* find ways
to break your game in ways that you never could have imagined. In Alpha you might be best off to
focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
feedback and bug reports (there *will* be bugs, there is no way around it). Follow the quick
instructions for [Online Setup](./Online-Setup) to make your game visible online. If you hadn't
already, make sure to put up your game on the [Evennia game index](http://games.evennia.com/) so
people know it's in the works (actually, even pre-alpha games are allowed in the index so don't be
shy)!
## Beta Release/Perpetual Beta
Once things stabilize in Alpha you can move to *Beta* and let more people in. Many MUDs are in
[perpetual beta](http://en.wikipedia.org/wiki/Perpetual_beta), meaning they are never considered
"finished", but just repeat the cycle of Planning, Coding, Testing and Building over and over as new
features get implemented or Players come with suggestions. As the game designer it is now up to you
to gradually perfect your vision.
## Congratulate yourself!
You are worthy of a celebration since at this point you have joined the small, exclusive crowd who
have made their dream game a reality!

View file

@ -0,0 +1,302 @@
# Gametime Tutorial
A lot of games use a separate time system we refer to as *game time*. This runs in parallel to what
we usually think of as *real time*. The game time might run at a different speed, use different
names for its time units or might even use a completely custom calendar. You don't need to rely on a
game time system at all. But if you do, Evennia offers basic tools to handle these various
situations. This tutorial will walk you through these features.
### A game time with a standard calendar
Many games let their in-game time run faster or slower than real time, but still use our normal
real-world calendar. This is common both for games set in present day as well as for games in
historical or futuristic settings. Using a standard calendar has some advantages:
- Handling repetitive actions is much easier, since converting from the real time experience to the
in-game perceived one is easy.
- The intricacies of the real world calendar, with leap years and months of different length etc are
automatically handled by the system.
Evennia's game time features assume a standard calendar (see the relevant section below for a custom
calendar).
#### Setting up game time for a standard calendar
All is done through the settings. Here are the settings you should use if you want a game time with
a standard calendar:
```python
# in a file settings.py in mygame/server/conf
# The time factor dictates if the game world runs faster (timefactor>1)
# or slower (timefactor<1) than the real world.
TIME_FACTOR = 2.0
# The starting point of your game time (the epoch), in seconds.
# In Python a value of 0 means Jan 1 1970 (use negatives for earlier
# start date). This will affect the returns from the utils.gametime
# module.
TIME_GAME_EPOCH = None
```
By default, the game time runs twice as fast as the real time. You can set the time factor to be 1
(the game time would run exactly at the same speed than the real time) or lower (the game time will
be slower than the real time). Most games choose to have the game time spinning faster (you will
find some games that have a time factor of 60, meaning the game time runs sixty times as fast as the
real time, a minute in real time would be an hour in game time).
The epoch is a slightly more complex setting. It should contain a number of seconds that would
indicate the time your game started. As indicated, an epoch of 0 would mean January 1st, 1970. If
you want to set your time in the future, you just need to find the starting point in seconds. There
are several ways to do this in Python, this method will show you how to do it in local time:
```python
# We're looking for the number of seconds representing
# January 1st, 2020
from datetime import datetime
import time
start = datetime(2020, 1, 1)
time.mktime(start.timetuple())
```
This should return a huge number - the number of seconds since Jan 1 1970. Copy that directly into
your settings (editing `server/conf/settings.py`):
```python
# in a file settings.py in mygame/server/conf
TIME_GAME_EPOCH = 1577865600
```
Reload the game with `@reload`, and then use the `@time` command. You should see something like
this:
```
+----------------------------+-------------------------------------+
| Server time | |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Current uptime | 20 seconds |
| Total runtime | 1 day, 1 hour, 55 minutes |
| First start | 2017-02-12 15:47:50.565000 |
| Current time | 2017-02-13 17:43:10.760000 |
+----------------------------+-------------------------------------+
| In-Game time | Real time x 2 |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Epoch (from settings) | 2020-01-01 00:00:00 |
| Total time passed: | 1 day, 17 hours, 34 minutes |
| Current time | 2020-01-02 17:34:55.430000 |
+----------------------------+-------------------------------------+
```
The line that is most relevant here is the game time epoch. You see it shown at 2020-01-01. From
this point forward, the game time keeps increasing. If you keep typing `@time`, you'll see the game
time updated correctly... and going (by default) twice as fast as the real time.
#### Time-related events
The `gametime` utility also has a way to schedule game-related events, taking into account your game
time, and assuming a standard calendar (see below for the same feature with a custom calendar). For
instance, it can be used to have a specific message every (in-game) day at 6:00 AM showing how the
sun rises.
The function `schedule()` should be used here. It will create a [script](./Scripts) with some
additional features to make sure the script is always executed when the game time matches the given
parameters.
The `schedule` function takes the following arguments:
- The *callback*, a function to be called when time is up.
- The keyword `repeat` (`False` by default) to indicate whether this function should be called
repeatedly.
- Additional keyword arguments `sec`, `min`, `hour`, `day`, `month` and `year` to describe the time
to schedule. If the parameter isn't given, it assumes the current time value of this specific unit.
Here is a short example for making the sun rise every day:
```python
# in a file ingame_time.py in mygame/world/
from evennia.utils import gametime
from typeclasses.rooms import Room
def at_sunrise():
"""When the sun rises, display a message in every room."""
# Browse all rooms
for room in Room.objects.all():
room.msg_contents("The sun rises from the eastern horizon.")
def start_sunrise_event():
"""Schedule an sunrise event to happen every day at 6 AM."""
script = gametime.schedule(at_sunrise, repeat=True, hour=6, min=0, sec=0)
script.key = "at sunrise"
```
If you want to test this function, you can easily do something like:
```
@py from world import ingame_time; ingame_time.start_sunrise_event()
```
The script will be created silently. The `at_sunrise` function will now be called every in-game day
at 6 AM. You can use the `@scripts` command to see it. You could stop it using `@scripts/stop`. If
we hadn't set `repeat` the sun would only have risen once and then never again.
We used the `@py` command here: nothing prevents you from adding the system into your game code.
Remember to be careful not to add each event at startup, however, otherwise there will be a lot of
overlapping events scheduled when the sun rises.
The `schedule` function when using `repeat` set to `True` works with the higher, non-specified unit.
In our example, we have specified hour, minute and second. The higher unit we haven't specified is
day: `schedule` assumes we mean "run the callback every day at the specified time". Therefore, you
can have an event that runs every hour at HH:30, or every month on the 3rd day.
> A word of caution for repeated scripts on a monthly or yearly basis: due to the variations in the
real-life calendar you need to be careful when scheduling events for the end of the month or year.
For example, if you set a script to run every month on the 31st it will run in January but find no
such day in February, April etc. Similarly, leap years may change the number of days in the year.
### A game time with a custom calendar
Using a custom calendar to handle game time is sometimes needed if you want to place your game in a
fictional universe. For instance you may want to create the Shire calendar which Tolkien described
having 12 months, each which 30 days. That would give only 360 days per year (presumably hobbits
weren't really fond of the hassle of following the astronomical calendar). Another example would be
creating a planet in a different solar system with, say, days 29 hours long and months of only 18
days.
Evennia handles custom calendars through an optional *contrib* module, called `custom_gametime`.
Contrary to the normal `gametime` module described above it is not active by default.
#### Setting up the custom calendar
In our first example of the Shire calendar, used by hobbits in books by Tolkien, we don't really
need the notion of weeks... but we need the notion of months having 30 days, not 28.
The custom calendar is defined by adding the `TIME_UNITS` setting to your settings file. It's a
dictionary containing as keys the name of the units, and as value the number of seconds (the
smallest unit for us) in this unit. Its keys must be picked among the following: "sec", "min",
"hour", "day", "week", "month" and "year" but you don't have to include them all. Here is the
configuration for the Shire calendar:
```python
# in a file settings.py in mygame/server/conf
TIME_UNITS = {"sec": 1,
"min": 60,
"hour": 60 * 60,
"day": 60 * 60 * 24,
"month": 60 * 60 * 24 * 30,
"year": 60 * 60 * 24 * 30 * 12 }
```
We give each unit we want as keys. Values represent the number of seconds in that unit. Hour is
set to 60 * 60 (that is, 3600 seconds per hour). Notice that we don't specify the week unit in this
configuration: instead, we skip from days to months directly.
In order for this setting to work properly, remember all units have to be multiples of the previous
units. If you create "day", it needs to be multiple of hours, for instance.
So for our example, our settings may look like this:
```python
# in a file settings.py in mygame/server/conf
# Time factor
TIME_FACTOR = 4
# Game time epoch
TIME_GAME_EPOCH = 0
# Units
TIME_UNITS = {
"sec": 1,
"min": 60,
"hour": 60 * 60,
"day": 60 * 60 * 24,
"month": 60 * 60 * 24 * 30,
"year": 60 * 60 * 24 * 30 * 12,
}
```
Notice we have set a time epoch of 0. Using a custom calendar, we will come up with a nice display
of time on our own. In our case the game time starts at year 0, month 0, day 0, and at midnight.
Note that while we use "month", "week" etc in the settings, your game may not use those terms in-
game, instead referring to them as "cycles", "moons", "sand falls" etc. This is just a matter of you
displaying them differently. See next section.
#### A command to display the current game time
As pointed out earlier, the `@time` command is meant to be used with a standard calendar, not a
custom one. We can easily create a new command though. We'll call it `time`, as is often the case
on other MU*. Here's an example of how we could write it (for the example, you can create a file
`showtime.py` in your `commands` directory and paste this code in it):
```python
# in a file mygame/commands/gametime.py
from evennia.contrib import custom_gametime
from commands.command import Command
class CmdTime(Command):
"""
Display the time.
Syntax:
time
"""
key = "time"
locks = "cmd:all()"
def func(self):
"""Execute the time command."""
# Get the absolute game time
year, month, day, hour, min, sec = custom_gametime.custom_gametime(absolute=True)
string = "We are in year {year}, day {day}, month {month}."
string += "\nIt's {hour:02}:{min:02}:{sec:02}."
self.msg(string.format(year=year, month=month, day=day,
hour=hour, min=min, sec=sec))
```
Don't forget to add it in your CharacterCmdSet to see this command:
```python
# in mygame/commands/default_cmdset.py
from commands.gametime import CmdTime # <-- Add
# ...
class CharacterCmdSet(default_cmds.CharacterCmdSet):
"""
The `CharacterCmdSet` contains general in-game commands like `look`,
`get`, etc available on in-game Character objects. It is merged with
the `AccountCmdSet` when an Account puppets a Character.
"""
key = "DefaultCharacter"
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
# ...
self.add(CmdTime()) # <- Add
```
Reload your game with the `@reload` command. You should now see the `time` command. If you enter
it, you might see something like:
We are in year 0, day 0, month 0.
It's 00:52:17.
You could display it a bit more prettily with names for months and perhaps even days, if you want.
And if "months" are called "moons" in your game, this is where you'd add that.
#### Time-related events in custom gametime
The `custom_gametime` module also has a way to schedule game-related events, taking into account
your game time (and your custom calendar). It can be used to have a specific message every day at
6:00 AM, to show the sun rises, for instance. The `custom_gametime.schedule` function works in the
same way as described for the default one above.

View file

@ -0,0 +1,539 @@
# Getting Started
This will help you download, install and start Evennia for the first time.
> Note: You don't need to make anything visible to the 'net in order to run and
> test out Evennia. Apart from downloading and updating you don't even need an
> internet connection until you feel ready to share your game with the world.
- [Quick Start](./Getting-Started#quick-start)
- [Requirements](./Getting-Started#requirements)
- [Linux Install](./Getting-Started#linux-install)
- [Mac Install](./Getting-Started#mac-install)
- [Windows Install](./Getting-Started#windows-install)
- [Running in Docker](./Running-Evennia-in-Docker)
- [Where to Go Next](./Getting-Started#where-to-go-next)
- [Troubleshooting](./Getting-Started#troubleshooting)
- [Glossary of terms](./Glossary)
## Quick Start
For the impatient. If you have trouble with a step, you should jump on to the
more detailed instructions for your platform.
1. Install Python, GIT and python-virtualenv. Start a Console/Terminal.
2. `cd` to some place you want to do your development (like a folder
`/home/anna/muddev/` on Linux or a folder in your personal user directory on Windows).
3. `git clone https://github.com/evennia/evennia.git`
4. `virtualenv evenv`
5. `source evenv/bin/activate` (Linux, Mac), `evenv\Scripts\activate` (Windows)
6. `pip install -e evennia`
7. `evennia --init mygame`
8. `cd mygame`
9. `evennia migrate`
10. `evennia start` (make sure to make a superuser when asked)
Evennia should now be running and you can connect to it by pointing a web browser to
`http://localhost:4001` or a MUD telnet client to `localhost:4000` (use `127.0.0.1` if your OS does
not recognize `localhost`).
We also release [Docker images](./Running-Evennia-in-Docker)
based on `master` and `develop` branches.
## Requirements
Any system that supports Python3.7+ should work. We'll describe how to install
everything in the following sections.
- Linux/Unix
- Windows (Vista, Win7, Win8, Win10)
- Mac OSX (>=10.5 recommended)
- [Python](http://www.python.org) (v3.7, 3.8 are tested)
- [virtualenv](http://pypi.python.org/pypi/virtualenv) for making isolated
Python environments. Installed with `pip install virtualenv`.
- [GIT](http://git-scm.com/) - version control software for getting and
updating Evennia itself - Mac users can use the
[git-osx-installer](http://code.google.com/p/git-osx-installer/) or the
[MacPorts version](http://git-scm.com/book/en/Getting-Started-Installing-Git#Installing-on-Mac).
- [Twisted](http://twistedmatrix.com) (v19.0+)
- [ZopeInterface](http://www.zope.org/Products/ZopeInterface) (v3.0+) - usually included in
Twisted packages
- Linux/Mac users may need the `gcc` and `python-dev` packages or equivalent.
- Windows users need [MS Visual C++](https://aka.ms/vs/16/release/vs_buildtools.exe) and *maybe*
[pypiwin32](https://pypi.python.org/pypi/pypiwin32).
- [Django](http://www.djangoproject.com) (v2.2.x), be warned that latest dev
version is usually untested with Evennia)
## Linux Install
If you run into any issues during the installation and first start, please
check out [Linux Troubleshooting](./Getting-Started#linux-troubleshooting).
For Debian-derived systems (like Ubuntu, Mint etc), start a terminal and
install the [dependencies](./Getting-Started#requirements):
```
sudo apt-get update
sudo apt-get install python3 python3-pip python3-dev python3-setuptools python3-git
python3-virtualenv gcc
# If you are using an Ubuntu version that defaults to Python3, like 18.04+, use this instead:
sudo apt-get update
sudo apt-get install python3.7 python3-pip python3.7-dev python3-setuptools virtualenv gcc
```
Note that, the default Python version for your distribution may still not be Python3.7 after this.
This is ok - we'll specify exactly which Python to use later.
You should make sure to *not* be `root` after this step, running as `root` is a
security risk. Now create a folder where you want to do all your Evennia
development:
```
mkdir muddev
cd muddev
```
Next we fetch Evennia itself:
```
git clone https://github.com/evennia/evennia.git
```
A new folder `evennia` will appear containing the Evennia library. This only
contains the source code though, it is not *installed* yet. To isolate the
Evennia install and its dependencies from the rest of the system, it is good
Python practice to install into a _virtualenv_. If you are unsure about what a
virtualenv is and why it's useful, see the [Glossary entry on
virtualenv](Glossary#virtualenv).
Run `python -V` to see which version of Python your system defaults to.
```
# If your Linux defaults to Python3.7+:
virtualenv evenv
# If your Linux defaults to Python2 or an older version
# of Python3, you must instead point to Python3.7+ explicitly:
virtualenv -p /usr/bin/python3.7 evenv
```
A new folder `evenv` will appear (we could have called it anything). This
folder will hold a self-contained setup of Python packages without interfering
with default Python packages on your system (or the Linux distro lagging behind
on Python package versions). It will also always use the right version of Python.
Activate the virtualenv:
```
source evenv/bin/activate
```
The text `(evenv)` should appear next to your prompt to show that the virtual
environment is active.
> Remember that you need to activate the virtualenv like this *every time* you
> start a new terminal to get access to the Python packages (notably the
> important `evennia` program) we are about to install.
Next, install Evennia into your active virtualenv. Make sure you are standing
at the top of your mud directory tree (so you see the `evennia/` and `evenv/`
folders) and run
```
pip install -e evennia
```
For more info about `pip`, see the [Glossary entry on pip](./Glossary#pip). If
install failed with any issues, see [Linux Troubleshooting](./Getting-Started#linux-troubleshooting).
Next we'll start our new game, here called "mygame". This will create yet
another new folder where you will be creating your new game:
```
evennia --init mygame
```
Your final folder structure should look like this:
```
./muddev
evenv/
evennia/
mygame/
```
You can [configure Evennia](./Server-Conf#settings-file) extensively, for example
to use a [different database](./Choosing-An-SQL-Server). For now we'll just stick
to the defaults though.
```
cd mygame
evennia migrate # (this creates the database)
evennia start # (create a superuser when asked. Email is optional.)
```
> Server logs are found in `mygame/server/logs/`. To easily view server logs
> live in the terminal, use `evennia -l` (exit the log-view with Ctrl-C).
Your game should now be running! Open a web browser at `http://localhost:4001`
or point a telnet client to `localhost:4000` and log in with the user you
created. Check out [where to go next](./Getting-Started#where-to-go-next).
## Mac Install
The Evennia server is a terminal program. Open the terminal e.g. from
*Applications->Utilities->Terminal*. [Here is an introduction to the Mac
terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line)
if you are unsure how it works. If you run into any issues during the
installation, please check out [Mac Troubleshooting](./Getting-Started#mac-troubleshooting).
* Python should already be installed but you must make sure it's a high enough version.
([This](http://docs.python-guide.org/en/latest/starting/install/osx/) discusses
how you may upgrade it). Remember that you need Python3.7, not Python2.7!
* GIT can be obtained with
[git-osx-installer](http://code.google.com/p/git-osx-installer/) or via
MacPorts [as described
here](http://git-scm.com/book/en/Getting-Started-Installing-Git#Installing-on-Mac).
* If you run into issues with installing `Twisted` later you may need to
install gcc and the Python headers.
After this point you should not need `sudo` or any higher privileges to install anything.
Now create a folder where you want to do all your Evennia development:
```
mkdir muddev
cd muddev
```
Next we fetch Evennia itself:
```
git clone https://github.com/evennia/evennia.git
```
A new folder `evennia` will appear containing the Evennia library. This only
contains the source code though, it is not *installed* yet. To isolate the
Evennia install and its dependencies from the rest of the system, it is good
Python practice to install into a _virtualenv_. If you are unsure about what a
virtualenv is and why it's useful, see the [Glossary entry on virtualenv](./Glossary#virtualenv).
Run `python -V` to check which Python your system defaults to.
```
# If your Mac defaults to Python3:
virtualenv evenv
# If your Mac defaults to Python2 you need to specify the Python3.7 binary explicitly:
virtualenv -p /path/to/your/python3.7 evenv
```
A new folder `evenv` will appear (we could have called it anything). This
folder will hold a self-contained setup of Python packages without interfering
with default Python packages on your system. Activate the virtualenv:
```
source evenv/bin/activate
```
The text `(evenv)` should appear next to your prompt to show the virtual
environment is active.
> Remember that you need to activate the virtualenv like this *every time* you
> start a new terminal to get access to the Python packages (notably the
> important `evennia` program) we are about to install.
Next, install Evennia into your active virtualenv. Make sure you are standing
at the top of your mud directory tree (so you see the `evennia/` and `evenv/`
folders) and run
```
pip install --upgrade pip # Old pip versions may be an issue on Mac.
pip install --upgrade setuptools # Ditto concerning Mac issues.
pip install -e evennia
```
For more info about `pip`, see the [Glossary entry on pip](./Glossary#pip). If
install failed with any issues, see [Mac Troubleshooting](./Getting-Started#mac-troubleshooting).
Next we'll start our new game. We'll call it "mygame" here. This creates a new
folder where you will be creating your new game:
```
evennia --init mygame
```
Your final folder structure should look like this:
```
./muddev
evenv/
evennia/
mygame/
```
You can [configure Evennia](./Server-Conf#settings-file) extensively, for example
to use a [different database](./Choosing-An-SQL-Server). We'll go with the
defaults here.
```
cd mygame
evennia migrate # (this creates the database)
evennia start # (create a superuser when asked. Email is optional.)
```
> Server logs are found in `mygame/server/logs/`. To easily view server logs
> live in the terminal, use `evennia -l` (exit the log-view with Ctrl-C).
Your game should now be running! Open a web browser at `http://localhost:4001`
or point a telnet client to `localhost:4000` and log in with the user you
created. Check out [where to go next](./Getting-Started#where-to-go-next).
## Windows Install
If you run into any issues during the installation, please check out
[Windows Troubleshooting](./Getting-Started#windows-troubleshooting).
> If you are running Windows10, consider using the Windows Subsystem for Linux
> ([WSL](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux)) instead.
> You should then follow the Linux install instructions above.
The Evennia server itself is a command line program. In the Windows launch
menu, start *All Programs -> Accessories -> command prompt* and you will get
the Windows command line interface. Here is [one of many tutorials on using the Windows command
line](http://www.bleepingcomputer.com/tutorials/windows-command-prompt-introduction/)
if you are unfamiliar with it.
* Install Python [from the Python homepage](https://www.python.org/downloads/windows/). You will
need to be a
Windows Administrator to install packages. You want Python version **3.7.0** (latest verified
version), usually
the 64-bit version (although it doesn't matter too much). **When installing, make sure
to check-mark *all* install options, especially the one about making Python
available on the path (you may have to scroll to see it)**. This allows you to
just write `python` in any console without first finding where the `python`
program actually sits on your hard drive.
* You need to also get [GIT](http://git-scm.com/downloads) and install it. You
can use the default install options but when you get asked to "Adjust your PATH
environment", you should select the second option "Use Git from the Windows
Command Prompt", which gives you more freedom as to where you can use the
program.
* Finally you must install the [Microsoft Visual C++ compiler for
Python](https://aka.ms/vs/16/release/vs_buildtools.exe). Download and run the linked installer and
install the C++ tools. Keep all the defaults. Allow the install of the "Win10 SDK", even if you are
on Win7 (not tested on older Windows versions). If you later have issues with installing Evennia due
to a failure to build the "Twisted wheels", this is where you are missing things.
* You *may* need the [pypiwin32](https://pypi.python.org/pypi/pypiwin32) Python headers. Install
these only if you have issues.
You can install Evennia wherever you want. `cd` to that location and create a
new folder for all your Evennia development (let's call it `muddev`).
```
mkdir muddev
cd muddev
```
> Hint: If `cd` isn't working you can use `pushd` instead to force the
> directory change.
Next we fetch Evennia itself:
```
git clone https://github.com/evennia/evennia.git
```
A new folder `evennia` will appear containing the Evennia library. This only
contains the source code though, it is not *installed* yet. To isolate the
Evennia install and its dependencies from the rest of the system, it is good
Python practice to install into a _virtualenv_. If you are unsure about what a
virtualenv is and why it's useful, see the [Glossary entry on virtualenv](./Glossary#virtualenv).
In your console, try `python -V` to see which version of Python your system
defaults to.
```
pip install virtualenv
# If your setup defaults to Python3.7:
virtualenv evenv
# If your setup defaults to Python2, specify path to python3.exe explicitly:
virtualenv -p C:\Python37\python.exe evenv
# If you get an infinite spooling response, press CTRL + C to interrupt and try using:
python -m venv evenv
```
A new folder `evenv` will appear (we could have called it anything). This
folder will hold a self-contained setup of Python packages without interfering
with default Python packages on your system. Activate the virtualenv:
```
# If you are using a standard command prompt, you can use the following:
evenv\scripts\activate.bat
# If you are using a PS Shell, Git Bash, or other, you can use the following:
.\evenv\scripts\activate
```
The text `(evenv)` should appear next to your prompt to show the virtual
environment is active.
> Remember that you need to activate the virtualenv like this *every time* you
> start a new console window if you want to get access to the Python packages
> (notably the important `evennia` program) we are about to install.
Next, install Evennia into your active virtualenv. Make sure you are standing
at the top of your mud directory tree (so you see the `evennia` and `evenv`
folders when you use the `dir` command) and run
```
pip install -e evennia
```
For more info about `pip`, see the [Glossary entry on pip](./Glossary#pip). If
the install failed with any issues, see [Windows Troubleshooting](./Getting-Started#windows-
troubleshooting).
Next we'll start our new game, we'll call it "mygame" here. This creates a new folder where you will
be
creating your new game:
```
evennia --init mygame
```
Your final folder structure should look like this:
```
path\to\muddev
evenv\
evennia\
mygame\
```
You can [configure Evennia](./Server-Conf#settings-file) extensively, for example
to use a [different database](./Choosing-An-SQL-Server). We'll go with the
defaults here.
```
cd mygame
evennia migrate # (this creates the database)
evennia start # (create a superuser when asked. Email is optional.)
```
> Server logs are found in `mygame/server/logs/`. To easily view server logs
> live in the terminal, use `evennia -l` (exit the log-view with Ctrl-C).
Your game should now be running! Open a web browser at `http://localhost:4001`
or point a telnet client to `localhost:4000` and log in with the user you
created. Check out [where to go next](./Getting-Started#where-to-go-next).
## Where to Go Next
Welcome to Evennia! Your new game is fully functioning, but empty. If you just
logged in, stand in the `Limbo` room and run
@batchcommand tutorial_world.build
to build [Evennia's tutorial world](./Tutorial-World-Introduction) - it's a small solo quest to
explore. Only run the instructed `@batchcommand` once. You'll get a lot of text scrolling by as the
tutorial is built. Once done, the `tutorial` exit will have appeared out of Limbo - just write
`tutorial` to enter it.
Once you get back to `Limbo` from the tutorial (if you get stuck in the tutorial quest you can do
`@tel #2` to jump to Limbo), a good idea is to learn how to [start, stop and reload](Start-Stop-
Reload) the Evennia server. You may also want to familiarize yourself with some [commonly used terms
in our Glossary](Glossary). After that, why not experiment with [creating some new items and build
some new rooms](Building-Quickstart) out from Limbo.
From here on, you could move on to do one of our [introductory tutorials](./Tutorials) or simply dive
headlong into Evennia's comprehensive [manual](https://github.com/evennia/evennia/wiki). While
Evennia has no major game systems out of the box, we do supply a range of optional *contribs* that
you can use or borrow from. They range from dice rolling and alternative color schemes to barter and
combat systems. You can find the [growing list of contribs
here](https://github.com/evennia/evennia/blob/master/evennia/contrib/README.md).
If you have any questions, you can always ask in [the developer
chat](http://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb)
`#evennia` on `irc.freenode.net` or by posting to the [Evennia
forums](https://groups.google.com/forum/#%21forum/evennia). You can also join the [Discord
Server](https://discord.gg/NecFePw).
Finally, if you are itching to help out or support Evennia (awesome!) have an
issue to report or a feature to request, [see here](./How-To-Get-And-Give-Help).
Enjoy your stay!
## Troubleshooting
If you have issues with installing or starting Evennia for the first time,
check the section for your operating system below. If you have an issue not
covered here, [please report it](https://github.com/evennia/evennia/issues)
so it can be fixed or a workaround found!
Remember, the server logs are in `mygame/server/logs/`. To easily view server logs in the terminal,
you can run `evennia -l`, or (in the future) start the server with `evennia start -l`.
### Linux Troubleshooting
- If you get an error when installing Evennia (especially with lines mentioning
failing to include `Python.h`) then try `sudo apt-get install python3-setuptools python3-dev`.
Once installed, run `pip install -e evennia` again.
- Under some not-updated Linux distributions you may run into errors with a
too-old `setuptools` or missing `functools`. If so, update your environment
with `pip install --upgrade pip wheel setuptools`. Then try `pip install -e evennia` again.
- One user reported a rare issue on Ubuntu 16 is an install error on installing Twisted; `Command
"python setup.py egg_info" failed with error code 1 in /tmp/pip-build-vnIFTg/twisted/` with errors
like `distutils.errors.DistutilsError: Could not find suitable distribution for
Requirement.parse('incremental>=16.10.1')`. This appears possible to solve by simply updating Ubuntu
with `sudo apt-get update && sudo apt-get dist-upgrade`.
- Users of Fedora (notably Fedora 24) has reported a `gcc` error saying the directory
`/usr/lib/rpm/redhat/redhat-hardened-cc1` is missing, despite `gcc` itself being installed. [The
confirmed work-around](https://gist.github.com/yograterol/99c8e123afecc828cb8c) seems to be to
install the `redhat-rpm-config` package with e.g. `sudo dnf install redhat-rpm-config`.
- Some users trying to set up a virtualenv on an NTFS filesystem find that it fails due to issues
with symlinks not being supported. Answer is to not use NTFS (seriously, why would you do that to
yourself?)
### Mac Troubleshooting
- Mac users have reported a critical `MemoryError` when trying to start Evennia on Mac with a Python
version below `2.7.12`. If you get this error, update to the latest XCode and Python2 version.
- Some Mac users have reported not being able to connect to `localhost` (i.e. your own computer). If
so, try to connect to `127.0.0.1` instead, which is the same thing. Use port 4000 from mud clients
and port 4001 from the web browser as usual.
### Windows Troubleshooting
- If you installed Python but the `python` command is not available (even in a new console), then
you might have missed installing Python on the path. In the Windows Python installer you get a list
of options for what to install. Most or all options are pre-checked except this one, and you may
even have to scroll down to see it. Reinstall Python and make sure it's checked.
- If your MUD client cannot connect to `localhost:4000`, try the equivalent `127.0.0.1:4000`
instead. Some MUD clients on Windows does not appear to understand the alias `localhost`.
- If you run `virtualenv evenv` and get a `'virtualenv' is not recognized as an internal or external
command,
operable program or batch file.` error, you can `mkdir evenv`, `cd evenv` and then `python -m
virtualenv .` as a workaround.
- Some Windows users get an error installing the Twisted 'wheel'. A wheel is a pre-compiled binary
package for Python. A common reason for this error is that you are using a 32-bit version of Python,
but Twisted has not yet uploaded the latest 32-bit wheel. Easiest way to fix this is to install a
slightly older Twisted version. So if, say, version `18.1` failed, install `18.0` manually with `pip
install twisted==18.0`. Alternatively you could try to get a 64-bit version of Python (uninstall the
32bit one). If so, you must then `deactivate` the virtualenv, delete the `evenv` folder and recreate
it anew (it will then use the new Python executable).
- If your server won't start, with no error messages (and no log files at all when starting from
scratch), try to start with `evennia ipstart` instead. If you then see an error about `system cannot
find the path specified`, it may be that the file `evennia/evennia/server/twistd.bat` has the wrong
path to the `twistd` executable. This file is auto-generated, so try to delete it and then run
`evennia start` to rebuild it and see if it works. If it still doesn't work you need to open it in a
text editor like Notepad. It's just one line containing the path to the `twistd.exe` executable as
determined by Evennia. If you installed Twisted in a non-standard location this might be wrong and
you should update the line to the real location.
- Some users have reported issues with Windows WSL and anti-virus software during Evennia
development. Timeout errors and the inability to run `evennia connections` may be due to your anti-
virus software interfering. Try disabling or changing your anti-virus software settings.

372
docs/source/Glossary.md Normal file
View file

@ -0,0 +1,372 @@
# Glossary
This explains common recurring terms used in the Evennia docs. It will be expanded as needed.
- _[account](./Glossary#account)_ - the player's account on the game
- _[admin-site](./Glossary#admin-site)_ - the Django web page for manipulating the database
- _[attribute](./Glossary#attribute)_ - persistent, custom data stored on typeclasses
- _[channel](./Glossary#channel)_ - game communication channels
- _[character](./Glossary#character)_ - the player's avatar in the game, controlled from
_[account](./Glossary#account)_
- _[core](./Glossary#core)_ - a term used for the code distributed with Evennia proper
- _[django](./Glossary#django)_ - web framework Evennia uses for database access and web integration
- _[field](./Glossary#field)_ - a _[typeclass](./Glossary#typeclass)_ property representing a database
column
- _[git](./Glossary#git)_ - the version-control system we use
- _[github](./Glossary#github)_ - the online hosting of our source code
- _[migrate](./Glossary#migrate)_ - updating the database schema
- _[multisession mode`](#multisession-mode)_ - a setting defining how users connect to Evennia
- _[object](./Glossary#object)_ - Python instance, general term or in-game
_[typeclass](./Glossary#typeclass)_
- _[pip](./Glossary#pip)_ - the Python installer
- _player_ - the human connecting to the game with their client
- _[puppet](./Glossary#puppet)_ - when an [account](./Glossary#account) controls an in-game
[object](./Glossary#object)
- _[property](./Glossary#property)_ - a python property
- _evenv_ - see _[virtualenv](./Glossary#virtualenv)_
- _[repository](./Glossary#repository)_ - a store of source code + source history
- _[script](./Glossary#script)_ - a building block for custom storage, systems and time-keepint
- _[session](./Glossary#session)_ - represents one client connection
- _[ticker](./Glossary#ticker)_ - Allows to run events on a steady 'tick'
- _[twisted](./Glossary#twisted)_ - networking engine responsible for Evennia's event loop and
communications
- _[typeclass](./Glossary#typeclass)_ - Evennia's database-connected Python class
- _upstream_ - see _[github](./Glossary#github)_
- _[virtualenv](./Glossary#virtualenv)_ - a Python program and way to make an isolated Python install
---
### _account_
The term 'account' refers to the [player's](./Glossary#player) unique account on the game. It is
represented by the `Account` [typeclass](./Glossary#typeclass) and holds things like email, password,
configuration etc.
When a player connects to the game, they connect to their account. The account has *no*
representation in the game world. Through their Account they can instead choose to
[puppet](./Glossary#puppet) one (or more, depending on game mode) [Characters](./Glossary#character) in
the game.
In the default [multisession mode](./Sessions#multisession-mode) of Evennia, you immediately start
puppeting a Character with the same name as your Account when you log in - mimicking how older
servers used to work.
### _admin-site_
This usually refers to [Django's](./Glossary#django) *Admin site* or database-administration web page
([link to Django docs](https://docs.djangoproject.com/en/2.1/ref/contrib/admin/)). The admin site is
an automatically generated web interface to the database (it can be customized extensively). It's
reachable from the `admin` link on the default Evennia website you get with your server.
### _attribute_
The term _Attribute_ should not be confused with ([properties](./Glossary#property) or
[fields](./Glossary#field). The `Attribute` represents arbitrary pieces of data that can be attached
to any [typeclassed](./Glossary#typeclass) entity in Evennia. Attributes allows storing new persistent
data on typeclasses without changing their underlying database schemas. [Read more about Attributes
here](Attributes).
### _channel_
A _Channel_ refers to an in-game communication channel. It's an entity that people subscribe to and
which re-distributes messages between all subscribers. Such subscribers default to being
[Accounts](./Glossary#account), for out-of-game communication but could also be [Objects (usually
Characters)](Glossary#character) if one wanted to adopt Channels for things like in-game walkie-
talkies or phone systems. It is represented by the `Channel` typeclass. [You can read more about the
comm system here](Communications#channels).
### _character_
The _Character_ is the term we use for the default avatar being [puppeted](./Glossary#puppet) by the
[account](./Glossary#account) in the game world. It is represented by the `Character` typeclass (which
is a child of [Object](./Glossary#object)). Many developers use children of this class to represent
monsters and other NPCs. You can [read more about it here](./Objects#subclasses-of-object).
### _django_
[Django](https://www.djangoproject.com/) is a professional and very popular Python web framework,
similar to Rails for the Ruby language. It is one of Evennia's central library dependencies (the
other one is [Twisted](./Glossary#twisted)). Evennia uses Django for two main things - to map all
database operations to Python and for structuring our web site.
Through Django, we can work with any supported database (SQlite3, Postgres, MySQL ...) using generic
Python instead of database-specific SQL: A database table is represented in Django as a Python class
(called a *model*). An Python instance of such a class represents a row in that table.
There is usually no need to know the details of Django's database handling in order to use Evennia -
it will handle most of the complexity for you under the hood using what we call
[typeclasses](./Glossary#typeclass). But should you need the power of Django you can always get it.
Most commonly people want to use "raw" Django when doing more advanced/custom database queries than
offered by Evennia's [default search functions](./Tutorial-Searching-For-Objects). One will then need
to read about Django's _querysets_. Querysets are Python method calls on a special form that lets
you build complex queries. They get converted into optimized SQL queries under the hood, suitable
for your current database. [Here is our tutorial/explanation of Django queries](Tutorial-Searching-
For-Objects#queries-in-django).
> By the way, Django (and Evennia) does allow you to fall through and send raw SQL if you really
want to. It's highly unlikely to be needed though; the Django database abstraction is very, very
powerful.
The other aspect where Evennia uses Django is for web integration. On one end Django gives an
infrastructure for wiring Python functions (called *views*) to URLs: the view/function is called
when a user goes that URL in their browser, enters data into a form etc. The return is the web page
to show. Django also offers templating with features such as being able to add special markers in
HTML where it will insert the values of Python variables on the fly (like showing the current player
count on the web page). [Here is one of our tutorials on wiring up such a web page](Add-a-simple-
new-web-page). Django also comes with the [admin site](./Glossary#admin-site), which automatically
maps the database into a form accessible from a web browser.
### _core_
This term is sometimes used to represent the main Evennia library code suite, *excluding* its
[contrib](./Glossary#contrib) directory. It can sometimes come up in code reviews, such as
> Evennia is game-agnostic but this feature is for a particular game genre. So it does not belong in
core. Better make it a contrib.
### _field_
A _field_ or _database field_ in Evennia refers to a [property](./Glossary#property) on a
[typeclass](./Glossary#typeclass) directly linked to an underlying database column. Only a few fixed
properties per typeclass are database fields but they are often tied to the core functionality of
that base typeclass (for example [Objects](./Glossary#object) store its location as a field). In all
other cases, [attributes](./Glossary#attribute) are used to add new persistent data to the typeclass.
[Read more about typeclass properties here](./Typeclasses#about-typeclass-properties).
### _git_
[Git](https://git-scm.com/) is a [version control](https://en.wikipedia.org/wiki/Version_control)
tool. It allows us to track the development of the Evennia code by dividing it into units called
*commits*. A 'commit' is sort of a save-spot - you save the current state of your code and can then
come back to it later if later changes caused problems. By tracking commits we know what 'version'
of the code we are currently using.
Evennia's source code + its source history is jointly called a [repository](./Glossary#repository).
This is centrally stored at our online home on [GitHub](./Glossary#github). Everyone using or
developing Evennia makes a 'clone' of this repository to their own computer - everyone
automatically gets everything that is online, including all the code history.
> Don't confuse Git and [GitHub](./Glossary#github). The former is the version control system. The
latter is a website (run by a company) that allows you to upload source code controlled by Git for
others to see (among other things).
Git allows multiple users from around the world to efficiently collaborate on Evennia's code: People
can make local commits on their cloned code. The commits they do can then be uploaded to GitHub and
reviewed by the Evennia lead devs - and if the changes look ok they can be safely *merged* into the
central Evennia code - and everyone can *pull* those changes to update their local copies.
Developers using Evennia often uses Git on their own games in the same way - to track their changes
and to help collaboration with team mates. This is done completely independently of Evennia's Git
usage.
Common usage (for non-Evennia developers):
- `git clone <github-url>` - clone an online repository to your computer. This is what you do when
you 'download' Evennia. You only need to do this once.
- `git pull` (inside local copy of repository) - sync your local repository with what is online.
> Full usage of Git is way beyond the scope of this glossary. See [Tutorial - version
control](Version-Control) for more info and links to the Git documentation.
### _migrate_
This term is used for upgrading the database structure (it's _schema_ )to a new version. Most often
this is due to Evennia's [upstream](./Glossary#github) schema changing. When that happens you need to
migrate that schema to the new version as well. Once you have used [git](./Glossary#git) to pull the
latest changes, just `cd` into your game dir and run
evennia migrate
That should be it (see [virtualenv](./Glossary#virtualenv) if you get a warning that the `evennia`
command is not available). See also [Updating your game](./Updating-Your-Game) for more details.
> Technically, migrations are shipped as little Python snippets of code that explains which database
actions must be taken to upgrade from one version of the schema to the next. When you run the
command above, those snippets are run in sequence.
### _multisession mode_
This term refers to the `MULTISESSION_MODE` setting, which has a value of 0 to 3. The mode alters
how players can connect to the game, such as how many Sessions a player can start with one account
and how many Characters they can control at the same time. It is [described in detail
here](Sessions#multisession-mode).
### _github_
[Github](https://github.com/evennia) is where Evennia's source code and documentation is hosted.
This online [repository](./Glossary#repository) of code we also sometimes refer to as _upstream_.
GitHub is a business, offering free hosting to Open-source projects like Evennia. Despite the
similarity in name, don't confuse GitHub the website with [Git](./Glossary#git), the versioning
system. Github hosts Git [repositories](./Glossary#repository) online and helps with collaboration and
infrastructure. Git itself is a separate project.
### _object_
In general Python (and other [object-oriented languages](https://en.wikipedia.org/wiki/Object-
oriented_programming)), an `object` is what we call the instance of a *class*. But one of Evennia's
core [typeclasses](./Glossary#typeclasss) is also called "Object". To separate these in the docs we
try to use `object` to refer to the general term and capitalized `Object` when we refer to the
typeclass.
The `Object` is a typeclass that represents all *in-game* entities, including
[Characters](./Glossary#character), rooms, trees, weapons etc. [Read more about Objects
here](Objects).
### _pip_
_[pip](https://pypi.org/project/pip/)_ comes with Python and is the main tool for installing third-
party Python packages from the web. Once a python package is installed you can do `import
<packagename>` in your Python code.
Common usage:
- `pip install <package-name>` - install the given package along with all its dependencies.
- `pip search <name>` - search Python's central package repository [PyPi](https://pypi.org/) for a
package of that name.
- `pip install --upgrade <package_name>` - upgrade a package you already have to the latest version.
- `pip install <packagename>==1.5` - install exactly a specific package version.
- `pip install <folder>` - install a Python package you have downloaded earlier (or cloned using
git).
- `pip install -e <folder>` - install a local package by just making a soft link to the folder. This
means that if the code in `<folder>` changes, the installed Python package is immediately updated.
If not using `-e`, one would need to run `pip install --upgrade <folder>` every time to make the
changes available when you import this package into your code. Evennia is installed this way.
For development, `pip` is usually used together with a [virtualenv](./Glossary#virtualenv) to install
all packages and dependencies needed for a project in one, isolated location on the hard drive.
### _puppet_
An [account](./Glossary#account) can take control and "play as" any [Object](./Glossary#object). When
doing so, we call this _puppeting_, (like [puppeteering](https://en.wikipedia.org/wiki/Puppeteer)).
Normally the entity being puppeted is of the [Character](./Glossary#character) subclass but it does
not have to be.
### _property_
A _property_ is a general term used for properties on any Python object. The term also sometimes
refers to the `property` built-in function of Python ([read more here](https://www.python-
course.eu/python3_properties.php)). Note the distinction between properties,
[fields](./Glossary#field) and [Attributes](./Glossary#attribute).
### _repository_
A _repository_ is a version control/[git](./Glossary#git) term. It represents a folder containing
source code plus its versioning history.
> In Git's case, that history is stored in a hidden folder `.git`. If you ever feel the need to look
into this folder you probably already know enough Git to know why.
The `evennia` folder you download from us with `git clone` is a repository. The code on
[GitHub](./Glossary#github) is often referred to as the 'online repository' (or the _upstream_
repository). If you put your game dir under version control, that of course becomes a repository as
well.
### _script_
When we refer to _Scripts_, we generally refer to the `Script` [typeclass](./Typeclasses). Scripts are
the mavericks of Evennia - they are like [Objects](./Glossary#object) but without any in-game
existence. They are useful as custom places to store data but also as building blocks in persistent
game systems. Since the can be initialized with timing capabilities they can also be used for long-
time persistent time keeping (for fast updates other types of timers may be better though). [Read
more about Scripts here](Scripts)
### _session_
A [Session](./Sessions) is a Python object representing a single client connection to the server. A
given human player could connect to the game from different clients and each would get a Session
(even if you did not allow them to actually log in and get access to an
[account](./Glossary#account)).
Sessions are _not_ [typeclassed](./Glossary#typeclass) and has no database persistence. But since they
always exist (also when not logged in), they share some common functionality with typeclasses that
can be useful for certain game states.
### _ticker_
The [Ticker handler](./TickerHandler) runs Evennia's optional 'ticker' system. In other engines, such
as [DIKU](https://en.wikipedia.org/wiki/DikuMUD), all game events are processed only at specific
intervals called 'ticks'. Evennia has no such technical limitation (events are processed whenever
needed) but using a fixed tick can still be useful for certain types of game systems, like combat.
Ticker Handler allows you to emulate any number of tick rates (not just one) and subscribe actions
to be called when those ticks come around.
### _typeclass_
The [typeclass](./Typeclasses) is an Evennia-specific term. A typeclass allows developers to work with
database-persistent objects as if they were normal Python objects. It makes use of specific
[Django](./Glossary#django) features to link a Python class to a database table. Sometimes we refer to
such code entities as _being typeclassed_.
Evennia's main typeclasses are [Account](./Glossary#account), [Object](./Glossary#object),
[Script](./Glossary#script) and [Channel](./Glossary#channel). Children of the base class (such as
[Character](./Glossary#character)) will use the same database table as the parent, but can have vastly
different Python capabilities (and persistent features through [Attributes](./Glossary#attributes) and
[Tags](./Glossary#tags). A typeclass can be coded and treated pretty much like any other Python class
except it must inherit (at any distance) from one of the base typeclasses. Also, creating a new
instance of a typeclass will add a new row to the database table to which it is linked.
The [core](./Glossary#core) typeclasses in the Evennia library are all named `DefaultAccount`,
`DefaultObject` etc. When you initialize your [game dir] you automatically get empty children of
these, called `Account`, `Object` etc that you can start working with.
### _twisted_
[Twisted](https://twistedmatrix.com/trac/) is a heavy-duty asynchronous networking engine. It is one
of Evennia's two major library dependencies (the other one is [Django](./Glossary#django)). Twisted is
what "runs" Evennia - it handles Evennia's event loop. Twisted also has the building blocks we need
to construct network protocols and communicate with the outside world; such as our MUD-custom
version of Telnet, Telnet+SSL, SSH, webclient-websockets etc. Twisted also runs our integrated web
server, serving the Django-based website for your game.
### _virtualenv_
The standard [virtualenv](https://virtualenv.pypa.io/en/stable/) program comes with Python. It is
used to isolate all Python packages needed by a given Python project into one folder (we call that
folder `evenv` but it could be called anything). A package environment created this way is usually
referred to as "a virtualenv". If you ever try to run the `evennia` program and get an error saying
something like "the command 'evennia' is not available" - it's probably because your virtualenv is
not 'active' yet (see below).
Usage:
- `virtualenv <name>` - initialize a new virtualenv `<name>` in a new folder `<name>` in the current
location. Called `evenv` in these docs.
- `virtualenv -p path/to/alternate/python_executable <name>` - create a virtualenv using another
Python version than default.
- `source <folder_name>/bin/activate`(linux/mac) - activate the virtualenv in `<folder_name>`.
- `<folder_name>\Scripts\activate` (windows)
- `deactivate` - turn off the currently activated virtualenv.
A virtualenv is 'activated' only for the console/terminal it was started in, but it's safe to
activate the same virtualenv many times in different windows if you want. Once activated, all Python
packages now installed with [pip](./Glossary#pip) will install to `evenv` rather than to a global
location like `/usr/local/bin` or `C:\Program Files`.
> Note that if you have root/admin access you *could* install Evennia globally just fine, without
using a virtualenv. It's strongly discouraged and considered bad practice though. Experienced Python
developers tend to rather create one new virtualenv per project they are working on, to keep the
varying installs cleanly separated from one another.
When you execute Python code within this activated virtualenv, *only* those packages installed
within will be possible to `import` into your code. So if you installed a Python package globally on
your computer, you'll need to install it again in your virtualenv.
> Virtualenvs *only* deal with Python programs/packages. Other programs on your computer couldn't
care less if your virtualenv is active or not. So you could use `git` without the virtualenv being
active, for example.
When your virtualenv is active you should see your console/terminal prompt change to
(evenv) ...
... or whatever name you gave the virtualenv when you initialized it.
> We sometimes say that we are "in" the virtualenv when it's active. But just to be clear - you
never have to actually `cd` into the `evenv` folder. You can activate it from anywhere and will
still be considered "in" the virtualenv wherever you go until you `deactivate` or close the
console/terminal.
So, when do I *need* to activate my virtualenv? If the virtualenv is not active, none of the Python
packages/programs you installed in it will be available to you. So at a minimum, *it needs to be
activated whenever you want to use the `evennia` command* for any reason.

71
docs/source/Grapevine.md Normal file
View file

@ -0,0 +1,71 @@
# Grapevine
[Grapevine](http://grapevine.haus) is a new chat network for `MU*`*** games. By
connecting an in-game channel to the grapevine network, players on your game
can chat with players in other games, also non-Evennia ones.
## Configuring Grapevine
To use Grapevine, you first need the `pyopenssl` module. Install it into your
Evennia python environment with
pip install pyopenssl
To configure Grapevine, you'll need to activate it in your settings file.
```python
GRAPEVINE_ENABLED = True
```
Next, register an account at https://grapevine.haus. When you have logged in,
go to your Settings/Profile and to the `Games` sub menu. Here you register your
new game by filling in its information. At the end of registration you are going
to get a `Client ID` and a `Client Secret`. These should not be shared.
Open/create the file `mygame/server/conf/secret_settings.py` and add the following:
```python
GRAPEVINE_CLIENT_ID = "<client ID>"
GRAPEVINE_CLIENT_SECRET = "<client_secret>"
```
You can also customize the Grapevine channels you are allowed to connect to. This
is added to the `GRAPEVINE_CHANNELS` setting. You can see which channels are available
by going to the Grapevine online chat here: https://grapevine.haus/chat.
Start/reload Evennia and log in as a privileged user. You should now have a new
command available: `@grapevine2chan`. This command is called like this:
@grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>
Here, the `evennia_channel` must be the name of an existing Evennia channel and
`grapevine_channel` one of the supported channels in `GRAPEVINE_CHANNELS`.
> At the time of writing, the Grapevine network only has two channels:
> `testing` and `gossip`. Evennia defaults to allowing connecting to both. Use
> `testing` for trying your connection.
## Setting up Grapevine, step by step
You can connect Grapevine to any Evennia channel (so you could connect it to
the default *public* channel if you like), but for testing, let's set up a
new channel `gw`.
@ccreate gw = This is connected to an gw channel!
You will automatically join the new channel.
Next we will create a connection to the Grapevine network.
@grapevine2chan gw = gossip
Evennia will now create a new connection and connect it to Grapevine. Connect
to https://grapevine.haus/chat to check.
Write something in the Evennia channel *gw* and check so a message appears in
the Grapevine chat. Write a reply in the chat and the grapevine bot should echo
it to your channel in-game.
Your Evennia gamers can now chat with users on external Grapevine channels!

View file

@ -0,0 +1,29 @@
# Guest Logins
Evennia supports *guest logins* out of the box. A guest login is an anonymous, low-access account
and can be useful if you want users to have a chance to try out your game without committing to
creating a real account.
Guest accounts are turned off by default. To activate, add this to your `game/settings.py` file:
GUEST_ENABLED = True
Henceforth users can use `connect guest` (in the default command set) to login with a guest account.
You may need to change your [Connection Screen](./Connection-Screen) to inform them of this
possibility. Guest accounts work differently from normal accounts - they are automatically *deleted*
whenever the user logs off or the server resets (but not during a reload). They are literally re-
usable throw-away accounts.
You can add a few more variables to your `settings.py` file to customize your guests:
- `BASE_GUEST_TYPECLASS` - the python-path to the default [typeclass](./Typeclasses) for guests.
Defaults to `"typeclasses.accounts.Guest"`.
- `PERMISSION_GUEST_DEFAULT` - [permission level](./Locks) for guest accounts. Defaults to `"Guests"`,
which is the lowest permission level in the hierarchy.
- `GUEST_START_LOCATION` - the `#dbref` to the starting location newly logged-in guests should
appear at. Defaults to `"#2` (Limbo).
- `GUEST_HOME` - guest home locations. Defaults to Limbo as well.
- `GUEST_LIST` - this is a list holding the possible guest names to use when entering the game. The
length of this list also sets how many guests may log in at the same time. By default this is a list
of nine names from `"Guest1"` to `"Guest9"`.

View file

@ -0,0 +1,176 @@
# HAProxy Config (Optional)
## Making Evennia, HTTPS and Secure Websockets play nicely together
This we can do by installing a _proxy_ between Evennia and the outgoing ports of your server.
Essentially,
Evennia will think it's only running locally (on localhost, IP 127.0.0.1) - the proxy will
transparently
map that to the "real" outgoing ports and handle HTTPS/WSS for us.
```
Evennia <-> (inside-visible IP/ports) <-> Proxy <-> (outside-visible IP/ports) <-> Internet
```
Here we will use [HAProxy](https://www.haproxy.org/), an open-source proxy that is easy to set up
and use. We will
also be using [LetsEncrypt](https://letsencrypt.org/getting-started/), especially the excellent
helper-program [Certbot](https://certbot.eff.org/instructions) which pretty much automates the whole
certificate setup process for us.
Before starting you also need the following:
- (optional) The host name of your game (like `myawesomegame.com`). This is something you must
previously have purchased from a _domain registrar_ and set up with DNS to point to the IP of your
server.
- If you don't have a domain name or haven't set it up yet, you must at least know the IP of your
server. Find this with `ifconfig` or similar from inside the server. If you use a hosting service
like DigitalOcean you can also find the droplet's IP address in the control panel.
- You must open port 80 in your firewall. This is used by Certbot below to auto-renew certificates.
So you can't really run another webserver alongside this setup without tweaking.
- You must open port 443 (HTTPS) in your firewall.
- You must open port 4002 (the default Websocket port) in your firewall.
## Getting certificates
Certificates guarantee that you are you. Easiest is to get this with
[Letsencrypt](https://letsencrypt.org/getting-started/) and the
[Certbot](https://certbot.eff.org/instructions) program. Certbot has a lot of install instructions
for various operating systems. Here's for Debian/Ubuntu:
```
sudo apt install certbot
```
Make sure to stop Evennia and that no port-80 using service is running, then
```
sudo certbot certonly --standalone
```
You will get some questions you need to answer, such as an email to send certificate errors to and
the host name (or IP, supposedly) to use with this certificate. After this, the certificates will
end up in `/etc/letsencrypt/live/<your-host-or-ip>/*pem` (example from Ubuntu). The critical files
for our purposes are `fullchain.pem` and `privkey.pem`.
Certbot sets up a cron-job/systemd job to regularly renew the certificate. To check this works, try
```
sudo certbot renew --dry-run
```
The certificate is only valid for 3 months at a time, so make sure this test works (it requires port
80 to be open). Look up Certbot's page for more help.
We are not quite done. HAProxy expects these two files to be _one_ file.
```
sudo cp /etc/letsencrypt/live/<your-host-or-ip>/privkey.pem /etc/letsencrypt/live/<your-host-or-
ip>/<yourhostname>.pem
sudo bash -c "cat /etc/letsencrypt/live/<your-host-or-ip>/fullchain.pem >>
/etc/letsencrypt/live/<your-host-or-ip>/<yourhostname>.pem"
```
This will create a new `.pem` file by concatenating the two files together. The `yourhostname.pem`
file (or whatever you named it) is what we will use when the the HAProxy config file (below) asks
for "your-certificate.pem".
## Installing and configuring HAProxy
Installing HaProxy is usually as simple as:
```
# Debian derivatives (Ubuntu, Mint etc)
sudo apt install haproxy
# Redhat derivatives (dnf instead of yum for very recent Fedora distros)
sudo yum install haproxy
```
Configuration of HAProxy is done in a single file. Put this wherever you like, for example in
your game dir; name it something like haproxy.conf.
Here is an example tested on Centos7 and Ubuntu. Make sure to change the file to put in your own
values.
```
# base stuff to set up haproxy
global
log /dev/log local0
chroot /var/lib/haproxy
maxconn 4000
user haproxy
tune.ssl.default-dh-param 2048
## uncomment this when everything works
# daemon
defaults
mode http
option forwardfor
# Evennia Specifics
listen evennia-https-website
bind <ip-address-or-hostname>:<public-SSL-port--probably-443> ssl no-sslv3 no-tlsv10 crt
/etc/letsencrypt/live/<your-host-or-ip>/<yourhostname>.pem
server localhost 127.0.0.1:<evennia-web-port-probably-4001>
timeout client 10m
timeout server 10m
timeout connect 5m
listen evennia-secure-websocket
bind <ip-address-or-hostname>:<wss-port--probably-4002> ssl no-sslv3 no-tlsv10 crt
/etc/letsencrypt/live/<your-host-or-ip>/<yourhostname>.pem
server localhost 127.0.0.1:<WEBSOCKET_CLIENT_PORT-probably-4002>
timeout client 10m
timeout server 10m
timeout connect 5m
```
## Putting it all together
Get back to the Evennia game dir and edit mygame/server/conf/settings.py. Add:
```
WEBSERVER_INTERFACES = ['127.0.0.1']
WEBSOCKET_CLIENT_INTERFACE = '127.0.0.1'
```
and
```
WEBSOCKET_CLIENT_URL="wss://fullhost.domain.name:4002/"
```
Make sure to reboot (stop + start) evennia completely:
```
evennia reboot
```
Finally you start the proxy:
```
sudo haproxy -f /path/to/the/above/config_file.cfg
```
Make sure you can connect to your game from your browser and that you end up with an `https://` page
and can use the websocket webclient.
Once everything works you may want to start the proxy automatically and in the background. Stop the
proxy with `Ctrl-C` and uncomment the line `# daemon` in the config file, then start the proxy again
- it will now start in the bacground.
You may also want to have the proxy start automatically; this you can do with `cron`, the inbuilt
Linux mechanism for running things at specific times.
```
sudo crontab -e
```
Choose your editor and add a new line at the end of the crontab file that opens:
```
@reboot haproxy -f /path/to/the/above/config_file.cfg
```
Save the file and haproxy should start up automatically when you reboot the server.

View file

@ -0,0 +1,467 @@
# Help System Tutorial
**Before doing this tutorial you will probably want to read the intro in [Basic Web tutorial](Web-
Tutorial).** Reading the three first parts of the [Django
tutorial](https://docs.djangoproject.com/en/2.2/) might help as well.
This tutorial will show you how to access the help system through your website. Both help commands
and regular help entries will be visible, depending on the logged-in user or an anonymous character.
This tutorial will show you how to:
- Create a new page to add to your website.
- Take advantage of a basic view and basic templates.
- Access the help system on your website.
- Identify whether the viewer of this page is logged-in and, if so, to what account.
## Creating our app
The first step is to create our new Django *app*. An app in Django can contain pages and
mechanisms: your website may contain different apps. Actually, the website provided out-of-the-box
by Evennia has already three apps: a "webclient" app, to handle the entire webclient, a "website"
app to contain your basic pages, and a third app provided by Django to create a simple admin
interface. So we'll create another app in parallel, giving it a clear name to represent our help
system.
From your game directory, use the following command:
evennia startapp help_system
> Note: calling the app "help" would have been more explicit, but this name is already used by
Django.
This will create a directory named `help_system` at the root of your game directory. It's a good
idea to keep things organized and move this directory in the "web" directory of your game. Your
game directory should look like:
mygame/
...
web/
help_system/
...
The "web/help_system" directory contains files created by Django. We'll use some of them, but if
you want to learn more about them all, you should read [the Django
tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/).
There is a last thing to be done: your folder has been added, but Django doesn't know about it, it
doesn't know it's a new app. We need to tell it, and we do so by editing a simple setting. Open
your "server/conf/settings.py" file and add, or edit, these lines:
```python
# Web configuration
INSTALLED_APPS += (
"web.help_system",
)
```
You can start Evennia if you want, and go to your website, probably at
[http://localhost:4001](http://localhost:4001) . You won't see anything different though: we added
the app but it's fairly empty.
## Our new page
At this point, our new *app* contains mostly empty files that you can explore. In order to create
a page for our help system, we need to add:
- A *view*, dealing with the logic of our page.
- A *template* to display our new page.
- A new *URL* pointing to our page.
> We could get away by creating just a view and a new URL, but that's not a recommended way to work
with your website. Building on templates is so much more convenient.
### Create a view
A *view* in Django is a simple Python function placed in the "views.py" file in your app. It will
handle the behavior that is triggered when a user asks for this information by entering a *URL* (the
connection between *views* and *URLs* will be discussed later).
So let's create our view. You can open the "web/help_system/views.py" file and paste the following
lines:
```python
from django.shortcuts import render
def index(request):
"""The 'index' view."""
return render(request, "help_system/index.html")
```
Our view handles all code logic. This time, there's not much: when this function is called, it will
render the template we will now create. But that's where we will do most of our work afterward.
### Create a template
The `render` function called into our *view* asks the *template* `help_system/index.html`. The
*templates* of our apps are stored in the app directory, "templates" sub-directory. Django may have
created the "templates" folder already. If not, create it yourself. In it, create another folder
"help_system", and inside of this folder, create a file named "index.html". Wow, that's some
hierarchy. Your directory structure (starting from `web`) should look like this:
web/
help_system/
...
templates/
help_system/
index.html
Open the "index.html" file and paste in the following lines:
```
{% extends "base.html" %}
{% block titleblock %}Help index{% endblock %}
{% block content %}
<h2>Help index</h2>
{% endblock %}
```
Here's a little explanation line by line of what this template does:
1. It loads the "base.html" *template*. This describes the basic structure of all your pages, with
a menu at the top and a footer, and perhaps other information like images and things to be present
on each page. You can create templates that do not inherit from "base.html", but you should have a
good reason for doing so.
2. The "base.html" *template* defines all the structure of the page. What is left is to override
some sections of our pages. These sections are called *blocks*. On line 2, we override the block
named "blocktitle", which contains the title of our page.
3. Same thing here, we override the *block* named "content", which contains the main content of our
web page. This block is bigger, so we define it on several lines.
4. This is perfectly normal HTML code to display a level-2 heading.
5. And finally we close the *block* named "content".
### Create a new URL
Last step to add our page: we need to add a *URL* leading to it... otherwise users won't be able to
access it. The URLs of our apps are stored in the app's directory "urls.py" file.
Open the "web/help_system/urls.py" file (you might have to create it) and write in it:
```python
# URL patterns for the help_system app
from django.urls import include, path
from web.help_system.views import index
urlpatterns = [
path(r'', index, name="help-index")
]
```
We also need to add our app as a namespace holder for URLS. Edit the file "web/urls.py" (you might
have to create this one too). In it you will find the `custom_patterns` variable. Replace it with:
```python
custom_patterns = [
path(r'help_system/', include('web.help_system.urls')),
]
```
When a user will ask for a specific *URL* on your site, Django will:
1. Read the list of custom patterns defined in "web/urls.py". There's one pattern here, which
describes to Django that all URLs beginning by 'help/' should be sent to the 'help_system' app. The
'help/' part is removed.
2. Then Django will check the "web.help_system/urls.py" file. It contains only one URL, which is
empty.
In other words, if the URL is '/help/', then Django will execute our defined view.
### Let's see it work
You can now reload or start Evennia. Open a tab in your browser and go to
[http://localhost:4001/help_system/](http://localhost:4001/help_system/) . If everything goes well,
you should see your new page... which isn't empty since Evennia uses our "base.html" *template*. In
the content of our page, there's only a heading that reads "help index". Notice that the title of
our page is "mygame - Help index" ("mygame" is replaced by the name of your game).
From now on, it will be easier to move forward and add features.
### A brief reminder
We'll be trying the following things:
- Have the help of commands and help entries accessed online.
- Have various commands and help entries depending on whether the user is logged in or not.
In terms of pages, we'll have:
- One to display the list of help topics.
- One to display the content of a help topic.
The first one would link to the second.
> Should we create two URLs?
The answer is... maybe. It depends on what you want to do. We have our help index accessible
through the "/help_system/" URL. We could have the detail of a help entry accessible through
"/help_system/desc" (to see the detail of the "desc" command). The problem is that our commands or
help topics may contain special characters that aren't to be present in URLs. There are different
ways around this problem. I have decided to use a *GET variable* here, which would create URLs like
this:
/help_system?name=desc
If you use this system, you don't have to add a new URL: GET and POST variables are accessible
through our requests and we'll see how soon enough.
## Handling logged-in users
One of our requirements is to have a help system tailored to our accounts. If an account with admin
access logs in, the page should display a lot of commands that aren't accessible to common users.
And perhaps even some additional help topics.
Fortunately, it's fairly easy to get the logged in account in our view (remember that we'll do most
of our coding there). The *request* object, passed to our function, contains a `user` attribute.
This attribute will always be there: we cannot test whether it's `None` or not, for instance. But
when the request comes from a user that isn't logged in, the `user` attribute will contain an
anonymous Django user. We then can use the `is_anonymous` method to see whether the user is logged-
in or not. Last gift by Evennia, if the user is logged in, `request.user` contains a reference to
an account object, which will help us a lot in coupling the game and online system.
So we might end up with something like:
```python
def index(request):
"""The 'index' view."""
user = request.user
if not user.is_anonymous() and user.character:
character = user.character
```
> Note: this code works when your MULTISESSION_MODE is set to 0 or 1. When it's above, you would
have something like:
```python
def index(request):
"""The 'index' view."""
user = request.user
if not user.is_anonymous() and user.db._playable_characters:
character = user.db._playable_characters[0]
```
In this second case, it will select the first character of the account.
But what if the user's not logged in? Again, we have different solutions. One of the most simple
is to create a character that will behave as our default character for the help system. You can
create it through your game: connect to it and enter:
@charcreate anonymous
The system should answer:
Created new character anonymous. Use @ic anonymous to enter the game as this character.
So in our view, we could have something like this:
```python
from typeclasses.characters import Character
def index(request):
"""The 'index' view."""
user = request.user
if not user.is_anonymous() and user.character:
character = user.character
else:
character = Character.objects.get(db_key="anonymous")
```
This time, we have a valid character no matter what: remember to adapt this code if you're running
in multisession mode above 1.
## The full system
What we're going to do is to browse through all commands and help entries, and list all the commands
that can be seen by this character (either our 'anonymous' character, or our logged-in character).
The code is longer, but it presents the entire concept in our view. Edit the
"web/help_system/views.py" file and paste into it:
```python
from django.http import Http404
from django.shortcuts import render
from evennia.help.models import HelpEntry
from typeclasses.characters import Character
def index(request):
"""The 'index' view."""
user = request.user
if not user.is_anonymous() and user.character:
character = user.character
else:
character = Character.objects.get(db_key="anonymous")
# Get the categories and topics accessible to this character
categories, topics = _get_topics(character)
# If we have the 'name' in our GET variable
topic = request.GET.get("name")
if topic:
if topic not in topics:
raise Http404("This help topic doesn't exist.")
topic = topics[topic]
context = {
"character": character,
"topic": topic,
}
return render(request, "help_system/detail.html", context)
else:
context = {
"character": character,
"categories": categories,
}
return render(request, "help_system/index.html", context)
def _get_topics(character):
"""Return the categories and topics for this character."""
cmdset = character.cmdset.all()[0]
commands = cmdset.commands
entries = [entry for entry in HelpEntry.objects.all()]
categories = {}
topics = {}
# Browse commands
for command in commands:
if not command.auto_help or not command.access(character):
continue
# Create the template for a command
template = {
"name": command.key,
"category": command.help_category,
"content": command.get_help(character, cmdset),
}
category = command.help_category
if category not in categories:
categories[category] = []
categories[category].append(template)
topics[command.key] = template
# Browse through the help entries
for entry in entries:
if not entry.access(character, 'view', default=True):
continue
# Create the template for an entry
template = {
"name": entry.key,
"category": entry.help_category,
"content": entry.entrytext,
}
category = entry.help_category
if category not in categories:
categories[category] = []
categories[category].append(template)
topics[entry.key] = template
# Sort categories
for entries in categories.values():
entries.sort(key=lambda c: c["name"])
categories = list(sorted(categories.items()))
return categories, topics
```
That's a bit more complicated here, but all in all, it can be divided in small chunks:
- The `index` function is our view:
- It begins by getting the character as we saw in the previous section.
- It gets the help topics (commands and help entries) accessible to this character. It's another
function that handles that part.
- If there's a *GET variable* "name" in our URL (like "/help?name=drop"), it will retrieve it. If
it's not a valid topic's name, it returns a *404*. Otherwise, it renders the template called
"detail.html", to display the detail of our topic.
- If there's no *GET variable* "name", render "index.html", to display the list of topics.
- The `_get_topics` is a private function. Its sole mission is to retrieve the commands a character
can execute, and the help entries this same character can see. This code is more Evennia-specific
than Django-specific, it will not be detailed in this tutorial. Just notice that all help topics
are stored in a dictionary. This is to simplify our job when displaying them in our templates.
Notice that, in both cases when we asked to render a *template*, we passed to `render` a third
argument which is the dictionary of variables used in our templates. We can pass variables this
way, and we will use them in our templates.
### The index template
Let's look at our full "index" *template*. You can open the
"web/help_system/templates/help_system/index.html" file and paste the following into it:
```
{% extends "base.html" %}
{% block titleblock %}Help index{% endblock %}
{% block content %}
<h2>Help index</h2>
{% if categories %}
{% for category, topics in categories %}
<h2>{{ category|capfirst }}</h2>
<table>
<tr>
{% for topic in topics %}
{% if forloop.counter|divisibleby:"5" %}
</tr>
<tr>
{% endif %}
<td><a href="{% url 'help-index' %}?name={{ topic.name|urlencode }}">
{{ topic.name }}</td>
{% endfor %}
</tr>
</table>
{% endfor %}
{% endif %}
{% endblock %}
```
This template is definitely more detailed. What it does is:
1. Browse through all categories.
2. For all categories, display a level-2 heading with the name of the category.
3. All topics in a category (remember, they can be either commands or help entries) are displayed in
a table. The trickier part may be that, when the loop is above 5, it will create a new line. The
table will have 5 columns at the most per row.
4. For every cell in the table, we create a link redirecting to the detail page (see below). The
URL would look something like "help?name=say". We use `urlencode` to ensure special characters are
properly escaped.
### The detail template
It's now time to show the detail of a topic (command or help entry). You can create the file
"web/help_system/templates/help_system/detail.html". You can paste into it the following code:
```
{% extends "base.html" %}
{% block titleblock %}Help for {{ topic.name }}{% endblock %}
{% block content %}
<h2>{{ topic.name|capfirst }} help topic</h2>
<p>Category: {{ topic.category|capfirst }}</p>
{{ topic.content|linebreaks }}
{% endblock %}
```
This template is much easier to read. Some *filters* might be unknown to you, but they are just
used to format here.
### Put it all together
Remember to reload or start Evennia, and then go to
[http://localhost:4001/help_system](http://localhost:4001/help_system/). You should see the list of
commands and topics accessible by all characters. Try to login (click the "login" link in the menu
of your website) and go to the same page again. You should now see a more detailed list of commands
and help entries. Click on one to see its detail.
## To improve this feature
As always, a tutorial is here to help you feel comfortable adding new features and code by yourself.
Here are some ideas of things to improve this little feature:
- Links at the bottom of the detail template to go back to the index might be useful.
- A link in the main menu to link to this page would be great... for the time being you have to
enter the URL, users won't guess it's there.
- Colors aren't handled at this point, which isn't exactly surprising. You could add it though.
- Linking help entries between one another won't be simple, but it would be great. For instance, if
you see a help entry about how to use several commands, it would be great if these commands were
themselves links to display their details.

122
docs/source/Help-System.md Normal file
View file

@ -0,0 +1,122 @@
# Help System
An important part of Evennia is the online help system. This allows the players and staff alike to
learn how to use the game's commands as well as other information pertinent to the game. The help
system has many different aspects, from the normal editing of help entries from inside the game, to
auto-generated help entries during code development using the *auto-help system*.
## Viewing the help database
The main command is `help`:
help [searchstring]
This will show a list of help entries, ordered after categories. You will find two sections,
*Command help entries* and *Other help entries* (initially you will only have the first one). You
can use help to get more info about an entry; you can also give partial matches to get suggestions.
If you give category names you will only be shown the topics in that category.
## Command Auto-help system
A common item that requires help entries are in-game commands. Keeping these entries up-to-date with
the actual source code functionality can be a chore. Evennia's commands are therefore auto-
documenting straight from the sources through its *auto-help system*. Only commands that you and
your character can actually currently use are picked up by the auto-help system. That means an admin
will see a considerably larger amount of help topics than a normal player when using the default
`help` command.
The auto-help system uses the `__doc__` strings of your command classes and formats this to a nice-
looking help entry. This makes for a very easy way to keep the help updated - just document your
commands well and updating the help file is just a `@reload` away. There is no need to manually
create and maintain help database entries for commands; as long as you keep the docstrings updated
your help will be dynamically updated for you as well.
Example (from a module with command definitions):
```python
class CmdMyCmd(Command):
"""
mycmd - my very own command
Usage:
mycmd[/switches] <args>
Switches:
test - test the command
run - do something else
This is my own command that does this and that.
"""
# [...]
help_category = "General" # default
auto_help = True # default
# [...]
```
The text at the very top of the command class definition is the class' `__doc__`-string and will be
shown to users looking for help. Try to use a consistent format - all default commands are using the
structure shown above.
You should also supply the `help_category` class property if you can; this helps to group help
entries together for people to more easily find them. See the `help` command in-game to see the
default categories. If you don't specify the category, "General" is assumed.
If you don't want your command to be picked up by the auto-help system at all (like if you want to
write its docs manually using the info in the next section or you use a [cmdset](./Command-Sets) that
has its own help functionality) you can explicitly set `auto_help` class property to `False` in your
command definition.
Alternatively, you can keep the advantages of *auto-help* in commands, but control the display of
command helps. You can do so by overriding the command's `get_help()` method. By default, this
method will return the class docstring. You could modify it to add custom behavior: the text
returned by this method will be displayed to the character asking for help in this command.
## Database help entries
These are all help entries not involving commands (this is handled automatically by the [Command
Auto-help system](Help-System#command-auto-help-system)). Non-automatic help entries describe how
your particular game is played - its rules, world descriptions and so on.
A help entry consists of four parts:
- The *topic*. This is the name of the help entry. This is what players search for when they are
looking for help. The topic can contain spaces and also partial matches will be found.
- The *help category*. Examples are *Administration*, *Building*, *Comms* or *General*. This is an
overall grouping of similar help topics, used by the engine to give a better overview.
- The *text* - the help text itself, of any length.
- locks - a [lock definition](./Locks). This can be used to limit access to this help entry, maybe
because it's staff-only or otherwise meant to be restricted. Help commands check for `access_type`s
`view` and `edit`. An example of a lock string would be `view:perm(Builders)`.
You can create new help entries in code by using `evennia.create_help_entry()`.
```python
from evennia import create_help_entry
entry = create_help_entry("emote",
"Emoting is important because ...",
category="Roleplaying", locks="view:all()")
```
From inside the game those with the right permissions can use the `@sethelp` command to add and
modify help entries.
> @sethelp/add emote = The emote command is ...
Using `@sethelp` you can add, delete and append text to existing entries. By default new entries
will go in the *General* help category. You can change this using a different form of the `@sethelp`
command:
> @sethelp/add emote, Roleplaying = Emoting is important because ...
If the category *Roleplaying* did not already exist, it is created and will appear in the help
index.
You can, finally, define a lock for the help entry by following the category with a [lock
definition](Locks):
> @sethelp/add emote, Roleplaying, view:all() = Emoting is ...

View file

@ -0,0 +1,76 @@
# How To Get And Give Help
### How to *get* Help
If you cannot find what you are looking for in the [online documentation](./index), here's what to do:
- If you think the documentation is not clear enough and are short on time, fill in our quick little
[online form][form] and let us know - no login required. Maybe the docs need to be improved or a new
tutorial added! Note that while this form is useful as a suggestion box we cannot answer questions
or reply to you. Use the discussion group or chat (linked below) if you want feedback.
- If you have trouble with a missing feature or a problem you think is a bug, go to the
[issue tracker][issues] and search to see if has been reported/suggested already. If you can't find an
existing entry create a new one.
- If you need help, want to start a discussion or get some input on something you are working on,
make a post to the [discussions group][group] This is technically a 'mailing list', but you don't
need to use e-mail; you can post and read all messages just as easily from your browser via the
online interface.
- If you want more direct discussions with developers and other users, consider dropping into our
IRC chat channel [#evennia][chat] on the *Freenode* network. Please note however that you have to be
patient if you don't get any response immediately; we are all in very different time zones and many
have busy personal lives. So you might have to hang around for a while - you'll get noticed
eventually!
### How to *give* Help
Evennia is a completely non-funded project. It relies on the time donated by its users and
developers in order to progress.
The first and easiest way you as a user can help us out is by taking part in [community
discussions][group] and by giving feedback on what is good or bad. Report bugs you find and features
you lack to our [issue tracker][issues]. Just the simple act of letting developers know you are out
there using their program is worth a lot. Generally mentioning and reviewing Evennia elsewhere is
also a nice way to spread the word.
If you'd like to help develop Evennia more hands-on, here are some ways to get going:
- Look through our [online documentation wiki](./index) and see if you
can help improve or expand the documentation (even small things like fixing typos!). You don't need
any particular permissions to edit the wiki.
- Send a message to our [discussion group][group] and/or our [IRC chat][chat] asking about what
needs doing, along with what your interests and skills are.
- Take a look at our [issue tracker][issues] and see if there's something you feel like taking on.
[here are bugs][issues-master] that need fixes. At any given time there may also be some
[bounties][issues-bounties] open - these are issues members of the community has put up money to see
fixed (if you want to put up a bounty yourself you can do so via our page on
[bountysource][bountysource]).
- Check out the [Contributing](./Contributing) page on how to practically contribute with code using
github.
... And finally, if you want to help motivate and support development you can also drop some coins
in the developer's cup. You can [make a donation via PayPal][paypal] or, even better, [become an
Evennia patron on Patreon][patreon]! This is a great way to tip your hat and show that you
appreciate the work done with the server! Finally, if you want to encourage the community to resolve
a particular
[form]: https://docs.google.com/spreadsheet/viewform?hl=en_US&formkey=dGN0VlJXMWpCT3VHaHpscDEzY1RoZG
c6MQ#gid=0
[group]: http://groups.google.com/group/evennia/
[issues]: https://github.com/evennia/evennia/issues
[issues-master]: https://github.com/evennia/evennia/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%2
0label%3Abug%20label%3Amaster-branch
[chat]: http://webchat.freenode.net/?channels=evennia
[paypal]: https://www.paypal.com/se/cgi-bin/webscr?cmd=_flow&SESSION=Z-VlOvfGjYq2qvCDOUGpb6C8Due7skT
0qOklQEy5EbaD1f0eyEQaYlmCc8O&dispatch=5885d80a13c0db1f8e263663d3faee8d64ad11bbf4d2a5a1a0d303a50933f9
b2
[donate-img]: http://images-focus-opensocial.googleusercontent.com/gadgets/proxy?url=https://www.pay
palobjects.com/en%255fUS/SE/i/btn/btn%255fdonateCC%255fLG.gif&container=focus&gadget=a&rewriteMime=i
mage/*
[patreon]: https://www.patreon.com/griatch
[patreon-img]: http://www.evennia.com/_/rsrc/1424724909023/home/evennia_patreon_100x100.png
[issues-bounties]: https://github.com/evennia/evennia/labels/bounty
[bountysource]: https://www.bountysource.com/teams/evennia

View file

@ -0,0 +1,110 @@
# How to connect Evennia to Twitter
[Twitter](http://en.wikipedia.org/wiki/twitter) is an online social networking service that enables
users to send and read short 280-character messages called "tweets". Following is a short tutorial
explaining how to enable users to send tweets from inside Evennia.
## Configuring Twitter
You must first have a Twitter account. Log in and register an App at the [Twitter Dev
Site](https://apps.twitter.com/). Make sure you enable access to "write" tweets!
To tweet from Evennia you will need both the "API Token" and the "API secret" strings as well as the
"Access Token" and "Access Secret" strings.
Twitter changed their requirements to require a Mobile number on the Twitter account to register new
apps with write access. If you're unable to do this, please see [this Dev
post](https://dev.twitter.com/notifications/new-apps-registration) which describes how to get around
it.
## Install the twitter python module
To use Twitter you must install the [Twitter](https://pypi.python.org/pypi/twitter) Python module:
```
pip install python-twitter
```
## A basic tweet command
Evennia doesn't have a `tweet` command out of the box so you need to write your own little
[Command](./Commands) in order to tweet. If you are unsure about how commands work and how to add
them, it can be an idea to go through the [Adding a Command Tutorial](./Adding-Command-Tutorial)
before continuing.
You can create the command in a separate command module (something like `mygame/commands/tweet.py`)
or together with your other custom commands, as you prefer.
This is how it can look:
```python
import twitter
from evennia import Command
# here you insert your unique App tokens
# from the Twitter dev site
TWITTER_API = twitter.Api(consumer_key='api_key',
consumer_secret='api_secret',
access_token_key='access_token_key',
access_token_secret='access_token_secret')
class CmdTweet(Command):
"""
Tweet a message
Usage:
tweet <message>
This will send a Twitter tweet to a pre-configured Twitter account.
A tweet has a maximum length of 280 characters.
"""
key = "tweet"
locks = "cmd:pperm(tweet) or pperm(Developers)"
help_category = "Comms"
def func(self):
"This performs the tweet"
caller = self.caller
tweet = self.args
if not tweet:
caller.msg("Usage: tweet <message>")
return
tlen = len(tweet)
if tlen > 280:
caller.msg("Your tweet was %i chars long (max 280)." % tlen)
return
# post the tweet
TWITTER_API.PostUpdate(tweet)
caller.msg("You tweeted:\n%s" % tweet)
```
Be sure to substitute your own actual API/Access keys and secrets in the appropriate places.
We default to limiting tweet access to players with `Developers`-level access *or* to those players
that have the permission "tweet" (allow individual characters to tweet with `@perm/player playername
= tweet`). You may change the [lock](./Locks) as you feel is appropriate. Change the overall
permission to `Players` if you want everyone to be able to tweet.
Now add this command to your default command set (e.g in `mygame/commands/defalt_cmdsets.py`") and
reload the server. From now on those with access can simply use `tweet <message>` to see the tweet
posted from the game's Twitter account.
## Next Steps
This shows only a basic tweet setup, other things to do could be:
* Auto-Adding the character name to the tweet
* More error-checking of postings
* Changing locks to make tweeting open to more people
* Echo your tweets to an in-game channel
Rather than using an explicit command you can set up a Script to send automatic tweets, for example
to post updated game stats. See the [Tweeting Game Stats tutorial](./Tutorial-Tweeting-Game-Stats) for
help.

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