mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
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:
commit
ce469f2765
590 changed files with 51168 additions and 4796 deletions
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal 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
|
||||
10
.github/ISSUE_TEMPLATE/bug-report.md
vendored
10
.github/ISSUE_TEMPLATE/bug-report.md
vendored
|
|
@ -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
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
59
.github/workflows/github_action_build_docs.yml
vendored
Normal file
59
.github/workflows/github_action_build_docs.yml
vendored
Normal 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!"
|
||||
109
.github/workflows/github_action_test_suite.yml
vendored
Normal file
109
.github/workflows/github_action_test_suite.yml
vendored
Normal 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
|
||||
|
|
@ -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'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
3
.gitignore
vendored
|
|
@ -47,3 +47,6 @@ nosetests.xml
|
|||
# Windows files generated during setup
|
||||
evennia.bat
|
||||
twistd.bat
|
||||
|
||||
# never commit docs/build
|
||||
docs/build
|
||||
|
|
|
|||
50
.travis.yml
50
.travis.yml
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
init_connect='SET collation_connection = utf8_general_ci; SET NAMES utf8;'
|
||||
66
CHANGELOG.md
66
CHANGELOG.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
136
INSTALL.md
136
INSTALL.md
|
|
@ -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).
|
||||
|
|
|
|||
8
Makefile
8
Makefile
|
|
@ -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);\
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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
145
docs/Makefile
Normal 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
496
docs/README.md
Normal 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
30
docs/checkenv.sh
Normal 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
37
docs/deploy.sh
Normal 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
35
docs/make.bat
Normal 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
|
||||
224
docs/pylib/auto_link_remapper.py
Normal file
224
docs/pylib/auto_link_remapper.py
Normal 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()
|
||||
92
docs/pylib/build_search_index.py
Normal file
92
docs/pylib/build_search_index.py
Normal 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)
|
||||
290
docs/pylib/copy_from_wiki.py
Normal file
290
docs/pylib/copy_from_wiki.py
Normal 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
11
docs/requirements.txt
Normal 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
|
||||
436
docs/source/A-voice-operated-elevator-using-events.md
Normal file
436
docs/source/A-voice-operated-elevator-using-events.md
Normal 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)
|
||||
46
docs/source/API-refactoring.md
Normal file
46
docs/source/API-refactoring.md
Normal 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
108
docs/source/Accounts.md
Normal 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.
|
||||
101
docs/source/Add-a-simple-new-web-page.md
Normal file
101
docs/source/Add-a-simple-new-web-page.md
Normal 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!
|
||||
232
docs/source/Add-a-wiki-on-your-website.md
Normal file
232
docs/source/Add-a-wiki-on-your-website.md
Normal 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!
|
||||
171
docs/source/Adding-Command-Tutorial.md
Normal file
171
docs/source/Adding-Command-Tutorial.md
Normal 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.
|
||||
109
docs/source/Adding-Object-Typeclass-Tutorial.md
Normal file
109
docs/source/Adding-Object-Typeclass-Tutorial.md
Normal 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()]
|
||||
|
||||
```
|
||||
76
docs/source/Administrative-Docs.md
Normal file
76
docs/source/Administrative-Docs.md
Normal 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
|
||||
|
||||
```
|
||||
171
docs/source/Apache-Config.md
Normal file
171
docs/source/Apache-Config.md
Normal 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).
|
||||
272
docs/source/Arxcode-installing-help.md
Normal file
272
docs/source/Arxcode-installing-help.md
Normal 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!
|
||||
234
docs/source/Async-Process.md
Normal file
234
docs/source/Async-Process.md
Normal 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
394
docs/source/Attributes.md
Normal 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
148
docs/source/Banning.md
Normal 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.
|
||||
229
docs/source/Batch-Code-Processor.md
Normal file
229
docs/source/Batch-Code-Processor.md
Normal 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.
|
||||
182
docs/source/Batch-Command-Processor.md
Normal file
182
docs/source/Batch-Command-Processor.md
Normal 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.
|
||||
82
docs/source/Batch-Processors.md
Normal file
82
docs/source/Batch-Processors.md
Normal 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.
|
||||
101
docs/source/Bootstrap-&-Evennia.md
Normal file
101
docs/source/Bootstrap-&-Evennia.md
Normal 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.
|
||||
82
docs/source/Bootstrap-Components-and-Utilities.md
Normal file
82
docs/source/Bootstrap-Components-and-Utilities.md
Normal 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)
|
||||
43
docs/source/Builder-Docs.md
Normal file
43
docs/source/Builder-Docs.md
Normal 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
|
||||
|
||||
```
|
||||
72
docs/source/Building-Permissions.md
Normal file
72
docs/source/Building-Permissions.md
Normal 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.
|
||||
274
docs/source/Building-Quickstart.md
Normal file
274
docs/source/Building-Quickstart.md
Normal 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).
|
||||
236
docs/source/Building-a-mech-tutorial.md
Normal file
236
docs/source/Building-a-mech-tutorial.md
Normal 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*.
|
||||
Let’s 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
|
||||
Evennia’s default commands may look vaguely MUX-like, you can change the syntax to look like
|
||||
whatever interface style you prefer.)
|
||||
|
||||
Before we continue, let’s 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 mech’s gun
|
||||
|
||||
Usage:
|
||||
shoot [target]
|
||||
|
||||
This will fire your mech’s 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 (let’s 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 won’t 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. Let’s 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! Let’s 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 we’ve 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 Character’s 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.
|
||||
|
||||
That’s it. When Objects of this type are created, they will always start out with the mech’s 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
|
||||
shouldn’t 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 you’ll 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.
|
||||
1233
docs/source/Building-menus.md
Normal file
1233
docs/source/Building-menus.md
Normal file
File diff suppressed because it is too large
Load diff
249
docs/source/Choosing-An-SQL-Server.md
Normal file
249
docs/source/Choosing-An-SQL-Server.md
Normal 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.
|
||||
109
docs/source/Client-Support-Grid.md
Normal file
109
docs/source/Client-Support-Grid.md
Normal 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
389
docs/source/Coding-FAQ.md
Normal 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.
|
||||
99
docs/source/Coding-Introduction.md
Normal file
99
docs/source/Coding-Introduction.md
Normal 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
297
docs/source/Coding-Utils.md
Normal 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)
|
||||
98
docs/source/Command-Cooldown.md
Normal file
98
docs/source/Command-Cooldown.md
Normal 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.
|
||||
403
docs/source/Command-Duration.md
Normal file
403
docs/source/Command-Duration.md
Normal 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).
|
||||
129
docs/source/Command-Prompt.md
Normal file
129
docs/source/Command-Prompt.md
Normal 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
376
docs/source/Command-Sets.md
Normal 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.
|
||||
9
docs/source/Command-System.md
Normal file
9
docs/source/Command-System.md
Normal 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
663
docs/source/Commands.md
Normal 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.
|
||||
113
docs/source/Communications.md
Normal file
113
docs/source/Communications.md
Normal 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`.
|
||||
36
docs/source/Connection-Screen.md
Normal file
36
docs/source/Connection-Screen.md
Normal 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.
|
||||
222
docs/source/Continuous-Integration.md
Normal file
222
docs/source/Continuous-Integration.md
Normal 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!
|
||||
679
docs/source/Contributing-Docs.md
Normal file
679
docs/source/Contributing-Docs.md
Normal 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
118
docs/source/Contributing.md
Normal 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
349
docs/source/Coordinates.md
Normal 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.
|
||||
239
docs/source/Custom-Protocols.md
Normal file
239
docs/source/Custom-Protocols.md
Normal 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.
|
||||
484
docs/source/Customize-channels.md
Normal file
484
docs/source/Customize-channels.md
Normal 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
296
docs/source/Debugging.md
Normal 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/).
|
||||
2643
docs/source/Default-Command-Help.md
Normal file
2643
docs/source/Default-Command-Help.md
Normal file
File diff suppressed because it is too large
Load diff
122
docs/source/Default-Exit-Errors.md
Normal file
122
docs/source/Default-Exit-Errors.md
Normal 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.
|
||||
170
docs/source/Developer-Central.md
Normal file
170
docs/source/Developer-Central.md
Normal 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
|
||||
|
||||
```
|
||||
248
docs/source/Dialogues-in-events.md
Normal file
248
docs/source/Dialogues-in-events.md
Normal 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).
|
||||
68
docs/source/Directory-Overview.md
Normal file
68
docs/source/Directory-Overview.md
Normal 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.
|
||||
117
docs/source/Docs-refactoring.md
Normal file
117
docs/source/Docs-refactoring.md
Normal 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.
|
||||
495
docs/source/Dynamic-In-Game-Map.md
Normal file
495
docs/source/Dynamic-In-Game-Map.md
Normal 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
181
docs/source/EvEditor.md
Normal 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
1312
docs/source/EvMenu.md
Normal file
File diff suppressed because it is too large
Load diff
38
docs/source/EvMore.md
Normal file
38
docs/source/EvMore.md
Normal 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.
|
||||
|
||||
97
docs/source/Evennia-API.md
Normal file
97
docs/source/Evennia-API.md
Normal 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))
|
||||
71
docs/source/Evennia-Game-Index.md
Normal file
71
docs/source/Evennia-Game-Index.md
Normal 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.
|
||||
178
docs/source/Evennia-Introduction.md
Normal file
178
docs/source/Evennia-Introduction.md
Normal 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.
|
||||
200
docs/source/Evennia-for-Diku-Users.md
Normal file
200
docs/source/Evennia-for-Diku-Users.md
Normal 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.
|
||||
|
||||
|
||||
|
||||
|
||||
222
docs/source/Evennia-for-MUSH-Users.md
Normal file
222
docs/source/Evennia-for-MUSH-Users.md
Normal 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 Evennia’s `desc` command updates your description and that’s 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
|
||||
you’ll find the file `default_cmdsets.py`. In Python lingo all `*.py` files are called *modules*.
|
||||
Open the module in a text editor. We won’t 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* doesn’t 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.
|
||||
|
||||
Here’s 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 (it’s 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)!
|
||||
732
docs/source/Evennia-for-roleplaying-sessions.md
Normal file
732
docs/source/Evennia-for-roleplaying-sessions.md
Normal 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.
|
||||
120
docs/source/Execute-Python-Code.md
Normal file
120
docs/source/Execute-Python-Code.md
Normal 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.
|
||||
292
docs/source/First-Steps-Coding.md
Normal file
292
docs/source/First-Steps-Coding.md
Normal 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.
|
||||
218
docs/source/Game-Planning.md
Normal file
218
docs/source/Game-Planning.md
Normal 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!
|
||||
302
docs/source/Gametime-Tutorial.md
Normal file
302
docs/source/Gametime-Tutorial.md
Normal 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.
|
||||
539
docs/source/Getting-Started.md
Normal file
539
docs/source/Getting-Started.md
Normal 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
372
docs/source/Glossary.md
Normal 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
71
docs/source/Grapevine.md
Normal 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!
|
||||
29
docs/source/Guest-Logins.md
Normal file
29
docs/source/Guest-Logins.md
Normal 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"`.
|
||||
176
docs/source/HAProxy-Config.md
Normal file
176
docs/source/HAProxy-Config.md
Normal 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.
|
||||
467
docs/source/Help-System-Tutorial.md
Normal file
467
docs/source/Help-System-Tutorial.md
Normal 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
122
docs/source/Help-System.md
Normal 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 ...
|
||||
76
docs/source/How-To-Get-And-Give-Help.md
Normal file
76
docs/source/How-To-Get-And-Give-Help.md
Normal 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
|
||||
|
||||
|
||||
110
docs/source/How-to-connect-Evennia-to-Twitter.md
Normal file
110
docs/source/How-to-connect-Evennia-to-Twitter.md
Normal 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
Loading…
Add table
Add a link
Reference in a new issue