mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 12:56:30 +01:00
Updated HTML docs.
This commit is contained in:
parent
59e50f3fa5
commit
06bc3c8bcd
663 changed files with 2 additions and 61705 deletions
|
|
@ -1,29 +0,0 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2019-, Evennia
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
164
docs/Makefile
164
docs/Makefile
|
|
@ -1,164 +0,0 @@
|
|||
# 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 -d 6 --separate --templatedir=$(SOURCEDIR)/_templates/
|
||||
SPHINXAPIDOCOPTSQUICK = --tocfile evennia-api --module-first -d 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 doc build requirements"
|
||||
@echo " $(cblue)clean$(cnorm) to remove remnants of a previous build"
|
||||
@echo " $(cblue)quick$(cnorm) to build local docs but skip the autodocs (for quick testing)"
|
||||
@echo " $(cblue)quickstrict$(cnorm) to build like 'quick' but abort immediately on any error"
|
||||
@echo " $(cblue)local$(cnorm) to build local html docs of the current branch (no multiversion)."
|
||||
@echo " $(cblue)localupdate$(cnorm) to build local html docs (only update changes)"
|
||||
@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
|
||||
pylib/api_rst2md.py
|
||||
|
||||
_quick_autodoc-index:
|
||||
@EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) SPHINX_APIDOC_OPTIONS=$(SPHINXAPIDOCENV) $(SPHINXAPIDOC) $(SPHINXAPIDOCOPTSQUICK) -o $(SOURCEDIR)/api/ $(EVDIR) $(SPHINXAPIDOCEXCLUDE)
|
||||
|
||||
_multiversion-autodoc-index:
|
||||
make _autodoc-index
|
||||
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:
|
||||
@python deploy.py
|
||||
# @bash -e deploy.sh
|
||||
|
||||
_latex-build:
|
||||
@NOAUTODOC=1 EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) $(SPHINXBUILD) -M latexpdf "$(SOURCEDIR)" "$(BUILDDIR)/latex" $(QUICKFILES)
|
||||
|
||||
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 _check-env
|
||||
make _quick_autodoc-index
|
||||
make _html-build
|
||||
@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 (not working atm)
|
||||
quickstrict:
|
||||
SPHINXOPTS=-W make quick
|
||||
|
||||
# we build index directly for the current branch
|
||||
local:
|
||||
make _check-env
|
||||
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."
|
||||
|
||||
# build only that which updated since last run (no clean or index-creation)
|
||||
localupdate:
|
||||
make _check-env
|
||||
make _html-build
|
||||
@echo ""
|
||||
@echo "Documentation built (single version, only updates, no auto-index)."
|
||||
@echo "To see result, open evennia/docs/build/html/index.html in a browser."
|
||||
|
||||
# note that this should be done for each relevant multiversion branch.
|
||||
mv-index:
|
||||
make _multiversion-autodoc-index
|
||||
@echo "(Re)Built and committed api rst files for this branch only."
|
||||
|
||||
mv-local:
|
||||
make _multiversion-check-env
|
||||
make _multiversion-build
|
||||
@echo ""
|
||||
@echo "Documentation built (multiversion + autodocs)."
|
||||
@echo "To see result, open evennia/docs/build/html/<version>/index.html in a browser."
|
||||
|
||||
# note - don't run deploy/release manually, the result will clash with the
|
||||
# result of the github actions!
|
||||
deploy:
|
||||
make _multiversion-deploy
|
||||
@echo "Documentation deployed."
|
||||
|
||||
# build and prepare the docs for release
|
||||
release:
|
||||
make mv-local
|
||||
make deploy
|
||||
@echo "Release complete."
|
||||
476
docs/README.md
476
docs/README.md
|
|
@ -1,476 +0,0 @@
|
|||
# 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 primarily 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 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.
|
||||
This action 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. To build the full documentation 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 make 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 for some reason you 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 (the font will be smaller
|
||||
and smaller).
|
||||
|
||||
- `# 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 give a warning during documentation compilation. This warning
|
||||
> 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
|
||||
|
||||
Code examples are a special case - we want them to get code-highlighting for readability. This is done by using
|
||||
the triple-backticks and specifying the language we use:
|
||||
|
||||
````
|
||||
```python
|
||||
|
||||
def a_python_func(x):
|
||||
return x * x
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
## ReST blocks
|
||||
|
||||
Markdown is easy to read and use, but it isn't as expressive as it needs to be for some things. 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.
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
````
|
||||
|
||||
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 that are 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 the 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
|
||||
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
|
||||
|
||||
Tables are done using Markdown syntax
|
||||
|
||||
```
|
||||
| A | B | A and B |
|
||||
| --- | --- | --- |
|
||||
| False | False | False |
|
||||
| True | False | False |
|
||||
| False | True | False |
|
||||
| True | True | True |
|
||||
```
|
||||
|
||||
| A | B | A and B |
|
||||
| --- | --- | --- |
|
||||
| False | False | False |
|
||||
| True | False | False |
|
||||
| False | True | False |
|
||||
| True | True | True |
|
||||
|
||||
|
||||
#### 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 [MyST][MyST] 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/
|
||||
[MyST]: https://myst-parser.readthedocs.io/en/latest/syntax/reference.html
|
||||
[commonmark]: https://spec.commonmark.org/current/
|
||||
[commonmark-help]: https://commonmark.org/help/
|
||||
[sphinx-autodoc]: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#module-sphinx.ext.autodoc
|
||||
[sphinx-napoleon]: https://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
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
|
||||
# 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
|
||||
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
"""
|
||||
Deploy to github, from github Action. This is run after the docs have finished building. All new
|
||||
documentation branches will be available in build/html/* at this point. We need to copy those
|
||||
contents to the root of the repo.
|
||||
|
||||
This can be tested with `make release` or `make deploy` and require git push rights to
|
||||
the evennia repo. Use DISABLE_GIT_PUSH for local testing - git-pushing from local can cause
|
||||
clashes upstream.
|
||||
|
||||
We will look in source/conf.py for the `.latest_version` string and `.legacy_versions` list,
|
||||
this allows us to skip deleting legacy docs (which may be ever harder to build) while correctly
|
||||
symlinking to the current 'latest' documentation.
|
||||
|
||||
This is assumed to be executed from inside the docs/ folder.
|
||||
|
||||
"""
|
||||
|
||||
import glob
|
||||
import importlib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# set for local testing
|
||||
DISABLE_GIT_PUSH = False
|
||||
|
||||
|
||||
def deploy():
|
||||
"""Perform the deploy of the built Evennia documentation to the gh-pages branch."""
|
||||
|
||||
conf_file = importlib.machinery.SourceFileLoader("conf", "source/conf.py").load_module()
|
||||
|
||||
latest_version = conf_file.latest_version
|
||||
legacy_versions = conf_file.legacy_versions
|
||||
|
||||
if subprocess.call(["git", "status", "--untracked=no", "--porcelain"]):
|
||||
print(
|
||||
"There are uncommitted or untracked changes. Make sure "
|
||||
"to commit everything in your current branch first."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# get the deployment branch
|
||||
os.system("git fetch")
|
||||
os.system("git checkout gh-pages")
|
||||
|
||||
os.system("pwd")
|
||||
os.system("ls")
|
||||
|
||||
names_to_skip = legacy_versions + ["build"]
|
||||
|
||||
for file_path in glob.glob("*"):
|
||||
# run from inside the docs/ dir
|
||||
# delete old but active doc branches
|
||||
|
||||
if file_path in names_to_skip:
|
||||
# skip deleting the legacy brancehs
|
||||
continue
|
||||
else:
|
||||
# we want to delete both active branches and old symlinks
|
||||
os.system(f"rm -Rf {file_path}")
|
||||
print(f"removed file_path: {file_path}")
|
||||
|
||||
# copy built branches to current dir
|
||||
os.system("ls")
|
||||
|
||||
os.system("cp -Rf build/html/* .")
|
||||
|
||||
os.system("ls")
|
||||
|
||||
# symlink to latest and link its index to the root
|
||||
os.system(f"ln -s {latest_version} latest")
|
||||
os.system(f"ln -s {latest_version}/index.html .")
|
||||
|
||||
os.system("ls")
|
||||
|
||||
if not DISABLE_GIT_PUSH:
|
||||
print("committing and pushing docs ...")
|
||||
os.system("git add .") # docs/build is in .gitignore so will be skipped
|
||||
os.system('git commit -a -m "Updated HTML docs."')
|
||||
os.system("git push origin gh-pages")
|
||||
else:
|
||||
print("Skipped git push.")
|
||||
|
||||
print("Deployed to https:// evennia.github.io/evennia/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
deploy()
|
||||
1
docs/index.html
Symbolic link
1
docs/index.html
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
0.9.5/index.html
|
||||
1
docs/latest
Symbolic link
1
docs/latest
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
0.9.5
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
@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
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
"""
|
||||
Remap autodoc API rst files to md files and wrap their contents.
|
||||
|
||||
"""
|
||||
|
||||
from glob import glob
|
||||
from os import rename
|
||||
from os.path import abspath, dirname
|
||||
from os.path import join as pathjoin
|
||||
|
||||
|
||||
def _rst2md(filename_rst):
|
||||
|
||||
with open(filename_rst, "r") as fil:
|
||||
# read rst file, reformat and save
|
||||
txt = fil.read()
|
||||
with open(filename_rst, "w") as fil:
|
||||
txt = "```{eval-rst}\n" + txt + "\n```"
|
||||
fil.write(txt)
|
||||
|
||||
# rename .rst file to .md file
|
||||
filename, _ = filename_rst.rsplit(".", 1)
|
||||
filename_md = filename + ".md"
|
||||
rename(filename_rst, filename_md)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
apidir = pathjoin(dirname(dirname(abspath(__file__))), "source", "api")
|
||||
for filename_rst in glob(pathjoin(apidir, "*.rst")):
|
||||
_rst2md(filename_rst)
|
||||
print(" Converted {apidir}/*.rst files to .md files".format(apidir=apidir))
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
"""
|
||||
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 os.path import abspath, dirname
|
||||
from os.path import join as pathjoin
|
||||
from os.path import relpath
|
||||
from pathlib import Path
|
||||
|
||||
from sphinx.errors import DocumentError
|
||||
|
||||
_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:",
|
||||
"feature-request",
|
||||
"report-bug",
|
||||
"issue",
|
||||
"bug-report",
|
||||
]
|
||||
# remove these prefixes from the url
|
||||
_STRIP_PREFIX = [
|
||||
"../../api/",
|
||||
"../api/",
|
||||
"./api/",
|
||||
"api/",
|
||||
"api:",
|
||||
]
|
||||
TXT_REMAPS = {
|
||||
"Developer Central": "Evennia Components overview",
|
||||
"Getting Started": "Setup Quickstart",
|
||||
}
|
||||
URL_REMAPS = {
|
||||
"Developer-Central": "Components/Components-Overview",
|
||||
"Tutorials": "Howtos/Howtos-Overview",
|
||||
"../Howtos/Beginner-Tutorial/Directory-Overview": "Gamedir-Overview",
|
||||
"Howtos/Beginner-Tutorial/Directory-Overview": "Gamedir-Overview",
|
||||
"Beginner-Tutorial/Directory-Overview": "Gamedir-Overview",
|
||||
"Directory-Overview": "Gamedir-Overview",
|
||||
"../Setup/Getting-Started": "Setup-Quickstart",
|
||||
"Setup/Getting-Started": "Setup-Quickstart",
|
||||
"Getting-Started": "Setup-Quickstart",
|
||||
"First-Steps-Coding": "Beginner-Tutorial-Part1",
|
||||
"../Howtos/Beginner-Tutorial/Adding-Command-Tutorial": "Adding-Commands",
|
||||
"Howtos/Beginner-Tutorial/Adding-Command-Tutorial": "Adding-Commands",
|
||||
"Beginner-Tutorial/Adding-Command-Tutorial": "Adding-Commands",
|
||||
"Adding-Command-Tutorial": "Adding-Commands",
|
||||
"CmdSet": "Command-Sets",
|
||||
"Spawner": "Prototypes",
|
||||
"issue": "github:issue",
|
||||
"issues": "github:issue",
|
||||
"bug": "github:issue",
|
||||
"bug-report": "github:issue",
|
||||
"./Default-Command-Help": "api:evennia.commands.default#modules",
|
||||
"../Components/Default-Command-Help": "api:evennia.commands.default#modules",
|
||||
"../../../Components/Default-Command-Help": "api:evennia.commands.default#modules",
|
||||
"./Locks.md#permissions": "Permissions",
|
||||
"modules": "Default-Commands.md",
|
||||
}
|
||||
|
||||
_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>[\n\w -\[\]\`]+?)\]\((?P<url>.+?)\)", re.I + re.S + re.U + re.M
|
||||
)
|
||||
# in document references
|
||||
ref_doc_regex = re.compile(
|
||||
r"\[(?P<txt>[\n\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)
|
||||
|
||||
for strip_prefix in _STRIP_PREFIX:
|
||||
if url.startswith(strip_prefix):
|
||||
url = url[len(strip_prefix) :]
|
||||
|
||||
if any(url.startswith(noremap) for noremap in _NO_REMAP_STARTSWITH):
|
||||
# skip regular http/s urls etc
|
||||
return f"[{txt}]({url})"
|
||||
|
||||
if url.startswith("evennia."):
|
||||
# api link - we want to remove legacy #reference and remove .md
|
||||
if "#" in url:
|
||||
_, url = url.rsplit("#", 1)
|
||||
if url.endswith(".md"):
|
||||
url, _ = url.rsplit(".", 1)
|
||||
return f"[{txt}]({url})"
|
||||
|
||||
fname, *part = url.rsplit("/", 1)
|
||||
fname = part[0] if part else fname
|
||||
fname, *anchor = fname.rsplit("#", 1)
|
||||
if ".md" in fname:
|
||||
fname = fname.rsplit(".", 1)[0]
|
||||
|
||||
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] + ".md" + ("#" + anchor[0].lower() 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)
|
||||
|
||||
for strip_prefix in _STRIP_PREFIX:
|
||||
if url.startswith(strip_prefix):
|
||||
url = url[len(strip_prefix) :]
|
||||
|
||||
if any(url.startswith(noremap) for noremap in _NO_REMAP_STARTSWITH):
|
||||
return f"[{txt}]: {url}"
|
||||
|
||||
if "http" in url and "://" in url:
|
||||
urlout = url
|
||||
elif url.startswith("evennia."):
|
||||
# api link - we want to remove legacy #reference
|
||||
if "#" in url:
|
||||
_, urlout = url.rsplit("#", 1)
|
||||
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 and not src_url.startswith("api/"):
|
||||
print(f" ORPHANED DOC: no refs found to {src_url}.md")
|
||||
|
||||
# write tocfile
|
||||
# with open(_TOC_FILE, "w") as fil:
|
||||
# fil.write("```{toctree}\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 not "/" in ref:
|
||||
# # ref = "./" + ref
|
||||
|
||||
# # linkname = ref.replace("-", " ")
|
||||
# fil.write(f"\n{ref}") # - [{linkname}]({ref})")
|
||||
|
||||
# # we add a self-reference so the toc itself is also a part of a toctree
|
||||
# fil.write("\n```\n\n```{toctree}\n :hidden:\n\ntoc\n```")
|
||||
# print(" -- File toc.md updated.")
|
||||
|
||||
print(" -- Auto-Remapper finished.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
auto_link_remapper()
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Builds a lunr static search index for optimized search
|
||||
|
||||
"""
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
from os.path import abspath, dirname
|
||||
from os.path import join as joinpath
|
||||
from os.path import sep
|
||||
|
||||
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)
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
"""
|
||||
Convert contribs' README files to proper documentation pages along with
|
||||
an index.
|
||||
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from glob import glob
|
||||
from os.path import abspath, dirname
|
||||
from os.path import join as pathjoin
|
||||
from os.path import sep
|
||||
|
||||
_EVENNIA_PATH = pathjoin(dirname(dirname(dirname(abspath(__file__)))))
|
||||
_DOCS_PATH = pathjoin(_EVENNIA_PATH, "docs")
|
||||
|
||||
_SOURCE_DIR = pathjoin(_EVENNIA_PATH, "evennia", "contrib")
|
||||
_OUT_DIR = pathjoin(_DOCS_PATH, "source", "Contribs")
|
||||
_OUT_INDEX_FILE = pathjoin(_OUT_DIR, "Contribs-Overview.md")
|
||||
|
||||
_FILE_STRUCTURE = """{header}
|
||||
{categories}
|
||||
{footer}"""
|
||||
|
||||
_CATEGORY_DESCS = {
|
||||
"base_systems": """
|
||||
This category contains systems that are not necessarily tied to a specific
|
||||
in-game mechanic but is useful for the game as a whole. Examples include
|
||||
login systems, new command syntaxes, and build helpers.
|
||||
""",
|
||||
"full_systems": """
|
||||
This category contains 'complete' game engines that can be used directly
|
||||
to start creating content without no further additions (unless you want to).
|
||||
""",
|
||||
"game_systems": """
|
||||
This category holds code implementing in-game gameplay systems like
|
||||
crafting, mail, combat and more. Each system is meant to be adopted
|
||||
piecemeal and adopted for your game. This does not include
|
||||
roleplaying-specific systems, those are found in the `rpg` folder.
|
||||
""",
|
||||
"grid": """
|
||||
Systems related to the game world's topology and structure. This has
|
||||
contribs related to rooms, exits and map building.
|
||||
""",
|
||||
"rpg": """
|
||||
These are systems specifically related to roleplaying
|
||||
and rule implementation like character traits, dice rolling and emoting.
|
||||
""",
|
||||
"tutorials": """
|
||||
Helper resources specifically meant to teach a development concept or
|
||||
to exemplify an Evennia system. Any extra resources tied to documentation
|
||||
tutorials are found here. Also the home of the Tutorial World demo adventure.
|
||||
""",
|
||||
"utils": """
|
||||
Miscellaneous, optional tools for manipulating text, auditing connections
|
||||
and more.
|
||||
""",
|
||||
}
|
||||
|
||||
|
||||
_FILENAME_MAP = {"rpsystem": "RPSystem", "xyzgrid": "XYZGrid", "awsstorage": "AWSStorage"}
|
||||
|
||||
HEADER = """# Contribs
|
||||
|
||||
_Contribs_ are optional code snippets and systems contributed by
|
||||
the Evennia community. They vary in size and complexity and
|
||||
may be more specific about game types and styles than 'core' Evennia.
|
||||
This page is auto-generated and summarizes all contribs currently included.
|
||||
|
||||
All contrib categories are imported from `evennia.contrib`, such as
|
||||
|
||||
from evennia.contrib.base_systems import building_menu
|
||||
|
||||
Each contrib contains installation instructions for how to integrate it
|
||||
with your other code. If you want to tweak the code of a contrib, just
|
||||
copy its entire folder to your game directory and modify/use it from there.
|
||||
|
||||
> Hint: Additional (potentially un-maintained) code snippets from the community can be found
|
||||
in our discussion forum's [Community Contribs & Snippets](https://github.com/evennia/evennia/discussions/categories/community-contribs-snippets) category.
|
||||
|
||||
If you want to contribute yourself, see [here](Contributing)!
|
||||
"""
|
||||
|
||||
|
||||
TOCTREE = """```{{toctree}}
|
||||
:maxdepth: 1
|
||||
|
||||
{listing}
|
||||
```"""
|
||||
|
||||
CATEGORY = """
|
||||
## {category}
|
||||
|
||||
_{category_desc}_
|
||||
|
||||
{toctree}
|
||||
|
||||
{blurbs}
|
||||
|
||||
|
||||
"""
|
||||
|
||||
BLURB = """
|
||||
### Contrib: `{name}`
|
||||
|
||||
_{credits}_
|
||||
|
||||
{blurb}
|
||||
|
||||
[Read the documentation](./{filename}) - [Browse the Code](api:{code_location})
|
||||
|
||||
"""
|
||||
|
||||
FOOTER = """
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `{path}`. Changes to this
|
||||
file will be overwritten, so edit that file rather than this one.</small>
|
||||
"""
|
||||
|
||||
INDEX_FOOTER = """
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is auto-generated. Manual changes
|
||||
will be overwritten.</small>
|
||||
"""
|
||||
|
||||
|
||||
def readmes2docs(directory=_SOURCE_DIR):
|
||||
"""
|
||||
Parse directory for README files and convert them to doc pages.
|
||||
|
||||
"""
|
||||
|
||||
ncount = 0
|
||||
categories = defaultdict(list)
|
||||
|
||||
glob_path = f"{directory}{sep}*{sep}*{sep}README.md"
|
||||
|
||||
for file_path in glob(glob_path):
|
||||
# paths are e.g. evennia/contrib/utils/auditing/README.md
|
||||
_, category, name, _ = file_path.rsplit(sep, 3)
|
||||
|
||||
pypath = f"evennia.contrib.{category}.{name}"
|
||||
|
||||
filename = (
|
||||
"Contrib-"
|
||||
+ "-".join(
|
||||
_FILENAME_MAP.get(part, part.capitalize() if part[0].islower() else part)
|
||||
for part in name.split("_")
|
||||
)
|
||||
+ ".md"
|
||||
)
|
||||
outfile = pathjoin(_OUT_DIR, filename)
|
||||
|
||||
with open(file_path) as fil:
|
||||
data = fil.read()
|
||||
|
||||
clean_file_path = f"evennia{sep}contrib{file_path[len(directory):]}"
|
||||
data += FOOTER.format(path=clean_file_path)
|
||||
|
||||
try:
|
||||
credits = data.split("\n\n", 3)[1]
|
||||
blurb = data.split("\n\n", 3)[2]
|
||||
except IndexError:
|
||||
blurb = name
|
||||
|
||||
with open(outfile, "w") as fil:
|
||||
fil.write(data)
|
||||
|
||||
categories[category].append((name, credits, blurb, filename, pypath))
|
||||
ncount += 1
|
||||
|
||||
# build the index with blurbs
|
||||
|
||||
category_sections = []
|
||||
for category in sorted(categories):
|
||||
filenames = []
|
||||
contrib_tups = categories[category]
|
||||
catlines = []
|
||||
for tup in sorted(contrib_tups, key=lambda tup: tup[0].lower()):
|
||||
catlines.append(
|
||||
BLURB.format(
|
||||
name=tup[0], credits=tup[1], blurb=tup[2], filename=tup[3], code_location=tup[4]
|
||||
)
|
||||
)
|
||||
filenames.append(f"{tup[3]}")
|
||||
toctree = TOCTREE.format(listing="\n".join(filenames))
|
||||
category_sections.append(
|
||||
CATEGORY.format(
|
||||
category=category,
|
||||
category_desc=_CATEGORY_DESCS[category].strip(),
|
||||
blurbs="\n".join(catlines),
|
||||
toctree=toctree,
|
||||
)
|
||||
)
|
||||
|
||||
text = _FILE_STRUCTURE.format(
|
||||
header=HEADER, categories="\n".join(category_sections), footer=INDEX_FOOTER
|
||||
)
|
||||
|
||||
with open(_OUT_INDEX_FILE, "w") as fil:
|
||||
fil.write(text)
|
||||
|
||||
print(f" -- Converted Contrib READMEs to {ncount} doc pages + index.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
readmes2docs(_SOURCE_DIR)
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
#!/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 datetime
|
||||
import glob
|
||||
import re
|
||||
|
||||
_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.9 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")
|
||||
)
|
||||
text = "\n".join(text)
|
||||
|
||||
if not is_index:
|
||||
text = f"# {title}\n\n{text}"
|
||||
|
||||
with open(outfile, "w") as fil:
|
||||
fil.write(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("This should not be run on develop files, it would overwrite changes.")
|
||||
# create_toctree(_INFILES)
|
||||
# convert_links(_INFILES, _OUTDIR)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
#!/usr/bin python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Format given files to a max width.
|
||||
|
||||
Usage:
|
||||
python fmtwidth.py --width 79 ../source/**.md
|
||||
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import textwrap
|
||||
|
||||
_DEFAULT_WIDTH = 100
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("files")
|
||||
parser.add_argument("-w", "--width", dest="width", type=int, default=_DEFAULT_WIDTH)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
filepaths = glob.glob(args.files, recursive=True)
|
||||
width = args.width
|
||||
|
||||
wrapper = textwrap.TextWrapper(
|
||||
width=width,
|
||||
break_long_words=False,
|
||||
expand_tabs=True,
|
||||
)
|
||||
|
||||
count = 0
|
||||
for filepath in filepaths:
|
||||
with open(filepath, "r") as fil:
|
||||
lines = fil.readlines()
|
||||
|
||||
outlines = [
|
||||
"\n".join(wrapper.wrap(line)) if len(line) > width else line.strip("\n")
|
||||
for line in lines
|
||||
]
|
||||
txt = "\n".join(outlines)
|
||||
with open(filepath, "w") as fil:
|
||||
fil.write(txt)
|
||||
count += 1
|
||||
|
||||
print(f"Wrapped {count} files.")
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
"""
|
||||
|
||||
Generates Components/Default-Commands.md from sources.
|
||||
|
||||
To test - import this from a Django-aware shell, then call run_update.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from os.path import abspath, dirname
|
||||
from os.path import join as pathjoin
|
||||
|
||||
from evennia.utils.utils import (callables_from_module, mod_import,
|
||||
variable_from_module)
|
||||
|
||||
__all__ = "run_update"
|
||||
|
||||
|
||||
PAGE = """
|
||||
# Default Commands
|
||||
|
||||
The full set of default Evennia commands currently contains {ncommands} commands in {nfiles} source
|
||||
files. Our policy for adding default commands is outlined [here](Using-MUX-as-a-Standard). The
|
||||
[Commands](Commands) documentation explains how Commands work as well as how to make new or customize
|
||||
existing ones.
|
||||
|
||||
> Note that this page is auto-generated. Report problems to the [issue tracker](github:issues).
|
||||
|
||||
```{{note}}
|
||||
Some game-states add their own Commands which are not listed here. Examples include editing a text
|
||||
with [EvEditor](EvEditor), flipping pages in [EvMore](EvMore) or using the
|
||||
[Batch-Processor](Batch-Processors)'s interactive mode.
|
||||
```
|
||||
|
||||
{alphabetical}
|
||||
|
||||
""".strip()
|
||||
|
||||
|
||||
def run_update(no_autodoc=False):
|
||||
|
||||
if no_autodoc:
|
||||
return
|
||||
|
||||
cmdsets = (
|
||||
("evennia.commands.default.cmdset_character", "CharacterCmdSet"),
|
||||
("evennia.commands.default.cmdset_account", "AccountCmdSet"),
|
||||
("evennia.commands.default.cmdset_unloggedin", "UnloggedinCmdSet"),
|
||||
("evennia.commands.default.cmdset_session", "SessionCmdSet"),
|
||||
)
|
||||
cmd_modules = (
|
||||
"evennia.commands.default.account",
|
||||
"evennia.commands.default.batchprocess",
|
||||
"evennia.commands.default.building",
|
||||
"evennia.commands.default.comms",
|
||||
"evennia.commands.default.general",
|
||||
"evennia.commands.default.help",
|
||||
"evennia.commands.default.syscommandsyyp",
|
||||
"evennia.commands.default.system",
|
||||
"evennia.commands.default.unloggedin",
|
||||
)
|
||||
|
||||
cmds_per_cmdset = {}
|
||||
cmd_to_cmdset_map = {}
|
||||
for modname, cmdsetname in cmdsets:
|
||||
cmdset = variable_from_module(modname, variable=cmdsetname)()
|
||||
cmdset.at_cmdset_creation()
|
||||
cmds_per_cmdset[cmdsetname] = cmdset.commands
|
||||
for cmd in cmdset.commands:
|
||||
cmd_to_cmdset_map[f"{cmd.__module__}.{cmd.__class__.__name__}"] = cmdset
|
||||
|
||||
cmds_per_module = {}
|
||||
cmd_to_module_map = {}
|
||||
cmds_alphabetically = []
|
||||
for modname in cmd_modules:
|
||||
module = mod_import(modname)
|
||||
cmds_per_module[module] = [
|
||||
cmd for cmd in callables_from_module(module).values() if cmd.__name__.startswith("Cmd")
|
||||
]
|
||||
for cmd in cmds_per_module[module]:
|
||||
cmd_to_module_map[cmd] = module
|
||||
cmds_alphabetically.append(cmd)
|
||||
cmds_alphabetically = list(sorted(cmds_alphabetically, key=lambda c: c.key))
|
||||
|
||||
cmd_infos = []
|
||||
for cmd in cmds_alphabetically:
|
||||
aliases = [
|
||||
alias[1:] if alias and alias[0] == "@" else alias for alias in sorted(cmd.aliases)
|
||||
]
|
||||
aliases = f" [{', '.join(sorted(cmd.aliases))}]" if aliases else ""
|
||||
cmdlink = f"[**{cmd.key}**{aliases}](api:{cmd.__module__}#{cmd.__name__})"
|
||||
category = f"help-category: _{cmd.help_category.capitalize()}_"
|
||||
cmdset = cmd_to_cmdset_map.get(f"{cmd.__module__}.{cmd.__name__}", None)
|
||||
if cmdset:
|
||||
cmodule = cmdset.__module__
|
||||
cname = cmdset.__class__.__name__
|
||||
cmdsetlink = f"cmdset: [{cname}](api:{cmodule}#{cname}), "
|
||||
else:
|
||||
# we skip commands not in the default cmdsets
|
||||
continue
|
||||
|
||||
cmd_infos.append(f"{cmdlink} ({cmdsetlink}{category})")
|
||||
|
||||
txt = PAGE.format(
|
||||
ncommands=len(cmd_to_cmdset_map),
|
||||
nfiles=len(cmds_per_module),
|
||||
alphabetical="\n".join(f"- {info}" for info in cmd_infos),
|
||||
)
|
||||
|
||||
outdir = pathjoin(dirname(dirname(abspath(__file__))), "source", "Components")
|
||||
fname = pathjoin(outdir, "Default-Commands.md")
|
||||
|
||||
with open(fname, "w") as fil:
|
||||
fil.write(txt)
|
||||
|
||||
print(" -- Updated Default Command index.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_update()
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
"""
|
||||
Update dynamically generated doc pages based on github sources.
|
||||
|
||||
"""
|
||||
|
||||
from os.path import abspath, dirname
|
||||
from os.path import join as pathjoin
|
||||
|
||||
ROOTDIR = dirname(dirname(dirname(abspath(__file__))))
|
||||
DOCDIR = pathjoin(ROOTDIR, "docs")
|
||||
DOCSRCDIR = pathjoin(DOCDIR, "source")
|
||||
EVENNIADIR = pathjoin(ROOTDIR, "evennia")
|
||||
|
||||
|
||||
def update_changelog():
|
||||
"""
|
||||
Plain CHANGELOG copy
|
||||
|
||||
"""
|
||||
|
||||
sourcefile = pathjoin(ROOTDIR, "CHANGELOG.md")
|
||||
targetfile = pathjoin(DOCSRCDIR, "Coding", "Changelog.md")
|
||||
|
||||
with open(sourcefile) as fil:
|
||||
txt = fil.read()
|
||||
|
||||
with open(targetfile, "w") as fil:
|
||||
fil.write(txt)
|
||||
|
||||
print(" -- Updated Changelog.md")
|
||||
|
||||
|
||||
def update_default_settings():
|
||||
"""
|
||||
Make a copy of the default settings file for easy reference in docs
|
||||
|
||||
"""
|
||||
|
||||
sourcefile = pathjoin(EVENNIADIR, "settings_default.py")
|
||||
targetfile = pathjoin(DOCSRCDIR, "Setup", "Settings-Default.md")
|
||||
|
||||
with open(sourcefile) as fil:
|
||||
txt = fil.read()
|
||||
|
||||
txt = f"""
|
||||
# Evennia Default settings file
|
||||
|
||||
Master file is located at `evennia/evennia/settings_default.py`. Read
|
||||
its comments to see what each setting does and copy only what you want
|
||||
to change into `mygame/server/conf/settings.py`.
|
||||
|
||||
Example of accessing settings:
|
||||
|
||||
```
|
||||
from django.conf import settings
|
||||
|
||||
if settings.SERVERNAME == "Evennia":
|
||||
print("Yay!")
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
```python
|
||||
{txt}
|
||||
```
|
||||
"""
|
||||
with open(targetfile, "w") as fil:
|
||||
fil.write(txt)
|
||||
|
||||
print(" -- Updated Settings-Default.md")
|
||||
|
||||
|
||||
def update_dynamic_pages():
|
||||
"""
|
||||
Run the various updaters
|
||||
|
||||
"""
|
||||
update_changelog()
|
||||
update_default_settings()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_dynamic_pages()
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# requirements for building the docs
|
||||
|
||||
sphinx==3.2.1
|
||||
myst-parser==0.15.2
|
||||
myst-parser[linkify]==0.15.2
|
||||
Jinja2 < 3.1
|
||||
|
||||
# sphinx-multiversion with evennia fixes
|
||||
git+https://github.com/evennia/sphinx-multiversion.git@evennia-mods#egg=sphinx-multiversion
|
||||
|
||||
# sphinxcontrib-lunrsearch custom branch with evennia-specific fixes
|
||||
# git+https://github.com/evennia/sphinxcontrib-lunrsearch.git@evennia-mods#egg=sphinxcontrib-lunrsearch
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
StylesPath = .vale
|
||||
|
||||
Vocab = docs
|
||||
Packages = write-good
|
||||
MinAlertLevel = error
|
||||
|
||||
[*.md]
|
||||
BasedOnStyles = Vale, write-good
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
Evennia
|
||||
Pastebin
|
||||
[Cc]ontrib(s)*
|
||||
Patreon
|
||||
[Rr]epo(s)*
|
||||
(?i)readme
|
||||
[Ss]ubfolder(s)*
|
||||
[Dd]ev(s)*
|
||||
Github
|
||||
[Dd]ocstring(s)*
|
||||
[Mm]ygame(s)*
|
||||
[Gg]amedir(s)*
|
||||
[Vv]irtualenv(s)*
|
||||
Python
|
||||
API
|
||||
[Tt]ypeclass(es)*?
|
||||
[Bb]ullet point(s)*
|
||||
CommonMark
|
||||
[Pp]reparser(s)*
|
||||
|
|
@ -1,702 +0,0 @@
|
|||
extends: existence
|
||||
message: "Try to avoid using clichés like '%s'."
|
||||
ignorecase: true
|
||||
level: warning
|
||||
tokens:
|
||||
- a chip off the old block
|
||||
- a clean slate
|
||||
- a dark and stormy night
|
||||
- a far cry
|
||||
- a fine kettle of fish
|
||||
- a loose cannon
|
||||
- a penny saved is a penny earned
|
||||
- a tough row to hoe
|
||||
- a word to the wise
|
||||
- ace in the hole
|
||||
- acid test
|
||||
- add insult to injury
|
||||
- against all odds
|
||||
- air your dirty laundry
|
||||
- all fun and games
|
||||
- all in a day's work
|
||||
- all talk, no action
|
||||
- all thumbs
|
||||
- all your eggs in one basket
|
||||
- all's fair in love and war
|
||||
- all's well that ends well
|
||||
- almighty dollar
|
||||
- American as apple pie
|
||||
- an axe to grind
|
||||
- another day, another dollar
|
||||
- armed to the teeth
|
||||
- as luck would have it
|
||||
- as old as time
|
||||
- as the crow flies
|
||||
- at loose ends
|
||||
- at my wits end
|
||||
- avoid like the plague
|
||||
- babe in the woods
|
||||
- back against the wall
|
||||
- back in the saddle
|
||||
- back to square one
|
||||
- back to the drawing board
|
||||
- bad to the bone
|
||||
- badge of honor
|
||||
- bald faced liar
|
||||
- ballpark figure
|
||||
- banging your head against a brick wall
|
||||
- baptism by fire
|
||||
- barking up the wrong tree
|
||||
- bat out of hell
|
||||
- be all and end all
|
||||
- beat a dead horse
|
||||
- beat around the bush
|
||||
- been there, done that
|
||||
- beggars can't be choosers
|
||||
- behind the eight ball
|
||||
- bend over backwards
|
||||
- benefit of the doubt
|
||||
- bent out of shape
|
||||
- best thing since sliced bread
|
||||
- bet your bottom dollar
|
||||
- better half
|
||||
- better late than never
|
||||
- better mousetrap
|
||||
- better safe than sorry
|
||||
- between a rock and a hard place
|
||||
- beyond the pale
|
||||
- bide your time
|
||||
- big as life
|
||||
- big cheese
|
||||
- big fish in a small pond
|
||||
- big man on campus
|
||||
- bigger they are the harder they fall
|
||||
- bird in the hand
|
||||
- bird's eye view
|
||||
- birds and the bees
|
||||
- birds of a feather flock together
|
||||
- bit the hand that feeds you
|
||||
- bite the bullet
|
||||
- bite the dust
|
||||
- bitten off more than he can chew
|
||||
- black as coal
|
||||
- black as pitch
|
||||
- black as the ace of spades
|
||||
- blast from the past
|
||||
- bleeding heart
|
||||
- blessing in disguise
|
||||
- blind ambition
|
||||
- blind as a bat
|
||||
- blind leading the blind
|
||||
- blood is thicker than water
|
||||
- blood sweat and tears
|
||||
- blow off steam
|
||||
- blow your own horn
|
||||
- blushing bride
|
||||
- boils down to
|
||||
- bolt from the blue
|
||||
- bone to pick
|
||||
- bored stiff
|
||||
- bored to tears
|
||||
- bottomless pit
|
||||
- boys will be boys
|
||||
- bright and early
|
||||
- brings home the bacon
|
||||
- broad across the beam
|
||||
- broken record
|
||||
- brought back to reality
|
||||
- bull by the horns
|
||||
- bull in a china shop
|
||||
- burn the midnight oil
|
||||
- burning question
|
||||
- burning the candle at both ends
|
||||
- burst your bubble
|
||||
- bury the hatchet
|
||||
- busy as a bee
|
||||
- by hook or by crook
|
||||
- call a spade a spade
|
||||
- called onto the carpet
|
||||
- calm before the storm
|
||||
- can of worms
|
||||
- can't cut the mustard
|
||||
- can't hold a candle to
|
||||
- case of mistaken identity
|
||||
- cat got your tongue
|
||||
- cat's meow
|
||||
- caught in the crossfire
|
||||
- caught red-handed
|
||||
- checkered past
|
||||
- chomping at the bit
|
||||
- cleanliness is next to godliness
|
||||
- clear as a bell
|
||||
- clear as mud
|
||||
- close to the vest
|
||||
- cock and bull story
|
||||
- cold shoulder
|
||||
- come hell or high water
|
||||
- cool as a cucumber
|
||||
- cool, calm, and collected
|
||||
- cost a king's ransom
|
||||
- count your blessings
|
||||
- crack of dawn
|
||||
- crash course
|
||||
- creature comforts
|
||||
- cross that bridge when you come to it
|
||||
- crushing blow
|
||||
- cry like a baby
|
||||
- cry me a river
|
||||
- cry over spilt milk
|
||||
- crystal clear
|
||||
- curiosity killed the cat
|
||||
- cut and dried
|
||||
- cut through the red tape
|
||||
- cut to the chase
|
||||
- cute as a bugs ear
|
||||
- cute as a button
|
||||
- cute as a puppy
|
||||
- cuts to the quick
|
||||
- dark before the dawn
|
||||
- day in, day out
|
||||
- dead as a doornail
|
||||
- devil is in the details
|
||||
- dime a dozen
|
||||
- divide and conquer
|
||||
- dog and pony show
|
||||
- dog days
|
||||
- dog eat dog
|
||||
- dog tired
|
||||
- don't burn your bridges
|
||||
- don't count your chickens
|
||||
- don't look a gift horse in the mouth
|
||||
- don't rock the boat
|
||||
- don't step on anyone's toes
|
||||
- don't take any wooden nickels
|
||||
- down and out
|
||||
- down at the heels
|
||||
- down in the dumps
|
||||
- down the hatch
|
||||
- down to earth
|
||||
- draw the line
|
||||
- dressed to kill
|
||||
- dressed to the nines
|
||||
- drives me up the wall
|
||||
- dull as dishwater
|
||||
- dyed in the wool
|
||||
- eagle eye
|
||||
- ear to the ground
|
||||
- early bird catches the worm
|
||||
- easier said than done
|
||||
- easy as pie
|
||||
- eat your heart out
|
||||
- eat your words
|
||||
- eleventh hour
|
||||
- even the playing field
|
||||
- every dog has its day
|
||||
- every fiber of my being
|
||||
- everything but the kitchen sink
|
||||
- eye for an eye
|
||||
- face the music
|
||||
- facts of life
|
||||
- fair weather friend
|
||||
- fall by the wayside
|
||||
- fan the flames
|
||||
- feast or famine
|
||||
- feather your nest
|
||||
- feathered friends
|
||||
- few and far between
|
||||
- fifteen minutes of fame
|
||||
- filthy vermin
|
||||
- fine kettle of fish
|
||||
- fish out of water
|
||||
- fishing for a compliment
|
||||
- fit as a fiddle
|
||||
- fit the bill
|
||||
- fit to be tied
|
||||
- flash in the pan
|
||||
- flat as a pancake
|
||||
- flip your lid
|
||||
- flog a dead horse
|
||||
- fly by night
|
||||
- fly the coop
|
||||
- follow your heart
|
||||
- for all intents and purposes
|
||||
- for the birds
|
||||
- for what it's worth
|
||||
- force of nature
|
||||
- force to be reckoned with
|
||||
- forgive and forget
|
||||
- fox in the henhouse
|
||||
- free and easy
|
||||
- free as a bird
|
||||
- fresh as a daisy
|
||||
- full steam ahead
|
||||
- fun in the sun
|
||||
- garbage in, garbage out
|
||||
- gentle as a lamb
|
||||
- get a kick out of
|
||||
- get a leg up
|
||||
- get down and dirty
|
||||
- get the lead out
|
||||
- get to the bottom of
|
||||
- get your feet wet
|
||||
- gets my goat
|
||||
- gilding the lily
|
||||
- give and take
|
||||
- go against the grain
|
||||
- go at it tooth and nail
|
||||
- go for broke
|
||||
- go him one better
|
||||
- go the extra mile
|
||||
- go with the flow
|
||||
- goes without saying
|
||||
- good as gold
|
||||
- good deed for the day
|
||||
- good things come to those who wait
|
||||
- good time was had by all
|
||||
- good times were had by all
|
||||
- greased lightning
|
||||
- greek to me
|
||||
- green thumb
|
||||
- green-eyed monster
|
||||
- grist for the mill
|
||||
- growing like a weed
|
||||
- hair of the dog
|
||||
- hand to mouth
|
||||
- happy as a clam
|
||||
- happy as a lark
|
||||
- hasn't a clue
|
||||
- have a nice day
|
||||
- have high hopes
|
||||
- have the last laugh
|
||||
- haven't got a row to hoe
|
||||
- head honcho
|
||||
- head over heels
|
||||
- hear a pin drop
|
||||
- heard it through the grapevine
|
||||
- heart's content
|
||||
- heavy as lead
|
||||
- hem and haw
|
||||
- high and dry
|
||||
- high and mighty
|
||||
- high as a kite
|
||||
- hit paydirt
|
||||
- hold your head up high
|
||||
- hold your horses
|
||||
- hold your own
|
||||
- hold your tongue
|
||||
- honest as the day is long
|
||||
- horns of a dilemma
|
||||
- horse of a different color
|
||||
- hot under the collar
|
||||
- hour of need
|
||||
- I beg to differ
|
||||
- icing on the cake
|
||||
- if the shoe fits
|
||||
- if the shoe were on the other foot
|
||||
- in a jam
|
||||
- in a jiffy
|
||||
- in a nutshell
|
||||
- in a pig's eye
|
||||
- in a pinch
|
||||
- in a word
|
||||
- in hot water
|
||||
- in the gutter
|
||||
- in the nick of time
|
||||
- in the thick of it
|
||||
- in your dreams
|
||||
- it ain't over till the fat lady sings
|
||||
- it goes without saying
|
||||
- it takes all kinds
|
||||
- it takes one to know one
|
||||
- it's a small world
|
||||
- it's only a matter of time
|
||||
- ivory tower
|
||||
- Jack of all trades
|
||||
- jockey for position
|
||||
- jog your memory
|
||||
- joined at the hip
|
||||
- judge a book by its cover
|
||||
- jump down your throat
|
||||
- jump in with both feet
|
||||
- jump on the bandwagon
|
||||
- jump the gun
|
||||
- jump to conclusions
|
||||
- just a hop, skip, and a jump
|
||||
- just the ticket
|
||||
- justice is blind
|
||||
- keep a stiff upper lip
|
||||
- keep an eye on
|
||||
- keep it simple, stupid
|
||||
- keep the home fires burning
|
||||
- keep up with the Joneses
|
||||
- keep your chin up
|
||||
- keep your fingers crossed
|
||||
- kick the bucket
|
||||
- kick up your heels
|
||||
- kick your feet up
|
||||
- kid in a candy store
|
||||
- kill two birds with one stone
|
||||
- kiss of death
|
||||
- knock it out of the park
|
||||
- knock on wood
|
||||
- knock your socks off
|
||||
- know him from Adam
|
||||
- know the ropes
|
||||
- know the score
|
||||
- knuckle down
|
||||
- knuckle sandwich
|
||||
- knuckle under
|
||||
- labor of love
|
||||
- ladder of success
|
||||
- land on your feet
|
||||
- lap of luxury
|
||||
- last but not least
|
||||
- last hurrah
|
||||
- last-ditch effort
|
||||
- law of the jungle
|
||||
- law of the land
|
||||
- lay down the law
|
||||
- leaps and bounds
|
||||
- let sleeping dogs lie
|
||||
- let the cat out of the bag
|
||||
- let the good times roll
|
||||
- let your hair down
|
||||
- let's talk turkey
|
||||
- letter perfect
|
||||
- lick your wounds
|
||||
- lies like a rug
|
||||
- life's a bitch
|
||||
- life's a grind
|
||||
- light at the end of the tunnel
|
||||
- lighter than a feather
|
||||
- lighter than air
|
||||
- like clockwork
|
||||
- like father like son
|
||||
- like taking candy from a baby
|
||||
- like there's no tomorrow
|
||||
- lion's share
|
||||
- live and learn
|
||||
- live and let live
|
||||
- long and short of it
|
||||
- long lost love
|
||||
- look before you leap
|
||||
- look down your nose
|
||||
- look what the cat dragged in
|
||||
- looking a gift horse in the mouth
|
||||
- looks like death warmed over
|
||||
- loose cannon
|
||||
- lose your head
|
||||
- lose your temper
|
||||
- loud as a horn
|
||||
- lounge lizard
|
||||
- loved and lost
|
||||
- low man on the totem pole
|
||||
- luck of the draw
|
||||
- luck of the Irish
|
||||
- make hay while the sun shines
|
||||
- make money hand over fist
|
||||
- make my day
|
||||
- make the best of a bad situation
|
||||
- make the best of it
|
||||
- make your blood boil
|
||||
- man of few words
|
||||
- man's best friend
|
||||
- mark my words
|
||||
- meaningful dialogue
|
||||
- missed the boat on that one
|
||||
- moment in the sun
|
||||
- moment of glory
|
||||
- moment of truth
|
||||
- money to burn
|
||||
- more power to you
|
||||
- more than one way to skin a cat
|
||||
- movers and shakers
|
||||
- moving experience
|
||||
- naked as a jaybird
|
||||
- naked truth
|
||||
- neat as a pin
|
||||
- needle in a haystack
|
||||
- needless to say
|
||||
- neither here nor there
|
||||
- never look back
|
||||
- never say never
|
||||
- nip and tuck
|
||||
- nip it in the bud
|
||||
- no guts, no glory
|
||||
- no love lost
|
||||
- no pain, no gain
|
||||
- no skin off my back
|
||||
- no stone unturned
|
||||
- no time like the present
|
||||
- no use crying over spilled milk
|
||||
- nose to the grindstone
|
||||
- not a hope in hell
|
||||
- not a minute's peace
|
||||
- not in my backyard
|
||||
- not playing with a full deck
|
||||
- not the end of the world
|
||||
- not written in stone
|
||||
- nothing to sneeze at
|
||||
- nothing ventured nothing gained
|
||||
- now we're cooking
|
||||
- off the top of my head
|
||||
- off the wagon
|
||||
- off the wall
|
||||
- old hat
|
||||
- older and wiser
|
||||
- older than dirt
|
||||
- older than Methuselah
|
||||
- on a roll
|
||||
- on cloud nine
|
||||
- on pins and needles
|
||||
- on the bandwagon
|
||||
- on the money
|
||||
- on the nose
|
||||
- on the rocks
|
||||
- on the spot
|
||||
- on the tip of my tongue
|
||||
- on the wagon
|
||||
- on thin ice
|
||||
- once bitten, twice shy
|
||||
- one bad apple doesn't spoil the bushel
|
||||
- one born every minute
|
||||
- one brick short
|
||||
- one foot in the grave
|
||||
- one in a million
|
||||
- one red cent
|
||||
- only game in town
|
||||
- open a can of worms
|
||||
- open and shut case
|
||||
- open the flood gates
|
||||
- opportunity doesn't knock twice
|
||||
- out of pocket
|
||||
- out of sight, out of mind
|
||||
- out of the frying pan into the fire
|
||||
- out of the woods
|
||||
- out on a limb
|
||||
- over a barrel
|
||||
- over the hump
|
||||
- pain and suffering
|
||||
- pain in the
|
||||
- panic button
|
||||
- par for the course
|
||||
- part and parcel
|
||||
- party pooper
|
||||
- pass the buck
|
||||
- patience is a virtue
|
||||
- pay through the nose
|
||||
- penny pincher
|
||||
- perfect storm
|
||||
- pig in a poke
|
||||
- pile it on
|
||||
- pillar of the community
|
||||
- pin your hopes on
|
||||
- pitter patter of little feet
|
||||
- plain as day
|
||||
- plain as the nose on your face
|
||||
- play by the rules
|
||||
- play your cards right
|
||||
- playing the field
|
||||
- playing with fire
|
||||
- pleased as punch
|
||||
- plenty of fish in the sea
|
||||
- point with pride
|
||||
- poor as a church mouse
|
||||
- pot calling the kettle black
|
||||
- pretty as a picture
|
||||
- pull a fast one
|
||||
- pull your punches
|
||||
- pulling your leg
|
||||
- pure as the driven snow
|
||||
- put it in a nutshell
|
||||
- put one over on you
|
||||
- put the cart before the horse
|
||||
- put the pedal to the metal
|
||||
- put your best foot forward
|
||||
- put your foot down
|
||||
- quick as a bunny
|
||||
- quick as a lick
|
||||
- quick as a wink
|
||||
- quick as lightning
|
||||
- quiet as a dormouse
|
||||
- rags to riches
|
||||
- raining buckets
|
||||
- raining cats and dogs
|
||||
- rank and file
|
||||
- rat race
|
||||
- reap what you sow
|
||||
- red as a beet
|
||||
- red herring
|
||||
- reinvent the wheel
|
||||
- rich and famous
|
||||
- rings a bell
|
||||
- ripe old age
|
||||
- ripped me off
|
||||
- rise and shine
|
||||
- road to hell is paved with good intentions
|
||||
- rob Peter to pay Paul
|
||||
- roll over in the grave
|
||||
- rub the wrong way
|
||||
- ruled the roost
|
||||
- running in circles
|
||||
- sad but true
|
||||
- sadder but wiser
|
||||
- salt of the earth
|
||||
- scared stiff
|
||||
- scared to death
|
||||
- sealed with a kiss
|
||||
- second to none
|
||||
- see eye to eye
|
||||
- seen the light
|
||||
- seize the day
|
||||
- set the record straight
|
||||
- set the world on fire
|
||||
- set your teeth on edge
|
||||
- sharp as a tack
|
||||
- shoot for the moon
|
||||
- shoot the breeze
|
||||
- shot in the dark
|
||||
- shoulder to the wheel
|
||||
- sick as a dog
|
||||
- sigh of relief
|
||||
- signed, sealed, and delivered
|
||||
- sink or swim
|
||||
- six of one, half a dozen of another
|
||||
- skating on thin ice
|
||||
- slept like a log
|
||||
- slinging mud
|
||||
- slippery as an eel
|
||||
- slow as molasses
|
||||
- smart as a whip
|
||||
- smooth as a baby's bottom
|
||||
- sneaking suspicion
|
||||
- snug as a bug in a rug
|
||||
- sow wild oats
|
||||
- spare the rod, spoil the child
|
||||
- speak of the devil
|
||||
- spilled the beans
|
||||
- spinning your wheels
|
||||
- spitting image of
|
||||
- spoke with relish
|
||||
- spread like wildfire
|
||||
- spring to life
|
||||
- squeaky wheel gets the grease
|
||||
- stands out like a sore thumb
|
||||
- start from scratch
|
||||
- stick in the mud
|
||||
- still waters run deep
|
||||
- stitch in time
|
||||
- stop and smell the roses
|
||||
- straight as an arrow
|
||||
- straw that broke the camel's back
|
||||
- strong as an ox
|
||||
- stubborn as a mule
|
||||
- stuff that dreams are made of
|
||||
- stuffed shirt
|
||||
- sweating blood
|
||||
- sweating bullets
|
||||
- take a load off
|
||||
- take one for the team
|
||||
- take the bait
|
||||
- take the bull by the horns
|
||||
- take the plunge
|
||||
- takes one to know one
|
||||
- takes two to tango
|
||||
- the more the merrier
|
||||
- the real deal
|
||||
- the real McCoy
|
||||
- the red carpet treatment
|
||||
- the same old story
|
||||
- there is no accounting for taste
|
||||
- thick as a brick
|
||||
- thick as thieves
|
||||
- thin as a rail
|
||||
- think outside of the box
|
||||
- third time's the charm
|
||||
- this day and age
|
||||
- this hurts me worse than it hurts you
|
||||
- this point in time
|
||||
- three sheets to the wind
|
||||
- through thick and thin
|
||||
- throw in the towel
|
||||
- tie one on
|
||||
- tighter than a drum
|
||||
- time and time again
|
||||
- time is of the essence
|
||||
- tip of the iceberg
|
||||
- tired but happy
|
||||
- to coin a phrase
|
||||
- to each his own
|
||||
- to make a long story short
|
||||
- to the best of my knowledge
|
||||
- toe the line
|
||||
- tongue in cheek
|
||||
- too good to be true
|
||||
- too hot to handle
|
||||
- too numerous to mention
|
||||
- touch with a ten foot pole
|
||||
- tough as nails
|
||||
- trial and error
|
||||
- trials and tribulations
|
||||
- tried and true
|
||||
- trip down memory lane
|
||||
- twist of fate
|
||||
- two cents worth
|
||||
- two peas in a pod
|
||||
- ugly as sin
|
||||
- under the counter
|
||||
- under the gun
|
||||
- under the same roof
|
||||
- under the weather
|
||||
- until the cows come home
|
||||
- unvarnished truth
|
||||
- up the creek
|
||||
- uphill battle
|
||||
- upper crust
|
||||
- upset the applecart
|
||||
- vain attempt
|
||||
- vain effort
|
||||
- vanquish the enemy
|
||||
- vested interest
|
||||
- waiting for the other shoe to drop
|
||||
- wakeup call
|
||||
- warm welcome
|
||||
- watch your p's and q's
|
||||
- watch your tongue
|
||||
- watching the clock
|
||||
- water under the bridge
|
||||
- weather the storm
|
||||
- weed them out
|
||||
- week of Sundays
|
||||
- went belly up
|
||||
- wet behind the ears
|
||||
- what goes around comes around
|
||||
- what you see is what you get
|
||||
- when it rains, it pours
|
||||
- when push comes to shove
|
||||
- when the cat's away
|
||||
- when the going gets tough, the tough get going
|
||||
- white as a sheet
|
||||
- whole ball of wax
|
||||
- whole hog
|
||||
- whole nine yards
|
||||
- wild goose chase
|
||||
- will wonders never cease?
|
||||
- wisdom of the ages
|
||||
- wise as an owl
|
||||
- wolf at the door
|
||||
- words fail me
|
||||
- work like a dog
|
||||
- world weary
|
||||
- worst nightmare
|
||||
- worth its weight in gold
|
||||
- wrong side of the bed
|
||||
- yanking your chain
|
||||
- yappy as a dog
|
||||
- years young
|
||||
- you are what you eat
|
||||
- you can run but you can't hide
|
||||
- you only live once
|
||||
- you're the boss
|
||||
- young and foolish
|
||||
- young and vibrant
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
extends: existence
|
||||
message: "Try to avoid using '%s'."
|
||||
ignorecase: true
|
||||
level: suggestion
|
||||
tokens:
|
||||
- am
|
||||
- are
|
||||
- aren't
|
||||
- be
|
||||
- been
|
||||
- being
|
||||
- he's
|
||||
- here's
|
||||
- here's
|
||||
- how's
|
||||
- i'm
|
||||
- is
|
||||
- isn't
|
||||
- it's
|
||||
- she's
|
||||
- that's
|
||||
- there's
|
||||
- they're
|
||||
- was
|
||||
- wasn't
|
||||
- we're
|
||||
- were
|
||||
- weren't
|
||||
- what's
|
||||
- where's
|
||||
- who's
|
||||
- you're
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
extends: repetition
|
||||
message: "'%s' is repeated!"
|
||||
level: warning
|
||||
alpha: true
|
||||
action:
|
||||
name: edit
|
||||
params:
|
||||
- truncate
|
||||
- " "
|
||||
tokens:
|
||||
- '[^\s]+'
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
extends: existence
|
||||
message: "'%s' may be passive voice. Use active voice if you can."
|
||||
ignorecase: true
|
||||
level: warning
|
||||
raw:
|
||||
- \b(am|are|were|being|is|been|was|be)\b\s*
|
||||
tokens:
|
||||
- '[\w]+ed'
|
||||
- awoken
|
||||
- beat
|
||||
- become
|
||||
- been
|
||||
- begun
|
||||
- bent
|
||||
- beset
|
||||
- bet
|
||||
- bid
|
||||
- bidden
|
||||
- bitten
|
||||
- bled
|
||||
- blown
|
||||
- born
|
||||
- bought
|
||||
- bound
|
||||
- bred
|
||||
- broadcast
|
||||
- broken
|
||||
- brought
|
||||
- built
|
||||
- burnt
|
||||
- burst
|
||||
- cast
|
||||
- caught
|
||||
- chosen
|
||||
- clung
|
||||
- come
|
||||
- cost
|
||||
- crept
|
||||
- cut
|
||||
- dealt
|
||||
- dived
|
||||
- done
|
||||
- drawn
|
||||
- dreamt
|
||||
- driven
|
||||
- drunk
|
||||
- dug
|
||||
- eaten
|
||||
- fallen
|
||||
- fed
|
||||
- felt
|
||||
- fit
|
||||
- fled
|
||||
- flown
|
||||
- flung
|
||||
- forbidden
|
||||
- foregone
|
||||
- forgiven
|
||||
- forgotten
|
||||
- forsaken
|
||||
- fought
|
||||
- found
|
||||
- frozen
|
||||
- given
|
||||
- gone
|
||||
- gotten
|
||||
- ground
|
||||
- grown
|
||||
- heard
|
||||
- held
|
||||
- hidden
|
||||
- hit
|
||||
- hung
|
||||
- hurt
|
||||
- kept
|
||||
- knelt
|
||||
- knit
|
||||
- known
|
||||
- laid
|
||||
- lain
|
||||
- leapt
|
||||
- learnt
|
||||
- led
|
||||
- left
|
||||
- lent
|
||||
- let
|
||||
- lighted
|
||||
- lost
|
||||
- made
|
||||
- meant
|
||||
- met
|
||||
- misspelt
|
||||
- mistaken
|
||||
- mown
|
||||
- overcome
|
||||
- overdone
|
||||
- overtaken
|
||||
- overthrown
|
||||
- paid
|
||||
- pled
|
||||
- proven
|
||||
- put
|
||||
- quit
|
||||
- read
|
||||
- rid
|
||||
- ridden
|
||||
- risen
|
||||
- run
|
||||
- rung
|
||||
- said
|
||||
- sat
|
||||
- sawn
|
||||
- seen
|
||||
- sent
|
||||
- set
|
||||
- sewn
|
||||
- shaken
|
||||
- shaven
|
||||
- shed
|
||||
- shod
|
||||
- shone
|
||||
- shorn
|
||||
- shot
|
||||
- shown
|
||||
- shrunk
|
||||
- shut
|
||||
- slain
|
||||
- slept
|
||||
- slid
|
||||
- slit
|
||||
- slung
|
||||
- smitten
|
||||
- sold
|
||||
- sought
|
||||
- sown
|
||||
- sped
|
||||
- spent
|
||||
- spilt
|
||||
- spit
|
||||
- split
|
||||
- spoken
|
||||
- spread
|
||||
- sprung
|
||||
- spun
|
||||
- stolen
|
||||
- stood
|
||||
- stridden
|
||||
- striven
|
||||
- struck
|
||||
- strung
|
||||
- stuck
|
||||
- stung
|
||||
- stunk
|
||||
- sung
|
||||
- sunk
|
||||
- swept
|
||||
- swollen
|
||||
- sworn
|
||||
- swum
|
||||
- swung
|
||||
- taken
|
||||
- taught
|
||||
- thought
|
||||
- thrived
|
||||
- thrown
|
||||
- thrust
|
||||
- told
|
||||
- torn
|
||||
- trodden
|
||||
- understood
|
||||
- upheld
|
||||
- upset
|
||||
- wed
|
||||
- wept
|
||||
- withheld
|
||||
- withstood
|
||||
- woken
|
||||
- won
|
||||
- worn
|
||||
- wound
|
||||
- woven
|
||||
- written
|
||||
- wrung
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
Based on [write-good](https://github.com/btford/write-good).
|
||||
|
||||
> Naive linter for English prose for developers who can't write good and wanna learn to do other stuff good too.
|
||||
|
||||
```
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Brian Ford
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
extends: existence
|
||||
message: "Don't start a sentence with '%s'."
|
||||
level: error
|
||||
raw:
|
||||
- '(?:[;-]\s)so[\s,]|\bSo[\s,]'
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
extends: existence
|
||||
message: "Don't start a sentence with '%s'."
|
||||
ignorecase: false
|
||||
level: error
|
||||
raw:
|
||||
- '(?:[;-]\s)There\s(is|are)|\bThere\s(is|are)\b'
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
extends: existence
|
||||
message: "'%s' is too wordy."
|
||||
ignorecase: true
|
||||
level: warning
|
||||
tokens:
|
||||
- a number of
|
||||
- abundance
|
||||
- accede to
|
||||
- accelerate
|
||||
- accentuate
|
||||
- accompany
|
||||
- accomplish
|
||||
- accorded
|
||||
- accrue
|
||||
- acquiesce
|
||||
- acquire
|
||||
- additional
|
||||
- adjacent to
|
||||
- adjustment
|
||||
- admissible
|
||||
- advantageous
|
||||
- adversely impact
|
||||
- advise
|
||||
- aforementioned
|
||||
- aggregate
|
||||
- aircraft
|
||||
- all of
|
||||
- all things considered
|
||||
- alleviate
|
||||
- allocate
|
||||
- along the lines of
|
||||
- already existing
|
||||
- alternatively
|
||||
- amazing
|
||||
- ameliorate
|
||||
- anticipate
|
||||
- apparent
|
||||
- appreciable
|
||||
- as a matter of fact
|
||||
- as a means of
|
||||
- as far as I'm concerned
|
||||
- as of yet
|
||||
- as to
|
||||
- as yet
|
||||
- ascertain
|
||||
- assistance
|
||||
- at the present time
|
||||
- at this time
|
||||
- attain
|
||||
- attributable to
|
||||
- authorize
|
||||
- because of the fact that
|
||||
- belated
|
||||
- benefit from
|
||||
- bestow
|
||||
- by means of
|
||||
- by virtue of
|
||||
- by virtue of the fact that
|
||||
- cease
|
||||
- close proximity
|
||||
- commence
|
||||
- comply with
|
||||
- concerning
|
||||
- consequently
|
||||
- consolidate
|
||||
- constitutes
|
||||
- demonstrate
|
||||
- depart
|
||||
- designate
|
||||
- discontinue
|
||||
- due to the fact that
|
||||
- each and every
|
||||
- economical
|
||||
- eliminate
|
||||
- elucidate
|
||||
- employ
|
||||
- endeavor
|
||||
- enumerate
|
||||
- equitable
|
||||
- equivalent
|
||||
- evaluate
|
||||
- evidenced
|
||||
- exclusively
|
||||
- expedite
|
||||
- expend
|
||||
- expiration
|
||||
- facilitate
|
||||
- factual evidence
|
||||
- feasible
|
||||
- finalize
|
||||
- first and foremost
|
||||
- for all intents and purposes
|
||||
- for the most part
|
||||
- for the purpose of
|
||||
- forfeit
|
||||
- formulate
|
||||
- have a tendency to
|
||||
- honest truth
|
||||
- however
|
||||
- if and when
|
||||
- impacted
|
||||
- implement
|
||||
- in a manner of speaking
|
||||
- in a timely manner
|
||||
- in a very real sense
|
||||
- in accordance with
|
||||
- in addition
|
||||
- in all likelihood
|
||||
- in an effort to
|
||||
- in between
|
||||
- in excess of
|
||||
- in lieu of
|
||||
- in light of the fact that
|
||||
- in many cases
|
||||
- in my opinion
|
||||
- in order to
|
||||
- in regard to
|
||||
- in some instances
|
||||
- in terms of
|
||||
- in the case of
|
||||
- in the event that
|
||||
- in the final analysis
|
||||
- in the nature of
|
||||
- in the near future
|
||||
- in the process of
|
||||
- inception
|
||||
- incumbent upon
|
||||
- indicate
|
||||
- indication
|
||||
- initiate
|
||||
- irregardless
|
||||
- is applicable to
|
||||
- is authorized to
|
||||
- is responsible for
|
||||
- it is
|
||||
- it is essential
|
||||
- it seems that
|
||||
- it was
|
||||
- magnitude
|
||||
- maximum
|
||||
- methodology
|
||||
- minimize
|
||||
- minimum
|
||||
- modify
|
||||
- monitor
|
||||
- multiple
|
||||
- necessitate
|
||||
- nevertheless
|
||||
- not certain
|
||||
- not many
|
||||
- not often
|
||||
- not unless
|
||||
- not unlike
|
||||
- notwithstanding
|
||||
- null and void
|
||||
- numerous
|
||||
- objective
|
||||
- obligate
|
||||
- obtain
|
||||
- on the contrary
|
||||
- on the other hand
|
||||
- one particular
|
||||
- optimum
|
||||
- overall
|
||||
- owing to the fact that
|
||||
- participate
|
||||
- particulars
|
||||
- pass away
|
||||
- pertaining to
|
||||
- point in time
|
||||
- portion
|
||||
- possess
|
||||
- preclude
|
||||
- previously
|
||||
- prior to
|
||||
- prioritize
|
||||
- procure
|
||||
- proficiency
|
||||
- provided that
|
||||
- purchase
|
||||
- put simply
|
||||
- readily apparent
|
||||
- refer back
|
||||
- regarding
|
||||
- relocate
|
||||
- remainder
|
||||
- remuneration
|
||||
- requirement
|
||||
- reside
|
||||
- residence
|
||||
- retain
|
||||
- satisfy
|
||||
- shall
|
||||
- should you wish
|
||||
- similar to
|
||||
- solicit
|
||||
- span across
|
||||
- strategize
|
||||
- subsequent
|
||||
- substantial
|
||||
- successfully complete
|
||||
- sufficient
|
||||
- terminate
|
||||
- the month of
|
||||
- the point I am trying to make
|
||||
- therefore
|
||||
- time period
|
||||
- took advantage of
|
||||
- transmit
|
||||
- transpire
|
||||
- type of
|
||||
- until such time as
|
||||
- utilization
|
||||
- utilize
|
||||
- validate
|
||||
- various different
|
||||
- what I mean to say is
|
||||
- whether or not
|
||||
- with respect to
|
||||
- with the exception of
|
||||
- witnessed
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
extends: existence
|
||||
message: "'%s' is a weasel word!"
|
||||
ignorecase: true
|
||||
level: warning
|
||||
tokens:
|
||||
- absolutely
|
||||
- accidentally
|
||||
- additionally
|
||||
- allegedly
|
||||
- alternatively
|
||||
- angrily
|
||||
- anxiously
|
||||
- approximately
|
||||
- awkwardly
|
||||
- badly
|
||||
- barely
|
||||
- beautifully
|
||||
- blindly
|
||||
- boldly
|
||||
- bravely
|
||||
- brightly
|
||||
- briskly
|
||||
- bristly
|
||||
- bubbly
|
||||
- busily
|
||||
- calmly
|
||||
- carefully
|
||||
- carelessly
|
||||
- cautiously
|
||||
- cheerfully
|
||||
- clearly
|
||||
- closely
|
||||
- coldly
|
||||
- completely
|
||||
- consequently
|
||||
- correctly
|
||||
- courageously
|
||||
- crinkly
|
||||
- cruelly
|
||||
- crumbly
|
||||
- cuddly
|
||||
- currently
|
||||
- daily
|
||||
- daringly
|
||||
- deadly
|
||||
- definitely
|
||||
- deliberately
|
||||
- doubtfully
|
||||
- dumbly
|
||||
- eagerly
|
||||
- early
|
||||
- easily
|
||||
- elegantly
|
||||
- enormously
|
||||
- enthusiastically
|
||||
- equally
|
||||
- especially
|
||||
- eventually
|
||||
- exactly
|
||||
- exceedingly
|
||||
- exclusively
|
||||
- extremely
|
||||
- fairly
|
||||
- faithfully
|
||||
- fatally
|
||||
- fiercely
|
||||
- finally
|
||||
- fondly
|
||||
- few
|
||||
- foolishly
|
||||
- fortunately
|
||||
- frankly
|
||||
- frantically
|
||||
- generously
|
||||
- gently
|
||||
- giggly
|
||||
- gladly
|
||||
- gracefully
|
||||
- greedily
|
||||
- happily
|
||||
- hardly
|
||||
- hastily
|
||||
- healthily
|
||||
- heartily
|
||||
- helpfully
|
||||
- honestly
|
||||
- hourly
|
||||
- hungrily
|
||||
- hurriedly
|
||||
- immediately
|
||||
- impatiently
|
||||
- inadequately
|
||||
- ingeniously
|
||||
- innocently
|
||||
- inquisitively
|
||||
- interestingly
|
||||
- irritably
|
||||
- jiggly
|
||||
- joyously
|
||||
- justly
|
||||
- kindly
|
||||
- largely
|
||||
- lately
|
||||
- lazily
|
||||
- likely
|
||||
- literally
|
||||
- lonely
|
||||
- loosely
|
||||
- loudly
|
||||
- loudly
|
||||
- luckily
|
||||
- madly
|
||||
- many
|
||||
- mentally
|
||||
- mildly
|
||||
- monthly
|
||||
- mortally
|
||||
- mostly
|
||||
- mysteriously
|
||||
- neatly
|
||||
- nervously
|
||||
- nightly
|
||||
- noisily
|
||||
- normally
|
||||
- obediently
|
||||
- occasionally
|
||||
- only
|
||||
- openly
|
||||
- painfully
|
||||
- particularly
|
||||
- patiently
|
||||
- perfectly
|
||||
- politely
|
||||
- poorly
|
||||
- powerfully
|
||||
- presumably
|
||||
- previously
|
||||
- promptly
|
||||
- punctually
|
||||
- quarterly
|
||||
- quickly
|
||||
- quietly
|
||||
- rapidly
|
||||
- rarely
|
||||
- really
|
||||
- recently
|
||||
- recklessly
|
||||
- regularly
|
||||
- remarkably
|
||||
- relatively
|
||||
- reluctantly
|
||||
- repeatedly
|
||||
- rightfully
|
||||
- roughly
|
||||
- rudely
|
||||
- sadly
|
||||
- safely
|
||||
- selfishly
|
||||
- sensibly
|
||||
- seriously
|
||||
- sharply
|
||||
- shortly
|
||||
- shyly
|
||||
- significantly
|
||||
- silently
|
||||
- simply
|
||||
- sleepily
|
||||
- slowly
|
||||
- smartly
|
||||
- smelly
|
||||
- smoothly
|
||||
- softly
|
||||
- solemnly
|
||||
- sparkly
|
||||
- speedily
|
||||
- stealthily
|
||||
- sternly
|
||||
- stupidly
|
||||
- substantially
|
||||
- successfully
|
||||
- suddenly
|
||||
- surprisingly
|
||||
- suspiciously
|
||||
- swiftly
|
||||
- tenderly
|
||||
- tensely
|
||||
- thoughtfully
|
||||
- tightly
|
||||
- timely
|
||||
- truthfully
|
||||
- unexpectedly
|
||||
- unfortunately
|
||||
- usually
|
||||
- very
|
||||
- victoriously
|
||||
- violently
|
||||
- vivaciously
|
||||
- warmly
|
||||
- waverly
|
||||
- weakly
|
||||
- wearily
|
||||
- weekly
|
||||
- wildly
|
||||
- wisely
|
||||
- worldly
|
||||
- wrinkly
|
||||
- yearly
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"feed": "https://github.com/errata-ai/write-good/releases.atom",
|
||||
"vale_version": ">=1.0.0"
|
||||
}
|
||||
|
|
@ -1,773 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
### Evennia 1.0
|
||||
|
||||
> Not released yet
|
||||
> 2019-2022 develop branch (WIP)
|
||||
|
||||
Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
||||
|
||||
- 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
|
||||
- REST API allows you external access to db objects through HTTP requests (Tehom)
|
||||
- `Object.normalize_name` and `.validate_name` added to (by default) enforce latinify
|
||||
on character name and avoid potential exploits using clever Unicode chars (trhr)
|
||||
- New `utils.format_grid` for easily displaying long lists of items in a block.
|
||||
- Using `lunr` search indexing for better `help` matching and suggestions. Also improve
|
||||
the main help command's default listing output.
|
||||
- Added `content_types` indexing to DefaultObject's ContentsHandler. (volund)
|
||||
- Made most of the networking classes such as Protocols and the SessionHandlers
|
||||
replaceable via `settings.py` for modding enthusiasts. (volund)
|
||||
- The `initial_setup.py` file can now be substituted in `settings.py` to customize
|
||||
initial game database state. (volund)
|
||||
- Added new Traits contrib, converted and expanded from Ainneve project.
|
||||
- Added new `requirements_extra.txt` file for easily getting all optional dependencies.
|
||||
- Change default multi-match syntax from 1-obj, 2-obj to obj-1, obj-2.
|
||||
- Make `object.search` support 'stacks=0' keyword - if ``>0``, the method will return
|
||||
N identical matches instead of triggering a multi-match error.
|
||||
- Add `tags.has()` method for checking if an object has a tag or tags (PR by ChrisLR)
|
||||
- Make IP throttle use Django-based cache system for optional persistence (PR by strikaco)
|
||||
- Renamed Tutorial classes "Weapon" and "WeaponRack" to "TutorialWeapon" and
|
||||
"TutorialWeaponRack" to prevent collisions with classes in mygame
|
||||
- New `crafting` contrib, adding a full crafting subsystem (Griatch 2020)
|
||||
- The `rplanguage` contrib now auto-capitalizes sentences and retains ellipsis (...). This
|
||||
change means that proper nouns at the start of sentences will not be treated as nouns.
|
||||
- Make MuxCommand `lhs/rhslist` always be lists, also if empty (used to be the empty string)
|
||||
- Fix typo in UnixCommand contrib, where `help` was given as `--hel`.
|
||||
- Latin (la) i18n translation (jamalainm)
|
||||
- Made the `evennia` dir possible to use without gamedir for purpose of doc generation.
|
||||
- Make Scripts' timer component independent from script object deletion; can now start/stop
|
||||
timer without deleting Script. The `.persistent` flag now only controls if timer survives
|
||||
reload - Script has to be removed with `.delete()` like other typeclassed entities.
|
||||
- Add `utils.repeat` and `utils.unrepeat` as shortcuts to TickerHandler add/remove, similar
|
||||
to how `utils.delay` is a shortcut for TaskHandler add.
|
||||
- Refactor the classic `red_button` example to use `utils.delay/repeat` and modern recommended
|
||||
code style and paradigms instead of relying on `Scripts` for everything.
|
||||
- Expand `CommandTest` with ability to check multiple message-receivers; inspired by PR by
|
||||
user davewiththenicehat. Also add new doc string.
|
||||
- Add central `FuncParser` as a much more powerful replacement for the old `parse_inlinefunc`
|
||||
function.
|
||||
- Attribute/NAttribute got a homogenous representation, using intefaces, both
|
||||
`AttributeHandler` and `NAttributeHandler` has same api now.
|
||||
- Add `evennia/utils/verb_conjugation` for automatic verb conjugation (English only). This
|
||||
is useful for implementing actor-stance emoting for sending a string to different targets.
|
||||
- New version of Italian translation (rpolve)
|
||||
- `utils.evmenu.ask_yes_no` is a helper function that makes it easy to ask a yes/no question
|
||||
to the user and respond to their input. This complements the existing `get_input` helper.
|
||||
- Allow sending messages with `page/tell` without a `=` if target name contains no spaces.
|
||||
- New FileHelpStorage system allows adding help entries via external files.
|
||||
- `sethelp` command now warns if shadowing other help-types when creating a new
|
||||
entry.
|
||||
- Help command now uses `view` lock to determine if cmd/entry shows in index and
|
||||
`read` lock to determine if it can be read. It used to be `view` in the role
|
||||
of the latter. Migration swaps these around.
|
||||
- In modules given by `settings.PROTOTYPE_MODULES`, spawner will now first look for a global
|
||||
list `PROTOTYPE_LIST` of dicts before loading all dicts in the module as prototypes.
|
||||
- New Channel-System using the `channel` command and nicks. Removed the `ChannelHandler` and the
|
||||
concept of a dynamically created `ChannelCmdSet`.
|
||||
- Add `Msg.db_receiver_external` field to allowe external, string-id message-receivers.
|
||||
- Renamed `app.css` to `website.css` for consistency. Removed old prosimii-css files.
|
||||
- Remove `mygame/web/static_overrides` and -`template_overrides`, reorganize website/admin/client/api
|
||||
into a more consistent structure for overriding. Expanded webpage documentation considerably.
|
||||
- REST API list-view was shortened (#2401). New CSS/HTML. Add ReDoc for API autodoc page.
|
||||
- Update and fix dummyrunner with cleaner code and setup.
|
||||
- Made `iter_to_str` format prettier strings, using Oxford comma.
|
||||
- Added an MXP anchor tag to also support clickable web links.
|
||||
- New `tasks` command for managing tasks started with `utils.delay` (PR by davewiththenicehat)
|
||||
- Make `help` index output clickable for webclient/clients with MXP (PR by davewiththenicehat)
|
||||
- Custom `evennia` launcher commands (e.g. `evennia mycmd foo bar`). Add new commands as callables
|
||||
accepting `*args`, as `settings.EXTRA_LAUNCHER_COMMANDS = {'mycmd': 'path.to.callable', ...}`.
|
||||
- New `XYZGrid` contrib, adding x,y,z grid coordinates with in-game map and
|
||||
pathfinding. Controlled outside of the game via custom evennia launcher command.
|
||||
- `Script.delete` has new kwarg `stop_task=True`, that can be used to avoid
|
||||
infinite recursion when wanting to set up Script to delete-on-stop.
|
||||
- Command executions now done on copies to make sure `yield` don't cause crossovers. Add
|
||||
`Command.retain_instance` flag for reusing the same command instance.
|
||||
- The `typeclass` command will now correctly search the correct database-table for the target
|
||||
obj (avoids mistakenly assigning an AccountDB-typeclass to a Character etc).
|
||||
- Merged `script` and `scripts` commands into one, for both managing global- and
|
||||
on-object Scripts. Moved `CmdScripts` and `CmdObjects` to `commands/default/building.py`.
|
||||
- Keep GMCP function case if outputfunc starts with capital letter (so `cmd_name` -> `Cmd.Name`
|
||||
but `Cmd_nAmE` -> `Cmd.nAmE`). This helps e.g Mudlet's legacy `Client_GUI` implementation)
|
||||
- Prototypes now allow setting `prototype_parent` directly to a prototype-dict.
|
||||
This makes it easier when dynamically building in-module prototypes.
|
||||
- `RPSystem contrib` was expanded to support case, so /tall becomes 'tall man'
|
||||
while /Tall becomes 'Tall man'. One can turn this off if wanting the old style.
|
||||
- Change `EvTable` fixed-height rebalance algorithm to fill with empty lines at end of
|
||||
column instead of inserting rows based on cell-size (could be mistaken for a bug).
|
||||
- Split `return_appearance` hook with helper methods and have it use a template
|
||||
string in order to make it easier to override.
|
||||
- Add validation question to default account creation.
|
||||
- Add `LOCALECHO` client option to add server-side echo for clients that does
|
||||
not support this (useful for getting a complete log).
|
||||
- Make `@lazy_property` decorator create read/delete-protected properties. This is
|
||||
because it's used for handlers, and e.g. self.locks=[] is a common beginner mistake.
|
||||
- Add `$pron()` inlinefunc for pronoun parsing in actor-stance strings using
|
||||
`msg_contents`.
|
||||
- Update defauklt website to show Telnet/SSL/SSH connect info. Added new
|
||||
`SERVER_HOSTNAME` setting for use in the server:port stanza.
|
||||
- Changed all `at_before/after_*` hooks to `at_pre/post_*` for consistency
|
||||
across Evennia (the old names still work but are deprecated)
|
||||
- Change `settings.COMMAND_DEFAULT_ARG_REGEX` default from `None` to a regex meaning that
|
||||
a space or `/` must separate the cmdname and args. This better fits common expectations.
|
||||
- Add confirmation question to `ban`/`unban` commands.
|
||||
- Check new `teleport` and `teleport_here` lock-types in `teleport` command to optionally
|
||||
allow to limit teleportation of an object or to a specific destination.
|
||||
- Add `settings.MXP_ENABLED=True` and `settings.MXP_OUTGOING_ONLY=True` as sane defaults,
|
||||
to avoid known security issues with players entering MXP links.
|
||||
- Add browser name to webclient `CLIENT_NAME` in `session.protocol_flags`, e.g.
|
||||
`"Evennia webclient (websocket:firefox)"` or `"evennia webclient (ajax:chrome)"`.
|
||||
- `TagHandler.add/has(tag=...)` kwarg changed to `add/has(key=...)` for consistency
|
||||
with other handlers.
|
||||
- Make `DefaultScript.delete`, `DefaultChannel.delete` and `DefaultAccount.delete` return
|
||||
bool True/False if deletion was successful (like `DefaultObject.delete` before them)
|
||||
- `contrib.custom_gametime` days/weeks/months now always starts from 1 (to match
|
||||
the standard calendar form ... there is no month 0 every year after all).
|
||||
- `AttributeProperty`/`NAttributeProperty` to allow managing Attributes/NAttributes
|
||||
on typeclasses in the same way as Django fields.
|
||||
- Give build/system commands a `@name` to fall back to if the non-@ name is used
|
||||
by another command (like `open` and `@open`. If no duplicate, @ is optional.
|
||||
- Move legacy channel-management commands (`ccreate`, `addcom` etc) to a contrib
|
||||
since their work is now fully handled by the single `channel` command.
|
||||
- Expand `examine` command's code to much more extensible and modular. Show
|
||||
attribute categories and value types (when not strings).
|
||||
- `AttributeHandler.remove(key, return_exception=False, category=None, ...)` changed
|
||||
to `.remove(key, category=None, return_exception=False, ...)` for consistency.
|
||||
- New `command cooldown` contrib for making it easier to manage commands using
|
||||
dynamic cooldowns between uses (owllex)
|
||||
- Restructured `contrib/` folder, placing all contribs as separate packages under
|
||||
subfolders. All imports will need to be updated.
|
||||
- Made `MonitorHandler.add/remove` support `category` for monitoring Attributes
|
||||
with a category (before only key was used, ignoring category entirely).
|
||||
- Move `create_*` functions into db managers, leaving `utils.create` only being
|
||||
wrapper functions (consistent with `utils.search`). No change of api otherwise.
|
||||
- Add support for `$dbref()` and `$search` when assigning an Attribute value
|
||||
with the `set` command. This allows assigning real objects from in-game.
|
||||
- Add ability to examine `/script` and `/channel` entities with `examine` command.
|
||||
- Homogenize manager search methods to return querysets and not lists.
|
||||
- Restructure unit tests to always honor default settings; make new parents in
|
||||
on location for easy use in game dir.
|
||||
- The `Lunr` search engine used by help excludes common words; the settings-list
|
||||
`LUNR_STOP_WORD_FILTER_EXCEPTIONS` can be extended to make sure common names are included.
|
||||
- Add `.deserialize()` method to `_Saver*` structures to help completely
|
||||
decouple structures from database without needing separate import.
|
||||
- Add `run_in_main_thread` as a helper for those wanting to code server code
|
||||
from a web view.
|
||||
- Update `evennia.utils.logger` to use Twisted's new logging API. No change in Evennia API
|
||||
except more standard aliases logger.error/info/exception/debug etc can now be used.
|
||||
- Have `type/force` default to `update`-mode rather than `reset`mode and add more verbose
|
||||
warning when using reset mode.
|
||||
- Attribute storage support defaultdics (Hendher)
|
||||
- Add ObjectParent mixin to default game folder template as an easy, ready-made
|
||||
way to override features on all ObjectDB-inheriting objects easily.
|
||||
source location, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||
- Add `TagProperty`, `AliasProperty` and `PermissionProperty` to assign these
|
||||
data in a similar way to django fields.
|
||||
- New `at_pre_object_receive(obj, source_location)` method on Objects. Called on
|
||||
destination, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||
- New `at_pre_object_leave(obj, destination)` method on Objects. Called on
|
||||
- The db pickle-serializer now checks for methods `__serialize_dbobjs__` and `__deserialize_dbobjs__`
|
||||
to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute.
|
||||
- Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will
|
||||
now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal)
|
||||
- Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal)
|
||||
- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal)
|
||||
- Allow `# CODE`, `# HEADER` etc as well as `#CODE`/`#HEADER` in batchcode
|
||||
files - this works better with black linting.
|
||||
- Added `move_type` str kwarg to `move_to()` calls, optionally identifying the type of
|
||||
move being done ('teleport', 'disembark', 'give' etc). (volund)
|
||||
- Made RPSystem contrib msg calls pass `pose` or `say` as msg-`type` for use in
|
||||
e.g. webclient pane filtering where desired. (volund)
|
||||
- Added `Account.uses_screenreader(session=None)` as a quick shortcut for
|
||||
finding if a user uses a screenreader (and adjust display accordingly).
|
||||
- Fixed bug in `cmdset.remove()` where a command could not be deleted by `key`,
|
||||
even though doc suggested one could (ChrisLR)
|
||||
- New contrib `name_generator` for building random real-world based or fantasy-names
|
||||
based on phonetic rules.
|
||||
- Enable proper serialization of dict subclasses in Attributes (aogier)
|
||||
- `object.search` fuzzy-matching now uses `icontains` instead of `istartswith`
|
||||
to better match how search works elsewhere (volund)
|
||||
- The `.at_traverse` hook now receives a `exit_obj` kwarg, linking back to the
|
||||
exit triggering the hook (volund)
|
||||
- Contrib `buffs` for managing temporary and permanent RPG status buffs effects (tegiminis)
|
||||
- New `at_server_init()` hook called before all other startup hooks for all
|
||||
startup modes. Used for more generic overriding (volund)
|
||||
- New `search` lock type used to completely hide an object from being found by
|
||||
the `DefaultObject.search` (`caller.search`) method. (CloudKeeper)
|
||||
- Change setting `MULTISESSION_MODE` to now only control sessions, not how many
|
||||
characters can be puppeted simultaneously. New settings now control that.
|
||||
- Add new setting `AUTO_CREATE_CHARACTER_WITH_ACCOUNT`, a boolean deciding if
|
||||
the new account should also get a matching character (legacy MUD style).
|
||||
- Add new setting `AUTO_PUPPET_ON_LOGIN`, boolean deciding if one should
|
||||
automatically puppet the last/available character on connection (legacy MUD style)
|
||||
- Add new setting `MAX_NR_SIMULTANEUS_PUPPETS` - how many puppets the account
|
||||
can run at the same time. Used to limit multi-playing.
|
||||
- Make setting `MAX_NR_CHARACTERS` interact better with the new settings above.
|
||||
- Allow `$search` funcparser func to search tags and to accept kwargs for more
|
||||
powerful searches passed into the regular search functions.
|
||||
- `spawner.spawn` and linked methods now has a kwarg `protfunc_raise_errors`
|
||||
(default True) to disable strict errors on malformed/not-found protfuncs
|
||||
- Improve search performance when having many DB-based prototypes via caching.
|
||||
- Remove the `return_parents` kwarg of `evennia.prototypes.spawner.spawn` since it
|
||||
was inefficient and unused.
|
||||
- Made all id fields BigAutoField for all databases. (owllex)
|
||||
- `EvForm` refactored. New `literals` mapping, for literal mappings into the
|
||||
main template (e.g. for single-character replacements).
|
||||
- `EvForm` `cells` kwarg now accepts `EvCells` with custom formatting options
|
||||
(mainly for custom align/valign). `EvCells` now makes use of `utils.justify`.
|
||||
- `utils.justify` now supports `align="a"` (absolute alignments. This keeps
|
||||
the given left indent but crops/fills to the width. Used in EvCells.
|
||||
- `EvTable` now supports passing `EvColumn`s as a list directly, (`EvTable(table=[colA,colB])`)
|
||||
|
||||
## Evennia 0.9.5
|
||||
|
||||
> 2019-2020
|
||||
> Released 2020-11-14.
|
||||
> Transitional release, including new doc system.
|
||||
|
||||
Backported from develop: Python 3.8, 3.9 support. Django 3.2+ support, Twisted 21+ support.
|
||||
|
||||
- `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.
|
||||
- Webclient fix of prompt display
|
||||
- Webclient multimedia support for relaying images, video and sounds via
|
||||
`.msg(image=URL)`, `.msg(video=URL)`
|
||||
and `.msg(audio=URL)`
|
||||
- Add Spanish translation (fermuch)
|
||||
- Expand `GLOBAL_SCRIPTS` container to always start scripts and to include all
|
||||
global scripts regardless of how they were created.
|
||||
- Change settings to always use lists instead of tuples, to make mutable
|
||||
settings easier to add to. (#1912)
|
||||
- Make new `CHANNEL_MUDINFO` setting for specifying the mudinfo channel
|
||||
- Make `CHANNEL_CONNECTINFO` take full channel definition
|
||||
- Make `DEFAULT_CHANNELS` list auto-create channels missing at reload
|
||||
- Webclient `ANSI->HTML` parser updated. Webclient line width changed from 1.6em to 1.1em
|
||||
to better make ANSI graphics look the same as for third-party clients
|
||||
- `AttributeHandler.get(return_list=True)` will return `[]` if there are no
|
||||
Attributes instead of `[None]`.
|
||||
- Remove `pillow` requirement (install especially if using imagefield)
|
||||
- Add Simplified Korean translation (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.
|
||||
- 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 splittine one table)
|
||||
- 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.
|
||||
- Fix for TaskHandler with proper deferred returns/ability to cancel etc (PR by davewiththenicehat)
|
||||
- Add `PermissionHandler.check` method for straight string perm-checks without needing lockstrings.
|
||||
- Add `evennia.utils.utils.strip_unsafe_input` for removing html/newlines/tags from user input. The
|
||||
`INPUT_CLEANUP_BYPASS_PERMISSIONS` is a list of perms that bypass this safety stripping.
|
||||
- Make default `set` and `examine` commands aware of Attribute categories.
|
||||
|
||||
## Evennia 0.9
|
||||
|
||||
> 2018-2019
|
||||
> Released Oct 2019
|
||||
|
||||
### Distribution
|
||||
|
||||
- New requirement: Python 3.7 (py2.7 support removed)
|
||||
- Django 2.1
|
||||
- Twisted 19.2.1
|
||||
- Autobahn websockets (removed old tmwx)
|
||||
- Docker image updated
|
||||
|
||||
### Commands
|
||||
|
||||
- Remove `@`-prefix from all default commands (prefixes still work, optional)
|
||||
- Removed default `@delaccount` command, incorporating as `@account/delete` instead. Added confirmation
|
||||
question.
|
||||
- Add new `@force` command to have another object perform a command.
|
||||
- Add the Portal uptime to the `@time` command.
|
||||
- Make the `@link` command first make a local search before a global search.
|
||||
- Have the default Unloggedin-look command look for optional `connection_screen()` callable in
|
||||
`mygame/server/conf/connection_screen.py`. This allows for more flexible welcome screens
|
||||
that are calculated on the fly.
|
||||
- `@py` command now defaults to escaping html tags in its output when viewing in the webclient.
|
||||
Use new `/clientraw` switch to get old behavior (issue #1369).
|
||||
- Shorter and more informative, dynamic, listing of on-command vars if not
|
||||
setting func() in child command class.
|
||||
- New Command helper methods
|
||||
- `.client_width()` returns client width of the session running the command.
|
||||
- `.styled_table(*args, **kwargs)` returns a formatted evtable styled by user's options
|
||||
- `.style_header(*args, **kwargs)` creates styled header entry
|
||||
- `.style_separator(*args, **kwargs)` " separator
|
||||
- `.style_footer(*args, **kwargs)` " footer
|
||||
|
||||
### Web
|
||||
|
||||
- Change webclient from old txws version to use more supported/feature-rich Autobahn websocket library
|
||||
|
||||
#### Evennia game index
|
||||
|
||||
- Made Evennia game index client a part of core - now configured from settings file (old configs
|
||||
need to be moved)
|
||||
- The `evennia connections` command starts a wizard that helps you connect your game to the game index.
|
||||
- The game index now accepts games with no public telnet/webclient info (for early prototypes).
|
||||
|
||||
#### New golden-layout based Webclient UI (@friarzen)
|
||||
- Features
|
||||
- Much slicker behavior and more professional look
|
||||
- Allows tabbing as well as click and drag of panes in any grid position
|
||||
- Renaming tabs, assignments of data tags and output types are simple per-pane menus now
|
||||
- Any number of input panes, with separate histories
|
||||
- Button UI (disabled in JS by default)
|
||||
|
||||
#### Web/Django standard initiative (@strikaco)
|
||||
- Features
|
||||
- Adds a series of web-based forms and generic class-based views
|
||||
- Accounts
|
||||
- Register - Enhances registration; allows optional collection of email address
|
||||
- Form - Adds a generic Django form for creating Accounts from the web
|
||||
- Characters
|
||||
- Create - Authenticated users can create new characters from the website (requires associated form)
|
||||
- Detail - Authenticated and authorized users can view select details about characters
|
||||
- List - Authenticated and authorized users can browse a list of all characters
|
||||
- Manage - Authenticated users can edit or delete owned characters from the web
|
||||
- Form - Adds a generic Django form for creating characters from the web
|
||||
- Channels
|
||||
- Detail - Authorized users can view channel logs from the web
|
||||
- List - Authorized users can browse a list of all channels
|
||||
- Help Entries
|
||||
- Detail - Authorized users can view help entries from the web
|
||||
- List - Authorized users can browse a list of all help entries from the web
|
||||
- Navbar changes
|
||||
- Characters - Link to character list
|
||||
- Channels - Link to channel list
|
||||
- Help - Link to help entry list
|
||||
- Puppeting
|
||||
- Users can puppet their own characters within the context of the website
|
||||
- Dropdown
|
||||
- Link to create characters
|
||||
- Link to manage characters
|
||||
- Link to quick-select puppets
|
||||
- Link to password change workflow
|
||||
- Functions
|
||||
- Updates Bootstrap to v4 stable
|
||||
- Enables use of Django Messages framework to communicate with users in browser
|
||||
- Implements webclient/website `_shared_login` functionality as Django middleware
|
||||
- 'account' and 'puppet' are added to all request contexts for authenticated users
|
||||
- Adds unit tests for all web views
|
||||
- Cosmetic
|
||||
- Prettifies Django 'forgot password' workflow (requires SMTP to actually function)
|
||||
- Prettifies Django 'change password' workflow
|
||||
- Bugfixes
|
||||
- Fixes bug on login page where error messages were not being displayed
|
||||
- Remove strvalue field from admin; it made no sense to have here, being an optimization field
|
||||
for internal use.
|
||||
|
||||
### Prototypes
|
||||
|
||||
- `evennia.prototypes.save_prototype` now takes the prototype as a normal
|
||||
argument (`prototype`) instead of having to give it as `**prototype`.
|
||||
- `evennia.prototypes.search_prototype` has a new kwarg `require_single=False` that
|
||||
raises a KeyError exception if query gave 0 or >1 results.
|
||||
- `evennia.prototypes.spawner` can now spawn by passing a `prototype_key`
|
||||
|
||||
### Typeclasses
|
||||
|
||||
- Add new methods on all typeclasses, useful specifically for object handling from the website/admin:
|
||||
+ `web_get_admin_url()`: Returns the path to the object detail page in the Admin backend.
|
||||
+ `web_get_create_url()`: Returns the path to the typeclass' creation page on the website, if implemented.
|
||||
+ `web_get_absolute_url()`: Returns the path to the object's detail page on the website, if implemented.
|
||||
+ `web_get_update_url()`: Returns the path to the object's update page on the website, if implemented.
|
||||
+ `web_get_delete_url()`: Returns the path to the object's delete page on the website, if implemented.
|
||||
- All typeclasses have new helper class method `create`, which encompasses useful functionality
|
||||
that used to be embedded for example in the respective `@create` or `@connect` commands.
|
||||
- DefaultAccount now has new class methods implementing many things that used to be in unloggedin
|
||||
commands (these can now be customized on the class instead):
|
||||
+ `is_banned()`: Checks if a given username or IP is banned.
|
||||
+ `get_username_validators`: Return list of validators for username validation (see
|
||||
`settings.AUTH_USERNAME_VALIDATORS`)
|
||||
+ `authenticate`: Method to check given username/password.
|
||||
+ `normalize_username`: Normalizes names so (for Unicode environments) users cannot mimic existing usernames by replacing select characters with visually-similar Unicode chars.
|
||||
+ `validate_username`: Mechanism for validating a username based on predefined Django validators.
|
||||
+ `validate_password`: Mechanism for validating a password based on predefined Django validators.
|
||||
+ `set_password`: Apply password to account, using validation checks.
|
||||
- `AttributeHandler.remove` and `TagHandler.remove` can now be used to delete by-category. If neither
|
||||
key nor category is given, they now work the same as .clear().
|
||||
|
||||
### Protocols
|
||||
|
||||
- Support for `Grapevine` MUD-chat network ("channels" supported)
|
||||
|
||||
### Server
|
||||
|
||||
- Convert ServerConf model to store its values as a Picklefield (same as
|
||||
Attributes) instead of using a custom solution.
|
||||
- OOB: Add support for MSDP LIST, REPORT, UNREPORT commands (re-mapped to `msdp_list`,
|
||||
`msdp_report`, `msdp_unreport`, inlinefuncs)
|
||||
- Added `evennia.ANSIString` to flat API.
|
||||
- Server/Portal log files now cycle to names on the form `server_.log_19_03_08_` instead of `server.log___19.3.8`, retaining
|
||||
unix file sorting order.
|
||||
- Django signals fire for important events: Puppet/Unpuppet, Object create/rename, Login,
|
||||
Logout, Login fail Disconnect, Account create/rename
|
||||
|
||||
### Settings
|
||||
|
||||
- `GLOBAL_SCRIPTS` - dict defining typeclasses of global scripts to store on the new
|
||||
`evennia.GLOBAL_SCRIPTS` container. These will auto-start when Evennia start and will always
|
||||
exist.
|
||||
- `OPTIONS_ACCOUNTS_DEFAULT` - option dict with option defaults and Option classes
|
||||
- `OPTION_CLASS_MODULES` - classes representing an on-Account Option, on special form
|
||||
- `VALIDATOR_FUNC_MODULES` - (general) text validator functions, for verifying an input
|
||||
is on a specific form.
|
||||
|
||||
### Utils
|
||||
|
||||
- `evennia` launcher now fully handles all django-admin commands, like running tests in parallel.
|
||||
- `evennia.utils.create.account` now also takes `tags` and `attrs` keywords.
|
||||
- `evennia.utils.interactive` decorator can now allow you to use yield(secs) to pause operation
|
||||
in any function, not just in Command.func. Likewise, response = yield(question) will work
|
||||
if the decorated function has an argument or kwarg `caller`.
|
||||
- Added many more unit tests.
|
||||
- Swap argument order of `evennia.set_trace` to `set_trace(term_size=(140, 40), debugger='auto')`
|
||||
since the size is more likely to be changed on the command line.
|
||||
- `utils.to_str(text, session=None)` now acts as the old `utils.to_unicode` (which was removed).
|
||||
This converts to the str() type (not to a byte-string as in Evennia 0.8), trying different
|
||||
encodings. This function will also force-convert any object passed to it into a string (so
|
||||
`force_string` flag was removed and assumed always set).
|
||||
- `utils.to_bytes(text, session=None)` replaces the old `utils.to_str()` functionality and converts
|
||||
str to bytes.
|
||||
- `evennia.MONITOR_HANDLER.all` now takes keyword argument `obj` to only retrieve monitors from that specific
|
||||
Object (rather than all monitors in the entire handler).
|
||||
- Support adding `\f` in command doc strings to force where EvMore puts page breaks.
|
||||
- Validation Functions now added with standard API to homogenize user input validation.
|
||||
- Option Classes added to make storing user-options easier and smoother.
|
||||
- `evennia.VALIDATOR_CONTAINER` and `evennia.OPTION_CONTAINER` added to load these.
|
||||
|
||||
### Contribs
|
||||
|
||||
- Evscaperoom - a full puzzle engine for making multiplayer escape rooms in Evennia. Used to make
|
||||
the entry for the MUD-Coder's Guild's 2019 Game Jam with the theme "One Room", where it ranked #1.
|
||||
- Evennia game-index client no longer a contrib - moved into server core and configured with new
|
||||
setting `GAME_INDEX_ENABLED`.
|
||||
- The `extended_room` contrib saw some backwards-incompatible refactoring:
|
||||
+ All commands now begin with `CmdExtendedRoom`. So before it was `CmdExtendedLook`, now
|
||||
it's `CmdExtendedRoomLook` etc.
|
||||
+ The `detail` command was broken out of the `desc` command and is now a new, stand-alone command
|
||||
`CmdExtendedRoomDetail`. This was done to make things easier to extend and to mimic how the detail
|
||||
command works in the tutorial-world.
|
||||
+ The `detail` command now also supports deleting details (like the tutorial-world version).
|
||||
+ The new `ExtendedRoomCmdSet` includes all the extended-room commands and is now the recommended way
|
||||
to install the extended-room contrib.
|
||||
- Reworked `menu_login` contrib to use latest EvMenu standards. Now also supports guest logins.
|
||||
- Mail contrib was refactored to have optional Command classes `CmdMail` for OOC+IC mail (added
|
||||
to the CharacterCmdSet and `CmdMailCharacter` for IC-only mailing between chars (added to CharacterCmdSet)
|
||||
|
||||
### Translations
|
||||
|
||||
- Simplified chinese, courtesy of user MaxAlex.
|
||||
|
||||
|
||||
## Evennia 0.8
|
||||
|
||||
> 2017-2018
|
||||
> Released Nov 2018
|
||||
|
||||
### Requirements
|
||||
|
||||
- Up requirements to Django 1.11.x, Twisted 18 and pillow 5.2.0
|
||||
- Add `inflect` dependency for automatic pluralization of object names.
|
||||
|
||||
### Server/Portal
|
||||
|
||||
- Removed `evennia_runner`, completely refactor `evennia_launcher.py` (the 'evennia' program)
|
||||
with different functionality).
|
||||
- Both Portal/Server are now stand-alone processes (easy to run as daemon)
|
||||
- Made Portal the AMP Server for starting/restarting the Server (the AMP client)
|
||||
- Dynamic logging now happens using `evennia -l` rather than by interactive mode.
|
||||
- Made AMP secure against erroneous HTTP requests on the wrong port (return error messages).
|
||||
- The `evennia istart` option will start/switch the Server in foreground (interactive) mode, where it logs
|
||||
to terminal and can be stopped with Ctrl-C. Using `evennia reload`, or reloading in-game, will
|
||||
return Server to normal daemon operation.
|
||||
- For validating passwords, use safe Django password-validation backend instead of custom Evennia one.
|
||||
- Alias `evennia restart` to mean the same as `evennia reload`.
|
||||
|
||||
### Prototype changes
|
||||
|
||||
- New OLC started from `olc` command for loading/saving/manipulating prototypes in a menu.
|
||||
- Moved evennia/utils/spawner.py into the new evennia/prototypes/ along with all new
|
||||
functionality around prototypes.
|
||||
- A new form of prototype - database-stored prototypes, editable from in-game, was added. The old,
|
||||
module-created prototypes remain as read-only prototypes.
|
||||
- All prototypes must have a key `prototype_key` identifying the prototype in listings. This is
|
||||
checked to be server-unique. Prototypes created in a module will use the global variable name they
|
||||
are assigned to if no `prototype_key` is given.
|
||||
- Prototype field `prototype` was renamed to `prototype_parent` to avoid mixing terms.
|
||||
- All prototypes must either have `typeclass` or `prototype_parent` defined. If using
|
||||
`prototype_parent`, `typeclass` must be defined somewhere in the inheritance chain. This is a
|
||||
change from Evennia 0.7 which allowed 'mixin' prototypes without `typeclass`/`prototype_key`. To
|
||||
make a mixin now, give it a default typeclass, like `evennia.objects.objects.DefaultObject` and just
|
||||
override in the child as needed.
|
||||
- Spawning an object using a prototype will automatically assign a new tag to it, named the same as
|
||||
the `prototype_key` and with the category `from_prototype`.
|
||||
- The spawn command was extended to accept a full prototype on one line.
|
||||
- The spawn command got the /save switch to save the defined prototype and its key
|
||||
- The command spawn/menu will now start an OLC (OnLine Creation) menu to load/save/edit/spawn prototypes.
|
||||
|
||||
### EvMenu
|
||||
|
||||
- Added `EvMenu.helptext_formatter(helptext)` to allow custom formatting of per-node help.
|
||||
- Added `evennia.utils.evmenu.list_node` decorator for turning an EvMenu node into a multi-page listing.
|
||||
- A `goto` option callable returning None (rather than the name of the next node) will now rerun the
|
||||
current node instead of failing.
|
||||
- Better error handling of in-node syntax errors.
|
||||
- Improve dedent of default text/helptext formatter. Right-strip whitespace.
|
||||
- Add `debug` option when creating menu - this turns off persistence and makes the `menudebug`
|
||||
command available for examining the current menu state.
|
||||
|
||||
|
||||
### Webclient
|
||||
|
||||
- Webclient now uses a plugin system to inject new components from the html file.
|
||||
- Split-windows - divide input field into any number of horizontal/vertical panes and
|
||||
assign different types of server messages to them.
|
||||
- Lots of cleanup and bug fixes.
|
||||
- Hot buttons plugin (friarzen) (disabled by default).
|
||||
|
||||
### Locks
|
||||
|
||||
- New function `evennia.locks.lockhandler.check_lockstring`. This allows for checking an object
|
||||
against an arbitrary lockstring without needing the lock to be stored on an object first.
|
||||
- New function `evennia.locks.lockhandler.validate_lockstring` allows for stand-alone validation
|
||||
of a lockstring.
|
||||
- New function `evennia.locks.lockhandler.get_all_lockfuncs` gives a dict {"name": lockfunc} for
|
||||
all available lock funcs. This is useful for dynamic listings.
|
||||
|
||||
|
||||
### Utils
|
||||
|
||||
- Added new `columnize` function for easily splitting text into multiple columns. At this point it
|
||||
is not working too well with ansi-colored text however.
|
||||
- Extend the `dedent` function with a new `baseline_index` kwarg. This allows to force all lines to
|
||||
the indentation given by the given line regardless of if other lines were already a 0 indentation.
|
||||
This removes a problem with the original `textwrap.dedent` which will only dedent to the least
|
||||
indented part of a text.
|
||||
- Added `exit_cmd` to EvMore pager, to allow for calling a command (e.g. 'look') when leaving the pager.
|
||||
- `get_all_typeclasses` will return dict `{"path": typeclass, ...}` for all typeclasses available
|
||||
in the system. This is used by the new `@typeclass/list` subcommand (useful for builders etc).
|
||||
- `evennia.utils.dbserialize.deserialize(obj)` is a new helper function to *completely* disconnect
|
||||
a mutable recovered from an Attribute from the database. This will convert all nested `_Saver*`
|
||||
classes to their plain-Python counterparts.
|
||||
|
||||
### General
|
||||
|
||||
- Start structuring the `CHANGELOG` to list features in more detail.
|
||||
- Docker image `evennia/evennia:develop` is now auto-built, tracking the develop branch.
|
||||
- Inflection and grouping of multiple objects in default room (an box, three boxes)
|
||||
- `evennia.set_trace()` is now a shortcut for launching pdb/pudb on a line in the Evennia event loop.
|
||||
- Removed the enforcing of `MAX_NR_CHARACTERS=1` for `MULTISESSION_MODE` `0` and `1` by default.
|
||||
- Add `evennia.utils.logger.log_sec` for logging security-related messages (marked SS in log).
|
||||
|
||||
### Contribs
|
||||
|
||||
- `Auditing` (Johnny): Log and filter server input/output for security purposes
|
||||
- `Build Menu` (vincent-lg): New @edit command to edit object properties in a menu.
|
||||
- `Field Fill` (Tim Ashley Jenkins): Wraps EvMenu for creating submittable forms.
|
||||
- `Health Bar` (Tim Ashley Jenkins): Easily create colorful bars/meters.
|
||||
- `Tree select` (Fluttersprite): Wrap EvMenu to create a common type of menu from a string.
|
||||
- `Turnbattle suite` (Tim Ashley Jenkins)- the old `turnbattle.py` was moved into its own
|
||||
`turnbattle/` package and reworked with many different flavors of combat systems:
|
||||
- `tb_basic` - The basic turnbattle system, with initiative/turn order attack/defense/damage.
|
||||
- `tb_equip` - Adds weapon and armor, wielding, accuracy modifiers.
|
||||
- `tb_items` - Extends `tb_equip` with item use with conditions/status effects.
|
||||
- `tb_magic` - Extends `tb_equip` with spellcasting.
|
||||
- `tb_range` - Adds system for abstract positioning and movement.
|
||||
- The `extended_room` contrib saw some backwards-incompatible refactoring:
|
||||
- All commands now begin with `CmdExtendedRoom`. So before it was `CmdExtendedLook`, now
|
||||
it's `CmdExtendedRoomLook` etc.
|
||||
- The `detail` command was broken out of the `desc` command and is now a new, stand-alone command
|
||||
`CmdExtendedRoomDetail`. This was done to make things easier to extend and to mimic how the detail
|
||||
command works in the tutorial-world.
|
||||
- The `detail` command now also supports deleting details (like the tutorial-world version).
|
||||
- The new `ExtendedRoomCmdSet` includes all the extended-room commands and is now the recommended way
|
||||
to install the extended-room contrib.
|
||||
- Updates and some cleanup of existing contribs.
|
||||
|
||||
|
||||
### Internationalization
|
||||
|
||||
- Polish translation by user ogotai
|
||||
|
||||
# Overview-Changelogs
|
||||
|
||||
> These are changelogs from a time before we used formal version numbers.
|
||||
|
||||
## Sept 2017:
|
||||
Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to
|
||||
'Account', rework the website template and a slew of other updates.
|
||||
Info on what changed and how to migrate is found here:
|
||||
https://groups.google.com/forum/#!msg/evennia/0JYYNGY-NfE/cDFaIwmPBAAJ
|
||||
|
||||
## Feb 2017:
|
||||
New devel branch created, to lead up to Evennia 0.7.
|
||||
|
||||
## Dec 2016:
|
||||
Lots of bugfixes and considerable uptick in contributors. Unittest coverage
|
||||
and PEP8 adoption and refactoring.
|
||||
|
||||
## May 2016:
|
||||
Evennia 0.6 with completely reworked Out-of-band system, making
|
||||
the message path completely flexible and built around input/outputfuncs.
|
||||
A completely new webclient, split into the evennia.js library and a
|
||||
gui library, making it easier to customize.
|
||||
|
||||
## Feb 2016:
|
||||
Added the new EvMenu and EvMore utilities, updated EvEdit and cleaned up
|
||||
a lot of the batchcommand functionality. Started work on new Devel branch.
|
||||
|
||||
## Sept 2015:
|
||||
Evennia 0.5. Merged devel branch, full library format implemented.
|
||||
|
||||
## Feb 2015:
|
||||
Development currently in devel/ branch. Moved typeclasses to use
|
||||
django's proxy functionality. Changed the Evennia folder layout to a
|
||||
library format with a stand-alone launcher, in preparation for making
|
||||
an 'evennia' pypy package and using versioning. The version we will
|
||||
merge with will likely be 0.5. There is also work with an expanded
|
||||
testing structure and the use of threading for saves. We also now
|
||||
use Travis for automatic build checking.
|
||||
|
||||
## Sept 2014:
|
||||
Updated to Django 1.7+ which means South dependency was dropped and
|
||||
minimum Python version upped to 2.7. MULTISESSION_MODE=3 was added
|
||||
and the web customization system was overhauled using the latest
|
||||
functionality of django. Otherwise, mostly bug-fixes and
|
||||
implementation of various smaller feature requests as we got used
|
||||
to github. Many new users have appeared.
|
||||
|
||||
## Jan 2014:
|
||||
Moved Evennia project from Google Code to github.com/evennia/evennia.
|
||||
|
||||
## Nov 2013:
|
||||
Moved the internal webserver into the Server and added support for
|
||||
out-of-band protocols (MSDP initially). This large development push
|
||||
also meant fixes and cleanups of the way attributes were handled.
|
||||
Tags were added, along with proper handlers for permissions, nicks
|
||||
and aliases.
|
||||
|
||||
## May 2013:
|
||||
Made players able to control more than one Character at the same
|
||||
time, through the MULTISESSION_MODE=2 addition. This lead to a lot
|
||||
of internal changes for the server.
|
||||
|
||||
## Oct 2012:
|
||||
Changed Evennia from the Modified Artistic 1.0 license to the more
|
||||
standard and permissive BSD license. Lots of updates and bug fixes as
|
||||
more people start to use it in new ways. Lots of new caching and
|
||||
speed-ups.
|
||||
|
||||
## March 2012:
|
||||
Evennia's API has changed and simplified slightly in that the
|
||||
base-modules where removed from game/gamesrc. Instead admins are
|
||||
encouraged to explicitly create new modules under game/gamesrc/ when
|
||||
they want to implement their game - gamesrc/ is empty by default
|
||||
except for the example folders that contain template files to use for
|
||||
this purpose. We also added the ev.py file, implementing a new, flat
|
||||
API. Work is ongoing to add support for mud-specific telnet
|
||||
extensions, notably the MSDP and GMCP out-of-band extensions. On the
|
||||
community side, evennia's dev blog was started and linked on planet
|
||||
Mud-dev aggregator.
|
||||
|
||||
## Nov 2011:
|
||||
After creating several different proof-of-concept game systems (in
|
||||
contrib and privately) as well testing lots of things to make sure the
|
||||
implementation is basically sound, we are declaring Evennia out of
|
||||
Alpha. This can mean as much or as little as you want, admittedly -
|
||||
development is still heavy but the issue list is at an all-time low
|
||||
and the server is slowly stabilizing as people try different things
|
||||
with it. So Beta it is!
|
||||
|
||||
## Aug 2011:
|
||||
Split Evennia into two processes: Portal and Server. After a lot of
|
||||
work trying to get in-memory code-reloading to work, it's clear this
|
||||
is not Python's forte - it's impossible to catch all exceptions,
|
||||
especially in asynchronous code like this. Trying to do so results in
|
||||
hackish, flakey and unstable code. With the Portal-Server split, the
|
||||
Server can simply be rebooted while players connected to the Portal
|
||||
remain connected. The two communicates over twisted's AMP protocol.
|
||||
|
||||
## May 2011:
|
||||
The new version of Evennia, originally hitting trunk in Aug2010, is
|
||||
maturing. All commands from the pre-Aug version, including IRC/IMC2
|
||||
support works again. An ajax web-client was added earlier in the year,
|
||||
including moving Evennia to be its own webserver (no more need for
|
||||
Apache or django-testserver). Contrib-folder added.
|
||||
|
||||
## Aug 2010:
|
||||
Evennia-griatch-branch is ready for merging with trunk. This marks a
|
||||
rather big change in the inner workings of the server, such as the
|
||||
introduction of TypeClasses and Scripts (as compared to the old
|
||||
ScriptParents and Events) but should hopefully bring everything
|
||||
together into one consistent package as code development continues.
|
||||
|
||||
## May 2010:
|
||||
Evennia is currently being heavily revised and cleaned from
|
||||
the years of gradual piecemeal development. It is thus in a very
|
||||
'Alpha' stage at the moment. This means that old code snippets
|
||||
will not be backwards compatabile. Changes touch almost all
|
||||
parts of Evennia's innards, from the way Objects are handled
|
||||
to Events, Commands and Permissions.
|
||||
|
||||
## April 2010:
|
||||
Griatch takes over Maintainership of the Evennia project from
|
||||
the original creator Greg Taylor.
|
||||
|
||||
# Older
|
||||
|
||||
Earlier revisions, with previous maintainer, used SVN on Google Code
|
||||
and have no changelogs.
|
||||
|
||||
First commit (Evennia's birthday) was November 20, 2006.
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
# 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.
|
||||
|
||||
## Start with the tutorial
|
||||
|
||||
It's highly recommended that you jump in on the [Starting Tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Part1-Intro.md). Even if
|
||||
you only the beginning or some part of it, it covers much of the things needed to get started.
|
||||
|
||||
## 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 [Python introduction](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-basic-introduction.md).
|
||||
|
||||
## 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](https://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 -r requirements_extra.txt # install ipython etc
|
||||
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.md) page for more
|
||||
info on how to explore it efficiently.
|
||||
|
||||
### Jupyter Notebook Support
|
||||
|
||||
You can also explore evennia interactively in a [Jupyter notebook](https://jupyter.readthedocs.io/en/latest/index.html#). This offers
|
||||
an in-browser view of your code similar to Matlab or similar programs. There are
|
||||
a few extra steps that must be taken in order for this to work:
|
||||
|
||||
# [open a new console/terminal]
|
||||
# [activate your evennia virtualenv in this console/terminal]
|
||||
cd evennia
|
||||
pip install -r requirements_extra.txt # if not done already above
|
||||
|
||||
Next, `cd` to your game folder. _It's important that you are in the _root_ of this folder for the next command_:
|
||||
|
||||
evennia shell_plus --notebook &
|
||||
|
||||
The `&` at the end starts the process as a background process on Linux/Unix.
|
||||
Skip it if your OS doesn't support this syntax. Your browser should now open
|
||||
with the Jupyter interface. If not, open a browser to the link given on the
|
||||
command line.
|
||||
|
||||
In the window, open the `new` menu in the top right and start a `Django Shell-Plus` notebook (or
|
||||
open an existing one if you had one from before). In the first cell you must initialize
|
||||
Evennia like so:
|
||||
|
||||
```python
|
||||
import evennia
|
||||
evennia._init()
|
||||
```
|
||||
|
||||
_Note that the above initialization must be run every time a new new notebook/kernel is started or restarted._
|
||||
|
||||
After this you can import and access all of the Evennia system, same as with `evennia shell`.
|
||||
|
||||
### More exploration
|
||||
|
||||
You can complement your exploration by peeking at the sections of the much more detailed
|
||||
[Evennia Component overview](../Components/Components-Overview.md). The [Tutorials](../Howtos/Howtos-Overview.md) 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](https://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](../Howtos/Beginner-Tutorial/Part2/Beginner-Tutorial-Game-Planning.md)
|
||||
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][bug]. If you add features or fix bugs
|
||||
yourself, please consider [Contributing](../Contributing.md) 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](https://webchat.freenode.net/?channels=evennia) are also there for you.
|
||||
|
||||
## The most important point
|
||||
|
||||
And finally, of course, have fun!
|
||||
|
||||
[feature-request]: (https://github.com/evennia/evennia/issues/new?title=Feature+Request%3a+%3Cdescriptive+title+here%3E&body=%23%23%23%23+Description+of+the+suggested+feature+and+how+it+is+supposed+to+work+for+the+admin%2fend+user%3a%0D%0A%0D%0A%0D%0A%23%23%23%23+A+list+of+arguments+for+why+you+think+this+new+feature+should+be+included+in+Evennia%3a%0D%0A%0D%0A1.%0D%0A2.%0D%0A%0D%0A%23%23%23%23+Extra+information%2c+such+as+requirements+or+ideas+on+implementation%3a%0D%0A%0D%0A
|
||||
[bug](https://github.com/evennia/evennia/issues/new?title=Bug%3a+%3Cdescriptive+title+here%3E&body=%23%23%23%23+Steps+to+reproduce+the+issue%3a%0D%0A%0D%0A1.+%0D%0A2.+%0D%0A3.+%0D%0A%0D%0A%23%23%23%23+What+I+expect+to+see+and+what+I+actually+see+%28tracebacks%2c+error+messages+etc%29%3a%0D%0A%0D%0A%0D%0A%0D%0A%23%23%23%23+Extra+information%2c+such+as+Evennia+revision%2frepo%2fbranch%2c+operating+system+and+ideas+for+how+to+solve%3a%0D%0A%0D%0A)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# Coding and development help
|
||||
|
||||
This documentation aims to help you set up a sane development environment to
|
||||
make your game, also if you never coded before. If you are an experienced coder, much of this will be familiar
|
||||
to you, but some things may still be useful.
|
||||
|
||||
|
||||
## Setting up a workflow
|
||||
|
||||
See also the [Beginner Tutorial](../Howtos/Beginner-Tutorial/Beginner-Tutorial-Intro.md).
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Version-Control.md
|
||||
Updating-Your-Game.md
|
||||
|
||||
```
|
||||
|
||||
## Coding away
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Coding-Introduction.md
|
||||
Debugging.md
|
||||
Unit-Testing.md
|
||||
Profiling.md
|
||||
Quirks.md
|
||||
Changelog.md
|
||||
|
||||
```
|
||||
|
||||
## Third-party integrations
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Continuous-Integration.md
|
||||
Setting-up-PyCharm.md
|
||||
|
||||
```
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
# Continuous Integration - TeamCity (linux)
|
||||
|
||||
This sets up a TeamCity build integration environment on Linux.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Follow [TeamCity](https://www.jetbrains.com/teamcity/) 's in-depth
|
||||
[Setup Guide](https://confluence.jetbrains.com/display/TCD8/Installing+and+Configuring+the+TeamCity+Server).
|
||||
- You need to use [Version Control](./Version-Control.md).
|
||||
|
||||
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" and add the script:
|
||||
|
||||
```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"
|
||||
``````
|
||||
|
||||
```bash
|
||||
|
||||
# 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 Sqlite3 for your game (default database), it's prudent to change working directory on this
|
||||
step to your game dir.
|
||||
|
||||
```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 Sqlite3 for your game (default database), it's prudent to change working directory on this
|
||||
step to your 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 SQlite3 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!
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
# Continuous integration with Travis
|
||||
|
||||
[Travis CI](https://travis-ci.org/) is an online service for checking, validating and potentially
|
||||
deploying code automatically. It can check that every commit is building successfully after every
|
||||
commit to its Github repository.
|
||||
|
||||
If your game is open source on Github you may use Travis for free.
|
||||
See [the Travis docs](https://docs.travis-ci.com/user/getting- started/) for how to get started.
|
||||
|
||||
After logging in you will get to point Travis to your repository on github. One further thing you
|
||||
need to set up yourself is a Travis config file named `.travis.yml` (note the initial period `.`).
|
||||
This should be created in the root of your game directory. The idea with this file is that it
|
||||
describes what Travis needs to import and build in order to create an instance of Evennia from
|
||||
scratch and then run validation tests on it. Here is an example:
|
||||
|
||||
``` yaml
|
||||
language: python
|
||||
python:
|
||||
- "3.10"
|
||||
install:
|
||||
- git clone https://github.com/evennia/evennia.git
|
||||
- cd evennia
|
||||
- pip install -e .
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
script:
|
||||
- evennia migrate
|
||||
- evennia test --settings settings.py .
|
||||
```
|
||||
|
||||
This will tell travis how to download Evennia, install it, set up a database and then run
|
||||
your own test suite (inside the game dir). Use `evennia test evennia` if you also want to
|
||||
run the Evennia full test suite.
|
||||
|
||||
You need to add this file to git (`git add .travis.yml`) and then commit your changes before Travis
|
||||
will be able to see it.
|
||||
|
||||
For properly testing your game you of course also need to write unittests.
|
||||
The [Unit testing](./Unit-Testing.md) doc page gives some ideas on how to set those up for Evennia.
|
||||
You should be able to refer to that for making tests fitting your game.
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# Continuous Integration
|
||||
|
||||
One of the advantages of Evennia over traditional MU* development systems is that Evennia can
|
||||
integrate into enterprise-level integration environments and source control.
|
||||
|
||||
## What is Continuous Integration (CI)?
|
||||
|
||||
[Continuous Integration (CI)](https://www.thoughtworks.com/continuous-integration) is a development
|
||||
practice that requires developers to integrate code into a shared repository.
|
||||
Each check-in is then verified by an automated build, allowing teams to detect problems early. This
|
||||
can be set up to safely deploy data to a production server only after tests have passed, for example.
|
||||
|
||||
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.
|
||||
|
||||
## List of continuous integration tools
|
||||
|
||||
There are a lot of tools and services providing CI functionality. Here are a few that people have used
|
||||
with Evennia:
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Continuous-Integration-Travis.md
|
||||
Continuous-Integration-TeamCity.md
|
||||
|
||||
```
|
||||
|
||||
[This is an overview of other tools](https://www.atlassian.com/continuous-delivery/continuous-integration/tools)
|
||||
(external link).
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
# 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/).
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
# Profiling
|
||||
|
||||
*This is considered an advanced topic mainly of interest to server developers.*
|
||||
|
||||
## Introduction
|
||||
|
||||
Sometimes it can be useful to try to determine just how efficient a particular
|
||||
piece of code is, or to figure out if one could speed up things more than they
|
||||
are. There are many ways to test the performance of Python and the running
|
||||
server.
|
||||
|
||||
Before digging into this section, remember Donald Knuth's
|
||||
[words of wisdom](https://en.wikipedia.org/wiki/Program_optimization#When_to_optimize):
|
||||
|
||||
> *[...]about 97% of the time: Premature optimization is the root of all evil*.
|
||||
|
||||
That is, don't start to try to optimize your code until you have actually
|
||||
identified a need to do so. This means your code must actually be working before
|
||||
you start to consider optimization. Optimization will also often make your code
|
||||
more complex and harder to read. Consider readability and maintainability and
|
||||
you may find that a small gain in speed is just not worth it.
|
||||
|
||||
## Simple timer tests
|
||||
|
||||
Python's `timeit` module is very good for testing small things. For example, in
|
||||
order to test if it is faster to use a `for` loop or a list comprehension you
|
||||
could use the following code:
|
||||
|
||||
```python
|
||||
import timeit
|
||||
# Time to do 1000000 for loops
|
||||
timeit.timeit("for i in range(100):\n a.append(i)", setup="a = []")
|
||||
<<< 10.70982813835144
|
||||
# Time to do 1000000 list comprehensions
|
||||
timeit.timeit("a = [i for i in range(100)]")
|
||||
<<< 5.358283996582031
|
||||
```
|
||||
|
||||
The `setup` keyword is used to set up things that should not be included in the
|
||||
time measurement, like `a = []` in the first call.
|
||||
|
||||
By default the `timeit` function will re-run the given test 1000000 times and
|
||||
returns the *total time* to do so (so *not* the average per test). A hint is to
|
||||
not use this default for testing something that includes database writes - for
|
||||
that you may want to use a lower number of repeats (say 100 or 1000) using the
|
||||
`number=100` keyword.
|
||||
|
||||
## Using cProfile
|
||||
|
||||
Python comes with its own profiler, named cProfile (this is for cPython, no
|
||||
tests have been done with `pypy` at this point). Due to the way Evennia's
|
||||
processes are handled, there is no point in using the normal way to start the
|
||||
profiler (`python -m cProfile evennia.py`). Instead you start the profiler
|
||||
through the launcher:
|
||||
|
||||
evennia --profiler start
|
||||
|
||||
This will start Evennia with the Server component running (in daemon mode) under
|
||||
cProfile. You could instead try `--profile` with the `portal` argument to
|
||||
profile the Portal (you would then need to
|
||||
[start the Server separately](../Setup/Start-Stop-Reload.md)).
|
||||
|
||||
Please note that while the profiler is running, your process will use a lot more
|
||||
memory than usual. Memory usage is even likely to climb over time. So don't
|
||||
leave it running perpetually but monitor it carefully (for example using the
|
||||
`top` command on Linux or the Task Manager's memory display on Windows).
|
||||
|
||||
Once you have run the server for a while, you need to stop it so the profiler
|
||||
can give its report. Do *not* kill the program from your task manager or by
|
||||
sending it a kill signal - this will most likely also mess with the profiler.
|
||||
Instead either use `evennia.py stop` or (which may be even better), use
|
||||
`@shutdown` from inside the game.
|
||||
|
||||
Once the server has fully shut down (this may be a lot slower than usual) you
|
||||
will find that profiler has created a new file `mygame/server/logs/server.prof`.
|
||||
|
||||
### Analyzing the profile
|
||||
|
||||
The `server.prof` file is a binary file. There are many ways to analyze and
|
||||
display its contents, all of which has only been tested in Linux (If you are a
|
||||
Windows/Mac user, let us know what works).
|
||||
|
||||
You can look at the contents of the profile file with Python's in-built `pstats`
|
||||
module in the evennia shell (it's recommended you install `ipython` with `pip
|
||||
install ipython` in your virtualenv first, for prettier output):
|
||||
|
||||
evennia shell
|
||||
|
||||
Then in the shell
|
||||
|
||||
```python
|
||||
import pstats
|
||||
from pstats import SortKey
|
||||
|
||||
p = pstats.Stats('server/log/server.prof')
|
||||
p.strip_dirs().sort_stats(-1).print_stats()
|
||||
|
||||
```
|
||||
|
||||
See the
|
||||
[Python profiling documentation](https://docs.python.org/3/library/profile.html#instant-user-s-manual)
|
||||
for more information.
|
||||
|
||||
You can also visualize the data in various ways.
|
||||
- [Runsnake](https://pypi.org/project/RunSnakeRun/) visualizes the profile to
|
||||
give a good overview. Install with `pip install runsnakerun`. Note that this
|
||||
may require a C compiler and be quite slow to install.
|
||||
- For more detailed listing of usage time, you can use
|
||||
[KCachegrind](http://kcachegrind.sourceforge.net/html/Home.html). To make
|
||||
KCachegrind work with Python profiles you also need the wrapper script
|
||||
[pyprof2calltree](https://pypi.python.org/pypi/pyprof2calltree/). You can get
|
||||
`pyprof2calltree` via `pip` whereas KCacheGrind is something you need to get
|
||||
via your package manager or their homepage.
|
||||
|
||||
How to analyze and interpret profiling data is not a trivial issue and depends
|
||||
on what you are profiling for. Evennia being an asynchronous server can also
|
||||
confuse profiling. Ask on the mailing list if you need help and be ready to be
|
||||
able to supply your `server.prof` file for comparison, along with the exact
|
||||
conditions under which it was obtained.
|
||||
|
||||
## The Dummyrunner
|
||||
|
||||
It is difficult to test "actual" game performance without having players in your
|
||||
game. For this reason Evennia comes with the *Dummyrunner* system. The
|
||||
Dummyrunner is a stress-testing system: a separate program that logs into your
|
||||
game with simulated players (aka "bots" or "dummies"). Once connected, these
|
||||
dummies will semi-randomly perform various tasks from a list of possible
|
||||
actions. Use `Ctrl-C` to stop the Dummyrunner.
|
||||
|
||||
```{warning}
|
||||
|
||||
You should not run the Dummyrunner on a production database. It
|
||||
will spawn many objects and also needs to run with general permissions.
|
||||
|
||||
This is the recommended process for using the dummy runner:
|
||||
```
|
||||
|
||||
1. Stop your server completely with `evennia stop`.
|
||||
1. At _the end_ of your `mygame/server/conf.settings.py` file, add the line
|
||||
|
||||
from evennia.server.profiling.settings_mixin import *
|
||||
|
||||
This will override your settings and disable Evennia's rate limiters and
|
||||
DoS-protections, which would otherwise block mass-connecting clients from
|
||||
one IP. Notably, it will also change to a different (faster) password hasher.
|
||||
1. (recommended): Build a new database. If you use default Sqlite3 and want to
|
||||
keep your existing database, just rename `mygame/server/evennia.db3` to
|
||||
`mygame/server/evennia.db3_backup` and run `evennia migrate` and `evennia
|
||||
start` to create a new superuser as usual.
|
||||
1. (recommended) Log into the game as your superuser. This is just so you
|
||||
can manually check response. If you kept an old database, you will _not_
|
||||
be able to connect with an _existing_ user since the password hasher changed!
|
||||
1. Start the dummyrunner with 10 dummy users from the terminal with
|
||||
|
||||
evennia --dummyrunner 10
|
||||
|
||||
Use `Ctrl-C` (or `Cmd-C`) to stop it.
|
||||
|
||||
If you want to see what the dummies are actually doing you can run with a single
|
||||
dummy:
|
||||
|
||||
evennia --dummyrunner 1
|
||||
|
||||
The inputs/outputs from the dummy will then be printed. By default the runner
|
||||
uses the 'looker' profile, which just logs in and sends the 'look' command
|
||||
over and over. To change the settings, copy the file
|
||||
`evennia/server/profiling/dummyrunner_settings.py` to your `mygame/server/conf/`
|
||||
directory, then add this line to your settings file to use it in the new
|
||||
location:
|
||||
|
||||
DUMMYRUNNER_SETTINGS_MODULE = "server/conf/dummyrunner_settings.py"
|
||||
|
||||
The dummyrunner settings file is a python code module in its own right - it
|
||||
defines the actions available to the dummies. These are just tuples of command
|
||||
strings (like "look here") for the dummy to send to the server along with a
|
||||
probability of them happening. The dummyrunner looks for a global variable
|
||||
`ACTIONS`, a list of tuples, where the first two elements define the
|
||||
commands for logging in/out of the server.
|
||||
|
||||
Below is a simplified minimal setup (the default settings file adds a lot more
|
||||
functionality and info):
|
||||
|
||||
```python
|
||||
# minimal dummyrunner setup file
|
||||
|
||||
# Time between each dummyrunner "tick", in seconds. Each dummy will be called
|
||||
# with this frequency.
|
||||
TIMESTEP = 1
|
||||
|
||||
# Chance of a dummy actually performing an action on a given tick. This
|
||||
# spreads out usage randomly, like it would be in reality.
|
||||
CHANCE_OF_ACTION = 0.5
|
||||
|
||||
# Chance of a currently unlogged-in dummy performing its login action every
|
||||
# tick. This emulates not all accounts logging in at exactly the same time.
|
||||
CHANCE_OF_LOGIN = 0.01
|
||||
|
||||
# Which telnet port to connect to. If set to None, uses the first default
|
||||
# telnet port of the running server.
|
||||
TELNET_PORT = None
|
||||
|
||||
# actions
|
||||
|
||||
def c_login(client):
|
||||
name = f"Character-{client.gid}"
|
||||
pwd = f"23fwsf23sdfw23wef23"
|
||||
return (
|
||||
f"create {name} {pwd}"
|
||||
f"connect {name} {pwd}"
|
||||
)
|
||||
|
||||
def c_logout(client):
|
||||
return ("quit", )
|
||||
|
||||
def c_look(client):
|
||||
return ("look here", "look me")
|
||||
|
||||
# this is read by dummyrunner.
|
||||
ACTIONS = (
|
||||
c_login,
|
||||
c_logout,
|
||||
(1.0, c_look) # (probability, command-generator)
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
At the bottom of the default file are a few default profiles you can test out
|
||||
by just setting the `PROFILE` variable to one of the options.
|
||||
|
||||
### Dummyrunner hints
|
||||
|
||||
- Don't start with too many dummies. The Dummyrunner taxes the server much more
|
||||
than 'real' users tend to do. Start with 10-100 to begin with.
|
||||
- Stress-testing can be fun, but also consider what a 'realistic' number of
|
||||
users would be for your game.
|
||||
- Note in the dummyrunner output how many commands/s are being sent to the
|
||||
server by all dummies. This is usually a lot higher than what you'd
|
||||
realistically expect to see from the same number of users.
|
||||
- The default settings sets up a 'lag' measure to measaure the round-about
|
||||
message time. It updates with an average every 30 seconds. It can be worth to
|
||||
have this running for a small number of dummies in one terminal before adding
|
||||
more by starting another dummyrunner in another terminal - the first one will
|
||||
act as a measure of how lag changes with different loads. Also verify the
|
||||
lag-times by entering commands manually in-game.
|
||||
- Check the CPU usage of your server using `top/htop` (linux). In-game, use the
|
||||
`server` command.
|
||||
- You can run the server with `--profiler start` to test it with dummies. Note
|
||||
that the profiler will itself affect server performance, especially memory
|
||||
consumption.
|
||||
- Generally, the dummyrunner system makes for a decent test of general
|
||||
performance; but it is of course hard to actually mimic human user behavior.
|
||||
For this, actual real-game testing is required.
|
||||
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
# Quirks
|
||||
|
||||
|
||||
This is a list of various quirks or common stumbling blocks that people often ask about or report
|
||||
when using (or trying to use) Evennia. They are not bugs.
|
||||
|
||||
## Forgetting to use `reload` to see changes to your typeclasses
|
||||
|
||||
Firstly: Reloading the server is a safe and usually quick operation which will *not* disconnect any
|
||||
accounts.
|
||||
|
||||
New users tend to forget this step. When editing source code (such as when tweaking typeclasses and
|
||||
commands or adding new commands to command sets) you need to either use the in-game `@reload`
|
||||
command or, from the command line do `python evennia.py reload` before you see your changes.
|
||||
|
||||
## Web admin to create new Account
|
||||
|
||||
If you use the default login system and are trying to use the Web admin to create a new Player
|
||||
account, you need to consider which `MULTIACCOUNT_MODE` you are in. If you are in
|
||||
`MULTIACCOUNT_MODE` `0` or `1`, the login system expects each Account to also have a Character
|
||||
object named the same as the Account - there is no character creation screen by default. If using
|
||||
the normal mud login screen, a Character with the same name is automatically created and connected
|
||||
to your Account. From the web interface you must do this manually.
|
||||
|
||||
So, when creating the Account, make sure to also create the Character *from the same form* as you
|
||||
create the Account from. This should set everything up for you. Otherwise you need to manually set
|
||||
the "account" property on the Character and the "character" property on the Account to point to each
|
||||
other. You must also set the lockstring of the Character to allow the Account to "puppet" this
|
||||
particular character.
|
||||
|
||||
## Mutable attributes and their connection to the database
|
||||
|
||||
When storing a mutable object (usually a list or a dictionary) in an Attribute
|
||||
|
||||
```python
|
||||
object.db.mylist = [1,2,3]
|
||||
```
|
||||
|
||||
you should know that the connection to the database is retained also if you later extract that
|
||||
Attribute into another variable (what is stored and retrieved is actually a `PackedList` or a
|
||||
`PackedDict` that works just like their namesakes except they save themselves to the database when
|
||||
changed). So if you do
|
||||
|
||||
```python
|
||||
alist = object.db.mylist
|
||||
alist.append(4)
|
||||
```
|
||||
|
||||
this updates the database behind the scenes, so both `alist` and `object.db.mylist` are now
|
||||
`[1,2,3,4]`
|
||||
|
||||
If you don't want this, Evennia provides a way to stably disconnect the mutable from the database by
|
||||
use of `evennia.utils.dbserialize.deserialize`:
|
||||
|
||||
```python
|
||||
from evennia.utils.dbserialize import deserialize
|
||||
|
||||
blist = deserialize(object.db.mylist)
|
||||
blist.append(4)
|
||||
```
|
||||
|
||||
The property `blist` is now `[1,2,3,4]` whereas `object.db.mylist` remains unchanged. If you want to
|
||||
update the database you'd need to explicitly re-assign the updated data to the `mylist` Attribute.
|
||||
|
||||
## Commands are matched by name *or* alias
|
||||
|
||||
When merging [command sets](../Components/Commands.md) it's important to remember that command objects are identified
|
||||
*both* by key *or* alias. So if you have a command with a key `look` and an alias `ls`, introducing
|
||||
another command with a key `ls` will be assumed by the system to be *identical* to the first one.
|
||||
This usually means merging cmdsets will overload one of them depending on priority. Whereas this is
|
||||
logical once you know how command objects are handled, it may be confusing if you are just looking
|
||||
at the command strings thinking they are parsed as-is.
|
||||
|
||||
## Objects turning to `DefaultObject`
|
||||
|
||||
A common confusing error for new developers is finding that one or more objects in-game are suddenly
|
||||
of the type `DefaultObject` rather than the typeclass you wanted it to be. 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 to get to the typeclasses within. To keep on running,
|
||||
Evennia will solve this by printing the full traceback to the terminal/console and temporarily fall
|
||||
back to the safe `DefaultObject` until you fix the problem and reload. Most errors of this kind will
|
||||
be caught by any good text editors. Keep an eye on the terminal/console during a reload to catch
|
||||
such errors - you may have to scroll up if your window is small.
|
||||
|
||||
## Overriding of magic methods
|
||||
|
||||
Python implements a system of [magic
|
||||
methods](https://docs.python.org/3/reference/datamodel.html#emulating-container-types), usually
|
||||
prefixed and suffixed by double-underscores (`__example__`) that allow object instances to have
|
||||
certain operations performed on them without needing to do things like turn them into strings or
|
||||
numbers first-- for example, is `obj1` greater than or equal to `obj2`?
|
||||
|
||||
Neither object is a number, but given `obj1.size == "small"` and `obj2.size == "large"`, how might
|
||||
one compare these two arbitrary English adjective strings to figure out which is greater than the
|
||||
other? By defining the `__ge__` (greater than or equal to) magic method on the object class in which
|
||||
you figure out which word has greater significance, perhaps through use of a mapping table
|
||||
(`{'small':0, 'large':10}`) or other lookup and comparing the numeric values of each.
|
||||
|
||||
Evennia extensively makes use of magic methods on typeclasses to do things like initialize objects,
|
||||
check object existence or iterate over objects in an inventory or container. If you override or
|
||||
interfere with the return values from the methods Evennia expects to be both present and working, it
|
||||
can result in very inconsistent and hard-to-diagnose errors.
|
||||
|
||||
The moral of the story-- it can be dangerous to tinker with magic methods on typeclassed objects.
|
||||
Try to avoid doing so.
|
||||
|
||||
## Things to remember about the flat API
|
||||
|
||||
The flat API is a series of 'shortcuts' on the `evennia` main library root (defined in
|
||||
`evennia/__init__.py`). Its componentas are documented [as part of the auto-documentation](../Evennia-API.md).
|
||||
|
||||
### To remember when importing from `evennia`
|
||||
|
||||
Properties on the root of the `evennia` package are *not* modules in their own right. They are just
|
||||
shortcut properties stored in the `evennia/__init__.py` module. That means that you cannot use dot-
|
||||
notation to `import` nested module-names over `evennia`. The rule of thumb is that you cannot use
|
||||
`import` for more than one level down. Hence you can do
|
||||
|
||||
```python
|
||||
import evennia
|
||||
print(evennia.default_cmds.CmdLook)
|
||||
```
|
||||
|
||||
or import one level down
|
||||
|
||||
```python
|
||||
from evennia import default_cmds
|
||||
print(default_cmds.CmdLook)
|
||||
```
|
||||
|
||||
but you *cannot* import two levels down
|
||||
|
||||
```python
|
||||
from evennia.default_cmds import CmdLook # error!
|
||||
```
|
||||
|
||||
This will give you an `ImportError` telling you that the module `default_cmds` cannot be found -
|
||||
this is becasue `default_cmds` is just a *variable* stored in `evennia.__init__.py`; this cannot be
|
||||
imported from. If you really want full control over which level of package you import you can always
|
||||
bypass the root package and import directly from from the real location. For example
|
||||
`evennia.DefaultObject` is a shortcut to `evennia.objects.objects.DefaultObject`. Using this full
|
||||
path will have the import mechanism work normally. See `evennia/__init__.py` to see where the
|
||||
package imports from.
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
# Setting up PyCharm with Evennia
|
||||
|
||||
[PyCharm](https://www.jetbrains.com/pycharm/) is a Python developer's IDE from Jetbrains available
|
||||
for Windows, Mac and Linux. It is a commercial product but offer free trials, a scaled-down
|
||||
community edition and also generous licenses for OSS projects like Evennia.
|
||||
|
||||
> This page was originally tested on Windows (so use Windows-style path examples), but should work
|
||||
the same for all platforms.
|
||||
|
||||
First, install Evennia on your local machine with [[Getting Started]]. If you're new to PyCharm,
|
||||
loading your project is as easy as selecting the `Open` option when PyCharm starts, and browsing to
|
||||
your game folder (the one created with `evennia --init`). We refer to it as `mygame` here.
|
||||
|
||||
If you want to be able to examine evennia's core code or the scripts inside your virtualenv, you'll
|
||||
need to add them to your project too:
|
||||
1. Go to `File > Open...`
|
||||
1. Select the folder (i.e. the `evennia` root)
|
||||
1. Select "Open in current window" and "Add to currently opened projects"
|
||||
|
||||
## Setting up the project interpreter
|
||||
|
||||
It's a good idea to do this before attempting anything further. The rest of this page assumes your
|
||||
project is already configured in PyCharm.
|
||||
|
||||
1. Go to `File > Settings... > Project: \<mygame\> > Project Interpreter`
|
||||
1. Click the Gear symbol `> Add local`
|
||||
1. Navigate to your `evenv/scripts directory`, and select Python.exe
|
||||
|
||||
Enjoy seeing all your imports checked properly, setting breakpoints, and live variable watching!
|
||||
|
||||
## Attaching PyCharm debugger to Evennia
|
||||
|
||||
1. Launch Evennia in your preferred way (usually from a console/terminal)
|
||||
1. Open your project in PyCharm
|
||||
1. In the PyCharm menu, select `Run > Attach to Local Process...`
|
||||
1. From the list, pick the `twistd` process with the `server.py` parameter (Example: `twistd.exe
|
||||
--nodaemon --logfile=\<mygame\>\server\logs\server.log --python=\<evennia
|
||||
repo\>\evennia\server\server.py`)
|
||||
|
||||
Of course you can attach to the `portal` process as well. If you want to debug the Evennia launcher
|
||||
or runner for some reason (or just learn how they work!), see Run Configuration below.
|
||||
|
||||
> NOTE: Whenever you reload Evennia, the old Server process will die and a new one start. So when
|
||||
you restart you have to detach from the old and then reattach to the new process that was created.
|
||||
|
||||
> To make the process less tedious you can apply a filter in settings to show only the server.py
|
||||
process in the list. To do that navigate to: `Settings/Preferences | Build, Execution, Deployment |
|
||||
Python Debugger` and then in `Attach to process` field put in: `twistd.exe" --nodaemon`. This is an
|
||||
example for windows, I don't have a working mac/linux box.
|
||||

|
||||
|
||||
## Setting up an Evennia run configuration
|
||||
|
||||
This configuration allows you to launch Evennia from inside PyCharm. Besides convenience, it also
|
||||
allows suspending and debugging the evennia_launcher or evennia_runner at points earlier than you
|
||||
could by running them externally and attaching. In fact by the time the server and/or portal are
|
||||
running the launcher will have exited already.
|
||||
|
||||
1. Go to `Run > Edit Configutations...`
|
||||
1. Click the plus-symbol to add a new configuration and choose Python
|
||||
1. Add the script: `\<yourrepo\>\evenv\Scripts\evennia_launcher.py` (substitute your virtualenv if
|
||||
it's not named `evenv`)
|
||||
1. Set script parameters to: `start -l` (-l enables console logging)
|
||||
1. Ensure the chosen interpreter is from your virtualenv
|
||||
1. Set Working directory to your `mygame` folder (not evenv nor evennia)
|
||||
1. You can refer to the PyCharm documentation for general info, but you'll want to set at least a
|
||||
config name (like "MyMUD start" or similar).
|
||||
|
||||
Now set up a "stop" configuration by following the same steps as above, but set your Script
|
||||
parameters to: stop (and name the configuration appropriately).
|
||||
|
||||
A dropdown box holding your new configurations should appear next to your PyCharm run button.
|
||||
Select MyMUD start and press the debug icon to begin debugging. Depending on how far you let the
|
||||
program run, you may need to run your "MyMUD stop" config to actually stop the server, before you'll
|
||||
be able start it again.
|
||||
|
||||
## Alternative run configuration - utilizing logfiles as source of data
|
||||
|
||||
This configuration takes a bit different approach as instead of focusing on getting the data back
|
||||
through logfiles. Reason for that is this way you can easily separate data streams, for example you
|
||||
rarely want to follow both server and portal at the same time, and this will allow it. This will
|
||||
also make sure to stop the evennia before starting it, essentially working as reload command (it
|
||||
will also include instructions how to disable that part of functionality). We will start by defining
|
||||
a configuration that will stop evennia. This assumes that `upfire` is your pycharm project name, and
|
||||
also the game name, hence the `upfire/upfire` path.
|
||||
|
||||
1. Go to `Run > Edit Configutations...`\
|
||||
1. Click the plus-symbol to add a new configuration and choose the python interpreter to use (should
|
||||
be project default)
|
||||
1. Name the configuration as "stop evennia" and fill rest of the fields accordingly to the image:
|
||||

|
||||
1. Press `Apply`
|
||||
|
||||
Now we will define the start/reload command that will make sure that evennia is not running already,
|
||||
and then start the server in one go.
|
||||
1. Go to `Run > Edit Configutations...`\
|
||||
1. Click the plus-symbol to add a new configuration and choose the python interpreter to use (should
|
||||
be project default)
|
||||
1. Name the configuration as "start evennia" and fill rest of the fields accordingly to the image:
|
||||

|
||||
1. Navigate to the `Logs` tab and add the log files you would like to follow. The picture shows
|
||||
adding `portal.log` which will show itself in `portal` tab when running:
|
||||

|
||||
1. Skip the following steps if you don't want the launcher to stop evennia before starting.
|
||||
1. Head back to `Configuration` tab and press the `+` sign at the bottom, under `Before launch....`
|
||||
and select `Run another configuration` from the submenu that will pop up.
|
||||
1. Click `stop evennia` and make sure that it's added to the list like on the image above.
|
||||
1. Click `Apply` and close the run configuration window.
|
||||
|
||||
You are now ready to go, and if you will fire up `start evennia` configuration you should see
|
||||
following in the bottom panel:
|
||||

|
||||
and you can click through the tabs to check appropriate logs, or even the console output as it is
|
||||
still running in interactive mode.
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
# Unit Testing
|
||||
|
||||
*Unit testing* means testing components of a program in isolation from each other to make sure every
|
||||
part works on its own before using it with others. Extensive testing helps avoid new updates causing
|
||||
unexpected side effects as well as alleviates general code rot (a more comprehensive wikipedia
|
||||
article on unit testing can be found [here](https://en.wikipedia.org/wiki/Unit_test)).
|
||||
|
||||
A typical unit test set calls some function or method with a given input, looks at the result and
|
||||
makes sure that this result looks as expected. Rather than having lots of stand-alone test programs,
|
||||
Evennia makes use of a central *test runner*. This is a program that gathers all available tests all
|
||||
over the Evennia source code (called *test suites*) and runs them all in one go. Errors and
|
||||
tracebacks are reported.
|
||||
|
||||
By default Evennia only tests itself. But you can also add your own tests to your game code and have
|
||||
Evennia run those for you.
|
||||
|
||||
## Running the Evennia test suite
|
||||
|
||||
To run the full Evennia test suite, go to your game folder and issue the command
|
||||
|
||||
evennia test evennia
|
||||
|
||||
This will run all the evennia tests using the default settings. You could also run only a subset of
|
||||
all tests by specifying a subpackage of the library:
|
||||
|
||||
evennia test evennia.commands.default
|
||||
|
||||
A temporary database will be instantiated to manage the tests. If everything works out you will see
|
||||
how many tests were run and how long it took. If something went wrong you will get error messages.
|
||||
If you contribute to Evennia, this is a useful sanity check to see you haven't introduced an
|
||||
unexpected bug.
|
||||
|
||||
## Running tests for your game dir
|
||||
|
||||
If you have implemented your own tests for your game you can run them from your game dir
|
||||
with
|
||||
|
||||
evennia test --settings settings.py .
|
||||
|
||||
The period (`.`) means to run all tests found in the current directory and all subdirectories. You
|
||||
could also specify, say, `typeclasses` or `world` if you wanted to just run tests in those subdirs.
|
||||
|
||||
An important thing to note is that those tests will all be run using the _default Evennia settings_.
|
||||
To run the tests with your own settings file you must use the `--settings` option:
|
||||
|
||||
evennia test --settings settings.py .
|
||||
|
||||
The `--settings` option of Evennia takes a file name in the `mygame/server/conf` folder. It is
|
||||
normally used to swap settings files for testing and development. In combination with `test`, it
|
||||
forces Evennia to use this settings file over the default one.
|
||||
|
||||
You can also test specific things by giving their path
|
||||
|
||||
evennia test --settings settings.py .world.tests.YourTest
|
||||
|
||||
|
||||
## Writing new tests
|
||||
|
||||
Evennia's test suite makes use of Django unit test system, which in turn relies on Python's
|
||||
*unittest* module.
|
||||
|
||||
To make the test runner find the tests, they must be put in a module named `test*.py` (so `test.py`,
|
||||
`tests.py` etc). Such a test module will be found wherever it is in the package. It can be a good
|
||||
idea to look at some of Evennia's `tests.py` modules to see how they look.
|
||||
|
||||
Inside the module you need to put a class inheriting (at any distance) from `unittest.TestCase`. Each
|
||||
method on that class that starts with `test_` will be run separately as a unit test. There
|
||||
are two special, optional methods `setUp` and `tearDown` that will (if you define them) run before
|
||||
_every_ test. This can be useful for setting up and deleting things.
|
||||
|
||||
To actually test things, you use special `assert...` methods on the class. Most common on is
|
||||
`assertEqual`, which makes sure a result is what you expect it to be.
|
||||
|
||||
Here's an example of the principle. Let's assume you put this in `mygame/world/tests.py`
|
||||
and want to test a function in `mygame/world/myfunctions.py`
|
||||
|
||||
```python
|
||||
# in a module tests.py somewhere i your game dir
|
||||
import unittest
|
||||
|
||||
from evennia import create_object
|
||||
# the function we want to test
|
||||
from .myfunctions import myfunc
|
||||
|
||||
|
||||
class TestObj(unittest.TestCase):
|
||||
"This tests a function myfunc."
|
||||
|
||||
def setUp(self):
|
||||
"""done before every of the test_ * methods below"""
|
||||
self.obj = create_object("mytestobject")
|
||||
|
||||
def tearDown(self):
|
||||
"""done after every test_* method below """
|
||||
self.obj.delete()
|
||||
|
||||
def test_return_value(self):
|
||||
"""test method. Makes sure return value is as expected."""
|
||||
actual_return = myfunc(self.obj)
|
||||
expected_return = "This is the good object 'mytestobject'."
|
||||
# test
|
||||
self.assertEqual(expected_return, actual_return)
|
||||
def test_alternative_call(self):
|
||||
"""test method. Calls with a keyword argument."""
|
||||
actual_return = myfunc(self.obj, bad=True)
|
||||
expected_return = "This is the baaad object 'mytestobject'."
|
||||
# test
|
||||
self.assertEqual(expected_return, actual_return)
|
||||
```
|
||||
|
||||
To test this, run
|
||||
|
||||
evennia test --settings settings.py .
|
||||
|
||||
to run the entire test module
|
||||
|
||||
evennia test --settings setings.py .world.tests
|
||||
|
||||
or a specific class:
|
||||
|
||||
evennia test --settings settings.py .world.tests.TestObj
|
||||
|
||||
You can also run a specific test:
|
||||
|
||||
evennia test --settings settings.py .world.tests.TestObj.test_alternative_call
|
||||
|
||||
You might also want to read the [Python documentation for the unittest module](https://docs.python.org/library/unittest.html).
|
||||
|
||||
## Using the Evennia testing classes
|
||||
|
||||
Evennia offers many custom testing classes that helps with testing Evennia features.
|
||||
They are all found in [evennia.utils.test_resources](evennia.utils.test_resources). Note that
|
||||
these classes implement the `setUp` and `tearDown` already, so if you want to add stuff in them
|
||||
yourself you should remember to use e.g. `super().setUp()` in your code.
|
||||
|
||||
### Classes for testing your game dir
|
||||
|
||||
These all use whatever setting you pass to them and works well for testing code in your game dir.
|
||||
|
||||
- `EvenniaTest` - this sets up a full object environment for your test. All the created entities
|
||||
can be accesses as properties on the class:
|
||||
- `.account` - A fake [Account](evennia.accounts.accounts.DefaultAccount) named "TestAccount".
|
||||
- `.account2` - Another account named "TestAccount2"
|
||||
- `char1` - A [Character](evennia.objects.objects.DefaultCharacter) linked to `.account`, named `Char`.
|
||||
This has 'Developer' permissions but is not a superuser.
|
||||
- `.char2` - Another character linked to `account`, named `Char2`. This has base permissions (player).
|
||||
- `.obj1` - A regular [Object](evennia.objects.objects.DefaultObject) named "Obj".
|
||||
- `.obj2` - Another object named "Obj2".
|
||||
- `.room1` - A [Room](evennia.objects.objects.DefaultRoom) named "Room". Both characters and both
|
||||
objects are located inside this room. It has a description of "room_desc".
|
||||
- `.room2` - Another room named "Room2". It is empty and has no set description.
|
||||
- `.exit` - An exit named "out" that leads from `.room1` to `.room2`.
|
||||
- `.script` - A [Script](evennia.scripts.scripts.DefaultScript) named "Script". It's an inert script
|
||||
without a timing component.
|
||||
- `.session` - A fake [Session](evennia.server.serversession.ServerSession) that mimics a player
|
||||
connecting to the game. It is used by `.account1` and has a sessid of 1.
|
||||
- `EvenniaCommandTest` - has the same environment like `EvenniaTest` but also adds a special
|
||||
[.call()](evennia.utils.test_resources.EvenniaCommandTestMixin.call) method specifically for
|
||||
testing Evennia [Commands](../Components/Commands.md). It allows you to compare what the command _actually_
|
||||
returns to the player with what you expect. Read the `call` api doc for more info.
|
||||
- `EvenniaTestCase` - This is identical to the regular Python `TestCase` class, it's
|
||||
just there for naming symmetry with `BaseEvenniaTestCase` below.
|
||||
|
||||
Here's an example of using `EvenniaTest`
|
||||
|
||||
```python
|
||||
# in a test module
|
||||
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
||||
class TestObject(EvenniaTest):
|
||||
"""Remember that the testing class creates char1 and char2 inside room1 ..."""
|
||||
def test_object_search_character(self):
|
||||
"""Check that char1 can search for char2 by name"""
|
||||
self.assertEqual(self.char1.search(self.char2.key), self.char2)
|
||||
|
||||
def test_location_search(self):
|
||||
"""Check so that char1 can find the current location by name"""
|
||||
self.assertEqual(self.char1.search(self.char1.location.key), self.char1.location)
|
||||
# ...
|
||||
```
|
||||
|
||||
This example tests a custom command.
|
||||
|
||||
```python
|
||||
from evennia.commands.default.tests import EvenniaCommandTest
|
||||
from commands import command as mycommand
|
||||
|
||||
|
||||
class TestSet(EvenniaCommandTest):
|
||||
"tests the look command by simple call, using Char2 as a target"
|
||||
|
||||
def test_mycmd_char(self):
|
||||
self.call(mycommand.CmdMyLook(), "Char2", "Char2(#7)")
|
||||
|
||||
def test_mycmd_room(self):
|
||||
"tests the look command by simple call, with target as room"
|
||||
self.call(mycommand.CmdMyLook(), "Room",
|
||||
"Room(#1)\nroom_desc\nExits: out(#3)\n"
|
||||
"You see: Obj(#4), Obj2(#5), Char2(#7)")
|
||||
```
|
||||
|
||||
When using `.call`, you don't need to specify the entire string; you can just give the beginning
|
||||
of it and if it matches, that's enough. Use `\n` to denote line breaks and (this is a special for
|
||||
the `.call` helper), `||` to indicate multiple uses of `.msg()` in the Command. The `.call` helper
|
||||
has a lot of arguments for mimicing different ways of calling a Command, so make sure to
|
||||
[read the API docs for .call()](evennia.utils.test_resources.EvenniaCommandTestMixin.call).
|
||||
|
||||
### Classes for testing Evennia core
|
||||
|
||||
These are used for testing Evennia itself. They provide the same resources as the classes
|
||||
above but enforce Evennias default settings found in `evennia/settings_default.py`, ignoring
|
||||
any settings changes in your game dir.
|
||||
|
||||
- `BaseEvenniaTest` - all the default objects above but with enforced default settings
|
||||
- `BaseEvenniaCommandTest` - for testing Commands, but with enforced default settings
|
||||
- `BaseEvenniaTestCase` - no default objects, only enforced default settings
|
||||
|
||||
There are also two special 'mixin' classes. These are uses in the classes above, but may also
|
||||
be useful if you want to mix your own testing classes:
|
||||
|
||||
- `EvenniaTestMixin` - A class mixin that creates all test environment objects.
|
||||
- `EvenniaCommandMixin` - A class mixin that adds the `.call()` Command-tester helper.
|
||||
|
||||
If you want to help out writing unittests for Evennia, take a look at Evennia's [coveralls.io
|
||||
page](https://coveralls.io/github/evennia/evennia). There you see which modules have any form of
|
||||
test coverage and which does not. All help is appreciated!
|
||||
|
||||
## Unit testing contribs with custom models
|
||||
|
||||
A special case is if you were to create a contribution to go to the `evennia/contrib` folder that
|
||||
uses its [own database models](../Concepts/New-Models.md). The problem with this is that Evennia (and Django) will
|
||||
only recognize models in `settings.INSTALLED_APPS`. If a user wants to use your contrib, they will
|
||||
be required to add your models to their settings file. But since contribs are optional you cannot
|
||||
add the model to Evennia's central `settings_default.py` file - this would always create your
|
||||
optional models regardless of if the user wants them. But at the same time a contribution is a part
|
||||
of the Evennia distribution and its unit tests should be run with all other Evennia tests using
|
||||
`evennia test evennia`.
|
||||
|
||||
The way to do this is to only temporarily add your models to the `INSTALLED_APPS` directory when the
|
||||
test runs. here is an example of how to do it.
|
||||
|
||||
> Note that this solution, derived from this [stackexchange
|
||||
answer](http://stackoverflow.com/questions/502916/django-how-to-create-a-model-dynamically-just-for-
|
||||
testing#503435) is currently untested! Please report your findings.
|
||||
|
||||
```python
|
||||
# a file contrib/mycontrib/tests.py
|
||||
|
||||
from django.conf import settings
|
||||
import django
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
OLD_DEFAULT_SETTINGS = settings.INSTALLED_APPS
|
||||
DEFAULT_SETTINGS = dict(
|
||||
INSTALLED_APPS=(
|
||||
'contrib.mycontrib.tests',
|
||||
),
|
||||
DATABASES={
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3"
|
||||
}
|
||||
},
|
||||
SILENCED_SYSTEM_CHECKS=["1_7.W001"],
|
||||
)
|
||||
|
||||
|
||||
class TestMyModel(BaseEvenniaTest):
|
||||
def setUp(self):
|
||||
if not settings.configured:
|
||||
settings.configure(**DEFAULT_SETTINGS)
|
||||
django.setup()
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.db.models import loading
|
||||
loading.cache.loaded = False
|
||||
call_command('syncdb', verbosity=0)
|
||||
|
||||
def tearDown(self):
|
||||
settings.configure(**OLD_DEFAULT_SETTINGS)
|
||||
django.setup()
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.db.models import loading
|
||||
loading.cache.loaded = False
|
||||
call_command('syncdb', verbosity=0)
|
||||
|
||||
# test cases below ...
|
||||
|
||||
def test_case(self):
|
||||
# test case here
|
||||
```
|
||||
|
||||
|
||||
## A note on making the test runner faster
|
||||
|
||||
If you have custom models with a large number of migrations, creating the test database can take a
|
||||
very long time. If you don't require migrations to run for your tests, you can disable them with the
|
||||
django-test-without-migrations package. To install it, simply:
|
||||
|
||||
```
|
||||
$ pip install django-test-without-migrations
|
||||
```
|
||||
|
||||
Then add it to your `INSTALLED_APPS` in your `server.conf.settings.py`:
|
||||
|
||||
```python
|
||||
INSTALLED_APPS = (
|
||||
# ...
|
||||
'test_without_migrations',
|
||||
)
|
||||
```
|
||||
|
||||
After doing so, you can then run tests without migrations by adding the `--nomigrations` argument:
|
||||
|
||||
```
|
||||
evennia test --settings settings.py --nomigrations .
|
||||
```
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
# Updating Your Game
|
||||
|
||||
|
||||
Fortunately, it's extremely easy to keep your Evennia server up-to-date. If you haven't already, see
|
||||
the [Getting Started guide](../Setup/Installation.md) and get everything running.
|
||||
|
||||
## Updating with the latest Evennia code changes
|
||||
|
||||
Very commonly we make changes to the Evennia code to improve things. There are many ways to get told
|
||||
when to update: You can subscribe to the RSS feed or manually check up on the feeds from
|
||||
https://www.evennia.com. You can also simply fetch the latest regularly.
|
||||
|
||||
When you're wanting to apply updates, simply `cd` to your cloned `evennia` root directory and type:
|
||||
|
||||
git pull
|
||||
|
||||
assuming you've got the command line client. If you're using a graphical client, you will probably
|
||||
want to navigate to the `evennia` directory and either right click and find your client's pull
|
||||
function, or use one of the menus (if applicable).
|
||||
|
||||
You can review the latest changes with
|
||||
|
||||
git log
|
||||
|
||||
or the equivalent in the graphical client. You can also see the latest changes online
|
||||
[here](https://github.com/evennia/evennia/blob/master/CHANGELOG.md).
|
||||
|
||||
You will always need to do `evennia reload` (or `reload` from -in-game) from your game-dir to have
|
||||
the new code affect your game. If you want to be really sure you should run a full `evennia reboot`
|
||||
so that both Server and Portal can restart (this will disconnect everyone though, so if you know the
|
||||
Portal has had no updates you don't have to do that).
|
||||
|
||||
## Upgrading Evennia dependencies
|
||||
|
||||
On occasion we update the versions of third-party libraries Evennia depend on (or we may add a new
|
||||
dependency). This will be announced on the mailing list/forum. If you run into errors when starting
|
||||
Evennia, always make sure you have the latest versions of everything. In some cases, like for
|
||||
Django, starting the server may also give warning saying that you are using a working, but too-old
|
||||
version that should not be used in production.
|
||||
|
||||
Upgrading `evennia` will automatically fetch all the latest packages that it now need. First `cd` to
|
||||
your cloned `evennia` folder. Make sure your `virtualenv` is active and use
|
||||
|
||||
pip install --upgrade -e .
|
||||
|
||||
Remember the period (`.`) at the end - that applies the upgrade to the current location (your
|
||||
`evennia` dir).
|
||||
|
||||
> The `-e` means that we are _linking_ the evennia sources rather than copying them into the
|
||||
environment. This means we can most of the time just update the sources (with `git pull`) and see
|
||||
those changes directly applied to our installed `evennia` package. Without installing/upgrading the
|
||||
package with `-e`, we would have to remember to upgrade the package every time we downloaded any new
|
||||
source-code changes.
|
||||
|
||||
Follow the upgrade output to make sure it finishes without errors. To check what packages are
|
||||
currently available in your python environment after the upgrade, use
|
||||
|
||||
pip list
|
||||
|
||||
This will show you the version of all installed packages. The `evennia` package will also show the
|
||||
location of its source code.
|
||||
|
||||
## Migrating the Database Schema
|
||||
|
||||
Whenever we change the database layout of Evennia upstream (such as when we add new features) you
|
||||
will need to *migrate* your existing database. When this happens it will be clearly noted in the
|
||||
`git log` (it will say something to the effect of "Run migrations"). Database changes will also be
|
||||
announced on the Evennia [mailing list](https://groups.google.com/forum/#!forum/evennia).
|
||||
|
||||
When the database schema changes, you just go to your game folder and run
|
||||
|
||||
evennia migrate
|
||||
|
||||
> Hint: If the `evennia` command is not found, you most likely need to activate your
|
||||
[virtualenv](../Glossary.md#virtualenv).
|
||||
|
||||
## Resetting your database
|
||||
|
||||
Should you ever want to start over completely from scratch, there is no need to re-download Evennia
|
||||
or anything like that. You just need to clear your database. Once you are done, you just rebuild it
|
||||
from scratch by running
|
||||
|
||||
evennia migrate
|
||||
|
||||
The first step in wiping your database is to stop Evennia completely with
|
||||
|
||||
evennia stop
|
||||
|
||||
If you run the default `SQlite3` database (to change this you need to edit your `settings.py` file),
|
||||
the database is actually just a normal file in `mygame/server/` called `evennia.db3`. *Simply delete
|
||||
that file* - that's it. Now run `evennia migrate` to recreate a new, fresh one.
|
||||
|
||||
If you run some other database system you can instead flush the database:
|
||||
|
||||
evennia flush
|
||||
|
||||
This will empty the database. However, it will not reset the internal counters of the database, so
|
||||
you will start with higher dbref values. If this is okay, this is all you need.
|
||||
|
||||
Django also offers an easy way to start the database's own management should we want more direct
|
||||
control:
|
||||
|
||||
evennia dbshell
|
||||
|
||||
In e.g. MySQL you can then do something like this (assuming your MySQL database is named "Evennia":
|
||||
|
||||
mysql> DROP DATABASE Evennia;
|
||||
mysql> exit
|
||||
|
||||
> NOTE: Under Windows OS, in order to access SQLite dbshell you need to [download the SQLite
|
||||
command-line shell program](https://www.sqlite.org/download.html). It's a single executable file
|
||||
(sqlite3.exe) that you should place in the root of either your MUD folder or Evennia's (it's the
|
||||
same, in both cases Django will find it).
|
||||
|
||||
## More about schema migrations
|
||||
|
||||
If and when an Evennia update modifies the database *schema* (that is, the under-the-hood details as
|
||||
to how data is stored in the database), you must update your existing database correspondingly to
|
||||
match the change. If you don't, the updated Evennia will complain that it cannot read the database
|
||||
properly. Whereas schema changes should become more and more rare as Evennia matures, it may still
|
||||
happen from time to time.
|
||||
|
||||
One way one could handle this is to apply the changes manually to your database using the database's
|
||||
command line. This often means adding/removing new tables or fields as well as possibly convert
|
||||
existing data to match what the new Evennia version expects. It should be quite obvious that this
|
||||
quickly becomes cumbersome and error-prone. If your database doesn't contain anything critical yet
|
||||
it's probably easiest to simply reset it and start over rather than to bother converting.
|
||||
|
||||
Enter *migrations*. Migrations keeps track of changes in the database schema and applies them
|
||||
automatically for you. Basically, whenever the schema changes we distribute small files called
|
||||
"migrations" with the source. Those tell the system exactly how to implement the change so you don't
|
||||
have to do so manually. When a migration has been added we will tell you so on Evennia's mailing
|
||||
lists and in commit messages -
|
||||
you then just run `evennia migrate` to be up-to-date again.
|
||||
|
|
@ -1,469 +0,0 @@
|
|||
# Version Control
|
||||
|
||||
Version control software allows you to track the changes you make to your code, as well as being
|
||||
able to easily backtrack these changes, share your development efforts and more.
|
||||
|
||||
It's strongly recommended that you put your game code under version control. Version
|
||||
control is also the way to contribue to Evennia itself.
|
||||
|
||||
For an introduction to the concept, start with the Wikipedia article
|
||||
[here](https://en.wikipedia.org/wiki/Version_control). Evennia uses the version
|
||||
control system [Git](https://git-scm.com/) and this is what will be covered
|
||||
henceforth. Note that this page primarily shows commands for Linux, but the
|
||||
syntax should be the same for Windows and Mac.
|
||||
|
||||
For more help on using Git, please refer to the [Official GitHub
|
||||
documentation](https://help.github.com/articles/set-up-git#platform-all).
|
||||
|
||||
## Setting up Git
|
||||
|
||||
You can find expanded instructions for
|
||||
installation [here](https://git-scm.com/book/en/Getting-Started-Installing-Git).
|
||||
|
||||
### Step 1: Install Git
|
||||
|
||||
- **Fedora Linux**
|
||||
|
||||
yum install git-core
|
||||
|
||||
- **Debian Linux** _(Ubuntu, Linux Mint, etc.)_
|
||||
|
||||
apt-get install git
|
||||
|
||||
- **Windows**: It is recommended to use [Git for Windows](https://gitforwindows.org/).
|
||||
- **Mac**: Mac platforms offer two methods for installation, one via MacPorts, which you can find
|
||||
out about [here](https://git-scm.com/book/en/Getting-Started-Installing-Git#Installing-on-Mac), or
|
||||
you can use the [Git OSX Installer](https://sourceforge.net/projects/git-osx-installer/).
|
||||
|
||||
### Step 2: Define user/e-mail Settings for Git
|
||||
|
||||
To avoid a common issue later, you will need to set a couple of settings; first you will need to
|
||||
tell Git your username, followed by your e-mail address, so that when you commit code later you will
|
||||
be properly credited.
|
||||
|
||||
> Note that your commit information will be visible to everyone if you ever contribute to Evennia or
|
||||
use an online service like github to host your code. So if you are not comfortable with using your
|
||||
real, full name online, put a nickname here.
|
||||
|
||||
1. Set the default name for git to use when you commit:
|
||||
|
||||
git config --global user.name "Your Name Here"
|
||||
|
||||
2. Set the default email for git to use when you commit:
|
||||
|
||||
git config --global user.email "your_email@example.com"
|
||||
|
||||
|
||||
## Putting your game folder under version control
|
||||
|
||||
> Note: The game folder's version control is completely separate from Evennia's repository.
|
||||
|
||||
After you have set up your game you will have created a new folder to host your particular game
|
||||
(let's call this folder `mygame` for now).
|
||||
|
||||
This folder is *not* under version control at this point.
|
||||
|
||||
git init mygame
|
||||
|
||||
Your mygame folder is now ready for version control! Add all the content and make a first
|
||||
commit:
|
||||
|
||||
cd mygame
|
||||
git add *
|
||||
git commit -a -m "Initial commit"
|
||||
|
||||
In turn these commands:
|
||||
- Move us into the `mygame` folder
|
||||
- Tell `git` that everything `*` means everything) in this folder should be put
|
||||
under version control.
|
||||
- _Commit_ all (`-a`) those newly added files to git and add a message `-m` so you remember
|
||||
what you did at this point. Doing a commit is like saving a snapshot of the
|
||||
current state of everything.
|
||||
|
||||
Read on for details!
|
||||
|
||||
### Tracking files
|
||||
|
||||
When working on your code or fix bugs in your local branches you may end up creating new files. If
|
||||
you do you must tell Git to track them by using the add command.
|
||||
|
||||
git add <filename>
|
||||
|
||||
You only need to do this once per file.
|
||||
|
||||
git status
|
||||
|
||||
will show if you have any modified, added or otherwise changed files. Some
|
||||
files, like database files, logs and temporary PID files are usually *not*
|
||||
tracked in version control. These should either not show up or have a question
|
||||
mark in front of them.
|
||||
|
||||
```{note}
|
||||
You will notice that some files are not covered by your git version control,
|
||||
notably your settings file (`mygame/server/conf/settings.py`) and your sqlite3
|
||||
database file `mygame/server/evennia.db3`. What is auto-ignored by is controlled
|
||||
by the hidden file `mygame/.gitignore`. Evennia creates this file as part of
|
||||
the creation of your game directory. Everything matched in this file will be
|
||||
ignored by git. If you want to, for example, include your settings file for
|
||||
collaborators to access, remove that entry in `.gitignore`.
|
||||
```
|
||||
|
||||
```{warning}
|
||||
You should *never* put your sqlite3 database file into git by removing its entry
|
||||
in `.gitignore`. GIT is for backing up your code, not your database. That way
|
||||
lies madness and a good chance you'll confuse yourself so that after a few
|
||||
commits and reverts don't know what is in your database or not. If you want to
|
||||
backup your database, do so by simply copying the file on your hard drive to a
|
||||
backup-name.
|
||||
```
|
||||
|
||||
### Committing your Code
|
||||
|
||||
_Committing_ your code means storing the current snapshot of your code within
|
||||
git. This creates a "save point" or "history" of your development process. You
|
||||
can later jump back and forth in your history, for example to figure out just
|
||||
when a bug was introduced or see what results the code used to produce compared
|
||||
to now. Or just wiping everything since the last commit, if you did something
|
||||
stupid.
|
||||
|
||||
It's usually a good idea to commit your changes often. Committing is fast and
|
||||
local only - you will never commit anything online at this point. To commit your
|
||||
changes, use
|
||||
|
||||
git commit --all
|
||||
|
||||
Also `-a` works. This will open a text editor for you to describe your change.
|
||||
Be brief but informative in your message - you'll appreciate it later. When you
|
||||
save and close the editor, the commit will be saved. You can create the message
|
||||
directly with
|
||||
|
||||
git commit -a -m "This fixes a bug in the combat code."
|
||||
|
||||
|
||||
### Changing your mind
|
||||
|
||||
If you have non-committed changes that you realize you want to throw away, you
|
||||
'check out' the file you want - this will re-load it from the last committed
|
||||
state:
|
||||
|
||||
git checkout <file_to_revert>
|
||||
git checkout foo/bar/dummy.py
|
||||
|
||||
If you want to revert _all_ changes you did since last commit, do
|
||||
|
||||
git checkout .
|
||||
|
||||
(that is, add a single `.` at the end).
|
||||
|
||||
### Pushing your code online
|
||||
|
||||
So far your code is only located on your private machine. A good idea is to back
|
||||
it up online. The easiest way to do this is to push it to your own remote
|
||||
repository on GitHub.
|
||||
|
||||
```{important}
|
||||
Just to avoid confusion, be aware that Github's documentation has changed to
|
||||
calling the primary branch 'main' rather than 'master'. While Evennia still
|
||||
uses 'master' branch (and this is what we refer to below), you can use either
|
||||
name for your personal primary branch - they are equivalent.
|
||||
```
|
||||
|
||||
1. Make sure you have your game directory setup under git version control as
|
||||
described in the previous section. Make sure to commit any changes you did.
|
||||
2. Create a new, empty repository on Github. Github explains how
|
||||
[here](https://help.github.com/articles/create-a-repo/) (do *not* "Initialize
|
||||
the repository with a README" or else you'll create unrelated histories).
|
||||
3. From your local game dir, do `git remote add origin <github URL>` where
|
||||
`<github URL>` is the URL to your online repo. This tells your game dir that
|
||||
it should be pushing to the remote online dir.
|
||||
4. `git remote -v` to verify the online dir.
|
||||
5. `git push origin master` (or `git push origin main`) now pushes your game dir
|
||||
online so you can see it on github.com.
|
||||
|
||||
You can commit your work locally (`git commit --all -m "Make a change that
|
||||
..."`) as many times as you want. When you want to push those changes to your
|
||||
online repo, you do `git push`. You can also `git clone <url_to_online_repo>`
|
||||
from your online repo to somewhere else (like your production server) and
|
||||
henceforth do `git pull` to update that to the latest thing you pushed.
|
||||
|
||||
Note that GitHub's repos are, by default publicly visible by all. Creating a
|
||||
publicly visible online clone might not be what you want for all parts of your
|
||||
development process - you may prefer a more private venue when sharing your
|
||||
revolutionary work with your team. If that's the case you can change your
|
||||
repository to "Private" in the github settings. Then your code will only be
|
||||
visible to those you specifically grant access.
|
||||
|
||||
|
||||
## Forking Evennia
|
||||
|
||||
This helps you set up an online *fork* of the main Evennia repository so you can
|
||||
easily commit fixes and help with upstream development. You can do this step
|
||||
also if you _didn't_ put your game dir under version control like in the
|
||||
previous section - the evennia repo and your game dir repo are completely
|
||||
separate.
|
||||
|
||||
### Step 1: Fork the evennia/master repository
|
||||
|
||||
> Before proceeding with the following step, make sure you have registered and
|
||||
> created an account on [GitHub.com](https://github.com/). This is necessary in order to create a fork
|
||||
of Evennia's master repository, and to push your commits to your fork either for
|
||||
yourself or for contributing to
|
||||
Evennia.
|
||||
|
||||
A _fork_ is a clone of the master repository that you can make your own commits
|
||||
and changes to. At the top of [this page](https://github.com/evennia/evennia),
|
||||
click the "Fork" button, as it appears below.
|
||||

|
||||
|
||||
### Step 2: Clone your online fork of Evennia
|
||||
|
||||
The fork only exists online as of yet. In a terminal, change your directory to
|
||||
the folder you wish to develop in. From this directory run the following
|
||||
command:
|
||||
|
||||
git clone https://github.com/yourusername/evennia.git
|
||||
|
||||
This will download your fork to your computer. It creates a new folder
|
||||
`evennia/` at your current location.
|
||||
|
||||
### Step 3: Configure remotes
|
||||
|
||||
Your Evennia-fork is now separate from upstream, 'official' Evennia. You will
|
||||
want to set it up so that you can easily sync our updates and changes to your
|
||||
fork.
|
||||
|
||||
We do this by setting up a new _remote_. We actually already have one remote,
|
||||
that is our own github form of Evennia. This got created when you cloned the
|
||||
repo and defaults to being called `origin`.
|
||||
|
||||
We will now create a new remote called `upstream`.
|
||||
|
||||
cd evennia
|
||||
git remote add upstream https://github.com/evennia/evennia.git
|
||||
|
||||
This adds a remote to the main evennia repo.
|
||||
|
||||
If you also want to access Evennia's `develop` branch (the bleeding edge
|
||||
development) do the following:
|
||||
|
||||
git fetch upstream develop
|
||||
git checkout develop
|
||||
|
||||
Use
|
||||
git checkout master
|
||||
git checkout develop
|
||||
|
||||
to switch between the branches. If you want to contribute a fix, ask first which
|
||||
branch to use. Normally `master` is for bug fixes and `develop` is for new
|
||||
features, but late in the development of a new Evennia version, all changes
|
||||
often go into `develop`.
|
||||
|
||||
|
||||
## Working with your Evennia fork
|
||||
|
||||
_Branches_ are stand-alone editions of the same code. You make a commit to a
|
||||
branch. Switching to a branch will change the code on-disk. You can easily
|
||||
make a new branch off a parent branch, and then merge it back into the same
|
||||
branch later (or throw it away). This is a very common way to work on new
|
||||
features in safety and isolation.
|
||||
|
||||
### Updating to latest Evennia
|
||||
|
||||
When Evennia's official repository updates, first make sure to commit all your
|
||||
changes to your branch and then checkout the "clean" master branch:
|
||||
|
||||
git checkout master
|
||||
git pull upstream master
|
||||
|
||||
Or, if you are working against Evennia's development branch:
|
||||
|
||||
git checkout develop
|
||||
git pull upstream develop
|
||||
|
||||
The `pull` command will fetch all the changes from the "upstream" remote and
|
||||
merge it into your local master/develop branch. It should now be a perfect copy
|
||||
of the latest Evennia changes.
|
||||
|
||||
### Making changes
|
||||
|
||||
As a rule of thumb you should _never_ work directly in Evennia's `master` or
|
||||
`develop` branches. Instead you make a _new_ branch off the branch you want
|
||||
and change _that_.
|
||||
|
||||
git checkout master (or develop)
|
||||
check checkout -b strange_bug
|
||||
|
||||
You now have a new branch `strange_bug` that is an exact replica of the branch you
|
||||
had checked out when you created it. Here you can now make your own
|
||||
modifications.
|
||||
|
||||
git branches
|
||||
|
||||
will show you which branches are available and which one you are currently
|
||||
using. Use `git checkout <branch>` to move between them, but remember to commit
|
||||
your changes before you do.
|
||||
|
||||
You often want to make sure also your work-branch has the latest upstream
|
||||
changes. To do this, you need to first update your copy of the
|
||||
`master`/`develop` branch and then _merge_ those changes into your work branch.
|
||||
Make sure you have committed everything first!
|
||||
|
||||
git commit -a -m "My latest changes ..." # on your strange_bug branch
|
||||
git checkout master (or develop)
|
||||
git pull upstream develop
|
||||
git checkout strange_bug
|
||||
git merge master (or develop)
|
||||
|
||||
If everything went well, your `strange_bug` branch will now have the latest version
|
||||
of Evennia merged with whatever changes you have done.
|
||||
|
||||
Now work away on your code and commit with reasonable commit messages
|
||||
|
||||
git commit -a -m "Fixed the issue in ..."
|
||||
git commit -a -m "Adding unit tests. This resolves #123."
|
||||
|
||||
Use
|
||||
|
||||
git diff
|
||||
|
||||
to see what you changed since last commit, and
|
||||
|
||||
git log
|
||||
|
||||
to see past commits (including those made by Evennia upstream, remember that
|
||||
your branch is a copy of the upstream one, including its history!)
|
||||
|
||||
## Sharing your Evennia fixes on Github
|
||||
|
||||
Up to this point your `strange_bug` branch only exists on your local computer. No
|
||||
one else can see it. If you want a copy of this branch to also appear in your
|
||||
online fork on GitHub, make sure to have checked out your "myfixes" branch and
|
||||
then run the following:
|
||||
|
||||
git push -u origin strange_bug
|
||||
|
||||
You only need to do this once, the `-u` makes this the default push-location. In
|
||||
the future, you can just push things online like this:
|
||||
|
||||
git push
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you hadn't setup a public key on GitHub or aren't asked for a
|
||||
username/password, you might get an error `403: Forbidden Access` at this stage.
|
||||
In that case, some users have reported that the workaround is to create a file
|
||||
`.netrc` under your home directory and add your github credentials there:
|
||||
|
||||
```bash
|
||||
machine github.com
|
||||
login <my_github_username>
|
||||
password <my_github_password>
|
||||
```
|
||||
|
||||
## Making an Evennia Pull Request
|
||||
|
||||
If you think that the fixes you did in your `strange_bug` branch should be a
|
||||
part of the regular Evennia, you should create a _Pull Request_ (PR). This is a
|
||||
call for the Evennia maintainer to pull your change into an upstream branch.
|
||||
|
||||
> It is wise to make separate branches for every fix or series of fixes you want
|
||||
to contribute.
|
||||
|
||||
Assuming you have followed the instructions above and have pushed your changes
|
||||
online, [create a pull request](https://github.com/evennia/evennia/pulls) and
|
||||
follow the instructions. Make sure to specifically select your `strange_bug`
|
||||
branch to be the source of the merge and use the branch you based that branch
|
||||
off (`master` or `develop`) as the target.
|
||||
|
||||
Evennia developers will then be able to examine your request and merge it if
|
||||
it's deemed suitable. They may also come back with feedback and request you do
|
||||
some changes.
|
||||
|
||||
Once approved and merged, your change will now be available in the upstream
|
||||
branch:
|
||||
|
||||
git checkout master (or develope)
|
||||
git pull upstream master (or develop)
|
||||
|
||||
Since your changes are now in upstream, your local `strange_bug` branch is now
|
||||
superfluous and should be deleted:
|
||||
|
||||
git branch -D strange_bug
|
||||
|
||||
You can also safely delete your online `strange_bug` branch in your fork
|
||||
(you can do this from the PR page on github).
|
||||
|
||||
|
||||
## GIT tips and tricks
|
||||
|
||||
Some of the GIT commands can feel a little long and clunky if you need to do them often. Luckily you
|
||||
can create aliases for those. Here are some useful commands to run:
|
||||
|
||||
|
||||
```
|
||||
# git st
|
||||
# - view brief status info
|
||||
git config --global alias.st 'status -s'
|
||||
```
|
||||
|
||||
Above, you only need to ever enter the `git config ...` command once - you have then added the new
|
||||
alias. Afterwards, just do `git st` to get status info. All the examples below follow the same
|
||||
template.
|
||||
|
||||
```
|
||||
# git cl
|
||||
# - clone a repository
|
||||
git config --global alias.cl clone
|
||||
```
|
||||
|
||||
```
|
||||
# git cma "commit message"
|
||||
# - commit all changes without opening editor for message
|
||||
git config --global alias.cma 'commit -a -m'
|
||||
```
|
||||
|
||||
```
|
||||
# git ca
|
||||
# - amend text to your latest commit message
|
||||
git config --global alias.ca 'commit --amend'
|
||||
```
|
||||
|
||||
```
|
||||
# git fl
|
||||
# - file log; shows diffs of files in latest commits
|
||||
git config --global alias.fl 'log -u'
|
||||
```
|
||||
|
||||
```
|
||||
# git co [branchname]
|
||||
# - checkout
|
||||
git config --global alias.co checkout
|
||||
```
|
||||
|
||||
```
|
||||
# git br <branchname>
|
||||
# - create branch
|
||||
git config --global alias.br branch
|
||||
```
|
||||
|
||||
```
|
||||
# git ls
|
||||
# - view log tree
|
||||
git config --global alias.ls 'log --pretty=format:"%C(green)%h\ %C(yellow)[%ad]%Cred%d\
|
||||
%Creset%s%Cblue\ [%cn]" --decorate --date=relative --graph'
|
||||
```
|
||||
|
||||
```
|
||||
# git diff
|
||||
# - show current uncommitted changes
|
||||
git config --global alias.diff 'diff --word-diff'
|
||||
```
|
||||
|
||||
```
|
||||
# git grep <query>
|
||||
# - search (grep) codebase for a search criterion
|
||||
git config --global alias.grep 'grep -Ii'
|
||||
```
|
||||
|
||||
To get a further feel for GIT there is also [a good YouTube talk about it](https://www.youtube.com/watch?v=1ffBJ4sVUb4#t=1m58s) - it's a bit long but it will help you understand the underlying ideas behind GIT
|
||||
(which in turn makes it a lot more intuitive to use).
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
# Accounts
|
||||
|
||||
|
||||
All *users* (real people) that starts a game [Session](./Sessions.md) 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.md)
|
||||
(normally a [Character](./Objects.md#characters)).
|
||||
|
||||
Exactly how many Sessions can interact with an Account and its Puppets at once is determined by
|
||||
Evennia's [MULTISESSION_MODE](./Sessions.md#multisession-mode) setting.
|
||||
|
||||
Apart from storing login information and other account-specific data, the Account object is what is
|
||||
chatting on [Channels](./Channels.md). It is also a good place to store [Permissions](./Locks.md) to be
|
||||
consistent between different in-game characters as well as configuration options. The Account
|
||||
object also has its own [CmdSet](./Command-Sets.md), the `AccountCmdSet`.
|
||||
|
||||
Logged into default evennia, you can use the `ooc` command to leave your current
|
||||
[character](./Objects.md) 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](./Permissions.md) 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.md)), 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.md) 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.md), in the same way as nicks it works on Objects.
|
||||
For Accounts, nicks are primarily used to store custom aliases for
|
||||
[Channels](./Channels.md).
|
||||
|
||||
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.
|
||||
|
|
@ -1,613 +0,0 @@
|
|||
# Attributes
|
||||
|
||||
```{code-block}
|
||||
:caption: In-game
|
||||
> set obj/myattr = "test"
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In-code, using the .db wrapper
|
||||
obj.db.foo = [1, 2, 3, "bar"]
|
||||
value = obj.db.foo
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In-code, using the .attributes handler
|
||||
obj.attributes.add("myattr", 1234, category="bar")
|
||||
value = attributes.get("myattr", category="bar")
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In-code, using `AttributeProperty` at class level
|
||||
from evennia import DefaultObject
|
||||
from evennia import AttributeProperty
|
||||
|
||||
class MyObject(DefaultObject):
|
||||
foo = AttributeProperty(default=[1, 2, 3, "bar"])
|
||||
myattr = AttributeProperty(100, category='bar')
|
||||
|
||||
```
|
||||
|
||||
_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a server reboot. An Attribute can store pretty much any
|
||||
Python data structure and data type, like numbers, strings, lists, dicts etc. You can also
|
||||
store (references to) database objects like characters and rooms.
|
||||
|
||||
- [What can be stored in an Attribute](#what-types-of-data-can-i-save-in-an-attribute) is a must-read to avoid being surprised, also for experienced developers. Attributes can store _almost_ everything
|
||||
but you need to know the quirks.
|
||||
- [NAttributes](#in-memory-attributes-nattributes) are the in-memory, non-persistent
|
||||
siblings of Attributes.
|
||||
- [Managing Attributes In-game](#managing-attributes-in-game) for in-game builder commands.
|
||||
|
||||
## Managing Attributes in Code
|
||||
|
||||
Attributes are usually handled in code. All [Typeclassed](./Typeclasses.md) entities
|
||||
([Accounts](./Accounts.md), [Objects](./Objects.md), [Scripts](./Scripts.md) and
|
||||
[Channels](./Channels.md)) can (and usually do) have Attributes associated with them. There
|
||||
are three ways to manage Attributes, all of which can be mixed.
|
||||
|
||||
- [Using the `.db` property shortcut](#using-db)
|
||||
- [Using the `.attributes` manager (`AttributeManager`)](#using-attributes)
|
||||
- [Using `AttributeProperty` for assigning Attributes in a way similar to Django fields](#using-attributeproperty)
|
||||
|
||||
### Using .db
|
||||
|
||||
The simplest way to get/set Attributes is to use the `.db` shortcut. This allows for setting and getting Attributes that lack a _category_ (having category `None`)
|
||||
|
||||
```python
|
||||
import evennia
|
||||
|
||||
obj = evennia.create_object(key="Foo")
|
||||
|
||||
obj.db.foo1 = 1234
|
||||
obj.db.foo2 = [1, 2, 3, 4]
|
||||
obj.db.weapon = "sword"
|
||||
obj.db.self_reference = obj # stores a reference to the obj
|
||||
|
||||
# (let's assume a rose exists in-game)
|
||||
rose = evennia.search_object(key="rose")[0] # returns a list, grab 0th element
|
||||
rose.db.has_thorns = True
|
||||
|
||||
# retrieving
|
||||
val1 = obj.db.foo1
|
||||
val2 = obj.db.foo2
|
||||
weap = obj.db.weapon
|
||||
myself = obj.db.self_reference # retrieve reference from db, get object back
|
||||
|
||||
is_ouch = rose.db.has_thorns
|
||||
|
||||
# this will return None, not AttributeError!
|
||||
not_found = obj.db.jiwjpowiwwerw
|
||||
|
||||
# returns all Attributes on the object
|
||||
obj.db.all
|
||||
|
||||
# delete an Attribute
|
||||
del obj.db.foo2
|
||||
```
|
||||
Trying to access a non-existing Attribute will never lead to an `AttributeError`. Instead
|
||||
you will get `None` back. The special `.db.all` will return a list of all Attributes on
|
||||
the object. You can replace this with your own Attribute `all` if you want, it will replace the
|
||||
default `all` functionality until you delete it again.
|
||||
|
||||
### Using .attributes
|
||||
|
||||
If you want to group your Attribute in a category, or don't know the name of the Attribute beforehand, you can make use of
|
||||
the [AttributeHandler](evennia.typeclasses.attributes.AttributeHandler), available as `.attributes` on all typeclassed entities. With no extra keywords, this is identical to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally):
|
||||
|
||||
```python
|
||||
is_ouch = rose.attributes.get("has_thorns")
|
||||
|
||||
obj.attributes.add("helmet", "Knight's helmet")
|
||||
helmet = obj.attributes.get("helmet")
|
||||
|
||||
# you can give space-separated Attribute-names (can't do that with .db)
|
||||
obj.attributes.add("my game log", "long text about ...")
|
||||
```
|
||||
|
||||
By using a category you can separate same-named Attributes on the same object to help organization.
|
||||
|
||||
```python
|
||||
# store (let's say we have gold_necklace and ringmail_armor from before)
|
||||
obj.attributes.add("neck", gold_necklace, category="clothing")
|
||||
obj.attributes.add("neck", ringmail_armor, category="armor")
|
||||
|
||||
# retrieve later - we'll get back gold_necklace and ringmail_armor
|
||||
neck_clothing = obj.attributes.get("neck", category="clothing")
|
||||
neck_armor = obj.attributes.get("neck", category="armor")
|
||||
```
|
||||
|
||||
If you don't specify a category, the Attribute's `category` will be `None` and can thus also be found via `.db`. `None` is considered a category of its own, so you won't find `None`-category Attributes mixed with Attributes having categories.
|
||||
|
||||
Here are the methods of the `AttributeHandler`. See
|
||||
the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for more details.
|
||||
|
||||
- `has(...)` - this checks if the object has an Attribute with this key. This is equivalent
|
||||
to doing `obj.db.attrname` except you can also check for a specific `category.
|
||||
- `get(...)` - this retrieves the given Attribute. You can also provide a `default` value to return
|
||||
if the Attribute is not defined (instead of None). By supplying an
|
||||
`accessing_object` to the call one can also make sure to check permissions before modifying
|
||||
anything. The `raise_exception` kwarg allows you to raise an `AttributeError` instead of returning
|
||||
`None` when you access a non-existing `Attribute`. The `strattr` kwarg tells the system to store
|
||||
the Attribute as a raw string rather than to pickle it. While an optimization this should usually
|
||||
not be used unless the Attribute is used for some particular, limited purpose.
|
||||
- `add(...)` - this adds a new Attribute to the object. An optional [lockstring](./Locks.md) 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(category=None)` - returns all Attributes (of the given category) attached to this object.
|
||||
|
||||
Examples:
|
||||
|
||||
```python
|
||||
try:
|
||||
# raise error if Attribute foo does not exist
|
||||
val = obj.attributes.get("foo", raise_exception=True):
|
||||
except AttributeError:
|
||||
# ...
|
||||
|
||||
# return default value if foo2 doesn't exist
|
||||
val2 = obj.attributes.get("foo2", default=[1, 2, 3, "bar"])
|
||||
|
||||
# delete foo if it exists (will silently fail if unset, unless
|
||||
# raise_exception is set)
|
||||
obj.attributes.remove("foo")
|
||||
|
||||
# view all clothes on obj
|
||||
all_clothes = obj.attributes.all(category="clothes")
|
||||
```
|
||||
|
||||
### Using AttributeProperty
|
||||
|
||||
The third way to set up an Attribute is to use an `AttributeProperty`. This
|
||||
is done on the _class level_ of your typeclass and allows you to treat Attributes a bit like Django database Fields. Unlike using `.db` and `.attributes`, an `AttributeProperty` can't be created on the fly, you must assign it in the class code.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/characters.py
|
||||
|
||||
from evennia import DefaultCharacter
|
||||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
|
||||
strength = AttributeProperty(10, category='stat')
|
||||
constitution = AttributeProperty(11, category='stat')
|
||||
agility = AttributeProperty(12, category='stat')
|
||||
magic = AttributeProperty(13, category='stat')
|
||||
|
||||
sleepy = AttributeProperty(False, autocreate=False)
|
||||
poisoned = AttributeProperty(False, autocreate=False)
|
||||
|
||||
def at_object_creation(self):
|
||||
# ...
|
||||
```
|
||||
|
||||
When a new instance of the class is created, new `Attributes` will be created with the value and category given.
|
||||
|
||||
With `AttributeProperty`'s set up like this, one can access the underlying `Attribute` like a regular property on the created object:
|
||||
|
||||
```python
|
||||
char = create_object(Character)
|
||||
|
||||
char.strength # returns 10
|
||||
char.agility = 15 # assign a new value (category remains 'stat')
|
||||
|
||||
char.db.magic # returns None (wrong category)
|
||||
char.attributes.get("agility", category="stat") # returns 15
|
||||
|
||||
char.db.sleepy # returns None because autocreate=False (see below)
|
||||
|
||||
```
|
||||
|
||||
```{warning}
|
||||
Be careful to not assign AttributeProperty's to names of properties and methods already existing on the class, like 'key' or 'at_object_creation'. That could lead to very confusing errors.
|
||||
```
|
||||
|
||||
The `autocreate=False` (default is `True`) used for `sleepy` and `poisoned` is worth a closer explanation. When `False`, _no_ Attribute will be auto-created for these AttributProperties unless they are _explicitly_ set.
|
||||
The advantage of not creating an Attribute is that the default value given to `AttributeProperty` is returned with no database access unless you change it. This also means that if you want to change the default later, all entities previously create will inherit the new default.
|
||||
The drawback is that without a database precense you can't find the Attribute via `.db` and `.attributes.get` (or by querying for it in other ways in the database):
|
||||
|
||||
```python
|
||||
char.sleepy # returns False, no db access
|
||||
|
||||
char.db.sleepy # returns None - no Attribute exists
|
||||
char.attributes.get("sleepy") # returns None too
|
||||
|
||||
char.sleepy = True # now an Attribute is created
|
||||
char.db.sleepy # now returns True!
|
||||
char.attributes.get("sleepy") # now returns True
|
||||
|
||||
char.sleepy # now returns True, involves db access
|
||||
|
||||
```
|
||||
|
||||
You can e.g. `del char.strength` to set the value back to the default (the value defined
|
||||
in the `AttributeProperty`).
|
||||
|
||||
See the [AttributeProperty API](evennia.typeclasses.attributes.AttributeProperty) for more details on how to create it with special options, like giving access-restrictions.
|
||||
|
||||
|
||||
## Managing Attributes in-game
|
||||
|
||||
Attributes are mainly used by code. But one can also allow the builder to use Attributes to
|
||||
'turn knobs' in-game. For example a builder could want to manually tweak the "level" Attribute of an
|
||||
enemy NPC to lower its difficuly.
|
||||
|
||||
When setting Attributes this way, you are severely limited in what can be stored - this is because
|
||||
giving players (even builders) the ability to store arbitrary Python would be a severe security
|
||||
problem.
|
||||
|
||||
In game you can set an Attribute like this:
|
||||
|
||||
set myobj/foo = "bar"
|
||||
|
||||
To view, do
|
||||
|
||||
set myobj/foo
|
||||
|
||||
or see them together with all object-info with
|
||||
|
||||
examine myobj
|
||||
|
||||
The first `set`-example will store a new Attribute `foo` on the object `myobj` and give it the
|
||||
value "bar".
|
||||
You can store numbers, booleans, strings, tuples, lists and dicts this way. But if
|
||||
you store a list/tuple/dict they must be proper Python structures and may _only_ contain strings
|
||||
or numbers. If you try to insert an unsupported structure, the input will be converted to a
|
||||
string.
|
||||
|
||||
set myobj/mybool = True
|
||||
set myobj/mybool = True
|
||||
set myobj/mytuple = (1, 2, 3, "foo")
|
||||
set myobj/mylist = ["foo", "bar", 2]
|
||||
set myobj/mydict = {"a": 1, "b": 2, 3: 4}
|
||||
set mypobj/mystring = [1, 2, foo] # foo is invalid Python (no quotes)
|
||||
|
||||
For the last line you'll get a warning and the value instead will be saved as a string `"[1, 2, foo]"`.
|
||||
|
||||
## Locking and checking Attributes
|
||||
|
||||
While the `set` command is limited to builders, individual Attributes are usually not
|
||||
locked down. You may want to lock certain sensitive Attributes, in particular for games
|
||||
where you allow player building. You can add such limitations by adding a [lock string](./Locks.md)
|
||||
to your Attribute. A NAttribute have no 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 must use the `AttributeHandler` to assign the lockstring to the Attribute:
|
||||
|
||||
```python
|
||||
lockstring = "attread:all();attredit:perm(Admins)"
|
||||
obj.attributes.add("myattr", "bar", lockstring=lockstring)"
|
||||
```
|
||||
|
||||
If you already have an Attribute and want to add a lock in-place you can do so
|
||||
by having the `AttributeHandler` return the `Attribute` object itself (rather than
|
||||
its value) and then assign the lock to it directly:
|
||||
|
||||
```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.
|
||||
To check the `lockstring` you provided, make sure you include `accessing_obj` and set
|
||||
`default_access=False` as you make a `get` call.
|
||||
|
||||
```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.
|
||||
|
||||
## What types of data can I save in an Attribute?
|
||||
|
||||
The database doesn't know anything about Python objects, so Evennia must *serialize* Attribute
|
||||
values into a string representation before storing it to the database. This is done using the
|
||||
[pickle](https://docs.python.org/library/pickle.html) 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* and those will not be pickled).
|
||||
|
||||
### 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_.
|
||||
* Single database objects/typeclasses can be stored, despite them normally not
|
||||
being possible to pickle. Evennia will convert them to an internal
|
||||
representation using theihr classname, database-id and creation-date with a
|
||||
microsecond precision. When retrieving, the object instance will be re-fetched
|
||||
from the database using this information.
|
||||
* If you 'hide' a db-obj as a property on a custom class, Evennia will not be
|
||||
able to find it to serialize it. For that you need to help it out (see below).
|
||||
|
||||
```{code-block} python
|
||||
:caption: Valid assignments
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
As mentioned, Evennia will not be able to automatically serialize db-objects
|
||||
'hidden' in arbitrary properties on an object. This will lead to an error
|
||||
when saving the Attribute.
|
||||
|
||||
```{code-block} python
|
||||
:caption: Invalid, 'hidden' dbobject
|
||||
# example of storing an invalid, "hidden" dbobject in Attribute
|
||||
class Container:
|
||||
def __init__(self, mydbobj):
|
||||
# no way for Evennia to know this is a database object!
|
||||
self.mydbobj = mydbobj
|
||||
|
||||
# let's assume myobj is a db-object
|
||||
container = Container(myobj)
|
||||
obj.db.mydata = container # will raise error!
|
||||
|
||||
```
|
||||
|
||||
By adding two methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` to the
|
||||
object you want to save, you can pre-serialize and post-deserialize all 'hidden'
|
||||
objects before Evennia's main serializer gets to work. Inside these methods, use Evennia's
|
||||
[evennia.utils.dbserialize.dbserialize](evennia.utils.dbserialize.dbserialize) and
|
||||
[dbunserialize](evennia.utils.dbserialize.dbunserialize) functions to safely
|
||||
serialize the db-objects you want to store.
|
||||
|
||||
```{code-block} python
|
||||
:caption: Fixing an invalid 'hidden' dbobj for storing in Attribute
|
||||
|
||||
from evennia.utils import dbserialize # important
|
||||
|
||||
class Container:
|
||||
def __init__(self, mydbobj):
|
||||
# A 'hidden' db-object
|
||||
self.mydbobj = mydbobj
|
||||
|
||||
def __serialize_dbobjs__(self):
|
||||
"""This is called before serialization and allows
|
||||
us to custom-handle those 'hidden' dbobjs"""
|
||||
self.mydbobj = dbserialize.dbserialize(self.mydbobj
|
||||
|
||||
def __deserialize_dbobjs__(self):
|
||||
"""This is called after deserialization and allows you to
|
||||
restore the 'hidden' dbobjs you serialized before"""
|
||||
if isinstance(self.mydbobj, bytes):
|
||||
# make sure to check if it's bytes before trying dbunserialize
|
||||
self.mydbobj = dbserialize.dbunserialize(self.mydbobj)
|
||||
|
||||
# let's assume myobj is a db-object
|
||||
container = Container(myobj)
|
||||
obj.db.mydata = container # will now work fine!
|
||||
```
|
||||
|
||||
> Note the extra check in `__deserialize_dbobjs__` to make sure the thing you
|
||||
> are deserializing is a `bytes` object. This is needed because the Attribute's
|
||||
> cache reruns deserializations in some situations when the data was already
|
||||
> once deserialized. If you see errors in the log saying
|
||||
> `Could not unpickle data for storage: ...`, the reason is
|
||||
> likely that you forgot to add this check.
|
||||
|
||||
|
||||
### 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.OrderedDict),
|
||||
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.md#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}]
|
||||
```
|
||||
|
||||
Note that if make some advanced iterable object, and store an db-object on it in
|
||||
a way such that it is _not_ returned by iterating over it, you have created a
|
||||
'hidden' db-object. See [the previous section](#storing-single-objects) for how
|
||||
to tell Evennia how to serialize such hidden objects safely.
|
||||
|
||||
|
||||
### 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) # now also [1, 2, 3, 5]
|
||||
```
|
||||
|
||||
When you extract your mutable Attribute data into a variable like `mylist`, think of it as getting a _snapshot_
|
||||
of the variable. If you update the snapshot, it will save to the database, but this change _will not propagate to
|
||||
any other snapshots you may have done previously_.
|
||||
|
||||
```python
|
||||
obj.db.mylist = [1, 2, 3, 4]
|
||||
mylist1 = obj.db.mylist
|
||||
mylist2 = obj.db.mylist
|
||||
mylist1[3] = 5
|
||||
|
||||
print(mylist1) # this is now [1, 2, 3, 5]
|
||||
print(obj.db.mylist) # also updated to [1, 2, 3, 5]
|
||||
|
||||
print(mylist2) # still [1, 2, 3, 4] !
|
||||
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
Remember, the complexities of this section only relate to *mutable* iterables - things you can update
|
||||
in-place, like lists and dicts. [Immutable](https://en.wikipedia.org/wiki/Immutable) objects (strings,
|
||||
numbers, tuples etc) are already disconnected from the database from the onset.
|
||||
```
|
||||
|
||||
To avoid confusion with mutable Attributes, only work with one variable (snapshot) at a time and save
|
||||
back the results as needed.
|
||||
|
||||
You can also choose to "disconnect" the Attribute entirely from the
|
||||
database with the help of the `.deserialize()` method:
|
||||
|
||||
```python
|
||||
obj.db.mylist = [1, 2, 3, 4, {1: 2}]
|
||||
mylist = obj.db.mylist.deserialize()
|
||||
```
|
||||
|
||||
The result of this operation will be a structure only consisting of normal Python mutables (`list`
|
||||
instead of `_SaverList`, `dict` instead of `_SaverDict` and so on). If you update it, you need to
|
||||
explicitly save it back to the Attribute for it to save.
|
||||
|
||||
## 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.md#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.md) is an example of using
|
||||
Attributes in this way). To modify this property you need to use the [Attribute Handler](#attributes)
|
||||
- `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.md) use it). It is only
|
||||
accessible via the [Attribute Handler](#attributes).
|
||||
|
||||
There are also two special properties:
|
||||
|
||||
- `attrtype` - this is used internally by Evennia to separate [Nicks](./Nicks.md), 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 are not stored in the database and have no equivalence
|
||||
to `category` nor `strvalue`, `attrtype` or `model`.
|
||||
|
||||
## In-memory Attributes (NAttributes)
|
||||
|
||||
_NAttributes_ (short of Non-database Attributes) mimic Attributes in most things except they
|
||||
are **non-persistent** - they will _not_ survive a server reload.
|
||||
|
||||
- Instead of `.db` use `.ndb`.
|
||||
- Instead of `.attributes` use `.nattributes`
|
||||
- Instead of `AttributeProperty`, use `NAttributeProperty`.
|
||||
|
||||
```python
|
||||
rose.ndb.has_thorns = True
|
||||
is_ouch = rose.ndb.has_thorns
|
||||
|
||||
rose.nattributes.add("has_thorns", True)
|
||||
is_ouch = rose.nattributes.get("has_thorns")
|
||||
```
|
||||
|
||||
Differences between `Attributes` and `NAttributes`:
|
||||
|
||||
- `NAttribute`s are always wiped on a server reload.
|
||||
- They only exist in memory and never involve the database at all, making them faster to
|
||||
access and edit than `Attribute`s.
|
||||
- `NAttribute`s can store _any_ Python structure (and database object) without limit.
|
||||
- They can _not_ be set with the standard `set` command (but they are visible with `examine`)
|
||||
|
||||
There are some important reasons we recommend using `ndb` to store temporary data rather than
|
||||
the simple alternative of just storing a variable directly on an object:
|
||||
|
||||
- NAttributes are tracked by Evennia and will not be purged in various cache-cleanup operations
|
||||
the server may do. So using them guarantees that they'll remain available at least as long as
|
||||
the server lives.
|
||||
- It's a consistent style - `.db/.attributes` and `.ndb/.nattributes` makes for clean-looking code
|
||||
where it's clear how long-lived (or not) your data is to be.
|
||||
|
||||
### 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.md) 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.
|
||||
- `NAttribute`s have no restrictions at all on what they can store, 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!
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
# Batch Code Processor
|
||||
|
||||
|
||||
For an introduction and motivation to using batch processors, see [here](./Batch-Processors.md). 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 = f"A {table} and {chair} were created."
|
||||
if DEBUG:
|
||||
table.delete()
|
||||
chair.delete()
|
||||
string += " Since debug was active, they were deleted again."
|
||||
caller.msg(string)
|
||||
```
|
||||
|
||||
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.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.md) 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.
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
# Batch Command Processor
|
||||
|
||||
|
||||
For an introduction and motivation to using batch processors, see [here](./Batch-Processors.md). 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.md)*: 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.md))
|
||||
|
||||
- [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](https://www.vim.org/) users can use amfl's [vim-evennia](https://github.com/amfl/vim-evennia)
|
||||
mode instead, see its readme for install instructions.
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
# 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.md)
|
||||
- The [Batch Code Processor](./Batch-Code-Processor.md)
|
||||
|
||||
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](https://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](https://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](../Concepts/Text-Encodings.md) and also in the
|
||||
Wikipedia article [here](https://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.
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
# 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](../Howtos/Beginner-Tutorial/Part5/Add-a-simple-new-web-page.md) or
|
||||
>[the web character view tutorial](../Howtos/Web-Character-View-Tutorial.md)
|
||||
> 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.](../Howtos/Web-Character-Generation.md)
|
||||
|
|
@ -1,380 +0,0 @@
|
|||
# Channels
|
||||
|
||||
In a multiplayer game, players often need other means of in-game communication
|
||||
than moving to the same room and use `say` or `emote`.
|
||||
|
||||
_Channels_ allows Evennia's to act as a fancy chat program. When a player is
|
||||
connected to a channel, sending a message to it will automatically distribute
|
||||
it to every other subscriber.
|
||||
|
||||
Channels can be used both for chats between [Accounts](./Accounts.md) and between
|
||||
[Objects](./Objects.md) (usually Characters). Chats could be both OOC
|
||||
(out-of-character) or IC (in-charcter) in nature. Some examples:
|
||||
|
||||
- A support channel for contacting staff (OOC)
|
||||
- A general chat for discussing anything and foster community (OOC)
|
||||
- Admin channel for private staff discussions (OOC)
|
||||
- Private guild channels for planning and organization (IC/OOC depending on game)
|
||||
- Cyberpunk-style retro chat rooms (IC)
|
||||
- In-game radio channels (IC)
|
||||
- Group telephathy (IC)
|
||||
- Walkie talkies (IC)
|
||||
|
||||
```{versionchanged} 1.0
|
||||
|
||||
Channel system changed to use a central 'channel' command and nicks instead of
|
||||
auto-generated channel-commands and -cmdset. ChannelHandler was removed.
|
||||
|
||||
```
|
||||
|
||||
## Using channels in-game
|
||||
|
||||
In the default command set, channels are all handled via the mighty
|
||||
[channel
|
||||
command](evennia.commands.default.comms.CmdChannel), `channel` (or
|
||||
`chan`). By default, this command will assume all entities dealing with
|
||||
channels are `Accounts`.
|
||||
|
||||
### Viewing and joining channels
|
||||
|
||||
channel - shows your subscriptions
|
||||
channel/all - shows all subs available to you
|
||||
channel/who - shows who subscribes to this channel
|
||||
|
||||
To join/unsub a channel do
|
||||
|
||||
channel/sub channelname
|
||||
channel/unsub channelname
|
||||
|
||||
If you temporarily don't want to hear the channel for a while (without actually
|
||||
unsubscribing), you can mute it:
|
||||
|
||||
channel/mute channelname
|
||||
channel/unmute channelname
|
||||
|
||||
### Chat on channels
|
||||
|
||||
To speak on a channel, do
|
||||
|
||||
channel public Hello world!
|
||||
|
||||
If the channel-name has spaces in it, you need to use a '`=`':
|
||||
|
||||
channel rest room = Hello world!
|
||||
|
||||
Now, this is more to type than we'd like, so when you join a channel, the
|
||||
system automatically sets up an personal alias so you can do this instead:
|
||||
|
||||
public Hello world
|
||||
|
||||
```{warning}
|
||||
|
||||
This shortcut will not work if the channel-name has spaces in it.
|
||||
So channels with long names should make sure to provide a one-word alias as
|
||||
well.
|
||||
```
|
||||
|
||||
Any user can make up their own channel aliases:
|
||||
|
||||
channel/alias public = foo;bar
|
||||
|
||||
You can now just do
|
||||
|
||||
foo Hello world!
|
||||
bar Hello again!
|
||||
|
||||
And even remove the default one if they don't want to use it
|
||||
|
||||
channel/unalias public
|
||||
public Hello (gives a command-not-found error now)
|
||||
|
||||
But you can also use your alias with the `channel` command:
|
||||
|
||||
channel foo Hello world!
|
||||
|
||||
> What happens when aliasing is that a [nick](./Nicks.md) is created that maps your
|
||||
> alias + argument onto calling the `channel` command. So when you enter `foo hello`,
|
||||
> what the server sees is actually `channel foo = hello`. The system is also
|
||||
> clever enough to know that whenever you search for channels, your channel-nicks
|
||||
> should also be considered so as to convert your input to an existing channel name.
|
||||
|
||||
You can check if you missed channel conversations by viewing the channel's
|
||||
scrollback with
|
||||
|
||||
channel/history public
|
||||
|
||||
This retrieves the last 20 lines of text (also from a time when you were
|
||||
offline). You can step further back by specifying how many lines back to start:
|
||||
|
||||
channel/history public = 30
|
||||
|
||||
This again retrieve 20 lines, but starting 30 lines back (so you'll get lines
|
||||
30-50 counting backwards).
|
||||
|
||||
|
||||
### Channel administration
|
||||
|
||||
To create/destroy a new channel you can do
|
||||
|
||||
channel/create channelname;alias;alias = description
|
||||
channel/destroy channelname
|
||||
|
||||
Aliases are optional but can be good for obvious shortcuts everyone may want to
|
||||
use. The description is used in channel-listings. You will automatically join a
|
||||
channel you created and will be controlling it. You can also use `channel/desc` to
|
||||
change the description on a channel you wnn later.
|
||||
|
||||
If you control a channel you can also kick people off it:
|
||||
|
||||
channel/boot mychannel = annoyinguser123 : stop spamming!
|
||||
|
||||
The last part is an optional reason to send to the user before they are booted.
|
||||
You can give a comma-separated list of channels to kick the same user from all
|
||||
those channels at once. The user will be unsubbed from the channel and all
|
||||
their aliases will be wiped. But they can still rejoin if they like.
|
||||
|
||||
channel/ban mychannel = annoyinguser123
|
||||
channel/ban - view bans
|
||||
channel/unban mychannel = annoyinguser123
|
||||
|
||||
Banning adds the user to the channels blacklist. This means they will not be
|
||||
able to _rejoin_ if you boot them. You will need to run `channel/boot` to
|
||||
actually kick them out.
|
||||
|
||||
See the [Channel command](evennia.commands.default.comms.CmdChannel) api
|
||||
docs (and in-game help) for more details.
|
||||
|
||||
Admin-level users can also modify channel's [locks](./Locks.md):
|
||||
|
||||
channel/lock buildchannel = listen:all();send:perm(Builders)
|
||||
|
||||
Channels use three lock-types by default:
|
||||
|
||||
- `listen` - who may listen to the channel. Users without this access will not
|
||||
even be able to join the channel and it will not appear in listings for them.
|
||||
- `send` - who may send to the channel.
|
||||
- `control` - this is assigned to you automatically when you create the channel. With
|
||||
control over the channel you can edit it, boot users and do other management tasks.
|
||||
|
||||
|
||||
#### Restricting channel administration
|
||||
|
||||
By default everyone can use the channel command ([evennia.commands.default.comms.CmdChannel](evennia.commands.default.comms.CmdChannel))
|
||||
to create channels and will then control the channels they created (to boot/ban
|
||||
people etc). If you as a developer does not want regular players to do this
|
||||
(perhaps you want only staff to be able to spawn new channels), you can
|
||||
override the `channel` command and change its `locks` property.
|
||||
|
||||
The default `help` command has the following `locks` property:
|
||||
|
||||
```python
|
||||
locks = "cmd:not perm(channel_banned); admin:all(); manage:all(); changelocks: perm(Admin)"
|
||||
```
|
||||
|
||||
This is a regular [lockstring](./Locks.md).
|
||||
|
||||
- `cmd: pperm(channel_banned)` - The `cmd` locktype is the standard one used for all Commands.
|
||||
an accessing object failing this will not even know that the command exists. The `pperm()` lockfunc
|
||||
checks an on-account [Permission](Building Permissions) 'channel_banned' - and the `not` means
|
||||
that if they _have_ that 'permission' they are cut off from using the `channel` command. You usually
|
||||
don't need to change this lock.
|
||||
- `admin:all()` - this is a lock checked in the `channel` command itself. It controls access to the
|
||||
`/boot`, `/ban` and `/unban` switches (by default letting everyone use them).
|
||||
- `manage:all()` - this controls access to the `/create`, `/destroy`, `/desc` switches.
|
||||
- `changelocks: perm(Admin)` - this controls access to the `/lock` and `/unlock` switches. By
|
||||
default this is something only [Admins](Building Permissions) can change.
|
||||
|
||||
> Note - while `admin:all()` and `manage:all()` will let everyone use these switches, users
|
||||
> will still only be able to admin or destroy channels they actually control!
|
||||
|
||||
If you only want (say) Builders and higher to be able to create and admin
|
||||
channels you could override the `help` command and change the lockstring to:
|
||||
|
||||
```python
|
||||
# in for example mygame/commands/commands.py
|
||||
|
||||
from evennia import default_cmds
|
||||
|
||||
class MyCustomChannelCmd(default_cmds.CmdChannel):
|
||||
locks = "cmd: not pperm(channel_banned);admin:perm(Builder);manage:perm(Builder);changelocks:perm(Admin)"
|
||||
|
||||
```
|
||||
|
||||
Add this custom command to your default cmdset and regular users wil now get an
|
||||
access-denied error when trying to use use these switches.
|
||||
|
||||
## Allowing Characters to use Channels
|
||||
|
||||
The default `channel` command ([evennia.commands.default.comms.CmdChannel](evennia.commands.default.comms.CmdChannel))
|
||||
sits in the `Account` [command set](./Command-Sets.md). It is set up such that it will
|
||||
always operate on `Accounts`, even if you were to add it to the
|
||||
`CharacterCmdSet`.
|
||||
|
||||
It's a one-line change to make this command accept non-account callers. But for
|
||||
convenience we provide a version for Characters/Objects. Just import
|
||||
[evennia.commands.default.comms.CmdObjectChannel](evennia.commands.default.comms.CmdObjectChannel)
|
||||
and inherit from that instead.
|
||||
|
||||
## Customizing channel output and behavior
|
||||
|
||||
When distributing a message, the channel will call a series of hooks on itself
|
||||
and (more importantly) on each recipient. So you can customize things a lot by
|
||||
just modifying hooks on your normal Object/Account typeclasses.
|
||||
|
||||
Internally, the message is sent with
|
||||
`channel.msg(message, senders=sender, bypass_mute=False, **kwargs)`, where
|
||||
`bypass_mute=True` means the message ignores muting (good for alerts or if you
|
||||
delete the channel etc) and `**kwargs` are any extra info you may want to pass
|
||||
to the hooks. The `senders` (it's always only one in the default implementation
|
||||
but could in principle be multiple) and `bypass_mute` are part of the `kwargs`
|
||||
below:
|
||||
|
||||
1. `channel.at_pre_msg(message, **kwargs)`
|
||||
2. For each recipient:
|
||||
- `message = recipient.at_pre_channel_msg(message, channel, **kwargs)` -
|
||||
allows for the message to be tweaked per-receiver (for example coloring it depending
|
||||
on the users' preferences). If this method returns `False/None`, that
|
||||
recipient is skipped.
|
||||
- `recipient.channel_msg(message, channel, **kwargs)` - actually sends to recipient.
|
||||
- `recipient.at_post_channel_msg(message, channel, **kwargs)` - any post-receive effects.
|
||||
3. `channel.at_post_channel_msg(message, **kwargs)`
|
||||
|
||||
Note that `Accounts` and `Objects` both have their have separate sets of hooks.
|
||||
So make sure you modify the set actually used by your subcribers (or both).
|
||||
Default channels all use `Account` subscribers.
|
||||
|
||||
## Channels in code
|
||||
|
||||
For most common changes, the default channel, the recipient hooks and possibly
|
||||
overriding the `channel` command will get you very far. But you can also tweak
|
||||
channels themselves.
|
||||
|
||||
Channels are [Typeclassed](./Typeclasses.md) entities. This means they are
|
||||
persistent in the database, can have [attributes](./Attributes.md) and [Tags](./Tags.md)
|
||||
and can be easily extended.
|
||||
|
||||
To change which channel typeclass Evennia uses for default commands, change
|
||||
`settings.BASE_CHANNEL_TYPECLASS`. The base command class is
|
||||
[`evennia.comms.comms.DefaultChannel`](evennia.comms.comms.DefaultChannel).
|
||||
There is an empty child class in `mygame/typeclasses/channels.py`, same
|
||||
as for other typelass-bases.
|
||||
|
||||
In code you create a new channel with `evennia.create_channel` or
|
||||
`Channel.create`:
|
||||
|
||||
```python
|
||||
from evennia import create_channel, search_object
|
||||
from typeclasses.channels import Channel
|
||||
|
||||
channel = create_channel("my channel", aliases=["mychan"], locks=..., typeclass=...)
|
||||
# alternative
|
||||
channel = Channel.create("my channel", aliases=["mychan"], locks=...)
|
||||
|
||||
# connect to it
|
||||
me = search_object(key="Foo")[0]
|
||||
channel.connect(me)
|
||||
|
||||
# send to it (this will trigger the channel_msg hooks described earlier)
|
||||
channel.msg("Hello world!", senders=me)
|
||||
|
||||
# view subscriptions (the SubscriptionHandler handles all subs under the hood)
|
||||
channel.subscriptions.has(me) # check we subbed
|
||||
channel.subscriptions.all() # get all subs
|
||||
channel.subscriptions.online() # get only subs currently online
|
||||
channel.subscriptions.clear() # unsub all
|
||||
|
||||
# leave channel
|
||||
channel.disconnect(me)
|
||||
|
||||
# permanently delete channel (will unsub everyone)
|
||||
channel.delete()
|
||||
|
||||
```
|
||||
|
||||
The Channel's `.connect` method will accept both `Account` and `Object` subscribers
|
||||
and will handle them transparently.
|
||||
|
||||
The channel has many more hooks, both hooks shared with all typeclasses as well
|
||||
as special ones related to muting/banning etc. See the channel class for
|
||||
details.
|
||||
|
||||
## Channel logging
|
||||
|
||||
```{versionchanged} 0.7
|
||||
|
||||
Channels changed from using Msg to TmpMsg and optional log files.
|
||||
```
|
||||
```{versionchanged} 1.0
|
||||
|
||||
Channels stopped supporting Msg and TmpMsg, using only log files.
|
||||
```
|
||||
|
||||
The channel messages are not stored in the database. A channel is instead
|
||||
always logged to a regular text log-file
|
||||
`mygame/server/logs/channel_<channelname>.log`. This is where `channels/history channelname`
|
||||
gets its data from. A channel's log will rotate when it grows too big, which
|
||||
thus also automatically limits the max amount of history a user can view with
|
||||
`/history`.
|
||||
|
||||
The log file name is set on the channel class as the `log_file` property. This
|
||||
is a string that takes the formatting token `{channelname}` to be replaced with
|
||||
the (lower-case) name of the channel. By default the log is written to in the
|
||||
channel's `at_post_channel_msg` method.
|
||||
|
||||
|
||||
### Properties on Channels
|
||||
|
||||
Channels have all the standard properties of a Typeclassed entity (`key`,
|
||||
`aliases`, `attributes`, `tags`, `locks` etc). This is not an exhaustive list;
|
||||
see the [Channel api docs](evennia.comms.comms.DefaultChannel) for details.
|
||||
|
||||
- `send_to_online_only` - this class boolean defaults to `True` and is a
|
||||
sensible optimization since people offline people will not see the message anyway.
|
||||
- `log_file` - this is a string that determines the name of the channel log file. Default
|
||||
is `"channel_{channelname}.log"`. The log file will appear in `settings.LOG_DIR` (usually
|
||||
`mygame/server/logs/`). You should usually not change this.
|
||||
- `channel_prefix_string` - this property is a string to easily change how
|
||||
the channel is prefixed. It takes the `channelname` format key. Default is `"[{channelname}] "`
|
||||
and produces output like `[public] ...`.
|
||||
- `subscriptions` - this is the [SubscriptionHandler](evennia.comms.models.SubscriptionHandler), which
|
||||
has methods `has`, `add`, `remove`, `all`, `clear` and also `online` (to get
|
||||
only actually online channel-members).
|
||||
- `wholist`, `mutelist`, `banlist` are properties that return a list of subscribers,
|
||||
as well as who are currently muted or banned.
|
||||
- `channel_msg_nick_pattern` - this is a regex pattern for performing the in-place nick
|
||||
replacement (detect that `channelalias <msg` means that you want to send a message to a channel).
|
||||
This pattern accepts an `{alias}` formatting marker. Don't mess with this unless you really
|
||||
want to change how channels work.
|
||||
- `channel_msg_nick_replacement` - this is a string on the [nick replacement
|
||||
- form](./Nicks.md). It accepts the `{channelname}` formatting tag. This is strongly tied to the
|
||||
`channel` command and is by default `channel {channelname} = $1`.
|
||||
|
||||
Notable `Channel` hooks:
|
||||
|
||||
- `at_pre_channel_msg(message, **kwargs)` - called before sending a message, to
|
||||
modify it. Not used by default.
|
||||
- `msg(message, senders=..., bypass_mute=False, **kwargs)` - send the message onto
|
||||
the channel. The `**kwargs` are passed on into the other call hooks (also on the recipient).
|
||||
- `at_post_channel_msg(message, **kwargs)` - by default this is used to store the message
|
||||
to the log file.
|
||||
- `channel_prefix(message)` - this is called to allow the channel to prefix. This is called
|
||||
by the object/account when they build the message, so if wanting something else one can
|
||||
also just remove that call.
|
||||
- every channel message. By default it just returns `channel_prefix_string`.
|
||||
- `has_connection(subscriber)` - shortcut to check if an entity subscribes to
|
||||
this channel.
|
||||
- `mute/unmute(subscriber)` - this mutes the channel for this user.
|
||||
- `ban/unban(subscriber)` - adds/remove user from banlist.
|
||||
- `connect/disconnect(subscriber)` - adds/removes a subscriber.
|
||||
- `add_user_channel_alias(user, alias, **kwargs)` - sets up a user-nick for this channel. This is
|
||||
what maps e.g. `alias <msg>` to `channel channelname = <msg>`.
|
||||
- `remove_user_channel_alias(user, alias, **kwargs)` - remove an alias. Note that this is
|
||||
a class-method that will happily remove found channel-aliases from the user linked to _any_
|
||||
channel, not only from the channel the method is called on.
|
||||
- `pre_join_channel(subscriber)` - if this returns `False`, connection will be refused.
|
||||
- `post_join_channel(subscriber)` - by default this sets up a users's channel-nicks/aliases.
|
||||
- `pre_leave_channel(subscriber)` - if this returns `False`, the user is not allowed to leave.
|
||||
- `post_leave_channel(subscriber)` - this will clean up any channel aliases/nicks of the user.
|
||||
- `delete` the standard typeclass-delete mechanism will also automatically un-subscribe all
|
||||
subscribers (and thus wipe all their aliases).
|
||||
|
||||
|
|
@ -1,297 +0,0 @@
|
|||
# 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`](evennia.accounts.manager.AccountDBManager.search_account)
|
||||
- [`evennia.search_object`](evennia.objects.manager.ObjectDBManager.search_object)
|
||||
- [`evennia.search(object)_by_tag`](evennia.utils.search.search_tag)
|
||||
- [`evennia.search_script`](evennia.scripts.manager.ScriptDBManager.search_script)
|
||||
- [`evennia.search_channel`](evennia.comms.managers.ChannelDBManager.search_channel)
|
||||
- [`evennia.search_message`](evennia.comms.managers.MsgManager.search_message)
|
||||
- [`evennia.search_help`](evennia.help.manager.HelpEntryManager.search_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`](evennia.utils.create.create_account)
|
||||
- [`evennia.create_object`](evennia.utils.create.create_object)
|
||||
- [`evennia.create_script`](evennia.utils.create.create_script)
|
||||
- [`evennia.create_channel`](evennia.utils.create.create_channel)
|
||||
- [`evennia.create_help_entry`](evennia.utils.create.create_help_entry)
|
||||
- [`evennia.create_message`](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](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.md).
|
||||
|
||||
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.md) and [Scripts](./Scripts.md).
|
||||
|
||||
> 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](https://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.md) 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](../Concepts/Text-Encodings.md) for more info.
|
||||
|
||||
### Ansi Coloring Tools
|
||||
- [evennia.utils.ansi](evennia.utils.ansi)
|
||||
|
||||
## Display utilities
|
||||
### Making ascii tables
|
||||
|
||||
The [EvTable](evennia.utils.evtable.EvTable) class (`evennia/utils/evtable.py`) can be used
|
||||
to create correctly formatted text tables. There is also
|
||||
[EvForm](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](evennia.utils.evmenu.EvMenu)
|
||||
|
|
@ -1,376 +0,0 @@
|
|||
# Command Sets
|
||||
|
||||
|
||||
Command Sets are intimately linked with [Commands](./Commands.md) 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](./Permissions.md) 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](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) 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, persistent=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](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md). 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.md)). 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.md#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.md#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.md#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.md#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.md). Default is the empty
|
||||
`SessionCmdSet` with merge priority `-20`.
|
||||
- The cmdsets defined on the [Account](./Accounts.md). 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](./Channels.md) 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.md). For example, [Character objects](./Objects.md) 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](https://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.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# Command System
|
||||
|
||||
- [Commands](./Commands.md)
|
||||
- [Command Sets](./Command-Sets.md)
|
||||
- [Command Auto-help](./Help-System.md#command-auto-help-system)
|
||||
|
||||
See also:
|
||||
- [Default Commands](./Default-Commands.md)
|
||||
- [Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md)
|
||||
|
|
@ -1,690 +0,0 @@
|
|||
# Commands
|
||||
|
||||
|
||||
Commands are intimately linked to [Command Sets](./Command-Sets.md) 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-Commands.md) 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.md) (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.md). There is also a step-by-step
|
||||
[Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) 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(f"Echo: {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.md). 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.md) 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.md) object itself.
|
||||
* `sessid` - `sessid.id`, a unique integer identifier of the session.
|
||||
* `account` - the [Account](./Accounts.md) object connected to this Session. None if not logged in.
|
||||
* An [Account](./Accounts.md). 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.md). 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.md) 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.md) 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.md) 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](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.md), 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.md#on-arg_regex) for the details.
|
||||
- `auto_help` (optional boolean). Defaults to `True`. This allows for turning off the
|
||||
[auto-help system](./Help-System.md#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](https://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.md) 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.md) page.
|
||||
|
||||
### Command prefixes
|
||||
|
||||
Historically, many MU* servers used to use prefix, such as `@` or `&` to signify that
|
||||
a command is used for administration or requires staff privileges. The problem with this is that
|
||||
newcomers to MU often find such extra symbols confusing. Evennia allows commands that can be
|
||||
accessed both with- or without such a prefix.
|
||||
|
||||
CMD_IGNORE_PREFIXES = "@&/+`
|
||||
|
||||
This is a setting consisting of a string of characters. Each is a prefix that will be considered
|
||||
a skippable prefix - _if the command is still unique in its cmdset when skipping the prefix_.
|
||||
|
||||
So if you wanted to write `@look` instead of `look` you can do so - the `@` will be ignored. But If
|
||||
we added an actual `@look` command (with a `key` or alias `@look`) then we would need to use the
|
||||
`@` to separate between the two.
|
||||
|
||||
This is also used in the default commands. For example, `@open` is a building
|
||||
command that allows you to create new exits to link two rooms together. Its `key` is set to `@open`,
|
||||
including the `@` (no alias is set). By default you can use both `@open` and `open` for
|
||||
this command. But "open" is a pretty common word and let's say a developer adds a new `open` command
|
||||
for opening a door. Now `@open` and `open` are two different commands and the `@` must be used to
|
||||
separate them.
|
||||
|
||||
> The `help` command will prefer to show all command names without prefix if
|
||||
> possible. Only if there is a collision, will the prefix be shown in the help system.
|
||||
|
||||
### 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](https://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("Beginner-Tutorial 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.returnValue](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.md) 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](./Channels.md) 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.md#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.md#dynamic-commands).
|
||||
|
||||
The functionality of [Exit](./Objects.md) objects in Evennia is not hard-coded in the engine. Instead
|
||||
Exits are normal [typeclassed](./Typeclasses.md) objects that auto-create a [CmdSet](./Command-Sets.md) 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(f"Command memory ID: {id(self)} (xval={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.md#exits).
|
||||
- Sets of dynamically created *System commands* representing available
|
||||
[Communications](./Channels.md)
|
||||
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](https://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(f"Returned is {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.
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
# Core Components
|
||||
|
||||
These are the 'building blocks' out of which Evennia is built. This documentation is complementary to, and often goes deeper than, the doc-strings of each component in the [API](../Evennia-API.md).
|
||||
|
||||
## Database entites
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Typeclasses.md
|
||||
Sessions.md
|
||||
Accounts.md
|
||||
Objects.md
|
||||
Scripts.md
|
||||
Channels.md
|
||||
Msg.md
|
||||
Attributes.md
|
||||
Nicks.md
|
||||
Tags.md
|
||||
Prototypes.md
|
||||
Help-System.md
|
||||
Permissions.md
|
||||
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Command-System.md
|
||||
Commands.md
|
||||
Command-Sets.md
|
||||
Default-Commands.md
|
||||
Connection-Screen.md
|
||||
Batch-Processors.md
|
||||
Batch-Code-Processor.md
|
||||
Batch-Command-Processor.md
|
||||
```
|
||||
|
||||
|
||||
## Utils and tools
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Coding-Utils.md
|
||||
EvEditor.md
|
||||
EvForm.md
|
||||
EvMenu.md
|
||||
EvMore.md
|
||||
EvTable.md
|
||||
FuncParser.md
|
||||
MonitorHandler.md
|
||||
TickerHandler.md
|
||||
Locks.md
|
||||
Signals.md
|
||||
```
|
||||
|
||||
## Web components
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Website.md
|
||||
Web-API.md
|
||||
Web-Admin.md
|
||||
```
|
||||
|
||||
## Server and network
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Portal-And-Server.md
|
||||
Inputfuncs.md
|
||||
Outputfuncs.md
|
||||
Server.md
|
||||
Setup/Server-Conf.md
|
||||
Webserver.md
|
||||
Webclient.md
|
||||
Bootstrap-Components-and-Utilities.md
|
||||
Signals.md
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# 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](../Setup/Start-Stop-Reload.md) 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.md) 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.md) and the
|
||||
tutorial section on how to add new commands to a default command set.
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
# Default Commands
|
||||
|
||||
The full set of default Evennia commands currently contains 88 commands in 9 source
|
||||
files. Our policy for adding default commands is outlined [here](../Concepts/Using-MUX-as-a-Standard.md). The
|
||||
[Commands](./Commands.md) documentation explains how Commands work as well as how to make new or customize
|
||||
existing ones.
|
||||
|
||||
> Note that this page is auto-generated. Report problems to the [issue tracker](github:issues).
|
||||
|
||||
```{note}
|
||||
Some game-states add their own Commands which are not listed here. Examples include editing a text
|
||||
with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using the
|
||||
[Batch-Processor](./Batch-Processors.md)'s interactive mode.
|
||||
```
|
||||
|
||||
- [**@about** [@version]](CmdAbout) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@accounts** [@account]](CmdAccounts) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@alias** [setobjalias]](CmdSetObjAlias) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@channel** [@chan, @channels]](CmdChannel) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**@cmdsets**](CmdListCmdSets) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@copy**](CmdCopy) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@cpattr**](CmdCpAttr) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@create**](CmdCreate) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@desc**](CmdDesc) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@destroy** [@del, @delete]](CmdDestroy) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@dig**](CmdDig) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@examine** [@ex, @exam]](CmdExamine) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Building_)
|
||||
- [**@find** [@locate, @search]](CmdFind) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@link**](CmdLink) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@lock** [@locks]](CmdLock) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@mvattr**](CmdMvAttr) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@name** [@rename]](CmdName) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@objects**](CmdObjects) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@open**](CmdOpen) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@py** [@!]](CmdPy) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _System_)
|
||||
- [**@reload** [@restart]](CmdReload) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _System_)
|
||||
- [**@reset** [@reboot]](CmdReset) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _System_)
|
||||
- [**@scripts** [@script]](CmdScripts) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@server** [@serverload]](CmdServerLoad) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@service** [@services]](CmdService) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@set**](CmdSetAttribute) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@sethome**](CmdSetHome) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@shutdown**](CmdShutdown) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _System_)
|
||||
- [**@spawn** [@olc]](CmdSpawn) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@tag** [@tags]](CmdTag) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@tasks** [@delays, @task]](CmdTasks) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@teleport** [@tel]](CmdTeleport) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@tickers**](CmdTickers) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@time** [@uptime]](CmdTime) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@tunnel** [@tun]](CmdTunnel) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@typeclass** [@parent, @swap, @type, @typeclasses, @update]](CmdTypeclass) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@wipe**](CmdWipe) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**__unloggedin_look_command** [l, look]](CmdUnconnectedLook) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**access** [groups, hierarchy]](CmdAccess) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**batchcode** [batchcodes]](CmdBatchCode) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**batchcommands** [batchcmd, batchcommand]](CmdBatchCommands) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**charcreate**](CmdCharCreate) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**chardelete**](CmdCharDelete) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**color**](CmdColorTest) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**connect** [co, con, conn]](CmdUnconnectedConnect) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**create** [cr, cre]](CmdUnconnectedCreate) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**drop**](CmdDrop) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**encoding** [encode]](CmdUnconnectedEncoding) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**get** [grab]](CmdGet) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**give**](CmdGive) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**grapevine2chan**](CmdGrapevine2Chan) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**help** [?]](CmdHelp) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**help** [?, h]](CmdUnconnectedHelp) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**home**](CmdHome) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**ic** [puppet]](CmdIC) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**info**](CmdUnconnectedInfo) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**inventory** [i, inv]](CmdInventory) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**irc2chan**](CmdIRC2Chan) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**ircstatus**](CmdIRCStatus) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**look** [l, ls]](CmdOOCLook) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**look** [l, ls]](CmdLook) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**nick** [nickname, nicks]](CmdNick) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**ooc** [unpuppet]](CmdOOC) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**option** [options]](CmdOption) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**page** [tell]](CmdPage) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**password**](CmdPassword) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**pose** [:, emote]](CmdPose) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**quell** [unquell]](CmdQuell) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**quit**](CmdQuit) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**quit** [q, qu]](CmdUnconnectedQuit) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**rss2chan**](CmdRSS2Chan) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**say** [", ']](CmdSay) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**screenreader**](CmdUnconnectedScreenreader) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**sessions**](CmdSessions) (cmdset: [SessionCmdSet](SessionCmdSet), help-category: _General_)
|
||||
- [**setdesc**](CmdSetDesc) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**sethelp**](CmdSetHelp) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**style**](CmdStyle) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**unlink**](CmdUnLink) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**whisper**](CmdWhisper) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**who** [doing]](CmdWho) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
# 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.add("test", buffer)
|
||||
def quit(caller):
|
||||
"Since we define it, we must handle messages"
|
||||
caller.msg("Editor exited")
|
||||
key = f"{self.caller}/test"
|
||||
# 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.add("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 = f"{self.caller}/test"
|
||||
# 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.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# EvForm
|
||||
|
||||
[Docstring in evennia/utils/evform.py](evennia.utils.evform)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,37 +0,0 @@
|
|||
# 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.md) or a [Account](./Accounts.md). 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.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# EvTable
|
||||
|
||||
[Docstring in evennia/utils/evtable.py](evennia.utils.evtable)
|
||||
|
|
@ -1,446 +0,0 @@
|
|||
# The Inline Function Parser
|
||||
|
||||
The [FuncParser](evennia.utils.funcparser.FuncParser) extracts and executes
|
||||
'inline functions'
|
||||
embedded in a string on the form `$funcname(args, kwargs)`. Under the hood, this will
|
||||
lead to a call to a Python function you control. The inline function call will be replaced by
|
||||
the return from the function.
|
||||
|
||||
```python
|
||||
from evennia.utils.funcparser import FuncParser
|
||||
|
||||
def _power_callable(*args, **kwargs):
|
||||
"""This will be callable as $pow(number, power=<num>) in string"""
|
||||
pow = int(kwargs.get('power', 2))
|
||||
return float(args[0]) ** pow
|
||||
|
||||
# create a parser and tell it that '$pow' means using _power_callable
|
||||
parser = FuncParser({"pow": _power_callable})
|
||||
|
||||
```
|
||||
Next, just pass a string into the parser, containing `$func(...)` markers:
|
||||
|
||||
```python
|
||||
parser.parse("We have that 4 x 4 x 4 is $pow(4, power=3).")
|
||||
"We have that 4 x 4 x 4 is 64."
|
||||
```
|
||||
|
||||
Normally the return is always converted to a string but you can also get the actual data type from the call:
|
||||
|
||||
```python
|
||||
parser.parse_to_any("$pow(4)")
|
||||
16
|
||||
```
|
||||
|
||||
To show a `$func()` verbatim in your code without parsing it, escape it as either `$$func()` or `\$func()`:
|
||||
|
||||
|
||||
```python
|
||||
parser.parse("This is an escaped $$pow(4) and so is this \$pow(3)")
|
||||
"This is an escaped $pow(4) and so is this $pow(3)"
|
||||
```
|
||||
|
||||
## Uses in default Evennia
|
||||
|
||||
The FuncParser can be applied to any string. Out of the box it's applied in a few situations:
|
||||
|
||||
- _Outgoing messages_. All messages sent from the server is processed through FuncParser and every
|
||||
callable is provided the [Session](./Sessions.md) of the object receiving the message. This potentially
|
||||
allows a message to be modified on the fly to look different for different recipients.
|
||||
- _Prototype values_. A [Prototype](./Prototypes.md) dict's values are run through the parser such that every
|
||||
callable gets a reference to the rest of the prototype. In the Prototype ORM, this would allow builders
|
||||
to safely call functions to set non-string values to prototype values, get random values, reference
|
||||
other fields of the prototype, and more.
|
||||
- _Actor-stance in messages to others_. In the
|
||||
[Object.msg_contents](evennia.objects.objects.DefaultObject.msg_contents) method,
|
||||
the outgoing string is parsed for special `$You()` and `$conj()` callables to decide if a given recipient
|
||||
should see "You" or the character's name.
|
||||
|
||||
```{important}
|
||||
The inline-function parser is not intended as a 'softcode' programming language. It does not
|
||||
have things like loops and conditionals, for example. While you could in principle extend it to
|
||||
do very advanced things and allow builders a lot of power, all-out coding is something
|
||||
Evennia expects you to do in a proper text editor, outside of the game, not from inside it.
|
||||
```
|
||||
|
||||
## Using the FuncParser
|
||||
|
||||
You can apply inline function parsing to any string. The
|
||||
[FuncParser](evennia.utils.funcparser.FuncParser) is imported as `evennia.utils.funcparser`.
|
||||
|
||||
```python
|
||||
from evennia.utils import funcparser
|
||||
|
||||
parser = FuncParser(callables, **default_kwargs)
|
||||
parsed_string = parser.parse(input_string, raise_errors=False,
|
||||
escape=False, strip=False,
|
||||
return_str=True, **reserved_kwargs)
|
||||
|
||||
# callables can also be passed as paths to modules
|
||||
parser = FuncParser(["game.myfuncparser_callables", "game.more_funcparser_callables"])
|
||||
```
|
||||
|
||||
Here, `callables` points to a collection of normal Python functions (see next section) for you to make
|
||||
available to the parser as you parse strings with it. It can either be
|
||||
- A `dict` of `{"functionname": callable, ...}`. This allows you do pick and choose exactly which callables
|
||||
to include and how they should be named. Do you want a callable to be available under more than one name?
|
||||
Just add it multiple times to the dict, with a different key.
|
||||
- A `module` or (more commonly) a `python-path` to a module. This module can define a dict
|
||||
`FUNCPARSER_CALLABLES = {"funcname": callable, ...}` - this will be imported and used like the `dict` above.
|
||||
If no such variable is defined, _every_ top-level function in the module (whose name doesn't start with
|
||||
an underscore `_`) will be considered a suitable callable. The name of the function will be the `$funcname`
|
||||
by which it can be called.
|
||||
- A `list` of modules/paths. This allows you to pull in modules from many sources for your parsing.
|
||||
- The `**default` kwargs are optional kwargs that will be passed to _all_
|
||||
callables every time this parser is used - unless the user overrides it explicitly in
|
||||
their call. This is great for providing sensible standards that the user can
|
||||
tweak as needed.
|
||||
|
||||
`FuncParser.parse` takes further arguments, and can vary for every string parsed.
|
||||
|
||||
- `raise_errors` - By default, any errors from a callable will be quietly ignored and the result
|
||||
will be that the failing function call will show verbatim. If `raise_errors` is set,
|
||||
then parsing will stop and whatever exception happened will be raised. It'd be up to you to handle
|
||||
this properly.
|
||||
- `escape` - Returns a string where every `$func(...)` has been escaped as `\$func()`.
|
||||
- `strip` - Remove all `$func(...)` calls from string (as if each returned `''`).
|
||||
- `return_str` - When `True` (default), `parser` always returns a string. If `False`, it may return
|
||||
the return value of a single function call in the string. This is the same as using the `.parse_to_any`
|
||||
method.
|
||||
- The `**reserved_keywords` are _always_ passed to every callable in the string.
|
||||
They override any `**defaults` given when instantiating the parser and cannot
|
||||
be overridden by the user - if they enter the same kwarg it will be ignored.
|
||||
This is great for providing the current session, settings etc.
|
||||
- The `funcparser` and `raise_errors`
|
||||
are always added as reserved keywords - the first is a
|
||||
back-reference to the `FuncParser` instance and the second
|
||||
is the `raise_errors` boolean given to `FuncParser.parse`.
|
||||
|
||||
Here's an example of using the default/reserved keywords:
|
||||
|
||||
```python
|
||||
def _test(*args, **kwargs):
|
||||
# do stuff
|
||||
return something
|
||||
|
||||
parser = funcparser.FuncParser({"test": _test}, mydefault=2)
|
||||
result = parser.parse("$test(foo, bar=4)", myreserved=[1, 2, 3])
|
||||
```
|
||||
Here the callable will be called as
|
||||
|
||||
```python
|
||||
_test('foo', bar='4', mydefault=2, myreserved=[1, 2, 3],
|
||||
funcparser=<FuncParser>, raise_errors=False)
|
||||
```
|
||||
|
||||
The `mydefault=2` kwarg could be overwritten if we made the call as `$test(mydefault=...)`
|
||||
but `myreserved=[1, 2, 3]` will _always_ be sent as-is and will override a call `$test(myreserved=...)`.
|
||||
The `funcparser`/`raise_errors` kwargs are also always included as reserved kwargs.
|
||||
|
||||
## Defining custom callables
|
||||
|
||||
All callables made available to the parser must have the following signature:
|
||||
|
||||
```python
|
||||
def funcname(*args, **kwargs):
|
||||
# ...
|
||||
return something
|
||||
```
|
||||
|
||||
> The `*args` and `**kwargs` must always be included. If you are unsure how `*args` and `**kwargs` work in Python,
|
||||
> [read about them here](https://www.digitalocean.com/community/tutorials/how-to-use-args-and-kwargs-in-python-3).
|
||||
|
||||
The input from the innermost `$funcname(...)` call in your callable will always be a `str`. Here's
|
||||
an example of an `$toint` function; it converts numbers to integers.
|
||||
|
||||
"There's a $toint(22.0)% chance of survival."
|
||||
|
||||
What will enter the `$toint` callable (as `args[0]`) is the _string_ `"22.0"`. The function is responsible
|
||||
for converting this to a number so that we can convert it to an integer. We must also properly handle invalid
|
||||
inputs (like non-numbers).
|
||||
|
||||
If you want to mark an error, raise `evennia.utils.funcparser.ParsingError`. This stops the entire parsing
|
||||
of the string and may or may not raise the exception depending on what you set `raise_errors` to when you
|
||||
created the parser.
|
||||
|
||||
However, if you _nest_ functions, the return of the innermost function may be something other than
|
||||
a string. Let's introduce the `$eval` function, which evaluates simple expressions using
|
||||
Python's `literal_eval` and/or `simple_eval`. It returns whatever data type it
|
||||
evaluates to.
|
||||
|
||||
"There's a $toint($eval(10 * 2.2))% chance of survival."
|
||||
|
||||
Since the `$eval` is the innermost call, it will get a string as input - the string `"10 * 2.2"`.
|
||||
It evaluates this and returns the `float` `22.0`. This time the outermost `$toint` will be called with
|
||||
this `float` instead of with a string.
|
||||
|
||||
> It's important to safely validate your inputs since users may end up nesting your callables in any order.
|
||||
> See the next section for useful tools to help with this.
|
||||
|
||||
In these examples, the result will be embedded in the larger string, so the result of the entire parsing
|
||||
will be a string:
|
||||
|
||||
```python
|
||||
parser.parse(above_string)
|
||||
"There's a 22% chance of survival."
|
||||
```
|
||||
|
||||
However, if you use the `parse_to_any` (or `parse(..., return_str=False)`) and
|
||||
_don't add any extra string around the outermost function call_,
|
||||
you'll get the return type of the outermost callable back:
|
||||
|
||||
```python
|
||||
parser.parse_to_any("$toint($eval(10 * 2.2)")
|
||||
22
|
||||
parser.parse_to_any("the number $toint($eval(10 * 2.2).")
|
||||
"the number 22"
|
||||
parser.parse_to_any("$toint($eval(10 * 2.2)%")
|
||||
"22%"
|
||||
```
|
||||
|
||||
### Escaping special character
|
||||
|
||||
When entering funcparser callables in strings, it looks like a regular
|
||||
function call inside a string:
|
||||
|
||||
```python
|
||||
"This is a $myfunc(arg1, arg2, kwarg=foo)."
|
||||
```
|
||||
|
||||
Commas (`,`) and equal-signs (`=`) are considered to separate the arguments and
|
||||
kwargs. In the same way, the right parenthesis (`)`) closes the argument list.
|
||||
Sometimes you want to include commas in the argument without it breaking the
|
||||
argument list.
|
||||
|
||||
```python
|
||||
"The $format(forest's smallest meadow, with dandelions) is to the west."
|
||||
```
|
||||
|
||||
You can escape in various ways.
|
||||
|
||||
- Prepending special characters like `,` and `=` with the escape character `\`
|
||||
|
||||
```python
|
||||
"The $format(forest's smallest meadow\, with dandelions) is to the west."
|
||||
```
|
||||
|
||||
- Wrapping your strings in double quotes. Unlike in raw Python, you
|
||||
can't escape with single quotes `'` since these could also be apostrophes (like
|
||||
`forest's` above). The result will be a verbatim string that contains
|
||||
everything but the outermost double quotes.
|
||||
|
||||
```python
|
||||
'The $format("forest's smallest meadow, with dandelions") is to the west.'
|
||||
```
|
||||
- If you want verbatim double-quotes to appear in your string, you can escape
|
||||
them with `\"` in turn.
|
||||
|
||||
```python
|
||||
'The $format("forest's smallest meadow, with \"dandelions\"') is to the west.'
|
||||
```
|
||||
|
||||
### Safe convertion of inputs
|
||||
|
||||
Since you don't know in which order users may use your callables, they should
|
||||
always check the types of its inputs and convert to the type the callable needs.
|
||||
Note also that when converting from strings, there are limits what inputs you
|
||||
can support. This is because FunctionParser strings can be used by
|
||||
non-developer players/builders and some things (such as complex
|
||||
classes/callables etc) are just not safe/possible to convert from string
|
||||
representation.
|
||||
|
||||
In `evennia.utils.utils` is a helper called
|
||||
[safe_convert_to_types](evennia.utils.utils.safe_convert_to_types). This function
|
||||
automates the conversion of simple data types in a safe way:
|
||||
|
||||
```python
|
||||
from evennia.utils.utils import safe_convert_to_types
|
||||
|
||||
def _process_callable(*args, **kwargs):
|
||||
"""
|
||||
$process(expression, local, extra1=34, extra2=foo)
|
||||
|
||||
"""
|
||||
args, kwargs = safe_convert_to_type(
|
||||
(('py', str), {'extra1': int, 'extra2': str}),
|
||||
*args, **kwargs)
|
||||
|
||||
# args/kwargs should be correct types now
|
||||
|
||||
```
|
||||
|
||||
In other words, in the callable `$process(expression, local, extra1=..,
|
||||
extra2=...)`, the first argument will be handled by the 'py' converter
|
||||
(described below), the second will passed through regular Python `str`,
|
||||
kwargs will be handled by `int` and `str` respectively. You can supply
|
||||
your own converter function as long as it takes one argument and returns
|
||||
the converted result.
|
||||
|
||||
In other words,
|
||||
|
||||
```python
|
||||
args, kwargs = safe_convert_to_type(
|
||||
(tuple_of_arg_converters, dict_of_kwarg_converters), *args, **kwargs)
|
||||
```
|
||||
|
||||
The special converter `"py"` will try to convert a string argument to a Python structure with the help of the
|
||||
following tools (which you may also find useful to experiment with on your own):
|
||||
|
||||
- [ast.literal_eval](https://docs.python.org/3.8/library/ast.html#ast.literal_eval) is an in-built Python
|
||||
function. It
|
||||
_only_ supports strings, bytes, numbers, tuples, lists, dicts, sets, booleans and `None`. That's
|
||||
it - no arithmetic or modifications of data is allowed. This is good for converting individual values and
|
||||
lists/dicts from the input line to real Python objects.
|
||||
- [simpleeval](https://pypi.org/project/simpleeval/) is a third-party tool included with Evennia. This
|
||||
allows for evaluation of simple (and thus safe) expressions. One can operate on numbers and strings
|
||||
with `+-/*` as well as do simple comparisons like `4 > 3` and more. It does _not_ accept more complex
|
||||
containers like lists/dicts etc, so this and `literal_eval` are complementary to each other.
|
||||
|
||||
```{warning}
|
||||
It may be tempting to run use Python's in-built ``eval()`` or ``exec()`` functions as converters since
|
||||
these are able to convert any valid Python source code to Python. NEVER DO THIS unless you really, really
|
||||
know that ONLY developers will ever modify the string going into the callable. The parser is intended
|
||||
for untrusted users (if you were trusted you'd have access to Python already). Letting untrusted users
|
||||
pass strings to ``eval``/``exec`` is a MAJOR security risk. It allows the caller to run arbitrary
|
||||
Python code on your server. This is the path to maliciously deleted hard drives. Just don't do it and
|
||||
sleep better at night.
|
||||
```
|
||||
|
||||
## Default callables
|
||||
|
||||
These are some example callables you can import and add your parser. They are divided into
|
||||
global-level dicts in `evennia.utils.funcparser`. Just import the dict(s) and merge/add one or
|
||||
more to them when you create your `FuncParser` instance to have those callables be available.
|
||||
|
||||
### `evennia.utils.funcparser.FUNCPARSER_CALLABLES`
|
||||
|
||||
These are the 'base' callables.
|
||||
|
||||
- `$eval(expression)` ([code](evennia.utils.funcparser.funcparser_callable_eval)) -
|
||||
this uses `literal_eval` and `simple_eval` (see previous section) attemt to convert a string expression
|
||||
to a python object. This handles e.g. lists of literals `[1, 2, 3]` and simple expressions like `"1 + 2"`.
|
||||
- `$toint(number)` ([code](evennia.utils.funcparser.funcparser_callable_toint)) -
|
||||
always converts an output to an integer, if possible.
|
||||
- `$add/sub/mult/div(obj1, obj2)` ([code](evennia.utils.funcparser.funcparser_callable_add)) -
|
||||
this adds/subtracts/multiplies and divides to elements together. While simple addition could be done with
|
||||
`$eval`, this could for example be used also to add two lists together, which is not possible with `eval`;
|
||||
for example `$add($eval([1,2,3]), $eval([4,5,6])) -> [1, 2, 3, 4, 5, 6]`.
|
||||
- `$round(float, significant)` ([code](evennia.utils.funcparser.funcparser_callable_round)) -
|
||||
rounds an input float into the number of provided significant digits. For example `$round(3.54343, 3) -> 3.543`.
|
||||
- `$random([start, [end]])` ([code](evennia.utils.funcparser.funcparser_callable_random)) -
|
||||
this works like the Python `random()` function, but will randomize to an integer value if both start/end are
|
||||
integers. Without argument, will return a float between 0 and 1.
|
||||
- `$randint([start, [end]])` ([code](evennia.utils.funcparser.funcparser_callable_randint)) -
|
||||
works like the `randint()` python function and always returns an integer.
|
||||
- `$choice(list)` ([code](evennia.utils.funcparser.funcparser_callable_choice)) -
|
||||
the input will automatically be parsed the same way as `$eval` and is expected to be an iterable. A random
|
||||
element of this list will be returned.
|
||||
- `$pad(text[, width, align, fillchar])` ([code](evennia.utils.funcparser.funcparser_callable_pad)) -
|
||||
this will pad content. `$pad("Hello", 30, c, -)` will lead to a text centered in a 30-wide block surrounded by `-`
|
||||
characters.
|
||||
- `$crop(text, width=78, suffix='[...]')` ([code](evennia.utils.funcparser.funcparser_callable_crop)) -
|
||||
this will crop a text longer than the width, by default ending it with a `[...]`-suffix that also fits within
|
||||
the width. If no width is given, the client width or `settings.DEFAULT_CLIENT_WIDTH` will be used.
|
||||
- `$space(num)` ([code](evennia.utils.funcparser.funcparser_callable_space)) -
|
||||
this will insert `num` spaces.
|
||||
- `$just(string, width=40, align=c, indent=2)` ([code](evennia.utils.funcparser.funcparser_callable_justify)) -
|
||||
justifies the text to a given width, aligning it left/right/center or 'f' for full (spread text across width).
|
||||
- `$ljust` - shortcut to justify-left. Takes all other kwarg of `$just`.
|
||||
- `$rjust` - shortcut to right justify.
|
||||
- `$cjust` - shortcut to center justify.
|
||||
- `$clr(startcolor, text[, endcolor])` ([code](evennia.utils.funcparser.funcparser_callable_clr)) -
|
||||
color text. The color is given with one or two characters without the preceeding `|`. If no endcolor is
|
||||
given, the string will go back to neutral, so `$clr(r, Hello)` is equivalent to `|rHello|n`.
|
||||
|
||||
### `evennia.utils.funcparser.SEARCHING_CALLABLES`
|
||||
|
||||
These are callables that requires access-checks in order to search for objects. So they require some
|
||||
extra reserved kwargs to be passed when running the parser:
|
||||
|
||||
```python
|
||||
|
||||
parser.parse_to_any(string, caller=<object or account>, access="control", ...)
|
||||
|
||||
```
|
||||
The `caller` is required, it's the the object to do the access-check for. The `access` kwarg is the
|
||||
[lock type](./Locks.md) to check, default being `"control"`.
|
||||
|
||||
- `$search(query,type=account|script,return_list=False)` ([code](evennia.utils.funcparser.funcparser_callable_search)) -
|
||||
this will look up and try to match an object by key or alias. Use the `type` kwarg to
|
||||
search for `account` or `script` instead. By default this will return nothing if there are more than one
|
||||
match; if `return_list` is `True` a list of 0, 1 or more matches will be returned instead.
|
||||
- `$obj(query)`, `$dbref(query)` - legacy aliases for `$search`.
|
||||
- `$objlist(query)` - legacy alias for `$search`, always returning a list.
|
||||
|
||||
|
||||
### `evennia.utils.funcparser.ACTOR_STANCE_CALLABLES`
|
||||
|
||||
These are used to implement actor-stance emoting. They are used by the
|
||||
[DefaultObject.msg_contents](evennia.objects.objects.DefaultObject.msg_contents) method
|
||||
by default. You can read a lot more about this on the page
|
||||
[Change messages per receiver](../Concepts/Change-Messages-Per-Receiver.md).
|
||||
|
||||
On the parser side, all these inline functions require extra kwargs be passed into the parser
|
||||
(done by `msg_contents` by default):
|
||||
|
||||
```python
|
||||
parser.parse(string, caller=<obj>, receiver=<obj>, mapping={'key': <obj>, ...})
|
||||
```
|
||||
|
||||
Here the `caller` is the one sending the message and `receiver` the one to see it. The `mapping` contains
|
||||
references to other objects accessible via these callables.
|
||||
|
||||
- `$you([key])` ([code](evennia.utils.funcparser.funcparser_callable_you)) -
|
||||
if no `key` is given, this represents the `caller`, otherwise an object from `mapping`
|
||||
will be used. As this message is sent to different recipients, the `receiver` will change and this will
|
||||
be replaced either with the string `you` (if you and the receiver is the same entity) or with the
|
||||
result of `you_obj.get_display_name(looker=receiver)`. This allows for a single string to echo differently
|
||||
depending on who sees it, and also to reference other people in the same way.
|
||||
- `$You([key])` - same as `$you` but always capitalized.
|
||||
- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb
|
||||
between 2nd person presens to 3rd person presence depending on who
|
||||
sees the string. For example `"$You() $conj(smiles)".` will show as "You smile." and "Tom smiles." depending
|
||||
on who sees it. This makes use of the tools in [evennia.utils.verb_conjugation](evennia.utils.verb_conjugation)
|
||||
to do this, and only works for English verbs.
|
||||
- `$pron(pronoun [,options])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically
|
||||
map pronouns (like his, herself, you, its etc) between 1st/2nd person to 3rd person.
|
||||
|
||||
### Example
|
||||
|
||||
Here's an example of including the default callables together with two custom ones.
|
||||
|
||||
```python
|
||||
from evennia.utils import funcparser
|
||||
from evennia.utils import gametime
|
||||
|
||||
def _dashline(*args, **kwargs):
|
||||
if args:
|
||||
return f"\n-------- {args[0]} --------"
|
||||
return ''
|
||||
|
||||
def _uptime(*args, **kwargs):
|
||||
return gametime.uptime()
|
||||
|
||||
callables = {
|
||||
"dashline": _dashline,
|
||||
"uptime": _uptime,
|
||||
**funcparser.FUNCPARSER_CALLABLES,
|
||||
**funcparser.ACTOR_STANCE_CALLABLES,
|
||||
**funcparser.SEARCHING_CALLABLES
|
||||
}
|
||||
|
||||
parser = funcparser.FuncParser(callables)
|
||||
|
||||
string = "This is the current uptime:$dashline($toint($uptime()) seconds)"
|
||||
result = parser.parse(string)
|
||||
|
||||
```
|
||||
|
||||
Above we define two callables `_dashline` and `_uptime` and map them to names `"dashline"` and `"uptime"`,
|
||||
which is what we then can call as `$header` and `$uptime` in the string. We also have access to
|
||||
all the defaults (like `$toint()`).
|
||||
|
||||
The parsed result of the above would be something like this:
|
||||
|
||||
This is the current uptime:
|
||||
------- 343 seconds -------
|
||||
|
|
@ -1,407 +0,0 @@
|
|||
# Help System
|
||||
|
||||
Evennia has an extensive help system covering both command-help and regular
|
||||
free-form help documentation. It supports subtopics and if failing to find a
|
||||
match it will provide suggestsions, first from alternative topics and then by
|
||||
finding mentions of the search term in help entries.
|
||||
|
||||
|
||||
> help theatre
|
||||
|
||||
```
|
||||
------------------------------------------------------------------------------
|
||||
Help for The theatre (aliases: the hub, curtains)
|
||||
|
||||
The theatre is at the centre of the city, both literally and figuratively ...
|
||||
(A lot more text about it follows ...)
|
||||
|
||||
Subtopics:
|
||||
theatre/lore
|
||||
theatre/layout
|
||||
theatre/dramatis personae
|
||||
------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
> help evennia
|
||||
|
||||
```
|
||||
------------------------------------------------------------------------------
|
||||
No help found
|
||||
|
||||
There is no help topic matching 'evennia'.
|
||||
... But matches where found within the help texts of the suggestions below.
|
||||
|
||||
Suggestions:
|
||||
grapevine2chan, about, irc2chan
|
||||
-----------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
## Using the help system from in-game
|
||||
|
||||
The help system is accessed in-game by use of the `help` command:
|
||||
|
||||
help <topic>
|
||||
|
||||
Sub-topics are accessed as `help <topic>/<subtopic>/...`.
|
||||
|
||||
Creating a new help entry from in-game is done with
|
||||
|
||||
sethelp <topic>[;aliases] [,category] [,lockstring] = <text>
|
||||
|
||||
For example
|
||||
|
||||
sethelp The Gods;pantheon, Lore = In the beginning all was dark ...
|
||||
|
||||
Use the `/edit` switch to open the EvEditor for more convenient in-game writing
|
||||
(but note that devs can also create help entries outside the game using their
|
||||
regular code editor, see below).
|
||||
|
||||
> You can also create help entries as Python modules, outside of the game. See
|
||||
> _FileHelp_ entries below.
|
||||
|
||||
## Sources of help entries
|
||||
|
||||
Evennia collects help entries from three sources:
|
||||
|
||||
- _Auto-generated command help_ - this is literally the doc-strings of
|
||||
the [Command classes](./Commands.md). The idea is that the command docs are
|
||||
easier to maintain and keep up-to-date if the developer can change them at the
|
||||
same time as they do the code.
|
||||
- _Database-stored help entries_ - These are created in-game (using the
|
||||
default `sethelp` command as exemplified in the previous section).
|
||||
- _File-stored help entries_ - These are created outside the game, as dicts in
|
||||
normal Python modules. They allows developers to write and maintain their help
|
||||
files using a proper text editor.
|
||||
|
||||
### The Help Entry
|
||||
|
||||
All help entries (no matter the source) have the following properties:
|
||||
|
||||
- `key` - This is the main topic-name. For Commands, this is literally the
|
||||
command's `key`.
|
||||
- `aliases` - Alternate names for the help entry. This can be useful if the main
|
||||
name is hard to remember.
|
||||
- `help_category` - The general grouping of the entry. This is optional. If not
|
||||
given it will use the default category given by
|
||||
`settings.COMMAND_DEFAULT_HELP_CATEGORY` for Commands and
|
||||
`settings.DEFAULT_HELP_CATEGORY` for file+db help entries.
|
||||
- `locks` - Lock string (for commands) or LockHandler (all help entries).
|
||||
This defines who may read this entry. See the next section.
|
||||
- `tags` - This is not used by default, but could be used to further organize
|
||||
help entries.
|
||||
- `text` - The actual help entry text. This will be dedented and stripped of
|
||||
extra space at beginning and end.
|
||||
|
||||
A `text` that scrolls off the screen will automatically be paginated by
|
||||
the [EvMore](./EvMore.md) pager (you can control this with
|
||||
`settings.HELP_MORE_ENABLED=False`). If you use EvMore and want to control
|
||||
exactly where the pager should break the page, mark the break with the control
|
||||
character `\f`.
|
||||
|
||||
#### Subtopics
|
||||
|
||||
```{versionadded} 1.0
|
||||
```
|
||||
|
||||
Rather than making a very long help entry, the `text` may also be broken up
|
||||
into _subtopics_. A list of the next level of subtopics are shown below the
|
||||
main help text and allows the user to read more about some particular detail
|
||||
that wouldn't fit in the main text.
|
||||
|
||||
Subtopics use a markup slightly similar to markdown headings. The top level
|
||||
heading must be named `# subtopics` (non case-sensitive) and the following
|
||||
headers must be sub-headings to this (so `## subtopic name` etc). All headings
|
||||
are non-case sensitive (the help command will format them). The topics can be
|
||||
nested at most to a depth of 5 (which is probably too many levels already). The
|
||||
parser uses fuzzy matching to find the subtopic, so one does not have to type
|
||||
it all out exactly.
|
||||
|
||||
Below is an example of a `text` with sub topics.
|
||||
|
||||
```
|
||||
The theatre is the heart of the city, here you can find ...
|
||||
(This is the main help text, what you get with `help theatre`)
|
||||
|
||||
# subtopics
|
||||
|
||||
## lore
|
||||
|
||||
The theatre holds many mysterious things...
|
||||
(`help theatre/lore`)
|
||||
|
||||
### the grand opening
|
||||
|
||||
The grand opening is the name for a mysterious event where ghosts appeared ...
|
||||
(`this is a subsub-topic to lore, accessible as `help theatre/lore/grand` or
|
||||
any other partial match).
|
||||
|
||||
### the Phantom
|
||||
|
||||
Deep under the theatre, rumors has it a monster hides ...
|
||||
(another subsubtopic, accessible as `help theatre/lore/phantom`)
|
||||
|
||||
## layout
|
||||
|
||||
The theatre is a two-story building situated at ...
|
||||
(`help theatre/layout`)
|
||||
|
||||
## dramatis personae
|
||||
|
||||
There are many interesting people prowling the halls of the theatre ...
|
||||
(`help theatre/dramatis` or `help theathre/drama` or `help theatre/personae` would work)
|
||||
|
||||
### Primadonna Ada
|
||||
|
||||
Everyone knows the primadonna! She is ...
|
||||
(A subtopic under dramatis personae, accessible as `help theatre/drama/ada` etc)
|
||||
|
||||
### The gatekeeper
|
||||
|
||||
He always keeps an eye on the door and ...
|
||||
(`help theatre/drama/gate`)
|
||||
|
||||
```
|
||||
### Command Auto-help system
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
# [...]
|
||||
|
||||
locks = "cmd:all();read:all()" # default
|
||||
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 can limit access to the help entry by the `view` and/or `read` locks on the
|
||||
Command. See [the section below](./Help-System.md#locking-help-entries) for details.
|
||||
|
||||
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, `settings.COMMAND_DEFAULT_HELP_CATEGORY` (default is "General") is
|
||||
used.
|
||||
|
||||
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.md) 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(caller, cmdset)` 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 most commonly created in-game using the `sethelp` command. If you need to create one
|
||||
manually, you can do so with `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()")
|
||||
```
|
||||
|
||||
The entity being created is a [evennia.help.models.HelpEntry](evennia.help.models.HelpEntry)
|
||||
object. This is _not_ a [Typeclassed](./Typeclasses.md) entity and is not meant to
|
||||
be modified to any great degree. It holds the properties listed earlier. The
|
||||
text is stored in a field `entrytext`. It does not provide a `get_help` method
|
||||
like commands, stores and returns the `entrytext` directly.
|
||||
|
||||
You can search for (db-)-`HelpEntry` objects using `evennia.search_help` but note that
|
||||
this will not return the two other types of help entries.
|
||||
|
||||
### File-help entries
|
||||
|
||||
```{versionadded} 1.0
|
||||
```
|
||||
|
||||
File-help entries are created by the game development team outside of the game. The
|
||||
help entries are defined in normal Python modules (`.py` file ending) containing
|
||||
a `dict` to represent each entry. They require a server `reload` before any changes
|
||||
apply.
|
||||
|
||||
- Evennia will look through all modules given by
|
||||
`settings.FILE_HELP_ENTRY_MODULES`. This should be a list of python-paths for
|
||||
Evennia to import.
|
||||
- If this module contains a top-level variable `HELP_ENTRY_DICTS`, this will be
|
||||
imported and must be a `list` of help-entry dicts.
|
||||
- If no `HELP_ENTRY_DICTS` list is found, _every_ top-level variable in the
|
||||
module that is a `dict` will be read as a help entry. The variable-names will
|
||||
be ignored in this case.
|
||||
|
||||
If you add multiple modules to be read, same-keyed help entries added later in
|
||||
the list will override coming before.
|
||||
|
||||
Each entry dict must define keys to match that needed by all help entries.
|
||||
Here's an example of a help module:
|
||||
|
||||
```python
|
||||
|
||||
# in a module pointed to by settings.FILE_HELP_ENTRY_MODULES
|
||||
|
||||
HELP_ENTRY_DICTS = [
|
||||
{
|
||||
"key": "The Gods", # case-insensitive, can be searched by 'gods' too
|
||||
"aliases": ['pantheon', 'religion']
|
||||
"category": "Lore",
|
||||
"locks": "read:all()", # optional
|
||||
"text": '''
|
||||
The gods formed the world ...
|
||||
|
||||
# Subtopics
|
||||
|
||||
## Pantheon
|
||||
|
||||
The pantheon consists of 40 gods that ...
|
||||
|
||||
### God of love
|
||||
|
||||
The most prominent god is ...
|
||||
|
||||
### God of war
|
||||
|
||||
Also known as 'the angry god', this god is known to ...
|
||||
|
||||
'''
|
||||
},
|
||||
{
|
||||
"key": "The mortals",
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
The help entry text will be dedented and will retain paragraphs. You should try
|
||||
to keep your strings a reasonable width (it will look better). Just reload the
|
||||
server and the file-based help entries will be available to view.
|
||||
|
||||
## Entry priority
|
||||
|
||||
Should you have clashing help-entries between the three types of available
|
||||
entries, the priority is
|
||||
|
||||
Command-auto-help > Db-help > File-help
|
||||
|
||||
So if you create a db-help entry 'foo', it will replace any file-based help
|
||||
entry 'foo'. But if there is a Command 'foo', that's the help you'll get when
|
||||
you enter `help foo`.
|
||||
|
||||
The reasoning for this is that commands must always be understood in order to
|
||||
play the game. Meanwhile db-based help can be kept up-to-date from in-game
|
||||
builders and may be less 'static' than the file-based ones.
|
||||
|
||||
The `sethelp` command (which only deals with creating db-based help entries)
|
||||
will warn you if a new help entry might shadow/be shadowed by a
|
||||
same/similar-named command or file-based help entry.
|
||||
|
||||
## Locking help entries
|
||||
|
||||
The default `help` command gather all available commands and help entries
|
||||
together so they can be searched or listed. By setting locks on the command/help
|
||||
entry one can limit who can read help about it.
|
||||
|
||||
- Commands failing the normal `cmd`-lock will be removed before even getting
|
||||
to the help command. In this case the other two lock types below are ignored.
|
||||
- The `view` access type determines if the command/help entry should be visible in
|
||||
the main help index. If not given, it is assumed everyone can view.
|
||||
- The `read` access type determines if the command/help entry can be actually read.
|
||||
If a `read` lock is given and `view` is not, the `read`-lock is assumed to
|
||||
apply to `view`-access as well (so if you can't read the help entry it will
|
||||
also not show up in the index). If `read`-lock is not given, it's assume
|
||||
everyone can read the help entry.
|
||||
|
||||
For Commands you set the help-related locks the same way you would any lock:
|
||||
|
||||
```python
|
||||
class MyCommand(Command):
|
||||
"""
|
||||
<docstring for command>
|
||||
"""
|
||||
key = "mycommand"
|
||||
# everyone can use the command, builders can view it in the help index
|
||||
# but only devs can actually read the help (a weird setup for sure!)
|
||||
locks = "cmd:all();view:perm(Builders);read:perm(Developers)
|
||||
|
||||
```
|
||||
|
||||
Db-help entries and File-Help entries work the same way (except the `cmd`-type
|
||||
lock is not used. A file-help example:
|
||||
|
||||
```python
|
||||
help_entry = {
|
||||
# ...
|
||||
locks = "read:perm(Developer)",
|
||||
# ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```{versionchanged} 1.0
|
||||
Changed the old 'view' lock to control the help-index inclusion and added
|
||||
the new 'read' lock-type to control access to the entry itself.
|
||||
```
|
||||
|
||||
## Customizing the look of the help system
|
||||
|
||||
This is done almost exclusively by overriding the `help` command
|
||||
[evennia.commands.default.help.CmdHelp](evennia.commands.default.help.CmdHelp).
|
||||
|
||||
Since the available commands may vary from moment to moment, `help` is
|
||||
responsible for collating the three sources of help-entries (commands/db/file)
|
||||
together and search through them on the fly. It also does all the formatting of
|
||||
the output.
|
||||
|
||||
To make it easier to tweak the look, the parts of the code that changes the
|
||||
visual presentation and entity searching has been broken out into separate
|
||||
methods on the command class. Override these in your version of `help` to change
|
||||
the display or tweak as you please. See the api link above for details.
|
||||
|
||||
## Technical notes
|
||||
|
||||
Since it needs to search so different types of data, the help system has to
|
||||
collect all possibilities in memory before searching through the entire set. It
|
||||
uses the [Lunr](https://github.com/yeraydiazdiaz/lunr.py) search engine to
|
||||
search through the main bulk of help entries. Lunr is a mature engine used for
|
||||
web-pages and produces much more sensible results than previous solutions.
|
||||
|
||||
Once the main entry has been found, subtopics are then searched with
|
||||
simple `==`, `startswith` and `in` matching (there are so relatively few of them
|
||||
at that point).
|
||||
|
||||
```{versionchanged} 1.0
|
||||
Replaced the old bag-of-words algorithm with lunr package.
|
||||
|
||||
```
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
# Inputfuncs
|
||||
|
||||
|
||||
An *inputfunc* is an Evennia function that handles a particular input (an [inputcommand](../Concepts/OOB.md)) from
|
||||
the client. The inputfunc is the last destination for the inputcommand along the [ingoing message
|
||||
path](../Concepts/Messagepath.md#the-ingoing-message-path). The inputcommand always has the form `(commandname,
|
||||
(args), {kwargs})` and Evennia will use this to try to find and call an inputfunc on the form
|
||||
|
||||
```python
|
||||
def commandname(session, *args, **kwargs):
|
||||
# ...
|
||||
|
||||
```
|
||||
Or, if no match was found, it will call an inputfunc named "default" on this form
|
||||
|
||||
```python
|
||||
def default(session, cmdname, *args, **kwargs):
|
||||
# cmdname is the name of the mismatched inputcommand
|
||||
|
||||
```
|
||||
|
||||
## Adding your own inputfuncs
|
||||
|
||||
This is simple. Add a function on the above form to `mygame/server/conf/inputfuncs.py`. Your
|
||||
function must be in the global, outermost scope of that module and not start with an underscore
|
||||
(`_`) to be recognized as an inputfunc. Reload the server. That's it. To overload a default
|
||||
inputfunc (see below), just add a function with the same name.
|
||||
|
||||
The modules Evennia looks into for inputfuncs are defined in the list `settings.INPUT_FUNC_MODULES`.
|
||||
This list will be imported from left to right and later imported functions will replace earlier
|
||||
ones.
|
||||
|
||||
## Default inputfuncs
|
||||
|
||||
Evennia defines a few default inputfuncs to handle the common cases. These are defined in
|
||||
`evennia/server/inputfuncs.py`.
|
||||
|
||||
### text
|
||||
|
||||
- Input: `("text", (textstring,), {})`
|
||||
- Output: Depends on Command triggered
|
||||
|
||||
This is the most common of inputcommands, and the only one supported by every traditional mud. The
|
||||
argument is usually what the user sent from their command line. Since all text input from the user
|
||||
like this is considered a [Command](./Commands.md), this inputfunc will do things like nick-replacement
|
||||
and then pass on the input to the central Commandhandler.
|
||||
|
||||
### echo
|
||||
|
||||
- Input: `("echo", (args), {})`
|
||||
- Output: `("text", ("Echo returns: %s" % args), {})`
|
||||
|
||||
This is a test input, which just echoes the argument back to the session as text. Can be used for
|
||||
testing custom client input.
|
||||
|
||||
### default
|
||||
|
||||
The default function, as mentioned above, absorbs all non-recognized inputcommands. The default one
|
||||
will just log an error.
|
||||
|
||||
### client_options
|
||||
|
||||
- Input: `("client_options, (), {key:value, ...})`
|
||||
- Output:
|
||||
- normal: None
|
||||
- get: `("client_options", (), {key:value, ...})`
|
||||
|
||||
This is a direct command for setting protocol options. These are settable with the `@option`
|
||||
command, but this offers a client-side way to set them. Not all connection protocols makes use of
|
||||
all flags, but here are the possible keywords:
|
||||
|
||||
- get (bool): If this is true, ignore all other kwargs and immediately return the current settings
|
||||
as an outputcommand `("client_options", (), {key=value, ...})`-
|
||||
- client (str): A client identifier, like "mushclient".
|
||||
- version (str): A client version
|
||||
- ansi (bool): Supports ansi colors
|
||||
- xterm256 (bool): Supports xterm256 colors or not
|
||||
- mxp (bool): Supports MXP or not
|
||||
- utf-8 (bool): Supports UTF-8 or not
|
||||
- screenreader (bool): Screen-reader mode on/off
|
||||
- mccp (bool): MCCP compression on/off
|
||||
- screenheight (int): Screen height in lines
|
||||
- screenwidth (int): Screen width in characters
|
||||
- inputdebug (bool): Debug input functions
|
||||
- nomarkup (bool): Strip all text tags
|
||||
- raw (bool): Leave text tags unparsed
|
||||
|
||||
> Note that there are two GMCP aliases to this inputfunc - `hello` and `supports_set`, which means
|
||||
it will be accessed via the GMCP `Hello` and `Supports.Set` instructions assumed by some clients.
|
||||
|
||||
### get_client_options
|
||||
|
||||
- Input: `("get_client_options, (), {key:value, ...})`
|
||||
- Output: `("client_options, (), {key:value, ...})`
|
||||
|
||||
This is a convenience wrapper that retrieves the current options by sending "get" to
|
||||
`client_options` above.
|
||||
|
||||
### get_inputfuncs
|
||||
|
||||
- Input: `("get_inputfuncs", (), {})`
|
||||
- Output: `("get_inputfuncs", (), {funcname:docstring, ...})`
|
||||
|
||||
Returns an outputcommand on the form `("get_inputfuncs", (), {funcname:docstring, ...})` - a list of
|
||||
all the available inputfunctions along with their docstrings.
|
||||
|
||||
### login
|
||||
|
||||
> Note: this is currently experimental and not very well tested.
|
||||
|
||||
- Input: `("login", (username, password), {})`
|
||||
- Output: Depends on login hooks
|
||||
|
||||
This performs the inputfunc version of a login operation on the current Session.
|
||||
|
||||
### get_value
|
||||
|
||||
Input: `("get_value", (name, ), {})`
|
||||
Output: `("get_value", (value, ), {})`
|
||||
|
||||
Retrieves a value from the Character or Account currently controlled by this Session. Takes one
|
||||
argument, This will only accept particular white-listed names, you'll need to overload the function
|
||||
to expand. By default the following values can be retrieved:
|
||||
|
||||
- "name" or "key": The key of the Account or puppeted Character.
|
||||
- "location": Name of the current location, or "None".
|
||||
- "servername": Name of the Evennia server connected to.
|
||||
|
||||
### repeat
|
||||
|
||||
- Input: `("repeat", (), {"callback":funcname,
|
||||
"interval": secs, "stop": False})`
|
||||
- Output: Depends on the repeated function. Will return `("text", (repeatlist),{}` with a list of
|
||||
accepted names if given an unfamiliar callback name.
|
||||
|
||||
This will tell evennia to repeatedly call a named function at a given interval. Behind the scenes
|
||||
this will set up a [Ticker](./TickerHandler.md). Only previously acceptable functions are possible to
|
||||
repeat-call in this way, you'll need to overload this inputfunc to add the ones you want to offer.
|
||||
By default only two example functions are allowed, "test1" and "test2", which will just echo a text
|
||||
back at the given interval. Stop the repeat by sending `"stop": True` (note that you must include
|
||||
both the callback name and interval for Evennia to know what to stop).
|
||||
|
||||
### unrepeat
|
||||
|
||||
- Input: `("unrepeat", (), ("callback":funcname,
|
||||
"interval": secs)`
|
||||
- Output: None
|
||||
|
||||
This is a convenience wrapper for sending "stop" to the `repeat` inputfunc.
|
||||
|
||||
### monitor
|
||||
|
||||
- Input: `("monitor", (), ("name":field_or_argname, stop=False)`
|
||||
- Output (on change): `("monitor", (), {"name":name, "value":value})`
|
||||
|
||||
This sets up on-object monitoring of Attributes or database fields. Whenever the field or Attribute
|
||||
changes in any way, the outputcommand will be sent. This is using the
|
||||
[MonitorHandler](./MonitorHandler.md) behind the scenes. Pass the "stop" key to stop monitoring. Note
|
||||
that you must supply the name also when stopping to let the system know which monitor should be
|
||||
cancelled.
|
||||
|
||||
Only fields/attributes in a whitelist are allowed to be used, you have to overload this function to
|
||||
add more. By default the following fields/attributes can be monitored:
|
||||
|
||||
- "name": The current character name
|
||||
- "location": The current location
|
||||
- "desc": The description Argument
|
||||
|
||||
## unmonitor
|
||||
|
||||
- Input: `("unmonitor", (), {"name":name})`
|
||||
- Output: None
|
||||
|
||||
A convenience wrapper that sends "stop" to the `monitor` function.
|
||||
|
|
@ -1,387 +0,0 @@
|
|||
# Locks
|
||||
|
||||
|
||||
For most games it is a good idea to restrict what people can do. In Evennia such restrictions are
|
||||
applied and checked by something called *locks*. All Evennia entities ([Commands](./Commands.md),
|
||||
[Objects](./Objects.md), [Scripts](./Scripts.md), [Accounts](./Accounts.md), [Help System](./Help-System.md),
|
||||
[messages](./Msg.md) and [channels](./Channels.md)) are accessed through locks.
|
||||
|
||||
A lock can be thought of as an "access rule" restricting a particular use of an Evennia entity.
|
||||
Whenever another entity wants that kind of access the lock will analyze that entity in different
|
||||
ways to determine if access should be granted or not. Evennia implements a "lockdown" philosophy -
|
||||
all entities are inaccessible unless you explicitly define a lock that allows some or full access.
|
||||
|
||||
Let's take an example: An object has a lock on itself that restricts how people may "delete" that
|
||||
object. Apart from knowing that it restricts deletion, the lock also knows that only players with
|
||||
the specific ID of, say, `34` are allowed to delete it. So whenever a player tries to run `delete`
|
||||
on the object, the `delete` command makes sure to check if this player is really allowed to do so.
|
||||
It calls the lock, which in turn checks if the player's id is `34`. Only then will it allow `delete`
|
||||
to go on with its job.
|
||||
|
||||
## Setting and checking a lock
|
||||
|
||||
The in-game command for setting locks on objects is `lock`:
|
||||
|
||||
> lock obj = <lockstring>
|
||||
|
||||
The `<lockstring>` is a string of a certain form that defines the behaviour of the lock. We will go
|
||||
into more detail on how `<lockstring>` should look in the next section.
|
||||
|
||||
Code-wise, Evennia handles locks through what is usually called `locks` on all relevant entities.
|
||||
This is a handler that allows you to add, delete and check locks.
|
||||
|
||||
```python
|
||||
myobj.locks.add(<lockstring>)
|
||||
```
|
||||
|
||||
One can call `locks.check()` to perform a lock check, but to hide the underlying implementation all
|
||||
objects also have a convenience function called `access`. This should preferably be used. In the
|
||||
example below, `accessing_obj` is the object requesting the 'delete' access whereas `obj` is the
|
||||
object that might get deleted. This is how it would look (and does look) from inside the `delete`
|
||||
command:
|
||||
|
||||
```python
|
||||
if not obj.access(accessing_obj, 'delete'):
|
||||
accessing_obj.msg("Sorry, you may not delete that.")
|
||||
return
|
||||
```
|
||||
|
||||
## Defining locks
|
||||
|
||||
Defining a lock (i.e. an access restriction) in Evennia is done by adding simple strings of lock
|
||||
definitions to the object's `locks` property using `obj.locks.add()`.
|
||||
|
||||
Here are some examples of lock strings (not including the quotes):
|
||||
|
||||
```python
|
||||
delete:id(34) # only allow obj #34 to delete
|
||||
edit:all() # let everyone edit
|
||||
# only those who are not "very_weak" or are Admins may pick this up
|
||||
get: not attr(very_weak) or perm(Admin)
|
||||
```
|
||||
|
||||
Formally, a lockstring has the following syntax:
|
||||
|
||||
```python
|
||||
access_type: [NOT] lockfunc1([arg1,..]) [AND|OR] [NOT] lockfunc2([arg1,...]) [...]
|
||||
```
|
||||
|
||||
where `[]` marks optional parts. `AND`, `OR` and `NOT` are not case sensitive and excess spaces are
|
||||
ignored. `lockfunc1, lockfunc2` etc are special _lock functions_ available to the lock system.
|
||||
|
||||
So, a lockstring consists of the type of restriction (the `access_type`), a colon (`:`) and then an
|
||||
expression involving function calls that determine what is needed to pass the lock. Each function
|
||||
returns either `True` or `False`. `AND`, `OR` and `NOT` work as they do normally in Python. If the
|
||||
total result is `True`, the lock is passed.
|
||||
|
||||
You can create several lock types one after the other by separating them with a semicolon (`;`) in
|
||||
the lockstring. The string below yields the same result as the previous example:
|
||||
|
||||
delete:id(34);edit:all();get: not attr(very_weak) or perm(Admin)
|
||||
|
||||
|
||||
### Valid access_types
|
||||
|
||||
An `access_type`, the first part of a lockstring, defines what kind of capability a lock controls,
|
||||
such as "delete" or "edit". You may in principle name your `access_type` anything as long as it is
|
||||
unique for the particular object. The name of the access types is not case-sensitive.
|
||||
|
||||
If you want to make sure the lock is used however, you should pick `access_type` names that you (or
|
||||
the default command set) actually checks for, as in the example of `delete` above that uses the
|
||||
'delete' `access_type`.
|
||||
|
||||
Below are the access_types checked by the default commandset.
|
||||
|
||||
- [Commands](./Commands.md)
|
||||
- `cmd` - this defines who may call this command at all.
|
||||
- [Objects](./Objects.md):
|
||||
- `control` - who is the "owner" of the object. Can set locks, delete it etc. Defaults to the
|
||||
creator of the object.
|
||||
- `call` - who may call Object-commands stored on this Object except for the Object itself. By
|
||||
default, Objects share their Commands with anyone in the same location (e.g. so you can 'press' a
|
||||
`Button` object in the room). For Characters and Mobs (who likely only use those Commands for
|
||||
themselves and don't want to share them) this should usually be turned off completely, using
|
||||
something like `call:false()`.
|
||||
- `examine` - who may examine this object's properties.
|
||||
- `delete` - who may delete the object.
|
||||
- `edit` - who may edit properties and attributes of the object.
|
||||
- `view` - if the `look` command will display/list this object in descriptions
|
||||
and if you will be able to see its description. Note that if
|
||||
you target it specifically by name, the system will still find it, just
|
||||
not be able to look at it. See `search` lock to completely hide the item.
|
||||
- `search` - this controls if the object can be found with the
|
||||
`DefaultObject.search` method (usually referred to with `caller.search`
|
||||
in Commands). This is how to create entirely 'undetectable' in-game objects.
|
||||
If not setting this lock excplicitly, all objects are assumed searchable.
|
||||
Note that if you are aiming to make some _permanently invisible game system,
|
||||
using a [Script](./Scripts.md) is a better bet.
|
||||
- `get`- who may pick up the object and carry it around.
|
||||
- `puppet` - who may "become" this object and control it as their "character".
|
||||
- `attrcreate` - who may create new attributes on the object (default True)
|
||||
- [Characters](./Objects.md#characters):
|
||||
- Same as for Objects
|
||||
- [Exits](./Objects.md#exits):
|
||||
- Same as for Objects
|
||||
- `traverse` - who may pass the exit.
|
||||
- [Accounts](./Accounts.md):
|
||||
- `examine` - who may examine the account's properties.
|
||||
- `delete` - who may delete the account.
|
||||
- `edit` - who may edit the account's attributes and properties.
|
||||
- `msg` - who may send messages to the account.
|
||||
- `boot` - who may boot the account.
|
||||
- [Attributes](./Attributes.md): (only checked by `obj.secure_attr`)
|
||||
- `attrread` - see/access attribute
|
||||
- `attredit` - change/delete attribute
|
||||
- [Channels](./Channels.md):
|
||||
- `control` - who is administrating the channel. This means the ability to delete the channel,
|
||||
boot listeners etc.
|
||||
- `send` - who may send to the channel.
|
||||
- `listen` - who may subscribe and listen to the channel.
|
||||
- [HelpEntry](./Help-System.md):
|
||||
- `examine` - who may view this help entry (usually everyone)
|
||||
- `edit` - who may edit this help entry.
|
||||
|
||||
So to take an example, whenever an exit is to be traversed, a lock of the type *traverse* will be
|
||||
checked. Defining a suitable lock type for an exit object would thus involve a lockstring `traverse:
|
||||
<lock functions>`.
|
||||
|
||||
### Custom access_types
|
||||
|
||||
As stated above, the `access_type` part of the lock is simply the 'name' or 'type' of the lock. The
|
||||
text is an arbitrary string that must be unique for an object. If adding a lock with the same
|
||||
`access_type` as one that already exists on the object, the new one override the old one.
|
||||
|
||||
For example, if you wanted to create a bulletin board system and wanted to restrict who can either
|
||||
read a board or post to a board. You could then define locks such as:
|
||||
|
||||
```python
|
||||
obj.locks.add("read:perm(Player);post:perm(Admin)")
|
||||
```
|
||||
|
||||
This will create a 'read' access type for Characters having the `Player` permission or above and a
|
||||
'post' access type for those with `Admin` permissions or above (see below how the `perm()` lock
|
||||
function works). When it comes time to test these permissions, simply check like this (in this
|
||||
example, the `obj` may be a board on the bulletin board system and `accessing_obj` is the player
|
||||
trying to read the board):
|
||||
|
||||
```python
|
||||
if not obj.access(accessing_obj, 'read'):
|
||||
accessing_obj.msg("Sorry, you may not read that.")
|
||||
return
|
||||
```
|
||||
|
||||
### Lock functions
|
||||
|
||||
A lock function is a normal Python function put in a place Evennia looks for such functions. The
|
||||
modules Evennia looks at is the list `settings.LOCK_FUNC_MODULES`. *All functions* in any of those
|
||||
modules will automatically be considered a valid lock function. The default ones are found in
|
||||
`evennia/locks/lockfuncs.py` and you can start adding your own in `mygame/server/conf/lockfuncs.py`.
|
||||
You can append the setting to add more module paths. To replace a default lock function, just add
|
||||
your own with the same name.
|
||||
|
||||
A lock function must always accept at least two arguments - the *accessing object* (this is the
|
||||
object wanting to get access) and the *accessed object* (this is the object with the lock). Those
|
||||
two are fed automatically as the first two arguments to the function when the lock is checked. Any
|
||||
arguments explicitly given in the lock definition will appear as extra arguments.
|
||||
|
||||
```python
|
||||
# A simple example lock function. Called with e.g. `id(34)`. This is
|
||||
# defined in, say mygame/server/conf/lockfuncs.py
|
||||
|
||||
def id(accessing_obj, accessed_obj, *args, **kwargs):
|
||||
if args:
|
||||
wanted_id = args[0]
|
||||
return accessing_obj.id == wanted_id
|
||||
return False
|
||||
```
|
||||
|
||||
The above could for example be used in a lock function like this:
|
||||
|
||||
```python
|
||||
# we have `obj` and `owner_object` from before
|
||||
obj.locks.add(f"edit: id({owner_object.id})")
|
||||
```
|
||||
|
||||
We could check if the "edit" lock is passed with something like this:
|
||||
|
||||
```python
|
||||
# as part of a Command's func() method, for example
|
||||
if not obj.access(caller, "edit"):
|
||||
caller.msg("You don't have access to edit this!")
|
||||
return
|
||||
```
|
||||
|
||||
In this example, everyone except the `caller` with the right `id` will get the error.
|
||||
|
||||
> (Using the `*` and `**` syntax causes Python to magically put all extra arguments into a list
|
||||
`args` and all keyword arguments into a dictionary `kwargs` respectively. If you are unfamiliar with
|
||||
how `*args` and `**kwargs` work, see the Python manuals).
|
||||
|
||||
Some useful default lockfuncs (see `src/locks/lockfuncs.py` for more):
|
||||
|
||||
- `true()/all()` - give access to everyone
|
||||
- `false()/none()/superuser()` - give access to none. Superusers bypass the check entirely and are
|
||||
thus the only ones who will pass this check.
|
||||
- `perm(perm)` - this tries to match a given `permission` property, on an Account firsthand, on a
|
||||
Character second. See [below](./Permissions.md).
|
||||
- `perm_above(perm)` - like `perm` but requires a "higher" permission level than the one given.
|
||||
- `id(num)/dbref(num)` - checks so the access_object has a certain dbref/id.
|
||||
- `attr(attrname)` - checks if a certain [Attribute](./Attributes.md) exists on accessing_object.
|
||||
- `attr(attrname, value)` - checks so an attribute exists on accessing_object *and* has the given
|
||||
value.
|
||||
- `attr_gt(attrname, value)` - checks so accessing_object has a value larger (`>`) than the given
|
||||
value.
|
||||
- `attr_ge, attr_lt, attr_le, attr_ne` - corresponding for `>=`, `<`, `<=` and `!=`.
|
||||
- `holds(objid)` - checks so the accessing objects contains an object of given name or dbref.
|
||||
- `inside()` - checks so the accessing object is inside the accessed object (the inverse of
|
||||
`holds()`).
|
||||
- `pperm(perm)`, `pid(num)/pdbref(num)` - same as `perm`, `id/dbref` but always looks for
|
||||
permissions and dbrefs of *Accounts*, not on Characters.
|
||||
- `serversetting(settingname, value)` - Only returns True if Evennia has a given setting or a
|
||||
setting set to a given value.
|
||||
|
||||
## Checking simple strings
|
||||
|
||||
Sometimes you don't really need to look up a certain lock, you just want to check a lockstring. A
|
||||
common use is inside Commands, in order to check if a user has a certain permission. The lockhandler
|
||||
has a method `check_lockstring(accessing_obj, lockstring, bypass_superuser=False)` that allows this.
|
||||
|
||||
```python
|
||||
# inside command definition
|
||||
if not self.caller.locks.check_lockstring(self.caller, "dummy:perm(Admin)"):
|
||||
self.caller.msg("You must be an Admin or higher to do this!")
|
||||
return
|
||||
```
|
||||
|
||||
Note here that the `access_type` can be left to a dummy value since this method does not actually do
|
||||
a Lock lookup.
|
||||
|
||||
## Default locks
|
||||
|
||||
Evennia sets up a few basic locks on all new objects and accounts (if we didn't, noone would have
|
||||
any access to anything from the start). This is all defined in the root [Typeclasses](./Typeclasses.md)
|
||||
of the respective entity, in the hook method `basetype_setup()` (which you usually don't want to
|
||||
edit unless you want to change how basic stuff like rooms and exits store their internal variables).
|
||||
This is called once, before `at_object_creation`, so just put them in the latter method on your
|
||||
child object to change the default. Also creation commands like `create` changes the locks of
|
||||
objects you create - for example it sets the `control` lock_type so as to allow you, its creator, to
|
||||
control and delete the object.
|
||||
|
||||
|
||||
## More Lock definition examples
|
||||
|
||||
examine: attr(eyesight, excellent) or perm(Builders)
|
||||
|
||||
You are only allowed to do *examine* on this object if you have 'excellent' eyesight (that is, has
|
||||
an Attribute `eyesight` with the value `excellent` defined on yourself) or if you have the
|
||||
"Builders" permission string assigned to you.
|
||||
|
||||
open: holds('the green key') or perm(Builder)
|
||||
|
||||
This could be called by the `open` command on a "door" object. The check is passed if you are a
|
||||
Builder or has the right key in your inventory.
|
||||
|
||||
cmd: perm(Builders)
|
||||
|
||||
Evennia's command handler looks for a lock of type `cmd` to determine if a user is allowed to even
|
||||
call upon a particular command or not. When you define a command, this is the kind of lock you must
|
||||
set. See the default command set for lots of examples. If a character/account don't pass the `cmd`
|
||||
lock type the command will not even appear in their `help` list.
|
||||
|
||||
cmd: not perm(no_tell)
|
||||
|
||||
"Permissions" can also be used to block users or implement highly specific bans. The above example
|
||||
would be be added as a lock string to the `tell` command. This will allow everyone *not* having the
|
||||
"permission" `no_tell` to use the `tell` command. You could easily give an account the "permission"
|
||||
`no_tell` to disable their use of this particular command henceforth.
|
||||
|
||||
|
||||
```python
|
||||
dbref = caller.id
|
||||
lockstring = "control:id(%s);examine:perm(Builders);delete:id(%s) or perm(Admin);get:all()" %
|
||||
(dbref, dbref)
|
||||
new_obj.locks.add(lockstring)
|
||||
```
|
||||
|
||||
This is how the `create` command sets up new objects. In sequence, this permission string sets the
|
||||
owner of this object be the creator (the one running `create`). Builders may examine the object
|
||||
whereas only Admins and the creator may delete it. Everyone can pick it up.
|
||||
|
||||
## A complete example of setting locks on an object
|
||||
|
||||
Assume we have two objects - one is ourselves (not superuser) and the other is an [Object](./Objects.md)
|
||||
called `box`.
|
||||
|
||||
> create/drop box
|
||||
> desc box = "This is a very big and heavy box."
|
||||
|
||||
We want to limit which objects can pick up this heavy box. Let's say that to do that we require the
|
||||
would-be lifter to to have an attribute *strength* on themselves, with a value greater than 50. We
|
||||
assign it to ourselves to begin with.
|
||||
|
||||
> set self/strength = 45
|
||||
|
||||
Ok, so for testing we made ourselves strong, but not strong enough. Now we need to look at what
|
||||
happens when someone tries to pick up the the box - they use the `get` command (in the default set).
|
||||
This is defined in `evennia/commands/default/general.py`. In its code we find this snippet:
|
||||
|
||||
```python
|
||||
if not obj.access(caller, 'get'):
|
||||
if obj.db.get_err_msg:
|
||||
caller.msg(obj.db.get_err_msg)
|
||||
else:
|
||||
caller.msg("You can't get that.")
|
||||
return
|
||||
```
|
||||
|
||||
So the `get` command looks for a lock with the type *get* (not so surprising). It also looks for an
|
||||
[Attribute](./Attributes.md) on the checked object called _get_err_msg_ in order to return a customized
|
||||
error message. Sounds good! Let's start by setting that on the box:
|
||||
|
||||
> set box/get_err_msg = You are not strong enough to lift this box.
|
||||
|
||||
Next we need to craft a Lock of type *get* on our box. We want it to only be passed if the accessing
|
||||
object has the attribute *strength* of the right value. For this we would need to create a lock
|
||||
function that checks if attributes have a value greater than a given value. Luckily there is already
|
||||
such a one included in evennia (see `evennia/locks/lockfuncs.py`), called `attr_gt`.
|
||||
|
||||
So the lock string will look like this: `get:attr_gt(strength, 50)`. We put this on the box now:
|
||||
|
||||
lock box = get:attr_gt(strength, 50)
|
||||
|
||||
Try to `get` the object and you should get the message that we are not strong enough. Increase your
|
||||
strength above 50 however and you'll pick it up no problem. Done! A very heavy box!
|
||||
|
||||
If you wanted to set this up in python code, it would look something like this:
|
||||
|
||||
```python
|
||||
|
||||
from evennia import create_object
|
||||
|
||||
# create, then set the lock
|
||||
box = create_object(None, key="box")
|
||||
box.locks.add("get:attr_gt(strength, 50)")
|
||||
|
||||
# or we can assign locks in one go right away
|
||||
box = create_object(None, key="box", locks="get:attr_gt(strength, 50)")
|
||||
|
||||
# set the attributes
|
||||
box.db.desc = "This is a very big and heavy box."
|
||||
box.db.get_err_msg = "You are not strong enough to lift this box."
|
||||
|
||||
# one heavy box, ready to withstand all but the strongest...
|
||||
```
|
||||
|
||||
## On Django's permission system
|
||||
|
||||
Django also implements a comprehensive permission/security system of its own. The reason we don't
|
||||
use that is because it is app-centric (app in the Django sense). Its permission strings are of the
|
||||
form `appname.permstring` and it automatically adds three of them for each database model in the app
|
||||
- for the app evennia/object this would be for example 'object.create', 'object.admin' and
|
||||
'object.edit'. This makes a lot of sense for a web application, not so much for a MUD, especially
|
||||
when we try to hide away as much of the underlying architecture as possible.
|
||||
|
||||
The django permissions are not completely gone however. We use it for validating passwords during
|
||||
login. It is also used exclusively for managing Evennia's web-based admin site, which is a graphical
|
||||
front-end for the database of Evennia. You edit and assign such permissions directly from the web
|
||||
interface. It's stand-alone from the permissions described above.
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# MonitorHandler
|
||||
|
||||
|
||||
The *MonitorHandler* is a system for watching changes in properties or Attributes on objects. A
|
||||
monitor can be thought of as a sort of trigger that responds to change.
|
||||
|
||||
The main use for the MonitorHandler is to report changes to the client; for example the client
|
||||
Session may ask Evennia to monitor the value of the Characer's `health` attribute and report
|
||||
whenever it changes. This way the client could for example update its health bar graphic as needed.
|
||||
|
||||
## Using the MonitorHandler
|
||||
|
||||
The MontorHandler is accessed from the singleton `evennia.MONITOR_HANDLER`. The code for the handler
|
||||
is in `evennia.scripts.monitorhandler`.
|
||||
|
||||
Here's how to add a new monitor:
|
||||
|
||||
```python
|
||||
from evennia import MONITOR_HANDLER
|
||||
|
||||
MONITOR_HANDLER.add(obj, fieldname, callback,
|
||||
idstring="", persistent=False, **kwargs)
|
||||
|
||||
```
|
||||
|
||||
- `obj` ([Typeclassed](./Typeclasses.md) entity) - the object to monitor. Since this must be
|
||||
typeclassed, it means you can't monitor changes on [Sessions](./Sessions.md) with the monitorhandler, for
|
||||
example.
|
||||
- `fieldname` (str) - the name of a field or [Attribute](./Attributes.md) on `obj`. If you want to
|
||||
monitor a database field you must specify its full name, including the starting `db_` (like
|
||||
`db_key`, `db_location` etc). Any names not starting with `db_` are instead assumed to be the names
|
||||
of Attributes. This difference matters, since the MonitorHandler will automatically know to watch
|
||||
the `db_value` field of the Attribute.
|
||||
- `callback`(callable) - This will be called as `callback(fieldname=fieldname, obj=obj, **kwargs)`
|
||||
when the field updates.
|
||||
- `idstring` (str) - this is used to separate multiple monitors on the same object and fieldname.
|
||||
This is required in order to properly identify and remove the monitor later. It's also used for
|
||||
saving it.
|
||||
- `persistent` (bool) - if True, the monitor will survive a server reboot.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from evennia import MONITOR_HANDLER as monitorhandler
|
||||
|
||||
def _monitor_callback(fieldname="", obj=None, **kwargs):
|
||||
# reporting callback that works both
|
||||
# for db-fields and Attributes
|
||||
if fieldname.startswith("db_"):
|
||||
new_value = getattr(obj, fieldname)
|
||||
else: # an attribute
|
||||
new_value = obj.attributes.get(fieldname)
|
||||
obj.msg(f"{obj.key}.{fieldname} changed to '{new_value}'.")
|
||||
|
||||
# (we could add _some_other_monitor_callback here too)
|
||||
|
||||
# monitor Attribute (assume we have obj from before)
|
||||
monitorhandler.add(obj, "desc", _monitor_callback)
|
||||
|
||||
# monitor same db-field with two different callbacks (must separate by id_string)
|
||||
monitorhandler.add(obj, "db_key", _monitor_callback, id_string="foo")
|
||||
monitorhandler.add(obj, "db_key", _some_other_monitor_callback, id_string="bar")
|
||||
|
||||
```
|
||||
|
||||
A monitor is uniquely identified by the combination of the *object instance* it is monitoring, the
|
||||
*name* of the field/attribute to monitor on that object and its `idstring` (`obj` + `fieldname` +
|
||||
`idstring`). The `idstring` will be the empty string unless given explicitly.
|
||||
|
||||
So to "un-monitor" the above you need to supply enough information for the system to uniquely find
|
||||
the monitor to remove:
|
||||
|
||||
```
|
||||
monitorhandler.remove(obj, "desc")
|
||||
monitorhandler.remove(obj, "db_key", idstring="foo")
|
||||
monitorhandler.remove(obj, "db_key", idstring="bar")
|
||||
```
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
# Msg
|
||||
|
||||
The [Msg](evennia.comms.models.Msg) object represents a database-saved
|
||||
piece of communication. Think of it as a discrete piece of email - it contains
|
||||
a message, some metadata and will always have a sender and one or more
|
||||
recipients.
|
||||
|
||||
Once created, a Msg is normally not changed. It is persitently saved in the
|
||||
database. This allows for comprehensive logging of communications. Here are some
|
||||
good uses for `Msg` objects:
|
||||
|
||||
- page/tells (the `page` command is how Evennia uses them out of the box)
|
||||
- messages in a bulletin board
|
||||
- game-wide email stored in 'mailboxes'.
|
||||
|
||||
|
||||
```{important}
|
||||
|
||||
A `Msg` does not have any in-game representation. So if you want to use them
|
||||
to represent in-game mail/letters, the physical letters would never be
|
||||
visible in a room (possible to steal, spy on etc) unless you make your
|
||||
spy-system access the Msgs directly (or go to the trouble of spawning an
|
||||
actual in-game letter-object based on the Msg)
|
||||
|
||||
|
||||
```
|
||||
|
||||
```{versionchanged} 1.0
|
||||
Channels dropped Msg-support. Now only used in `page` command by default.
|
||||
```
|
||||
|
||||
## Msg in code
|
||||
|
||||
The Msg is intended to be used exclusively in code, to build other game systems. It is _not_
|
||||
a [Typeclassed](./Typeclasses.md) entity, which means it cannot (easily) be overridden. It
|
||||
doesn't support Attributes (but it _does_ support [Tags](./Tags.md)). It tries to be lean
|
||||
and small since a new one is created for every message.
|
||||
|
||||
You create a new message with `evennia.create_message`:
|
||||
|
||||
```python
|
||||
from evennia import create_message
|
||||
message = create_message(senders, message, receivers,
|
||||
locks=..., tags=..., header=...)
|
||||
```
|
||||
|
||||
You can search for `Msg` objects in various ways:
|
||||
|
||||
|
||||
```python
|
||||
from evennia import search_message, Msg
|
||||
|
||||
# args are optional. Only a single sender/receiver should be passed
|
||||
messages = search_message(sender=..., receiver=..., freetext=..., dbref=...)
|
||||
|
||||
# get all messages for a given sender/receiver
|
||||
messages = Msg.objects.get_msg_by_sender(sender)
|
||||
messages = Msg.objects.get_msg_by_receiver(recipient)
|
||||
|
||||
```
|
||||
|
||||
### Properties on Msg
|
||||
|
||||
- `senders` - there must always be at least one sender. This is a set of
|
||||
- [Account](./Accounts.md), [Object](./Objects.md), [Script](./Scripts.md)
|
||||
or `str` in any combination (but usually a message only targets one type).
|
||||
Using a `str` for a sender indicates it's an 'external' sender and
|
||||
and can be used to point to a sender that is not a typeclassed entity. This is not used by default
|
||||
and what this would be depends on the system (it could be a unique id or a
|
||||
python-path, for example). While most systems expect a single sender, it's
|
||||
possible to have any number of them.
|
||||
- `receivers` - these are the ones to see the Msg. These are again any combination of
|
||||
[Account](./Accounts.md), [Object](./Objects.md) or [Script](./Scripts.md) or `str` (an 'external' receiver).
|
||||
It's in principle possible to have zero receivers but most usages of Msg expects one or more.
|
||||
- `header` - this is an optional text field that can contain meta-information about the message. For
|
||||
an email-like system it would be the subject line. This can be independently searched, making
|
||||
this a powerful place for quickly finding messages.
|
||||
- `message` - the actual text being sent.
|
||||
- `date_sent` - this is auto-set to the time the Msg was created (and thus presumably sent).
|
||||
- `locks` - the Evennia [lock handler](./Locks.md). Use with `locks.add()` etc and check locks with `msg.access()`
|
||||
like for all other lockable entities. This can be used to limit access to the contents
|
||||
of the Msg. The default lock-type to check is `'read'`.
|
||||
- `hide_from` - this is an optional list of [Accounts](./Accounts.md) or [Objects](./Objects.md) that
|
||||
will not see this Msg. This relationship is available mainly for optimization
|
||||
reasons since it allows quick filtering of messages not intended for a given
|
||||
target.
|
||||
|
||||
|
||||
## TempMsg
|
||||
|
||||
[evennia.comms.models.TempMsg](evennia.comms.models.TempMsg) is an object
|
||||
that implements the same API as the regular `Msg`, but which has no database
|
||||
component (and thus cannot be searched). It's meant to plugged into systems
|
||||
expecting a `Msg` but where you just want to process the message without saving
|
||||
it.
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
# Nicks
|
||||
|
||||
|
||||
*Nicks*, short for *Nicknames* is a system allowing an object (usually a [Account](./Accounts.md)) to
|
||||
assign custom replacement names for other game entities.
|
||||
|
||||
Nicks are not to be confused with *Aliases*. Setting an Alias on a game entity actually changes an
|
||||
inherent attribute on that entity, and everyone in the game will be able to use that alias to
|
||||
address the entity thereafter. A *Nick* on the other hand, is used to map a different way *you
|
||||
alone* can refer to that entity. Nicks are also commonly used to replace your input text which means
|
||||
you can create your own aliases to default commands.
|
||||
|
||||
Default Evennia use Nicks in three flavours that determine when Evennia actually tries to do the
|
||||
substitution.
|
||||
|
||||
- inputline - replacement is attempted whenever you write anything on the command line. This is the
|
||||
default.
|
||||
- objects - replacement is only attempted when referring to an object
|
||||
- accounts - replacement is only attempted when referring an account
|
||||
|
||||
Here's how to use it in the default command set (using the `nick` command):
|
||||
|
||||
nick ls = look
|
||||
|
||||
This is a good one for unix/linux users who are accustomed to using the `ls` command in their daily
|
||||
life. It is equivalent to `nick/inputline ls = look`.
|
||||
|
||||
nick/object mycar2 = The red sports car
|
||||
|
||||
With this example, substitutions will only be done specifically for commands expecting an object
|
||||
reference, such as
|
||||
|
||||
look mycar2
|
||||
|
||||
becomes equivalent to "`look The red sports car`".
|
||||
|
||||
nick/accounts tom = Thomas Johnsson
|
||||
|
||||
This is useful for commands searching for accounts explicitly:
|
||||
|
||||
@find *tom
|
||||
|
||||
One can use nicks to speed up input. Below we add ourselves a quicker way to build red buttons. In
|
||||
the future just writing *rb* will be enough to execute that whole long string.
|
||||
|
||||
nick rb = @create button:examples.red_button.RedButton
|
||||
|
||||
Nicks could also be used as the start for building a "recog" system suitable for an RP mud.
|
||||
|
||||
nick/account Arnold = The mysterious hooded man
|
||||
|
||||
The nick replacer also supports unix-style *templating*:
|
||||
|
||||
nick build $1 $2 = @create/drop $1;$2
|
||||
|
||||
This will catch space separated arguments and store them in the the tags `$1` and `$2`, to be
|
||||
inserted in the replacement string. This example allows you to do `build box crate` and have Evennia
|
||||
see `@create/drop box;crate`. You may use any `$` numbers between 1 and 99, but the markers must
|
||||
match between the nick pattern and the replacement.
|
||||
|
||||
> If you want to catch "the rest" of a command argument, make sure to put a `$` tag *with no spaces
|
||||
to the right of it* - it will then receive everything up until the end of the line.
|
||||
|
||||
You can also use [shell-type wildcards](http://www.linfo.org/wildcard.html):
|
||||
|
||||
- \* - matches everything.
|
||||
- ? - matches a single character.
|
||||
- [seq] - matches everything in the sequence, e.g. [xyz] will match both x, y and z
|
||||
- [!seq] - matches everything *not* in the sequence. e.g. [!xyz] will match all but x,y z.
|
||||
|
||||
## Coding with nicks
|
||||
|
||||
Nicks are stored as the `Nick` database model and are referred from the normal Evennia
|
||||
[object](./Objects.md) through the `nicks` property - this is known as the *NickHandler*. The NickHandler
|
||||
offers effective error checking, searches and conversion.
|
||||
|
||||
```python
|
||||
# A command/channel nick:
|
||||
obj.nicks.add("greetjack", "tell Jack = Hello pal!")
|
||||
|
||||
# An object nick:
|
||||
obj.nicks.add("rose", "The red flower", nick_type="object")
|
||||
|
||||
# An account nick:
|
||||
obj.nicks.add("tom", "Tommy Hill", nick_type="account")
|
||||
|
||||
# My own custom nick type (handled by my own game code somehow):
|
||||
obj.nicks.add("hood", "The hooded man", nick_type="my_identsystem")
|
||||
|
||||
# get back the translated nick:
|
||||
full_name = obj.nicks.get("rose", nick_type="object")
|
||||
|
||||
# delete a previous set nick
|
||||
object.nicks.remove("rose", nick_type="object")
|
||||
```
|
||||
|
||||
In a command definition you can reach the nick handler through `self.caller.nicks`. See the `nick`
|
||||
command in `evennia/commands/default/general.py` for more examples.
|
||||
|
||||
As a last note, The Evennia [channel](./Channels.md) alias systems are using nicks with the
|
||||
`nick_type="channel"` in order to allow users to create their own custom aliases to channels.
|
||||
|
||||
## Advanced note
|
||||
|
||||
Internally, nicks are [Attributes](./Attributes.md) saved with the `db_attrype` set to "nick" (normal
|
||||
Attributes has this set to `None`).
|
||||
|
||||
The nick stores the replacement data in the Attribute.db_value field as a tuple with four fields
|
||||
`(regex_nick, template_string, raw_nick, raw_template)`. Here `regex_nick` is the converted regex
|
||||
representation of the `raw_nick` and the `template-string` is a version of the `raw_template`
|
||||
prepared for efficient replacement of any `$`- type markers. The `raw_nick` and `raw_template` are
|
||||
basically the unchanged strings you enter to the `nick` command (with unparsed `$` etc).
|
||||
|
||||
If you need to access the tuple for some reason, here's how:
|
||||
|
||||
```python
|
||||
tuple = obj.nicks.get("nickname", return_tuple=True)
|
||||
# or, alternatively
|
||||
tuple = obj.nicks.get("nickname", return_obj=True).value
|
||||
```
|
||||
|
|
@ -1,238 +0,0 @@
|
|||
# Objects
|
||||
|
||||
|
||||
All in-game objects in Evennia, be it characters, chairs, monsters, rooms or hand grenades are
|
||||
represented by an Evennia *Object*. Objects form the core of Evennia and is probably what you'll
|
||||
spend most time working with. Objects are [Typeclassed](./Typeclasses.md) entities.
|
||||
|
||||
An Evennia Object is, by definition, a Python class that includes
|
||||
[evennia.objects.objects.DefaultObject](evennia.objects.objects.DefaultObject) among its
|
||||
parents. Evennia defines several subclasses of `DefaultObject`:
|
||||
|
||||
- [evennia.objects.objects.DefaultCharacter](evennia.objects.objects.DefaultCharacter) -
|
||||
the normal in-game Character, controlled by a player.
|
||||
- [evennia.objects.objects.DefaultRoom](evennia.objects.objects.DefaultRoom) - a location in the game world.
|
||||
- [evennia.objects.objects.DefaultExit](evennia.objects.objects.DefaultExit) - an entity that (usually) sits
|
||||
in a room and represents a one-way connection to another location.
|
||||
|
||||
You will usually not use the `Default*` parents themselves. In `mygame/typeclasses/` there are
|
||||
convenient subclasses to use. They are empty, and thus identical to
|
||||
the defaults. Tweaking them is one of the main ways to customize you game!
|
||||
|
||||
- `mygame.typeclasses.objects.Object` (inherits from `DefaultObject`)
|
||||
- `mygame.typeclasses.characters.Character` (inherits from `DefaultCharacter`)
|
||||
- `mygame.typeclasses.rooms.Room` (inherits from `DefaultRoom`)
|
||||
- `mygame.typeclasses.exits.Exit` (inherits from `DefaultExit`)
|
||||
|
||||
## How to create your own object types
|
||||
|
||||
You can easily add your own in-game behavior by either modifying one of the typeclasses in
|
||||
your game dir or by inheriting from them.
|
||||
|
||||
You can put your new typeclass directly in the relevant parent
|
||||
module, or you could organize your code in some other way. Here we assume we make a new module
|
||||
`mygame/typeclasses/flowers.py`:
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/flowers.py
|
||||
|
||||
from typeclasses.objects import Object
|
||||
|
||||
class Rose(Object):
|
||||
"""
|
||||
This creates a simple rose object
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"this is called only once, when object is first created"
|
||||
# add a persistent attribute 'desc'
|
||||
# to object (silly example).
|
||||
self.db.desc = "This is a pretty rose with thorns."
|
||||
```
|
||||
|
||||
Now you just need to point to the class *Rose* with the `create` command
|
||||
to make a new rose:
|
||||
|
||||
@create/drop MyRose:flowers.Rose
|
||||
|
||||
What the `create` command actually *does* is to use the [evennia.create_object](evennia.utils.create.create_object)
|
||||
function. You can do the same thing yourself in code:
|
||||
|
||||
```python
|
||||
from evennia import create_object
|
||||
new_rose = create_object("typeclasses.flowers.Rose", key="MyRose")
|
||||
```
|
||||
|
||||
(The `create` command will auto-append the most likely path to your typeclass, if you enter the
|
||||
call manually you have to give the full path to the class. The `create.create_object` function is
|
||||
powerful and should be used for all coded object creating (so this is what you use when defining
|
||||
your own building commands).
|
||||
|
||||
This particular Rose class doesn't really do much, all it does it make sure the attribute
|
||||
`desc`(which is what the `look` command looks for) is pre-set, which is pretty pointless since you
|
||||
will usually want to change this at build time (using the `desc` command or using the
|
||||
[Spawner](./Prototypes.md)).
|
||||
|
||||
## Adding common functionality
|
||||
|
||||
`Object`, `Character`, `Room` and `Exit` also inherit from `mygame.typeclasses.objects.ObjectParent`.
|
||||
This is an empty 'mixin' class. Optionally, you can modify this class if you want to easily add some _common_ functionality to all
|
||||
your Objects, Characters, Rooms and Exits at once. You can still customize each subclass separately (see the Python
|
||||
docs on [multiple inheritance](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance) for details).
|
||||
|
||||
For example:
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/objects.py
|
||||
# ...
|
||||
|
||||
from evennia.objects.objects import DefaultObject
|
||||
|
||||
class ObjectParent:
|
||||
def at_pre_get(self, getter, **kwargs):
|
||||
# make all entities by default un-pickable
|
||||
return False
|
||||
|
||||
class Object(ObjectParent, DefaultObject):
|
||||
# replaces at_pre_get with its own
|
||||
def at_pre_get(self, getter, **kwargs):
|
||||
return True
|
||||
|
||||
# each in their respective modules ...
|
||||
|
||||
class Character(ObjectParent, DefaultCharacter):
|
||||
# will inherit at_pre_get from ObjectParent
|
||||
pass
|
||||
|
||||
class Exit(ObjectParent, DefaultExit):
|
||||
# Overrides and uses the DefaultExit version of at_pre_get instead
|
||||
def at_pre_get(self, getter, **kwargs):
|
||||
return DefaultExit.at_pre_get(self, getter, **kwargs)
|
||||
|
||||
```
|
||||
|
||||
## Properties and functions on Objects
|
||||
|
||||
Beyond the properties assigned to all [typeclassed](./Typeclasses.md) objects (see that page for a list
|
||||
of those), the Object also has the following custom properties:
|
||||
|
||||
- `aliases` - a handler that allows you to add and remove aliases from this object. Use
|
||||
`aliases.add()` to add a new alias and `aliases.remove()` to remove one.
|
||||
- `location` - a reference to the object currently containing this object.
|
||||
- `home` is a backup location. The main motivation is to have a safe place to move the object to if
|
||||
its `location` is destroyed. All objects should usually have a home location for safety.
|
||||
- `destination` - this holds a reference to another object this object links to in some way. Its
|
||||
main use is for [Exits](./Objects.md#exits), it's otherwise usually unset.
|
||||
- `nicks` - as opposed to aliases, a [Nick](./Nicks.md) holds a convenient nickname replacement for a
|
||||
real name, word or sequence, only valid for this object. This mainly makes sense if the Object is
|
||||
used as a game character - it can then store briefer shorts, example so as to quickly reference game
|
||||
commands or other characters. Use nicks.add(alias, realname) to add a new one.
|
||||
- `account` - this holds a reference to a connected [Account](./Accounts.md) controlling this object (if
|
||||
any). Note that this is set also if the controlling account is *not* currently online - to test if
|
||||
an account is online, use the `has_account` property instead.
|
||||
- `sessions` - if `account` field is set *and the account is online*, this is a list of all active
|
||||
sessions (server connections) to contact them through (it may be more than one if multiple
|
||||
connections are allowed in settings).
|
||||
- `has_account` - a shorthand for checking if an *online* account is currently connected to this
|
||||
object.
|
||||
- `contents` - this returns a list referencing all objects 'inside' this object (i,e. which has this
|
||||
object set as their `location`).
|
||||
- `exits` - this returns all objects inside this object that are *Exits*, that is, has the
|
||||
`destination` property set.
|
||||
|
||||
The last two properties are special:
|
||||
|
||||
- `cmdset` - this is a handler that stores all [command sets](./Command-Sets.md) defined on the
|
||||
object (if any).
|
||||
- `scripts` - this is a handler that manages [Scripts](./Scripts.md) attached to the object (if any).
|
||||
|
||||
The Object also has a host of useful utility functions. See the function headers in
|
||||
`src/objects/objects.py` for their arguments and more details.
|
||||
|
||||
- `msg()` - this function is used to send messages from the server to an account connected to this
|
||||
object.
|
||||
- `msg_contents()` - calls `msg` on all objects inside this object.
|
||||
- `search()` - this is a convenient shorthand to search for a specific object, at a given location
|
||||
or globally. It's mainly useful when defining commands (in which case the object executing the
|
||||
command is named `caller` and one can do `caller.search()` to find objects in the room to operate
|
||||
on).
|
||||
- `execute_cmd()` - Lets the object execute the given string as if it was given on the command line.
|
||||
- `move_to` - perform a full move of this object to a new location. This is the main move method
|
||||
and will call all relevant hooks, do all checks etc.
|
||||
- `clear_exits()` - will delete all [Exits](./Objects.md#exits) to *and* from this object.
|
||||
- `clear_contents()` - this will not delete anything, but rather move all contents (except Exits) to
|
||||
their designated `Home` locations.
|
||||
- `delete()` - deletes this object, first calling `clear_exits()` and
|
||||
`clear_contents()`.
|
||||
|
||||
The Object Typeclass defines many more *hook methods* beyond `at_object_creation`. Evennia calls
|
||||
these hooks at various points. When implementing your custom objects, you will inherit from the
|
||||
base parent and overload these hooks with your own custom code. See `evennia.objects.objects` for an
|
||||
updated list of all the available hooks or the [API for DefaultObject here](evennia.objects.objects.DefaultObject).
|
||||
|
||||
## Subclasses of `Object`
|
||||
|
||||
There are three special subclasses of *Object* in default Evennia - *Characters*, *Rooms* and
|
||||
*Exits*. The reason they are separated is because these particular object types are fundamental,
|
||||
something you will always need and in some cases requires some extra attention in order to be
|
||||
recognized by the game engine (there is nothing stopping you from redefining them though). In
|
||||
practice they are all pretty similar to the base Object.
|
||||
|
||||
### Characters
|
||||
|
||||
Characters are objects controlled by [Accounts](./Accounts.md). When a new Account
|
||||
logs in to Evennia for the first time, a new `Character` object is created and
|
||||
the Account object is assigned to the `account` attribute. A `Character` object
|
||||
must have a [Default Commandset](./Command-Sets.md) set on itself at
|
||||
creation, or the account will not be able to issue any commands! If you just
|
||||
inherit your own class from `evennia.DefaultCharacter` and make sure to use
|
||||
`super()` to call the parent methods you should be fine. In
|
||||
`mygame/typeclasses/characters.py` is an empty `Character` class ready for you
|
||||
to modify.
|
||||
|
||||
### Rooms
|
||||
|
||||
*Rooms* are the root containers of all other objects. The only thing really separating a room from
|
||||
any other object is that they have no `location` of their own and that default commands like `@dig`
|
||||
creates objects of this class - so if you want to expand your rooms with more functionality, just
|
||||
inherit from `ev.DefaultRoom`. In `mygame/typeclasses/rooms.py` is an empty `Room` class ready for
|
||||
you to modify.
|
||||
|
||||
### Exits
|
||||
|
||||
*Exits* are objects connecting other objects (usually *Rooms*) together. An object named *North* or
|
||||
*in* might be an exit, as well as *door*, *portal* or *jump out the window*. An exit has two things
|
||||
that separate them from other objects. Firstly, their *destination* property is set and points to a
|
||||
valid object. This fact makes it easy and fast to locate exits in the database. Secondly, exits
|
||||
define a special [Transit Command](./Commands.md) on themselves when they are created. This command is
|
||||
named the same as the exit object and will, when called, handle the practicalities of moving the
|
||||
character to the Exits's *destination* - this allows you to just enter the name of the exit on its
|
||||
own to move around, just as you would expect.
|
||||
|
||||
The exit functionality is all defined on the Exit typeclass, so you could in principle completely
|
||||
change how exits work in your game (it's not recommended though, unless you really know what you are
|
||||
doing). Exits are [locked](./Locks.md) using an access_type called *traverse* and also make use of a few
|
||||
hook methods for giving feedback if the traversal fails. See `evennia.DefaultExit` for more info.
|
||||
In `mygame/typeclasses/exits.py` there is an empty `Exit` class for you to modify.
|
||||
|
||||
The process of traversing an exit is as follows:
|
||||
|
||||
1. The traversing `obj` sends a command that matches the Exit-command name on the Exit object. The
|
||||
[cmdhandler](./Commands.md) detects this and triggers the command defined on the Exit. Traversal always
|
||||
involves the "source" (the current location) and the `destination` (this is stored on the Exit
|
||||
object).
|
||||
1. The Exit command checks the `traverse` lock on the Exit object
|
||||
1. The Exit command triggers `at_traverse(obj, destination)` on the Exit object.
|
||||
1. In `at_traverse`, `object.move_to(destination)` is triggered. This triggers the following hooks,
|
||||
in order:
|
||||
1. `obj.at_pre_move(destination)` - if this returns False, move is aborted.
|
||||
1. `origin.at_pre_leave(obj, destination)`
|
||||
1. `obj.announce_move_from(destination)`
|
||||
1. Move is performed by changing `obj.location` from source location to `destination`.
|
||||
1. `obj.announce_move_to(source)`
|
||||
1. `destination.at_object_receive(obj, source)`
|
||||
1. `obj.at_post_move(source)`
|
||||
1. On the Exit object, `at_post_traverse(obj, source)` is triggered.
|
||||
|
||||
If the move fails for whatever reason, the Exit will look for an Attribute `err_traverse` on itself
|
||||
and display this as an error message. If this is not found, the Exit will instead call
|
||||
`at_failed_traverse(obj)` on itself.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Outputfuncs
|
||||
|
||||
TODO. For now info about outputfuncs are found in [OOB](../Concepts/OOB.md).
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
# Permissions
|
||||
|
||||
A *permission* is simply a text string stored in the handler `permissions` on `Objects`
|
||||
and `Accounts`. Think of it as a specialized sort of [Tag](./Tags.md) - one specifically dedicated
|
||||
to access checking. They are thus often tightly coupled to [Locks](./Locks.md).
|
||||
Permission strings are not case-sensitive, so "Builder" is the same as "builder"
|
||||
etc.
|
||||
|
||||
Permissions are used as a convenient way to structure access levels and
|
||||
hierarchies. It is set by the `perm` command and checked by the
|
||||
`PermissionHandler.check` method as well as by the specially the `perm()` and
|
||||
`pperm()` [lock functions](./Locks.md).
|
||||
|
||||
All new accounts are given a default set of permissions defined by
|
||||
`settings.PERMISSION_ACCOUNT_DEFAULT`.
|
||||
|
||||
## Managing Permissions
|
||||
|
||||
In-game, you use the `perm` command to add and remove permissions
|
||||
j
|
||||
perm/account Tommy = Builders
|
||||
perm/account/del Tommy = Builders
|
||||
|
||||
Note the use of the `/account` switch. It means you assign the permission to the
|
||||
[Accounts](./Accounts.md) Tommy instead of any [Character](./Objects.md) that also
|
||||
happens to be named "Tommy".
|
||||
|
||||
There can be reasons for putting permissions on Objects (especially NPCS), but
|
||||
for granting powers to players, you should usually put the permission on the
|
||||
`Account` - this guarantees that they are kept, *regardless*
|
||||
of which Character they are currently puppeting. This is especially important to
|
||||
remember when assigning permissions from the *hierarchy tree* (see below), as an
|
||||
Account's permissions will overrule that of its character. So to be sure to
|
||||
avoid confusion you should generally put hierarchy permissions on the Account,
|
||||
not on their Characters (but see also [quelling](#quelling)).
|
||||
|
||||
In code, you add/remove Permissions via the `PermissionHandler`, which sits on all
|
||||
typeclassed entities as the property `.permissions`:
|
||||
|
||||
```python
|
||||
account.permissions.add("Builders")
|
||||
account.permissions.add("cool_guy")
|
||||
obj.permissions.add("Blacksmith")
|
||||
obj.permissions.remove("Blacksmith")
|
||||
```
|
||||
|
||||
|
||||
## The permission hierarchy
|
||||
|
||||
Selected permission strings can be organized in a *permission hierarchy* by editing the tuple
|
||||
`settings.PERMISSION_HIERARCHY`. Evennia's default permission hierarchy is as follows
|
||||
(in increasing order of power):
|
||||
|
||||
Player # can chat and send tells (default level) (lowest)
|
||||
Helper # can edit help files
|
||||
Builder # can edit the world
|
||||
Admin # can administrate accounts
|
||||
Developer # like superuser but affected by locks (highest)
|
||||
|
||||
(Besides being case-insensitive, hierarchical permissions also understand the
|
||||
plural form, so you could use `Developers` and `Developer` interchangeably).
|
||||
|
||||
> There is also a `Guest` level below `Player` that is only active if `settings.GUEST_ENABLED` is
|
||||
set. The Guest is is never part of `settings.PERMISSION_HIERARCHY`.
|
||||
|
||||
When checking a hierarchical permission (using one of the methods to follow),
|
||||
you will pass checks for your level and all *below* you. That is, even if the
|
||||
check explicitly checks for "Builder" level access, you will actually pass if you have
|
||||
one of "Builder", "Admin" or "Developer". By contrast, if you check for a
|
||||
non-hierarchical permission, like "Blacksmith" you *must* have exactly
|
||||
that permission to pass.
|
||||
|
||||
## Checking permissions
|
||||
|
||||
It's important to note that you check for the permission of a *puppeted*
|
||||
[Object](./Objects.md) (like a Character), the check will always first use the
|
||||
permissions of any `Account` connected to that Object before checking for
|
||||
permissions on the Object. In the case of hierarchical permissions (Admins,
|
||||
Builders etc), the Account permission will always be used (this stops an Account
|
||||
from escalating their permission by puppeting a high-level Character). If the
|
||||
permission looked for is not in the hierarchy, an exact match is required, first
|
||||
on the Account and if not found there (or if no Account is connected), then on
|
||||
the Object itself.
|
||||
|
||||
### Checking with obj.permissions.check()
|
||||
|
||||
The simplest way to check if an entity has a permission is to check its
|
||||
_PermissionHandler_, stored as `.permissions` on all typeclassed entities.
|
||||
|
||||
if obj.permissions.check("Builder"):
|
||||
# allow builder to do stuff
|
||||
|
||||
if obj.permissions.check("Blacksmith", "Warrior"):
|
||||
# do stuff for blacksmiths OR warriors
|
||||
|
||||
if obj.permissions.check("Blacksmith", "Warrior", require_all=True):
|
||||
# only for those that are both blacksmiths AND warriors
|
||||
|
||||
Using the `.check` method is the way to go, it will take hierarchical
|
||||
permissions into account, check accounts/sessions etc.
|
||||
|
||||
```{warning}
|
||||
|
||||
Don't confuse `.permissions.check()` with `.permissions.has()`. The .has()
|
||||
method checks if a string is defined specifically on that PermissionHandler.
|
||||
It will not consider permission-hierarchy, puppeting etc. `.has` can be useful
|
||||
if you are manipulating permissions, but use `.check` for access checking.
|
||||
|
||||
```
|
||||
|
||||
### Lock funcs
|
||||
|
||||
While the `PermissionHandler` offers a simple way to check perms, [Lock
|
||||
strings](./Locks.md) offers a mini-language for describing how something is accessed.
|
||||
The `perm()` _lock function_ is the main tool for using Permissions in locks.
|
||||
|
||||
Let's say we have a `red_key` object. We also have red chests that we want to
|
||||
unlock with this key.
|
||||
|
||||
perm red_key = unlocks_red_chests
|
||||
|
||||
This gives the `red_key` object the permission "unlocks_red_chests". Next we
|
||||
lock our red chests:
|
||||
|
||||
lock red chest = unlock:perm(unlocks_red_chests)
|
||||
|
||||
When trying to unlock the red chest with this key, the chest Typeclass could
|
||||
then take the key and do an access check:
|
||||
|
||||
```python
|
||||
# in some typeclass file where chest is defined
|
||||
|
||||
class TreasureChest(Object):
|
||||
|
||||
# ...
|
||||
|
||||
def open_chest(self, who, tried_key):
|
||||
|
||||
if not chest.access(who, tried_key, "unlock"):
|
||||
who.msg("The key does not fit!")
|
||||
return
|
||||
else:
|
||||
who.msg("The key fits! The chest opens.")
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
There are several variations to the default `perm` lockfunc:
|
||||
|
||||
- `perm_above` - requires a hierarchical permission *higher* than the one
|
||||
provided. Example: `"edit: perm_above(Player)"`
|
||||
- `pperm` - looks *only* for permissions on `Accounts`, never at any puppeted
|
||||
objects (regardless of hierarchical perm or not).
|
||||
- `pperm_above` - like `perm_above`, but for Accounts only.
|
||||
|
||||
### Some examples
|
||||
|
||||
Adding permissions and checking with locks
|
||||
|
||||
```python
|
||||
account.permissions.add("Builder")
|
||||
account.permissions.add("cool_guy")
|
||||
account.locks.add("enter:perm_above(Player) and perm(cool_guy)")
|
||||
account.access(obj1, "enter") # this returns True!
|
||||
```
|
||||
|
||||
An example of a puppet with a connected account:
|
||||
|
||||
```python
|
||||
account.permissions.add("Player")
|
||||
puppet.permissions.add("Builders")
|
||||
puppet.permissions.add("cool_guy")
|
||||
obj2.locks.add("enter:perm_above(Accounts) and perm(cool_guy)")
|
||||
|
||||
obj2.access(puppet, "enter") # this returns False, since puppet permission
|
||||
# is lower than Account's perm, and perm takes
|
||||
# precedence.
|
||||
```
|
||||
|
||||
## Superusers
|
||||
|
||||
There is normally only one *superuser* account and that is the one first created
|
||||
when starting Evennia (User #1). This is sometimes known as the "Owner" or "God"
|
||||
user. A superuser has more than full access - it completely *bypasses* all
|
||||
locks and will always pass the `PermissionHandler.check()` check. This allows
|
||||
for the superuser to always have access to everything in an emergency. But it
|
||||
could also hide any eventual errors you might have made in your lock definitions. So
|
||||
when trying out game systems you should either use quelling (see below) or make
|
||||
a second Developer-level character that does not bypass such checks.
|
||||
|
||||
## Quelling
|
||||
|
||||
The `quell` command can be used to enforce the `perm()` lockfunc to ignore
|
||||
permissions on the Account and instead use the permissions on the Character
|
||||
only. This can be used e.g. by staff to test out things with a lower permission
|
||||
level. Return to the normal operation with `unquell`. Note that quelling will
|
||||
use the smallest of any hierarchical permission on the Account or Character, so
|
||||
one cannot escalate one's Account permission by quelling to a high-permission
|
||||
Character. Also the superuser can quell their powers this way, making them
|
||||
affectable by locks.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# Portal And Server
|
||||
|
||||
|
||||
Evennia consists of two processes, known as *Portal* and *Server*. They can be controlled from
|
||||
inside the game or from the command line as described [here](../Setup/Start-Stop-Reload.md).
|
||||
|
||||
If you are new to the concept, the main purpose of separating the two is to have accounts connect to
|
||||
the Portal but keep the MUD running on the Server. This way one can restart/reload the game (the
|
||||
Server part) without Accounts getting disconnected.
|
||||
|
||||

|
||||
|
||||
The Server and Portal are glued together via an AMP (Asynchronous Messaging Protocol) connection.
|
||||
This allows the two programs to communicate seamlessly.
|
||||
|
|
@ -1,334 +0,0 @@
|
|||
# Spawner and Prototypes
|
||||
|
||||
|
||||
The *spawner* is a system for defining and creating individual objects from a base template called a
|
||||
*prototype*. It is only designed for use with in-game [Objects](./Objects.md), not any other type of
|
||||
entity.
|
||||
|
||||
The normal way to create a custom object in Evennia is to make a [Typeclass](./Typeclasses.md). If you
|
||||
haven't read up on Typeclasses yet, think of them as normal Python classes that save to the database
|
||||
behind the scenes. Say you wanted to create a "Goblin" enemy. A common way to do this would be to
|
||||
first create a `Mobile` typeclass that holds everything common to mobiles in the game, like generic
|
||||
AI, combat code and various movement methods. A `Goblin` subclass is then made to inherit from
|
||||
`Mobile`. The `Goblin` class adds stuff unique to goblins, like group-based AI (because goblins are
|
||||
smarter in a group), the ability to panic, dig for gold etc.
|
||||
|
||||
But now it's time to actually start to create some goblins and put them in the world. What if we
|
||||
wanted those goblins to not all look the same? Maybe we want grey-skinned and green-skinned goblins
|
||||
or some goblins that can cast spells or which wield different weapons? We *could* make subclasses of
|
||||
`Goblin`, like `GreySkinnedGoblin` and `GoblinWieldingClub`. But that seems a bit excessive (and a
|
||||
lot of Python code for every little thing). Using classes can also become impractical when wanting
|
||||
to combine them - what if we want a grey-skinned goblin shaman wielding a spear - setting up a web
|
||||
of classes inheriting each other with multiple inheritance can be tricky.
|
||||
|
||||
This is what the *prototype* is for. It is a Python dictionary that describes these per-instance
|
||||
changes to an object. The prototype also has the advantage of allowing an in-game builder to
|
||||
customize an object without access to the Python backend. Evennia also allows for saving and
|
||||
searching prototypes so other builders can find and use (and tweak) them later. Having a library of
|
||||
interesting prototypes is a good reasource for builders. The OLC system allows for creating, saving,
|
||||
loading and manipulating prototypes using a menu system.
|
||||
|
||||
The *spawner* takes a prototype and uses it to create (spawn) new, custom objects.
|
||||
|
||||
## Using the OLC
|
||||
|
||||
Enter the `olc` command or `@spawn/olc` to enter the prototype wizard. This is a menu system for
|
||||
creating, loading, saving and manipulating prototypes. It's intended to be used by in-game builders
|
||||
and will give a better understanding of prototypes in general. Use `help` on each node of the menu
|
||||
for more information. Below are further details about how prototypes work and how they are used.
|
||||
|
||||
## The prototype
|
||||
|
||||
The prototype dictionary can either be created for you by the OLC (see above), be written manually
|
||||
in a Python module (and then referenced by the `@spawn` command/OLC), or created on-the-fly and
|
||||
manually loaded into the spawner function or `@spawn` command.
|
||||
|
||||
The dictionary defines all possible database-properties of an Object. It has a fixed set of allowed
|
||||
keys. When preparing to store the prototype in the database (or when using the OLC), some
|
||||
of these keys are mandatory. When just passing a one-time prototype-dict to the spawner the system
|
||||
is
|
||||
more lenient and will use defaults for keys not explicitly provided.
|
||||
|
||||
In dictionary form, a prototype can look something like this:
|
||||
|
||||
```python
|
||||
{
|
||||
"prototype_key": "house"
|
||||
"key": "Large house"
|
||||
"typeclass": "typeclasses.rooms.house.House"
|
||||
}
|
||||
```
|
||||
If you wanted to load it into the spawner in-game you could just put all on one line:
|
||||
|
||||
@spawn {"prototype_key="house", "key": "Large house", ...}
|
||||
|
||||
> Note that the prototype dict as given on the command line must be a valid Python structure -
|
||||
so you need to put quotes around strings etc. For security reasons, a dict inserted from-in game
|
||||
cannot have any
|
||||
other advanced Python functionality, such as executable code, `lambda` etc. If builders are supposed
|
||||
to be able to use such features, you need to offer them through [$protfuncs](Spawner-and-
|
||||
Prototypes#protfuncs), embedded runnable functions that you have full control to check and vet
|
||||
before running.
|
||||
|
||||
### Prototype keys
|
||||
|
||||
All keys starting with `prototype_` are for book keeping.
|
||||
|
||||
- `prototype_key` - the 'name' of the prototype, used for referencing the prototype
|
||||
when spawning and inheritance. If defining a prototype in a module and this
|
||||
not set, it will be auto-set to the name of the prototype's variable in the module.
|
||||
- `prototype_parent` - If given, this should be the `prototype_key` of another prototype stored in
|
||||
the system or available in a module. This makes this prototype *inherit* the keys from the
|
||||
parent and only override what is needed. Give a tuple `(parent1, parent2, ...)` for multiple
|
||||
left-right inheritance. If this is not given, a `typeclass` should usually be defined (below).
|
||||
- `prototype_desc` - this is optional and used when listing the prototype in in-game listings.
|
||||
- `protototype_tags` - this is optional and allows for tagging the prototype in order to find it
|
||||
easier later.
|
||||
- `prototype_locks` - two lock types are supported: `edit` and `spawn`. The first lock restricts
|
||||
the copying and editing of the prototype when loaded through the OLC. The second determines who
|
||||
may use the prototype to create new objects.
|
||||
|
||||
|
||||
The remaining keys determine actual aspects of the objects to spawn from this prototype:
|
||||
|
||||
- `key` - the main object identifier. Defaults to "Spawned Object *X*", where *X* is a random
|
||||
integer.
|
||||
- `typeclass` - A full python-path (from your gamedir) to the typeclass you want to use. If not
|
||||
set, the `prototype_parent` should be
|
||||
defined, with `typeclass` defined somewhere in the parent chain. When creating a one-time
|
||||
prototype
|
||||
dict just for spawning, one could omit this - `settings.BASE_OBJECT_TYPECLASS` will be used
|
||||
instead.
|
||||
- `location` - this should be a `#dbref`.
|
||||
- `home` - a valid `#dbref`. Defaults to `location` or `settings.DEFAULT_HOME` if location does not
|
||||
exist.
|
||||
- `destination` - a valid `#dbref`. Only used by exits.
|
||||
- `permissions` - list of permission strings, like `["Accounts", "may_use_red_door"]`
|
||||
- `locks` - a [lock-string](./Locks.md) like `"edit:all();control:perm(Builder)"`
|
||||
- `aliases` - list of strings for use as aliases
|
||||
- `tags` - list [Tags](./Tags.md). These are given as tuples `(tag, category, data)`.
|
||||
- `attrs` - list of [Attributes](./Attributes.md). These are given as tuples `(attrname, value,
|
||||
category, lockstring)`
|
||||
- Any other keywords are interpreted as non-category [Attributes](./Attributes.md) and their values.
|
||||
This is convenient for simple Attributes - use `attrs` for full control of Attributes.
|
||||
|
||||
#### More on prototype inheritance
|
||||
|
||||
- A prototype can inherit by defining a `prototype_parent` pointing to the name
|
||||
(`prototype_key` of another prototype). If a list of `prototype_keys`, this
|
||||
will be stepped through from left to right, giving priority to the first in
|
||||
the list over those appearing later. That is, if your inheritance is
|
||||
`prototype_parent = ('A', 'B,' 'C')`, and all parents contain colliding keys,
|
||||
then the one from `A` will apply.
|
||||
- The prototype keys that start with `prototype_*` are all unique to each
|
||||
prototype. They are _never_ inherited from parent to child.
|
||||
- The prototype fields `'attr': [(key, value, category, lockstring),...]`
|
||||
and `'tags': [(key, category, data), ...]` are inherited in a _complementary_
|
||||
fashion. That means that only colliding key+category matches will be replaced, not the entire list.
|
||||
Remember that the category `None` is also considered a valid category!
|
||||
- Adding an Attribute as a simple `key:value` will under the hood be translated
|
||||
into an Attribute tuple `(key, value, None, '')` and may replace an Attribute
|
||||
in the parent if it the same key and a `None` category.
|
||||
- All other keys (`permissions`, `destination`, `aliases` etc) are completely
|
||||
_replaced_ by the child's value if given. For the parent's value to be
|
||||
retained, the child must not define these keys at all.
|
||||
|
||||
### Prototype values
|
||||
|
||||
The prototype supports values of several different types.
|
||||
|
||||
It can be a hard-coded value:
|
||||
|
||||
```python
|
||||
{"key": "An ugly goblin", ...}
|
||||
|
||||
```
|
||||
|
||||
It can also be a *callable*. This callable is called without arguments whenever the prototype is
|
||||
used to
|
||||
spawn a new object:
|
||||
|
||||
```python
|
||||
{"key": _get_a_random_goblin_name, ...}
|
||||
|
||||
```
|
||||
|
||||
By use of Python `lambda` one can wrap the callable so as to make immediate settings in the
|
||||
prototype:
|
||||
|
||||
```python
|
||||
{"key": lambda: random.choice(("Urfgar", "Rick the smelly", "Blargh the foul", ...)), ...}
|
||||
|
||||
```
|
||||
|
||||
#### Protfuncs
|
||||
|
||||
Finally, the value can be a *prototype function* (*Protfunc*). These look like simple function calls
|
||||
that you embed in strings and that has a `$` in front, like
|
||||
|
||||
```python
|
||||
{"key": "$choice(Urfgar, Rick the smelly, Blargh the foul)",
|
||||
"attrs": {"desc": "This is a large $red(and very red) demon. "
|
||||
"He has $randint(2,5) skulls in a chain around his neck."}
|
||||
```
|
||||
At execution time, the place of the protfunc will be replaced with the result of that protfunc being
|
||||
called (this is always a string). A protfunc is a [FuncParser function](./FuncParser.md) run
|
||||
every time the prototype is used to spawn a new object.
|
||||
|
||||
Here is how a protfunc is defined (same as an inlinefunc).
|
||||
|
||||
```python
|
||||
# this is a silly example, you can just color the text red with |r directly!
|
||||
def red(*args, **kwargs):
|
||||
"""
|
||||
Usage: $red(<text>)
|
||||
Returns the same text you entered, but red.
|
||||
"""
|
||||
if not args or len(args) > 1:
|
||||
raise ValueError("Must have one argument, the text to color red!")
|
||||
return f"|r{args[0]}|n"
|
||||
```
|
||||
|
||||
> Note that we must make sure to validate input and raise `ValueError` if that fails. Also, it is
|
||||
*not* possible to use keywords in the call to the protfunc (so something like `$echo(text,
|
||||
align=left)` is invalid). The `kwargs` requred is for internal evennia use and not used at all for
|
||||
protfuncs (only by inlinefuncs).
|
||||
|
||||
To make this protfunc available to builders in-game, add it to a new module and add the path to that
|
||||
module to `settings.PROT_FUNC_MODULES`:
|
||||
|
||||
```python
|
||||
# in mygame/server/conf/settings.py
|
||||
|
||||
PROT_FUNC_MODULES += ["world.myprotfuncs"]
|
||||
|
||||
```
|
||||
All *global callables* in your added module will be considered a new protfunc. To avoid this (e.g.
|
||||
to have helper functions that are not protfuncs on their own), name your function something starting
|
||||
with `_`.
|
||||
|
||||
The default protfuncs available out of the box are defined in `evennia/prototypes/profuncs.py`. To
|
||||
override the ones available, just add the same-named function in your own protfunc module.
|
||||
|
||||
| Protfunc | Description |
|
||||
| --- | --- |
|
||||
| `$random()` | Returns random value in range [0, 1) |
|
||||
| `$randint(start, end)` | Returns random value in range [start, end] |
|
||||
| `$left_justify(<text>)` | Left-justify text |
|
||||
| `$right_justify(<text>)` | Right-justify text to screen width |
|
||||
| `$center_justify(<text>)` | Center-justify text to screen width |
|
||||
| `$full_justify(<text>)` | Spread text across screen width by adding spaces |
|
||||
| `$protkey(<name>)` | Returns value of another key in this prototype (self-reference) |
|
||||
| `$add(<value1>, <value2>)` | Returns value1 + value2. Can also be lists, dicts etc |
|
||||
| `$sub(<value1>, <value2>)` | Returns value1 - value2 |
|
||||
| `$mult(<value1>, <value2>)` | Returns value1 * value2 |
|
||||
| `$div(<value1>, <value2>)` | Returns value2 / value1 |
|
||||
| `$toint(<value>)` | Returns value converted to integer (or value if not possible) |
|
||||
| `$eval(<code>)` | Returns result of [literal-eval](https://docs.python.org/2/library/ast.html#ast.literal_eval) of code string. Only simple python expressions. |
|
||||
| `$obj(<query>)` | Returns object #dbref searched globally by key, tag or #dbref. Error if more than one found. |
|
||||
| `$objlist(<query>)` | Like `$obj`, except always returns a list of zero, one or more results. |
|
||||
| `$dbref(dbref)` | Returns argument if it is formed as a #dbref (e.g. #1234), otherwise error. |
|
||||
|
||||
For developers with access to Python, using protfuncs in prototypes is generally not useful. Passing
|
||||
real Python functions is a lot more powerful and flexible. Their main use is to allow in-game
|
||||
builders to
|
||||
do limited coding/scripting for their prototypes without giving them direct access to raw Python.
|
||||
|
||||
## Storing prototypes
|
||||
|
||||
A prototype can be defined and stored in two ways, either in the database or as a dict in a module.
|
||||
|
||||
### Database prototypes
|
||||
|
||||
Stored as [Scripts](./Scripts.md) in the database. These are sometimes referred to as *database-
|
||||
prototypes* This is the only way for in-game builders to modify and add prototypes. They have the
|
||||
advantage of being easily modifiable and sharable between builders but you need to work with them
|
||||
using in-game tools.
|
||||
|
||||
### Module-based prototypes
|
||||
|
||||
These prototypes are defined as dictionaries assigned to global variables in one of the modules
|
||||
defined in `settings.PROTOTYPE_MODULES`. They can only be modified from outside the game so they are
|
||||
are necessarily "read-only" from in-game and cannot be modified (but copies of them could be made
|
||||
into database-prototypes). These were the only prototypes available before Evennia 0.8. Module based
|
||||
prototypes can be useful in order for developers to provide read-only "starting" or "base"
|
||||
prototypes to build from or if they just prefer to work offline in an external code editor.
|
||||
|
||||
By default `mygame/world/prototypes.py` is set up for you to add your own prototypes. *All global
|
||||
dicts* in this module will be considered by Evennia to be a prototype. You could also tell Evennia
|
||||
to look for prototypes in more modules if you want:
|
||||
|
||||
```python
|
||||
# in mygame/server/conf.py
|
||||
|
||||
PROTOTYPE_MODULES = += ["world.myownprototypes", "combat.prototypes"]
|
||||
|
||||
```
|
||||
|
||||
Here is an example of a prototype defined in a module:
|
||||
|
||||
```python
|
||||
# in a module Evennia looks at for prototypes,
|
||||
# (like mygame/world/prototypes.py)
|
||||
|
||||
ORC_SHAMAN = {"key":"Orc shaman",
|
||||
"typeclass": "typeclasses.monsters.Orc",
|
||||
"weapon": "wooden staff",
|
||||
"health": 20}
|
||||
```
|
||||
|
||||
> Note that in the example above, `"ORC_SHAMAN"` will become the `prototype_key` of this prototype.
|
||||
> It's the only case when `prototype_key` can be skipped in a prototype. However, if `prototype_key`
|
||||
> was given explicitly, that would take precedence. This is a legacy behavior and it's recommended
|
||||
> that you always add `prototype_key` to be consistent.
|
||||
|
||||
|
||||
## Using @spawn
|
||||
|
||||
The spawner can be used from inside the game through the Builder-only `@spawn` command. Assuming the
|
||||
"goblin" typeclass is available to the system (either as a database-prototype or read from module),
|
||||
you can spawn a new goblin with
|
||||
|
||||
@spawn goblin
|
||||
|
||||
You can also specify the prototype directly as a valid Python dictionary:
|
||||
|
||||
@spawn {"prototype_key": "shaman", \
|
||||
"key":"Orc shaman", \
|
||||
"prototype_parent": "goblin", \
|
||||
"weapon": "wooden staff", \
|
||||
"health": 20}
|
||||
|
||||
> Note: The `@spawn` command is more lenient about the prototype dictionary than shown here. So you
|
||||
can for example skip the `prototype_key` if you are just testing a throw-away prototype. A random
|
||||
hash will be used to please the validation. You could also skip `prototype_parent/typeclass` - then
|
||||
the typeclass given by `settings.BASE_OBJECT_TYPECLASS` will be used.
|
||||
|
||||
## Using evennia.prototypes.spawner()
|
||||
|
||||
In code you access the spawner mechanism directly via the call
|
||||
|
||||
```python
|
||||
new_objects = evennia.prototypes.spawner.spawn(*prototypes)
|
||||
```
|
||||
|
||||
All arguments are prototype dictionaries. The function will return a
|
||||
matching list of created objects. Example:
|
||||
|
||||
```python
|
||||
obj1, obj2 = evennia.prototypes.spawner.spawn({"key": "Obj1", "desc": "A test"},
|
||||
{"key": "Obj2", "desc": "Another test"})
|
||||
```
|
||||
> Hint: Same as when using `@spawn`, when spawning from a one-time prototype dict like this, you can
|
||||
skip otherwise required keys, like `prototype_key` or `typeclass`/`prototype_parent`. Defaults will
|
||||
be used.
|
||||
|
||||
Note that no `location` will be set automatically when using `evennia.prototypes.spawner.spawn()`,
|
||||
you
|
||||
have to specify `location` explicitly in the prototype dict.
|
||||
|
||||
If the prototypes you supply are using `prototype_parent` keywords, the spawner will read prototypes
|
||||
from modules
|
||||
in `settings.PROTOTYPE_MODULES` as well as those saved to the database to determine the body of
|
||||
available parents. The `spawn` command takes many optional keywords, you can find its definition [in
|
||||
the api docs](github:evennia.prototypes.spawner#spawn).
|
||||
|
|
@ -1,437 +0,0 @@
|
|||
# Scripts
|
||||
|
||||
[Script API reference](evennia.scripts.scripts)
|
||||
|
||||
*Scripts* are the out-of-character siblings to the in-character
|
||||
[Objects](./Objects.md). Scripts are so flexible that the name "Script" is a bit limiting
|
||||
in itself - but we had to pick _something_ to name them. Other possible names
|
||||
(depending on what you'd use them for) would be `OOBObjects`, `StorageContainers` or `TimerObjects`.
|
||||
|
||||
If you ever consider creating an [Object](./Objects.md) with a `None`-location just to store some game data,
|
||||
you should really be using a Script instead.
|
||||
|
||||
- Scripts are full [Typeclassed](./Typeclasses.md) entities - they have [Attributes](./Attributes.md) and
|
||||
can be modified in the same way. But they have _no in-game existence_, so no
|
||||
location or command-execution like [Objects](./Objects.md) and no connection to a particular
|
||||
player/session like [Accounts](./Accounts.md). This means they are perfectly suitable for acting
|
||||
as database-storage backends for game _systems_: Storing the current state of the economy,
|
||||
who is involved in the current fight, tracking an ongoing barter and so on. They are great as
|
||||
persistent system handlers.
|
||||
- Scripts have an optional _timer component_. This means that you can set up the script
|
||||
to tick the `at_repeat` hook on the Script at a certain interval. The timer can be controlled
|
||||
independently of the rest of the script as needed. This component is optional
|
||||
and complementary to other timing functions in Evennia, like
|
||||
[evennia.utils.delay](evennia.utils.utils.delay) and
|
||||
[evennia.utils.repeat](evennia.utils.utils.repeat).
|
||||
- Scripts can _attach_ to Objects and Accounts via e.g. `obj.scripts.add/remove`. In the
|
||||
script you can then access the object/account as `self.obj` or `self.account`. This can be used to
|
||||
dynamically extend other typeclasses but also to use the timer component to affect the parent object
|
||||
in various ways. For historical reasons, a Script _not_ attached to an object is referred to as a
|
||||
_Global_ Script.
|
||||
|
||||
```{versionchanged} 1.0
|
||||
In previus Evennia versions, stopping the Script's timer also meant deleting the Script object.
|
||||
Starting with this version, the timer can be start/stopped separately and `.delete()` must be called
|
||||
on the Script explicitly to delete it.
|
||||
|
||||
```
|
||||
|
||||
## In-game command examples
|
||||
|
||||
There are two main commands controlling scripts in the default cmdset:
|
||||
|
||||
The `addscript` command is used for attaching scripts to existing objects:
|
||||
|
||||
> addscript obj = bodyfunctions.BodyFunctions
|
||||
|
||||
The `scripts` command is used to view all scripts and perform operations on them:
|
||||
|
||||
> scripts
|
||||
> scripts/stop bodyfunctions.BodyFunctions
|
||||
> scripts/start #244
|
||||
> scripts/pause #11
|
||||
> scripts/delete #566
|
||||
|
||||
```{versionchanged} 1.0
|
||||
The `addscript` command used to be only `script` which was easy to confuse with `scripts`.
|
||||
```
|
||||
|
||||
## Code examples
|
||||
|
||||
Here are some examples of working with Scripts in-code (more details to follow in later
|
||||
sections).
|
||||
|
||||
Create a new script:
|
||||
```python
|
||||
new_script = evennia.create_script(key="myscript", typeclass=...)
|
||||
```
|
||||
|
||||
Create script with timer component:
|
||||
|
||||
```python
|
||||
# (note that this will call `timed_script.at_repeat` which is empty by default)
|
||||
timed_script = evennia.create_script(key="Timed script",
|
||||
interval=34, # seconds <=0 means off
|
||||
start_delay=True, # wait interval before first call
|
||||
autostart=True) # start timer (else needing .start() )
|
||||
|
||||
# manipulate the script's timer
|
||||
timed_script.stop()
|
||||
timed_script.start()
|
||||
timed_script.pause()
|
||||
timed_script.unpause()
|
||||
```
|
||||
|
||||
Attach script to another object:
|
||||
|
||||
```python
|
||||
myobj.scripts.add(new_script)
|
||||
myobj.scripts.add(evennia.DefaultScript)
|
||||
all_scripts_on_obj = myobj.scripts.all()
|
||||
```
|
||||
|
||||
Search/find scripts in various ways:
|
||||
|
||||
```python
|
||||
# regular search (this is always a list, also if there is only one match)
|
||||
list_of_myscripts = evennia.search_script("myscript")
|
||||
|
||||
# search through Evennia's GLOBAL_SCRIPTS container (based on
|
||||
# script's key only)
|
||||
from evennia import GLOBAL_SCRIPTS
|
||||
|
||||
myscript = GLOBAL_SCRIPTS.myscript
|
||||
GLOBAL_SCRIPTS.get("Timed script").db.foo = "bar"
|
||||
```
|
||||
|
||||
Delete the Script (this will also stop its timer):
|
||||
|
||||
```python
|
||||
new_script.delete()
|
||||
timed_script.delete()
|
||||
```
|
||||
|
||||
## Defining new Scripts
|
||||
|
||||
A Script is defined as a class and is created in the same way as other
|
||||
[typeclassed](./Typeclasses.md) entities. The parent class is `evennia.DefaultScript`.
|
||||
|
||||
|
||||
### Simple storage script
|
||||
|
||||
In `mygame/typeclasses/scripts.py` is an empty `Script` class already set up. You
|
||||
can use this as a base for your own scripts.
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/scripts.py
|
||||
|
||||
from evennia import DefaultScript
|
||||
|
||||
class Script(DefaultScript):
|
||||
# stuff common for all your scripts goes here
|
||||
|
||||
class MyScript(Script):
|
||||
def at_script_creation(self):
|
||||
"""Called once, when script is first created"""
|
||||
self.key = "myscript"
|
||||
self.db.foo = "bar"
|
||||
|
||||
```
|
||||
|
||||
Once created, this simple Script could act as a global storage:
|
||||
|
||||
```python
|
||||
evennia.create_script('typeclasses.scripts.MyScript')
|
||||
|
||||
# from somewhere else
|
||||
|
||||
myscript = evennia.search_script("myscript").first()
|
||||
bar = myscript.db.foo
|
||||
myscript.db.something_else = 1000
|
||||
|
||||
```
|
||||
|
||||
Note that if you give keyword arguments to `create_script` you can override the values
|
||||
you set in your `at_script_creation`:
|
||||
|
||||
```python
|
||||
|
||||
evennia.create_script('typeclasses.scripts.MyScript', key="another name",
|
||||
attributes=[("foo", "bar-alternative")])
|
||||
|
||||
|
||||
```
|
||||
|
||||
See the [create_script](evennia.utils.create.create_script) and
|
||||
[search_script](evennia.utils.search.search_script) API documentation for more options
|
||||
on creating and finding Scripts.
|
||||
|
||||
|
||||
## Timed Scripts
|
||||
|
||||
There are several properties one can set on the Script to control its timer component.
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/scripts.py
|
||||
|
||||
class TimerScript(Script):
|
||||
|
||||
def at_script_creation(self):
|
||||
self.key = "myscript"
|
||||
self.desc = "An example script"
|
||||
self.interval = 60 # 1 min repeat
|
||||
|
||||
def at_repeat(self):
|
||||
# do stuff every minute
|
||||
|
||||
```
|
||||
|
||||
This example will call `at_repeat` every minute. The `create_script` function has an `autostart=True` keyword
|
||||
set by default - this means the script's timer component will be started automatically. Otherwise
|
||||
`.start()` must be called separately.
|
||||
|
||||
Supported properties are:
|
||||
|
||||
- `key` (str): The name of the script. This makes it easier to search for it later. If it's a script
|
||||
attached to another object one can also get all scripts off that object and get the script that way.
|
||||
- `desc` (str): Note - not `.db.desc`! This is a database field on the Script shown in script listings
|
||||
to help identifying what does what.
|
||||
- `interval` (int): The amount of time (in seconds) between every 'tick' of the timer. Note that
|
||||
it's generally bad practice to use sub-second timers for anything in a text-game - the player will
|
||||
not be able to appreciate the precision (and if you print it, it will just spam the screen). For
|
||||
calculations you can pretty much always do them on-demand, or at a much slower interval without the
|
||||
player being the wiser.
|
||||
- `start_delay` (bool): If timer should start right away or wait `interval` seconds first.
|
||||
- `repeats` (int): If >0, the timer will only run this many times before stopping. Otherwise the
|
||||
number of repeats are infinite. If set to 1, the Script mimics a `delay` action.
|
||||
- `persistent` (bool): This defaults to `True` and means the timer will survive a server reload/reboot.
|
||||
If not, a reload will have the timer come back in a stopped state. Setting this to `False` will _not_
|
||||
delete the Script object itself (use `.delete()` for this).
|
||||
|
||||
The timer component is controlled with methods on the Script class:
|
||||
|
||||
- `.at_repeat()` - this method is called every `interval` seconds while the timer is
|
||||
active.
|
||||
- `.is_valid()` - this method is called by the timer just before `at_repeat()`. If it returns `False`
|
||||
the timer is immediately stopped.
|
||||
- `.start()` - start/update the timer. If keyword arguments are given, they can be used to
|
||||
change `interval`, `start_delay` etc on the fly. This calls the `.at_start()` hook.
|
||||
This is also called after a server reload assuming the timer was not previously stopped.
|
||||
- `.update()` - legacy alias for `.start`.
|
||||
- `.stop()` - stops and resets the timer. This calls the `.at_stop()` hook.
|
||||
- `.pause()` - pauses the timer where it is, storing its current position. This calls
|
||||
the `.at_pause(manual_pause=True)` hook. This is also called on a server reload/reboot,
|
||||
at which time the `manual_pause` will be `False`.
|
||||
- `.unpause()` - unpause a previously paused script. This will call the `at_start` hook.
|
||||
- `.time_until_next_repeat()` - get the time until next time the timer fires.
|
||||
- `.remaining_repeats()` - get the number of repeats remaining, or `None` if repeats are infinite.
|
||||
- `.reset_callcount()` - this resets the repeat counter to start over from 0. Only useful if `repeats>0`.
|
||||
- `.force_repeat()` - this prematurely forces `at_repeat` to be called right away. Doing so will reset the
|
||||
countdown so that next call will again happen after `interval` seconds.
|
||||
|
||||
### Script timers vs delay/repeat
|
||||
|
||||
If the _only_ goal is to get a repeat/delay effect, the
|
||||
[evennia.utils.delay](evennia.utils.utils.delay) and
|
||||
[evennia.utils.repeat](evennia.utils.utils.repeat) functions
|
||||
should generally be considered first. A Script is a lot 'heavier' to create/delete on the fly.
|
||||
In fact, for making a single delayed call (`script.repeats==1`), the `utils.delay` call is
|
||||
probably always the better choice.
|
||||
|
||||
For repeating tasks, the `utils.repeat` is optimized for quick repeating of a large number of objects. It
|
||||
uses the TickerHandler under the hood. Its subscription-based model makes it very efficient to
|
||||
start/stop the repeating action for an object. The side effect is however that all objects set to tick
|
||||
at a given interval will _all do so at the same time_. This may or may not look strange in-game depending
|
||||
on the situation. By contrast the Script uses its own ticker that will operate independently from the
|
||||
tickers of all other Scripts.
|
||||
|
||||
It's also worth noting that once the script object has _already been created_,
|
||||
starting/stopping/pausing/unpausing the timer has very little overhead. The pause/unpause and update
|
||||
methods of the script also offers a bit more fine-control than using `utils.delays/repeat`.
|
||||
|
||||
## Script attached to another object
|
||||
|
||||
Scripts can be attached to an [Account](./Accounts.md) or (more commonly) an [Object](./Objects.md).
|
||||
If so, the 'parent object' will be available to the script as either `.obj` or `.account`.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/scripts.py
|
||||
# Script class is defined at the top of this module
|
||||
|
||||
import random
|
||||
|
||||
class Weather(Script):
|
||||
"""
|
||||
A timer script that displays weather info. Meant to
|
||||
be attached to a room.
|
||||
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
self.key = "weather_script"
|
||||
self.desc = "Gives random weather messages."
|
||||
self.interval = 60 * 5 # every 5 minutes
|
||||
|
||||
def at_repeat(self):
|
||||
"called every self.interval seconds."
|
||||
rand = random.random()
|
||||
if rand < 0.5:
|
||||
weather = "A faint breeze is felt."
|
||||
elif rand < 0.7:
|
||||
weather = "Clouds sweep across the sky."
|
||||
else:
|
||||
weather = "There is a light drizzle of rain."
|
||||
# send this message to everyone inside the object this
|
||||
# script is attached to (likely a room)
|
||||
self.obj.msg_contents(weather)
|
||||
```
|
||||
|
||||
If attached to a room, this Script will randomly report some weather
|
||||
to everyone in the room every 5 minutes.
|
||||
|
||||
```python
|
||||
myroom.scripts.add(scripts.Weather)
|
||||
```
|
||||
|
||||
> Note that `typeclasses` in your game dir is added to the setting `TYPECLASS_PATHS`.
|
||||
> Therefore we don't need to give the full path (`typeclasses.scripts.Weather`
|
||||
> but only `scripts.Weather` above.
|
||||
|
||||
You can also attach the script as part of creating it:
|
||||
|
||||
```python
|
||||
create_script('typeclasses.weather.Weather', obj=myroom)
|
||||
```
|
||||
|
||||
## Other Script methods
|
||||
|
||||
A Script has all the properties of a typeclassed object, such as `db` and `ndb`(see
|
||||
[Typeclasses](./Typeclasses.md)). Setting `key` is useful in order to manage scripts (delete them by name
|
||||
etc). These are usually set up in the Script's typeclass, but can also be assigned on the fly as
|
||||
keyword arguments to `evennia.create_script`.
|
||||
|
||||
- `at_script_creation()` - this is only called once - when the script is first created.
|
||||
- `at_server_reload()` - this is called whenever the server is warm-rebooted (e.g. with the
|
||||
`reload` command). It's a good place to save non-persistent data you might want to survive a
|
||||
reload.
|
||||
- `at_server_shutdown()` - this is called when a system reset or systems shutdown is invoked.
|
||||
- `at_server_start()` - this is called when the server comes back (from reload/shutdown/reboot). It
|
||||
can be usuful for initializations and caching of non-persistent data when starting up a script's
|
||||
functionality.
|
||||
- `at_repeat()`
|
||||
- `at_start()`
|
||||
- `at_pause()`
|
||||
- `at_stop()`
|
||||
- `delete()` - same as for other typeclassed entities, this will delete the Script. Of note is that
|
||||
it will also stop the timer (if it runs), leading to the `at_stop` hook being called.
|
||||
|
||||
In addition, Scripts support [Attributes](./Attributes.md), [Tags](./Tags.md) and [Locks](./Locks.md) etc like other
|
||||
Typeclassed entities.
|
||||
|
||||
See also the methods involved in controlling a [Timed Script](#timed-scripts) above.
|
||||
|
||||
## The GLOBAL_SCRIPTS container
|
||||
|
||||
A Script not attached to another entity is commonly referred to as a _Global_ script since it't available
|
||||
to access from anywhere. This means they need to be searched for in order to be used.
|
||||
|
||||
Evennia supplies a convenient "container" `evennia.GLOBAL_SCRIPTS` to help organize your global
|
||||
scripts. All you need is the Script's `key`.
|
||||
|
||||
|
||||
```python
|
||||
from evennia import GLOBAL_SCRIPTS
|
||||
|
||||
# access as a property on the container, named the same as the key
|
||||
my_script = GLOBAL_SCRIPTS.my_script
|
||||
# needed if there are spaces in name or name determined on the fly
|
||||
another_script = GLOBAL_SCRIPTS.get("another script")
|
||||
# get all global scripts (this returns a Django Queryset)
|
||||
all_scripts = GLOBAL_SCRIPTS.all()
|
||||
# you can operate directly on the script
|
||||
GLOBAL_SCRIPTS.weather.db.current_weather = "Cloudy"
|
||||
|
||||
```
|
||||
|
||||
```{warning}
|
||||
Note that global scripts appear as properties on `GLOBAL_SCRIPTS` based on their `key`.
|
||||
If you were to create two global scripts with the same `key` (even with different typeclasses),
|
||||
the `GLOBAL_SCRIPTS` container will only return one of them (which one depends on order in
|
||||
the database). Best is to organize your scripts so that this does not happen. Otherwise, use
|
||||
`evennia.search_scripts` to get exactly the script you want.
|
||||
```
|
||||
|
||||
There are two ways to make a script appear as a property on `GLOBAL_SCRIPTS`:
|
||||
|
||||
1. Manually create a new global script with a `key` using `create_script`.
|
||||
2. Define the script's properties in the `GLOBAL_SCRIPTS` settings variable. This tells Evennia
|
||||
that it should check if a script with that `key` exists and if not, create it for you.
|
||||
This is very useful for scripts that must always exist and/or should be auto-created
|
||||
when your server restarts. If you use this method, you must make sure all
|
||||
script keys are globally unique.
|
||||
|
||||
Here's how to tell Evennia to manage the script in settings:
|
||||
|
||||
```python
|
||||
# in mygame/server/conf/settings.py
|
||||
|
||||
GLOBAL_SCRIPTS = {
|
||||
"my_script": {
|
||||
"typeclass": "typeclasses.scripts.Weather",
|
||||
"repeats": -1,
|
||||
"interval": 50,
|
||||
"desc": "Weather script"
|
||||
},
|
||||
"storagescript": {}
|
||||
}
|
||||
```
|
||||
|
||||
Above we add two scripts with keys `myscript` and `storagescript`respectively. The following dict
|
||||
can be empty - the `settings.BASE_SCRIPT_TYPECLASS` will then be used. Under the hood, the provided
|
||||
dict (along with the `key`) will be passed into `create_script` automatically, so
|
||||
all the [same keyword arguments as for create_script](evennia.utils.create.create_script) are
|
||||
supported here.
|
||||
|
||||
```{warning}
|
||||
Before setting up Evennia to manage your script like this, make sure that your Script typeclass
|
||||
does not have any critical errors (test it separately). If there are, you'll see errors in your log
|
||||
and your Script will temporarily fall back to being a `DefaultScript` type.
|
||||
```
|
||||
|
||||
Moreover, a script defined this way is *guaranteed* to exist when you try to access it:
|
||||
|
||||
```python
|
||||
from evennia import GLOBAL_SCRIPTS
|
||||
# Delete the script
|
||||
GLOBAL_SCRIPTS.storagescript.delete()
|
||||
# running the `scripts` command now will show no storagescript
|
||||
# but below it's automatically recreated again!
|
||||
storage = GLOBAL_SCRIPTS.storagescript
|
||||
```
|
||||
|
||||
That is, if the script is deleted, next time you get it from `GLOBAL_SCRIPTS`, Evennia will use the
|
||||
information in settings to recreate it for you on the fly.
|
||||
|
||||
|
||||
## Hints: Dealing with Script Errors
|
||||
|
||||
Errors inside a timed, executing script can sometimes be rather terse or point to
|
||||
parts of the execution mechanism that is hard to interpret. One way to make it
|
||||
easier to debug scripts is to import Evennia's native logger and wrap your
|
||||
functions in a try/catch block. Evennia's logger can show you where the
|
||||
traceback occurred in your script.
|
||||
|
||||
```python
|
||||
|
||||
from evennia.utils import logger
|
||||
|
||||
class Weather(Script):
|
||||
|
||||
# [...]
|
||||
|
||||
def at_repeat(self):
|
||||
|
||||
try:
|
||||
# [...]
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Server component
|
||||
|
||||
TODO: This is currently in [Portal-and-Server](./Portal-And-Server.md).
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
# Sessions
|
||||
|
||||
|
||||
An Evennia *Session* represents one single established connection to the server. Depending on the
|
||||
Evennia session, it is possible for a person to connect multiple times, for example using different
|
||||
clients in multiple windows. Each such connection is represented by a session object.
|
||||
|
||||
A session object has its own [cmdset](./Command-Sets.md), usually the "unloggedin" cmdset. This is what
|
||||
is used to show the login screen and to handle commands to create a new account (or
|
||||
[Account](./Accounts.md) in evennia lingo) read initial help and to log into the game with an existing
|
||||
account. A session object can either be "logged in" or not. Logged in means that the user has
|
||||
authenticated. When this happens the session is associated with an Account object (which is what
|
||||
holds account-centric stuff). The account can then in turn puppet any number of objects/characters.
|
||||
|
||||
> Warning: A Session is not *persistent* - it is not a [Typeclass](./Typeclasses.md) and has no
|
||||
connection to the database. The Session will go away when a user disconnects and you will lose any
|
||||
custom data on it if the server reloads. The `.db` handler on Sessions is there to present a uniform
|
||||
API (so you can assume `.db` exists even if you don't know if you receive an Object or a Session),
|
||||
but this is just an alias to `.ndb`. So don't store any data on Sessions that you can't afford to
|
||||
lose in a reload. You have been warned.
|
||||
|
||||
## Properties on Sessions
|
||||
|
||||
Here are some important properties available on (Server-)Sessions
|
||||
|
||||
- `sessid` - The unique session-id. This is an integer starting from 1.
|
||||
- `address` - The connected client's address. Different protocols give different information here.
|
||||
- `logged_in` - `True` if the user authenticated to this session.
|
||||
- `account` - The [Account](./Accounts.md) this Session is attached to. If not logged in yet, this is
|
||||
`None`.
|
||||
- `puppet` - The [Character/Object](./Objects.md) currently puppeted by this Account/Session combo. If
|
||||
not logged in or in OOC mode, this is `None`.
|
||||
- `ndb` - The [Non-persistent Attribute](./Attributes.md) handler.
|
||||
- `db` - As noted above, Sessions don't have regular Attributes. This is an alias to `ndb`.
|
||||
- `cmdset` - The Session's [CmdSetHandler](./Command-Sets.md)
|
||||
|
||||
Session statistics are mainly used internally by Evennia.
|
||||
|
||||
- `conn_time` - How long this Session has been connected
|
||||
- `cmd_last` - Last active time stamp. This will be reset by sending `idle` keepalives.
|
||||
- `cmd_last_visible` - last active time stamp. This ignores `idle` keepalives and representes the
|
||||
last time this session was truly visibly active.
|
||||
- `cmd_total` - Total number of Commands passed through this Session.
|
||||
|
||||
|
||||
## Multisession mode
|
||||
|
||||
The number of sessions possible to connect to a given account at the same time and how it works is
|
||||
given by the `MULTISESSION_MODE` setting:
|
||||
|
||||
* `MULTISESSION_MODE=0`: One session per account. When connecting with a new session the old one is
|
||||
disconnected. This is the default mode and emulates many classic mud code bases. In default Evennia,
|
||||
this mode also changes how the `create account` Command works - it will automatically create a
|
||||
Character with the *same name* as the Account. When logging in, the login command is also modified
|
||||
to have the player automatically puppet that Character. This makes the distinction between Account
|
||||
and Character minimal from the player's perspective.
|
||||
* `MULTISESSION_MODE=1`: Many sessions per account, input/output from/to each session is treated the
|
||||
same. For the player this means they can connect to the game from multiple clients and see the same
|
||||
output in all of them. The result of a command given in one client (that is, through one Session)
|
||||
will be returned to *all* connected Sessions/clients with no distinction. This mode will have the
|
||||
Session(s) auto-create and puppet a Character in the same way as mode 0.
|
||||
* `MULTISESSION_MODE=2`: Many sessions per account, one character per session. In this mode,
|
||||
puppeting an Object/Character will link the puppet back only to the particular Session doing the
|
||||
puppeting. That is, input from that Session will make use of the CmdSet of that Object/Character and
|
||||
outgoing messages (such as the result of a `look`) will be passed back only to that puppeting
|
||||
Session. If another Session tries to puppet the same Character, the old Session will automatically
|
||||
un-puppet it. From the player's perspective, this will mean that they can open separate game clients
|
||||
and play a different Character in each using one game account.
|
||||
This mode will *not* auto-create a Character and *not* auto-puppet on login like in modes 0 and 1.
|
||||
Instead it changes how the account-cmdsets's `OOCLook` command works so as to show a simple
|
||||
'character select' menu.
|
||||
* `MULTISESSION_MODE=3`: Many sessions per account *and* character. This is the full multi-puppeting
|
||||
mode, where multiple sessions may not only connect to the player account but multiple sessions may
|
||||
also puppet a single character at the same time. From the user's perspective it means one can open
|
||||
multiple client windows, some for controlling different Characters and some that share a Character's
|
||||
input/output like in mode 1. This mode otherwise works the same as mode 2.
|
||||
|
||||
> Note that even if multiple Sessions puppet one Character, there is only ever one instance of that
|
||||
Character.
|
||||
|
||||
## Returning data to the session
|
||||
|
||||
When you use `msg()` to return data to a user, the object on which you call the `msg()` matters. The
|
||||
`MULTISESSION_MODE` also matters, especially if greater than 1.
|
||||
|
||||
For example, if you use `account.msg("hello")` there is no way for evennia to know which session it
|
||||
should send the greeting to. In this case it will send it to all sessions. If you want a specific
|
||||
session you need to supply its session to the `msg` call (`account.msg("hello",
|
||||
session=mysession)`).
|
||||
|
||||
On the other hand, if you call the `msg()` message on a puppeted object, like
|
||||
`character.msg("hello")`, the character already knows the session that controls it - it will
|
||||
cleverly auto-add this for you (you can specify a different session if you specifically want to send
|
||||
stuff to another session).
|
||||
|
||||
Finally, there is a wrapper for `msg()` on all command classes: `command.msg()`. This will
|
||||
transparently detect which session was triggering the command (if any) and redirects to that session
|
||||
(this is most often what you want). If you are having trouble redirecting to a given session,
|
||||
`command.msg()` is often the safest bet.
|
||||
|
||||
You can get the `session` in two main ways:
|
||||
* [Accounts](./Accounts.md) and [Objects](./Objects.md) (including Characters) have a `sessions` property.
|
||||
This is a *handler* that tracks all Sessions attached to or puppeting them. Use e.g.
|
||||
`accounts.sessions.get()` to get a list of Sessions attached to that entity.
|
||||
* A Command instance has a `session` property that always points back to the Session that triggered
|
||||
it (it's always a single one). It will be `None` if no session is involved, like when a mob or
|
||||
script triggers the Command.
|
||||
|
||||
## Customizing the Session object
|
||||
|
||||
When would one want to customize the Session object? Consider for example a character creation
|
||||
system: You might decide to keep this on the out-of-character level. This would mean that you create
|
||||
the character at the end of some sort of menu choice. The actual char-create cmdset would then
|
||||
normally be put on the account. This works fine as long as you are `MULTISESSION_MODE` below 2.
|
||||
For higher modes, replacing the Account cmdset will affect *all* your connected sessions, also those
|
||||
not involved in character creation. In this case you want to instead put the char-create cmdset on
|
||||
the Session level - then all other sessions will keep working normally despite you creating a new
|
||||
character in one of them.
|
||||
|
||||
By default, the session object gets the `commands.default_cmdsets.UnloggedinCmdSet` when the user
|
||||
first connects. Once the session is authenticated it has *no* default sets. To add a "logged-in"
|
||||
cmdset to the Session, give the path to the cmdset class with `settings.CMDSET_SESSION`. This set
|
||||
will then henceforth always be present as soon as the account logs in.
|
||||
|
||||
To customize further you can completely override the Session with your own subclass. To replace the
|
||||
default Session class, change `settings.SERVER_SESSION_CLASS` to point to your custom class. This is
|
||||
a dangerous practice and errors can easily make your game unplayable. Make sure to take heed of the
|
||||
[original](https://github.com/evennia/evennia/blob/master/evennia/server/session.py) and make your
|
||||
changes carefully.
|
||||
|
||||
## Portal and Server Sessions
|
||||
|
||||
*Note: This is considered an advanced topic. You don't need to know this on a first read-through.*
|
||||
|
||||
Evennia is split into two parts, the [Portal and the Server](./Portal-And-Server.md). Each side tracks
|
||||
its own Sessions, syncing them to each other.
|
||||
|
||||
The "Session" we normally refer to is actually the `ServerSession`. Its counter-part on the Portal
|
||||
side is the `PortalSession`. Whereas the server sessions deal with game states, the portal session
|
||||
deals with details of the connection-protocol itself. The two are also acting as backups of critical
|
||||
data such as when the server reboots.
|
||||
|
||||
New Account connections are listened for and handled by the Portal using the [protocols](Portal-And-
|
||||
Server) it understands (such as telnet, ssh, webclient etc). When a new connection is established, a
|
||||
`PortalSession` is created on the Portal side. This session object looks different depending on
|
||||
which protocol is used to connect, but all still have a minimum set of attributes that are generic
|
||||
to all
|
||||
sessions.
|
||||
|
||||
These common properties are piped from the Portal, through the AMP connection, to the Server, which
|
||||
is now informed a new connection has been established. On the Server side, a `ServerSession` object
|
||||
is created to represent this. There is only one type of `ServerSession`; It looks the same
|
||||
regardless of how the Account connects.
|
||||
|
||||
From now on, there is a one-to-one match between the `ServerSession` on one side of the AMP
|
||||
connection and the `PortalSession` on the other. Data arriving to the Portal Session is sent on to
|
||||
its mirror Server session and vice versa.
|
||||
|
||||
During certain situations, the portal- and server-side sessions are
|
||||
"synced" with each other:
|
||||
- The Player closes their client, killing the Portal Session. The Portal syncs with the Server to
|
||||
make sure the corresponding Server Session is also deleted.
|
||||
- The Player quits from inside the game, killing the Server Session. The Server then syncs with the
|
||||
Portal to make sure to close the Portal connection cleanly.
|
||||
- The Server is rebooted/reset/shutdown - The Server Sessions are copied over ("saved") to the
|
||||
Portal side. When the Server comes back up, this data is returned by the Portal so the two are again
|
||||
in sync. This way an Account's login status and other connection-critical things can survive a
|
||||
server reboot (assuming the Portal is not stopped at the same time, obviously).
|
||||
|
||||
## Sessionhandlers
|
||||
|
||||
Both the Portal and Server each have a *sessionhandler* to manage the connections. These handlers
|
||||
are global entities contain all methods for relaying data across the AMP bridge. All types of
|
||||
Sessions hold a reference to their respective Sessionhandler (the property is called
|
||||
`sessionhandler`) so they can relay data. See [protocols](../Concepts/Custom-Protocols.md) for more info
|
||||
on building new protocols.
|
||||
|
||||
To get all Sessions in the game (i.e. all currently connected clients), you access the server-side
|
||||
Session handler, which you get by
|
||||
```
|
||||
from evennia.server.sessionhandler import SESSION_HANDLER
|
||||
```
|
||||
> Note: The `SESSION_HANDLER` singleton has an older alias `SESSIONS` that is commonly seen in
|
||||
various places as well.
|
||||
|
||||
See the
|
||||
[sessionhandler.py](https://github.com/evennia/evennia/blob/master/evennia/server/sessionhandler.py)
|
||||
module for details on the capabilities of the `ServerSessionHandler`.
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
# Signals
|
||||
|
||||
|
||||
_This is feature available from evennia 0.9 and onward_.
|
||||
|
||||
There are multiple ways for you to plug in your own functionality into Evennia.
|
||||
The most common way to do so is through *hooks* - methods on typeclasses that
|
||||
gets called at particular events. Hooks are great when you want a game entity
|
||||
to behave a certain way when something happens to it. _Signals_ complements
|
||||
hooks for cases when you want to easily attach new functionality without
|
||||
overriding things on the typeclass.
|
||||
|
||||
When certain events happen in Evennia, a _Signal_ is fired. The idea is that
|
||||
you can "attach" any number of event-handlers to these signals. You can attach
|
||||
any number of handlers and they'll all fire whenever any entity triggers the
|
||||
signal.
|
||||
|
||||
Evennia uses the [Django Signal system](https://docs.djangoproject.com/en/2.2/topics/signals/).
|
||||
|
||||
|
||||
## Attaching a handler to a signal
|
||||
|
||||
First you create your handler
|
||||
|
||||
```python
|
||||
|
||||
def myhandler(sender, **kwargs):
|
||||
# do stuff
|
||||
|
||||
```
|
||||
|
||||
The `**kwargs` is mandatory. Then you attach it to the signal of your choice:
|
||||
|
||||
```python
|
||||
from evennia.server import signals
|
||||
|
||||
signals.SIGNAL_OBJECT_POST_CREATE.connect(myhandler)
|
||||
|
||||
```
|
||||
|
||||
This particular signal fires after (post) an Account has connected to the game.
|
||||
When that happens, `myhandler` will fire with the `sender` being the Account that just connected.
|
||||
|
||||
If you want to respond only to the effects of a specific entity you can do so
|
||||
like this:
|
||||
|
||||
```python
|
||||
from evennia import search_account
|
||||
from evennia import signals
|
||||
|
||||
account = search_account("foo")[0]
|
||||
signals.SIGNAL_ACCOUNT_POST_CONNECT.connect(myhandler, account)
|
||||
```
|
||||
|
||||
## Available signals
|
||||
|
||||
All signals (including some django-specific defaults) are available in the module
|
||||
`evennia.server.signals`
|
||||
(with a shortcut `evennia.signals`). Signals are named by the sender type. So `SIGNAL_ACCOUNT_*`
|
||||
returns
|
||||
`Account` instances as senders, `SIGNAL_OBJECT_*` returns `Object`s etc. Extra keywords (kwargs)
|
||||
should
|
||||
be extracted from the `**kwargs` dict in the signal handler.
|
||||
|
||||
- `SIGNAL_ACCOUNT_POST_CREATE` - this is triggered at the very end of `Account.create()`. Note that
|
||||
calling `evennia.create.create_account` (which is called internally by `Account.create`) will
|
||||
*not*
|
||||
trigger this signal. This is because using `Account.create()` is expected to be the most commonly
|
||||
used way for users to themselves create accounts during login. It passes and extra kwarg `ip` with
|
||||
the client IP of the connecting account.
|
||||
- `SIGNAL_ACCOUNT_POST_LOGIN` - this will always fire when the account has authenticated. Sends
|
||||
extra kwarg `session` with the new [Session](./Sessions.md) object involved.
|
||||
- `SIGNAL_ACCCOUNT_POST_FIRST_LOGIN` - this fires just before `SIGNAL_ACCOUNT_POST_LOGIN` but only
|
||||
if
|
||||
this is the *first* connection done (that is, if there are no previous sessions connected). Also
|
||||
passes the `session` along as a kwarg.
|
||||
- `SIGNAL_ACCOUNT_POST_LOGIN_FAIL` - sent when someone tried to log into an account by failed.
|
||||
Passes
|
||||
the `session` as an extra kwarg.
|
||||
- `SIGNAL_ACCOUNT_POST_LOGOUT` - always fires when an account logs off, no matter if other sessions
|
||||
remain or not. Passes the disconnecting `session` along as a kwarg.
|
||||
- `SIGNAL_ACCOUNT_POST_LAST_LOGOUT` - fires before `SIGNAL_ACCOUNT_POST_LOGOUT`, but only if this is
|
||||
the *last* Session to disconnect for that account. Passes the `session` as a kwarg.
|
||||
- `SIGNAL_OBJECT_POST_PUPPET` - fires when an account puppets this object. Extra kwargs `session`
|
||||
and `account` represent the puppeting entities.
|
||||
`SIGNAL_OBJECT_POST_UNPUPPET` - fires when the sending object is unpuppeted. Extra kwargs are
|
||||
`session` and `account`.
|
||||
- `SIGNAL_ACCOUNT_POST_RENAME` - triggered by the setting of `Account.username`. Passes extra
|
||||
kwargs `old_name`, `new_name`.
|
||||
- `SIGNAL_TYPED_OBJECT_POST_RENAME` - triggered when any Typeclassed entity's `key` is changed.
|
||||
Extra
|
||||
kwargs passed are `old_key` and `new_key`.
|
||||
- `SIGNAL_SCRIPT_POST_CREATE` - fires when a script is first created, after any hooks.
|
||||
- `SIGNAL_CHANNEL_POST_CREATE` - fires when a Channel is first created, after any hooks.
|
||||
- `SIGNAL_HELPENTRY_POST_CREATE` - fires when a help entry is first created.
|
||||
|
||||
The `evennia.signals` module also gives you conveneient access to the default Django signals (these
|
||||
use a
|
||||
different naming convention).
|
||||
|
||||
- `pre_save` - fired when any database entitiy's `.save` method fires, before any saving has
|
||||
happened.
|
||||
- `post_save` - fires after saving a database entity.
|
||||
- `pre_delete` - fires just before a database entity is deleted.
|
||||
- `post_delete` - fires after a database entity was deleted.
|
||||
- `pre_init` - fires before a typeclass' `__init__` method (which in turn
|
||||
happens before the `at_init` hook fires).
|
||||
- `post_init` - triggers at the end of `__init__` (still before the `at_init` hook).
|
||||
|
||||
These are highly specialized Django signals that are unlikely to be useful to most users. But
|
||||
they are included here for completeness.
|
||||
|
||||
- `m2m_changed` - fires after a Many-to-Many field (like `db_attributes`) changes.
|
||||
- `pre_migrate` - fires before database migration starts with `evennia migrate`.
|
||||
- `post_migrate` - fires after database migration finished.
|
||||
- `request_started` - sent when HTTP request begins.
|
||||
- `request_finished` - sent when HTTP request ends.
|
||||
- `settings_changed` - sent when changing settings due to `@override_settings`
|
||||
decorator (only relevant for unit testing)
|
||||
- `template_rendered` - sent when test system renders http template (only useful for unit tests).
|
||||
- `connection_creation` - sent when making initial connection to database.
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
# Tags
|
||||
|
||||
```{code-block}
|
||||
:caption: In game
|
||||
> tag obj = tagname
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In code, using .tags (TagHandler)
|
||||
|
||||
obj.tags.add("mytag", category="foo")
|
||||
obj.tags.get("mytag", category="foo")
|
||||
```
|
||||
|
||||
```{code-block} python
|
||||
:caption: In code, using TagProperty (auto-assign tag to all instances of the class)
|
||||
|
||||
from evennia import DefaultObject
|
||||
from evennia import TagProperty
|
||||
class Sword(DefaultObject):
|
||||
can_be_wielded = TagProperty(category='combat')
|
||||
has_sharp_edge = TagProperty(category='combat')
|
||||
|
||||
```
|
||||
|
||||
_Tags_ are short text lables one can 'hang' on objects in order to organize, group and quickly find out their properties. An Evennia entity can be tagged by any number of tags. They are more efficient than [Attributes](./Attributes.md) since on the database-side, Tags are _shared_ between all objects with that particular tag. A tag does not carry a value in itself; it either sits on the entity
|
||||
|
||||
Above, the tags inform us that the `Sword` is both sharp and can be wielded. If that's all they do, they could just be a normal Python flag. When tags become important is if there are a lot of objects with different combinations of tags. Maybe you have a magical spell that dulls _all_ sharp-edged objects in the castle - whether sword, dagger, spear or kitchen knife! You can then just grab all objects with the `has_sharp_edge` tag.
|
||||
Another example would be a weather script affecting all rooms tagged as `outdoors` or finding all characters tagged with `belongs_to_fighter_guild`.
|
||||
|
||||
In Evennia, Tags are technically also used to implement `Aliases` (alternative names for objects) and `Permissions` (simple strings for [Locks](./Locks.md) to check for).
|
||||
|
||||
|
||||
## Properties of Tags (and Aliases and Permissions)
|
||||
|
||||
Tags are *unique*. This means that there is only ever one Tag object with a given key and category.
|
||||
|
||||
> Not specifying a category (default) gives the tag a category of `None`, which is also considered a
|
||||
unique key + category combination.
|
||||
|
||||
When Tags are assigned to game entities, these entities are actually sharing the same Tag. This
|
||||
means that Tags are not suitable for storing information about a single object - use an
|
||||
[Attribute](./Attributes.md) for this instead. Tags are a lot more limited than Attributes but this also
|
||||
makes them very quick to lookup in the database - this is the whole point.
|
||||
|
||||
Tags have the following properties, stored in the database:
|
||||
|
||||
- **key** - the name of the Tag. This is the main property to search for when looking up a Tag.
|
||||
- **category** - this category allows for retrieving only specific subsets of tags used for
|
||||
different purposes. You could have one category of tags for "zones", another for "outdoor
|
||||
locations", for example. If not given, the category will be `None`, which is also considered a
|
||||
separate, default, category.
|
||||
- **data** - this is an optional text field with information about the tag. Remember that Tags are
|
||||
shared between entities, so this field cannot hold any object-specific information. Usually it would
|
||||
be used to hold info about the group of entities the Tag is tagging - possibly used for contextual
|
||||
help like a tool tip. It is not used by default.
|
||||
|
||||
There are also two special properties. These should usually not need to be changed or set, it is
|
||||
used internally by Evennia to implement various other uses it makes of the `Tag` object:
|
||||
- **model** - this holds a *natural-key* description of the model object that this tag deals with,
|
||||
on the form *application.modelclass*, for example `objects.objectdb`. It used by the TagHandler of
|
||||
each entity type for correctly storing the data behind the scenes.
|
||||
- **tagtype** - this is a "top-level category" of sorts for the inbuilt children of Tags, namely
|
||||
*Aliases* and *Permissions*. The Taghandlers using this special field are especially intended to
|
||||
free up the *category* property for any use you desire.
|
||||
|
||||
## Adding/Removing Tags
|
||||
|
||||
You can tag any *typeclassed* object, namely [Objects](./Objects.md), [Accounts](./Accounts.md),
|
||||
[Scripts](./Scripts.md) and [Channels](./Channels.md). General tags are added by the *Taghandler*. The
|
||||
tag handler is accessed as a property `tags` on the relevant entity:
|
||||
|
||||
```python
|
||||
mychair.tags.add("furniture")
|
||||
mychair.tags.add("furniture", category="luxurious")
|
||||
myroom.tags.add("dungeon#01")
|
||||
myscript.tags.add("weather", category="climate")
|
||||
myaccount.tags.add("guestaccount")
|
||||
|
||||
mychair.tags.all() # returns a list of Tags
|
||||
mychair.tags.remove("furniture")
|
||||
mychair.tags.clear()
|
||||
```
|
||||
|
||||
Adding a new tag will either create a new Tag or re-use an already existing one. Note that there are
|
||||
_two_ "furniture" tags, one with a `None` category, and one with the "luxurious" category.
|
||||
|
||||
When using `remove`, the `Tag` is not deleted but are just disconnected from the tagged object. This
|
||||
makes for very quick operations. The `clear` method removes (disconnects) all Tags from the object.
|
||||
You can also use the default `@tag` command:
|
||||
|
||||
@tag mychair = furniture
|
||||
|
||||
This tags the chair with a 'furniture' Tag (the one with a `None` category).
|
||||
|
||||
## Searching for objects with a given tag
|
||||
|
||||
Usually tags are used as a quick way to find tagged database entities. You can retrieve all objects
|
||||
with a given Tag like this in code:
|
||||
|
||||
```python
|
||||
import evennia
|
||||
|
||||
# all methods return Querysets
|
||||
|
||||
# search for objects
|
||||
objs = evennia.search_tag("furniture")
|
||||
objs2 = evennia.search_tag("furniture", category="luxurious")
|
||||
dungeon = evennia.search_tag("dungeon#01")
|
||||
forest_rooms = evennia.search_tag(category="forest")
|
||||
forest_meadows = evennia.search_tag("meadow", category="forest")
|
||||
magic_meadows = evennia.search_tag("meadow", category="magical")
|
||||
|
||||
# search for scripts
|
||||
weather = evennia.search_tag_script("weather")
|
||||
climates = evennia.search_tag_script(category="climate")
|
||||
|
||||
# search for accounts
|
||||
accounts = evennia.search_tag_account("guestaccount")
|
||||
```
|
||||
|
||||
> Note that searching for just "furniture" will only return the objects tagged with the "furniture"
|
||||
tag that
|
||||
has a category of `None`. We must explicitly give the category to get the "luxurious" furniture.
|
||||
|
||||
Using any of the `search_tag` variants will all return [Django
|
||||
Querysets](https://docs.djangoproject.com/en/2.1/ref/models/querysets/), including if you only have
|
||||
one match. You can treat querysets as lists and iterate over them, or continue building search
|
||||
queries with them.
|
||||
|
||||
Remember when searching that not setting a category means setting it to `None` - this does *not*
|
||||
mean that category is undefined, rather `None` is considered the default, unnamed category.
|
||||
|
||||
```python
|
||||
import evennia
|
||||
|
||||
myobj1.tags.add("foo") # implies category=None
|
||||
myobj2.tags.add("foo", category="bar")
|
||||
|
||||
# this returns a queryset with *only* myobj1
|
||||
objs = evennia.search_tag("foo")
|
||||
|
||||
# these return a queryset with *only* myobj2
|
||||
objs = evennia.search_tag("foo", category="bar")
|
||||
# or
|
||||
objs = evennia.search_tag(category="bar")
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
There is also an in-game command that deals with assigning and using ([Object-](./Objects.md)) tags:
|
||||
|
||||
@tag/search furniture
|
||||
|
||||
## Using Aliases and Permissions
|
||||
|
||||
Aliases and Permissions are implemented using normal TagHandlers that simply save Tags with a
|
||||
different `tagtype`. These handlers are named `aliases` and `permissions` on all Objects. They are
|
||||
used in the same way as Tags above:
|
||||
|
||||
```python
|
||||
boy.aliases.add("rascal")
|
||||
boy.permissions.add("Builders")
|
||||
boy.permissions.remove("Builders")
|
||||
|
||||
all_aliases = boy.aliases.all()
|
||||
```
|
||||
|
||||
and so on. Similarly to how `@tag` works in-game, there is also the `@perm` command for assigning
|
||||
permissions and `@alias` command for aliases.
|
||||
|
||||
## Assorted notes
|
||||
|
||||
Generally, tags are enough on their own for grouping objects. Having no tag `category` is perfectly
|
||||
fine and the normal operation. Simply adding a new Tag for grouping objects is often better than
|
||||
making a new category. So think hard before deciding you really need to categorize your Tags.
|
||||
|
||||
That said, tag categories can be useful if you build some game system that uses tags. You can then
|
||||
use tag categories to make sure to separate tags created with this system from any other tags
|
||||
created elsewhere. You can then supply custom search methods that *only* find objects tagged with
|
||||
tags of that category. An example of this
|
||||
is found in the [Zone tutorial](../Concepts/Zones.md).
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
# TickerHandler
|
||||
|
||||
|
||||
One way to implement a dynamic MUD is by using "tickers", also known as "heartbeats". A ticker is a
|
||||
timer that fires ("ticks") at a given interval. The tick triggers updates in various game systems.
|
||||
|
||||
## About Tickers
|
||||
|
||||
Tickers are very common or even unavoidable in other mud code bases. Certain code bases are even
|
||||
hard-coded to rely on the concept of the global 'tick'. Evennia has no such notion - the decision to
|
||||
use tickers is very much up to the need of your game and which requirements you have. The "ticker
|
||||
recipe" is just one way of cranking the wheels.
|
||||
|
||||
The most fine-grained way to manage the flow of time is of course to use [Scripts](./Scripts.md). Many
|
||||
types of operations (weather being the classic example) are however done on multiple objects in the
|
||||
same way at regular intervals, and for this, storing separate Scripts on each object is inefficient.
|
||||
The way to do this is to use a ticker with a "subscription model" - let objects sign up to be
|
||||
triggered at the same interval, unsubscribing when the updating is no longer desired.
|
||||
|
||||
Evennia offers an optimized implementation of the subscription model - the *TickerHandler*. This is
|
||||
a singleton global handler reachable from `evennia.TICKER_HANDLER`. You can assign any *callable* (a
|
||||
function or, more commonly, a method on a database object) to this handler. The TickerHandler will
|
||||
then call this callable at an interval you specify, and with the arguments you supply when adding
|
||||
it. This continues until the callable un-subscribes from the ticker. The handler survives a reboot
|
||||
and is highly optimized in resource usage.
|
||||
|
||||
Here is an example of importing `TICKER_HANDLER` and using it:
|
||||
|
||||
```python
|
||||
# we assume that obj has a hook "at_tick" defined on itself
|
||||
from evennia import TICKER_HANDLER as tickerhandler
|
||||
|
||||
tickerhandler.add(20, obj.at_tick)
|
||||
```
|
||||
|
||||
That's it - from now on, `obj.at_tick()` will be called every 20 seconds.
|
||||
|
||||
You can also import function and tick that:
|
||||
|
||||
```python
|
||||
from evennia import TICKER_HANDLER as tickerhandler
|
||||
from mymodule import myfunc
|
||||
|
||||
tickerhandler.add(30, myfunc)
|
||||
```
|
||||
|
||||
Removing (stopping) the ticker works as expected:
|
||||
|
||||
```python
|
||||
tickerhandler.remove(20, obj.at_tick)
|
||||
tickerhandler.remove(30, myfunc)
|
||||
```
|
||||
|
||||
Note that you have to also supply `interval` to identify which subscription to remove. This is
|
||||
because the TickerHandler maintains a pool of tickers and a given callable can subscribe to be
|
||||
ticked at any number of different intervals.
|
||||
|
||||
The full definition of the `tickerhandler.add` method is
|
||||
|
||||
```python
|
||||
tickerhandler.add(interval, callback,
|
||||
idstring="", persistent=True, *args, **kwargs)
|
||||
```
|
||||
|
||||
Here `*args` and `**kwargs` will be passed to `callback` every `interval` seconds. If `persistent`
|
||||
is `False`, this subscription will not survive a server reload.
|
||||
|
||||
Tickers are identified and stored by making a key of the callable itself, the ticker-interval, the
|
||||
`persistent` flag and the `idstring` (the latter being an empty string when not given explicitly).
|
||||
|
||||
Since the arguments are not included in the ticker's identification, the `idstring` must be used to
|
||||
have a specific callback triggered multiple times on the same interval but with different arguments:
|
||||
|
||||
```python
|
||||
tickerhandler.add(10, obj.update, "ticker1", True, 1, 2, 3)
|
||||
tickerhandler.add(10, obj.update, "ticker2", True, 4, 5)
|
||||
```
|
||||
|
||||
> Note that, when we want to send arguments to our callback within a ticker handler, we need to
|
||||
specify `idstring` and `persistent` before, unless we call our arguments as keywords, which would
|
||||
often be more readable:
|
||||
|
||||
```python
|
||||
tickerhandler.add(10, obj.update, caller=self, value=118)
|
||||
```
|
||||
|
||||
If you add a ticker with exactly the same combination of callback, interval and idstring, it will
|
||||
overload the existing ticker. This identification is also crucial for later removing (stopping) the
|
||||
subscription:
|
||||
|
||||
```python
|
||||
tickerhandler.remove(10, obj.update, idstring="ticker1")
|
||||
tickerhandler.remove(10, obj.update, idstring="ticker2")
|
||||
```
|
||||
|
||||
The `callable` can be on any form as long as it accepts the arguments you give to send to it in
|
||||
`TickerHandler.add`.
|
||||
|
||||
> Note that everything you supply to the TickerHandler will need to be pickled at some point to be
|
||||
saved into the database. Most of the time the handler will correctly store things like database
|
||||
objects, but the same restrictions as for [Attributes](./Attributes.md) apply to what the TickerHandler
|
||||
may store.
|
||||
|
||||
When testing, you can stop all tickers in the entire game with `tickerhandler.clear()`. You can also
|
||||
view the currently subscribed objects with `tickerhandler.all()`.
|
||||
|
||||
See the [Weather Tutorial](../Howtos/Weather-Tutorial.md) for an example of using the TickerHandler.
|
||||
|
||||
### When *not* to use TickerHandler
|
||||
|
||||
Using the TickerHandler may sound very useful but it is important to consider when not to use it.
|
||||
Even if you are used to habitually relying on tickers for everything in other code bases, stop and
|
||||
think about what you really need it for. This is the main point:
|
||||
|
||||
> You should *never* use a ticker to catch *changes*.
|
||||
|
||||
Think about it - you might have to run the ticker every second to react to the change fast enough.
|
||||
Most likely nothing will have changed at a given moment. So you are doing pointless calls (since
|
||||
skipping the call gives the same result as doing it). Making sure nothing's changed might even be
|
||||
computationally expensive depending on the complexity of your system. Not to mention that you might
|
||||
need to run the check *on every object in the database*. Every second. Just to maintain status quo
|
||||
...
|
||||
|
||||
Rather than checking over and over on the off-chance that something changed, consider a more
|
||||
proactive approach. Could you implement your rarely changing system to *itself* report when its
|
||||
status changes? It's almost always much cheaper/efficient if you can do things "on demand". Evennia
|
||||
itself uses hook methods for this very reason.
|
||||
|
||||
So, if you consider a ticker that will fire very often but which you expect to have no effect 99% of
|
||||
the time, consider handling things things some other way. A self-reporting on-demand solution is
|
||||
usually cheaper also for fast-updating properties. Also remember that some things may not need to be
|
||||
updated until someone actually is examining or using them - any interim changes happening up to that
|
||||
moment are pointless waste of computing time.
|
||||
|
||||
The main reason for needing a ticker is when you want things to happen to multiple objects at the
|
||||
same time without input from something else.
|
||||
|
|
@ -1,352 +0,0 @@
|
|||
# Typeclasses
|
||||
|
||||
*Typeclasses* form the core of Evennia's data storage. It allows Evennia to represent any number of
|
||||
different game entities as Python classes, without having to modify the database schema for every
|
||||
new type.
|
||||
|
||||
In Evennia the most important game entities, [Accounts](./Accounts.md), [Objects](./Objects.md),
|
||||
[Scripts](./Scripts.md) and [Channels](./Channels.md) are all Python classes inheriting, at
|
||||
varying distance, from `evennia.typeclasses.models.TypedObject`. In the documentation we refer to
|
||||
these objects as being "typeclassed" or even "being a typeclass".
|
||||
|
||||
This is how the inheritance looks for the typeclasses in Evennia:
|
||||
|
||||
```
|
||||
TypedObject
|
||||
_________________|_________________________________
|
||||
| | | |
|
||||
1: AccountDB ObjectDB ScriptDB ChannelDB
|
||||
| | | |
|
||||
2: DefaultAccount DefaultObject DefaultScript DefaultChannel
|
||||
| DefaultCharacter | |
|
||||
| DefaultRoom | |
|
||||
| DefaultExit | |
|
||||
| | | |
|
||||
3: Account Object Script Channel
|
||||
Character
|
||||
Room
|
||||
Exit
|
||||
```
|
||||
|
||||
- **Level 1** above is the "database model" level. This describes the database tables and fields
|
||||
(this is technically a [Django model](https://docs.djangoproject.com/en/2.2/topics/db/models/)).
|
||||
- **Level 2** is where we find Evennia's default implementations of the various game entities, on
|
||||
top of the database. These classes define all the hook methods that Evennia calls in various
|
||||
situations. `DefaultObject` is a little special since it's the parent for `DefaultCharacter`,
|
||||
`DefaultRoom` and `DefaultExit`. They are all grouped under level 2 because they all represents
|
||||
defaults to build from.
|
||||
- **Level 3**, finally, holds empty template classes created in your game directory. This is the
|
||||
level you are meant to modify and tweak as you please, overloading the defaults as befits your game.
|
||||
The templates inherit directly from their defaults, so `Object` inherits from `DefaultObject` and
|
||||
`Room` inherits from `DefaultRoom`.
|
||||
|
||||
The `typeclass/list` command will provide a list of all typeclasses known to
|
||||
Evennia. This can be useful for getting a feel for what is available. Note
|
||||
however that if you add a new module with a class in it but do not import that
|
||||
module from anywhere, the `typeclass/list` will not find it. To make it known
|
||||
to Evennia you must import that module from somewhere.
|
||||
|
||||
|
||||
## Difference between typeclasses and classes
|
||||
|
||||
All Evennia classes inheriting from class in the table above share one important feature and two
|
||||
important limitations. This is why we don't simply call them "classes" but "typeclasses".
|
||||
|
||||
1. A typeclass can save itself to the database. This means that some properties (actually not that
|
||||
many) on the class actually represents database fields and can only hold very specific data types.
|
||||
This is detailed [below](./Typeclasses.md#about-typeclass-properties).
|
||||
1. Due to its connection to the database, the typeclass' name must be *unique* across the _entire_
|
||||
server namespace. That is, there must never be two same-named classes defined anywhere. So the below
|
||||
code would give an error (since `DefaultObject` is now globally found both in this module and in the
|
||||
default library):
|
||||
|
||||
```python
|
||||
from evennia import DefaultObject as BaseObject
|
||||
class DefaultObject(BaseObject):
|
||||
pass
|
||||
```
|
||||
|
||||
1. A typeclass' `__init__` method should normally not be overloaded. This has mostly to do with the
|
||||
fact that the `__init__` method is not called in a predictable way. Instead Evennia suggest you use
|
||||
the `at_*_creation` hooks (like `at_object_creation` for Objects) for setting things the very first
|
||||
time the typeclass is saved to the database or the `at_init` hook which is called every time the
|
||||
object is cached to memory. If you know what you are doing and want to use `__init__`, it *must*
|
||||
both accept arbitrary keyword arguments and use `super` to call its parent::
|
||||
|
||||
```python
|
||||
def __init__(self, **kwargs):
|
||||
# my content
|
||||
super().__init__(**kwargs)
|
||||
# my content
|
||||
```
|
||||
|
||||
Apart from this, a typeclass works like any normal Python class and you can
|
||||
treat it as such.
|
||||
|
||||
|
||||
## Creating a new typeclass
|
||||
|
||||
It's easy to work with Typeclasses. Either you use an existing typeclass or you create a new Python
|
||||
class inheriting from an existing typeclass. Here is an example of creating a new type of Object:
|
||||
|
||||
```python
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Furniture(DefaultObject):
|
||||
# this defines what 'furniture' is, like
|
||||
# storing who sits on it or something.
|
||||
pass
|
||||
|
||||
```
|
||||
|
||||
You can now create a new `Furniture` object in two ways. First (and usually not the most
|
||||
convenient) way is to create an instance of the class and then save it manually to the database:
|
||||
|
||||
```python
|
||||
chair = Furniture(db_key="Chair")
|
||||
chair.save()
|
||||
|
||||
```
|
||||
|
||||
To use this you must give the database field names as keywords to the call. Which are available
|
||||
depends on the entity you are creating, but all start with `db_*` in Evennia. This is a method you
|
||||
may be familiar with if you know Django from before.
|
||||
|
||||
It is recommended that you instead use the `create_*` functions to create typeclassed entities:
|
||||
|
||||
|
||||
```python
|
||||
from evennia import create_object
|
||||
|
||||
chair = create_object(Furniture, key="Chair")
|
||||
# or (if your typeclass is in a module furniture.py)
|
||||
chair = create_object("furniture.Furniture", key="Chair")
|
||||
```
|
||||
|
||||
The `create_object` (`create_account`, `create_script` etc) takes the typeclass as its first
|
||||
argument; this can both be the actual class or the python path to the typeclass as found under your
|
||||
game directory. So if your `Furniture` typeclass sits in `mygame/typeclasses/furniture.py`, you
|
||||
could point to it as `typeclasses.furniture.Furniture`. Since Evennia will itself look in
|
||||
`mygame/typeclasses`, you can shorten this even further to just `furniture.Furniture`. The create-
|
||||
functions take a lot of extra keywords allowing you to set things like [Attributes](./Attributes.md) and
|
||||
[Tags](./Tags.md) all in one go. These keywords don't use the `db_*` prefix. This will also automatically
|
||||
save the new instance to the database, so you don't need to call `save()` explicitly.
|
||||
|
||||
## About typeclass properties
|
||||
|
||||
An example of a database field is `db_key`. This stores the "name" of the entity you are modifying
|
||||
and can thus only hold a string. This is one way of making sure to update the `db_key`:
|
||||
|
||||
```python
|
||||
chair.db_key = "Table"
|
||||
chair.save()
|
||||
|
||||
print(chair.db_key)
|
||||
<<< Table
|
||||
```
|
||||
|
||||
That is, we change the chair object to have the `db_key` "Table", then save this to the database.
|
||||
However, you almost never do things this way; Evennia defines property wrappers for all the database
|
||||
fields. These are named the same as the field, but without the `db_` part:
|
||||
|
||||
```python
|
||||
chair.key = "Table"
|
||||
|
||||
print(chair.key)
|
||||
<<< Table
|
||||
|
||||
```
|
||||
|
||||
The `key` wrapper is not only shorter to write, it will make sure to save the field for you, and
|
||||
does so more efficiently by levering sql update mechanics under the hood. So whereas it is good to
|
||||
be aware that the field is named `db_key` you should use `key` as much as you can.
|
||||
|
||||
Each typeclass entity has some unique fields relevant to that type. But all also share the
|
||||
following fields (the wrapper name without `db_` is given):
|
||||
|
||||
- `key` (str): The main identifier for the entity, like "Rose", "myscript" or "Paul". `name` is an
|
||||
alias.
|
||||
- `date_created` (datetime): Time stamp when this object was created.
|
||||
- `typeclass_path` (str): A python path pointing to the location of this (type)class
|
||||
|
||||
There is one special field that doesn't use the `db_` prefix (it's defined by Django):
|
||||
|
||||
- `id` (int): the database id (database ref) of the object. This is an ever-increasing, unique
|
||||
integer. It can also be accessed as `dbid` (database ID) or `pk` (primary key). The `dbref` property
|
||||
returns the string form "#id".
|
||||
|
||||
The typeclassed entity has several common handlers:
|
||||
|
||||
- `tags` - the [TagHandler](./Tags.md) that handles tagging. Use `tags.add()` , `tags.get()` etc.
|
||||
- `locks` - the [LockHandler](./Locks.md) that manages access restrictions. Use `locks.add()`,
|
||||
`locks.get()` etc.
|
||||
- `attributes` - the [AttributeHandler](./Attributes.md) that manages Attributes on the object. Use
|
||||
`attributes.add()`
|
||||
etc.
|
||||
- `db` (DataBase) - a shortcut property to the AttributeHandler; allowing `obj.db.attrname = value`
|
||||
- `nattributes` - the [Non-persistent AttributeHandler](./Attributes.md) for attributes not saved in the
|
||||
database.
|
||||
- `ndb` (NotDataBase) - a shortcut property to the Non-peristent AttributeHandler. Allows
|
||||
`obj.ndb.attrname = value`
|
||||
|
||||
|
||||
Each of the typeclassed entities then extend this list with their own properties. Go to the
|
||||
respective pages for [Objects](./Objects.md), [Scripts](./Scripts.md), [Accounts](./Accounts.md) and
|
||||
[Channels](./Channels.md) for more info. It's also recommended that you explore the available
|
||||
entities using [Evennia's flat API](../Evennia-API.md) to explore which properties and methods they have
|
||||
available.
|
||||
|
||||
## Overloading hooks
|
||||
|
||||
The way to customize typeclasses is usually to overload *hook methods* on them. Hooks are methods
|
||||
that Evennia call in various situations. An example is the `at_object_creation` hook on `Objects`,
|
||||
which is only called once, the very first time this object is saved to the database. Other examples
|
||||
are the `at_login` hook of Accounts and the `at_repeat` hook of Scripts.
|
||||
|
||||
## Querying for typeclasses
|
||||
|
||||
Most of the time you search for objects in the database by using convenience methods like the
|
||||
`caller.search()` of [Commands](./Commands.md) or the search functions like `evennia.search_objects`.
|
||||
|
||||
You can however also query for them directly using [Django's query
|
||||
language](https://docs.djangoproject.com/en/1.7/topics/db/queries/). This makes use of a _database
|
||||
manager_ that sits on all typeclasses, named `objects`. This manager holds methods that allow
|
||||
database searches against that particular type of object (this is the way Django normally works
|
||||
too). When using Django queries, you need to use the full field names (like `db_key`) to search:
|
||||
|
||||
```python
|
||||
matches = Furniture.objects.get(db_key="Chair")
|
||||
|
||||
```
|
||||
|
||||
It is important that this will *only* find objects inheriting directly from `Furniture` in your
|
||||
database. If there was a subclass of `Furniture` named `Sitables` you would not find any chairs
|
||||
derived from `Sitables` with this query (this is not a Django feature but special to Evennia). To
|
||||
find objects from subclasses Evennia instead makes the `get_family` and `filter_family` query
|
||||
methods available:
|
||||
|
||||
```python
|
||||
# search for all furnitures and subclasses of furnitures
|
||||
# whose names starts with "Chair"
|
||||
matches = Furniture.objects.filter_family(db_key__startswith="Chair")
|
||||
|
||||
```
|
||||
|
||||
To make sure to search, say, all `Scripts` *regardless* of typeclass, you need to query from the
|
||||
database model itself. So for Objects, this would be `ObjectDB` in the diagram above. Here's an
|
||||
example for Scripts:
|
||||
|
||||
```python
|
||||
from evennia import ScriptDB
|
||||
matches = ScriptDB.objects.filter(db_key__contains="Combat")
|
||||
```
|
||||
|
||||
When querying from the database model parent you don't need to use `filter_family` or `get_family` -
|
||||
you will always query all children on the database model.
|
||||
|
||||
## Updating existing typeclass instances
|
||||
|
||||
If you already have created instances of Typeclasses, you can modify the *Python code* at any time -
|
||||
due to how Python inheritance works your changes will automatically be applied to all children once
|
||||
you have reloaded the server.
|
||||
|
||||
However, database-saved data, like `db_*` fields, [Attributes](./Attributes.md), [Tags](./Tags.md) etc, are
|
||||
not themselves embedded into the class and will *not* be updated automatically. This you need to
|
||||
manage yourself, by searching for all relevant objects and updating or adding the data:
|
||||
|
||||
```python
|
||||
# add a worth Attribute to all existing Furniture
|
||||
for obj in Furniture.objects.all():
|
||||
# this will loop over all Furniture instances
|
||||
obj.db.worth = 100
|
||||
```
|
||||
|
||||
A common use case is putting all Attributes in the `at_*_creation` hook of the entity, such as
|
||||
`at_object_creation` for `Objects`. This is called every time an object is created - and only then.
|
||||
This is usually what you want but it does mean already existing objects won't get updated if you
|
||||
change the contents of `at_object_creation` later. You can fix this in a similar way as above
|
||||
(manually setting each Attribute) or with something like this:
|
||||
|
||||
```python
|
||||
# Re-run at_object_creation only on those objects not having the new Attribute
|
||||
for obj in Furniture.objects.all():
|
||||
if not obj.db.worth:
|
||||
obj.at_object_creation()
|
||||
```
|
||||
|
||||
The above examples can be run in the command prompt created by `evennia shell`. You could also run
|
||||
it all in-game using `@py`. That however requires you to put the code (including imports) as one
|
||||
single line using `;` and [list
|
||||
comprehensions](http://www.secnetix.de/olli/Python/list_comprehensions.hawk), like this (ignore the
|
||||
line break, that's only for readability in the wiki):
|
||||
|
||||
```
|
||||
@py from typeclasses.furniture import Furniture;
|
||||
[obj.at_object_creation() for obj in Furniture.objects.all() if not obj.db.worth]
|
||||
```
|
||||
|
||||
It is recommended that you plan your game properly before starting to build, to avoid having to
|
||||
retroactively update objects more than necessary.
|
||||
|
||||
## Swap typeclass
|
||||
|
||||
If you want to swap an already existing typeclass, there are two ways to do so: From in-game and via
|
||||
code. From inside the game you can use the default `@typeclass` command:
|
||||
|
||||
```
|
||||
@typeclass objname = path.to.new.typeclass
|
||||
```
|
||||
|
||||
There are two important switches to this command:
|
||||
- `/reset` - This will purge all existing Attributes on the object and re-run the creation hook
|
||||
(like `at_object_creation` for Objects). This assures you get an object which is purely of this new
|
||||
class.
|
||||
- `/force` - This is required if you are changing the class to be *the same* class the object
|
||||
already has - it's a safety check to avoid user errors. This is usually used together with `/reset`
|
||||
to re-run the creation hook on an existing class.
|
||||
|
||||
In code you instead use the `swap_typeclass` method which you can find on all typeclassed entities:
|
||||
|
||||
```python
|
||||
obj_to_change.swap_typeclass(new_typeclass_path, clean_attributes=False,
|
||||
run_start_hooks="all", no_default=True, clean_cmdsets=False)
|
||||
```
|
||||
|
||||
The arguments to this method are described [in the API docs
|
||||
here](github:evennia.typeclasses.models#typedobjectswap_typeclass).
|
||||
|
||||
|
||||
## How typeclasses actually work
|
||||
|
||||
*This is considered an advanced section.*
|
||||
|
||||
Technically, typeclasses are [Django proxy
|
||||
models](https://docs.djangoproject.com/en/1.7/topics/db/models/#proxy-models). The only database
|
||||
models that are "real" in the typeclass system (that is, are represented by actual tables in the
|
||||
database) are `AccountDB`, `ObjectDB`, `ScriptDB` and `ChannelDB` (there are also
|
||||
[Attributes](./Attributes.md) and [Tags](./Tags.md) but they are not typeclasses themselves). All the
|
||||
subclasses of them are "proxies", extending them with Python code without actually modifying the
|
||||
database layout.
|
||||
|
||||
Evennia modifies Django's proxy model in various ways to allow them to work without any boiler plate
|
||||
(for example you don't need to set the Django "proxy" property in the model `Meta` subclass, Evennia
|
||||
handles this for you using metaclasses). Evennia also makes sure you can query subclasses as well as
|
||||
patches django to allow multiple inheritance from the same base class.
|
||||
|
||||
## Caveats
|
||||
|
||||
Evennia uses the *idmapper* to cache its typeclasses (Django proxy models) in memory. The idmapper
|
||||
allows things like on-object handlers and properties to be stored on typeclass instances and to not
|
||||
get lost as long as the server is running (they will only be cleared on a Server reload). Django
|
||||
does not work like this by default; by default every time you search for an object in the database
|
||||
you'll get a *different* instance of that object back and anything you stored on it that was not in
|
||||
the database would be lost. The bottom line is that Evennia's Typeclass instances subside in memory
|
||||
a lot longer than vanilla Django model instance do.
|
||||
|
||||
There is one caveat to consider with this, and that relates to [making your own models](New-
|
||||
Models): Foreign relationships to typeclasses are cached by Django and that means that if you were
|
||||
to change an object in a foreign relationship via some other means than via that relationship, the
|
||||
object seeing the relationship may not reliably update but will still see its old cached version.
|
||||
Due to typeclasses staying so long in memory, stale caches of such relationships could be more
|
||||
visible than common in Django. See the [closed issue #1098 and its
|
||||
comments](https://github.com/evennia/evennia/issues/1098) for examples and solutions.
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
# Evennia REST API
|
||||
|
||||
Evennia makes its database accessible via a REST API found on
|
||||
[http://localhost:4001/api](http://localhost:4001/api) if running locally with
|
||||
default setup. The API allows you to retrieve, edit and create resources from
|
||||
outside the game, for example with your own custom client or game editor.
|
||||
|
||||
While you can view and learn about the api in the web browser, it is really
|
||||
meant to be accessed in code, by other programs.
|
||||
|
||||
The API is using [Django Rest Framework][drf]. This automates the process
|
||||
of setting up _views_ (Python code) to process the result of web requests.
|
||||
The process of retrieving data is similar to that explained on the
|
||||
[Webserver](./Webserver.md) page, except the views will here return [JSON][json]
|
||||
data for the resource you want. You can also _send_ such JSON data
|
||||
in order to update the database from the outside.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
To activate the API, add this to your settings file.
|
||||
|
||||
REST_API_ENABLED = True
|
||||
|
||||
The main controlling setting is `REST_FRAMEWORK`, which is a dict. The keys
|
||||
`DEFAULT_LIST_PERMISSION` and `DEFAULT_CREATE_PERMISSIONS` control who may
|
||||
view and create new objects via the api respectively. By default, users with
|
||||
['Builder'-level permission](./Permissions.md) or higher may access both actions.
|
||||
|
||||
While the api is meant to be expanded upon, Evennia supplies several operations
|
||||
out of the box. If you click the `Autodoc` button in the upper right of the `/api`
|
||||
website you'll get a fancy graphical presentation of the available endpoints.
|
||||
|
||||
Here is an example of calling the api in Python using the standard `requests` library.
|
||||
|
||||
>>> import requests
|
||||
>>> response = requests.get("https://www.mygame.com/api", auth=("MyUsername", "password123"))
|
||||
>>> response.json()
|
||||
{'accounts': 'http://www.mygame.com/api/accounts/',
|
||||
'objects': 'http://www.mygame.com/api/objects/',
|
||||
'characters': 'http://www.mygame.comg/api/characters/',
|
||||
'exits': 'http://www.mygame.com/api/exits/',
|
||||
'rooms': 'http://www.mygame.com/api/rooms/',
|
||||
'scripts': 'http://www.mygame.com/api/scripts/'
|
||||
'helpentries': 'http://www.mygame.com/api/helpentries/' }
|
||||
|
||||
To list a specific type of object:
|
||||
|
||||
>>> response = requests.get("https://www.mygame.com/api/objects",
|
||||
auth=("Myusername", "password123"))
|
||||
>>> response.json()
|
||||
{
|
||||
"count": 125,
|
||||
"next": "https://www.mygame.com/api/objects/?limit=25&offset=25",
|
||||
"previous": null,
|
||||
"results" : [{"db_key": "A rusty longsword", "id": 57, "db_location": 213, ...}]}
|
||||
|
||||
In the above example, it now displays the objects inside the "results" array,
|
||||
while it has a "count" value for the number of total objects, and "next" and
|
||||
"previous" links for the next and previous page, if any. This is called
|
||||
[pagination][pagination], and the link displays "limit" and "offset" as query
|
||||
parameters that can be added to the url to control the output.
|
||||
|
||||
|
||||
Other query parameters can be defined as [filters][filters] which allow you to
|
||||
further narrow the results. For example, to only get accounts with developer
|
||||
permissions:
|
||||
|
||||
>>> response = requests.get("https://www.mygame.com/api/accounts/?permission=developer",
|
||||
auth=("MyUserName", "password123"))
|
||||
>>> response.json()
|
||||
{
|
||||
"count": 1,
|
||||
"results": [{"username": "bob",...}]
|
||||
}
|
||||
|
||||
Now suppose that you want to use the API to create an [Object](./Objects.md):
|
||||
|
||||
>>> data = {"db_key": "A shiny sword"}
|
||||
>>> response = requests.post("https://www.mygame.com/api/objects",
|
||||
data=data, auth=("Anotherusername", "mypassword"))
|
||||
>>> response.json()
|
||||
{"db_key": "A shiny sword", "id": 214, "db_location": None, ...}
|
||||
|
||||
|
||||
Here we made a HTTP POST request to the `/api/objects` endpoint with the `db_key`
|
||||
we wanted. We got back info for the newly created object. You can now make
|
||||
another request with PUT (replace everything) or PATCH (replace only what you
|
||||
provide). By providing the id to the endpoint (`/api/objects/214`),
|
||||
we make sure to update the right sword:
|
||||
|
||||
>>> data = {"db_key": "An even SHINIER sword", "db_location": 50}
|
||||
>>> response = requests.put("https://www.mygame.com/api/objects/214",
|
||||
data=data, auth=("Anotherusername", "mypassword"))
|
||||
>>> response.json()
|
||||
{"db_key": "An even SHINIER sword", "id": 214, "db_location": 50, ...}
|
||||
|
||||
|
||||
In most cases, you won't be making API requests to the backend with Python,
|
||||
but with Javascript from some frontend application.
|
||||
There are many Javascript libraries which are meant to make this process
|
||||
easier for requests from the frontend, such as [AXIOS][axios], or using
|
||||
the native [Fetch][fetch].
|
||||
|
||||
## Customizing the API
|
||||
|
||||
Overall, reading up on [Django Rest Framework ViewSets](https://www.django-rest-framework.org/api-guide/viewsets) and
|
||||
other parts of their documentation is required for expanding and
|
||||
customizing the API.
|
||||
|
||||
Check out the [Website](./Website.md) page for help on how to override code, templates
|
||||
and static files.
|
||||
- API templates (for the web-display) is located in `evennia/web/api/templates/rest_framework/` (it must
|
||||
be named such to allow override of the original REST framework templates).
|
||||
- Static files is in `evennia/web/api/static/rest_framework/`
|
||||
- The api code is located in `evennia/web/api/` - the `url.py` file here is responsible for
|
||||
collecting all view-classes.
|
||||
|
||||
Contrary to other web components, there is no pre-made urls.py set up for
|
||||
`mygame/web/api/`. This is because the registration of models with the api is
|
||||
strongly integrated with the REST api functionality. Easiest is probably to
|
||||
copy over `evennia/web/api/urls.py` and modify it in place.
|
||||
|
||||
|
||||
[wiki-api]: https://en.wikipedia.org/wiki/Application_programming_interface
|
||||
[drf]: https://www.django-rest-framework.org/
|
||||
[pagination]: https://www.django-rest-framework.org/api-guide/pagination/
|
||||
[filters]: https://www.django-rest-framework.org/api-guide/filtering/#filtering
|
||||
[json]: https://en.wikipedia.org/wiki/JSON
|
||||
[crud]: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete
|
||||
[serializers]: https://www.django-rest-framework.org/api-guide/serializers/
|
||||
[ajax]: https://en.wikipedia.org/wiki/Ajax_(programming)
|
||||
[rest]: https://en.wikipedia.org/wiki/Representational_state_transfer
|
||||
[requests]: https://requests.readthedocs.io/en/master/
|
||||
[axios]: https://github.com/axios/axios
|
||||
[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
# The Web Admin
|
||||
|
||||
The Evennia _Web admin_ is a customized [Django admin site](https://docs.djangoproject.com/en/3.2/ref/contrib/admin/)
|
||||
used for manipulating the game database using a graphical interface. You
|
||||
have to be logged into the site to use it. It then appears as an `Admin` link
|
||||
the top of your website. You can also go to [http://localhost:4001/admin](http://localhost:4001/admin) when
|
||||
running locally.
|
||||
|
||||
Almost all actions done in the admin can also be done in-game by use of Admin-
|
||||
or Builder-commands.
|
||||
|
||||
## Usage
|
||||
|
||||
The admin is pretty self-explanatory - you can see lists of each object type,
|
||||
create new instances of each type and also add new Attributes/tags them. The
|
||||
admin frontpage will give a summary of all relevant entities and how they are
|
||||
used.
|
||||
|
||||
There are a few use cases that requires some additional explanation though.
|
||||
|
||||
### Adding objects to Attributes
|
||||
|
||||
The `value` field of an Attribute is pickled into a special form. This is usually not
|
||||
something you need to worry about (the admin will pickle/unpickle) the value
|
||||
for you), _except_ if you want to store a database-object in an attribute. Such
|
||||
objects are actually stored as a `tuple` with object-unique data.
|
||||
|
||||
1. Find the object you want to add to the Attribute. At the bottom of the first section
|
||||
you'll find the field _Serialized string_. This string shows a Python tuple like
|
||||
|
||||
('__packed_dbobj__', ('objects', 'objectdb'), '2021:05:15-08:59:30:624660', 358)
|
||||
|
||||
Mark and copy this tuple-string to your clipboard exactly as it stands (parentheses and all).
|
||||
2. Go to the entity that should have the new Attribute and create the Attribute. In its `value`
|
||||
field, paste the tuple-string you copied before. Save!
|
||||
3. If you want to store multiple objects in, say, a list, you can do so by literally
|
||||
typing a python list `[tuple, tuple, tuple, ...]` where you paste in the serialized
|
||||
tuple-strings with commas. At some point it's probably easier to do this in code though ...
|
||||
|
||||
### Linking Accounts and Characters
|
||||
|
||||
In `MULTISESSION_MODE` 0 or 1, each connection can have one Account and one
|
||||
Character, usually with the same name. Normally this is done by the user
|
||||
creating a new account and logging in - a matching Character will then be
|
||||
created for them. You can however also do so manually in the admin:
|
||||
|
||||
1. First create the complete Account in the admin.
|
||||
2. Next, create the Object (usually of `Character` typeclass) and name it the same
|
||||
as the Account. It also needs a command-set. The default CharacterCmdset is a good bet.
|
||||
3. In the `Puppeting Account` field, select the Account.
|
||||
4. Make sure to save everything.
|
||||
5. Click the `Link to Account` button (this will only work if you saved first). This will
|
||||
add the needed locks and Attributes to the Account to allow them to immediately
|
||||
connect to the Character when they next log in. This will (where possible):
|
||||
- Set `account.db._last_puppet` to the Character.
|
||||
- Add Character to `account.db._playabel_characters` list.
|
||||
- Add/extend the `puppet:` lock on the Character to include `puppet:pid(<Character.id>)`
|
||||
|
||||
### Building with the Admin
|
||||
|
||||
It's possible (if probably not very practical at scale) to build and describe
|
||||
rooms in the Admin.
|
||||
|
||||
1. Create an `Object` of a Room-typeclass with a suitable room-name.
|
||||
2. Set an Attribute 'desc' on the room - the value of this Attribute is the
|
||||
room's description.
|
||||
3. Add `Tags` of `type` 'alias' to add room-aliases (no type for regular tags)
|
||||
|
||||
Exits:
|
||||
|
||||
1. Exits are `Objects` of an `Exit` typeclass, so create one.
|
||||
2. The exit has `Location` of the room you just created.
|
||||
3. Set `Destination` set to where the exit leads to.
|
||||
4. Set a 'desc' Attribute, this is shown if someone looks at the exit.
|
||||
5. `Tags` of `type` 'alias' are alternative names users can use to go through
|
||||
this exit.
|
||||
|
||||
## Grant others access to the admin
|
||||
|
||||
The access to the admin is controlled by the `Staff status` flag on the
|
||||
Account. Without this flag set, even superusers will not even see the admin
|
||||
link on the web page. The staff-status has no in-game equivalence.
|
||||
|
||||
|
||||
Only Superusers can change the `Superuser status` flag, and grant new
|
||||
permissions to accounts. The superuser is the only permission level that is
|
||||
also relevant in-game. `User Permissions` and `Groups` found on the `Account`
|
||||
admin page _only_ affects the admin - they have no connection to the in-game
|
||||
[Permissions](./Permissions.md) (Player, Builder, Admin etc).
|
||||
|
||||
For a staffer with `Staff status` to be able to actually do anything, the
|
||||
superuser must grant at least some permissions for them on their Account. This
|
||||
can also be good in order to limit mistakes. It can be a good idea to not allow
|
||||
the `Can delete Account` permission, for example.
|
||||
|
||||
```{important}
|
||||
|
||||
If you grant staff-status and permissions to an Account and they still cannot
|
||||
access the admin's content, try reloading the server.
|
||||
|
||||
```
|
||||
|
||||
```{warning}
|
||||
|
||||
If a staff member has access to the in-game ``py`` command, they can just as
|
||||
well have their admin ``Superuser status`` set too. The reason is that ``py``
|
||||
grants them all the power they need to set the ``is_superuser`` flag on their
|
||||
account manually. There is a reason access to the ``py`` command must be
|
||||
considered carefully ...
|
||||
|
||||
```
|
||||
|
||||
## Customizing the web admin
|
||||
|
||||
Customizing the admin is a big topic and something beyond the scope of this
|
||||
documentation. See the [official Django docs](https://docs.djangoproject.com/en/3.2/ref/contrib/admin/) for
|
||||
the details. This is just a brief summary.
|
||||
|
||||
See the [Website](./Website.md) page for an overview of the components going into
|
||||
generating a web page. The Django admin uses the same principle except that
|
||||
Django provides a lot of tools to automate the admin-generation for us.
|
||||
|
||||
Admin templates are found in `evennia/web/templates/admin/` but you'll find
|
||||
this is relatively empty. This is because most of the templates are just
|
||||
inherited directly from their original location in the Django package
|
||||
(`django/contrib/admin/templates/`). So if you wanted to override one you'd have
|
||||
to copy it from _there_ into your `mygame/templates/admin/` folder. Same is true
|
||||
for CSS files.
|
||||
|
||||
The admin site's backend code (the views) is found in `evennia/web/admin/`. It
|
||||
is organized into `admin`-classes, like `ObjectAdmin`, `AccountAdmin` etc.
|
||||
These automatically use the underlying database models to generate useful views
|
||||
for us without us havint go code the forms etc ourselves.
|
||||
|
||||
The top level `AdminSite` (the admin configuration referenced in django docs)
|
||||
is found in `evennia/web/utils/adminsite.py`.
|
||||
|
||||
|
||||
### Change the title of the admin
|
||||
|
||||
By default the admin's title is `Evennia web admin`. To change this, add the
|
||||
following to your `mygame/web/urls.py`:
|
||||
|
||||
```python
|
||||
# in mygame/web/urls.py
|
||||
|
||||
# ...
|
||||
|
||||
from django.conf.admin import site
|
||||
|
||||
#...
|
||||
|
||||
site.site_header = "My great game admin"
|
||||
|
||||
|
||||
```
|
||||
|
||||
Reload the server and the admin's title header will have changed.
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
# Web Client
|
||||
|
||||
Evennia comes with a MUD client accessible from a normal web browser. During development you can try
|
||||
it at `http://localhost:4001/webclient`. The client consists of several parts, all under
|
||||
`evennia/web`:
|
||||
|
||||
`templates/webclient/webclient.html` and `templates/webclient/base.html` are the very simplistic
|
||||
django html templates describing the webclient layout.
|
||||
|
||||
`static/webclient/js/evennia.js` is the main evennia javascript library. This handles all
|
||||
communication between Evennia and the client over websockets and via AJAX/COMET if the browser can't
|
||||
handle websockets. It will make the Evennia object available to the javascript namespace, which
|
||||
offers methods for sending and receiving data to/from the server transparently. This is intended to
|
||||
be used also if swapping out the gui front end.
|
||||
|
||||
`static/webclient/js/webclient_gui.js` is the default plugin manager. It adds the `plugins` and
|
||||
`plugin_manager` objects to the javascript namespace, coordinates the GUI operations between the
|
||||
various plugins, and uses the Evennia object library for all in/out.
|
||||
|
||||
`static/webclient/js/plugins` provides a default set of plugins that implement a "telnet-like"
|
||||
interface, and a couple of example plugins to show how you could implement new plugin features.
|
||||
|
||||
`static/webclient/css/webclient.css` is the CSS file for the client; it also defines things like how
|
||||
to display ANSI/Xterm256 colors etc.
|
||||
|
||||
The server-side webclient protocols are found in `evennia/server/portal/webclient.py` and
|
||||
`webclient_ajax.py` for the two types of connections. You can't (and should not need to) modify
|
||||
these.
|
||||
|
||||
## Customizing the web client
|
||||
|
||||
Like was the case for the website, you override the webclient from your game directory. You need to
|
||||
add/modify a file in the matching directory locations within your project's `mygame/web/` directories.
|
||||
These directories are NOT directly used by the web server when the game is running, the
|
||||
server copies everything web related in the Evennia folder over to `mygame/server/.static/` and then
|
||||
copies in all of your `mygame/web/` files. This can cause some cases were you edit a file, but it doesn't
|
||||
seem to make any difference in the servers behavior. **Before doing anything else, try shutting
|
||||
down the game and running `evennia collectstatic` from the command line then start it back up, clear
|
||||
your browser cache, and see if your edit shows up.**
|
||||
|
||||
Example: To change the list of in-use plugins, you need to override base.html by copying
|
||||
`evennia/web/templates/webclient/base.html` to
|
||||
`mygame/web/templates/webclient/base.html` and editing it to add your new plugin.
|
||||
|
||||
# Evennia Web Client API (from evennia.js)
|
||||
* `Evennia.init( opts )`
|
||||
* `Evennia.connect()`
|
||||
* `Evennia.isConnected()`
|
||||
* `Evennia.msg( cmdname, args, kwargs, callback )`
|
||||
* `Evennia.emit( cmdname, args, kwargs )`
|
||||
* `log()`
|
||||
|
||||
# Plugin Manager API (from webclient_gui.js)
|
||||
* `options` Object, Stores key/value 'state' that can be used by plugins to coordinate behavior.
|
||||
* `plugins` Object, key/value list of the all the loaded plugins.
|
||||
* `plugin_handler` Object
|
||||
* `plugin_handler.add("name", plugin)`
|
||||
* `plugin_handler.onSend(string)`
|
||||
|
||||
# Plugin callbacks API
|
||||
* `init()` -- The only required callback
|
||||
* `boolean onKeydown(event)` This plugin listens for Keydown events
|
||||
* `onBeforeUnload()` This plugin does something special just before the webclient page/tab is
|
||||
closed.
|
||||
* `onLoggedIn(args, kwargs)` This plugin does something when the webclient first logs in.
|
||||
* `onGotOptions(args, kwargs)` This plugin does something with options sent from the server.
|
||||
* `boolean onText(args, kwargs)` This plugin does something with messages sent from the server.
|
||||
* `boolean onPrompt(args, kwargs)` This plugin does something when the server sends a prompt.
|
||||
* `boolean onUnknownCmd(cmdname, args, kwargs)` This plugin does something with "unknown commands".
|
||||
* `onConnectionClose(args, kwargs)` This plugin does something when the webclient disconnects from
|
||||
the server.
|
||||
* `newstring onSend(string)` This plugin examines/alters text that other plugins generate. **Use
|
||||
with caution**
|
||||
|
||||
The order of the plugins defined in `base.html` is important. All the callbacks for each plugin
|
||||
will be executed in that order. Functions marked "boolean" above must return true/false. Returning
|
||||
true will short-circuit the execution, so no other plugins lower in the base.html list will have
|
||||
their callback for this event called. This enables things like the up/down arrow keys for the
|
||||
history.js plugin to always occur before the default_in.js plugin adds that key to the current input
|
||||
buffer.
|
||||
|
||||
# Example/Default Plugins (plugins/*.js)
|
||||
* `clienthelp.js` Defines onOptionsUI from the options2 plugin. This is a mostly empty plugin to
|
||||
add some "How To" information for your game.
|
||||
* `default_in.js` Defines onKeydown. <enter> key or mouse clicking the arrow will send the currently
|
||||
typed text.
|
||||
* `default_out.js` Defines onText, onPrompt, and onUnknownCmd. Generates HTML output for the user.
|
||||
* `default_unload.js` Defines onBeforeUnload. Prompts the user to confirm that they meant to
|
||||
leave/close the game.
|
||||
* `font.js` Defines onOptionsUI. The plugin adds the ability to select your font and font size.
|
||||
* `goldenlayout_default_config.js` Not actually a plugin, defines a global variable that
|
||||
goldenlayout uses to determine its window layout, known tag routing, etc.
|
||||
* `goldenlayout.js` Defines onKeydown, onText and custom functions. A very powerful "tabbed" window
|
||||
manager for drag-n-drop windows, text routing and more.
|
||||
* `history.js` Defines onKeydown and onSend. Creates a history of past sent commands, and uses arrow
|
||||
keys to peruse.
|
||||
* `hotbuttons.js` Defines onGotOptions. A Disabled-by-default plugin that defines a button bar with
|
||||
user-assignable commands.
|
||||
* `html.js` A basic plugin to allow the client to handle "raw html" messages from the server, this
|
||||
allows the server to send native HTML messages like >div style='s'<styled text>/div<
|
||||
* `iframe.js` Defines onOptionsUI. A goldenlayout-only plugin to create a restricted browsing sub-
|
||||
window for a side-by-side web/text interface, mostly an example of how to build new HTML
|
||||
"components" for goldenlayout.
|
||||
* `message_routing.js` Defines onOptionsUI, onText, onKeydown. This goldenlayout-only plugin
|
||||
implements regex matching to allow users to "tag" arbitrary text that matches, so that it gets
|
||||
routed to proper windows. Similar to "Spawn" functions for other clients.
|
||||
* `multimedia.js` An basic plugin to allow the client to handle "image" "audio" and "video" messages
|
||||
from the server and display them as inline HTML.
|
||||
* `notifications.js` Defines onText. Generates browser notification events for each new message
|
||||
while the tab is hidden.
|
||||
* `oob.js` Defines onSend. Allows the user to test/send Out Of Band json messages to the server.
|
||||
* `options.js` Defines most callbacks. Provides a popup-based UI to coordinate options settings with the server.
|
||||
* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the options/settings tab.
|
||||
Integrates with other plugins via the custom onOptionsUI callback.
|
||||
* `popups.js` Provides default popups/Dialog UI for other plugins to use.
|
||||
* `text2html.js` Provides a new message handler type: `text2html`, similar to the multimedia and html
|
||||
plugins. This plugin provides a way to offload rendering the regular pipe-styled ASCII messages
|
||||
to the client. This allows the server to do less work, while also allowing the client a place to
|
||||
customize this conversion process. To use this plugin you will need to override the current commands
|
||||
in Evennia, changing any place where a raw text output message is generated and turn it into a
|
||||
`text2html` message. For example: `target.msg("my text")` becomes: `target.msg(text2html=("my text"))`
|
||||
(even better, use a webclient pane routing tag: `target.msg(text2html=("my text", {"type": "sometag"}))`)
|
||||
`text2html` messages should format and behave identically to the server-side generated text2html() output.
|
||||
|
||||
# A side note on html messages vrs text2html messages
|
||||
|
||||
So...lets say you have a desire to make your webclient output more like standard webpages...
|
||||
For telnet clients, you could collect a bunch of text lines together, with ASCII formatted borders, etc.
|
||||
Then send the results to be rendered client-side via the text2html plugin.
|
||||
|
||||
But for webclients, you could format a message directly with the html plugin to render the whole thing as an
|
||||
HTML table, like so:
|
||||
```
|
||||
# Server Side Python Code:
|
||||
|
||||
if target.is_webclient():
|
||||
# This can be styled however you like using CSS, just add the CSS file to web/static/webclient/css/...
|
||||
table = [
|
||||
"<table>",
|
||||
"<tr><td>1</td><td>2</td><td>3</td></tr>",
|
||||
"<tr><td>4</td><td>5</td><td>6</td></tr>",
|
||||
"</table>"
|
||||
]
|
||||
target.msg( html=( "".join(table), {"type": "mytag"}) )
|
||||
else:
|
||||
# This will use the client to render this as "plain, simple" ASCII text, the same
|
||||
# as if it was rendered server-side via the Portal's text2html() functions
|
||||
table = [
|
||||
"#############",
|
||||
"# 1 # 2 # 3 #",
|
||||
"#############",
|
||||
"# 4 # 5 # 6 #",
|
||||
"#############"
|
||||
]
|
||||
target.msg( html2html=( "\n".join(table), {"type": "mytag"}) )
|
||||
```
|
||||
|
||||
# Writing your own Plugins
|
||||
|
||||
So, you love the functionality of the webclient, but your game has specific
|
||||
types of text that need to be separated out into their own space, visually.
|
||||
The Goldenlayout plugin framework can help with this.
|
||||
|
||||
## GoldenLayout
|
||||
|
||||
GoldenLayout is a web framework that allows web developers and their users to create their own
|
||||
tabbed/windowed layouts. Windows/tabs can be click-and-dragged from location to location by
|
||||
clicking on their titlebar and dragging until the "frame lines" appear. Dragging a window onto
|
||||
another window's titlebar will create a tabbed "Stack". The Evennia goldenlayout plugin defines 3
|
||||
basic types of window: The Main window, input windows and non-main text output windows. The Main
|
||||
window and the first input window are unique in that they can't be "closed".
|
||||
|
||||
The most basic customization is to provide your users with a default layout other than just one Main
|
||||
output and the one starting input window. This is done by modifying your server's
|
||||
goldenlayout_default_config.js.
|
||||
|
||||
Start by creating a new
|
||||
`mygame/web/static/webclient/js/plugins/goldenlayout_default_config.js` file, and adding
|
||||
the following JSON variable:
|
||||
|
||||
```
|
||||
var goldenlayout_config = {
|
||||
content: [{
|
||||
type: 'column',
|
||||
content: [{
|
||||
type: 'row',
|
||||
content: [{
|
||||
type: 'column',
|
||||
content: [{
|
||||
type: 'component',
|
||||
componentName: 'Main',
|
||||
isClosable: false,
|
||||
tooltip: 'Main - drag to desired position.',
|
||||
componentState: {
|
||||
cssClass: 'content',
|
||||
types: 'untagged',
|
||||
updateMethod: 'newlines',
|
||||
},
|
||||
}, {
|
||||
type: 'component',
|
||||
componentName: 'input',
|
||||
id: 'inputComponent',
|
||||
height: 10,
|
||||
tooltip: 'Input - The last input in the layout is always the default.',
|
||||
}, {
|
||||
type: 'component',
|
||||
componentName: 'input',
|
||||
id: 'inputComponent',
|
||||
height: 10,
|
||||
isClosable: false,
|
||||
tooltip: 'Input - The last input in the layout is always the default.',
|
||||
}]
|
||||
},{
|
||||
type: 'column',
|
||||
content: [{
|
||||
type: 'component',
|
||||
componentName: 'evennia',
|
||||
componentId: 'evennia',
|
||||
title: 'example',
|
||||
height: 60,
|
||||
isClosable: false,
|
||||
componentState: {
|
||||
types: 'some-tag-here',
|
||||
updateMethod: 'newlines',
|
||||
},
|
||||
}, {
|
||||
type: 'component',
|
||||
componentName: 'evennia',
|
||||
componentId: 'evennia',
|
||||
title: 'sheet',
|
||||
isClosable: false,
|
||||
componentState: {
|
||||
types: 'sheet',
|
||||
updateMethod: 'replace',
|
||||
},
|
||||
}],
|
||||
}],
|
||||
}]
|
||||
}]
|
||||
};
|
||||
```
|
||||
This is a bit ugly, but hopefully, from the indentation, you can see that it creates a side-by-side
|
||||
(2-column) interface with 3 windows down the left side (The Main and 2 inputs) and a pair of windows
|
||||
on the right side for extra outputs. Any text tagged with "some-tag-here" will flow to the bottom
|
||||
of the "example" window, and any text tagged "sheet" will replace the text already in the "sheet"
|
||||
window.
|
||||
|
||||
Note: GoldenLayout gets VERY confused and will break if you create two windows with the "Main"
|
||||
componentName.
|
||||
|
||||
Now, let's say you want to display text on each window using different CSS. This is where new
|
||||
goldenlayout "components" come in. Each component is like a blueprint that gets stamped out when
|
||||
you create a new instance of that component, once it is defined, it won't be easily altered. You
|
||||
will need to define a new component, preferably in a new plugin file, and then add that into your
|
||||
page (either dynamically to the DOM via javascript, or by including the new plugin file into the
|
||||
base.html).
|
||||
|
||||
First up, follow the directions in Customizing the Web Client section above to override the
|
||||
base.html.
|
||||
|
||||
Next, add the new plugin to your copy of base.html:
|
||||
```
|
||||
<script src={% static "webclient/js/plugins/myplugin.js" %} language="javascript"
|
||||
type="text/javascript"></script>
|
||||
```
|
||||
Remember, plugins are load-order dependent, so make sure the new `<script>` tag comes before the
|
||||
goldenlayout.js
|
||||
|
||||
Next, create a new plugin file `mygame/web/static/webclient/js/plugins/myplugin.js` and
|
||||
edit it.
|
||||
|
||||
```
|
||||
let myplugin = (function () {
|
||||
//
|
||||
//
|
||||
var postInit = function() {
|
||||
var myLayout = window.plugins['goldenlayout'].getGL();
|
||||
|
||||
// register our component and replace the default messagewindow
|
||||
myLayout.registerComponent( 'mycomponent', function (container, componentState) {
|
||||
let mycssdiv = $('<div>').addClass('myCSS');
|
||||
mycssdiv.attr('types', 'mytag');
|
||||
mycssdiv.attr('update_method', 'newlines');
|
||||
mycssdiv.appendTo( container.getElement() );
|
||||
});
|
||||
|
||||
console.log("MyPlugin Initialized.");
|
||||
}
|
||||
|
||||
return {
|
||||
init: function () {},
|
||||
postInit: postInit,
|
||||
}
|
||||
})();
|
||||
window.plugin_handler.add("myplugin", myplugin);
|
||||
```
|
||||
You can then add "mycomponent" to an item's componentName in your goldenlayout_default_config.js.
|
||||
|
||||
Make sure to stop your server, evennia collectstatic, and restart your server. Then make sure to clear your browser cache before loading the webclient page.
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
# Webserver
|
||||
|
||||
When Evennia starts it also spins up its own Twisted-based web server. The
|
||||
webserver is responsible for serving the html pages of the game's website. It
|
||||
can also serve static resources like images and music.
|
||||
|
||||
The webclient runs as part of the [Server](./Portal-And-Server.md) process of
|
||||
Evennia. This means that it can directly access cached objects modified
|
||||
in-game, and there is no risk of working with objects that are temporarily
|
||||
out-of-sync in the database.
|
||||
|
||||
The webserver runs on Twisted and is meant to be used in a production
|
||||
environment. It leverages the Django web framework and provides:
|
||||
|
||||
- A [Game Website](./Website.md) - this is what you see when you go to
|
||||
`localhost:4001`. The look of the website is meant to be customized to your
|
||||
game. Users logged into the website will be auto-logged into the game if they
|
||||
do so with the webclient since they share the same login credentials (there
|
||||
is no way to safely do auto-login with telnet clients).
|
||||
- The [Web Admin](./Web-Admin.md) is based on the Django web admin and allows you to
|
||||
edit the game database in a graphical interface.
|
||||
- The [Webclient](./Webclient.md) page is served by the webserver, but the actual
|
||||
game communication (sending/receiving data) is done by the javascript client
|
||||
on the page opening a websocket connection directly to Evennia's Portal.
|
||||
- The [Evennia REST-API](./Web-API.md) allows for accessing the database from outside the game
|
||||
(only if `REST_API_ENABLED=True).
|
||||
|
||||
|
||||
## Basic Webserver data flow
|
||||
|
||||
1. A user enters an url in their browser (or clicks a button). This leads to
|
||||
the browser sending a _HTTP request_ to the server containing an url-path
|
||||
(like for `https://localhost:4001/`, the part of the url we need to consider
|
||||
`/`). Other possibilities would be `/admin/`, `/login/`, `/channels/` etc.
|
||||
2. evennia (through Django) will make use of the regular expressions registered
|
||||
in the `urls.py` file. This acts as a rerouter to _views_, which are
|
||||
regular Python functions or callable classes able to process the incoming
|
||||
request (think of these as similar to the right Evennia Command being
|
||||
selected to handle your input - views are like Commands in this sense). In
|
||||
the case of `/` we reroute to a view handling the main index-page of the
|
||||
website.
|
||||
3. The view code will prepare all the data needed by the web page. For the default
|
||||
index page, this means gather the game statistics so you can see how many
|
||||
are currently connected to the game etc.
|
||||
4. The view will next fetch a _template_. A template is a HTML-document with special
|
||||
'placeholder' tags (written as `{{...}}` or `{% ... %}` usually). These
|
||||
placeholders allow the view to inject dynamic content into the HTML and make
|
||||
the page customized to the current situation. For the index page, it means
|
||||
injecting the current player-count in the right places of the html page. This
|
||||
is called 'rendering' the template. The result is a complete HTML page.
|
||||
5. (The view can also pull in a _form_ to customize user-input in a similar way.)
|
||||
6. The finished HTML page is packed into a _HTTP response_ and returned to the
|
||||
web browser, which can now display the page!
|
||||
|
||||
### A note on the webclient
|
||||
|
||||
The web browser can also execute code directly without talking to the Server.
|
||||
This code must be written/loaded into the web page and is written using the
|
||||
Javascript programming language (there is no way around this, it is what web
|
||||
browsers understand). Executing Javascript is something the web browser does,
|
||||
it operates independently from Evennia. Small snippets of javascript can be
|
||||
used on a page to have buttons react, make small animations etc that doesn't
|
||||
require the server.
|
||||
|
||||
In the case of the [Webclient](./Webclient.md), Evennia will load the Webclient page
|
||||
as above, but the page then initiates Javascript code (a lot of it) responsible
|
||||
for actually displaying the client GUI, allows you to resize windows etc.
|
||||
|
||||
After it starts, the webclient 'calls home' and spins up a
|
||||
[websocket](https://en.wikipedia.org/wiki/WebSocket) link to the Evennia Portal - this
|
||||
is how all data is then exchanged. So after the initial loading of the
|
||||
webclient page, the above sequence doesn't happen again until close the tab and
|
||||
come back or you reload it manually in your browser.
|
||||
|
|
@ -1,420 +0,0 @@
|
|||
# Game website
|
||||
|
||||
When Evennia starts it will also start a [Webserver](./Webserver.md) as part of the
|
||||
[Server](./Portal-And-Server.md) process. This uses [Django](https://docs.djangoproject.com)
|
||||
to present a simple but functional default game website. With the default setup,
|
||||
open your browser to [localhost:4001](http://localhost:4001) or [127.0.0.1:4001](http://127.0.0.1:4001)
|
||||
to see it.
|
||||
|
||||
The website allows existing players to log in using an account-name and
|
||||
password they previously used to register with the game. If a user logs in with
|
||||
the [Webclient](./Webclient.md) they will also log into the website and vice-versa.
|
||||
So if you are logged into the website, opening the webclient will automatically
|
||||
log you into the game as that account.
|
||||
|
||||
The default website shows a "Welcome!" page with a few links to useful
|
||||
resources. It also shows some statistics about how many players are currently
|
||||
connected.
|
||||
|
||||
In the top menu you can find
|
||||
- _Home_ - Get back to front page.
|
||||
- _Documentation_ - A link to the latest stable Evennia documentation.
|
||||
- _Characters_ - This is a demo of connecting in-game characters to the website.
|
||||
It will display a list of all entities of the
|
||||
_typeclasses.characters.Character` typeclass and allow you to view their
|
||||
description with an optional image. The list is only available to logged-in
|
||||
users.
|
||||
- _Channels_ - This is a demo of connecting in-game chats to the website. It will
|
||||
show a list of all channels available to you and allow you to view the latest
|
||||
discussions. Most channels require logging in, but the `Public` channel can
|
||||
also be viewed by non-loggedin users.
|
||||
- _Help_ - This ties the in-game [Help system](./Help-System.md) to the website. All
|
||||
database-based help entries that are publicly available or accessible to your
|
||||
account can be read. This is a good way to present a body of help for people
|
||||
to read outside of the game.
|
||||
- _Play Online_ - This opens the [Webclient](./Webclient.md) in the browser.
|
||||
- _Admin_ The [Web admin](Web admin) will only show if you are logged in.
|
||||
- _Log in/out_ - Allows you to authenticate using the same credentials you use
|
||||
in the game.
|
||||
- _Register_ - Allows you to register a new account. This is the same as
|
||||
creating a new account upon first logging into the game).
|
||||
|
||||
## Modifying the default Website
|
||||
|
||||
You can modify and override all aspects of the web site from your game dir.
|
||||
You'll mostly be doing so in your settings file
|
||||
(`mygame/server/conf/settings.py` and in the gamedir's `web/folder`
|
||||
(`mygame/web/` if your game folder is `mygame/`).
|
||||
|
||||
> When testing your modifications, it's a good idea to add `DEBUG = True` to
|
||||
> your settings file. This will give you nice informative tracebacks directly
|
||||
> in your browser instead of generic 404 or 500 error pages. Just remember that
|
||||
> DEBUG mode leaks memory (for retaining debug info) and is *not* safe to use
|
||||
> for a production game!
|
||||
|
||||
As explained on the [Webserver](./Webserver.md) page, the process for getting a web
|
||||
page is
|
||||
|
||||
1. Web browser sends HTTP request to server with an URL
|
||||
2. `urls.py` uses regex to match that URL to a _view_ (a Python function or callable class).
|
||||
3. The correct Python view is loaded and executes.
|
||||
4. The view pulls in a _template_, a HTML document with placeholder markers in it,
|
||||
and fills those in as needed (it may also use a _form_ to customize user-input in the same way).
|
||||
A HTML page may also in turn point to static resources (usually CSS, sometimes images etc).
|
||||
5. The rendered HTML page is returned to the browser as a HTTP response. If
|
||||
the HTML page requires static resources are requested, the browser will
|
||||
fetch those separately before displaying it to the user.
|
||||
|
||||
If you look at the [evennia/web/](github:develop/evennia/web) directory you'll find the following
|
||||
structure (leaving out stuff not relevant to the website):
|
||||
|
||||
```
|
||||
evennia/web/
|
||||
...
|
||||
static/
|
||||
website/
|
||||
css/
|
||||
(css style files)
|
||||
images/
|
||||
(images to show)
|
||||
|
||||
templates/
|
||||
website/
|
||||
(html files)
|
||||
|
||||
website/
|
||||
urls.py
|
||||
views/
|
||||
(python files related to website)
|
||||
|
||||
urls.py
|
||||
|
||||
```
|
||||
|
||||
The top-level `web/urls.py` file 'includes' the `web/website/urls.py` file -
|
||||
that way all the website-related url-handling is kept in the same place.
|
||||
|
||||
This is the layout of the `mygame/web/` folder relevant for the website:
|
||||
|
||||
```
|
||||
mygame/web/
|
||||
...
|
||||
static/
|
||||
website/
|
||||
css/
|
||||
images/
|
||||
|
||||
templates/
|
||||
website/
|
||||
|
||||
website/
|
||||
urls.py
|
||||
views/
|
||||
|
||||
urls.py
|
||||
|
||||
```
|
||||
|
||||
```{versionchanged} 1.0
|
||||
|
||||
Game folders created with older versions of Evennia will lack most of this
|
||||
convenient `mygame/web/` layout. If you use a game dir from an older version,
|
||||
you should copy over the missing `evennia/game_template/web/` folders from
|
||||
there, as well as the main urls.py file.
|
||||
|
||||
```
|
||||
|
||||
As you can see, the `mygame/web/` folder is a copy of the `evennia/web/` folder
|
||||
structure except the `mygame` folders are mostly empty.
|
||||
|
||||
For static- and template-files, Evennia will _first_
|
||||
look in `mygame/static` and `mygame/templates` before going to the default
|
||||
locations in `evennia/web/`. So override these resources, you just need to put
|
||||
a file with the same name in the right spot under `mygame/web/` (and then
|
||||
reload the server). Easiest is often to copy the original over and modify it.
|
||||
|
||||
Overridden views (Python modules) also need an additional tweak to the
|
||||
`website/urls.py` file - you must make sure to repoint the url to the new
|
||||
version rather than it using the original.
|
||||
|
||||
## Examples of commom web changes
|
||||
|
||||
```{important}
|
||||
|
||||
Django is a very mature web-design framework. There are endless
|
||||
internet-tutorials, courses and books available to explain how to use Django.
|
||||
So these examples only serve as a first primer to get you started.
|
||||
|
||||
```
|
||||
|
||||
### Change Title and blurb
|
||||
|
||||
The website's title and blurb are simply changed by tweaking
|
||||
`settings.SERVERNAME` and `settings.GAME_SLOGAN`. Your settings file is in
|
||||
`mygame/server/conf/settings.py`, just set/add
|
||||
|
||||
SERVERNAME = "My Awesome Game"
|
||||
GAME_SLOGAN = "The best game in the world"
|
||||
|
||||
### Change the Logo
|
||||
|
||||
The Evennia googly-eyed snake logo is probably not what you want for your game.
|
||||
The template looks for a file `web/static/website/images/evennia_logo.png`. Just
|
||||
plop your own PNG logo (64x64 pixels large) in there and name it the same.
|
||||
|
||||
|
||||
### Change front page HTML
|
||||
|
||||
The front page of the website is usually referred to as the 'index' in HTML
|
||||
parlance.
|
||||
|
||||
The frontpage template is found in `evennia/web/templates/website/index.html`.
|
||||
Just copy this to the equivalent place in `mygame/web/`. Modify it there and
|
||||
reload the server to see your changes.
|
||||
|
||||
Django templates has a few special features that separate them from normal HTML
|
||||
documents - they contain a special templating language marked with `{% ... %}` and
|
||||
`{{ ... }}`.
|
||||
|
||||
Some important things to know:
|
||||
|
||||
- `{% extends "base.html" %}` - This is equivalent to a Python
|
||||
`from othermodule import *` statement, but for templates. It allows a given template
|
||||
to use everything from the imported (extended) template, but also to override anything
|
||||
it wants to change. This makes it easy to keep all pages looking the same and avoids
|
||||
a lot of boiler plate.
|
||||
- `{% block blockname %}...{% endblock %}` - Blocks are inheritable, named pieces of code
|
||||
that are modified in one place and then used elsewhere. This works a bit in reverse to
|
||||
normal inheritance, because it's commonly in such a way that `base.html` defines an empty
|
||||
block, let's say `contents`: `{% block contents %}{% endblock %}` but makes sure to put
|
||||
that _in the right place_, say in the main body, next to the sidebar etc. Then each page
|
||||
does `{% extends "base.html %"}` and makes their own `{% block contents} <actual content> {% endblock %}`.
|
||||
Their `contents` block will now override the empty one in `base.html` and appear in the right
|
||||
place in the document, without the extending template having to specifying everything else
|
||||
around it!
|
||||
- `{{ ... }}` are 'slots' usually embedded inside HTML tags or content. They reference a
|
||||
_context_ (basically a dict) that the Python _view_ makes available to it.
|
||||
Keys on the context are accessed with dot-notation, so if you provide a
|
||||
context `{"stats": {"hp": 10, "mp": 5}}` to your template, you could access
|
||||
that as `{{ stats.hp }}` to display `10` at that location to display `10` at
|
||||
that location.
|
||||
|
||||
This allows for template inheritance (making it easier to make all
|
||||
pages look the same without rewriting the same thing over and over)
|
||||
|
||||
There's a lot more information to be found in the [Django template language documentation](https://docs.djangoproject.com/en/3.2/ref/templates/language/).
|
||||
|
||||
### Change webpage colors and styling
|
||||
|
||||
You can tweak the [CSS](https://en.wikipedia.org/wiki/Cascading_Style_Sheets) of the entire
|
||||
website. If you investigate the `evennia/web/templates/website/base.html` file you'll see that we
|
||||
use the [Bootstrap
|
||||
4](https://getbootstrap.com/docs/4.6/getting-started/introduction/) toolkit.
|
||||
|
||||
Much structural HTML functionality is actually coming from bootstrap, so you
|
||||
will often be able to just add bootstrap CSS classes to elements in the HTML
|
||||
file to get various effects like text-centering or similar.
|
||||
|
||||
The website's custom CSS is found in
|
||||
`evennia/web/static/website/css/website.css` but we also look for a (currently
|
||||
empty) `custom.css` in the same location. You can override either, but it may
|
||||
be easier to revert your changes if you only add things to `custom.css`.
|
||||
|
||||
Copy the CSS file you want to modify to the corresponding location in `mygame/web`.
|
||||
Modify it and reload the server to see your changes.
|
||||
|
||||
You can also apply static files without reloading, but running this in the
|
||||
terminal:
|
||||
|
||||
evennia collectstatic --no-input
|
||||
|
||||
(this is run automatically when reloading the server).
|
||||
|
||||
> Note that before you see new CSS files applied you may need to refresh your
|
||||
> browser without cache (Ctrl-F5 in Firefox, for example).
|
||||
|
||||
As an example, add/copy `custom.css` to `mygame/web/static/website/css/` and
|
||||
add the following:
|
||||
|
||||
|
||||
```css
|
||||
|
||||
.navbar {
|
||||
background-color: #7a3d54;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #7a3d54;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Reload and your website now has a red theme!
|
||||
|
||||
> Hint: Learn to use your web browser's [Developer tools](https://torquemag.io/2020/06/browser-developer-tools-tutorial/).
|
||||
> These allow you to tweak CSS 'live' to find a look you like and copy it into
|
||||
> the .css file only when you want to make the changes permanent.
|
||||
|
||||
|
||||
### Change front page functionality
|
||||
|
||||
The logic is all in the view. To find where the index-page view is found, we
|
||||
look in `evennia/web/website/urls.py`. Here we find the following line:
|
||||
|
||||
```python
|
||||
# in evennia/web/website/urls.py
|
||||
|
||||
...
|
||||
# website front page
|
||||
path("", index.EvenniaIndexView.as_view(), name="index"),
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
The first `""` is the empty url - root - what you get if you just enter `localhost:4001/`
|
||||
with no extra path. As expected, this leads to the index page. By looking at the imports
|
||||
we find the view is in in `evennia/web/website/views/index.py`.
|
||||
|
||||
Copy this file to the corresponding location in `mygame/web`. Then tweak your `mygame/web/website/urls.py`
|
||||
file to point to the new file:
|
||||
|
||||
```python
|
||||
# in mygame/web/website/urls.py
|
||||
|
||||
# ...
|
||||
|
||||
from web.website.views import index
|
||||
|
||||
urlpatterns = [
|
||||
path("", index.EvenniaIndexView.as_view(), name="index")
|
||||
|
||||
]
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
So we just import `index` from the new location and point to it. After a reload
|
||||
the front page will now redirect to use your copy rather than the original.
|
||||
|
||||
The frontpage view is a class `EvenniaIndexView`. This is a [Django class-based view](https://docs.djangoproject.com/en/3.2/topics/class-based-views/).
|
||||
It's a little less visible what happens in a class-based view than in a function (since
|
||||
the class implements a lot of functionality as methods), but it's powerful and
|
||||
much easier to extend/modify.
|
||||
|
||||
The class property `template_name` sets the location of the template used under
|
||||
the `templates/` folder. So `website/index.html` points to
|
||||
`web/templates/website/index.html` (as we already explored above.
|
||||
|
||||
The `get_context_data` is a convenient method for providing the context for the
|
||||
template. In the index-page's case we want the game stats (number of recent
|
||||
players etc). These are then made available to use in `{{ ... }}` slots in the
|
||||
template as described in the previous section.
|
||||
|
||||
### Change other website pages
|
||||
|
||||
The other sub pages are handled in the same way - copy the template or static
|
||||
resource to the right place, or copy the view and repoint your `website/urls.py` to
|
||||
your copy. Just remember to reload.
|
||||
|
||||
## Adding a new web page
|
||||
|
||||
### Using Flat Pages
|
||||
|
||||
The absolutely simplest way to add a new web page is to use the `Flat Pages`
|
||||
app available in the [Web Admin](./Web-Admin.md). The page will appear with the same
|
||||
styling as the rest of the site.
|
||||
|
||||
For the `Flat pages` module to work you must first set up a _Site_ (or
|
||||
domain) to use. You only need to this once.
|
||||
|
||||
- Go to the Web admin and select `Sites`. If your
|
||||
game is at `mygreatgame.com`, that's the domain you need to add. For local
|
||||
experimentation, add the domain `localhost:4001`. Note the `id` of the domain
|
||||
(look at the url when you click on the new domain, if it's for example
|
||||
`http://localhost:4001/admin/sites/site/2/change/`, then the id is `2`).
|
||||
- Now add the line `SITE_ID = <id>` to your settings file.
|
||||
|
||||
Next you create new pages easily.
|
||||
|
||||
- Go the `Flat Pages` web admin and choose to add a new flat page.
|
||||
- Set the url. If you want the page to appear as e.g. `localhost:4001/test/`, then
|
||||
add `/test/` here. You need to add both leading and trailing slashes.
|
||||
- Set `Title` to the name of the page.
|
||||
- The `Content` is the HTML content of the body of the page. Go wild!
|
||||
- Finally pick the `Site` you made before, and save.
|
||||
- (in the advanced section you can make it so that you have to login to see the page etc).
|
||||
|
||||
You can now go to `localhost:4001/test/` and see your new page!
|
||||
|
||||
### Add Custom new page
|
||||
|
||||
The `Flat Pages` page doesn't allow for (much) dynamic content and customization. For
|
||||
this you need to add the needed components yourself.
|
||||
|
||||
Let's see how to make a `/test/` page from scratch.
|
||||
|
||||
- Add a new `test.html` file under `mygame/web/templates/website/`. Easiest is to base
|
||||
this off an existing file. Make sure to `{% extend base.html %}` if you want to
|
||||
get the same styling as the rest of your site.
|
||||
- Add a new view `testview.py` under `mygame/web/website/views/` (don't name it `test.py` or
|
||||
Django/Evennia will think it contains unit tests). Add a view there to process
|
||||
your page. This is a minimal view to start from (read much more [in the Django docs](https://docs.djangoproject.com/en/3.2/topics/class-based-views/)):
|
||||
|
||||
```python
|
||||
# mygame/web/website/views/testview.py
|
||||
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
class MyTestView(TemplateView):
|
||||
template_name = "website/test.html"
|
||||
|
||||
|
||||
```
|
||||
|
||||
- Finally, point to your view from the `mygame/web/website/urls.py`:
|
||||
|
||||
```python
|
||||
# in mygame/web/website/urls.py
|
||||
|
||||
# ...
|
||||
from web.website.views import testview
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
# we can skip the initial / here
|
||||
path("test/", testview.MyTestView.as_view())
|
||||
]
|
||||
|
||||
```
|
||||
- Reload the server and your new page is available. You can now continue to add
|
||||
all sorts of advanced dynamic content through your view and template!
|
||||
|
||||
|
||||
## User forms
|
||||
|
||||
All the pages created so far deal with _presenting_ information to the user.
|
||||
It's also possible for the user to _input_ data on the page through _forms_. An
|
||||
example would be a page of fields and sliders you fill in to create a
|
||||
character, with a big 'Submit' button at the bottom.
|
||||
|
||||
Firstly, this must be represented in HTML. The `<form> ... </form>` is a
|
||||
standard HTML element you need to add to your template. It also has some other
|
||||
requirements, such as `<input>` and often Javascript components as well (but
|
||||
usually Django will help with this). If you are unfamiliar with how HTML forms
|
||||
work, [read about them here](https://docs.djangoproject.com/en/3.2/topics/forms/#html-forms).
|
||||
|
||||
The basic gist of it is that when you click to 'submit' the form, a POST HTML
|
||||
request will be sent to the server containing the data the user entered. It's
|
||||
now up to the server to make sure the data makes sense (validation) and then
|
||||
process the input somehow (like creating a new character).
|
||||
|
||||
On the backend side, we need to specify the logic for validating and processing
|
||||
the form data. This is done by the `Form` [Django class](https://docs.djangoproject.com/en/3.2/topics/forms/#forms-in-django).
|
||||
This specifies _fields_ on itself that define how to validate that piece of data.
|
||||
|
||||
The form is then linked into the view-class by adding `form_class = MyFormClass` to
|
||||
the view (next to `template_name`).
|
||||
|
||||
There are several example forms in `evennia/web/website/forms.py`. It's also a good
|
||||
idea to read [Building a form in Django](https://docs.djangoproject.com/en/3.2/topics/forms/#building-a-form-in-django)
|
||||
on the Django website - it covers all you need.
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
# 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](../Components/Commands.md) 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(f"The final value is {r}")
|
||||
|
||||
def at_err_function(e):
|
||||
self.caller.msg(f"There was an error: {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](../Howtos/Command-Duration.md).
|
||||
|
||||
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](https://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.
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
# 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:
|
||||
|
||||
accounts/delete 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](../Components/Locks.md).
|
||||
|
||||
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](../Components/Attributes.md) or [Tag](../Components/Tags.md), 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
|
||||
- **accounts/delete 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.
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
# Building Permissions
|
||||
|
||||
|
||||
*OBS: This gives only a brief introduction to the access system. Locks and permissions are fully
|
||||
detailed* [here](../Components/Locks.md).
|
||||
|
||||
## 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](../Components/Locks.md) 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.
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
# Sending different messages depending on viewpoint and receiver
|
||||
|
||||
Sending messages to everyong in a location is handled by the
|
||||
[msg_contents](evennia.objects.objects.DefaultObject.msg_contents) method on
|
||||
all [Objects](../Components/Objects.md). It's most commonly called on rooms.
|
||||
|
||||
```python
|
||||
room.msg_contents("Anna walks into the room.")
|
||||
```
|
||||
|
||||
You can also embed references in the string:
|
||||
|
||||
```python
|
||||
|
||||
room.msg_contents("{anna} walks into the room.",
|
||||
from_obj=caller,
|
||||
mapping={'anna': anna_object})
|
||||
```
|
||||
|
||||
Use `exclude=object_or_list_of_object` to skip sending the message one or more targets.
|
||||
|
||||
The advantage of this is that `anna_object.get_display_name(looker)` will be called
|
||||
for every onlooker; this allows the `{anna}` stanza to be different depending on who
|
||||
sees the strings. How this is to work depends on the _stance_ of your game.
|
||||
|
||||
The stance indicates how your game echoes its messages to the player. Knowing how you want to
|
||||
handle the stance is important for a text game. There are two main stances that are usually considered,
|
||||
_Actor stance_ and _Director stance_.
|
||||
|
||||
| Stance | You see | Others in the same location see |
|
||||
| --- | --- | --- |
|
||||
| Actor stance | You pick up the stone | Anna picks up the stone |
|
||||
|Director stance | Anna picks up the stone | Anna picks up the stone |
|
||||
|
||||
It's not unheard of to mix the two stances - with commands from the game being told
|
||||
in Actor stance while Director stance is used for complex emoting and roleplaying. One should
|
||||
usually try to be consistent however.
|
||||
|
||||
## Director Stance
|
||||
|
||||
While not so common as Actor stance, director stance has the advantage of simplicity, particularly
|
||||
in roleplaying MUDs where longer roleplaying emotes are used. It is also a pretty simple stance to
|
||||
implement technically since everyone sees the same text, regardless of viewpoint.
|
||||
|
||||
Here's an example of a flavorful text to show the room:
|
||||
|
||||
Tom picks up the gun, whistling to himself.
|
||||
|
||||
Everyone will see this string, both Tom and others. Here's how to send it to everyone in
|
||||
the room.
|
||||
|
||||
```python
|
||||
text = "Tom picks up the gun, whistling to himself."
|
||||
room.msg_contents(text)
|
||||
```
|
||||
|
||||
One may want to expand on it by making the name `Tom` be seen differently by different people,
|
||||
but the English grammar of the sentence does not change. Not only is this pretty easy to do
|
||||
technically, it's also easy to write for the player.
|
||||
|
||||
## Actor Stance
|
||||
|
||||
This means that the game addresses "you" when it does things. In actor stance, whenever you perform
|
||||
an action, you should get a different message than those _observing_ you doing that action.
|
||||
|
||||
Tom picks up the gun, whistling to himself.
|
||||
|
||||
This is what _others_ should see. The player themselves should see this:
|
||||
|
||||
You pick up the gun, whistling to yourself.
|
||||
|
||||
Not only do you need to map "Tom" to "You" above, there are also grammatical differences -
|
||||
"Tom walks" vs "You walk" and "himself" vs "yourself". This is a lot more complex to handle. For a
|
||||
developer making simple "You/Tom pick/picks up the stone" messages, you could in principle hand-craft
|
||||
the strings from every view point, but there's a better way.
|
||||
|
||||
The `msg_contents` method helps by parsing the ingoing string with a
|
||||
[FuncParser functions](../Components/FuncParser.md) with some very specific `$inline-functions`. The inline funcs
|
||||
basically provides you with a mini-language for building _one_ string that will change
|
||||
appropriately depending on who sees it.
|
||||
|
||||
|
||||
```python
|
||||
text = "$You() $conj(pick) up the gun, whistling to $pron(yourself)."
|
||||
room.msg_contents(text, from_obj=caller, mapping={"gun": gun_object})
|
||||
```
|
||||
|
||||
These are the inline-functions available:
|
||||
|
||||
- `$You()/$you()` - this is a reference to 'you' in the text. It will be replaced with "You/you" for
|
||||
the one sending the text and with the return from `caller.get_display_name(looker)` for everyone else.
|
||||
- `$conj(verb)` - this will conjugate the given verb depending on who sees the string (like `pick`
|
||||
to `picks`). Enter the root form of the verb.
|
||||
- `$pron(pronoun[,options])` - A pronoun is a word you want to use instead of a proper noun, like
|
||||
_him_, _herself_, _its_, _me_, _I_, _their_ and so on. The `options` is a space- or comma-separated
|
||||
set of options to help the system map your pronoun from 1st/2nd person to 3rd person and vice versa.
|
||||
See next section.
|
||||
|
||||
### More on $pron()
|
||||
|
||||
The `$pron()` inline func maps between 1st/2nd person (I/you) to 3rd person (he/she etc). In short,
|
||||
it translates between this table ...
|
||||
|
||||
| | Subject Pronoun | Object Pronoun | Possessive Adjective | Possessive Pronoun | Reflexive Pronoun |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| **1st person** | I | me | my | mine | myself |
|
||||
| **1st person plural** | we | us | our | ours | ourselves |
|
||||
| **2nd person** | you | you | your | yours | yourself |
|
||||
| **2nd person plural** | you | you | your | yours | yourselves |
|
||||
|
||||
... to this table (in both directions):
|
||||
|
||||
| | Subject Pronoun | Object Pronoun | Possessive Adjective | Possessive Pronoun | Reflexive Pronoun |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| **3rd person male** | he | him | his | his | himself |
|
||||
| **3rd person female** | she | her | her | hers | herself |
|
||||
| **3rd person neutral** | it | it | its | theirs* | itself |
|
||||
| **3rd person plural** | they | them | their | theirs | themselves |
|
||||
|
||||
> *) The neutral 3rd person possessive pronoun is not actually used in English. We set it to "theirs"
|
||||
> just to have something to show should someone accidentally ask for a neutral possessive pronoun.
|
||||
|
||||
Some mappings are easy. For example, if you write `$pron(yourselves)` then the 3rd-person
|
||||
form is always `themselves`. But because English grammar is the way it is, not all mappings
|
||||
are 1:1. For example, if you write
|
||||
`$pron(you)`, Evennia will not know which 3rd-persion equivalent this should map to - you need to
|
||||
provide more info to help out. This can either be provided as a second space-separated option
|
||||
to `$pron` or the system will try to figure it out on its own.
|
||||
|
||||
- `pronoun_type` - this is one of the columns in the table and can be set as a `$pron` option.
|
||||
|
||||
- `subject pronoun` (aliases `subject` or `sp`)
|
||||
- `object pronoun` (aliases `object` or `op`)
|
||||
- `possessive adjective` (aliases `adjective` or `pa`)
|
||||
- `possessive pronoun` (aliases `pronoun` or `pp`).
|
||||
|
||||
(There is no need to specify reflexive pronouns since they
|
||||
are all uniquely mapped 1:1). Speciying the pronoun-type is mainly needed when using `you`,
|
||||
since the same 'you' is used to represent all sorts of things in English grammar.
|
||||
If not specified and the mapping is not clear, a 'subject pronoun' (he/she/it/they) is assumed.
|
||||
- `gender` - set in `$pron` option as
|
||||
|
||||
- `male`, or `m`
|
||||
- `female'` or `f`
|
||||
- `neutral`, or `n`
|
||||
- `plural`, or `p` (yes plural is considered a 'gender' for this purpose).
|
||||
|
||||
If not set as an option the system will
|
||||
look for a callable or property `.gender` on the current `from_obj`. A callable will be called
|
||||
with no arguments and is expected to return a string 'male/female/neutral/plural'. If none
|
||||
is found, a neutral gender is assumed.
|
||||
- `viewpoint`- set in `$pron` option as
|
||||
|
||||
- `1st person` (aliases `1st` or `1`)
|
||||
- `2nd person` (aliases `2nd` or `2`)
|
||||
|
||||
This is only needed if you want to have 1st person perspective - if
|
||||
not, 2nd person is assumed wherever the viewpoint is unclear.
|
||||
|
||||
`$pron()` examples:
|
||||
|
||||
| Input | you see | others see | note |
|
||||
| --- | --- | ---| --- |
|
||||
| `$pron(I, male)` | I | he | |
|
||||
| `$pron(I, f)` | I | she | |
|
||||
| `$pron(my)` | my | its | figures out it's an possessive adjective, assumes neutral |
|
||||
| `$pron(you)` | you | it | assumes neutral subject pronoun |
|
||||
| `$pron(you, f)` | you | she | female specified, assumes subject pronoun |
|
||||
| `$pron(you,op f)` | you | her | |
|
||||
| `$pron(you,op p)` | you | them | |
|
||||
| `$pron(you, f op)` | you | her | specified female and objective pronoun|
|
||||
| `$pron(yourself)` | yourself | itself | |
|
||||
| `$pron(its)` | your | its | |
|
||||
| `$Pron(its)` | Your | Its | Using $Pron always capitalizes |
|
||||
| `$pron(her)` | you | her | 3rd person -> 2nd person |
|
||||
| `$pron(her, 1)` | I | her | 3rd person -> 1st person |
|
||||
| `$pron(its, 1st)` | my | its | 3rd person -> 1st person |
|
||||
|
||||
|
||||
Note the three last examples - instead of specifying the 2nd person form you
|
||||
can also specify the 3rd-person and do a 'reverse' lookup - you will still see the proper 1st/2nd text.
|
||||
So writing `$pron(her)` instead of `$pron(you, op f)` gives the same result.
|
||||
|
||||
The [$pron inlinefunc api is found here](evennia.utils.funcparser.funcparser_callable_pronoun)
|
||||
|
||||
# Referencing other objects
|
||||
|
||||
There is one more inlinefunc understood by `msg_contents`. This can be used natively to spruce up
|
||||
your strings (for both director- and actor stance):
|
||||
|
||||
- `$Obj(name)/$obj(name)` references another entity, which must be supplied
|
||||
in the `mapping` keyword argument to `msg_contents`. The object's `.get_display_name(looker)` will be
|
||||
called and inserted instead. This is essentially the same as using the `{anna}` marker we used
|
||||
in the first example at the top of this page, but using `$Obj/$obj` allows you to easily
|
||||
control capitalization.
|
||||
|
||||
This is used like so:
|
||||
|
||||
```python
|
||||
# director stance
|
||||
text = "Tom picks up the $obj(gun), whistling to himself"
|
||||
|
||||
# actor stance
|
||||
text = "$You() $conj(pick) up the $obj(gun), whistling to $pron(yourself)"
|
||||
|
||||
room.msg_contents(text, from_obj=caller, mapping={"gun": gun_object})
|
||||
```
|
||||
Depending on your game, Tom may now see himself picking up `A rusty old gun`, whereas an onlooker
|
||||
with a high gun smith skill may instead see him picking up `A rare-make Smith & Wesson model 686
|
||||
in poor condition" ...`
|
||||
|
||||
# Recog systems and roleplaying
|
||||
|
||||
The `$funcparser` inline functions are very powerful for the game developer, but they may
|
||||
be a bit too much to write for the regular player.
|
||||
|
||||
The [rpsystem contrib](evennia.contrib.rpg.rpsystem) implements a full dynamic emote/pose and recognition
|
||||
system with short-descriptions and disguises. It uses director stance with a custom markup
|
||||
language, like `/me` `/gun` and `/tall man` to refer to players and objects in the location. It can be
|
||||
worth checking out for inspiration.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Clickable links
|
||||
|
||||
Evennia supports clickable links for clients that supports it. This marks certain text so it can be
|
||||
clicked by a mouse and either trigger a given Evennia command, or open a URL in an external web
|
||||
browser. To support clickable links, Evennia requires the webclient or an third-party telnet client
|
||||
with [MXP](http://www.zuggsoft.com/zmud/mxp.htm) support (*Note: Evennia only supports clickable links, no other MXP features*).
|
||||
|
||||
- `|lc` to start the link, by defining the command to execute.
|
||||
- `|lu` to start the link, by defining the URL to open.
|
||||
- `|lt` to continue with the text to show to the user (the link text).
|
||||
- `|le` to end the link text and the link definition.
|
||||
|
||||
All elements must appear in exactly this order to make a valid link. For example,
|
||||
|
||||
```
|
||||
"If you go |lcnorth|ltto the north|le you will find a cottage."
|
||||
```
|
||||
|
||||
This will display as "If you go __to the north__ you will find a cottage." where clicking the link
|
||||
will execute the command `north`. If the client does not support clickable links, only the link text
|
||||
will be shown.
|
||||
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
# Colors
|
||||
|
||||
*Note that the Documentation does not display colour the way it would look on the screen.*
|
||||
|
||||
Color can be a very useful tool for your game. It can be used to increase readability and make your
|
||||
game more appealing visually.
|
||||
|
||||
Remember however that, with the exception of the webclient, you generally don't control the client
|
||||
used to connect to the game. There is, for example, one special tag meaning "yellow". But exactly
|
||||
*which* hue of yellow is actually displayed on the user's screen depends on the settings of their
|
||||
particular mud client. They could even swap the colours around or turn them off altogether if so
|
||||
desired. Some clients don't even support color - text games are also played with special reading
|
||||
equipment by people who are blind or have otherwise diminished eyesight.
|
||||
|
||||
So a good rule of thumb is to use colour to enhance your game but don't *rely* on it to display
|
||||
critical information. If you are coding the game, you can add functionality to let users disable
|
||||
colours as they please, as described [here](../Howtos/Manually-Configuring-Color.md).
|
||||
|
||||
To see which colours your client support, use the default `@color` command. This will list all
|
||||
available colours for ANSI and Xterm256 along with the codes you use for them. You can find a list
|
||||
of all the parsed `ANSI`-colour codes in `evennia/utils/ansi.py`.
|
||||
|
||||
## ANSI colours
|
||||
|
||||
Evennia supports the `ANSI` standard for text. This is by far the most supported MUD-color standard,
|
||||
available in all but the most ancient mud clients. The ANSI colours are **r**ed, **g**reen,
|
||||
**y**ellow, **b**lue, **m**agenta, **c**yan, **w**hite and black. They are abbreviated by their
|
||||
first letter except for black which is abbreviated with the letter **x**. In ANSI there are "bright"
|
||||
and "normal" (darker) versions of each color, adding up to a total of 16 colours to use for
|
||||
foreground text. There are also 8 "background" colours. These have no bright alternative in ANSI
|
||||
(but Evennia uses the [Xterm256](#xterm256-colours) extension behind the scenes to offer
|
||||
them anyway).
|
||||
|
||||
To colour your text you put special tags in it. Evennia will parse these and convert them to the
|
||||
correct markup for the client used. If the user's client/console/display supports ANSI colour, they
|
||||
will see the text in the specified colour, otherwise the tags will be stripped (uncolored text).
|
||||
This works also for non-terminal clients, such as the webclient. For the webclient, Evennia will
|
||||
translate the codes to HTML RGB colors.
|
||||
|
||||
Here is an example of the tags in action:
|
||||
|
||||
|rThis text is bright red.|n This is normal text.
|
||||
|RThis is a dark red text.|n This is normal text.
|
||||
|[rThis text has red background.|n This is normal text.
|
||||
|b|[yThis is bright blue text on yellow background.|n This is normal text.
|
||||
|
||||
- `|n` - this tag will turn off all color formatting, including background colors.
|
||||
- `|#`- markup marks the start of foreground color. The case defines if the text is "bright" or
|
||||
"normal". So `|g` is a bright green and `|G` is "normal" (darker) green.
|
||||
- `|[#` is used to add a background colour to the text. The case again specifies if it is "bright"
|
||||
or "normal", so `|[c` starts a bright cyan background and `|[C` a darker cyan background.
|
||||
- `|!#` is used to add foreground color without any enforced brightness/normal information.
|
||||
These are normal-intensity and are thus always given as uppercase, such as
|
||||
`|!R` for red. The difference between e.g. `|!R` and `|R` is that
|
||||
`|!R` will "inherit" the brightness setting from previously set color tags, whereas `|R` will
|
||||
always reset to the normal-intensity red. The `|#` format contains an implicit `|h`/`|H` tag in it:
|
||||
disabling highlighting when switching to a normal color, and enabling it for bright ones. So `|btest
|
||||
|!Rtest2` will result in a bright red `test2` since the brightness setting from `|b` "bleeds over".
|
||||
You could use this to for example quickly switch the intensity of a multitude of color tags. There
|
||||
is no background-color equivalent to `|!` style tags.
|
||||
- `|h` is used to make any following foreground ANSI colors bright (it has no effect on Xterm
|
||||
colors). This is only relevant to use with `|!` type tags and will be valid until the next `|n`,
|
||||
`|H` or normal (upper-case) `|#` tag. This tag will never affect background colors, those have to be
|
||||
set bright/normal explicitly. Technically, `|h|!G` is identical to `|g`.
|
||||
- `|H` negates the effects `|h` and returns all ANSI foreground colors (`|!` and `|` types) to
|
||||
'normal' intensity. It has no effect on background and Xterm colors.
|
||||
|
||||
> Note: The ANSI standard does not actually support bright backgrounds like `|[r` - the standard
|
||||
only supports "normal" intensity backgrounds. To get around this Evennia instead implements these
|
||||
as [Xterm256 colours](#xterm256-colours) behind the scenes. If the client does not support
|
||||
Xterm256 the ANSI colors will be used instead and there will be no visible difference between using
|
||||
upper- and lower-case background tags.
|
||||
|
||||
If you want to display an ANSI marker as output text (without having any effect), you need to escape
|
||||
it by preceding its `|` with another `|`:
|
||||
|
||||
```
|
||||
say The ||r ANSI marker changes text color to bright red.
|
||||
```
|
||||
|
||||
This will output the raw `|r` without any color change. This can also be necessary if you are doing
|
||||
ansi art that uses `|` with a letter directly following it.
|
||||
|
||||
Use the command
|
||||
|
||||
@color ansi
|
||||
|
||||
to get a list of all supported ANSI colours and the tags used to produce them.
|
||||
|
||||
A few additional ANSI codes are supported:
|
||||
|
||||
- `|/` A line break. You cannot put the normal Python `\n` line breaks in text entered inside the
|
||||
game (Evennia will filter this for security reasons). This is what you use instead: use the `|/`
|
||||
marker to format text with line breaks from the game command line.
|
||||
- `` This will translate into a `TAB` character. This will not always show (or show differently) to
|
||||
the client since it depends on their local settings. It's often better to use multiple spaces.
|
||||
- `|_` This is a space. You can usually use the normal space character, but if the space is *at the
|
||||
end of the line*, Evennia will likely crop it. This tag will not be cropped but always result in a
|
||||
space.
|
||||
- `|*` This will invert the current text/background colours. Can be useful to mark things (but see
|
||||
below).
|
||||
|
||||
### Caveats of `|*`
|
||||
|
||||
The `|*` tag (inverse video) is an old ANSI standard and should usually not be used for more than to
|
||||
mark short snippets of text. If combined with other tags it comes with a series of potentially
|
||||
confusing behaviors:
|
||||
|
||||
* The `|*` tag will only work once in a row:, ie: after using it once it won't have an effect again
|
||||
until you declare another tag. This is an example:
|
||||
|
||||
```
|
||||
Normal text, |*reversed text|*, still reversed text.
|
||||
```
|
||||
|
||||
that is, it will not reverse to normal at the second `|*`. You need to reset it manually:
|
||||
|
||||
```
|
||||
Normal text, |*reversed text|n, normal again.
|
||||
```
|
||||
|
||||
* The `|*` tag does not take "bright" colors into account:
|
||||
|
||||
```
|
||||
|RNormal red, |hnow brightened. |*BG is normal red.
|
||||
```
|
||||
|
||||
So `|*` only considers the 'true' foreground color, ignoring any highlighting. Think of the bright
|
||||
state (`|h`) as something like like `<strong>` in HTML: it modifies the _appearance_ of a normal
|
||||
foreground color to match its bright counterpart, without changing its normal color.
|
||||
* Finally, after a `|*`, if the previous background was set to a dark color (via `|[`), `|!#`) will
|
||||
actually change the background color instead of the foreground:
|
||||
|
||||
```
|
||||
|*reversed text |!R now BG is red.
|
||||
```
|
||||
For a detailed explanation of these caveats, see the [Understanding Color Tags](Understanding-Color-
|
||||
Tags) tutorial. But most of the time you might be better off to simply avoid `|*` and mark your text
|
||||
manually instead.
|
||||
|
||||
### Xterm256 Colours
|
||||
|
||||
The _Xterm256_ standard is a colour scheme that supports 256 colours for text and/or background.
|
||||
While this offers many more possibilities than traditional ANSI colours, be wary that too many text
|
||||
colors will be confusing to the eye. Also, not all clients support Xterm256 - these will instead see
|
||||
the closest equivalent ANSI color. You can mix Xterm256 tags with ANSI tags as you please.
|
||||
|
||||
|555 This is pure white text.|n This is normal text.
|
||||
|230 This is olive green text.
|
||||
|[300 This text has a dark red background.
|
||||
|005|[054 This is dark blue text on a bright cyan background.
|
||||
|=a This is a greyscale value, equal to black.
|
||||
|=m This is a greyscale value, midway between white and black.
|
||||
|=z This is a greyscale value, equal to white.
|
||||
|[=m This is a background greyscale value.
|
||||
|
||||
- `|###` - markup consists of three digits, each an integer from 0 to 5. The three digits describe
|
||||
the amount of **r**ed, **g**reen and **b**lue (RGB) components used in the colour. So `|500` means
|
||||
maximum red and none of the other colours - the result is a bright red. `|520` is red with a touch
|
||||
of green - the result is orange. As opposed to ANSI colors, Xterm256 syntax does not worry about
|
||||
bright/normal intensity, a brighter (lighter) color is just achieved by upping all RGB values with
|
||||
the same amount.
|
||||
- `|[###` - this works the same way but produces a coloured background.
|
||||
- `|=#` - markup produces the xterm256 gray scale tones, where `#` is a letter from `a` (black) to
|
||||
`z` (white). This offers many more nuances of gray than the normal `|###` markup (which only has
|
||||
four gray tones between solid black and white (`|000`, `|111`, `|222`, `|333` and `|444`)).
|
||||
- `|[=#` - this works in the same way but produces background gray scale tones.
|
||||
|
||||
If you have a client that supports Xterm256, you can use
|
||||
|
||||
@color xterm256
|
||||
|
||||
to get a table of all the 256 colours and the codes that produce them. If the table looks broken up
|
||||
into a few blocks of colors, it means Xterm256 is not supported and ANSI are used as a replacement.
|
||||
You can use the `@options` command to see if xterm256 is active for you. This depends on if your
|
||||
client told Evennia what it supports - if not, and you know what your client supports, you may have
|
||||
to activate some features manually.
|
||||
|
||||
## More reading
|
||||
|
||||
There is an [Understanding Color Tags](../Howtos/Understanding-Color-Tags.md) tutorial which expands on the
|
||||
use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
|
||||
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
# Core Concepts
|
||||
|
||||
This documentation cover more over-arching concepts of Evennia, often involving many [Core Components](../Components/Components-Overview.md) acting together.
|
||||
|
||||
## General concepts
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Async-Process.md
|
||||
Soft-Code.md
|
||||
Using-MUX-as-a-Standard.md
|
||||
Messagepath.md
|
||||
OOB.md
|
||||
|
||||
```
|
||||
|
||||
## Access
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Multisession-modes.md
|
||||
Building-Permissions.md
|
||||
Guest-Logins.md
|
||||
Banning.md
|
||||
|
||||
```
|
||||
|
||||
## Extending the Server
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Custom-Protocols.md
|
||||
Bootstrap-&-Evennia.md
|
||||
New-Models.md
|
||||
Zones.md
|
||||
|
||||
```
|
||||
|
||||
## Text processing
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Internationalization.md
|
||||
Text-Encodings.md
|
||||
TextTags.md
|
||||
Change-Messages-Per-Receiver.md
|
||||
Clickable-Links.md
|
||||
Colors.md
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Web features
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Web-Features.md
|
||||
```
|
||||
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
# 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](../Components/Sessions.md#portal-and-server-sessions) is the basic data object representing an
|
||||
external
|
||||
connection to the Evennia [Portal](../Components/Portal-And-Server.md) -- 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.md) 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(f" myproc: {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](../Components/Inputfuncs.md) 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](../Components/Inputfuncs.md) 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.
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# 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](../Components/Connection-Screen.md) 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](../Components/Typeclasses.md) for guests.
|
||||
Defaults to `"typeclasses.accounts.Guest"`.
|
||||
- `PERMISSION_GUEST_DEFAULT` - [permission level](../Components/Locks.md) 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"`.
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
# Internationalization
|
||||
|
||||
*Internationalization* (often abbreviated *i18n* since there are 18 characters
|
||||
between the first "i" and the last "n" in that word) allows Evennia's core
|
||||
server to return texts in other languages than English - without anyone having
|
||||
to edit the source code.
|
||||
|
||||
Language-translations are done by volunteers, so support can vary a lot
|
||||
depending on when a given language was last updated. Below are all languages
|
||||
(besides English) with some level of support. Generally, any language not
|
||||
updated after Sept 2022 will be missing some translations.
|
||||
|
||||
```{eval-rst}
|
||||
|
||||
+---------------+----------------------+--------------+
|
||||
| Language Code | Language | Last updated |
|
||||
+===============+======================+==============+
|
||||
| es | Spanish | Aug 2019 |
|
||||
+---------------+----------------------+--------------+
|
||||
| fr | French | Mar 2022 |
|
||||
+---------------+----------------------+--------------+
|
||||
| it | Italian | Oct 2022 |
|
||||
+---------------+----------------------+--------------+
|
||||
| ko | Korean (simplified) | Sep 2019 |
|
||||
+---------------+----------------------+--------------+
|
||||
| la | Latin | Feb 2021 |
|
||||
+---------------+----------------------+--------------+
|
||||
| pl | Polish | Feb 2019 |
|
||||
+---------------+----------------------+--------------+
|
||||
| pt | Portugese | Oct 2022 |
|
||||
+---------------+----------------------+--------------+
|
||||
| ru | Russian | Apr 2020 |
|
||||
+---------------+----------------------+--------------+
|
||||
| sv | Swedish | Sep 2022 |
|
||||
+---------------+----------------------+--------------+
|
||||
| zh | Chinese (simplified) | May 2019 |
|
||||
+---------------+----------------------+--------------+
|
||||
```
|
||||
|
||||
Language translations are found in the [evennia/locale](github:evennia/locale/)
|
||||
folder. Read below if you want to help improve an existing translation of
|
||||
contribute a new one.
|
||||
|
||||
## Changing server language
|
||||
|
||||
Change language by adding the following to your `mygame/server/conf/settings.py`
|
||||
file:
|
||||
|
||||
```python
|
||||
USE_I18N = True
|
||||
LANGUAGE_CODE = 'en'
|
||||
|
||||
```
|
||||
|
||||
Here `'en'` (the default English) should be changed to the abbreviation for one
|
||||
of the supported languages found in `locale/` (and in the list above). Restart
|
||||
the server to activate i18n.
|
||||
|
||||
```{important}
|
||||
|
||||
Even for a 'fully translated' language you will still see English text
|
||||
in many places when you start Evennia. This is because we expect you (the
|
||||
developer) to know English (you are reading this manual after all). So we
|
||||
translate *hard-coded strings that the end player may see* - things you
|
||||
can't easily change from your mygame/ folder. Outputs from Commands and
|
||||
Typeclasses are generally *not* translated, nor are console/log outputs.
|
||||
|
||||
```
|
||||
|
||||
```{sidebar} Windows users
|
||||
|
||||
If you get errors concerning `gettext` or `xgettext` on Windows,
|
||||
see the [Django documentation](https://docs.djangoproject.com/en/3.2/topics/i18n/translation/#gettext-on-windows).
|
||||
A self-installing and up-to-date version of gettext for Windows (32/64-bit) is
|
||||
available on Github as [gettext-iconv-windows](https://github.com/mlocati/gettext-iconv-windows).
|
||||
|
||||
```
|
||||
|
||||
## Translating Evennia
|
||||
|
||||
Translations are found in the core `evennia/` library, under
|
||||
`evennia/evennia/locale/`. You must make sure to have cloned this repository
|
||||
from [Evennia's github](github:evennia) before you can proceed.
|
||||
|
||||
If you cannot find your language in `evennia/evennia/locale/` it's because noone
|
||||
has translated it yet. Alternatively you might have the language but find the
|
||||
translation bad ... You are welcome to help improve the situation!
|
||||
|
||||
To start a new translation you need to first have cloned the Evennia repositry
|
||||
with GIT and activated a python virtualenv as described on the
|
||||
[Setup Quickstart](../Setup/Installation.md) page.
|
||||
|
||||
Go to `evennia/evennia/` - that is, not your game dir, but inside the `evennia/`
|
||||
repo itself. If you see the `locale/` folder you are in the right place. Make
|
||||
sure your `virtualenv` is active so the `evennia` command is available. Then run
|
||||
|
||||
evennia makemessages --locale <language-code>
|
||||
|
||||
where `<language-code>` is the [two-letter locale code](http://www.science.co.il/Language/Codes.asp)
|
||||
for the language you want to translate, like 'sv' for Swedish or 'es' for
|
||||
Spanish. After a moment it will tell you the language has been processed. For
|
||||
instance:
|
||||
|
||||
evennia makemessages --locale sv
|
||||
|
||||
If you started a new language, a new folder for that language will have emerged
|
||||
in the `locale/` folder. Otherwise the system will just have updated the
|
||||
existing translation with eventual new strings found in the server. Running this
|
||||
command will not overwrite any existing strings so you can run it as much as you
|
||||
want.
|
||||
|
||||
Next head to `locale/<language-code>/LC_MESSAGES` and edit the `**.po` file you
|
||||
find there. You can edit this with a normal text editor but it is easiest if
|
||||
you use a special po-file editor from the web (search the web for "po editor"
|
||||
for many free alternatives), for example:
|
||||
|
||||
- [gtranslator](https://wiki.gnome.org/Apps/Gtranslator)
|
||||
- [poeditor](https://poeditor.com/)
|
||||
|
||||
The concept of translating is simple, it's just a matter of taking the english
|
||||
strings you find in the `**.po` file and add your language's translation best
|
||||
you can. Once you are done, run
|
||||
|
||||
`evennia compilemessages`
|
||||
|
||||
This will compile all languages. Check your language and also check back to your
|
||||
`.po` file in case the process updated it - you may need to fill in some missing
|
||||
header fields and should usually note who did the translation.
|
||||
|
||||
When you are done, make sure that everyone can benefit from your translation!
|
||||
Make a PR against Evennia with the updated `**.po` file. Less ideally (if git is
|
||||
not your thing) you can also attach it to a new post in our forums.
|
||||
|
||||
### Hints on translation
|
||||
|
||||
Many of the translation strings use `{ ... }` placeholders. This is because they
|
||||
are to be used in `.format()` python operations. While you can change the
|
||||
_order_ of these if it makes more sense in your language, you must _not_
|
||||
translate the variables in these formatting tags - Python will look for them!
|
||||
|
||||
Original: "|G{key} connected|n"
|
||||
Swedish: "|G{key} anslöt|n"
|
||||
|
||||
You must also retain line breaks _at the start and end_ of a message, if any
|
||||
(your po-editor should stop you if you don't). Try to also end with the same
|
||||
sentence delimiter (if that makes sense in your language).
|
||||
|
||||
Original: "\n(Unsuccessfull tried '{path}')."
|
||||
Swedish: "\nMisslyckades med att nå '{path}')."
|
||||
|
||||
Finally, try to get a feel for who a string is for. If a special technical term
|
||||
is used it may be more confusing than helpful to translate it, even if it's
|
||||
outside of a `{...}` tag. A mix of English and your language may be clearer
|
||||
than you forcing some ad-hoc translation for a term everyone usually reads in
|
||||
English anyway.
|
||||
|
||||
Original: "\nError loading cmdset: No cmdset class '{classname}' in '{path}'.
|
||||
\n(Traceback was logged {timestamp})"
|
||||
Swedish: "Fel medan cmdset laddades: Ingen cmdset-klass med namn '{classname}' i {path}.
|
||||
\n(Traceback loggades {timestamp})"
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
# Messagepath
|
||||
|
||||
|
||||
The main functionality of Evennia is to communicate with clients connected to it; a player enters
|
||||
commands or their client queries for a gui update (ingoing data). The server responds or sends data
|
||||
on its own as the game changes (outgoing data). It's important to understand how this flow of
|
||||
information works in Evennia.
|
||||
|
||||
## The ingoing message path
|
||||
|
||||
We'll start by tracing data from the client to the server. Here it is in short:
|
||||
|
||||
Client ->
|
||||
PortalSession ->
|
||||
PortalSessionhandler ->
|
||||
(AMP) ->
|
||||
ServerSessionHandler ->
|
||||
ServerSession ->
|
||||
Inputfunc
|
||||
|
||||
### Client (ingoing)
|
||||
|
||||
The client sends data to Evennia in two ways.
|
||||
|
||||
- When first connecting, the client can send data to the server about its
|
||||
capabilities. This is things like "I support xterm256 but not unicode" and is
|
||||
mainly used when a Telnet client connects. This is called a "handshake" and
|
||||
will generally set some flags on the [Portal Session](../Components/Portal-And-Server.md) that
|
||||
are later synced to the Server Session. Since this is not something the player
|
||||
controls, we'll not explore this further here.
|
||||
- The client can send an *inputcommand* to the server. Traditionally this only
|
||||
happens when the player enters text on the command line. But with a custom
|
||||
client GUI, a command could also come from the pressing of a button. Finally
|
||||
the client may send commands based on a timer or some trigger.
|
||||
|
||||
Exactly how the inputcommand looks when it travels from the client to Evennia
|
||||
depends on the [Protocol](./Custom-Protocols.md) used:
|
||||
- Telnet: A string. If GMCP or MSDP OOB protocols are used, this string will
|
||||
be formatted in a special way, but it's still a raw string. If Telnet SSL is
|
||||
active, the string will be encrypted.
|
||||
- SSH: An encrypted string
|
||||
- Webclient: A JSON-serialized string.
|
||||
|
||||
### Portal Session (ingoing)
|
||||
|
||||
Each client is connected to the game via a *Portal Session*, one per connection. This Session is
|
||||
different depending on the type of connection (telnet, webclient etc) and thus know how to handle
|
||||
that particular data type. So regardless of how the data arrives, the Session will identify the type
|
||||
of the instruction and any arguments it should have. For example, the telnet protocol will figure
|
||||
that anything arriving normally over the wire should be passed on as a "text" type.
|
||||
|
||||
### PortalSessionHandler (ingoing)
|
||||
|
||||
The *PortalSessionhandler* manages all connected Sessions in the Portal. Its `data_in` method
|
||||
(called by each Portal Session) will parse the command names and arguments from the protocols and
|
||||
convert them to a standardized form we call the *inputcommand*:
|
||||
|
||||
```python
|
||||
(commandname, (args), {kwargs})
|
||||
```
|
||||
|
||||
All inputcommands must have a name, but they may or may not have arguments and keyword arguments -
|
||||
in fact no default inputcommands use kwargs at all. The most common inputcommand is "text", which
|
||||
has the argument the player input on the command line:
|
||||
|
||||
```python
|
||||
("text", ("look",), {})
|
||||
```
|
||||
|
||||
This inputcommand-structure is pickled together with the unique session-id of the Session to which
|
||||
it belongs. This is then sent over the AMP connection.
|
||||
|
||||
### ServerSessionHandler (ingoing)
|
||||
|
||||
On the Server side, the AMP unpickles the data and associates the session id with the server-side
|
||||
[Session](../Components/Sessions.md). Data and Session are passed to the server-side `SessionHandler.data_in`. This
|
||||
in turn calls `ServerSession.data_in()`
|
||||
|
||||
### ServerSession (ingoing)
|
||||
|
||||
The method `ServerSession.data_in` is meant to offer a single place to override if they want to
|
||||
examine *all* data passing into the server from the client. It is meant to call the
|
||||
`ssessionhandler.call_inputfuncs` with the (potentially processed) data (so this is technically a
|
||||
sort of detour back to the sessionhandler).
|
||||
|
||||
In `call_inputfuncs`, the inputcommand's name is compared against the names of all the *inputfuncs*
|
||||
registered with the server. The inputfuncs are named the same as the inputcommand they are supposed
|
||||
to handle, so the (default) inputfunc for handling our "look" command is called "text". These are
|
||||
just normal functions and one can plugin new ones by simply putting them in a module where Evennia
|
||||
looks for such functions.
|
||||
|
||||
If a matching inputfunc is found, it will be called with the Session and the inputcommand's
|
||||
arguments:
|
||||
|
||||
```python
|
||||
text(session, *("look",), **{})
|
||||
```
|
||||
|
||||
If no matching inputfunc is found, an inputfunc named "default" will be tried and if that is also
|
||||
not found, an error will be raised.
|
||||
|
||||
### Inputfunc
|
||||
|
||||
The [Inputfunc](../Components/Inputfuncs.md) must be on the form `func(session, *args, **kwargs)`. An exception is
|
||||
the `default` inputfunc which has form `default(session, cmdname, *args, **kwargs)`, where `cmdname`
|
||||
is the un-matched inputcommand string.
|
||||
|
||||
This is where the message's path diverges, since just what happens next depends on the type of
|
||||
inputfunc was triggered. In the example of sending "look", the inputfunc is named "text". It will
|
||||
pass the argument to the `cmdhandler` which will eventually lead to the `look` command being
|
||||
executed.
|
||||
|
||||
|
||||
## The outgoing message path
|
||||
|
||||
Next let's trace the passage from server to client.
|
||||
|
||||
msg ->
|
||||
ServerSession ->
|
||||
ServerSessionHandler ->
|
||||
(AMP) ->
|
||||
PortalSessionHandler ->
|
||||
PortalSession ->
|
||||
Client
|
||||
|
||||
### msg
|
||||
|
||||
All outgoing messages start in the `msg` method. This is accessible from three places:
|
||||
|
||||
- `Object.msg`
|
||||
- `Account.msg`
|
||||
- `Session.msg`
|
||||
|
||||
The call sign of the `msg` method looks like this:
|
||||
|
||||
```python
|
||||
msg(text=None, from_obj=None, session=None, options=None, **kwargs)
|
||||
```
|
||||
|
||||
For our purposes, what is important to know is that with the exception of `from_obj`, `session` and
|
||||
`options`, all keywords given to the `msg` method is the name of an *outputcommand* and its
|
||||
arguments. So `text` is actually such a command, taking a string as its argument. The reason `text`
|
||||
sits as the first keyword argument is that it's so commonly used (`caller.msg("Text")` for example).
|
||||
Here are some examples
|
||||
|
||||
```python
|
||||
msg("Hello!") # using the 'text' outputfunc
|
||||
msg(prompt=f"HP: {HP}, SP: {SP}, MP: {MP}")
|
||||
msg(mycommand=((1,2,3,4), {"foo": "bar"})
|
||||
|
||||
```
|
||||
Note the form of the `mycommand` outputfunction. This explicitly defines the arguments and keyword
|
||||
arguments for the function. In the case of the `text` and `prompt` calls we just specify a string -
|
||||
this works too: The system will convert this into a single argument for us later in the message
|
||||
path.
|
||||
|
||||
> Note: The `msg` method sits on your Object- and Account typeclasses. It means you can easily
|
||||
override `msg` and make custom- or per-object modifications to the flow of data as it passes
|
||||
through.
|
||||
|
||||
### ServerSession (outgoing)
|
||||
|
||||
Nothing is processed on the Session, it just serves as a gathering points for all different `msg`.
|
||||
It immediately passes the data on to ...
|
||||
|
||||
### ServerSessionHandler (outgoing)
|
||||
|
||||
In the *ServerSessionhandler*, the keywords from the `msg` method are collated into one or more
|
||||
*outputcommands* on a standardized form (identical to inputcommands):
|
||||
|
||||
```
|
||||
(commandname, (args), {kwargs})
|
||||
```
|
||||
|
||||
This will intelligently convert different input to the same form. So `msg("Hello")` will end up as
|
||||
an outputcommand `("text", ("Hello",), {})`.
|
||||
|
||||
This is also the point where the [FuncParser](../Components/FuncParser.md)) is applied, depending on the
|
||||
session to receive the data. Said data is pickled together with the Session id then sent over the
|
||||
AMP bridge.
|
||||
|
||||
### PortalSessionHandler (outgoing)
|
||||
|
||||
After the AMP connection has unpickled the data and paired the session id to the matching
|
||||
PortalSession, the handler next determines if this Session has a suitable method for handling the
|
||||
outputcommand.
|
||||
|
||||
The situation is analogous to how inputfuncs work, except that protocols are fixed things that don't
|
||||
need a plugin infrastructure like the inputfuncs are handled. So instead of an "outputfunc", the
|
||||
handler looks for methods on the PortalSession with names of the form `send_<commandname>`.
|
||||
|
||||
For example, the common sending of text expects a PortalSession method `send_text`. This will be
|
||||
called as `send_text(*("Hello",), **{})`. If the "prompt" outputfunction was used, send_prompt is
|
||||
called. In all other cases the `send_default(cmdname, *args, **kwargs)` will be called - this is the
|
||||
case for all client-custom outputcommands, like when wanting to tell the client to update a graphic
|
||||
or play a sound.
|
||||
|
||||
### PortalSession (outgoing)
|
||||
|
||||
At this point it is up to the session to convert the command into a form understood by this
|
||||
particular protocol. For telnet, `send_text` will just send the argument as a string (since that is
|
||||
what telnet clients expect when "text" is coming). If `send_default` was called (basically
|
||||
everything that is not traditional text or a prompt), it will pack the data as an GMCP or MSDP
|
||||
command packet if the telnet client supports either (otherwise it won't send at all). If sending to
|
||||
the webclient, the data will get packed into a JSON structure at all times.
|
||||
|
||||
### Client (outgoing)
|
||||
|
||||
Once arrived at the client, the outputcommand is handled in the way supported by the client (or it
|
||||
may be quietly ignored if not). "text" commands will be displayed in the main window while others
|
||||
may trigger changes in the GUI or play a sound etc.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Multisession modes
|
||||
|
||||
TODO: This is covered in various places before.
|
||||
|
|
@ -1,264 +0,0 @@
|
|||
# New Models
|
||||
|
||||
*Note: This is considered an advanced topic.*
|
||||
|
||||
Evennia offers many convenient ways to store object data, such as via Attributes or Scripts. This is
|
||||
sufficient for most use cases. But if you aim to build a large stand-alone system, trying to squeeze
|
||||
your storage requirements into those may be more complex than you bargain for. Examples may be to
|
||||
store guild data for guild members to be able to change, tracking the flow of money across a game-
|
||||
wide economic system or implement other custom game systems that requires the storage of custom data
|
||||
in a quickly accessible way. Whereas [Tags](../Components/Tags.md) or [Scripts](../Components/Scripts.md) can handle many situations,
|
||||
sometimes things may be easier to handle by adding your own database model.
|
||||
|
||||
## Overview of database tables
|
||||
|
||||
SQL-type databases (which is what Evennia supports) are basically highly optimized systems for
|
||||
retrieving text stored in tables. A table may look like this
|
||||
|
||||
```
|
||||
id | db_key | db_typeclass_path | db_permissions ...
|
||||
------------------------------------------------------------------
|
||||
1 | Griatch | evennia.DefaultCharacter | Developers ...
|
||||
2 | Rock | evennia.DefaultObject | None ...
|
||||
```
|
||||
|
||||
Each line is considerably longer in your database. Each column is referred to as a "field" and every
|
||||
row is a separate object. You can check this out for yourself. If you use the default sqlite3
|
||||
database, go to your game folder and run
|
||||
|
||||
evennia dbshell
|
||||
|
||||
You will drop into the database shell. While there, try:
|
||||
|
||||
sqlite> .help # view help
|
||||
|
||||
sqlite> .tables # view all tables
|
||||
|
||||
# show the table field names for objects_objectdb
|
||||
sqlite> .schema objects_objectdb
|
||||
|
||||
# show the first row from the objects_objectdb table
|
||||
sqlite> select * from objects_objectdb limit 1;
|
||||
|
||||
sqlite> .exit
|
||||
|
||||
Evennia uses [Django](https://docs.djangoproject.com), which abstracts away the database SQL
|
||||
manipulation and allows you to search and manipulate your database entirely in Python. Each database
|
||||
table is in Django represented by a class commonly called a *model* since it describes the look of
|
||||
the table. In Evennia, Objects, Scripts, Channels etc are examples of Django models that we then
|
||||
extend and build on.
|
||||
|
||||
## Adding a new database table
|
||||
|
||||
Here is how you add your own database table/models:
|
||||
|
||||
1. In Django lingo, we will create a new "application" - a subsystem under the main Evennia program.
|
||||
For this example we'll call it "myapp". Run the following (you need to have a working Evennia
|
||||
running before you do this, so make sure you have run the steps in [Setup Quickstart](Getting-
|
||||
Started) first):
|
||||
|
||||
cd mygame/world
|
||||
evennia startapp myapp
|
||||
|
||||
1. A new folder `myapp` is created. "myapp" will also be the name (the "app label") from now on. We
|
||||
chose to put it in the `world/` subfolder here, but you could put it in the root of your `mygame` if
|
||||
that makes more sense.
|
||||
1. The `myapp` folder contains a few empty default files. What we are
|
||||
interested in for now is `models.py`. In `models.py` you define your model(s). Each model will be a
|
||||
table in the database. See the next section and don't continue until you have added the models you
|
||||
want.
|
||||
1. You now need to tell Evennia that the models of your app should be a part of your database
|
||||
scheme. Add this line to your `mygame/server/conf/settings.py`file (make sure to use the path where
|
||||
you put `myapp` and don't forget the comma at the end of the tuple):
|
||||
|
||||
```
|
||||
INSTALLED_APPS = INSTALLED_APPS + ("world.myapp", )
|
||||
```
|
||||
|
||||
1. From `mygame/`, run
|
||||
|
||||
evennia makemigrations myapp
|
||||
evennia migrate
|
||||
|
||||
This will add your new database table to the database. If you have put your game under version
|
||||
control (if not, [you should](../Coding/Version-Control.md)), don't forget to `git add myapp/*` to add all items
|
||||
to version control.
|
||||
|
||||
## Defining your models
|
||||
|
||||
A Django *model* is the Python representation of a database table. It can be handled like any other
|
||||
Python class. It defines *fields* on itself, objects of a special type. These become the "columns"
|
||||
of the database table. Finally, you create new instances of the model to add new rows to the
|
||||
database.
|
||||
|
||||
We won't describe all aspects of Django models here, for that we refer to the vast [Django
|
||||
documentation](https://docs.djangoproject.com/en/2.2/topics/db/models/) on the subject. Here is a
|
||||
(very) brief example:
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
|
||||
class MyDataStore(models.Model):
|
||||
"A simple model for storing some data"
|
||||
db_key = models.CharField(max_length=80, db_index=True)
|
||||
db_category = models.CharField(max_length=80, null=True, blank=True)
|
||||
db_text = models.TextField(null=True, blank=True)
|
||||
# we need this one if we want to be
|
||||
# able to store this in an Evennia Attribute!
|
||||
db_date_created = models.DateTimeField('date created', editable=False,
|
||||
auto_now_add=True, db_index=True)
|
||||
```
|
||||
|
||||
We create four fields: two character fields of limited length and one text field which has no
|
||||
maximum length. Finally we create a field containing the current time of us creating this object.
|
||||
|
||||
> The `db_date_created` field, with exactly this name, is *required* if you want to be able to store
|
||||
instances of your custom model in an Evennia [Attribute](../Components/Attributes.md). It will automatically be set
|
||||
upon creation and can after that not be changed. Having this field will allow you to do e.g.
|
||||
`obj.db.myinstance = mydatastore`. If you know you'll never store your model instances in Attributes
|
||||
the `db_date_created` field is optional.
|
||||
|
||||
You don't *have* to start field names with `db_`, this is an Evennia convention. It's nevertheless
|
||||
recommended that you do use `db_`, partly for clarity and consistency with Evennia (if you ever want
|
||||
to share your code) and partly for the case of you later deciding to use Evennia's
|
||||
`SharedMemoryModel` parent down the line.
|
||||
|
||||
The field keyword `db_index` creates a *database index* for this field, which allows quicker
|
||||
lookups, so it's recommended to put it on fields you know you'll often use in queries. The
|
||||
`null=True` and `blank=True` keywords means that these fields may be left empty or set to the empty
|
||||
string without the database complaining. There are many other field types and keywords to define
|
||||
them, see django docs for more info.
|
||||
|
||||
Similar to using [django-admin](https://docs.djangoproject.com/en/2.2/howto/legacy-databases/) you
|
||||
are able to do `evennia inspectdb` to get an automated listing of model information for an existing
|
||||
database. As is the case with any model generating tool you should only use this as a starting
|
||||
point for your models.
|
||||
|
||||
## Creating a new model instance
|
||||
|
||||
To create a new row in your table, you instantiate the model and then call its `save()` method:
|
||||
|
||||
```python
|
||||
from evennia.myapp import MyDataStore
|
||||
|
||||
new_datastore = MyDataStore(db_key="LargeSword",
|
||||
db_category="weapons",
|
||||
db_text="This is a huge weapon!")
|
||||
# this is required to actually create the row in the database!
|
||||
new_datastore.save()
|
||||
|
||||
```
|
||||
|
||||
Note that the `db_date_created` field of the model is not specified. Its flag `at_now_add=True`
|
||||
makes sure to set it to the current date when the object is created (it can also not be changed
|
||||
further after creation).
|
||||
|
||||
When you update an existing object with some new field value, remember that you have to save the
|
||||
object afterwards, otherwise the database will not update:
|
||||
|
||||
```python
|
||||
my_datastore.db_key = "Larger Sword"
|
||||
my_datastore.save()
|
||||
```
|
||||
|
||||
Evennia's normal models don't need to explicitly save, since they are based on `SharedMemoryModel`
|
||||
rather than the raw django model. This is covered in the next section.
|
||||
|
||||
## Using the `SharedMemoryModel` parent
|
||||
|
||||
Evennia doesn't base most of its models on the raw `django.db.models` but on the Evennia base model
|
||||
`evennia.utils.idmapper.models.SharedMemoryModel`. There are two main reasons for this:
|
||||
|
||||
1. Ease of updating fields without having to explicitly call `save()`
|
||||
2. On-object memory persistence and database caching
|
||||
|
||||
The first (and least important) point means that as long as you named your fields `db_*`, Evennia
|
||||
will automatically create field wrappers for them. This happens in the model's
|
||||
[Metaclass](http://en.wikibooks.org/wiki/Python_Programming/Metaclasses) so there is no speed
|
||||
penalty for this. The name of the wrapper will be the same name as the field, minus the `db_`
|
||||
prefix. So the `db_key` field will have a wrapper property named `key`. You can then do:
|
||||
|
||||
```python
|
||||
my_datastore.key = "Larger Sword"
|
||||
```
|
||||
|
||||
and don't have to explicitly call `save()` afterwards. The saving also happens in a more efficient
|
||||
way under the hood, updating only the field rather than the entire model using django optimizations.
|
||||
Note that if you were to manually add the property or method `key` to your model, this will be used
|
||||
instead of the automatic wrapper and allows you to fully customize access as needed.
|
||||
|
||||
To explain the second and more important point, consider the following example using the default
|
||||
Django model parent:
|
||||
|
||||
```python
|
||||
shield = MyDataStore.objects.get(db_key="SmallShield")
|
||||
shield.cracked = True # where cracked is not a database field
|
||||
```
|
||||
|
||||
And then later:
|
||||
|
||||
```python
|
||||
shield = MyDataStore.objects.get(db_key="SmallShield")
|
||||
print(shield.cracked) # error!
|
||||
```
|
||||
|
||||
The outcome of that last print statement is *undefined*! It could *maybe* randomly work but most
|
||||
likely you will get an `AttributeError` for not finding the `cracked` property. The reason is that
|
||||
`cracked` doesn't represent an actual field in the database. It was just added at run-time and thus
|
||||
Django don't care about it. When you retrieve your shield-match later there is *no* guarantee you
|
||||
will get back the *same Python instance* of the model where you defined `cracked`, even if you
|
||||
search for the same database object.
|
||||
|
||||
Evennia relies heavily on on-model handlers and other dynamically created properties. So rather than
|
||||
using the vanilla Django models, Evennia uses `SharedMemoryModel`, which levies something called
|
||||
*idmapper*. The idmapper caches model instances so that we will always get the *same* instance back
|
||||
after the first lookup of a given object. Using idmapper, the above example would work fine and you
|
||||
could retrieve your `cracked` property at any time - until you rebooted when all non-persistent data
|
||||
goes.
|
||||
|
||||
Using the idmapper is both more intuitive and more efficient *per object*; it leads to a lot less
|
||||
reading from disk. The drawback is that this system tends to be more memory hungry *overall*. So if
|
||||
you know that you'll *never* need to add new properties to running instances or know that you will
|
||||
create new objects all the time yet rarely access them again (like for a log system), you are
|
||||
probably better off making "plain" Django models rather than using `SharedMemoryModel` and its
|
||||
idmapper.
|
||||
|
||||
To use the idmapper and the field-wrapper functionality you just have to have your model classes
|
||||
inherit from `evennia.utils.idmapper.models.SharedMemoryModel` instead of from the default
|
||||
`django.db.models.Model`:
|
||||
|
||||
```python
|
||||
from evennia.utils.idmapper.models import SharedMemoryModel
|
||||
|
||||
class MyDataStore(SharedMemoryModel):
|
||||
# the rest is the same as before, but db_* is important; these will
|
||||
# later be settable as .key, .category, .text ...
|
||||
db_key = models.CharField(max_length=80, db_index=True)
|
||||
db_category = models.CharField(max_length=80, null=True, blank=True)
|
||||
db_text = models.TextField(null=True, blank=True)
|
||||
db_date_created = models.DateTimeField('date created', editable=False,
|
||||
auto_now_add=True, db_index=True)
|
||||
```
|
||||
|
||||
## Searching for your models
|
||||
|
||||
To search your new custom database table you need to use its database *manager* to build a *query*.
|
||||
Note that even if you use `SharedMemoryModel` as described in the previous section, you have to use
|
||||
the actual *field names* in the query, not the wrapper name (so `db_key` and not just `key`).
|
||||
|
||||
```python
|
||||
from world.myapp import MyDataStore
|
||||
|
||||
# get all datastore objects exactly matching a given key
|
||||
matches = MyDataStore.objects.filter(db_key="Larger Sword")
|
||||
# get all datastore objects with a key containing "sword"
|
||||
# and having the category "weapons" (both ignoring upper/lower case)
|
||||
matches2 = MyDataStore.objects.filter(db_key__icontains="sword",
|
||||
db_category__iequals="weapons")
|
||||
# show the matching data (e.g. inside a command)
|
||||
for match in matches2:
|
||||
self.caller.msg(match.db_text)
|
||||
```
|
||||
|
||||
See the [Django query documentation](https://docs.djangoproject.com/en/2.2/topics/db/queries/) for a
|
||||
lot more information about querying the database.
|
||||
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