Update with 6.x branch docs

This commit is contained in:
Griatch 2026-02-15 19:06:04 +01:00
parent 4544902e08
commit 9e122e89ca
1715 changed files with 710990 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
# 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.
See also the [Beginner Tutorial](../Howtos/Beginner-Tutorial/Beginner-Tutorial-Overview.md).
```{toctree}
:maxdepth: 2
Evennia-Code-Style.md
Default-Command-Syntax.md
Version-Control.md
Debugging.md
Unit-Testing.md
Profiling.md
Continuous-Integration.md
Setting-up-PyCharm.md
```
## Evennia Changelog
```{toctree}
:maxdepth: 2
Changelog.md
```
```{toctree}
:hidden:
Release-Notes-1.0
```

View file

@ -0,0 +1,18 @@
# Continuous Integration (CI)
[Continuous Integration (CI)](https://en.wikipedia.org/wiki/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.
## Continuous-Integration guides
Evennia itself is making heavy use of [github actions](https://github.com/features/actions). This is integrated with Github and is probably the one to go for most people, especially if your code is on Github already. You can see and analyze how Evennia's actions are running [here](https://github.com/evennia/evennia/actions).
There are however a lot of tools and services providing CI functionality. [Here is a blog overview](https://www.atlassian.com/continuous-delivery/continuous-integration/tools) (external link).

View file

@ -0,0 +1,260 @@
# Debugging
Sometimes, an error is not trivial to resolve. A few simple `print` statements is not enough to find the cause of the issue. The traceback is not informative or even non-existing.
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 `evennia istart`
4. Run the code again and see the debugger open.
5. Run the program line by line, examining variables, checking the logic of instructions.
6. Continue and try again, each step a bit further toward the truth and the working feature.
## 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/).
## Debugging with debugpy
If you use Visual Studio Code and would like to debug Evennia using a graphical debugger, please follow the instructions here:
[debugpy contrib](https://github.com/evennia/evennia/tree/main/evennia/contrib/utils/debugpy)

View file

@ -0,0 +1,24 @@
# Default Command Syntax
Evennia allows for any command syntax.
If you like the way DikuMUDs, LPMuds or MOOs handle things, you could emulate that with Evennia. If you are ambitious you could even design a whole new style, perfectly fitting your own dreams of the ideal game. See the [Command](../Components/Commands.md) documentation for how to do this.
We do offer a default however. The default Evennia setup tends to *resemble* [MUX2](https://www.tinymux.org/), and its cousins [PennMUSH](https://www.pennmush.org), [TinyMUSH](https://github.com/TinyMUSH/TinyMUSH/wiki), and [RhostMUSH](http://www.rhostmush.com/):
```
command[/switches] object [= options]
```
While the reason for this similarity is partly historical, these codebases offer very mature feature sets for administration and building.
Evennia is *not* a MUX system though. It works very differently in many ways. For example, Evennia
deliberately lacks an online softcode language (a policy explained on our [softcode policy page](./Soft-Code.md)). Evennia also does not shy from using its own syntax when deemed appropriate: the
MUX syntax has grown organically over a long time and is, frankly, rather arcane in places. All in
all the default command syntax should at most be referred to as "MUX-like" or "MUX-inspired".
```{toctree}
:hidden:
Soft-Code
```

View file

@ -0,0 +1,278 @@
# Evennia Code Style
All code submitted or committed to the Evennia project should aim to follow the
guidelines outlined in [Python PEP 8][pep8]. Keeping the code style uniform
makes it much easier for people to collaborate and read the code.
A good way to check if your code follows PEP8 is to use the [PEP8 tool][pep8tool]
on your sources.
## Main code style specification
* 4-space indentation, NO TABS!
* Unix line endings.
* 100 character line widths
* CamelCase is only used for classes, nothing else.
* All non-global variable names and all function names are to be
lowercase, words separated by underscores. Variable names should
always be more than two letters long.
* Module-level global variables (only) are to be in CAPITAL letters.
* Imports should be done in this order:
- Python modules (builtins and standard library)
- Twisted modules
- Django modules
- Evennia library modules (`evennia`)
- Evennia contrib modules (`evennia.contrib`)
* All modules, classes, functions and methods should have doc strings formatted
as outlined below.
* All default commands should have a consistent docstring formatted as
outlined below.
## Code Docstrings
All modules, classes, functions and methods should have docstrings
formatted with [Google style][googlestyle] -inspired indents, using
[Markdown][githubmarkdown] formatting where needed. Evennia's `api2md`
parser will use this to create pretty API documentation.
### Module docstrings
Modules should all start with at least a few lines of docstring at
their top describing the contents and purpose of the module.
Example of module docstring (top of file):
```python
"""
This module handles the creation of `Objects` that
are useful in the game ...
"""
```
Sectioning (`# title`, `## subtile` etc) should not be used in
freeform docstrings - this will confuse the sectioning of the auto
documentation page and the auto-api will create this automatically.
Write just the section name bolded on its own line to mark a section.
Beyond sections markdown should be used as needed to format
the text.
Code examples should use [multi-line syntax highlighting][markdown-hilight]
to mark multi-line code blocks, using the "python" identifier. Just
indenting code blocks (common in markdown) will not produce the
desired look.
When using any code tags (inline or blocks) it's recommended that you
don't let the code extend wider than about 70 characters or it will
need to be scrolled horizontally in the wiki (this does not affect any
other text, only code).
### Class docstrings
The root class docstring should describe the over-arching use of the
class. It should usually not describe the exact call sequence nor list
important methods, this tends to be hard to keep updated as the API
develops. Don't use section markers (`#`, `##` etc).
Example of class docstring:
```python
class MyClass(object):
"""
This class describes the creation of `Objects`. It is useful
in many situations, such as ...
"""
```
### Function / method docstrings
Example of function or method docstring:
```python
def funcname(a, b, c, d=False, **kwargs):
"""
This is a brief introduction to the function/class/method
Args:
a (str): This is a string argument that we can talk about
over multiple lines.
b (int or str): Another argument.
c (list): A list argument.
d (bool, optional): An optional keyword argument.
Keyword Args:
test (list): A test keyword.
Returns:
str: The result of the function.
Raises:
RuntimeException: If there is a critical error,
this is raised.
IOError: This is only raised if there is a
problem with the database.
Notes:
This is an example function. If `d=True`, something
amazing will happen.
"""
```
The syntax is very "loose" but the indentation matters. That is, you
should end the block headers (like `Args:`) with a line break followed by
an indent. When you need to break a line you should start the next line
with another indent. For consistency with the code we recommend all
indents to be 4 spaces wide (no tabs!).
Here are all the supported block headers:
```
"""
Args
argname (freeform type): Description endind with period.
Keyword Args:
argname (freeform type): Description.
Returns/Yields:
type: Description.
Raises:
Exceptiontype: Description.
Notes/Note/Examples/Example:
Freeform text.
"""
```
Parts marked with "freeform" means that you can in principle put any
text there using any formatting except for sections markers (`#`, `##`
etc). You must also keep indentation to mark which block you are part
of. You should normally use the specified format rather than the
freeform counterpart (this will produce nicer output) but in some
cases the freeform may produce a more compact and readable result
(such as when describing an `*args` or `**kwargs` statement in general
terms). The first `self` argument of class methods should never be
documented.
Note that
```
"""
Args:
argname (type, optional): Description.
"""
```
and
```
"""
Keyword Args:
sargname (type): Description.
"""
```
mean the same thing! Which one is used depends on the function or
method documented, but there are no hard rules; If there is a large
`**kwargs` block in the function, using the `Keyword Args:` block may be a
good idea, for a small number of arguments though, just using `Args:`
and marking keywords as `optional` will shorten the docstring and make
it easier to read.
## Default Command Docstrings
These represent a special case since Commands in Evennia use their class
docstrings to represent the in-game help entry for that command.
All the commands in the _default command_ sets should have their doc-strings
formatted on a similar form. For contribs, this is loosened, but if there is
no particular reason to use a different form, one should aim to use the same
style for contrib-command docstrings as well.
```python
"""
Short header
Usage:
key[/switches, if any] <mandatory args> [optional] choice1||choice2||choice3
Switches:
switch1 - description
switch2 - description
Examples:
Usage example and output
Longer documentation detailing the command.
"""
```
- Two spaces are used for *indentation* in all default commands.
- Square brackets `[ ]` surround *optional, skippable arguments*.
- Angled brackets `< >` surround a _description_ of what to write rather than the exact syntax.
- Explicit choices are separated by `|`. To avoid this being parsed as a color code, use `||` (this
will come out as a single `|`) or put spaces around the character ("` | `") if there's plenty of room.
- The `Switches` and `Examples` blocks are optional and based on the Command.
Here is the `nick` command as an example:
```python
"""
Define a personal alias/nick
Usage:
nick[/switches] <nickname> = [<string>]
alias ''
Switches:
object - alias an object
account - alias an account
clearall - clear all your aliases
list - show all defined aliases (also "nicks" works)
Examples:
nick hi = say Hello, I'm Sarah!
nick/object tom = the tall man
A 'nick' is a personal shortcut you create for your own use [...]
"""
```
For commands that *require arguments*, the policy is for it to return a `Usage:`
string if the command is entered without any arguments. So for such commands,
the Command body should contain something to the effect of
```python
if not self.args:
self.caller.msg("Usage: nick[/switches] <nickname> = [<string>]")
return
```
## Tools for auto-linting
### black
Automatic pep8 compliant formatting and linting can be performed using the
`black` formatter:
black --line-length 100
### PyCharm
The Python IDE [Pycharm][pycharm] can auto-generate empty doc-string stubs. The
default is to use `reStructuredText` form, however. To change to Evennia's
Google-style docstrings, follow [this guide][pycharm-guide].
[pep8]: http://www.python.org/dev/peps/pep-0008
[pep8tool]: https://pypi.python.org/pypi/pep8
[googlestyle]: https://www.sphinx-doc.org/en/master/usage/extensions/example_google.html
[githubmarkdown]: https://help.github.com/articles/github-flavored-markdown/
[markdown-hilight]: https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting
[command-docstrings]: https://github.com/evennia/evennia/wiki/Using%20MUX%20As%20a%20Standard#documentation-policy
[pycharm]: https://www.jetbrains.com/pycharm/
[pycharm-guide]: https://www.jetbrains.com/help/pycharm/2016.3/python-integrated-tools.html

View file

@ -0,0 +1,197 @@
# Profiling
```{important}
This is considered an advanced topic. It's mainly of interest to server developers.
```
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.
In the example above, we see that this number of calls, using a list comprehension is about twice as fast as building a list using `.append()`.
## 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/Running-Evennia.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.

View file

@ -0,0 +1,151 @@
# Evennia 1.0 Release Notes
This summarizes the changes. See the [Changelog](./Changelog.md) for the full list.
- Main development now on `main` branch. `master` branch remains, but will not be updated anymore.
## Minimum requirements
- Python 3.10 is now required minimum. Ubuntu LTS now installs with 3.10. Evennia 1.0 is also tested with Python 3.11 - this is the recommended version for Linux/Mac. Windows users may want to stay on Python 3.10 unless they are okay with installing a C++ compiler.
- Twisted 22.10+
- Django 4.1+
## Major new features
- Evennia is now on PyPi and is installable as [pip install evennia](../Setup/Installation.md).
- A completely revamped documentation at https://www.evennia.com/docs/latest. The old wiki and readmedocs pages will close.
- Evennia 1.0 now has a REST API which allows you access game objects using CRUD operations GET/POST etc. See [The Web-API docs][Web-API] for more information.
- [Evennia<>Discord Integration](../Setup/Channels-to-Discord.md) between Evennia channels and Discord servers.
- [Script](../Components/Scripts.md) overhaul: 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. This makes Scripts even more useful as general storage entities.
- The [FuncParser](../Components/FuncParser.md) centralizes and vastly improves all in-string function calls, such as `say the result is $eval(3 * 7)` and say the result `the result is 21`. The parser completely replaces the old `parse_inlinefunc`. The new parser can handle both arguments and kwargs and are also used for in-prototype parsing as well as director stance messaging, such as using `$You()` to represent yourself in a string and having the result come out differently depending on who see you.
- [Channels](../Components/Channels.md) New Channel-System using the `channel` command and nicks. The old `ChannelHandler` was removed and the customization and operation of channels have been simplified a lot. The old command syntax commands are now available as a contrib.
- [Help System](../Components/Help-System.md) was refactored.
- A new type of `FileHelp` system allows you to add in-game help files as external Python files. This means there are three ways to add help entries in Evennia: 1) Auto-generated from Command's code. 2) Manually added to the database from the `sethelp` command in-game and 3) Created as external Python files that Evennia loads and makes available in-game.
- We now use `lunr` search indexing for better `help` matching and suggestions. Also improve
the main help command's default listing output.
- 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.
- `sethelp` command now warns if shadowing other help-types when creating a new entry.
- Make `help` index output clickable for webclient/clients with MXP (PR by davewiththenicehat)
- Rework of the [Web](../Components/Website.md) setup, into a much more consistent structure and update to latest Django. The `mygame/web/static_overrides` and `-template_overrides` were removed. The folders are now just `mygame/web/static` and `/templates` and handle the automatic copying of data behind the scenes. `app.css` to `website.css` for consistency. The old `prosimii-css` files were removed.
- [AttributeProperty](../Components/Attributes.md#using-attributeproperty)/[TagProperty](../Components/Tags.md) along with `AliasProperty` and `PermissionProperty` to allow managing Attributes, Tags, Aliases and Permissios on typeclasses in the same way as Django fields. This dramatically reduces the need to assign Attributes/Tags in `at_create_object` hook.
- The old `MULTISESSION_MODE` was divided into smaller settings, for better controlling what happens when a user connects, if a character should be auto-created, and how many characters they can control at the same time. See [Connection-Styles](../Concepts/Connection-Styles.md) for a detailed explanation.
- Evennia now supports 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', ...}`.
## Contribs
The `contrib` folder structure was changed from 0.9.5. All contribs are now in sub-folders and organized into categories. All import paths must be updated. See [Contribs overview](../Contribs/Contribs-Overview.md).
- New [Traits contrib](../Contribs/Contrib-Traits.md), converted and expanded from Ainneve project. (whitenoise, Griatch)
- New [Crafting contrib](../Contribs/Contrib-Crafting.md), adding a full crafting subsystem (Griatch)
- New [XYZGrid contrib](../Contribs/Contrib-XYZGrid.md), adding x,y,z grid coordinates with in-game map and pathfinding. Controlled outside of the game via custom evennia launcher command (Griatch)
- New [Command cooldown contrib](../Contribs/Contrib-Cooldowns.md) contrib for making it easier to manage commands using
dynamic cooldowns between uses (owllex)
- New [Godot Protocol contrib](../Contribs/Contrib-Godotwebsocket.md) for connecting to Evennia from a client written in the open-source game engine [Godot](https://godotengine.org/) (ChrisLR).
- New [name_generator contrib](../Contribs/Contrib-Name-Generator.md) for building random real-world based or fantasy-names based on phonetic rules (InspectorCaracal)
- New [Buffs contrib](../Contribs/Contrib-Buffs.md) for managing temporary and permanent RPG status buffs effects (tegiminis)
- The existing [RPSystem contrib](../Contribs/Contrib-RPSystem.md) was refactored and saw a speed boost (InspectorCaracal, other contributors)
## Translations
- New Latin (la) translation (jamalainm)
- New German (de) translation (Zhuraj)
- Updated Italian translation (rpolve)
- Updated Swedish translation
## Utils
- New `utils.format_grid` for easily displaying long lists of items in a block. This is now used for the default help display.
- Add `utils.repeat` and `utils.unrepeat` as shortcuts to TickerHandler add/remove, similar
to how `utils.delay` is a shortcut for TaskHandler add.
- Add `utils/verb_conjugation` for automatic verb conjugation (English only). This
is useful for implementing actor-stance emoting for sending a string to different targets.
- `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.
- New `tasks` command for managing tasks started with `utils.delay` (PR by davewiththenicehat)
- 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.
- Made `utils.iter_to_str` format prettier strings, using Oxford comma.
- Move `create_*` functions into db managers, leaving `utils.create` only being
wrapper functions (consistent with `utils.search`). No change of api otherwise.
## Locks
- New `search:` lock type used to completely hide an object from being found by
the `DefaultObject.search` (`caller.search`) method. (CloudKeeper)
- New default for `holds()` lockfunc - changed from default of `True` to default of `False` in order to disallow dropping nonsensical things (such as things you don't hold).
## Hook changes
- Changed all `at_before/after_*` hooks to `at_pre/post_*` for consistency
across Evennia (the old names still work but are deprecated)
- New `at_pre_object_leave(obj, destination)` method on `Objects`.
- New `at_server_init()` hook called before all other startup hooks for all
startup modes. Used for more generic overriding (volund)
- 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.
- `Object.normalize_name` and `.validate_name` added to (by default) enforce latinify
on character name and avoid potential exploits using clever Unicode chars (trhr)
- 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)
- Add `Msg.db_receiver_external` field to allowe external, string-id message-receivers.
- Add `$pron()` and `$You()` inlinefuncs for pronoun parsing in actor-stance strings using `msg_contents`.
## Command changes
- Change default multi-match syntax from `1-obj`, `2-obj` to `obj-1`, `obj-2`, which seems to be what most expect.
- Split `return_appearance` hook with helper methods and have it use a template
string in order to make it easier to override.
- 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.
- Allow sending messages with `page/tell` without a `=` if target name contains no spaces.
- 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`.
- The `channel` commands replace all old channel-related commands, such as `cset` etc
- Expand `examine` command's code to much more extensible and modular. Show
attribute categories and value types (when not strings).
- Add ability to examine `/script` and `/channel` entities with `examine` command.
- Add support for `$dbref()` and `$search` when assigning an Attribute value
with the `set` command. This allows assigning real objects from in-game.
- Have `type/force` default to `update`-mode rather than `reset`mode and add more verbose
warning when using reset mode.
## Coding improvement highlights
- 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. See [Attributes](../Components/Attributes.md) documentation.
- 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.
- New Unit test parent classes, for use both in Evenia core and in mygame. Restructured unit tests to always honor default settings.
## Other
- Homogenize manager search methods to always return querysets and not sometimes querysets and sometimes lists.
- Attribute/NAttribute got a homogenous representation, using intefaces, both
`AttributeHandler` and `NAttributeHandler` has same api now.
- 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)
- Make IP throttle use Django-based cache system for optional persistence (PR by strikaco)
- 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.
concept of a dynamically created `ChannelCmdSet`.
- Prototypes now allow setting `prototype_parent` directly to a prototype-dict.
This makes it easier when dynamically building in-module prototypes.
- 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.
- 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 `settings.MXP_ENABLED=True` and `settings.MXP_OUTGOING_ONLY=True` as sane defaults, to avoid known security issues with players entering MXP links.
- Made `MonitorHandler.add/remove` support `category` for monitoring Attributes with a category (before only key was used, ignoring category entirely).

View file

@ -0,0 +1,125 @@
# 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.
First, download and install the IDE edition of your choosing.
The community edition should have everything you need,
but the professional edition has integrated support for Django which can help.
## From an existing project
Use this if you want to use PyCharm with an already existing Evennia game.
First, ensure you have completed the steps outlined [here](https://www.evennia.com/docs/latest/Setup/Installation.html#requirements).
Especially the virtualenv part, this will make setting the IDE up much easier.
1. Open Pycharm and click on the open button, open your root folder corresponding to `mygame/`.
2. Click on File -> Settings -> Project -> Python Interpreter -> Add Interpreter -> Add Local Interpreter
![Example](https://imgur.com/QRo8O1C.png)
3. Click on VirtualEnv -> Existing Interpreter -> Select your existing virtualenv folder,
should be `evenv` if you followed the default installation.
![Example](https://imgur.com/XDmgjTw.png)
## From a new project
Use this if you are starting from scratch or want to make a new Evennia game.
1. Click on the new project button.
2. Select the location for your project.
You should create two new folders, one for the root of your project and one
for the evennia game directly. It should look like `/location/projectfolder/gamefolder`
3. Select the `Custom environment` interpreter type, using `Generate New` of type `Virtual env` using a
compatible base python version as recommended in https://www.evennia.com/docs/latest/Setup/Installation.html#requirements
Then choose a folder for your virtual environment as a sub folder of your project folder.
![Example new project configuration](https://imgur.com/R5Yr9I4.png)
Click on the create button and it will take you inside your new project with a bare bones virtual environment.
To install Evennia, you can then either clone evennia in your project folder or install it via pip.
The simplest way is to use pip.
Click on the `terminal` button
![Terminal Button](https://i.imgur.com/fDr4nhv.png)
1. Type in `pip install evennia`
2. Close the IDE and navigate to the project folder
3. Rename the game folder to a temporary name and create a new empty folder with the previous name
4. Open your OS terminal, navigate to your project folder and activate your virtualenv.
On linux, `source .evenv/bin/activate`
On windows, `evenv\Scripts\activate`
5. Type in `evennia --init mygame`
6. Move the files from your temporary folder, which should contain the `.idea/` folder into
the folder you have created at step 3 and delete the now empty temporary folder.
7. In the terminal, Move into the folder and type in `evennia migrate`
8. Start evennia to ensure that it works with `evennia start` and stop it with `evennia stop`
At this point, you can reopen your IDE and it should be functional.
[Look here for additional information](https://www.evennia.com/docs/latest/Setup/Installation.html)
## Debug Evennia from inside PyCharm
### Attaching to the process
1. Launch Evennia in the pycharm terminal
2. Attempt to start it twice, this will give you the process ID of the server
3. In the PyCharm menu, select `Run > Attach to Process...`
4. From the list, pick the corresponding process id, it should be 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`)
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.
### Run Evennia with a Run/Debug 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.
#### On Windows
1. Go to `Run > Edit Configutations...`
2. Click the plus-symbol to add a new configuration and choose Python
3. Add the script: `\<yourprojectfolder>\.evenv\Scripts\evennia_launcher.py` (substitute your virtualenv if it's not named `evenv`)
4. Set script parameters to: `start -l` (-l enables console logging)
5. Ensure the chosen interpreter is your virtualenv
6. Set Working directory to your `mygame` folder (not your project folder nor evennia)
7. 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).
A dropdown box holding your new configurations should appear next to your PyCharm run button.
Select it start and press the debug icon to begin debugging.
#### On Linux
1. Go to `Run > Edit Configutations...`
2. Click the plus-symbol to add a new configuration and choose Python
3. Add the script: `/<yourprojectfolder>/.evenv/bin/twistd` (substitute your virtualenv if it's not named `evenv`)
4. Set script parameters to: `--python=/<yourprojectfolder>/.evenv/lib/python3.11/site-packages/evennia/server/server.py --logger=evennia.utils.logger.GetServerLogObserver --pidfile=/<yourprojectfolder>/<yourgamefolder>/server/server.pid --nodaemon`
5. Add an environment variable `DJANGO_SETTINGS_MODULE=server.conf.settings`
6. Ensure the chosen interpreter is your virtualenv
7. Set Working directory to your game folder (not your project folder nor evennia)
8. You can refer to the PyCharm documentation for general info, but you'll want to set at least a config name (like "MyMUD Server" or similar).
A dropdown box holding your new configurations should appear next to your PyCharm run button.
Select it start and press the debug icon to begin debugging.
Note that this only starts the server process, you can either start the portal manually or set up
the configuration for the portal. The steps are very similar to the ones above.
1. Go to `Run > Edit Configutations...`
2. Click the plus-symbol to add a new configuration and choose Python
3. Add the script: `/<yourprojectfolder>/.evenv/bin/twistd` (substitute your virtualenv if it's not named `evenv`)
4. Set script parameters to: `--python=/<yourprojectfolder>/.evenv/lib/python3.11/site-packages/evennia/server/portal/portal.py --logger=evennia.utils.logger.GetServerLogObserver --pidfile=/<yourprojectfolder>/<yourgamefolder>/server/portal.pid --nodaemon`
5. Add an environment variable `DJANGO_SETTINGS_MODULE=server.conf.settings`
6. Ensure the chosen interpreter is your virtualenv
7. Set Working directory to your game folder (not your project folder nor evennia)
8. You can refer to the PyCharm documentation for general info, but you'll want to set at least a config name (like "MyMUD Portal" or similar).
You should now be able to start both modes and get full debugging.
If you want to go one step further, you can add another config to automatically start both.
1. Go to `Run > Edit Configutations...`
2. Click the plus-symbol to add a new configuration and choose Compound
3. Add your two previous configurations, name it appropriately and press Ok.
You can now start your game with one click with full debugging active.

View file

@ -0,0 +1,64 @@
# Soft Code
Softcode is a simple programming language that was created for in-game development on TinyMUD derivatives such as MUX, PennMUSH, TinyMUSH, and RhostMUSH. The idea was that by providing a stripped down, minimalistic language for in-game use, you could allow quick and easy building and game development to happen without builders having to learn the 'hardcode' language for those servers (C/C++). There is an added benefit of not having to have to hand out shell access to all developers. Permissions in softcode can be used to alleviate many security problems.
Writing and installing softcode is done through a MUD client. Thus it is not a formatted language. Each softcode function is a single line of varying size. Some functions can be a half of a page long or more which is obviously not very readable nor (easily) maintainable over time.
## Examples of Softcode
Here is a simple 'Hello World!' command:
```bash
@set me=HELLO_WORLD.C:$hello:@pemit %#=Hello World!
```
Pasting this into a MUD client, sending it to a MUX/MUSH server and typing 'hello' will theoretically yield 'Hello World!', assuming certain flags are not set on your account object.
Setting attributes in Softcode is done via `@set`. Softcode also allows the use of the ampersand (`&`) symbol. This shorter version looks like this:
```bash
&HELLO_WORLD.C me=$hello:@pemit %#=Hello World!
```
We could also read the text from an attribute which is retrieved when emitting:
```bash
&HELLO_VALUE.D me=Hello World
&HELLO_WORLD.C me=$hello:@pemit %#=[v(HELLO_VALUE.D)]
```
The `v()` function returns the `HELLO_VALUE.D` attribute on the object that the command resides (`me`, which is yourself in this case). This should yield the same output as the first example.
If you are curious about how MUSH/MUX Softcode works, take a look at some external resources:
- https://wiki.tinymux.org/index.php/Softcode
- https://www.duh.com/discordia/mushman/man2x1
## Problems with Softcode
Softcode is excellent at what it was intended for: *simple things*. It is a great tool for making an interactive object, a room with ambiance, simple global commands, simple economies and coded systems. However, once you start to try to write something like a complex combat system or a higher end economy, you're likely to find yourself buried under a mountain of functions that span multiple objects across your entire code.
Not to mention, softcode is not an inherently fast language. It is not compiled, it is parsed with each calling of a function. While MUX and MUSH parsers have jumped light years ahead of where they once were, they can still stutter under the weight of more complex systems if those are not designed properly.
Also, Softcode is not a standardized language. Different servers each have their own slight variations. Code tools and resources are also limited to the documentation from those servers.
## Changing Times
Now that starting text-based games is easy and an option for even the most technically inarticulate, new projects are a dime a dozen. People are starting new MUDs every day with varying levels of commitment and ability. Because of this shift from fewer, larger, well-staffed games to a bunch of small, one or two developer games, the benefit of softcode fades.
Softcode is great in that it allows a mid to large sized staff all work on the same game without stepping on one another's toes without shell access. However, the rise of modern code collaboration tools (such as private github/gitlab repos) has made it trivial to collaborate on code.
## Our Solution
Evennia shuns in-game softcode for on-disk Python modules. Python is a popular, mature and professional programming language. Evennia developers have access to the entire library of Python modules out there in the wild - not to mention the vast online help resources available. Python code is not bound to one-line functions on objects; complex systems may be organized neatly into real source code modules, sub-modules, or even broken out into entire Python packages as desired.
So what is *not* included in Evennia is a MUX/MOO-like online player-coding system (aka Softcode). Advanced coding in Evennia is primarily intended to be done outside the game, in full-fledged Python modules (what MUSH would call 'hardcode'). Advanced building is best handled by extending Evennia's command system with your own sophisticated building commands.
In Evennia you develop your MU like you would any piece of modern software - using your favorite code editor/IDE and online code sharing tools.
## Your Solution
Adding advanced and flexible building commands to your game is easy and will probably be enough to satisfy most creative builders. However, if you really, *really* want to offer online coding, there is of course nothing stopping you from adding that to Evennia, no matter our recommendations. You could even re-implement MUX' softcode in Python should you be very ambitious.
In default Evennia, the [Funcparser](Funcparser) system allows for simple remapping of text on-demand without becomeing a full softcode language. The [contribs](Contrib-Overview) has several tools and utililities to start from when adding more complex in-game building.

View file

@ -0,0 +1,273 @@
# 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 custom game-dir unit tests
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 unit 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) respectively run before and after _every_ test. This can be useful for creating, configuring and cleaning up things that every test in the class needs.
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 settings.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).
```{important}
Note that these base 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](evennia.accounts.accounts.DefaultAccount) 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](evennia.objects.objects.DefaultCharacter) linked to `account2`, named `Char2`.
This has base permissions (player).
- `.obj1` - A regular [Object](evennia.objects.objects.DefaultObject) named "Obj".
- `.obj2` - Another [Object](evennia.objects.objects.DefaultObject) 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](evennia.objects.objects.DefaultRoom) 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/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 .
```

View file

@ -0,0 +1,344 @@
# Coding using Version Control
[Version control](https://en.wikipedia.org/wiki/Version_control) allows you to track changes to your code. You can save 'snapshots' of your progress which means you can roll back undo things easily. Version control also allows you to easily back up your code to an online _repository_ such as Github. It also allows you to collaborate with others on the same code without clashing or worry about who changed what.
```{sidebar} Do it!
It's _strongly_ recommended that you [put your game folder under version control](#putting-your-game-dir-under-version-control). Using git is is also the way to contribue to Evennia itself.
```
Evennia uses the most commonly used version control system, [Git](https://git-scm.com/) . For additional help on using Git, please refer to the [Official GitHub documentation](https://help.github.com/articles/set-up-git#platform-all).
## Setting up 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/).
> You can find expanded instructions for installation [here](https://git-scm.com/book/en/Getting-Started-Installing-Git).
```{sidebar} Git user nickname
If you ever make your code available online (or contribute to Evennia), your name will be visible to those reading the code-commit history. So if you are not comfortable with using your real, full name online, put a nickname (or your github handler) here.
```
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.
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"
> To get a running start with Git, here's [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).
## Common Git commands
```{sidebar} Git repository
This is just a fancy name for the folder you have designated to be under version control. We will make your `mygame` game folder into such a repository. The Evennia code is also in a (separate) git repository.
```
Git can be controlled via a GUI. But it's often easier to use the base terminal/console commands, since it makes it clear if something goes wrong.
All these actions need to be done from inside the _git repository_ .
Git may seem daunting at first. But when working with git, you'll be using the same 2-3 commands 99% of the time. And you can make git _aliases_ to have them be even easier to remember.
### `git init`
This initializes a folder/directory on your drive as a 'git repository'
git init .
The `.` means to apply to the current directory. If you are inside `mygame`, this makes your game dir into a git repository. That's all there is to it, really. You only need to do this once.
### `git add`
git add <file>
This tells Git to start to _track_ the file under version control. You need to do this when you create a new file. You can also add all files in your current directory:
git add .
Or
git add *
All files in the current directory are now tracked by Git. You only need to do this once for every file you want to track.
### `git commit`
git commit -a -m "This is the initial commit"
This _commits_ your changes. It stores a snapshot of all (`-a`) your code at the current time, adding a message `-m` so you know what you did. Later you can _check out_ your code the way it was at a given time. The message is mandatory and you will thank yourself later if write clear and descriptive log messages. If you don't add `-m`, a text editor opens for you to write the message instead.
The `git commit` is something you'll be using all the time, so it can be useful to make a _git alias_ for it:
git config --global alias.cma 'commit -a -m'
After you've run this, you can commit much simpler, like this:
git cma "This is the initial commit"
Much easier to remember!
### `git status`, `git diff` and `git log`
git status -s
This gives a short (`-s`) of the files that changes since your last `git commit`.
git diff --word-diff`
This shows exactly what changed in each file since you last made a `git commit`. The `--word-diff` option means it will mark if a single word changed on a line.
git log
This shows the log of all `commits` done. Each log will show you who made the change, the commit-message and a unique _hash_ (like `ba214f12ab12e123...`) that uniquely describes that commit.
You can make the `log` command more succinct with some more options:
ls=log --pretty=format:%C(green)%h\ %C(yellow)[%ad]%Cred%d\ %Creset%s%Cblue\ [%an] --decorate --date=relative
This adds coloration and another fancy effects (use `git help log` to see what they mean).
Let's add aliases:
git config --global alias.st 'status -s'
git config --global alias.df 'diff --word-diff'
git config --global alias.ls 'log --pretty=format:%C(green)%h\ %C(yellow)[%ad]%Cred%d\ %Creset%s%Cblue\ [%an] --decorate --date=relative'
You can now use the much shorter
git st # short status
git dif # diff with word-marking
git ls # log with pretty formatting
for these useful functions.
### `git branch`, `checkout` and `merge`
Git allows you to work with _branches_. These are separate development paths your code may take, completely separate from each other. You can later _merge_ the code from a branch back into another branch. Evennia's `main` and `develop` branches are examples of this.
git branch -b branchaname
This creates a new branch, exactly identical to the branch you were on. It also moves you to that branch.
git branch -D branchname
Deletes a branch.
git branch
Shows all your branches, marking which one you are currently on.
git checkout branchname
This checks out another branch. As long as you are in a branch all `git commit`s will commit the code to that branch only.
git checkout .
This checks out your _current branch_ and has the effect of throwing away all your changes since your last commit. This is like undoing what you did since the last save point.
git checkout b2342bc21c124
This checks out a particular _commit_, identified by the hash you find with `git log`. This open a 'temporary branch' where the code is as it was when you made this commit. As an example, you can use this to check where a bug was introduced. Check out an existing branch to go back to your normal timeline, or use `git branch -b newbranch` to break this code off into a new branch you can continue working from.
git merge branchname
This _merges_ the code from `branchname` into the branch you are currently in. Doing so may lead to _merge conflicts_ if the same code changed in different ways in the two branches. See [how to resolve merge conflicts in git](https://phoenixnap.com/kb/how-to-resolve-merge-conflicts-in-git) for more help.
### `git glone`, `git push` and `git pull`
All of these other commands have dealt with code only sitting in your local repository-folder. These commands instead allows you to exchange code with a _remote_ repository - usually one that is online (like on github).
> How you actually set up a remote repository is described [in the next section](#pushing-your-code-online).
git clone repository/path
This copies the remote repository to your current location. If you used the [Git installation instructions](../Setup/Installation-Git.md) to install Evennia, this is what you used to get your local copy of the Evennia repository.
git pull
Once you cloned or otherwise set up a remote repository, using `git pull` will re-sync the remote with what you have locally. If what you download clashes with local changes, git will force you to `git commit` your changes before you can continue with `git pull`.
git push
This uploads your local changes _of your current branch_ to the same-named branch on the remote repository. To be able to do this you must have write-permissions to the remote repository.
### Other git commands
There are _many_ other git commands. Read up on them online:
git reflog
Shows hashes of individual git actions. This allows you to go back in the git event history itself.
git reset
Force reset a branch to an earlier commit. This could throw away some history, so be careful.
git grep -n -I -i <query>
Quickly search for a phrase/text in all files tracked by git. Very useful to quickly find where things are. Set up an alias `git gr` with
```
git config --global alias.gr 'grep -n -I -i'
```
## Putting your game dir under version control
This makes use of the git commands listed in the previous section.
```{sidebar} git aliases
If you set up the git aliases for commands suggested in the previous section, you can use them instead!
```
cd mygame
git init .
git add *
git commit -a -m "Initial commit"
Your game-dir is now tracked by git.
You will notice that some files are not covered by your git version control, notably your secret-settings file (`mygame/server/conf/secret_settings.py`) and your sqlite3 database file `mygame/server/evennia.db3`. This is intentional and controlled from the file `mygame/.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. Make one mistake or local change and after a few commits and reverts you will have lost track of what is in your database or not. If you want to backup your SQlite3 database, do so by simply copying the database file to a safe location.
```
### 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 `git push` it to your own remote repository on GitHub. So for this you need a (free) Github account.
If you don't want your code to be publicly visible, Github also allows you set up a _private_ repository, only visible to you.
Create a new, empty repository on Github. [Github explains how here](https://help.github.com/articles/create-a-repo/) . _Don't_ allow it to add a README, license etc, that will just clash with what we upload later.
```{sidebar} Origin
We label the remote repository 'origin'. This is the git default and means we won't need to specify it explicitly later.
```
Make sure you are in your local game dir (previously initialized as a git repo).
git remote add origin <github URL>
This tells Git that there is a remote repository at `<github URL>`. See the github docs as to which URL to use. Verify that the remote works with `git remote -v`
Now we push to the remote (labeled 'origin' which is the default):
git push
Depending on how you set up your authentication with github, you may be asked to enter your github username and password. If you set up SSH authentication, this command will just work.
You use `git push` to upload your local changes so the remote repository is in sync with your local one. If you edited a file online using the Github editor (or a collaborator pushed code), you use `git pull` to sync in the other direction.
## Contributing to Evennia
If you want to help contributing to Evennia you must do so by _forking_ - making your own remote copy of the Evennia repository on Github. So for this, you need a (free) Github account. Doing so is a completely separate process from [putting your game dir under version control](#putting-your-game-dir-under-version-control) (which you should also do!).
At the top right of [the evennia github page](https://github.com/evennia/evennia), click the "Fork" button:
![fork button](../_static/images/fork_button.png)
This will create a new online fork Evennia under your github account.
The fork only exists online as of yet. In a terminal, `cd` to the folder you wish to develop in. This folder should _not_ be your game dir, nor the place you cloned Evennia into if you used the [Git installation](../Setup/Installation-Git.md).
From this directory run the following command:
git clone https://github.com/yourusername/evennia.git evennia
This will download your fork to your computer. It creates a new folder `evennia/` at your current location. If you installed Evennia using the [Git installation](../Setup/Installation-Git.md), this folder will be identical in content to the `evennia` folder you cloned during that installation. The difference is that this repo is connected to your remote fork and not to the 'original' _upstream_ Evennia.
When we cloned our fork, git automatically set up a 'remote repository' labeled `origin` pointing to it. So if we do `git pull` and `git push`, we'll push to our fork.
We now want to add a second remote repository linked to the original Evennia repo. We will label this remote repository `upstream`:
cd evennia
git remote add upstream https://github.com/evennia/evennia.git
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 main
git checkout develop
to switch between the branches.
To pull the latest from upstream Evennia, just checkout the branch you want and do
git pull upstream
```{sidebar} Pushing to upstream
You can't do `git push upstream` unless you have write-access to the upstream Evennia repository. So there is no risk of you accidentally pushing your own code into the main, public repository.
```
### Fixing an Evennia bug or feature
This should be done in your fork of Evennia. You should _always_ do this in a _separate git branch_ based off the Evennia branch you want to improve.
git checkout main (or develop)
git branch -b myfixbranch
Now fix whatever needs fixing. Abide by the [Evennia code style](./Evennia-Code-Style.md). You can `git commit` commit your changes along the way as normal.
Upstream Evennia is not standing still, so you want to make sure that your work is up-to-date with upstream changes. Make sure to first commit your `myfixbranch` changes, then
git checkout main (or develop)
git pull upstream
git checkout myfixbranch
git merge main (or develop)
Up to this point your `myfixbranch` branch only exists on your local computer. No
one else can see it.
git push
This will automatically create a matching `myfixbranch` in your forked version of Evennia and push to it. On github you will be able to see appear it in the `branches` dropdown. You can keep pushing to your remote `myfixbranch` as much as you like.
Once you feel you have something to share, you need to [create a pull request](https://github.com/evennia/evennia/pulls) (PR):
This is a formal request for upstream Evennia to adopt and pull your code into the main repository.
1. Click `New pull request`
2. Choose `compare across forks`
3. Select your fork from dropdown list of `head repository` repos. Pick the right branch to `compare`.
4. On the Evennia side (to the left) make sure to pick the right `base` branch: If you want to contribute a change to the `develop` branch, you must pick `develop` as the `base`.
5. Then click `Create pull request` and fill in as much information as you can in the form.
6. Optional: Once you saved your PR, you can go into your code (on github) and add some per-line comments; this can help reviewers by explaining complex code or decisions you made.
Now you just need to wait for your code to be reviewed. Expect to get feedback and be asked to make changes, add more documentation etc. Getting as PR merged can take a few iterations.
```{sidebar} Not all PRs can merge
While most PRs get merged, Evennia can't **guarantee** that your PR code will be deemed suitable to merge into upstream Evennia. For this reason it's a good idea to check in with the community _before_ you spend a lot of time on a large piece of code (fixing bugs is always a safe bet though!)
```
## Troubleshooting
### Getting 403: Forbidden access
Some users have experienced this on `git push` to their remote repository. They are not asked for username/password (and don't have a ssh key set up).
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>
```

View file

@ -0,0 +1,90 @@
# Accounts
```
┌──────┐ │ ┌───────┐ ┌───────┐ ┌──────┐
│Client├─┼──►│Session├───►│Account├──►│Object│
└──────┘ │ └───────┘ └───────┘ └──────┘
^
```
An _Account_ represents a unique game account - one player playing the game. Whereas a player can potentially connect to the game from several Clients/Sessions, they will normally have only one Account.
The Account object has no in-game representation. 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](../Concepts/Connection-Styles.md#multisession-mode-and-multiplaying)
Apart from storing login information and other account-specific data, the Account object is what is chatting on Evennia's default [Channels](./Channels.md). It is also a good place to store [Permissions](./Locks.md) to be consistent between different in-game characters. It can also hold player-level configuration options.
The Account object has its own default [CmdSet](./Command-Sets.md), the `AccountCmdSet`. The commands in this set are available to the player no matter which character they are puppeting. Most notably the default game's `exit`, `who` and chat-channel commands are in the Account cmdset.
> ooc
The default `ooc` command causes you to leave your current puppet and go into OOC mode. In this mode you have no location and have only the Account-cmdset available. It acts a staging area for switching characters (if your game supports that) as well as a safety fallback if your character gets accidentally deleted.
> ic
This re-puppets the latest 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).
## Working with Accounts
You will usually not want more than one Account typeclass for all new accounts.
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):
# [...]
def 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.

View file

@ -0,0 +1,516 @@
# 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.
## Working with Attributes
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 .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.
```{warning}
While the `AttributeProperty` uses the `AttributeHandler` (`.attributes`) under the hood, the reverse is _not_ true. The `AttributeProperty` has helper methods, like `at_get` and `at_set`. These will _only_ be called if you access the Attribute using the property.
That is, if you do `obj.yourattribute = 1`, the `AttributeProperty.at_set` will be called. But while doing `obj.db.yourattribute = 1`, will lead to the same Attribute being saved, this is 'bypassing' the `AttributeProperty` and using the `AttributeHandler` directly. So in this case the `AttributeProperty.at_set` will _not_ be called. If you added some special functionality in `at_get` this may be confusing.
To avoid confusion, you should aim to be consistent in how you access your Attributes - if you use a `AttributeProperty` to define it, use that also to access and modify the Attribute later.
```
### 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`.
### 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.
## Querying by Attribute
While you can get attributes using the `obj.attributes.get` handler, you can also find objects based on the Attributes they have through the `db_attributes` many-to-many field available on each typeclassed entity:
```python
# find objects by attribue assigned (regardless of value)
objs = evennia.ObjectDB.objects.filter(db_attributes__db_key="foo")
# find objects with attribute of particular value assigned to them
objs = evennia.ObjectDB.objects.filter(db_attributes__db_key="foo", db_attributes__db_value="bar")
```
```{important}
Internally, Attribute values are stored as _pickled strings_ (see next section). When querying, your search string is converted to the same format and matched in that form. While this means Attributes can store arbitrary Python structures, the drawback is that you cannot do more advanced database comparisons on them. For example doing `db_attributes__db__value__lt=4` or `__gt=0` will not work since less-than and greater-than doesn't do what you want between strings.
```
## 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/3/library/functions.html#tuple), like `(1,2,"test", <dbobj>)`.
* [Lists](https://docs.python.org/3/tutorial/datastructures.html#more-on-lists), like `[1,2,"test", <dbobj>]`.
* [Dicts](https://docs.python.org/3/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/3/library/collections.html#collections.OrderedDict),
like `OrderedDict((1,2), ("test", <dbobj>))`.
* [collections.Deque](https://docs.python.org/3/library/collections.html#collections.deque), like `deque((1,2,"test",<dbobj>))`.
* [collections.DefaultDict](https://docs.python.org/3/library/collections.html#collections.defaultdict) like `defaultdict(list)`.
* *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.
## 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. However, if you were to _delete_ a database object you previously stored in an `NAttribute`, the `NAttribute` will not know about this and may give you a python object without a matching database entry. In comparison, an `Attribute` always checks this). If this is a concern, use an `Attribute` or check that the object's `.pk` property is not `None` before saving it.
- 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!

View file

@ -0,0 +1,163 @@
# 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).
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.
- `#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.

View file

@ -0,0 +1,137 @@
# 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).
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 main issue with batch-command builds is that when you run a batch-command script you (*you*, as in your 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. It also means that you may be affected by the things you create, such as mobs attacking you or traps immediately hurting you.
If you know that your rooms and objects are going to be deployed via a batch-command script, you can plan for this beforehand. To help with this, you can use the fact that the non-persistent Attribute `batch_batchmode` is _only_ set while the batch-processor is running. Here's an example of how to use it:
```python
class HorribleTrapRoom(Room):
# ...
def at_object_receive(self, received_obj, source_location, **kwargs):
"""Apply the horrible traps the moment the room is entered!"""
if received_obj.ndb.batch_batchmode:
# skip if we are currently building the room
return
# commence horrible trap code
```
So we skip the hook if we are currently building the room. This can work for anything, including making sure mobs don't start attacking you while you are creating them.
There are other strategies, such as adding an on/off switch to actiive objects and make sure it's always set to *off* upon creation.
## Editor highlighting for .ev files
- [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.

View file

@ -0,0 +1,47 @@
# Batch Processors
```{toctree}
:maxdepth: 2
Batch-Command-Processor.md
Batch-Code-Processor.md
```
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.
## 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.

View file

@ -0,0 +1,387 @@
# 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 telepathy (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.
```
## Working with channels
### Viewing and joining channels
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 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
### Talk 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
Evennia can create certain channels when it starts. Channels can also
be created on-the-fly in-game.
#### Default channels from settings
You can specify 'default' channels you want to auto-create from the Evennia
settings. New accounts will automatically be subscribed to such 'default' channels if
they have the right permissions. This is a list of one dict per channel (example is the default public channel):
```python
# in mygame/server/conf/settings.py
DEFAULT_CHANNELS = [
{
"key": "Public",
"aliases": ("pub",),
"desc": "Public discussion",
"locks": "control:perm(Admin);listen:all();send:all()",
},
]
```
Each dict is fed as `**channeldict` into the [create_channel](evennia.utils.create.create_channel) function, and thus supports all the same keywords.
Evennia also has two system-related channels:
- `CHANNEL_MUDINFO` is a dict describing the "MudInfo" channel. This is assumed to exist and is a place for Evennia to echo important server information. The idea is that server admins and staff can subscribe to this channel to stay in the loop.
- `CHANNEL_CONECTINFO` is not defined by default. It will receive connect/disconnect-messages and could be visible also for regular players. If not given, connection-info will just be logged quietly.
#### Managing channels in-game
To create/destroy a new channel on the fly 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 own 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 will now get an
access-denied error when trying to use use these switches.
## Using 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.
### 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 subscribers (or both).
Default channels all use `Account` subscribers.
### Channel class
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' 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).

View file

@ -0,0 +1,29 @@
# Characters
**Inheritance Tree:
```
┌─────────────┐
│DefaultObject│
└─────▲───────┘
┌─────┴──────────┐
│DefaultCharacter│
└─────▲──────────┘
│ ┌────────────┐
│ ┌─────────►ObjectParent│
│ │ └────────────┘
┌───┴─┴───┐
│Character│
└─────────┘
```
_Characters_ is an in-game [Object](./Objects.md) commonly used to represent the player's in-game avatar. The empty `Character` class is found in `mygame/typeclasses/characters.py`. It inherits from [DefaultCharacter](evennia.objects.objects.DefaultCharacter) and the (by default empty) `ObjectParent` class (used if wanting to add share properties between all in-game Objects).
When a new [Account](./Accounts.md) logs in to Evennia for the first time, a new `Character` object is created and the [Account](./Accounts.md) will be set to _puppet_ it. By default this first Character will get the same name as the Account (but Evennia supports [alternative connection-styles](../Concepts/Connection-Styles.md) if so desired).
A `Character` object will usually have a [Default Commandset](./Command-Sets.md) set on itself at creation, or the account will not be able to issue any in-game commands!
If you want to change the default character created by the default commands, you can change it in settings:
BASE_CHARACTER_TYPECLASS = "typeclasses.characters.Character"
This deafult points at the empty class in `mygame/typeclasses/characters.py` , ready for you to modify as you please.

View file

@ -0,0 +1,229 @@
# 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.
> This is just a small selection of the tools in `evennia/utils`. It's worth to browse [the directory](evennia.utils) and in particular the content of [evennia/utils/utils.py](evennia.utils.utils) directly to find more useful stuff.
## 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`.
See also the [Debugging](../Coding/Debugging.md) documentation for help with finding elusive bugs.
## 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()
This allows for making a delayed call.
```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)
utils.delay(10, _callback, obj, "Echo!", persistent=False)
# code here will run immediately, not waiting for the delay to fire!
```
See [The Asynchronous process](../Concepts/Async-Process.md#delay) for more information.
## Finding 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.

View file

@ -0,0 +1,376 @@
# 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.

View file

@ -0,0 +1,474 @@
# 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.md#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#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.
### 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.
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.
### Dynamic commands - Exits
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.
## Create a command on the fly
*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.

View file

@ -0,0 +1,79 @@
# 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).
## Base components
These are base pieces used to make an Evennia game. Most are long-lived and are persisted in the database.
```{toctree}
:maxdepth: 2
Portal-And-Server.md
Sessions.md
Typeclasses.md
Accounts.md
Objects.md
Characters.md
Rooms.md
Exits.md
Scripts.md
Channels.md
Msg.md
Attributes.md
Nicks.md
Tags.md
Prototypes.md
Help-System.md
Permissions.md
Locks.md
```
## Commands
Evennia's Command system handle everything sent to the server by the user.
```{toctree}
:maxdepth: 2
Commands.md
Command-Sets.md
Default-Commands.md
Batch-Processors.md
Inputfuncs.md
```
## Utils and tools
Evennia provides a library of code resources to help the creation of a game.
```{toctree}
:maxdepth: 2
Coding-Utils.md
EvEditor.md
EvForm.md
EvMenu.md
EvMore.md
EvTable.md
FuncParser.md
MonitorHandler.md
OnDemandHandler.md
TickerHandler.md
Signals.md
```
## Web components
Evennia is also its own webserver, with a website and in-browser webclient you can expand on.
```{toctree}
:maxdepth: 2
Website.md
Webclient.md
Web-Admin.md
Webserver.md
Web-API.md
Web-Bootstrap-Framework.md
```

View file

@ -0,0 +1,96 @@
# Default Commands
The full set of default Evennia commands currently contains 89 commands in 9 source
files. Our policy for adding default commands is outlined [here](../Coding/Default-Command-Syntax.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**](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_)
- [**discord2chan** [discord]](CmdDiscord2Chan) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
- [**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_)

View file

@ -0,0 +1,172 @@
# 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.
## Working with EvEditor
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 before <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.

View file

@ -0,0 +1,3 @@
# EvForm
[Docstring in evennia/utils/evform.py](evennia.utils.evform)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
# 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.
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.

View file

@ -0,0 +1,3 @@
# EvTable
[Docstring in evennia/utils/evtable.py](evennia.utils.evtable)

View file

@ -0,0 +1,61 @@
# Exits
**Inheritance Tree:**
```
┌─────────────┐
│DefaultObject│
└─────▲───────┘
┌─────┴─────┐
│DefaultExit│
└─────▲─────┘
│ ┌────────────┐
│ ┌─────►ObjectParent│
│ │ └────────────┘
┌─┴─┴┐
│Exit│
└────┘
```
*Exits* are in-game [Objects](./Objects.md) connecting other objects (usually [Rooms](./Rooms.md)) together.
> Note that Exits are one-way objects, so in order for two Rooms to be linked bi-directionally, there will need to be two exits.
An object named `north` or `in` might be exits, as well as `door`, `portal` or `jump out the window`.
An exit has two things that separate them from other objects.
1. Their `.destination` property is set and points to a valid target location. This fact makes it easy and fast to locate exits in the database.
2. 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 default exit functionality is all defined on the [DefaultExit](DefaultExit) typeclass. You could in principle completely change how exits work in your game by overriding this - 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.
Exits are normally overridden on a case-by-case basis, but if you want to change the default exit created by rooms like `dig`, `tunnel` or `open` you can change it in settings:
BASE_EXIT_TYPECLASS = "typeclasses.exits.Exit"
In `mygame/typeclasses/exits.py` there is an empty `Exit` class for you to modify.
### Exit details
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.
### Creating Exits in code
For an example of how to create Exits programatically please see [this guide](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Creating-Things.md#linking-exits-and-rooms-in-code).

View file

@ -0,0 +1,400 @@
# FuncParser inline text parsing
The [FuncParser](evennia.utils.funcparser.FuncParser) extracts and executes 'inline functions' embedded in a string on the form `$funcname(args, kwargs)`, executes the matching 'inline function' and replaces the call with the return from the call.
To test it, let's tell Evennia to apply the Funcparser on every outgoing message. This is disabled by default (not everyone needs this functionality). To activate, add to your settings file:
FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = True
After a reload, you can try this in-game
```shell
> say I got $randint(1,5) gold!
You say "I got 3 gold!"
```
To escape the inlinefunc (e.g. to explain to someone how it works, use `$$`)
```{shell}
> say To get a random value from 1 to 5, use $$randint(1,5).
You say "To get a random value from 1 to 5, use $randint(1,5)."
```
While `randint` may look and work just like `random.randint` from the standard Python library, it is _not_. Instead it's an `inlinefunc` named `randint` made available to Evennia (which in turn uses the standard library function). For security reasons, only functions explicitly assigned to be used as inlinefuncs are viable.
You can apply the `FuncParser` manually. The parser is initialized with the inlinefunc(s) it's supposed to recognize in that string. Below is an example of a parser only understanding a single `$pow` inlinefunc:
```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
```
You don't have to define all your inline functions from scratch. In `evennia.utils.funcparser` you'll find ready-made dicts of inline-funcs you can import and plug into your parsers. See [default funcparser callables](#default-funcparser-callables) below for the defails.
## Working with FuncParser
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.
```
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 to 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 on 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.
```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 funcparser 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-Message-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 [,key])` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb
between 2nd person presence 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] [,key])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically
map pronouns (like his, herself, you, its etc) between 1st/2nd person to 3rd person.
- `$pconj(verb, [,key])` ([code](evennia.utils.funcparser.funcparser_callable_conjugate_for_pronouns)) - conjugates
a verb between 2nd and 3rd person, like `$conj`, but for pronouns instead of nouns to account for plural
gendering. For example `"$Pron(you) $pconj(smiles)"` will show to others as "He smiles" for a gender of "male", or
"They smile" for a gender of "plural".
### `evennia.prototypes.protfuncs`
This is used by the [Prototype system](./Prototypes.md) and allows for adding references inside the prototype. The funcparsing will happen before the spawn.
Available inlinefuncs to prototypes:
- All `FUNCPARSER_CALLABLES` and `SEARCHING_CALLABLES`
- `$protkey(key)` - returns the value of another key within the same prototype. Note that the system will try to convert this to a 'real' value (like turning the string "3" into the integer 3), for security reasons, not all embedded values can be converted this way. Note however that you can do nested calls with inlinefuncs, including adding your own converters.
### 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 -------

View file

@ -0,0 +1,315 @@
# Help System
```shell
> help theatre
```
```shell
------------------------------------------------------------------------------
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
------------------------------------------------------------------------------
```
```shell
> help evennia
```
```shell
------------------------------------------------------------------------------
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
-----------------------------------------------------------------------------
```
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.
The help system is accessed in-game by use of the `help` command:
help <topic>
Sub-topics are accessed as `help <topic>/<subtopic>/...`.
## Working with three types of help entries
There are three ways to generate help entries:
- In the database
- As Python modules
- From Command doc strings
### Database-stored help entries
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 ...
This will create a new help entry in the database. 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).
The [HelpEntry](evennia.help.models.HelpEntry) stores database help. It is _not_ a Typeclassed entity and can't be extended using the typeclass mechanism.
Here's how to create a database-help entry in code:
```python
from evennia import create_help_entry
entry = create_help_entry("emote",
"Emoting is important because ...",
category="Roleplaying", locks="view:all()")
```
### File-stored 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.
### Command-help entries
The `__docstring__` of [Command classes](./Commands.md) are automatically extracted into a help entry. You set `help_category` directly on the class.
```python
from evennia import Command
class MyCommand(Command):
"""
This command is great!
Usage:
mycommand [argument]
When this command is called, great things happen. If you
pass an argument, even GREATER things HAPPEN!
"""
key = "mycommand"
locks: "cmd:all();read:all()" # default
help_category = "General" # default
auto_help = True # default
# ...
```
When you update your code, the command's help will follow. 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.
### 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.
## 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`)
```
## Technical notes
#### Help-entry clashes
Should you have clashing help-entries (of the same name) between the three types of available entries, the priority is
Command-auto-help > Db-help > File-help
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.
#### The Help Entry container
All help entries (no matter the source) are parsed into an object with 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.
#### Help pagination
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`.
#### Search engine
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.
```

View file

@ -0,0 +1,169 @@
# Inputfuncs
```
Internet│
┌─────┐ │ ┌────────┐
┌──────┐ │Text │ │ ┌────────────┐ ┌─────────┐ │Command │
│Client├────┤JSON ├─┼──►commandtuple├────►Inputfunc├────►DB query│
└──────┘ │etc │ │ └────────────┘ └─────────┘ │etc │
└─────┘ │ └────────┘
│Evennia
```
The Inputfunc is the last fixed step on the [Ingoing message path](../Concepts/Messagepath.md#ingoing-message-path). The available Inputfuncs are looked up and called using `commandtuple` structures sent from the client. The job of the Inputfunc is to perform whatever action is requested, by firing a Command, performing a database query or whatever is needed.
Given a `commandtuple` on the form
(commandname, (args), {kwargs})
Evennia will 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
```
The default inputfuncs are found in [evennia/server/inputfuncs.py](evennia.server.inputfuncs).
## Adding your own inputfuncs
1. 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. i
2. `reload` the server.
To overload a default inputfunc (see below), just add a function with the same name. You can also extend the settings-list `INPUT_FUNC_MODULES`.
INPUT_FUNC_MODULES += ["path.to.my.inputfunc.module"]
All global-level functions with a name not starting with `_` in these module(s) will be used by Evennia as an inputfunc. The list is imported from left to right, so latter 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 inputs, 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. It's meant to be used by custom client setups.
### 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.

View file

@ -0,0 +1,298 @@
# 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.
## Working with locks
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 explicitly, all objects are assumed searchable.
- `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):
- `view` - if the help entry header should show up in the help index
- `read` - 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.
This is the basic definition of a lock function:
```python
def lockfunc_name(accessing_obj, accessed_obj, *args, **kwargs):
return True # or False
```
The `accessing object` is the object wanting to get access. The `accessed object` is the object being accessed (the object with the lock). The function always return a boolean determining if the lock is passed or not.
The `*args` will become the tuple of arguments given to the lockfunc. So for a lockstring `"edit:id(3)"` (a lockfunc named `id`), `*args` in the lockfunc would be `(3,)` .
The `**kwargs` dict has one default keyword always provided by Evennia, the `access_type`, which is a string with the access type being checked for. For the lockstring `"edit:id(3)"`, `access_type"` would be `"edit"`. This is unused by default Evennia.
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 `!=`.
- `tag(tagkey[, category])` - checks if the accessing_object has the specified tag and optional category.
- `objtag(tagkey[, category])` - checks if the *accessed_object* has the specified tag and optional category.
- `objloctag(tagkey[, category])` - checks if the *accessed_obj*'s location has the specified tag and optional category.
- `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.

View file

@ -0,0 +1,77 @@
# 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 Character'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")
```

View file

@ -0,0 +1,81 @@
# 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.
```
## Working with Msg
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.

View file

@ -0,0 +1,120 @@
# 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
```

View file

@ -0,0 +1,164 @@
# Objects
**Message-path:**
```
┌──────┐ │ ┌───────┐ ┌───────┐ ┌──────┐
│Client├─┼──►│Session├───►│Account├──►│Object│
└──────┘ │ └───────┘ └───────┘ └──────┘
^
```
All in-game objects in Evennia, be it characters, chairs, monsters, rooms or hand grenades are jointly referred to as an Evennia *Object*. An Object is generally something you can look and interact with in the game world. When a message travels from the client, the Object-level is the last stop.
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`:
- `Object` - the base in-game entity. Found in `mygame/typeclasses/objects.py`. Inherits directly from `DefaultObject`.
- [Characters](./Characters.md) - the normal in-game Character, controlled by a player. Found in `mygame/typeclasses/characters.py`. Inherits from `DefaultCharacter`, which is turn a child of `DefaultObject`.
- [Rooms](./Rooms.md) - a location in the game world. Found in `mygame/typeclasses/rooms.py`. Inherits from `DefaultRoom`, which is in turn a child of `DefaultObject`).
- [Exits](./Exits.md) - represents a one-way connection to another location. Found in `mygame/typeclasses/exits.py` (inherits from `DefaultExit`, which is in turn a child of `DefaultObject`).
## Object
**Inheritance Tree:**
```
┌─────────────┐
│DefaultObject│
└──────▲──────┘
│ ┌────────────┐
│ ┌─────►ObjectParent│
│ │ └────────────┘
┌─┴─┴──┐
│Object│
└──────┘
```
> For an explanation of `ObjectParent`, see next section.
The `Object` class is meant to be used as the basis for creating things that are neither characters, rooms or exits - anything from weapons and armour, equipment and houses can be represented by extending the Object class. Depending on your game, this also goes for NPCs and monsters (in some games you may want to treat NPCs as just an un-puppeted [Character](./Characters.md) instead).
You should not use Objects for game _systems_. Don't use an 'invisible' Object for tracking weather, combat, economy or guild memberships - that's what [Scripts](./Scripts.md) are for.
## ObjectParent - Adding common functionality
`Object`, as well as `Character`, `Room` and `Exit` classes all additionally inherit from `mygame.typeclasses.objects.ObjectParent`.
`ObjectParent` is an empty 'mixin' class. You can add stuff to this class that you want _all_ in-game entities to have.
Here is an 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
```
Now all of `Object`, `Exit`. `Room` and `Character` default to not being able to be picked up using the `get` command.
## Working with children of DefaultObject
This functionality is shared by all sub-classes of `DefaultObject`. You can easily add your own in-game behavior by either modifying one of the typeclasses in your game dir or by inheriting further from them.
You can put your new typeclass directly in the relevant 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)).
### 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](./Exits.md), 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.
- `appearance_template` - this helps formatting the look of the Object when someone looks at it (see next section).l
- `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](./Exits.md) 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()`.
- `return_appearance` is the main hook letting the object visually describe itself.
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).
## Changing an Object's appearance
When you type `look <obj>`, this is the sequence of events that happen:
1. The command checks if the `caller` of the command (the 'looker') passes the `view` [lock](./Locks.md) of the target `obj`. If not, they will not find anything to look at (this is how you make objects invisible).
1. The `look` command calls `caller.at_look(obj)` - that is, the `at_look` hook on the 'looker' (the caller of the command) is called to perform the look on the target object. The command will echo whatever this hook returns.
2. `caller.at_look` calls and returns the outcome of `obj.return_apperance(looker, **kwargs)`. Here `looker` is the `caller` of the command. In other words, we ask the `obj` to descibe itself to `looker`.
3. `obj.return_appearance` makes use of its `.appearance_template` property and calls a slew of helper-hooks to populate this template. This is how the template looks by default:
```python
appearance_template = """
{header}
|c{name}|n
{desc}
{exits}{characters}{things}
{footer}
"""```
4. Each field of the template is populated by a matching helper method (and their default returns):
- `name` -> `obj.get_display_name(looker, **kwargs)` - returns `obj.name`.
- `desc` -> `obj.get_display_desc(looker, **kwargs)` - returns `obj.db.desc`.
- `header` -> `obj.get_display_header(looker, **kwargs)` - empty by default.
- `footer` -> `obj.get_display_footer(looker, **kwargs)` - empty by default.
- `exits` -> `obj.get_display_exits(looker, **kwargs)` - a list of `DefaultExit`-inheriting objects found inside this object (usually only present if `obj` is a `Room`).
- `characters` -> `obj.get_display_characters(looker, **kwargs)` - a list of `DefaultCharacter`-inheriting entities inside this object.
- `things` -> `obj.get_display_things(looker, **kwargs)` - a list of all other Objects inside `obj`.
5. `obj.format_appearance(string, looker, **kwargs)` is the last step the populated template string goes through. This can be used for final adjustments, such as stripping whitespace. The return from this method is what the user will see.
As each of these hooks (and the template itself) can be overridden in your child class, you can customize your look extensively. You can also have objects look different depending on who is looking at them. The extra `**kwargs` are not used by default, but are there to allow you to pass extra data into the system if you need it (like light conditions etc.)

View file

@ -0,0 +1,229 @@
# OnDemandHandler
This handler offers help for implementing on-demand state changes. On-demand means that the state won't be computed until the player _actually looks for it_. Until they do, nothing happens. This is the most compute-efficient way to handle your systems and you should consider using this style of system whenever you can.
Take for example a gardening system. A player goes to a room and plants a seed. After a certain time, that plant will then move through a set of stages; it will move from "seedling" to 'sprout' to 'flowering' and then on to 'wilting' and eventually 'dead'.
Now, you _could_ use `utils.delay` to track each phase, or use the [TickerHandler](./TickerHandler.md) to tick the flower. You could even use a [Script](./Scripts.md) on the flower. This would work like this:
1. The ticker/task/Script would automatically fire at regular intervals to update the plant through its stages.
2. Whenever a player comes to the room, the state is already updated on the flower, so they just read the state.
This will work fine, but if no one comes back to that room, that's a lot of updating that no one will see. While maybe not a big deal for a single player, what if you have flowers in thousands of rooms, all growing indepedently? Or some even more complex system requiring calculation on every state change. You should avoid spending computing on things that bring nothing extra to your player base.
Using the The on-demand style, the flower would instead work like this:
1. When the player plants the seed, we register _the current timestamp_ - the time the plant starts to grow. We store this with the `OnDemandHandler` (below).
2. When a player enters the room and/or looks at the plant (or a code system needs to know the plant's state), _then_ (and only then) we check _the current time_ to figure out the state the flower must now be in (the `OnDemandHandler` does the book-keeping for us). The key is that _until we check_, the flower object is completely inactive and uses no computing resources.
## A blooming flower using the OnDemandHandler
This handler is found as `evennia.ON_DEMAND_HANDLER`. It is meant to be integrated into your other code. Here's an example of a flower that goes through its stages of life in 12 hours.
```python
# e.g. in mygame/typeclasses/objects.py
from evennia import ON_DEMAND_HANDLER
# ...
class Flower(Object):
def at_object_creation(self):
minute = 60
hour = minute * 60
ON_DEMAND_HANDLER.add(
self,
category="plantgrowth"
stages={
0: "seedling",
10 * minute: "sprout",
5 * hour: "flowering",
10 * hour: "wilting",
12 * hour: "dead"
})
def at_desc(self, looker):
"""
Called whenever someone looks at this object
"""
stage = ON_DEMAND_HANDLER.get_state(self, category="plantgrowth")
match stage:
case "seedling":
return "There's nothing to see. Nothing has grown yet."
case "sprout":
return "A small delicate sprout has emerged!"
case "flowering":
return f"A beautiful {self.name}!"
case "wilting":
return f"This {self.name} has seen better days."
case "dead":
# it's dead and gone. Stop and delete
ON_DEMAND_HANDLER.remove(self, category="plantgrowth")
self.delete()
```
The `get_state(key, category=None, **kwargs)` methoid is used to get the current stage. The `get_dt(key, category=None, **kwargs)` method instead retrieves the currently passed time.
You could now create the rose and it would figure out its state only when you are actually looking at it. It will stay a seedling for 10 minutes (of in-game real time) before it sprouts. Within 12 hours it will be dead again.
If you had a `harvest` command in your game, you could equally have it check the stage of bloom and give you different results depending on if you pick the rose at the right time or not.
The on-demand handler's tasks survive a reload and will properly account for downtime.
## More usage examples
The [OnDemandHandler API](evennia.scripts.ondemandhandler.OnDemandHandler) describes how to use the handler in detail. While it's available as `evennia.ON_DEMAND_HANDLER`, its code is located in `evennia.scripts.ondemandhandler.py`.
```python
from evennia import ON_DEMAND_HANDLER
ON_DEMAND_HANDLER.add("key", category=None, stages=None)
time_passed = ON_DEMAND_HANDLER.get_dt("key", category=None)
current_state = ON_DEMAND_HANDLER.get_stage("key", category=None)
# remove things
ON_DEMAND_HANDLER.remove("key", category=None)
ON_DEMAND_HANDLER.clear(cateogory="category") #clear all with category
```
```{sidebar} Not all stages may fire!
This is important. If no-one checks in on the flower until a time when it's already wilting, it will simply _skip_ all its previous stages, directly to the 'wilting' stage. So don't write code for a stage that assumes previous stages to have made particular changes to the object - those changes may not have happened because those stages could have been skipped entirely!
```
- The `key` can be a string, but also a typeclassed object (its string representation will be used, which normally includes its `#dbref`). You can also pass a `callable` - this will be called without arguments and is expected to return a string to use for the `key`. Finally, you can also pass [OnDemandTask](evennia.scripts.ondemandhandler.OnDemandTask) entities - these are the objects the handler uses under the hood to represent each task.
- The `category` allows you to further categorize your demandhandler tasks to make sure they are unique. Since the handler is global, you need to make sure `key` + `category` is unique. While `category` is optional, if you use it you must also use it to retrieve your state later.
- `stages` is a `dict` `{dt: statename}` or `{dt: (statename, callable)}` that represents how much time (in seconds) from _the start of the task_ it takes for that stage to begin. In the flower example above, it was 10 hours until the `wilting` state began. If a callable is included, it will fire the first time that stage is reached. This callable takes the current `OnDemandTask` and `**kwargs` as arguments; the keywords are passed on from the `get_stages/dt` methods. [See below](#stage-callables) for information about the allowed callables. Having `stages` is optional - sometimes you only want to know how much time has passed.
- `.get_dt()` - get the current time (in seconds) since the task started. This is a `float`.
- `.get_stage()` - get the current state name, such as "flowering" or "seedling". If you didn't specify any `stages`, this will return `None`, and you need to interpret the `dt` yourself to determine which state you are in.
Under the hood, the handler uses [OnDemandTask](evennia.scripts.ondemandhandler.OnDemandTask) objects. It can sometimes be practical to create tasks directly with these, and pass them to the handler in bulk:
```python
from evennia import ON_DEMAND_HANDLER, OnDemandTask
task1 = OnDemandTask("key1", {0: "state1", 100: ("state2", my_callable)})
task2 = OnDemandTask("key2", category="state-category")
# batch-start on-demand tasks
ON_DEMAND_HANDLER.batch_add(task1, task2)
# get the tasks back later
task1 = ON_DEMAND_HANDLER.get("key1")
task2 = ON_DEMAND_HANDLER.get("key1", category="state-category")
# batch-deactivate tasks you have available
ON_DEMAND_HANDLER.batch_remove(task1, task2)
```
### Stage callables
If you define one or more of your `stages` dict keys as `{dt: (statename, callable)}`, this callable will be called when that stage is checked for the first time. This 'stage callable' have a few requirements:
- The stage callable must be [possible to pickle](https://docs.python.org/3/library/pickle.html#pickle-picklable) because it will be saved to the database. This basically means your callable needs to be a stand-alone function or a method decorated with `@staticmethod`. You won't be able to access the object instance as `self` directly from such a method or function - you need to pass it explicitly.
- The callable must always take `task` as its first element. This is the `OnDemandTask` object firing this callable.
- It may optionally take `**kwargs` . This will be passed down from your call of `get_dt` or `get_stages`.
Here's an example:
```python
from evennia DefaultObject, ON_DEMAND_HANDLER
def mycallable(task, **kwargs)
# this function is outside the class and is pickleable just fine
obj = kwargs.get("obj")
# do something with the object
class SomeObject(DefaultObject):
def at_object_creation(self):
ON_DEMAND_HANDLER.add(
"key1",
stages={0: "new", 10: ("old", mycallable)}
)
def do_something(self):
# pass obj=self into the handler; to be passed into
# mycallable if we are in the 'old' stage.
state = ON_DEMAND_HANDLER.get_state("key1", obj=self)
```
Above, the `obj=self` will passed into `mycallable` once we reach the 'old' state. If we are not in the 'old' stage, the extra kwargs go nowhere. This way a function can be made aware of the object calling it while still being possible to pickle. You can also pass any other information into the callable this way.
> If you don't want to deal with the complexity of callables you can also just read off the current stage and do all the logic outside of the handler. This can often be easier to read and maintain.
### Looping repeatedly
Normally, when a sequence of `stages` have been cycled through, the task will just stop at the last stage indefinitely.
`evennia.OnDemandTask.stagefunc_loop` is an included static-method stage callable you can use to make the task loop. Here's an example of how to use it:
```python
from evennia import ON_DEMAND_HANDLER, OnDemandTask
ON_DEMAND_HANDLER.add(
"trap_state",
stages={
0: "harmless",
50: "solvable",
100: "primed",
200: "deadly",
250: ("_reset", OnDemandTask.stagefunc_loop)
}
)
```
This is a trap state that loops through its states depending on timing. Note that the looping helper callable will _immediately_ reset the cycle back to the first stage, so the last stage will never be visible to the player/game system. So it's a good (if optional) idea to name it with `_*` to remember this is a 'virtual' stage. In the example above, the "deadly" state will cycle directly to "harmless".
The `OnDemandTask` task instance has a `.iterations` variable that will go up by one for every loop.
If the state is not checked for a long time, the looping function will correctly update the `.iterations` property on the task it would have used so far and figure out where in the cycle it is right now.
### Bouncing back and forth
`evennia.OnDemandTask.stagefunc_bounce` is an included static-method callable you can use to 'bounce' the sequence of stages. That is, it will cycle to the end of the cycle and then reverse direction and cycle through the sequence in reverse, keeping the same time intervals between each stage.
To make this repeat indefinitely, you need to put these callables at both ends of the list:
```python
from evennia import ON_DEMAND_HANDLER, OnDemandTask
ON_DEMAND_HANDLER.add(
"cycling reactor",
"nuclear",
stages={
0: ("cold", OnDemandTask.stagefunc_bounce),
150: "luke warm",
300: "warm",
450: "hot"
600: ("HOT!", OnDemandTask.stagefunc_bounce)
}
)
```
This will cycle
cold -> luke warm -> warm -> hot -> HOT!
before reversing and go back (over and over):
HOT! -> hot -> warm -> luke warm -> cold
Unlike the `stagefunc_loop` callable, the bouncing one _will_ visibly stay at the first and last stage until it changes to the next one in the sequence. The `OnDemandTask` instance has an `.iterations` property that will step up by one every time the sequence reverses.
If the state is not checked for a long time, the bounce function will correctly update the `.iterations` property to the amount of iterations it would have done in that time, and figure out where in the cycle it is right now.
## When is it not suitable to do things on-demand?
If you put your mind to it, you can probably make of your game on-demand. The player will not be the wiser.
There is only really one case where on-demand doesn't work, and that is if the player should be informed of something _without first providing any input_.
If a player has to run `check health` command to see how much health they have, that could happen on demand. Similarly, a prompt could be set to update every time you move. But if you would an idling player to get a message popping up out of nowhere saying "You are feeling hungry" or to have some HP meter visually increasing also when standing still, then some sort of timer/ticker would be necessary to crank the wheels.
Remember however, that in a text-medium (especially with traditional line-by-line MUD clients), there is only so much spam you can push on the player before they get overwhelmed.

View file

@ -0,0 +1,179 @@
# 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`.
## 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.
## Working with Permissions
In-game, you use the `perm` command to add and remove permissions
> 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". If you don't want to use `/account`, you can also prefix the name with `*` to indicate an Account is sought:
> perm *Tommy = Builders
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/puppets.
If you _do_ want to start using the permissions on your _puppet_, you use `quell`
> quell
> unquell
This drops to the permissions on the puppeted object, and then back to your Account-permissions again. Quelling is useful if you want to try something "as" someone else. It's also useful for superusers since this makes them susceptible to locks (so they can test things).
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):
Guest # temporary account, only used if GUEST_ENABLED=True (lowest)
Player # can chat and send tells (default level)
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).
When checking a hierarchical permission (using one of the methods to follow), you will pass checks for your level *and below*. That is, if you have the "Admin" hierarchical permission, you will also pass checks asking for "Builder", "Helper" and so on.
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.
```
## 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.

View file

@ -0,0 +1,26 @@
# Portal And Server
```
Internet│ ┌──────────┐ ┌─┐ ┌─┐ ┌─────────┐
│ │Portal │ │S│ ┌───┐ │S│ │Server │
P │ │ │ │e│ │AMP│ │e│ │ │
l ──┼──┤ Telnet ├─┤s├───┤ ├───┤s├─┤ │
a │ │ Webclient│ │s│ │ │ │s│ │ Game │
y ──┼──┤ SSH ├─┤i├───┤ ├───┤i├─┤ Database│
e │ │ ... │ │o│ │ │ │o│ │ │
r ──┼──┤ ├─┤n├───┤ ├───┤n├─┤ │
s │ │ │ │s│ └───┘ │s│ │ │
│ └──────────┘ └─┘ └─┘ └─────────┘
│Evennia
```
The _Portal_ and _Server_ consitutes the two main halves of Evennia.
These are two separate `twistd` processes and can be controlled from inside the game or from the command line as described [in the Running-Evennia doc](../Setup/Running-Evennia.md).
- The Portal knows everything about internet protocols (telnet, websockets etc), but knows very little about the game.
- The Server knows everything about the game. It knows that a player has connected but now _how_ they connected.
The effect of this is that you can fully `reload` the Server and have players still connected to the game. One the server comes back up, it will re-connect to the Portal and re-sync all players as if nothing happened.
The Portal and Server are intended to always run on the same machine. They are glued together via an AMP (Asynchronous Messaging Protocol) connection. This allows the two programs to communicate seamlessly.

View file

@ -0,0 +1,243 @@
# Spawner and Prototypes
```shell
> spawn goblin
Spawned Goblin Grunt(#45)
```
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.
## Working with Prototypes
### 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."}
```
> If you want to escape a protfunc and have it appear verbatim, use `$$funcname()`.
At spawn 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. See the FuncParse for a lot more information.
Here is how a protfunc is defined (same as other funcparser functions).
```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` on failure.
The parser will always include the following reserved `kwargs`:
- `session` - the current [Session](evennia.server.ServerSession) performing the spawning.
- `prototype` - The Prototype-dict this function is a part of. This is intended to be used _read-only_. Be careful to modify a mutable structure like this from inside the function - you can cause really hard-to-find bugs this way.
- `current_key` - The current key of the `prototype` dict under which this protfunc is executed.
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 searched globally by key, tag or #dbref. Requires `caller` kwarg in `spawner.spawn()` for access checks. See [searching callables](./FuncParser.md#evenniautilsfuncparsersearching_callables). ($dbref(<query>) is an alias and works the same) |
| `$objlist(<query>)` | Like `$obj`, except always returns a list of zero, one or more results. Requires `caller` kwarg in `spawner.spawn()` for access checks. See [searching callables](./FuncParser.md#evenniautilsfuncparsersearching_callables). |
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.
## 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.
## Spawning
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](https://www.evennia.com/docs/latest/api/evennia.prototypes.spawner.html#evennia.prototypes.spawner.spawn)

View file

@ -0,0 +1,31 @@
# Rooms
**Inheritance Tree:**
```
┌─────────────┐
│DefaultObject│
└─────▲───────┘
┌─────┴─────┐
│DefaultRoom│
└─────▲─────┘
│ ┌────────────┐
│ ┌─────►ObjectParent│
│ │ └────────────┘
┌─┴─┴┐
│Room│
└────┘
```
[Rooms](evennia.objects.objects.DefaultRoom) are in-game [Objects](./Objects.md) representing the root containers of all other objects.
The only thing technically 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 `evennia.DefaultRoom`.
To change the default room created by `dig`, `tunnel` and other default commands, change it in settings:
BASE_ROOM_TYPECLASS = "typeclases.rooms.Room"
The empty class in `mygame/typeclasses/rooms.py` is a good place to start!
While the default Room is very simple, there are several Evennia [contribs](../Contribs/Contribs-Overview.md) customizing and extending rooms with more functionality.

View file

@ -0,0 +1,384 @@
# 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 previous 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.
```
## Working with Scripts
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 Script
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-script) above.
### 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()
```
## Using GLOBAL_SCRIPTS
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_script` 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.

View file

@ -0,0 +1,121 @@
# Sessions
```
┌──────┐ │ ┌───────┐ ┌───────┐ ┌──────┐
│Client├─┼──►│Session├───►│Account├──►│Object│
└──────┘ │ └───────┘ └───────┘ └──────┘
^
```
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.
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.
## Working with Sessions
### 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.
### 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](evennia.server.session) 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/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](evennia.server.sessionhandler) module for details on the capabilities of the `ServerSessionHandler`.

View file

@ -0,0 +1,122 @@
# 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/4.1/topics/signals/).
## Working with Signals
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.
- `SIGNAL_EXIT_TRAVERSED` - fires when an exit is traversed, just after `at_traverse` hook. The `sender` is the exit itself, `traverser=` keyword hold the one traversing the exit.
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.

View file

@ -0,0 +1,226 @@
# Tags
_Tags_ are short text lables one can 'attach' to objects in order to organize, group and quickly find out their properties, similarly to how you attach labels to your luggage.
```{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 or TagCategoryProperty
from evennia import DefaultObject
from evennia import TagProperty, TagCategoryProperty
class Sword(DefaultObject):
# name of property is the tagkey, category as argument
can_be_wielded = TagProperty(category='combat')
has_sharp_edge = TagProperty(category='combat')
# name of property is the category, tag-keys are arguments
damage_type = TagCategoryProperty("piercing", "slashing")
crafting_element = TagCategoryProperty("blade", "hilt", "pommel")
```
In-game, tags are controlled by the default `tag` command:
> tag Chair = furniture
> tag Chair = furniture
> tag Table = furniture
> tag/search furniture
Chair, Sofa, Table
An Evennia entity can be tagged by any number of tags. Tags 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; rather the existence of the tag itself is what is checked - a given object either has a given tag or not.
In code, you manage Tags using the `TagHandler` (`.tags`) on typeclassed entities. You can also assign Tags on the class level through the `TagProperty` (one tag, one category per line) or the `TagCategoryProperty` (one category, multiple tags per line). Both of these use the `TagHandler` under the hood, they are just convenient ways to add tags already when you define your class.
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 the `belongs_to_fighter_guild` tag.
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).
## Working with Tags
### Searching for tags
The common way to use tags (once they have been set) is find all objects tagged with a particular tag combination:
objs = evennia.search_tag(key=("foo", "bar"), category='mycategory')
As shown above, you can also have tags without a category (category of `None`).
```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/4.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
### TagHandler
This is the main way to work with tags when you have the entry already. This handler sits on all typeclassed entities as `.tags` and you use `.tags.add()`, `.tags.remove()` and `.tags.has()` to manage Tags on the object. [See the api docs](evennia.typeclasses.tags.TagHandler) for more useful methods.
The TagHandler can be found on any of the base *typeclassed* objects, namely [Objects](./Objects.md), [Accounts](./Accounts.md), [Scripts](./Scripts.md) and [Channels](./Channels.md) (as well as their children). Here are some examples of use:
```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.
### TagProperty
This is used as a property when you create a new class:
```python
from evennia import TagProperty
from typeclasses import Object
class MyClass(Object):
mytag = TagProperty(tagcategory)
```
This will create a Tag named `mytag` and category `tagcategory` in the database. You'll be able to find it by `obj.mytag` but more useful you can find it with the normal Tag searching methods in the database.
Note that if you were to delete this tag with `obj.tags.remove("mytag", "tagcategory")`, that tag will be _re-added_ to the object next time this property is accessed!
### TagCategoryProperty
This is the inverse of `TagProperty`:
```python
from evennia import TagCategoryProperty
from typeclasses import Object
class MyClass(Object):
tagcategory = TagCategroyProperty(tagkey1, tagkey2)
```
The above example means you'll have two tags (`tagkey1` and `tagkey2`), each with the `tagcategory` category, assigned to this object.
Note that similarly to how it works for `TagProperty`, if you were to delete these tags from the object with the `TagHandler` (`obj.tags.remove("tagkey1", "tagcategory")`, then these tags will be _re-added_ automatically next time the property is accessed.
The reverse is however not true: If you were to _add_ a new tag of the same category to the object, via the `TagHandler`, then this property will include that in the list of returned tags.
If you want to 're-sync' the tags in the property with that in the database, you can use the `del` operation on it - next time the property is accessed, it will then only show the default keys you specify in it. Here's how it works:
```python
>>> obj.tagcategory
["tagkey1", "tagkey2"]
# remove one of the default tags outside the property
>>> obj.tags.remove("tagkey1", "tagcategory")
>>> obj.tagcategory
["tagkey1", "tagkey2"] # missing tag is auto-created!
# add a new tag from outside the property
>>> obj.tags.add("tagkey3", "tagcategory")
>>> obj.tagcategory
["tagkey1", "tagkey2", "tagkey3"] # includes the new tag!
# sync property with datbase
>>> del obj.tagcategory
>>> obj.tagcategory
["tagkey1", "tagkey2"] # property/database now in sync
```
## 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.
```{important}
Not specifying a category (default) gives the tag a category of `None`, which is also considered a unique key + category combination. You cannot use `TagCategoryProperty` to set Tags with `None` categories, since the property name may not be `None`. Use the `TagHandler` (or `TagProperty`) for this.
```
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.
## 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.

View file

@ -0,0 +1,102 @@
# 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.
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 to use [utils.delay](evennia.utils.utils.delay) (using the [TaskHandler](evennia.scripts.taskhandler.TaskHandler)). Another is to use the time-repeat capability of [Scripts](./Scripts.md). These tools operate on individual objects.
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, it's inefficient to set up separate delays/scripts for every such object.
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. This means that the time-keeping mechanism is only set up once for all objects, making subscribing/unsubscribing faster.
Evennia offers an optimized implementation of the subscription model - the *TickerHandler*. This is a singleton global handler reachable from [evennia.TICKER_HANDLER](evennia.utils.tickerhandler.TickerHandler). 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.
## 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.
```{important}
Everything you supply to `TickerHandler.add` will need to be pickled at some point to be saved into the database - also if you use `persistent=False`. 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.
```
You can also import a 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 be wiped by a _server shutdown_ (it will still survive a normal 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`.
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/Tutorial-Weather-Effects.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.

View file

@ -0,0 +1,264 @@
# 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│ │ScriptDB│ │ChannelDB│ │ObjectDB│
└────▲────┘ └────▲───┘ └────▲────┘ └────▲───┘
┌───────┴──────┐ ┌──────┴──────┐ ┌──────┴───────┐ ┌──────┴──────┐
2: │DefaultAccount│ │DefaultScript│ │DefaultChannel│ │DefaultObject│
└───────▲──────┘ └──────▲──────┘ └──────▲───────┘ └──────▲──────┘
│ │ │ │ Evennia
────────┼───────────────┼───────────────┼────────────────┼─────────
│ │ │ │ Gamedir
┌───┴───┐ ┌───┴──┐ ┌───┴───┐ ┌──────┐ │
3: │Account│ │Script│ │Channel│ │Object├─┤
└───────┘ └──────┘ └───────┘ └──────┘ │
┌─────────┐ │
│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/4.1/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`.
> This diagram doesn't include the `ObjectParent` mixin for `Object`, `Character`, `Room` and `Exit`. This establishes a common parent for those classes, for shared properties. See [Objects](./Objects.md) for more details.
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.
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.
## Working with typeclasses
### 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.
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/4.1/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/4.1/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 subsist in memory a lot longer than vanilla Django model instances 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.
## Will I run out of dbrefs?
Evennia does not re-use its `#dbrefs`. This means new objects get an ever-increasing `#dbref`, even if you delete older objects. There are technical and safety reasons for this. But you may wonder if this means you have to worry about a big game 'running out' of dbref integers eventually.
The answer is simply **no**.
For example, the max dbref value for the default sqlite3 database is `2**64`. If you *created 10 000 new objects every second of every minute of every day of the year it would take about **60 million years** for you to run out of dbref numbers*. That's a database of 140 TeraBytes, just to store the dbrefs, no other data.
If you are still using Evennia at that point and have this concern, get back to us and we can discuss adding dbref reuse then.

View file

@ -0,0 +1,122 @@
# 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

View file

@ -0,0 +1,158 @@
# The Web Admin
The Evennia _Web admin_ is a customized [Django admin site](https://docs.djangoproject.com/en/4.1/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/4.1/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.

View file

@ -0,0 +1,164 @@
# Bootstrap frontend framework
Evennia's 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.
e, a brief overview of Bootstrap follows. For more in-depth info, please
read [the documentation](https://getbootstrap.com/docs/4.0/getting-started/introduction/).
## Grid 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 container
The first part of the grid 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 grid
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/)
I
## General Styling elements
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.
### 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)
## Further reading
Bootstrap also provides a huge amount of utilities, as well as styling and content elements. To learn more about them, please [read the Bootstrap docs](https://getbootstrap.com/docs/4.0/getting- started/introduction/) or read one of our other web tutorials.

View file

@ -0,0 +1,286 @@
# 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 &gt;div style='s'&lt;styled text&gt;/div&lt;
* `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 vs 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.

View file

@ -0,0 +1,73 @@
# 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.

View file

@ -0,0 +1,344 @@
# 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: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/4.1/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/4.1/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/4.1/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/4.1/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/4.1/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/4.1/topics/forms/#building-a-form-in-django) on the Django website - it covers all you need.

View file

@ -0,0 +1,185 @@
# Async Process
```{important}
This is considered an advanced topic.
```
## Synchronous versus Asynchronous
```{sidebar}
This is also explored in the [Command Duration Tutorial](../Howtos/Howto-Command-Duration.md).
```
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.
## `utils.delay`
```{sidebar} delay() vs time.sleep()
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.
```
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.
```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. Provide `persistent=True` to make the delay survive a server `reload`. While waiting, you can input commands normally.
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.
## `@utils.interactive` decorator
The `@interactive` [decorator](https://realpython.com/primer-on-python- decorators/) 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.
All of this makes the `@interactive` decorator very useful. But it comes with a few caveats.
- The decorated function/method/callable must have an argument named exactly `caller`. Evennia will look for an argument with this name and treat it as the source of input.
- Decorating a function this way turns it turns it into a Python [generator](https://wiki.python.org/moin/Generators). This means
- You can't 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)
```
## `utils.run_async`
```{warning}
Unless you have a very clear purpose in mind, you are unlikely to get an expected result from `run_async`. Notably, it will still run your long-running function _in the same thread_ as the rest of the server. So while it does run async, a very heavy and CPU-heavy operation will still block the server. So don't consider this as a way to offload heavy operations without affecting the rest of the server.
```
When 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.
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.
> 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.

View file

@ -0,0 +1,115 @@
# 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`.
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.
## Creating a ban
### 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.
### 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.*
## 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.
## 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/main/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.

View file

@ -0,0 +1,189 @@
# Messages varying per 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 as 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 |
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.

View file

@ -0,0 +1,69 @@
# Clickable links
Evennia allows for clickable links in text 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 see clickable links, the player must use the Evennia webclient or a third-party telnet client with [MXP](http://www.zuggsoft.com/zmud/mxp.htm) support (*Note: Evennia only supports clickable links, no other MXP features*).
Users with clients lacking MXP support will only see the link as normal text.
```{important}
By default, clickable links can _not_ be added from in-game. Trying to do so will have the link come back as normal text. This is a security measure. See [Settings](#settings) for more information.
```
## Click to run a command
```
|lc command |lt text |le
```
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`.
## Click to open an url in a web browser
```
|lu url |lt text |le
```
Example:
```
"Omnious |luhttps://mycoolsounds.com/chanting|ltchanting sounds|le are coming from beyond the door."
```
This will show as "Omnious **chanting sounds** are coming from beyond the door", where clicking the link will open the url in a browser if the client supports doing so.
## Settings
Enable / disable MXP overall (enabled by default).
```
MXP_ENABLED = True
```
By default help entries have clickable topics.
```
HELP_CLICKABLE_TOPICS = True
```
By default clickable links are only available _from strings provided in code_ (or via a [batch script](../Components/Batch-Processors.md)). You _cannot_ create clickable links from inside the game - the result will not come out as clickable.
This is a security measure. Consider if a user were able to enter clickable links in their description, like this:
```
|lc give 1000 gold to Bandit |ltClick here to read my backstory!|le
```
This would be executed by the poor player clicking the link, resulting in them paying 1000 gold to the bandit.
This is controlled by the following default setting:
```
MXP_OUTGOING_ONLY = True
```
Only disable this protection if you know your game cannot be exploited in this way.

View file

@ -0,0 +1,210 @@
# 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. The default `screenreader` command will automatically turn off all color for a user (as well as clean up many line decorations etc). Make sure your game is still playable and understandable with this active.
Evennia supports two color standards:
- `ANSI` - 16 foreground colors + 8 background colors. Widely supported.
- `Xterm256` - 128 RGB colors, 32 greyscales. Not always supported in old clients. Falls back to ANSI.
- `Truecolor` - 24B RGB colors using hex notation. Not supported by many clients. Falls back to `XTerm256`.
To see which colours your client support, use the default `color` command. This will list all
available colours for ANSI and Xterm256, as well as a selection of True color codes along with the codes you use for them. The central ansi/xterm256 parser is located in [evennia/utils/ansi.py](evennia.utils.ansi), the true color one in [evennia/utils/true/hex_colors.py](evennia.utils.hex_colors).
## ANSI colours and symbols
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.
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).
For the webclient, Evennia will translate the codes to CSS tags.
| Tag | Effect | |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- |
| \|n | end all color formatting, including background colors. | |
| \|r | bright red foreground color | |
| \|g | bright green foreground color | |
| \|y | bright yellow foreground color | |
| \|b | bright blue foreground color | |
| \|m | bright magentaforeground color | |
| \|c | bright cyan foreground color | |
| \|w | bright white foreground color | |
| \|x | bright black (dark grey) foreground color | |
| \|R | normal red foreground color | |
| \|G | normal green foreground color | |
| \|Y | normal yellow foreground color | |
| \|B | normal blue foreground color | |
| \|M | normal magentaforeground color | |
| \|C | normal cyan foreground color | |
| \|W | normal white (light grey) foreground color | |
| \|X | normal black foreground color | |
| \|\[# | background colours, e.g. \|\[c for bright cyan background and \|\[C a normal cyan background. | |
| \|!# | foreground color that inherits brightness from previous tags. Always uppcase, like \|!R | |
| \|h | make any following foreground ANSI colors bright (for Xterm256/true color makes the font bold if client supports it). Use with \|!#. Technically, \|h\|G == \|g. | |
| \|H | negates the effects of \|h | |
| \|u | underline font (not supported in Evennia webclient) | |
| \|U | negates the effects of \|u | |
| \|i | italic font (not supported in Evennia webclient) | |
| \|I | negates the effects of \|i | |
| \|s | strikethrough font (not supported in Evennia webclient) | |
| \|S | negates the effects of \|s | |
| \|/ | line break. Use instead of Python \\n when adding strings from in-game. | |
| \|- | tab character when adding strings in-game. Can vay per client, so usually better with spaces. | |
| \|_ | a space. Only needed to avoid auto-cropping at the end of a in-game input | |
| \|* | invert the current text/background colours, like a marker. See note below. | |
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.
Note: The ANSI standard does not actually support bright backgrounds like `|[r` - the standard
only supports "normal" intensity backgrounds. To get around this Evennia 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:
### 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](../Howtos/Tutorial\-Understanding\-Color\-Tags.md)
tutorial. But most of the time you might be better off to simply avoid `|*` and mark your text
manually instead.
## Xterm256 Colours
```{sidebar}
See the [Understanding Color Tags](../Howtos/Tutorial\-Understanding\-Color\-Tags.md) tutorial, for more on the use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
```
The _Xterm256_ standard is a colour scheme that supports 256 colours for text and/or background. It can be combined freely with ANSI colors (above), but some ANSI tags don't affect Xterm256 tags.
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.
| Tag | Effect |
| ---- | ---- |
| \|### | foreground RGB (red/green/blue), each from 0 to 5. |
| \|\[### | background RGB |
| \|=# | a-z foreground greyscale, where `a` is black and `z` is white. |
| \|\[=#| a-z background greyscale
Some examples:
| Tag | Effect |
| ---- | ---- |
| \|500 | bright red |
| \|050 | bright green |
| \|005 | bright blue |
| \|520 | red + a little green = orange |
|\|555 | pure white foreground |
|\|230 | olive green foreground |
|\|\[300 | text with a dark red background |
|\|005\|\[054 | dark blue text on a bright cyan background |
|\|=a | greyscale foreground, equal to black |
| \|=m | greyscale foreground, midway between white and black.
| \|=z | greyscale foreground, equal to white |
| \|\[=m | greyscale background |
Xterm256 don't use bright/normal intensity like ANSI does; intensity is just varied by increasing/decreasing all RGB values by the same amount.
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.
## 24-bit Colors (True color)
```{sidebar}
See the [Wikipedia entry on web colors](https://en.wikipedia.org/wiki/Web_colors) for more detailed information on this color format.
```
Some clients support 24-bit colors. This is also called [true color](https://en.wikipedia.org/wiki/Color_depth#True_color_(24-bit)).
Not all clients support true color, they will instead see the closest equivalent. It's important to bear in mind that things may look quite different from what you intended if you use subtle gradations in true color and it's viewed with a client that doesn't support true color.
The hexadecimal color codes used here are the same ones used in web design.
| Tag | Effect |
| -------- | ---- |
| \|#$$$$$$ | foreground RGB (red/green/blue), 6-digit hexadecimal format, where $ = 0-F |
| \|\[#$$$$$$ | background RGB |
| \|#$$$ | foreground RGB (red/green/blue), 3-digit hexadecimal format. |
| \|\[#$$$ | background RGB |
Some 6-digit examples:
| Tag | Effect |
| -------- | ---- |
| \|#ff0000 | bright red foreground|
| \|#00ff00 | bright green foreground|
| \|#0000ff | bright blue foreground|
| \|#\[ff0000 | bright red background|
Some 3-digit examples:
| Tag | Effect |
| ---- | ---- |
| \|#f00 | bright red foreground|
| \|#0f0 | bright green foreground|
| \|#00f | bright blue foreground|
| \|\[#f00 | bright red background|

View file

@ -0,0 +1,44 @@
# 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
Messagepath.md
OOB.md
Async-Process.md
```
## Text processing
```{toctree}
:maxdepth: 2
Tags-Parsed-By-Evennia.md
Change-Message-Per-Receiver.md
Internationalization.md
Text-Encodings.md
```
## Access
```{toctree}
:maxdepth: 2
Connection-Styles.md
Guests.md
Banning.md
```
## Extending the Server
```{toctree}
:maxdepth: 2
Server-Lifecycle
Protocols.md
Models.md
Zones.md
```

View file

@ -0,0 +1,133 @@
# Character connection styles
```shell
> login Foobar password123
```
Evennia supports multiple ways for players to connect to the game. This allows Evennia to mimic the behavior of various other servers, or open things up for a custom solution.
## Changing the login screen
This is done by modifying `mygame/server/conf/connection_screens.py` and reloading. If you don't like the default login, there are two contribs to check out as inspiration.
- [Email login](../Contribs/Contrib-Email-Login.md) - require email during install, use email for login.
- [Menu login](../Contribs/Contrib-Menu-Login.md) - login using several prompts, asking to enter username and password in sequence.
## Customizing the login command
When a player connects to the game, it runs the `CMD_LOGINSTART` [system command](../Components/Commands.md#system-commands). By default, this is the [CmdUnconnectedLook](evennia.commands.default.unloggedin.CmdUnconnectedLook). This shows the welcome screen. The other commands in the [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet) are what defines the login experience. So if you want to customise it, you just need to replace/remove those commands.
```{sidebar}
If you instead had your command inherit from `default_cmds.UnConnectedLook`, you didn't even have to speciy the key (since your class would inherit it)!
```
```python
# in mygame/commands/mylogin_commands.py
from evennia import syscmdkeys, default_cmds, Command
class MyUnloggedinLook(Command):
# this will now be the first command called when connecting
key = syscmdkeys.CMD_LOGINSTART
def func(self):
# ...
```
Next, add this to the right place in the `UnloggedinCmdSet`:
```python
# in mygame/commands/default_cmdsets.py
from commands.mylogin_commands import MyUnloggedinLook
# ...
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
# ...
def at_cmdset_creation(self):
super().at_cmdset_creation
self.add(MyUnloggedinLook())
```
`reload` and your alternate command will be used. Examine the default commands and you'll be able to change everything about the login.
## Multisession mode and multi-playing
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.
```
┌──────┐ │ ┌───────┐ ┌───────┐ ┌─────────┐
│Client├─┼──►│Session├───►│Account├──►│Character│
└──────┘ │ └───────┘ └───────┘ └─────────┘
```
* `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.
```
┌──────┐ │ ┌───────┐
│Client├─┼──►│Session├──┐
└──────┘ │ └───────┘ └──►┌───────┐ ┌─────────┐
│ │Account├──►│Character│
┌──────┐ │ ┌───────┐ ┌──►└───────┘ └─────────┘
│Client├─┼──►│Session├──┘
└──────┘ │ └───────┘
```
* `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.
```
│ ┌───────┐
┌──────┐ │ ┌───────┐ │Account│ ┌─────────┐
│Client├─┼──►│Session├──┐ │ │ ┌►│Character│
└──────┘ │ └───────┘ └──┼───────┼──┘ └─────────┘
│ │ │
┌──────┐ │ ┌───────┐ ┌──┼───────┼──┐ ┌─────────┐
│Client├─┼──►│Session├──┘ │ │ └►│Character│
└──────┘ │ └───────┘ │ │ └─────────┘
│ └───────┘
```
* `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.
```
│ ┌───────┐
┌──────┐ │ ┌───────┐ │Account│ ┌─────────┐
│Client├─┼──►│Session├──┐ │ │ ┌►│Character│
└──────┘ │ └───────┘ └──┼───────┼──┘ └─────────┘
│ │ │
┌──────┐ │ ┌───────┐ ┌──┼───────┼──┐
│Client├─┼──►│Session├──┘ │ │ └►┌─────────┐
└──────┘ │ └───────┘ │ │ │Character│
│ │ │ ┌►└─────────┘
┌──────┐ │ ┌───────┐ ┌──┼───────┼──┘ ▼
│Client├─┼──►│Session├──┘ │ │
└──────┘ │ └───────┘ └───────┘
```
> Note that even if multiple Sessions puppet one Character, there is only ever one instance of that Character.
Mode `0` is the default and mimics how many legacy codebases work, especially in the DIKU world. The equivalence of higher modes are often 'hacked' into existing servers to allow for players to have multiple characters.
MAX_NR_SIMULTANEOUS_PUPPETS = 1
This setting limits how many _different_ puppets your _Account_ can puppet _simultaneously_. This is used to limit true multiplaying. A value higher than one makes no sense unless `MULTISESSION_MODE` is also set `>1`. Set to `None` for no limit.
## Character creation and auto-puppeting
When a player first creates an account, Evennia will auto-create a `Character` puppet of the same name. When the player logs in, they will auto-puppet this Character. This default hides the Account-Character separation from the player and puts them immediately in the game. This default behavior is similar to how it works in many legacy MU servers.
To control this behavior, you need to tweak the settings. These are the defaults:
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = True
AUTO_PUPPET_ON_LOGIN = True
MAX_NR_CHARACTERS = 1
There is a default `charcreate` command. This heeds the `MAX_NR_CHARACTERS`; and if you make your own character-creation command, you should do the same. It needs to be at least `1`. Set to `None` for no limit. See the [Beginner Tutorial](../Howtos/Beginner-Tutorial/Beginner-Tutorial-Overview.md) for ideas on how to make a more advanced character generation system.
```{sidebar}
Combining these settings with `MAX_NR_SIMULTANEOUS_PUPPETS` could allow for a game where (for example) a player can create a 'stable' of Characters, but only be able to play one at a time.
```
If you choose to not auto-create a character, you will need to provide a character-generation, and there will be no (initial) Character to puppet. In both of these settings, you will initially end up in `ooc` mode after you login. This is a good place to put a character generation screen/menu (you can e.g. replace the [CmdOOCLook](evennia.commands.default.account.CmdOOCLook) to trigger something other than the normal ooc-look).
Once you created a Character, if your auto-puppet is set, you will automatically puppet your latest-puppeted Character whenever you login. If not set, you will always start OOC (and should be able to select which Character to puppet).

View file

@ -0,0 +1,18 @@
# 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 `"Guest"`, which is the lowest permission level in the hierarchy (below `Player`).
- `GUEST_START_LOCATION` - the `#dbref` to the starting location newly logged-in guests should appear at. Defaults to `"#2` (Limbo).
- `GUEST_HOME` - guest home locations. Defaults to Limbo as well.
- `GUEST_LIST` - this is a list holding the possible guest names to use when entering the game. The length of this list also sets how many guests may log in at the same time. By default this is a list of nine names from `"Guest1"` to `"Guest9"`.

View file

@ -0,0 +1,22 @@
# Inline functions
```{sidebar}
For much more information about inline functions, see the [FuncParser](../Components/FuncParser.md) documentation
```
_Inline functions_, also known as _funcparser functions_ are embedded strings on the form
$funcname(args, kwargs)
For example
> say the answer is $eval(24 * 12)!
You say, "the answer is 288!"
General processing of outgoing strings is disabled by default. To activate inline-function parsing of outgoing strings, add this to your settings file:
FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED=True
Inline functions are provided by the [FuncParser](../Components/FuncParser.md). It is enabled in a few other situations:
- Processing of [Prototypes](../Components/Prototypes.md); these 'prototypefuncs' allow for prototypes whose values change dynamically upon spawning. For example, you would set `{key: '$choice(["Bo", "Anne", "Tom"])'` and spawn a random-named character every time.
- Processing of strings to the `msg_contents` method. This allows for [sending different messages depending on who will see them](./Change-Message-Per-Receiver.md).

View file

@ -0,0 +1,191 @@
# 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 |
+===============+======================+==============+
| de | German | Aug 2024 |
+---------------+----------------------+--------------+
| es | Spanish | Aug 2019 |
+---------------+----------------------+--------------+
| fr | French | Dec 2022 |
+---------------+----------------------+--------------+
| it | Italian | Oct 2022 |
+---------------+----------------------+--------------+
| ko | Korean (simplified) | Sep 2019 |
+---------------+----------------------+--------------+
| la | Latin | Feb 2021 |
+---------------+----------------------+--------------+
| pl | Polish | Apr 2024 |
+---------------+----------------------+--------------+
| pt | Portugese | Oct 2022 |
+---------------+----------------------+--------------+
| ru | Russian | Apr 2020 |
+---------------+----------------------+--------------+
| sv | Swedish | Sep 2022 |
+---------------+----------------------+--------------+
| zh | Chinese (simplified) | Oct 2024 |
+---------------+----------------------+--------------+
```
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.
To cut down on work, you may consider only translating the player-facing commands (look, get etc) and leave the default admin commands in English. To change the language of some commands (such as `look`) you need to override the relevant hook-methods on your Typeclasses (check out the code for the default command to see what it calls).
```
```{sidebar} Windows users
If you get errors concerning `gettext` or `xgettext` on Windows,
see the [Django documentation](https://docs.djangoproject.com/en/4.1/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 no one
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 repository
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 `django.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 `django.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})"
## Marking Strings in Code for Translation
If you modify the Python module code, you can mark strings for translation by passing them to the `gettext()` method. In Evennia, this is usually imported as `_()` for convenience:
```python
from django.utils.translation import gettext as _
string = _("Text to translate")
```
### Formatting Considerations
When using formatted strings, ensure that you pass the "raw" string to `gettext` for translation first and then format the output. Otherwise, placeholders will be replaced before translation occurs, preventing the correct string from being found in the `.po` file. It's also recommended to use named placeholders (e.g., `{char}`) instead of positional ones (e.g., `{}`) for better readability and maintainability.
```python
# incorrect:
string2 = _("Hello {char}!".format(char=caller.name))
# correct:
string2 = _("Hello {char}!").format(char=caller.name)
```
This is also why f-strings don't work with `gettext`:
```python
# will not work
string = _(f"Hello {char}!")
```

View file

@ -0,0 +1,192 @@
# The Message path
```shell
> look
A Meadow
This is a beautiful meadow. It is full of flowers.
You see: a flower
Exits: north, east
```
When you send a command like `look` into Evennia - what actually happens? How does that `look` string end up being handled by the `CmdLook` class? What happens when we use e.g. `caller.msg()` to send the message back?
Understanding this flow of data - the _message path_ is important in order to understand how Evennia works.
## Ingoing message path
```
Internet│
┌─────┐ │ ┌────────┐
┌──────┐ │Text │ │ ┌────────────┐ ┌─────────┐ │Command │
│Client├────┤JSON ├─┼──►commandtuple├────►Inputfunc├────►DB query│
└──────┘ │etc │ │ └────────────┘ └─────────┘ │etc │
└─────┘ │ └────────┘
│Evennia
```
### Incoming command tuples
Ingoing data from the client (coming in as raw strings or serialized JSON) is converted by Evennia to a `commandtuple`. Thesa are the same regardless of what client or connection was used. A `commandtuple` is a simple tuple with three elements:
```python
(commandname, (args), {kwargs})
```
For the `look`-command (and anything else written by the player), the `text` `commandtuple` is generated:
```python
("text", ("look",), {})
```
### Inputfuncs
On the Evennia server side, a list of [inputfuncs](../Components/Inputfuncs.md) are registered. You can add your own by extending `settings.INPUT_FUNC_MODULES`.
```python
inputfunc_commandname(session, *args, **kwargs)
```
Here the `session` represents the unique client connection this is coming from (that is, it's identifying just _who_ is sending this input).
One such inputfunc is named `text`. For sending a `look`, it will be called as
```{sidebar}
If you know how `*args` and `**kwargs` work in Python, you'll see that this is the same as a call `text(session, "look")`
```
```python
text(session, *("look",), **{})
```
What an `inputfunc` does with this depends. For an [Out-of-band](./OOB.md) instruction, it could fetch the health of a player or tick down some counter.
```{sidebar} No text parsing happens before this
If you send `look here`, the call would be `text(session, *("look here", **{})`. All parsing of the text input happens in the command-parser, after this step.
```
For the `text` `inputfunc` the Evennia [CommandHandler](../Components/Commands.md) is invoked and the argument is parsed further in order to figure which command was intended.
In the example of `look`, the `CmdLook` command-class will be invoked. This will retrieve the description of the current location.
## Outgoing message path
```
Internet│
┌─────┐ │
┌──────┐ │Text │ │ ┌──────────┐ ┌────────────┐ ┌─────┐
│Client◄────┤JSON ├─┼──┤outputfunc◄────┤commandtuple◄───┤msg()│
└──────┘ │etc │ │ └──────────┘ └────────────┘ └─────┘
└─────┘ │
│Evennia
```
### `msg` to outgoing commandtuple
When the `inputfunc` has finished whatever it is supposed to, the server may or may not decide to return a result (Some types of `inputcommands` may not expect or require a response at all). The server also often sends outgoing messages without any prior matching ingoing data.
Whenever data needs to be sent "out" of Evennia, we must generalize it into a (now outgoing) `commandtuple` `(commandname, (args), {kwargs})`. This we do with the `msg()` method. For convenience, this methods is available on every major entity, such as `Object.msg()` and `Account.msg()`. They all link back to `Session.msg()`.
```python
msg(text=None, from_obj=None, session=None, options=None, **kwargs)
```
`text` is so common that it is given as the default:
```python
msg("A meadow\n\nThis is a beautiful meadow...")
```
This is converted to a `commandtuple` looking like this:
```python
("text", ("A meadow\n\nThis is a beutiful meadow...",) {})
```
The `msg()` method allows you to define the `commandtuple` directly, for whatever outgoing instruction you want to find:
```python
msg(current_status=(("healthy", "charged"), {"hp": 12, "mp": 20}))
```
This will be converted to a `commandtuple` looking like this:
```python
("current_status", ("healthy", "charged"), {"hp": 12, "mp": 20})
```
### outputfuncs
```{sidebar}
`outputfuncs` are tightly coupled to the protocol and you usually don't need to touch them, unless you are [adding a new protocol](./Protocols.md) entirely.
```
Since `msg()` is aware of which [Session](../Components/Sessions.md) to send to, the outgoing `commandtuple` is always end up pointed at the right client.
Each supported Evennia Protocol (Telnet, SSH, Webclient etc) has their own `outputfunc`, which converts the generic `commandtuple` into a form that particular protocol understands, such as telnet instructions or JSON.
For telnet (no SSL), the `look` will return over the wire as plain text:
A meadow\n\nThis is a beautiful meadow...
When sending to the webclient, the `commandtuple` is converted as serialized JSON, like this:
'["look", ["A meadow\\n\\nThis is a beautiful meadow..."], {}]'
This is then sent to the client over the wire. It's then up to the client to interpret and handle the data properly.
## Components along the path
### Ingoing
```
┌──────┐ ┌─────────────────────────┐
│Client│ │ │
└──┬───┘ │ ┌────────────────────┐ │
│ ┌──────┼─►│ServerSessionHandler│ │
┌──────────────────┼──────┐ │ │ └───┬────────────────┘ │
│ Portal │ │ │ │ │ │
│ ┌─────────▼───┐ │ ┌─┴─┐ │ ┌───▼─────────┐ │
│ │PortalSession│ │ │AMP│ │ │ServerSession│ │
│ └─────────┬───┘ │ └─┬─┘ │ └───┬─────────┘ │
│ │ │ │ │ │ │
│ ┌────────────────▼───┐ │ │ │ ┌───▼─────┐ │
│ │PortalSessionHandler├──┼──────┘ │ │Inputfunc│ │
│ └────────────────────┘ │ │ └─────────┘ │
│ │ │ Server │
└─────────────────────────┘ └─────────────────────────┘
```
1. Client - sends handshake or commands over the wire. This is received by the Evennia [Portal](../Components/Portal-And-Server.md).
2. `PortalSession` represents one client connection. It understands the communiation protocol used. It converts the protocol-specific input to a generic `commandtuple` structure `(cmdname, (args), {kwargs})`.
3. `PortalSessionHandler` handles all connections. It pickles the `commandtuple` together with the session-id.
4. Pickled data is sent across the `AMP` (Asynchronous Message Protocol) connection to the [Server](Server-And-Portal) part of Evennia.
5. `ServerSessionHandler` unpickles the `commandtuple` and matches the session-id to a matching `SessionSession`.
6. `ServerSession` represents the session-connection on the Server side. It looks through its registry of [Inputfuncs](../Components/Inputfuncs.md) to find a match.
7. The appropriate `Inputfunc` is called with the args/kwargs included in the `commandtuple`. Depending on `Inputfunc`, this could have different effects. For the `text` inputfunc, it fires the [CommandHandler](../Components/Commands.md).
### Outgoing
```
┌──────┐ ┌─────────────────────────┐
│Client│ │ │
└──▲───┘ │ ┌────────────────────┐ │
│ ┌──────┼──┤ServerSessionHandler│ │
┌──────────────────┼──────┐ │ │ └───▲────────────────┘ │
│ Portal │ │ │ │ │ │
│ ┌─────────┴───┐ │ ┌─┴─┐ │ ┌───┴─────────┐ │
│ │PortalSession│ │ │AMP│ │ │ServerSession│ │
│ └─────────▲───┘ │ └─┬─┘ │ └───▲─────────┘ │
│ │ │ │ │ │ │
│ ┌────────────────┴───┐ │ │ │ ┌───┴──────┐ │
│ │PortalSessionHandler◄──┼──────┘ │ │msg() call│ │
│ └────────────────────┘ │ │ └──────────┘ │
│ │ │ Server │
└─────────────────────────┘ └─────────────────────────┘
```
1. The `msg()` method is called
2. `ServerSession` and in particular `ServerSession.msg()` is the central point through which all `msg()` calls are routed in order to send data to that [Session](../Components/Sessions.md).
3. `ServerSessionHandler` converts the `msg` input to a proper `commandtuple` structure `(cmdname, (args), {kwargs})`. It pickles the `commandtuple` together with the session-id.
4. Pickled data is sent across across the `AMP` (Asynchronous Message Protocol) connection to the [Portal](Server-And-Portal) part of Evennia.
5. `PortalSessionHandler` unpickles the `commandtuple` and matches its session id to a matching `PortalSession`.
6. The `PortalSession` is now responsible for converting the generic `commandtuple` to the communication protocol used by that particular connection.
7. The Client receives the data and can act on it.

View file

@ -0,0 +1,229 @@
# 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):
evennia startapp myapp
mv myapp world (linux)
move myapp world (windows)
1. A new folder `myapp` is created. "myapp" will also be the name (the "app label") from now on. We move it into the `world/` subfolder here, but you could keep 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 myapp
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/4.1/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/4.1/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.
## Referencing existing models and typeclasses
You may want to use `ForeignKey` or `ManyToManyField` to relate your new model to existing ones.
To do this we need to specify the app-path for the root object type we want to store as a string (we must use a string rather than the class directly or you'll run into problems with models not having been initialized yet).
- `"objects.ObjectDB"` for all [Objects](../Components/Objects.md) (like exits, rooms, characters etc)
- `"accounts.AccountDB"` for [Accounts](../Components/Accounts.md).
- `"scripts.ScriptDB"` for [Scripts](../Components/Scripts.md).
- `"comms.ChannelDB"` for [Channels](../Components/Channels.md).
- `"comms.Msg"` for [Msg](../Components/Msg.md) objects.
- `"help.HelpEntry"` for [Help Entries](../Components/Help-System.md).
Here's an example:
```python
from django.db import models
class MySpecial(models.Model):
db_character = models.ForeignKey("objects.ObjectDB")
db_items = models.ManyToManyField("objects.ObjectDB")
db_account = modeles.ForeignKey("accounts.AccountDB")
```
It may seem counter-intuitive, but this will work correctly:
myspecial.db_character = my_character # a Character instance
my_character = myspecial.db_character # still a Character
This works because when the `.db_character` field is loaded into Python, the entity itself knows that it's supposed to be a `Character` and loads itself to that form.
The drawback of this is that the database won't _enforce_ the type of object you store in the relation. This is the price we pay for many of the other advantages of the Typeclass system.
While the `db_character` field fail if you try to store an `Account`, it will gladly accept any instance of a typeclass that inherits from `ObjectDB`, such as rooms, exits or other non-character Objects. It's up to you to validate that what you store is what you expect it to be.
## 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.Model` 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 in another function you do
```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 [Beginner Tutorial lesson on Django querying](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Django-queries.md) for a lot more information about querying the database.

View file

@ -0,0 +1,134 @@
# Out-of-Band messaging
OOB, or Out-Of-Band, means sending data between Evennia and the user's client without the user
prompting it or necessarily being aware that it's being passed. Common uses would be to update
client health-bars, handle client button-presses or to display certain tagged text in a different
window pane.
If you haven't, you should be familiar with the [Messagepath](./Messagepath.md), which describes how a message enters and leaves Evennia and how along the way, all messages are converted to a generic format called a `commandtuple`:
(commandname, (args), {kwargs})
## Sending and receiving an OOB message
Sending is simple. You just use the normal `msg` method of the object whose session you want to send to.
```python
caller.msg(commandname=((args, ...), {key:value, ...}))
```
The keyword becomes the command-name part of the `commandtuple` and the value its `args` and `kwargs` parts. You can also send multiple messages of different `commandname`s at the same time.
A special case is the `text` call. It's so common that it's the default of the `msg` method. So these are equivalent:
```python
caller.msg("Hello")
caller.msg(text="Hello")
```
You don't have to specify the full `commandtuple` definition. So for example, if your particular command only needs kwargs, you can skip the `(args)` part. Like in the `text` case you can skip
writing the tuple if there is only one arg ... and so on - the input is pretty flexible. If there
are no args at all you need to give the empty tuple `msg(cmdname=(,)` (giving `None` would mean a
single argument `None`).
### Which command-names can I send?
This depends on the client and protocol. If you use the Evennia [webclient](../Components/Webclient.md), you can modify it to have it support whatever command-names you like.
Many third-party MUD clients support a range of OOB protocols listed below. If a client does not support a particular OOB instruction/command, Evennia will just send the `text` command to them and quietly drop all other OOB instructions.
> Note that a given message may go to multiple clients with different capabilities. So unless you turn off telnet completely and only rely on the webclient, you should never rely on non-`text` OOB messages always reaching all targets.
### Which command-names can I receive
This is decided by which [Inputfuncs](../Components/Inputfuncs.md) you define. You can extend Evennia's default as you like, but adding your own functions in a module pointed to by `settings.INPUT_FUNC_MODULES`.
## Supported OOB protocols
Evennia supports clients using one of the following protocols:
### Telnet
By default telnet (and telnet+SSL) supports only the plain `text` outputcommand. Evennia detects if the Client supports one of two MUD-specific OOB *extensions* to the standard telnet protocol - GMCP or MSDP. Evennia supports both simultaneously and will switch to the protocol the client uses. If the client supports both, GMCP will be used.
> Note that for Telnet, `text` has a special status as the "in-band" operation. So the `text` outputcommand sends the `text` argument directly over the wire, without going through the OOB translations described below.
#### Telnet + GMCP
[GMCP](https://www.gammon.com.au/gmcp), the *Generic Mud Communication Protocol* sends data on the form `cmdname + JSONdata`. Here the cmdname is expected to be on the form "Package.Subpackage". There could also be additional Sub-sub packages etc. The names of these 'packages' and 'subpackages' are not that well standardized beyond what individual MUDs or companies have chosen to go with over the years. You can decide on your own package names, but here are what others are using:
- [Aardwolf GMCP](https://www.aardwolf.com/wiki/index.php/Clients/GMCP)
- [Discworld GMCP](https://discworld.starturtle.net/lpc/playing/documentation.c?path=/concepts/gmcp)
- [Avatar GMCP](https://www.outland.org/infusions/wiclear/index.php?title=MUD%20Protocols&lang=en)
- [IRE games GMCP](https://nexus.ironrealms.com/GMCP)
Evennia will translate underscores to `.` and capitalize to fit the specification. So the outputcommand `foo_bar` will become a GMCP command-name `Foo.Bar`. A GMCP command "Foo.Bar" will be come `foo_bar`. To send a GMCP command that turns into an Evennia inputcommand without an underscore, use the `Core` package. So `Core.Cmdname` becomes just `cmdname` in Evennia and vice versa.
On the wire, the `commandtuple`
("cmdname", ("arg",), {})
will be sent over the wire as this GMCP telnet instruction
IAC SB GMCP "cmdname" "arg" IAC SE
where all the capitalized words are telnet character constants specified in ][evennia/server/portal/telnet_oob](evennia.server.portal/telnet_oob.py). These are parsed/added by the protocol and we don't include these in the listings below.
| `commandtuple` | GMCP-Command |
| --- | ---|
| `(cmd_name, (), {})` | `Cmd.Name` |
| `(cmd_name, (arg,), {})` | `Cmd.Name arg` |
| `(cmd_na_me, (args,...),{})` | `Cmd.Na.Me [arg, arg...]` |
| `(cmd_name, (), {kwargs})` | `Cmd.Name {kwargs}` |
| `(cmdname, (arg,), {kwargs})` | `Core.Cmdname [[args],{kwargs}]` |
Since Evennia already supplies default Inputfuncs that don't match the names expected by the most common GMCP implementations we have a few hard-coded mappings for those:
| GMCP command name | `commandtuple` command name |
| --- | --- |
| `"Core.Hello"` | `"client_options"` |
| `"Core.Supports.Get"` | `"client_options"` |
| `"Core.Commands.Get"` | `"get_inputfuncs"` |
| `"Char.Value.Get"` | `"get_value"` |
| `"Char.Repeat.Update"` | `"repeat"` |
| `"Char.Monitor.Update"`| `"monitor"` |
#### Telnet + MSDP
[MSDP](http://tintin.sourceforge.net/msdp/), the *Mud Server Data Protocol*, is a competing standard to GMCP. The MSDP protocol page specifies a range of "recommended" available MSDP command names. Evennia does *not* support those - since MSDP doesn't specify a special format for its command names (like GMCP does) the client can and should just call the internal Evennia inputfunc by its actual name.
MSDP uses Telnet character constants to package various structured data over the wire. MSDP supports strings, arrays (lists) and tables (dicts). These are used to define the cmdname, args and kwargs needed. When sending MSDP for `("cmdname", ("arg",), {})` the resulting MSDP instruction will look like this:
IAC SB MSDP VAR cmdname VAL arg IAC SE
The various available MSDP constants like `VAR` (variable), `VAL` (value), `ARRAYOPEN`/`ARRAYCLOSE`
and `TABLEOPEN`/`TABLECLOSE` are specified in `evennia/server/portal/telnet_oob`.
| `commandtuple` | MSDP instruction |
| --- | --- |
| `(cmdname, (), {})` | `VAR cmdname VAL` |
| `(cmdname, (arg,), {})` | `VAR cmdname VAL arg` |
| `(cmdname, (arg,...),{})` | `VAR cmdname VAL ARRAYOPEN VAL arg VAL arg ... ARRAYCLOSE` |
| `(cmdname, (), {kwargs})` | `VAR cmdname VAL TABLEOPEN VAR key VAL val ... TABLECLOSE` |
| `(cmdname, (args,...), {kwargs})` | `VAR cmdname VAL ARRAYOPEN VAL arg VAL arg ... ARRAYCLOSE VAR cmdname VAL TABLEOPEN VAR key VAL val ... TABLECLOSE` |
Observe that `VAR ... VAL` always identifies `cmdnames`, so if there are multiple arrays/dicts tagged with the same cmdname they will be appended to the args, kwargs of that inputfunc. Vice-versa, a
different `VAR ... VAL` (outside a table) will come out as a second, different command input.
### SSH
SSH only supports the `text` input/outputcommand.
### Web client
Our web client uses pure [JSON](https://en.wikipedia.org/wiki/JSON) structures for all its communication, including `text`. This maps directly to the Evennia internal output/inputcommand, including eventual empty args/kwargs.
| `commandtuple` | Evennia Webclient JSON |
| --- | --- |
| `(cmdname, (), {})` | `["cmdname", [], {}]` |
| `(cmdname, (arg,), {})` | `["cmdname", [arg], {}]` |
| `(cmdname, (arg,...),{})` | `["cmdname", [arg, ...], {})` |
| `(cmdname, (), {kwargs})` | `["cmdname", [], {kwargs})` |
| `(cmdname, (arg,...), {kwargs})` | `["cmdname", [arg, ...], {kwargs})` |
Since JSON is native to Javascript, this becomes very easy for the webclient to handle.

View file

@ -0,0 +1,196 @@
# Protocols
```
Internet│ Protocol
┌─────┐ │ |
┌──────┐ │Text │ │ ┌──────────┐ ┌────────────┐ ┌─────┐
│Client◄────┤JSON ├─┼──┤outputfunc◄────┤commandtuple◄───┤msg()│
└──────┘ │etc │ │ └──────────┘ └────────────┘ └─────┘
└─────┘ │
│Evennia
```
The _Protocol_ describes how Evennia sends and receives data over the wire to the client. Each connection-type (telnet, ssh, webclient etc) has its own protocol. Some protocols may also have variations (such plain-text Telnet vs Telnet SSL).
See the [Message Path](./Messagepath.md) for the bigger picture of how data flows through Evennia.
In Evennia, the `PortalSession` represents the client connection. The session is told to use a particular protocol. When sending data out, the session must provide an "Outputfunc" to convert the generic `commandtuple` to a form the protocol understands. For ingoing data, the server must also provide suitable [Inputfuncs](../Components/Inputfuncs.md) to handle the instructions sent to the server.
## Adding a new Protocol
Evennia has a plugin-system that add the protocol as a new "service" to the application.
To add a new service of your own (for example your own custom client protocol) to the Portal or Server, expand `mygame/server/conf/server_services_plugins` and `portal_services_plugins`.
To expand where Evennia looks for plugins, use the following settings:
```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 client connection you'll most likely only need to add new things to the Portal-plugin files.
The plugin module must contain a function `start_plugin_services(app)`, where the `app` arguments refers to the Portal/Server application itself. This is called by the Server or Portal when it starts up. It must contatin all startup code needed.
Example:
```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
```{important}
This is considered an advanced topic.
```
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.

View file

@ -0,0 +1,80 @@
# Evennia Server Lifecycle
As part of your game design you may want to change how Evennia behaves when starting or stopping. A common use case would be to start up some piece of custom code you want to always have available once the server is up.
Evennia has three main life cycles, all of which you can add custom behavior for:
- **Database life cycle**: Evennia uses a database. This exists in parallel to the code changes you do. The database exists until you choose to reset or delete it. Doing so doesn't require re-downloading Evennia.
- **Reboot life cycle**: From When Evennia starts to it being fully shut down, which means both Portal and Server are stopped. At the end of this cycle, all players are disconnected.
- **Reload life cycle:** This is the main runtime, until a "reload" event. Reloads refreshes game code but do not kick any players.
## When Evennia starts for the first time
This is the beginning of the **Database life cycle**, just after the database is created and migrated for the first time (or after it was deleted and re-built). See [Choosing a Database](../Setup/Choosing-a-Database.md) for instructions on how to reset a database, should you want to re-run this sequence after the first time.
Hooks called, in sequence:
1. `evennia.server.initial_setup.handle_setup(last_step=None)`: Evennia's core initialization function. This is what creates the #1 Character (tied to the superuser account) and `Limbo` room. It calls the next hook below and also understands to restart at the last failed step if there was some issue. You should normally not override this function unless you _really_ know what you are doing. To override, change `settings.INITIAL_SETUP_MODULE` to your own module with a `handle_setup` function in it.
2. `mygame/server/conf/at_initial_setup.py` contains a single function, `at_initial_setup()`, which will be called without arguments. It's called last in the setup sequence by the above function. Use this to add your own custom behavior or to tweak the initialization. If you for example wanted to change the auto-generated Limbo room, you should do it from here. If you want to change where this function is found, you can do so by changing `settings.AT_INITIAL_SETUP_HOOK_MODULE`.
## When Evennia starts and shutdowns
This is part of the **Reboot life cycle**. Evennia consists of two main processes, the [Portal and the Server](../Components/Portal-And-Server.md). On a reboot or shutdown, both Portal and Server shuts down, which means all players are disconnected.
Each process call a series of hooks located in `mygame/server/conf/at_server_startstop.py`. You can customize the module used with `settings.AT_SERVER_STARTSTOP_MODULE` - this can even be a list of modules, if so, the appropriately-named functions will be called from each module, in sequence.
All hooks are called without arguments.
> The use of the term 'server' in the hook-names indicate the whole of Evennia, not just the `Server` component.
### Server cold start
Starting the server from zero, after a full stop. This is done with `evennia start` from the terminal.
1. `at_server_init()` - Always called first in the startup sequence.
2. `at_server_cold_start()` - Only called on cold starts.
3. `at_server_start()` - Always called last in the startup sequece.
### Server cold shutdown
Shutting everything down. Done with `shutdown` in-game or `evennia stop` from the terminal.
1. `at_server_cold_stop()` - Only called on cold stops.
2. `at_server_stop()` - Always called last in the stopping sequence.
### Server reboots
This is done with `evennia reboot` and effectively constitutes an automatic cold shutdown followed by a cold start controlled from the `evennia` launcher. There are no special `reboot` hooks for this, instead it looks like you'd expect:
1. `at_server_cold_stop()`
2. `at_server_stop()` (after this, both `Server` + `Portal` have both shut down)
3. `at_server_init()` (like a cold start)
4. `at_server_cold_start()`
5. `at_server_start()`
## When Evennia reloads and resets
This is the **Reload life cycle**. As mentioned above, Evennia consists of two components, the [Portal and Server](../Components/Portal-And-Server.md). During a reload, only the `Server` component is shut down and restarted. Since the Portal stays up, players are not disconnected.
All hooks are called without arguments.
### Server reload
Reloads are initiated with the `reload` command in-game, or with `evennia reload` from the terminal.
1. `at_server_reload_stop()` - Only called on reload stops.
2. `at_server_stop` - Always called last in the stopping sequence.
3. `at_server_init()` - Always called first in startup sequence.
4. `at_server_reload_start()` - Only called on a reload (re)start.
5. `at_server_start()` - Always called last in the startup sequence.
### Server reset
A 'reset' is a hybrid reload state, where the reload is treated as a cold shutdown only for the sake of running hooks (players are not disconnected). It's run with `reset` in-game or with `evennia reset` from the terminal.
1. `at_server_cold_stop()`
2. `at_server_stop()` (after this, only `Server` has shut down)
3. `at_server_init()` (`Server` coming back up)
4. `at_server_cold_start()`
5. `at_server_start()`

View file

@ -0,0 +1,27 @@
# In-text tags parsed by Evennia
```{toctree}
:maxdepth: 2
Colors.md
Clickable-Links.md
Inline-Functions.md
```
Evennia will parse various special tags and markers embedded in text and convert it dynamically depending on if the data is going in or out of the server.
- _Colors_ - Using `|r`, `|n` etc can be used to mark parts of text with a color. The color will
become ANSI/XTerm256 color tags for Telnet connections and CSS information for the webclient.
```
> say Hello, I'm wearing my |rred hat|n today.
```
- _Clickable links_ - This allows you to provide a text the user can click to execute an
in-game command. This is on the form `|lc command |lt text |le`. Clickable links are generally only parsed in the _outgoing_ direction, since if users could provde them, they could be a potential security problem. To activate, `MXP_ENABLED=True` must be added to settings (disabled by default).
```
py self.msg("This is a |c look |ltclickable 'look' link|le")
```
- _FuncParser callables_ - These are full-fledged function calls on the form `$funcname(args, kwargs)` that lead to calls to Python functions. The parser can be run with different available callables in different circumstances. The parser is run on all outgoing messages if `settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED=True` (disabled by default).
```
> say The answer is $eval(40 + 2)!
```

View file

@ -0,0 +1,66 @@
# Text Encodings
Evennia is a text-based game server. This makes it important to understand how
it actually deals with data in the form of text.
Text *byte encodings* describe how a string of text is actually stored in the
computer - that is, the particular sequence of bytes used to represent the
letters of your particular alphabet. A common encoding used in English-speaking
languages is the *ASCII* encoding. This describes the letters in the English
alphabet (Aa-Zz) as well as a bunch of special characters. For describing other
character sets (such as that of other languages with other letters than
English), sets with names such as *Latin-1*, *ISO-8859-3* and *ARMSCII-8*
are used. There are hundreds of different byte encodings in use around the
world.
A string of letters in a byte encoding is represented with the `bytes` type.
In contrast to the byte encoding is the *unicode representation*. In Python
this is the `str` type. The unicode is an internationally agreed-upon table
describing essentially all available letters you could ever want to print.
Everything from English to Chinese alphabets and all in between. So what
Evennia (as well as Python and Django) does is to store everything in Unicode
internally, but then converts the data to one of the encodings whenever
outputting data to the user.
An easy memory aid is that `bytes` are what are sent over the network wire. At
all other times, `str` (unicode) is used. This means that we must convert
between the two at the points where we send/receive network data.
The problem is that when receiving a string of bytes over the network it's
impossible for Evennia to guess which encoding was used - it's just a bunch of
bytes! Evennia must know the encoding in order to convert back and from the
correct unicode representation.
## How to customize encodings
As long as you stick to the standard ASCII character set (which means the
normal English characters, basically) you should not have to worry much
about this section.
If you want to build your game in another language however, or expect your
users to want to use special characters not in ASCII, you need to consider
which encodings you want to support.
As mentioned, there are many, many byte-encodings used around the world. It
should be clear at this point that Evennia can't guess but has to assume or
somehow be told which encoding you want to use to communicate with the server.
Basically the encoding used by your client must be the same encoding used by
the server. This can be customized in two complementary ways.
1. Point users to the default `@encoding` command or the `@options` command.
This allows them to themselves set which encoding they (and their client of
choice) uses. Whereas data will remain stored as unicode strings internally in
Evennia, all data received from and sent to this particular player will be
converted to the given format before transmitting.
1. As a back-up, in case the user-set encoding translation is erroneous or
fails in some other way, Evennia will fall back to trying with the names
defined in the settings variable `ENCODINGS`. This is a list of encoding
names Evennia will try, in order, before giving up and giving an encoding
error message.
Note that having to try several different encodings every input/output adds
unneccesary overhead. Try to guess the most common encodings you players will
use and make sure these are tried first. The International *UTF-8* encoding is
what Evennia assumes by default (and also what Python/Django use normally). See
the Wikipedia article [here](https://en.wikipedia.org/wiki/Text_encodings) for more help.

View file

@ -0,0 +1,47 @@
# Zones
Evennia recommends using [Tags](../Components/Tags.md) to create zones and other groupings.
Say you create a room named *Meadow* in your nice big forest MUD. That's all nice and dandy, but
what if you, in the other end of that forest want another *Meadow*? As a game creator, this can
cause all sorts of confusion. For example, teleporting to *Meadow* will now give you a warning that
there are two *Meadow* s and you have to select which one. It's no problem to do that, you just
choose for example to go to `2-meadow`, but unless you examine them you couldn't be sure which of
the two sat in the magical part of the forest and which didn't.
Another issue is if you want to group rooms in geographic regions. Let's say the "normal" part of
the forest should have separate weather patterns from the magical part. Or maybe a magical
disturbance echoes through all magical-forest rooms. It would then be convenient to be able to
simply find all rooms that are "magical" so you could send messages to them.
## Zones in Evennia
*Zones* try to separate rooms by global location. In our example we would separate the forest into two parts - the magical and the non-magical part. Each have a *Meadow* and rooms belonging to each part should be easy to retrieve.
Many MUD codebases hardcode zones as part of the engine and database. Evennia does no such
distinction.
All objects in Evennia can hold any number of [Tags](../Components/Tags.md). Tags are short labels that you attach to objects. They make it very easy to retrieve groups of objects. An object can have any number of different tags. So let's attach the relevant tag to our forest:
```python
forestobj.tags.add("magicalforest", category="zone")
```
You could add this manually, or automatically during creation somehow (you'd need to modify your
`dig` command for this, most likely). You can also use the default `tag` command during building:
tag forestobj = magicalforest : zone
Henceforth you can then easily retrieve only objects with a given tag:
```python
import evennia
rooms = evennia.search_tag("magicalforest", category="zone")
```
## Using typeclasses and inheritance for zoning
The tagging or aliasing systems above don't instill any sort of functional difference between a magical forest room and a normal one - they are just arbitrary ways to mark objects for quick retrieval later. Any functional differences must be expressed using [Typeclasses](../Components/Typeclasses.md).
Of course, an alternative way to implement zones themselves is to have all rooms/objects in a zone inherit from a given typeclass parent - and then limit your searches to objects inheriting from that given parent. The effect would be similar but you'd need to expand the search functionality to
properly search the inheritance tree.

View file

@ -0,0 +1,234 @@
# AWSstorage system
Contrib by The Right Honourable Reverend (trhr), 2020
This plugin migrates the Web-based portion of Evennia, namely images,
javascript, and other items located inside staticfiles into Amazon AWS (S3)
cloud hosting. Great for those serving media with the game.
Files hosted on S3 are "in the cloud," and while your personal
server may be sufficient for serving multimedia to a minimal number of users,
the perfect use case for this plugin would be:
- Servers supporting heavy web-based traffic (webclient, etc) ...
- With a sizable number of users ...
- Where the users are globally distributed ...
- Where multimedia files are served to users as a part of gameplay
Bottom line - if you're sending an image to a player every time they traverse a
map, the bandwidth reduction of using this will be substantial. If not, probably
skip this contrib.
## On costs
Note that storing and serving files via S3 is not technically free outside of
Amazon's "free tier" offering, which you may or may not be eligible for;
setting up a vanilla evennia server with this contrib currently requires 1.5MB
of storage space on S3, making the current total cost of running this plugin
~$0.0005 per year. If you have substantial media assets and intend to serve
them to many users, caveat emptor on a total cost of ownership - check AWS's
pricing structure.
## Technical details
This is a drop-in replacement that operates deeper than all of Evennia's code,
so your existing code does not need to change at all to support it.
For example, when Evennia (or Django), tries to save a file permanently (say, an
image uploaded by a user), the save (or load) communication follows the path:
Evennia -> Django
Django -> Storage backend
Storage backend -> file storage location (e.g. hard drive)
[django docs](https://docs.djangoproject.com/en/4.1/ref/settings/#std:setting-STATICFILES_STORAGE)
This plugin, when enabled, overrides the default storage backend,
which defaults to saving files at mygame/website/, instead,
sending the files to S3 via the storage backend defined herein.
There is no way (or need) to directly access or use the functions here with
other contributions or custom code. Simply work how you would normally, Django
will handle the rest.
## Installation
### Set up AWS account
If you don't have an AWS S3 account, you should create one at
https://aws.amazon.com/ - documentation for AWS S3 is available at:
https://docs.aws.amazon.com/AmazonS3/latest/gsg/GetStartedWithS3.html
Credentials required within the app are AWS IAM Access Key and Secret Keys,
which can be generated/found in the AWS Console.
The following example IAM Control Policy Permissions can be added to
the IAM service inside AWS. Documentation for this can be found here:
https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html
Note that this is only required if you want to tightly secure the roles
that this plugin has access to.
```
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "evennia",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObjectAcl",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject",
"s3:PutObjectAcl"
],
"Resource": [
"arn:aws:s3:::YOUR_BUCKET_NAME/*",
"arn:aws:s3:::YOUR_BUCKET_NAME"
]
}
],
[
{
"Sid":"evennia",
"Effect":"Allow",
"Action":[
"s3:CreateBucket",
],
"Resource":[
"arn:aws:s3:::*"
]
}
]
}
```
Advanced Users: The second IAM statement, CreateBucket, is only needed
for initial installation. You can remove it later, or you can
create the bucket and set the ACL yourself before you continue.
## Dependencies
This package requires the dependency "boto3 >= 1.4.4", the official
AWS python package. To install, it's easiest to just install Evennia's
extra requirements;
pip install evennia[extra]
If you installed Evennia with `git`, you can also
- `cd` to the root of the Evennia repository.
- `pip install --upgrade -e .[extra]`
## Configure Evennia
Customize the variables defined below in `secret_settings.py`. No further
configuration is needed. Note the three lines that you need to set to your
actual values.
```python
# START OF SECRET_SETTINGS.PY COPY/PASTE >>>
AWS_ACCESS_KEY_ID = 'THIS_IS_PROVIDED_BY_AMAZON'
AWS_SECRET_ACCESS_KEY = 'THIS_IS_PROVIDED_BY_AMAZON'
AWS_STORAGE_BUCKET_NAME = 'mygame-evennia' # CHANGE ME! I suggest yourgamename-evennia
# The settings below need to go in secret_settings,py as well, but will
# not need customization unless you want to do something particularly fancy.
AWS_S3_REGION_NAME = 'us-east-1' # N. Virginia
AWS_S3_OBJECT_PARAMETERS = { 'Expires': 'Thu, 31 Dec 2099 20:00:00 GMT',
'CacheControl': 'max-age=94608000', }
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_CUSTOM_DOMAIN = '%s.s3.amazonaws.com' % settings.AWS_BUCKET_NAME
AWS_AUTO_CREATE_BUCKET = True
STATICFILES_STORAGE = 'evennia.contrib.base_systems.awsstorage.aws-s3-cdn.S3Boto3Storage'
# <<< END OF SECRET_SETTINGS.PY COPY/PASTE
```
You may also store these keys as environment variables of the same name.
For advanced configuration, refer to the docs for django-storages.
After copying the above, run `evennia reboot`.
## Check that it works
Confirm that web assets are being served from S3 by visiting your website, then
checking the source of any image (for instance, the logo). It should read
`https://your-bucket-name.s3.amazonaws.com/path/to/file`. If so, the system
works and you shouldn't need to do anything else.
## Uninstallation
If you haven't made changes to your static files (uploaded images, etc),
you can simply remove the lines you added to `secret_settings.py`. If you
have made changes and want to uninstall at a later date, you can export
your files from your S3 bucket and put them in /static/ in the evennia
directory.
## License
Draws heavily from code provided by django-storages, for which these contributors
are authors:
Marty Alchin (S3)
David Larlet (S3)
Arne Brodowski (S3)
Sebastian Serrano (S3)
Andrew McClain (MogileFS)
Rafal Jonca (FTP)
Chris McCormick (S3 with Boto)
Ivanov E. (Database)
Ariel Núñez (packaging)
Wim Leers (SymlinkOrCopy + patches)
Michael Elsdörfer (Overwrite + PEP8 compatibility)
Christian Klein (CouchDB)
Rich Leland (Mosso Cloud Files)
Jason Christa (patches)
Adam Nelson (patches)
Erik CW (S3 encryption)
Axel Gembe (Hash path)
Waldemar Kornewald (MongoDB)
Russell Keith-Magee (Apache LibCloud patches)
Jannis Leidel (S3 and GS with Boto)
Andrei Coman (Azure)
Chris Streeter (S3 with Boto)
Josh Schneier (Fork maintainer, Bugfixes, Py3K)
Anthony Monthe (Dropbox)
EunPyo (Andrew) Hong (Azure)
Michael Barrientos (S3 with Boto3)
piglei (patches)
Matt Braymer-Hayes (S3 with Boto3)
Eirik Martiniussen Sylliaas (Google Cloud Storage native support)
Jody McIntyre (Google Cloud Storage native support)
Stanislav Kaledin (Bug fixes in SFTPStorage)
Filip Vavera (Google Cloud MIME types support)
Max Malysh (Dropbox large file support)
Scott White (Google Cloud updates)
Alex Watt (Google Cloud Storage patch)
Jumpei Yoshimura (S3 docs)
Jon Dufresne
Rodrigo Gadea (Dropbox fixes)
Martey Dodoo
Chris Rink
Shaung Cheng (S3 docs)
Andrew Perry (Bug fixes in SFTPStorage)
The repurposed code from django-storages is released under BSD 3-Clause,
same as Evennia, so for detailed licensing, refer to the Evennia license.
## Versioning
This is confirmed to work for Django 2 and Django 3.
----
<small>This document page is generated from `evennia/contrib/base_systems/awsstorage/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,251 @@
# Achievements
A simple, but reasonably comprehensive, system for tracking achievements. Achievements are defined using ordinary Python dicts, reminiscent of the core prototypes system, and while it's expected you'll use it only on Characters or Accounts, they can be tracked for any typeclassed object.
The contrib provides several functions for tracking and accessing achievements, as well as a basic in-game command for viewing achievement status.
## Installation
This contrib requires creation one or more module files containing your achievement data, which you then add to your settings file to make them available.
> See the section below on "Creating Achievements" for what to put in this module.
```python
# in server/conf/settings.py
ACHIEVEMENT_CONTRIB_MODULES = ["world.achievements"]
```
To allow players to check their achievements, you'll also want to add the `achievements` command to your default Character and/or Account command sets.
```python
# in commands/default_cmdsets.py
from evennia.contrib.game_systems.achievements.achievements import CmdAchieve
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
# ...
self.add(CmdAchieve)
```
**Optional** - The achievements contrib stores individual progress data on the `achievements` attribute by default, visible via `obj.db.achievements`. You can change this by assigning an attribute (key, category) tuple to the setting `ACHIEVEMENT_CONTRIB_ATTRIBUTE`
Example:
```py
# in settings.py
ACHIEVEMENT_CONTRIB_ATTRIBUTE = ("progress_data", "achievements")
```
## Creating achievements
An achievement is represented by a simple python dictionary defined at the module level in your achievements module(s).
Each achievement requires certain specific keys to be defined to work properly, along with several optional keys that you can use to override defaults.
> Note: Any additional keys not described here are included in the achievement data when you access those acheivements through the contrib, so you can easily add your own extended features.
#### Required keys
- **name** (str): The searchable name for the achievement. Doesn't need to be unique.
- **category** (str): The category, or general type, of condition which can progress this achievement. Usually this will be a player action or result. e.g. you would use a category of "defeat" on an achievement for killing 10 rats.
- **tracking** (str or list): The specific subset of condition which can progress this achievement. e.g. you would use a tracking value of "rat" on an achievement for killing 10 rats. An achievement can also track multiple things, for example killing 10 rats or snakes. For that situation, assign a list of all the values to check against, e.g. `["rat", "snake"]`
#### Optional keys
- **key** (str): *Default value if unset: the variable name.* The unique, case-insensitive key identifying this achievement.
> Note: If any achievements have the same unique key, only *one* will be loaded. It is case-insensitive, but punctuation is respected - "ten_rats", "Ten_Rats" and "TEN_RATS" will conflict, but "ten_rats" and "ten rats" will not.
- **desc** (str): A longer description of the achievement. Common uses for this would be flavor text or hints on how to complete it.
- **count** (int): *Default value if unset: 1* The number of tallies this achievement's requirements need to build up in order to complete the achievement. e.g. killing 10 rats would have a `"count"` value of `10`. For achievements using the "separate" tracking type, *each* item being tracked must tally up to this number to be completed.
- **tracking_type** (str): *Default value if unset: `"sum"`* There are two valid tracking types: "sum" (which is the default) and "separate". `"sum"` will increment a single counter every time any of the tracked items match. `"separate"` will have a counter for each individual item in the tracked items. ("See the Example Achievements" section for a demonstration of the difference.)
- **prereqs** (str or list): The *keys* of any achievements which must be completed before this achievement can start tracking progress.
### Example achievements
A simple achievement which you can get just for logging in the first time. This achievement has no prerequisites and it only needs to be fulfilled once to complete.
```python
# This achievement has the unique key of "first_login_achieve"
FIRST_LOGIN_ACHIEVE = {
"name": "Welcome!", # the searchable, player-friendly display name
"desc": "We're glad to have you here.", # the longer description
"category": "login", # the type of action this tracks
"tracking": "first", # the specific login action
}
```
An achievement for killing a total of 10 rats, and another for killing 10 *dire* rats which requires the "kill 10 rats" achievement to be completed first. The dire rats achievement won't begin tracking *any* progress until the first achievement is completed.
```python
# This achievement has the unique key of "ten_rats" instead of "achieve_ten_rats"
ACHIEVE_TEN_RATS = {
"key": "ten_rats",
"name": "The Usual",
"desc": "Why do all these inns have rat problems?",
"category": "defeat",
"tracking": "rat",
"count": 10,
}
ACHIEVE_DIRE_RATS = {
"name": "Once More, But Bigger",
"desc": "Somehow, normal rats just aren't enough any more.",
"category": "defeat",
"tracking": "dire rat",
"count": 10,
"prereqs": "ACHIEVE_TEN_RATS",
}
```
An achievement for buying a total of 5 of apples, oranges, *or* pears. The "sum" tracking types means that all items are tallied together - so it can be completed by buying 5 apples, or 5 pears, or 3 apples, 1 orange and 1 pear, or any other combination of those three fruits that totals to 5.
```python
FRUIT_FAN_ACHIEVEMENT = {
"name": "A Fan of Fruit", # note, there is no desc here - that's allowed!
"category": "buy",
"tracking": ("apple", "orange", "pear"),
"count": 5,
"tracking_type": "sum", # this is the default, but it's included here for clarity
}
```
An achievement for buying 5 *each* of apples, oranges, and pears. The "separate" tracking type means that each of the tracked items is tallied independently of the other items - so you will need 5 apples, 5 oranges, and 5 pears.
```python
FRUIT_BASKET_ACHIEVEMENT = {
"name": "Fruit Basket",
"desc": "One kind of fruit just isn't enough.",
"category": "buy",
"tracking": ("apple", "orange", "pear"),
"count": 5,
"tracking_type": "separate",
}
```
## Usage
The two main things you'll need to do in order to use the achievements contrib in your game are **tracking achievements** and **getting achievement information**. The first is done with the function `track_achievements`; the second can be done with `search_achievement` or `get_achievement`.
### Tracking achievements
#### `track_achievements`
In any actions or functions in your game's mechanics which you might want to track in an achievement, add a call to `track_achievements` to update that player's achievement progress.
Using the "kill 10 rats" example achievement from earlier, you might have some code that triggers when a character is defeated: for the sake of example, we'll pretend we have an `at_defeated` method on the base Object class that gets called when the Object is defeated.
Adding achievement tracking to it could then look something like this:
```python
# in typeclasses/objects.py
from contrib.game_systems.achievements import track_achievements
class Object(ObjectParent, DefaultObject):
# ....
def at_defeated(self, victor):
"""called when this object is defeated in combat"""
# we'll use the "mob_type" tag-category as the tracked info
# this way we can have rats named "black rat" and "brown rat" that are both rats
mob_type = self.tags.get(category="mob_type")
# only one mob was defeated, so we include a count of 1
track_achievements(victor, category="defeated", tracking=mob_type, count=1)
```
If a player defeats something tagged `rat` with a tag category of `mob_type`, it'd now count towards the rat-killing achievement.
You can also have the tracking information hard-coded into your game, for special or unique situations. The achievement described earlier, `FIRST_LOGIN_ACHIEVE`, for example, would be tracked like this:
```py
# in typeclasses/accounts.py
from contrib.game_systems.achievements import track_achievements
class Account(DefaultAccount):
# ...
def at_first_login(self, **kwargs):
# this function is only called on the first time the account logs in
# so we already know and can just tell the tracker that this is the first
track_achievements(self, category="login", tracking="first")
```
The `track_achievements` function does also return a value: an iterable of keys for any achievements which were newly completed by that update. You can ignore this value, or you can use it to e.g. send a message to the player with their latest achievements.
### Getting achievements
The main method for getting a specific achievement's information is `get_achievement`, which takes an already-known achievement key and returns the data for that one achievement.
For handling more variable and player-friendly input, however, there is also `search_achievement`, which does partial matching on not just the keys, but also the display names and descriptions for the achievements.
#### `get_achievement`
A utility function for retrieving a specific achievement's data from the achievement's unique key. It cannot be used for searching, but if you already have an achievement's key - for example, from the results of `track_achievements` - you can retrieve its data this way.
#### Example:
```py
from evennia.contrib.game_systems.achievements import get_achievement
def toast(achiever, completed_list):
if completed_list:
# `completed_data` will be a list of dictionaries - unrecognized keys return empty dictionaries
completed_data = [get_achievement(key) for key in args]
names = [data.get('name') for data in completed]
achiever.msg(f"|wAchievement Get!|n {iter_to_str(name for name in names if name)}"))
```
#### `search_achievement`
A utility function for searching achievements by name or description. It handles partial matching and returns a dictionary of matching achievements. The provided `achievement` command for in-game uses this function to find matching achievements from user inputs.
#### Example:
The first example does a search for "fruit", which returns the fruit medley achievement as it contains "fruit" in the key and name.
The second example searches for "usual", which returns the ten rats achievement due to its display name.
```py
>>> from evennia.contrib.game_systems.achievements import search_achievement
>>> search_achievement("fruit")
{'fruit_basket_achievement': {'name': 'Fruit Basket', 'desc': "One kind of fruit just isn't enough.", 'category': 'buy', 'tracking': ('apple', 'orange', 'pear'), 'count': 5, 'tracking_type': 'separate'}}
>>> search_achievement("usual")
{'ten_rats': {'key': 'ten_rats', 'name': 'The Usual', 'desc': 'Why do all these inns have rat problems?', 'category': 'defeat', 'tracking': 'rat', 'count': 10}}
```
### The `achievements` command
The contrib's provided command, `CmdAchieve`, aims to be usable as-is, with multiple switches to filter achievements by various progress statuses and the ability to search by achievement names.
To make it easier to customize for your own game (e.g. displaying some of that extra achievement data you might have added), the format and style code is split out from the command logic into the `format_achievement` method and the `template` attribute, both on `CmdAchieve`
#### Example output
```
> achievements
The Usual
Why do all these inns have rat problems?
70% complete
A Fan of Fruit
Not Started
```
```
> achievements/progress
The Usual
Why do all these inns have rat problems?
70% complete
```
```
> achievements/done
There are no matching achievements.
```
----
<small>This document page is generated from `evennia/contrib/game_systems/achievements/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,77 @@
# Input/Output Auditing
Contribution by Johnny, 2017
Utility that taps and intercepts all data sent to/from clients and the
server and passes it to a callback of your choosing. This is intended for
quality assurance, post-incident investigations and debugging.
Note that this should be used with care since it can obviously be abused. All
data is recorded in cleartext. Please be ethical, and if you are unwilling to
properly deal with the implications of recording user passwords or private
communications, please do not enable this module.
Some checks have been implemented to protect the privacy of users.
Files included in this module:
outputs.py - Example callback methods. This module ships with examples of
callbacks that send data as JSON to a file in your game/server/logs
dir or to your native Linux syslog daemon. You can of course write
your own to do other things like post them to Kafka topics.
server.py - Extends the Evennia ServerSession object to pipe data to the
callback upon receipt.
tests.py - Unit tests that check to make sure commands with sensitive
arguments are having their PII scrubbed.
## Installation/Configuration:
Deployment is completed by configuring a few settings in server.conf. This line
is required:
SERVER_SESSION_CLASS = 'evennia.contrib.utils.auditing.server.AuditedServerSession'
This tells Evennia to use this ServerSession instead of its own. Below are the
other possible options along with the default value that will be used if unset.
# Where to send logs? Define the path to a module containing your callback
# function. It should take a single dict argument as input
AUDIT_CALLBACK = 'evennia.contrib.utils.auditing.outputs.to_file'
# Log user input? Be ethical about this; it will log all private and
# public communications between players and/or admins (default: False).
AUDIT_IN = False
# Log server output? This will result in logging of ALL system
# messages and ALL broadcasts to connected players, so on a busy game any
# broadcast to all users will yield a single event for every connected user!
AUDIT_OUT = False
# The default output is a dict. Do you want to allow key:value pairs with
# null/blank values? If you're just writing to disk, disabling this saves
# some disk space, but whether you *want* sparse values or not is more of a
# consideration if you're shipping logs to a NoSQL/schemaless database.
# (default: False)
AUDIT_ALLOW_SPARSE = False
# If you write custom commands that handle sensitive data like passwords,
# you must write a regular expression to remove that before writing to log.
# AUDIT_MASKS is a list of dictionaries that define the names of commands
# and the regexes needed to scrub them.
# The system already has defaults to filter out sensitive login/creation
# commands in the default command set. Your list of AUDIT_MASKS will be appended
# to those defaults.
#
# In the regex, the sensitive data itself must be captured in a named group with a
# label of 'secret' (see the Python docs on the `re` module for more info). For
# example: `{'authentication': r"^@auth\s+(?P<secret>[\w]+)"}`
AUDIT_MASKS = []
----
<small>This document page is generated from `evennia/contrib/utils/auditing/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,130 @@
# Barter system
Contribution by Griatch, 2012
This implements a full barter system - a way for players to safely
trade items between each other in code rather than simple `give/get`
commands. This increases both safety (at no time will one player have
both goods and payment in-hand) and speed, since agreed goods will
be moved automatically). By just replacing one side with coin objects,
(or a mix of coins and goods), this also works fine for regular money
transactions.
## Installation
Just import the CmdsetTrade command into (for example) the default
cmdset. This will make the trade (or barter) command available
in-game.
```python
# in mygame/commands/default_cmdsets.py
from evennia.contrib.game_systems import barter # <---
# ...
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# ...
def at cmdset_creation(self):
# ...
self.add(barter.CmdsetTrade) # <---
```
## Usage
In this module, a "barter" is generally referred to as a "trade".
Below is an example of a barter sequence. A and B are the parties.
The `A>` and `B>` are their inputs.
1) opening a trade
A> trade B: Hi, I have a nice extra sword. You wanna trade?
B sees:
A says: "Hi, I have a nice extra sword. You wanna trade?"
A wants to trade with you. Enter 'trade A <emote>' to accept.
B> trade A: Hm, I could use a good sword ...
A sees:
B says: "Hm, I could use a good sword ...
B accepts the trade. Use 'trade help' for aid.
B sees:
You are now trading with A. Use 'trade help' for aid.
2) negotiating
A> offer sword: This is a nice sword. I would need some rations in trade.
B sees: A says: "This is a nice sword. I would need some rations in trade."
[A offers Sword of might.]
B> evaluate sword
B sees:
<Sword's description and possibly stats>
B> offer ration: This is a prime ration.
A sees:
B says: "This is a prime ration."
[B offers iron ration]
A> say Hey, this is a nice sword, I need something more for it.
B sees:
A says: "Hey this is a nice sword, I need something more for it."
B> offer sword,apple: Alright. I will also include a magic apple. That's my last offer.
A sees:
B says: "Alright, I will also include a magic apple. That's my last offer."
[B offers iron ration and magic apple]
A> accept: You are killing me here, but alright.
B sees: A says: "You are killing me here, but alright."
[A accepts your offer. You must now also accept.]
B> accept: Good, nice making business with you.
You accept the deal. Deal is made and goods changed hands.
A sees: B says: "Good, nice making business with you."
B accepts the deal. Deal is made and goods changed hands.
At this point the trading system is exited and the negotiated items
are automatically exchanged between the parties. In this example B was
the only one changing their offer, but also A could have changed their
offer until the two parties found something they could agree on. The
emotes are optional but useful for RP-heavy worlds.
## Technical info
The trade is implemented by use of a TradeHandler. This object is a
common place for storing the current status of negotiations. It is
created on the object initiating the trade, and also stored on the
other party once that party agrees to trade. The trade request times
out after a certain time - this is handled by a Script. Once trade
starts, the CmdsetTrade cmdset is initiated on both parties along with
the commands relevant for the trading.
## Ideas for NPC bartering
This module is primarily intended for trade between two players. But
it can also in principle be used for a player negotiating with an
AI-controlled NPC. If the NPC uses normal commands they can use it
directly -- but more efficient is to have the NPC object send its
replies directly through the tradehandler to the player. One may want
to add some functionality to the decline command, so players can
decline specific objects in the NPC offer (decline <object>) and allow
the AI to maybe offer something else and make it into a proper
barter. Along with an AI that "needs" things or has some sort of
personality in the trading, this can make bartering with NPCs at least
moderately more interesting than just plain 'buy'.
----
<small>This document page is generated from `evennia/contrib/game_systems/barter/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,41 @@
# Batch processor examples
Contibution by Griatch, 2012
Simple examples for the batch-processor. The batch processor is used for generating
in-game content from one or more static files. Files can be stored with version
control and then 'applied' to the game to create content.
There are two batch processor types:
- Batch-cmd processor: A list of `#`-separated Evennia commands being executed
in sequence, such as `create`, `dig`, `north` etc. When running a script
of this type (filename ending with `.ev`), the caller of the script will be
the one performing the script's actions.
- Batch-code processor: A full Python script (filename ending with `.py` that
executes Evennia api calls to build, such as `evennia.create_object` or
`evennia.search_object` etc. It can be divided up into comment-separated
chunks so one can execute only parts of the script at a time (in this way it's
a little different than a normal Python file).
## Usage
To test the two example batch files, you need `Developer` or `superuser`
permissions, be logged into the game and run of
> batchcommand/interactive tutorials.batchprocessor.example_batch_cmds
> batchcode/interactive tutorials.batchprocessor.example_batch_code
The `/interactive` drops you in interactive mode so you can follow along what
the scripts do. Skip it to build it all at once.
Both commands produce the same results - they create a red-button object,
a table and a chair. If you run either with the `/debug` switch, the objects will
be deleted afterwards (for quick tests of syntax that you don't want to spam new
objects, for example).
----
<small>This document page is generated from `evennia/contrib/tutorials/batchprocessor/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,22 @@
# Script example
Contribution by Griatch, 2012
Example script for testing. This adds a simple timer that has your
character make small verbal observations at irregular intervals.
To test, use (in game)
> script me = contrib.tutorials.bodyfunctions.BodyFunctions
## Notes
Use `scripts me` to see the script running on you. Note that even though
the timer ticks down to 0, you will _not_ see an echo every tick (it's
random if an echo is given on a tick or not).
----
<small>This document page is generated from `evennia/contrib/tutorials/bodyfunctions/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,440 @@
# Buffs
Contribution by Tegiminis 2022
A buff is a timed object, attached to a game entity. It is capable of modifying values, triggering code, or both.
It is a common design pattern in RPGs, particularly action games.
Features:
- `BuffHandler`: A buff handler to apply to your objects.
- `BaseBuff`: A buff class to extend from to create your own buffs.
- `BuffableProperty`: A sample property class to show how to automatically check modifiers.
- `CmdBuff`: A command which applies buffs.
- `samplebuffs.py`: Some sample buffs to learn from.
## Quick Start
Assign the handler to a property on the object, like so.
```python
@lazy_property
def buffs(self) -> BuffHandler:
return BuffHandler(self)
```
You may then call the handler to add or manipulate buffs like so: `object.buffs`. See **Using the Handler**.
### Customization
If you want to customize the handler, you can feed the constructor two arguments:
- `dbkey`: The string you wish to use as the attribute key for the buff database. Defaults to "buffs". This allows you to keep separate buff pools - for example, "buffs" and "perks".
- `autopause`: If you want this handler to automatically pause playtime buffs when its owning object is unpuppeted.
> **Note**: If you enable autopausing, you MUST initialize the property in your owning object's
> `at_init` hook. Otherwise, a hot reload can cause playtime buffs to not update properly
> on puppet/unpuppet. You have been warned!
Let's say you want another handler for an object, `perks`, which has a separate database and
respects playtime buffs. You'd assign this new property as so:
```python
class BuffableObject(Object):
@lazy_property
def perks(self) -> BuffHandler:
return BuffHandler(self, dbkey='perks', autopause=True)
def at_init(self):
self.perks
```
## Using the Handler
Here's how to make use of your new handler.
### Apply a Buff
Call the handler's `add` method. This requires a class reference, and also contains a number of
optional arguments to customize the buff's duration, stacks, and so on. You can also store any arbitrary value
in the buff's cache by passing a dictionary through the `to_cache` optional argument. This will not overwrite the normal
values on the cache.
```python
self.buffs.add(StrengthBuff) # A single stack of StrengthBuff with normal duration
self.buffs.add(DexBuff, stacks=3, duration=60) # Three stacks of DexBuff, with a duration of 60 seconds
self.buffs.add(ReflectBuff, to_cache={'reflect': 0.5}) # A single stack of ReflectBuff, with an extra cache value
```
Two important attributes on the buff are checked when the buff is applied: `refresh` and `unique`.
- `refresh` (default: True) determines if a buff's timer is refreshed when it is reapplied.
- `unique` (default: True) determines if this buff is unique; that is, only one of it exists on the object.
The combination of these two booleans creates one of three kinds of keys:
- `Unique is True, Refresh is True/False`: The buff's default key.
- `Unique is False, Refresh is True`: The default key mixed with the applier's dbref. This makes the buff "unique-per-player", so you can refresh through reapplication.
- `Unique is False, Refresh is False`: The default key mixed with a randomized number.
### Get Buffs
The handler has several getter methods which return instanced buffs. You won't need to use these for basic functionality, but if you want to manipulate
buffs after application, they are very useful. The handler's `check`/`trigger` methods utilize some of these getters, while others are just for developer convenience.
`get(key)` is the most basic getter. It returns a single buff instance, or `None` if the buff doesn't exist on the handler. It is also the only getter
that returns a single buff instance, rather than a dictionary.
> **Note**: The handler method `has(buff)` allows you to check if a matching key (if a string) or buff class (if a class) is present on the handler cache, without actually instantiating the buff. You should use this method for basic "is this buff present?" checks.
Group getters, listed below, return a dictionary of values in the format `{buffkey: instance}`. If you want to iterate over all of these buffs,
you should do so via the `dict.values()` method.
- `get_all()` returns all buffs on this handler. You can also use the `handler.all` property.
- `get_by_type(BuffClass)` returns buffs of the specified type.
- `get_by_stat(stat)` returns buffs with a `Mod` object of the specified `stat` string in their `mods` list.
- `get_by_trigger(string)` returns buffs with the specified string in their `triggers` list.
- `get_by_source(Object)` returns buffs applied by the specified `source` object.
- `get_by_cachevalue(key, value)` returns buffs with the matching `key: value` pair in their cache. `value` is optional.
All group getters besides `get_all()` can "slice" an existing dictionary through the optional `to_filter` argument.
```python
dict1 = handler.get_by_type(Burned) # This finds all "Burned" buffs on the handler
dict2 = handler.get_by_source(self, to_filter=dict1) # This filters dict1 to find buffs with the matching source
```
> **Note**: Most of these getters also have an associated handler property. For example, `handler.effects` returns all buffs that can be triggered, which
> is then iterated over by the `get_by_trigger` method.
### Remove Buffs
There are also a number of remover methods. Generally speaking, these follow the same format as the getters.
- `remove(key)` removes the buff with the specified key.
- `clear()` removes all buffs.
- `remove_by_type(BuffClass)` removes buffs of the specified type.
- `remove_by_stat(stat)` removes buffs with a `Mod` object of the specified `stat` string in their `mods` list.
- `remove_by_trigger(string)` removes buffs with the specified string in their `triggers` list.
- `remove_by_source(Object)` removes buffs applied by the specified source
- `remove_by_cachevalue(key, value)` removes buffs with the matching `key: value` pair in their cache. `value` is optional.
You can also remove a buff by calling the instance's `remove` helper method. You can do this on the dictionaries returned by the
getters listed above.
```python
to_remove = handler.get_by_trigger(trigger) # Finds all buffs with the specified trigger
for buff in to_remove.values(): # Removes all buffs in the to_remove dictionary via helper methods
buff.remove()
```
### Check Modifiers
Call the handler `check(value, stat)` method when you want to see the modified value.
This will return the `value`, modified by any relevant buffs on the handler's owner (identified by
the `stat` string).
For example, let's say you want to modify how much damage you take. That might look something like this:
```python
# The method we call to damage ourselves
def take_damage(self, source, damage):
_damage = self.buffs.check(damage, 'taken_damage')
self.db.health -= _damage
```
This method calls the `at_pre_check` and `at_post_check` methods at the relevant points in the process. You can use to this make
buffs that are reactive to being checked; for example, removing themselves, altering their values, or interacting with the game state.
> **Note**: You can also trigger relevant buffs at the same time as you check them by ensuring the optional argument `trigger` is True in the `check` method.
Modifiers are calculated additively - that is, all modifiers of the same type are added together before being applied. They are then
applied through the following formula.
```python
(base + total_add) / max(1, 1.0 + total_div) * max(0, 1.0 + total_mult)
```
#### Multiplicative Buffs (Advanced)
Multiply/divide modifiers in this buff system are additive by default. This means that two +50% modifiers will equal a +100% modifier. But what if you want to apply mods multiplicatively?
First, you should carefully consider if you truly want multiplicative modifiers. Here's some things to consider.
- They are unintuitive to the average user, as two +50% damage buffs equal +125% instead of +100%.
- They lead to "power explosion", where stacking buffs in the right way can turn characters into unstoppable forces
Doing purely-additive multipliers allows you to better control the balance of your game. Conversely, doing multiplicative multipliers enables very fun build-crafting where smart usage of buffs and skills can turn you into a one-shot powerhouse. Each has its place.
The best design practice for multiplicative buffs is to divide your multipliers into "tiers", where each tier is applied separately. You can easily do this with multiple `check` calls.
```python
damage = damage
damage = handler.check(damage, 'damage')
damage = handler.check(damage, 'empower')
damage = handler.check(damage, 'radiant')
damage = handler.check(damage, 'overpower')
```
#### Buff Strength Priority (Advanced)
Sometimes you only want to apply the strongest modifier to a stat. This is supported by the optional `strongest` bool arg in the handler's check method
```python
def take_damage(self, source, damage):
_damage = self.buffs.check(damage, 'taken_damage', strongest=True)
self.db.health -= _damage
```
### Trigger Buffs
Call the handler's `trigger(string)` method when you want an event call. This will call the `at_trigger` hook method on all buffs with the relevant trigger `string`.
For example, let's say you want to trigger a buff to "detonate" when you hit your target with an attack.
You'd write a buff that might look like this:
```python
class Detonate(BaseBuff):
...
triggers = ['take_damage']
def at_trigger(self, trigger, *args, **kwargs)
self.owner.take_damage(100)
self.remove()
```
And then call `handler.trigger('take_damage')` in the method you use to take damage.
> **Note** You could also do this through mods and `at_post_check` if you like, depending on how to want to add the damage.
### Ticking
Ticking buffs are slightly special. They are similar to trigger buffs in that they run code, but instead of
doing so on an event trigger, they do so on a periodic tick. A common use case for a buff like this is a poison,
or a heal over time.
```python
class Poison(BaseBuff):
...
tickrate = 5
def at_tick(self, initial=True, *args, **kwargs):
_dmg = self.dmg * self.stacks
if not initial:
self.owner.location.msg_contents(
"Poison courses through {actor}'s body, dealing {damage} damage.".format(
actor=self.owner.named, damage=_dmg
)
)
```
To make a buff ticking, ensure the `tickrate` is 1 or higher, and it has code in its `at_tick`
method. Once you add it to the handler, it starts ticking!
> **Note**: Ticking buffs always tick on initial application, when `initial` is `True`. If you don't want your hook to fire at that time,
> make sure to check the value of `initial` in your `at_tick` method.
### Context
Every important handler method optionally accepts a `context` dictionary.
Context is an important concept for this handler. Every method which checks, triggers, or ticks a buff passes this
dictionary (default: empty) to the buff hook methods as keyword arguments (`**kwargs`). It is used for nothing else. This allows you to make those
methods "event-aware" by storing relevant data in the dictionary you feed to the method.
For example, let's say you want a "thorns" buff which damages enemies that attack you. Let's take our `take_damage` method
and add a context to the mix.
```python
def take_damage(attacker, damage):
context = {'attacker': attacker, 'damage': damage}
_damage = self.buffs.check(damage, 'taken_damage', context=context)
self.buffs.trigger('taken_damage', context=context)
self.db.health -= _damage
```
Now we use the values that context passes to the buff kwargs to customize our logic.
```python
class ThornsBuff(BaseBuff):
...
triggers = ['taken_damage']
# This is the hook method on our thorns buff
def at_trigger(self, trigger, attacker=None, damage=0, **kwargs):
if not attacker:
return
attacker.db.health -= damage * 0.2
```
Apply the buff, take damage, and watch the thorns buff do its work!
### Viewing
There are two helper methods on the handler that allow you to get useful buff information back.
- `view`: Returns a dictionary of tuples in the format `{buffkey: (buff.name, buff.flavor)}`. Finds all buffs by default, but optionally accepts a dictionary of buffs to filter as well. Useful for basic buff readouts.
- `view_modifiers(stat)`: Returns a nested dictionary of information on modifiers that affect the specified stat. The first layer is the modifier type (`add/mult/div`) and the second layer is the value type (`total/strongest`). Does not return the buffs that cause these modifiers, just the modifiers themselves (akin to using `handler.check` but without actually modifying a value). Useful for stat sheets.
You can also create your own custom viewing methods through the various handler getters, which will always return the entire buff object.
## Creating New Buffs
Creating a new buff is very easy: extend `BaseBuff` into a new class, and fill in all the relevant buff details.
However, there are a lot of individual moving parts to a buff. Here's a step-through of the important stuff.
### Basics
Regardless of any other functionality, all buffs have the following class attributes:
- They have customizable `key`, `name`, and `flavor` strings.
- They have a `duration` (float), and automatically clean-up at the end. Use -1 for infinite duration, and 0 to clean-up immediately. (default: -1)
- They have a `tickrate` (float), and automatically tick if it is greater than 1 (default: 0)
- They can stack, if `maxstacks` (int) is not equal to 1. If it's 0, the buff stacks forever. (default: 1)
- They can be `unique` (bool), which determines if they have a unique namespace or not. (default: True)
- They can `refresh` (bool), which resets the duration when stacked or reapplied. (default: True)
- They can be `playtime` (bool) buffs, where duration only counts down during active play. (default: False)
Buffs also have a few useful properties:
- `owner`: The object this buff is attached to
- `ticknum`: How many ticks the buff has gone through
- `timeleft`: How much time is remaining on the buff
- `ticking`/`stacking`: If this buff ticks/stacks (checks `tickrate` and `maxstacks`)
#### Buff Cache (Advanced)
Buffs always store some useful mutable information about themselves in the cache (what is stored on the owning object's database attribute). A buff's cache corresponds to `{buffkey: buffcache}`, where `buffcache` is a dictionary containing __at least__ the information below:
- `ref` (class): The buff class path we use to construct the buff.
- `start` (float): The timestamp of when the buff was applied.
- `source` (Object): If specified; this allows you to track who or what applied the buff.
- `prevtick` (float): The timestamp of the previous tick.
- `duration` (float): The cached duration. This can vary from the class duration, depending on if the duration has been modified (paused, extended, shortened, etc).
- `tickrate` (float): The buff's tick rate. Cannot go below 0. Altering the tickrate on an applied buff will not cause it to start ticking if it wasn't ticking before. (`pause` and `unpause` to start/stop ticking on existing buffs)
- `stacks` (int): How many stacks they have.
- `paused` (bool): Paused buffs do not clean up, modify values, tick, or fire any hook methods.
Sometimes you will want to dynamically update a buff's cache at runtime, such as changing a tickrate in a hook method, or altering a buff's duration.
You can do so by using the interface `buff.cachekey`. As long as the attribute name matches a key in the cache dictionary, it will update the stored
cache with the new value.
If there is no matching key, it will do nothing. If you wish to add a new key to the cache, you must use the `buff.update_cache(dict)` method,
which will properly update the cache (including adding new keys) using the dictionary provided.
> **Example**: You want to increase a buff's duration by 30 seconds. You use `buff.duration += 30`. This new duration is now reflected on both the instance and the cache.
The buff cache can also store arbitrary information. To do so, pass a dictionary through the handler `add` method (`handler.add(BuffClass, to_cache=dict)`),
set the `cache` dictionary attribute on your buff class, or use the aforementioned `buff.update_cache(dict)` method.
> **Example**: You store `damage` as a value in the buff cache and use it for your poison buff. You want to increase it over time, so you use `buff.damage += 1` in the tick method.
### Modifiers
Mods are stored in the `mods` list attribute. Buffs which have one or more Mod objects in them can modify stats. You can use the handler method to check all
mods of a specific stat string and apply their modifications to the value; however, you are encouraged to use `check` in a getter/setter, for easy access.
Mod objects consist of only four values, assigned by the constructor in this order:
- `stat`: The stat you want to modify. When `check` is called, this string is used to find all the mods that are to be collected.
- `mod`: The modifier. Defaults are `add` (addition/subtraction), `mult` (multiply), and `div` (divide). Modifiers are calculated additively (see `_calculate_mods` for more)
- `value`: How much value the modifier gives regardless of stacks
- `perstack`: How much value the modifier grants per stack, **INCLUDING** the first. (default: 0)
The most basic way to add a Mod to a buff is to do so in the buff class definition, like this:
```python
class DamageBuff(BaseBuff):
mods = [Mod('damage', 'add', 10)]
```
No mods applied to the value are permanent in any way. All calculations are done at runtime, and the mod values are never stored
anywhere except on the buff in question. In other words: you don't need to track the origin of particular stat mods, and you will
never permanently change a stat modified by a buff. To remove the modification, simply remove the buff from the object.
> **Note**: You can add your own modifier types by overloading the `_calculate_mods` method, which contains the basic modifier application logic.
#### Generating Mods (Advanced)
An advanced way to do mods is to generate them when the buff is initialized. This lets you create mods on the fly that are reactive to the game state.
```python
class GeneratedStatBuff(BaseBuff):
...
def at_init(self, *args, **kwargs) -> None:
# Finds our "modgen" cache value, and generates a mod from it
modgen = list(self.cache.get("modgen"))
if modgen:
self.mods = [Mod(*modgen)]
```
### Triggers
Buffs which have one or more strings in the `triggers` attribute can be triggered by events.
When the handler's `trigger` method is called, it searches all buffs on the handler for any with a matchingtrigger,
then calls their `at_trigger` hooks. Buffs can have multiple triggers, and you can tell which trigger was used by
the `trigger` argument in the hook.
```python
class AmplifyBuff(BaseBuff):
triggers = ['damage', 'heal']
def at_trigger(self, trigger, **kwargs):
if trigger == 'damage': print('Damage trigger called!')
if trigger == 'heal': print('Heal trigger called!')
```
### Ticking
A buff which ticks isn't much different than one which triggers. You're still executing arbitrary hooks on
the buff class. To tick, the buff must have a `tickrate` of 1 or higher.
```python
class Poison(BaseBuff):
...
# this buff will tick 6 times between application and cleanup.
duration = 30
tickrate = 5
def at_tick(self, initial, **kwargs):
self.owner.take_damage(10)
```
> **Note**: The buff always ticks once when applied. For this **first tick only**, `initial` will be True in the `at_tick` hook method. `initial` will be False on subsequent ticks.
Ticks utilize a persistent delay, so they should be pickleable. As long as you are not adding new properties to your buff class, this shouldn't be a concern.
If you **are** adding new properties, try to ensure they do not end up with a circular code path to their object or handler, as this will cause pickling errors.
### Extras
Buffs have a grab-bag of extra functionality to let you add complexity to your designs.
#### Conditionals
You can restrict whether or not the buff will `check`, `trigger`, or `tick` through defining the `conditional` hook. As long
as it returns a "truthy" value, the buff will apply itself. This is useful for making buffs dependent on game state - for
example, if you want a buff that makes the player take more damage when they are on fire:
```python
class FireSick(BaseBuff):
...
def conditional(self, *args, **kwargs):
if self.owner.buffs.has(FireBuff):
return True
return False
```
Conditionals for `check`/`trigger` are checked when the buffs are gathered by the handler methods for the respective operations. `Tick`
conditionals are checked each tick.
#### Helper Methods
Buff instances have a number of helper methods.
- `remove`/`dispel`: Allows you to remove or dispel the buff. Calls `at_remove`/`at_dispel`, depending on optional arguments.
- `pause`/`unpause`: Pauses and unpauses the buff. Calls `at_pause`/`at_unpause`.
- `reset`: Resets the buff's start to the current time; same as "refreshing" it.
- `alter_cache`: Updates the buff's cache with the `{key:value}` pairs in the provided dictionary. Can overwrite default values, so be careful!
#### Playtime Duration
If your handler has `autopause` enabled, any buffs with truthy `playtime` value will automatically pause
and unpause when the object the handler is attached to is puppetted or unpuppetted. This even works with ticking buffs,
although if you have less than 1 second of tick duration remaining, it will round up to 1s.
> **Note**: If you want more control over this process, you can comment out the signal subscriptions on the handler and move the autopause logic
> to your object's `at_pre/post_puppet/unpuppet` hooks.
----
<small>This document page is generated from `evennia/contrib/rpg/buffs/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,129 @@
# Character Creator
Contribution by InspectorCaracal, 2022
Commands for managing and initiating an in-game character-creation menu.
## Installation
In your game folder `commands/default_cmdsets.py`, import and add
`ContribChargenCmdSet` to your `AccountCmdSet`.
Example:
```python
from evennia.contrib.rpg.character_creator.character_creator import ContribChargenCmdSet
class AccountCmdSet(default_cmds.AccountCmdSet):
def at_cmdset_creation(self):
super().at_cmdset_creation()
self.add(ContribChargenCmdSet)
```
In your game folder `typeclasses/accounts.py`, import and inherit from `ContribChargenAccount`
on your Account class.
(Alternatively, you can copy the `at_look` method directly into your own class.)
### Example:
```python
from evennia.contrib.rpg.character_creator.character_creator import ContribChargenAccount
class Account(ContribChargenAccount):
# your Account class code
```
In your settings file `server/conf/settings.py`, add the following settings:
```python
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
AUTO_PUPPET_ON_LOGIN = False
```
(If you want to allow players to create more than one character, you can
customize that with the setting `MAX_NR_CHARACTERS`.)
By default, the new `charcreate` command will reference the example menu
provided by the contrib, so you can test it out before building your own menu.
You can reference
[the example menu here](github:develop/evennia/contrib/rpg/character_creator/example_menu.py) for
ideas on how to build your own.
Once you have your own menu, just add it to your settings to use it. e.g. if your menu is in
`mygame/word/chargen_menu.py`, you'd add the following to your settings file:
```python
CHARGEN_MENU = "world.chargen_menu"
```
## Usage
### The EvMenu
In order to use the contrib, you will need to create your own chargen EvMenu.
The included `example_menu.py` gives a number of useful menu node techniques
with basic attribute examples for you to reference. It can be run as-is as a
tutorial for yourself/your devs, or used as base for your own menu.
The example menu includes code, tips, and instructions for the following types
of decision nodes:
#### Informational Pages
A small set of nodes that let you page through information on different choices before committing to one.
#### Option Categories
A pair of nodes which let you divide an arbitrary number of options into separate categories.
The base node has a list of categories as the options, and the child node displays the actual character choices.
#### Multiple Choice
Allows players to select and deselect options from the list in order to choose more than one.
#### Starting Objects
Allows players to choose from a selection of starting objects, which are then created on chargen completion.
#### Choosing a Name
The contrib assumes the player will choose their name during character creation,
so the necessary code for doing so is of course included!
### `charcreate` command
The contrib overrides the character creation command - `charcreate` - to use a
character creator menu, as well as supporting exiting/resuming the process. In
addition, unlike the core command, it's designed for the character name to be
chosen later on via the menu, so it won't parse any arguments passed to it.
### Changes to `Account`
The contrib version works mostly the same as core evennia, but modifies `ooc_appearance_template`
to match the contrib's command syntax, and the `at_look` method to recognize an in-progress
character.
If you've modified your own `at_look` hook, it's an easy change to add: just add this section to the
playable character list loop.
```python
# the beginning of the loop starts here
for char in characters:
# ...
# contrib code starts here
if char.db.chargen_step:
# currently in-progress character; don't display placeholder names
result.append(" - |Yin progress|n (|wcharcreate|n to continue)")
continue
# the rest of your code continues here
```
----
<small>This document page is generated from `evennia/contrib/rpg/character_creator/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,142 @@
# Clothing
Contribution by Tim Ashley Jenkins, 2017
Provides a typeclass and commands for wearable clothing. These
look of these clothes are appended to the character's description when worn.
Clothing items, when worn, are added to the character's description
in a list. For example, if wearing the following clothing items:
a thin and delicate necklace
a pair of regular ol' shoes
one nice hat
a very pretty dress
Would result in this added description:
Tim is wearing one nice hat, a thin and delicate necklace,
a very pretty dress and a pair of regular ol' shoes.
## Installation
To install, import this module and have your default character
inherit from ClothedCharacter in your game's `characters.py` file:
```python
from evennia.contrib.game_systems.clothing import ClothedCharacter
class Character(ClothedCharacter):
```
And then add `ClothedCharacterCmdSet` in your character set in
`mygame/commands/default_cmdsets.py`:
```python
from evennia.contrib.game_systems.clothing import ClothedCharacterCmdSet # <--
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# ...
at_cmdset_creation(self):
super().at_cmdset_creation()
# ...
self.add(ClothedCharacterCmdSet) # <--
```
## Usage
Once installed, you can use the default builder commands to create clothes
with which to test the system:
create a pretty shirt : evennia.contrib.game_systems.clothing.ContribClothing
set shirt/clothing_type = 'top'
wear shirt
A character's description may look like this:
Superuser(#1)
This is User #1.
Superuser is wearing one nice hat, a thin and delicate necklace,
a very pretty dress and a pair of regular ol' shoes.
Characters can also specify the style of wear for their clothing - I.E.
to wear a scarf 'tied into a tight knot around the neck' or 'draped
loosely across the shoulders' - to add an easy avenue of customization.
For example, after entering:
wear scarf draped loosely across the shoulders
The garment appears like so in the description:
Superuser(#1)
This is User #1.
Superuser is wearing a fanciful-looking scarf draped loosely
across the shoulders.
Items of clothing can be used to cover other items, and many options
are provided to define your own clothing types and their limits and
behaviors. For example, to have undergarments automatically covered
by outerwear, or to put a limit on the number of each type of item
that can be worn. The system as-is is fairly freeform - you
can cover any garment with almost any other, for example - but it
can easily be made more restrictive, and can even be tied into a
system for armor or other equipment.
## Configuration
The contrib has several optional configurations which you can define in your `settings.py`
Here are the settings and their default values.
```python
# Maximum character length of 'wear style' strings, or None for unlimited.
CLOTHING_WEARSTYLE_MAXLENGTH = 50
# The order in which clothing types appear on the description.
# Untyped clothing or clothing with a type not in this list goes last.
CLOTHING_TYPE_ORDERED = [
"hat",
"jewelry",
"top",
"undershirt",
"gloves",
"fullbody",
"bottom",
"underpants",
"socks",
"shoes",
"accessory",
]
# The maximum number of clothing items that can be worn, or None for unlimited.
CLOTHING_OVERALL_LIMIT = 20
# The maximum number for specific clothing types that can be worn.
# If the clothing item has no type or is not specified here, the only maximum is the overall limit.
CLOTHING_TYPE_LIMIT = {"hat": 1, "gloves": 1, "socks": 1, "shoes": 1}
# What types of clothes will automatically cover what other types of clothes when worn.
# Note that clothing only gets auto-covered if it's already being worn. It's perfectly possible
# to have your underpants showing if you put them on after your pants!
CLOTHING_TYPE_AUTOCOVER = {
"top": ["undershirt"],
"bottom": ["underpants"],
"fullbody": ["undershirt", "underpants"],
"shoes": ["socks"],
}
# Any types of clothes that can't be used to cover other clothes at all.
CLOTHING_TYPE_CANT_COVER_WITH = ["jewelry"]
```
----
<small>This document page is generated from `evennia/contrib/game_systems/clothing/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,64 @@
# Additional Color markups
Contrib by Griatch, 2017
Additional color markup styles for Evennia (extending or replacing the default
`|r`, `|234`). Adds support for MUSH-style (`%cr`, `%c123`) and/or legacy-Evennia
(`{r`, `{123`).
## Installation
Import the desired style variables from this module into
mygame/server/conf/settings.py and add them to the settings variables below.
Each are specified as a list, and multiple such lists can be added to each
variable to support multiple formats. Note that list order affects which regexes
are applied first. You must restart both Portal and Server for color tags to
update.
Assign to the following settings variables (see below for example):
COLOR_ANSI_EXTRA_MAP - a mapping between regexes and ANSI colors
COLOR_XTERM256_EXTRA_FG - regex for defining XTERM256 foreground colors
COLOR_XTERM256_EXTRA_BG - regex for defining XTERM256 background colors
COLOR_XTERM256_EXTRA_GFG - regex for defining XTERM256 grayscale foreground colors
COLOR_XTERM256_EXTRA_GBG - regex for defining XTERM256 grayscale background colors
COLOR_ANSI_BRIGHT_BG_EXTRA_MAP = ANSI does not support bright backgrounds; we fake
this by mapping ANSI markup to matching bright XTERM256 backgrounds
COLOR_NO_DEFAULT - Set True/False. If False (default), extend the default
markup, otherwise replace it completely.
## Example
To add the {- "curly-bracket" style, add the following to your settings file,
then reboot both Server and Portal:
```python
from evennia.contrib.base_systems import color_markups
COLOR_ANSI_EXTRA_MAP = color_markups.CURLY_COLOR_ANSI_EXTRA_MAP
COLOR_XTERM256_EXTRA_FG = color_markups.CURLY_COLOR_XTERM256_EXTRA_FG
COLOR_XTERM256_EXTRA_BG = color_markups.CURLY_COLOR_XTERM256_EXTRA_BG
COLOR_XTERM256_EXTRA_GFG = color_markups.CURLY_COLOR_XTERM256_EXTRA_GFG
COLOR_XTERM256_EXTRA_GBG = color_markups.CURLY_COLOR_XTERM256_EXTRA_GBG
COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP = color_markups.CURLY_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
```
To add the `%c-` "mux/mush" style, add the following to your settings file, then
reboot both Server and Portal:
```python
from evennia.contrib.base_systems import color_markups
COLOR_ANSI_EXTRA_MAP = color_markups.MUX_COLOR_ANSI_EXTRA_MAP
COLOR_XTERM256_EXTRA_FG = color_markups.MUX_COLOR_XTERM256_EXTRA_FG
COLOR_XTERM256_EXTRA_BG = color_markups.MUX_COLOR_XTERM256_EXTRA_BG
COLOR_XTERM256_EXTRA_GFG = color_markups.MUX_COLOR_XTERM256_EXTRA_GFG
COLOR_XTERM256_EXTRA_GBG = color_markups.MUX_COLOR_XTERM256_EXTRA_GBG
COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP = color_markups.MUX_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
```
----
<small>This document page is generated from `evennia/contrib/base_systems/color_markups/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,219 @@
# Components
Contrib by ChrisLR, 2021
Expand typeclasses using a components/composition approach.
## The Components Contrib
This contrib introduces Components and Composition to Evennia.
Each 'Component' class represents a feature that will be 'enabled' on a typeclass instance.
You can register these components on an entire typeclass or a single object at runtime.
It supports both persisted attributes and in-memory attributes by using Evennia's AttributeHandler.
## Pros
- You can reuse a feature across multiple typeclasses without inheritance
- You can cleanly organize each feature into a self-contained class.
- You can check if your object supports a feature without checking its instance.
## Cons
- It introduces additional complexity.
- A host typeclass instance is required.
## How to install
To enable component support for a typeclass,
import and inherit the ComponentHolderMixin, similar to this
```python
from evennia.contrib.base_systems.components import ComponentHolderMixin
class Character(ComponentHolderMixin, DefaultCharacter):
# ...
```
Components need to inherit the Component class and require a unique name.
Components may inherit from other components but must specify another name.
You can assign the same 'slot' to both components to have alternative implementations.
```python
from evennia.contrib.base_systems.components import Component
class Health(Component):
name = "health"
class ItemHealth(Health):
name = "item_health"
slot = "health"
```
Components may define DBFields or NDBFields at the class level.
DBField will store its values in the host's DB with a prefixed key.
NDBField will store its values in the host's NDB and will not persist.
The key used will be 'component_name::field_name'.
They use AttributeProperty under the hood.
Example:
```python
from evennia.contrib.base_systems.components import Component, DBField
class Health(Component):
health = DBField(default=1)
```
Note that default is optional and will default to None.
Adding a component to a host will also a similarly named tag with 'components' as category.
A Component named health will appear as key="health, category="components".
This allows you to retrieve objects with specific components by searching with the tag.
It is also possible to add Component Tags the same way, using TagField.
TagField accepts a default value and can be used to store a single or multiple tags.
Default values are automatically added when the component is added.
Component Tags are cleared from the host if the component is removed.
Example:
```python
from evennia.contrib.base_systems.components import Component, TagField
class Health(Component):
resistances = TagField()
vulnerability = TagField(default="fire", enforce_single=True)
```
The 'resistances' field in this example can be set to multiple times and it will keep the added tags.
The 'vulnerability' field in this example will override the previous tag with the new one.
Each typeclass using the ComponentHolderMixin can declare its components
in the class via the ComponentProperty.
These are components that will always be present in a typeclass.
You can also pass kwargs to override the default values
Example
```python
from evennia.contrib.base_systems.components import ComponentHolderMixin
class Character(ComponentHolderMixin, DefaultCharacter):
health = ComponentProperty("health", hp=10, max_hp=50)
```
You can then use character.components.health to access it.
The shorter form character.cmp.health also exists.
character.health would also be accessible but only for typeclasses that have
this component defined on the class.
Alternatively you can add those components at runtime.
You will have to access those via the component handler.
Example
```python
character = self
vampirism = components.Vampirism.create(character)
character.components.add(vampirism)
...
vampirism = character.components.get("vampirism")
# Alternatively
vampirism = character.cmp.vampirism
```
Keep in mind that all components must be imported to be visible in the listing.
As such, I recommend regrouping them in a package.
You can then import all your components in that package's __init__
Because of how Evennia import typeclasses and the behavior of python imports
I recommend placing the components package inside the typeclass package.
In other words, create a folder named components inside your typeclass folder.
Then, inside the 'typeclasses/__init__.py' file add the import to the folder, like
```python
from typeclasses import components
```
This ensures that the components package will be imported when the typeclasses are imported.
You will also need to import each components inside the package's own 'typeclasses/components/__init__.py' file.
You only need to import each module/file from there but importing the right class is a good practice.
```python
from typeclasses.components.health import Health
```
```python
from typeclasses.components import health
```
Both of the above examples will work.
## Known Issues
Assigning mutable default values such as a list to a DBField will share it across instances.
To avoid this, you must set autocreate=True on the field, like this.
```python
health = DBField(default=[], autocreate=True)
```
## Full Example
```python
from evennia.contrib.base_systems import components
# This is the Component class
class Health(components.Component):
name = "health"
# Stores the current and max values as Attributes on the host, defaulting to 100
current = components.DBField(default=100)
max = components.DBField(default=100)
def damage(self, value):
if self.current <= 0:
return
self.current -= value
if self.current > 0:
return
self.current = 0
self.on_death()
def heal(self, value):
hp = self.current
hp += value
if hp >= self.max_hp:
hp = self.max_hp
self.current = hp
@property
def is_dead(self):
return self.current <= 0
def on_death(self):
# Behavior is defined on the typeclass
self.host.on_death()
# This is how the Character inherits the mixin and registers the component 'health'
class Character(ComponentHolderMixin, DefaultCharacter):
health = ComponentProperty("health")
# This is an example of a command that checks for the component
class Attack(Command):
key = "attack"
aliases = ('melee', 'hit')
def at_pre_cmd(self):
caller = self.caller
targets = self.caller.search(args, quiet=True)
valid_target = None
for target in targets:
# Attempt to retrieve the component, None is obtained if it does not exist.
if target.components.health:
valid_target = target
if not valid_target:
caller.msg("You can't attack that!")
return True
```
----
<small>This document page is generated from `evennia/contrib/base_systems/components/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,60 @@
# Containers
Contribution by InspectorCaracal (2023)
Adds the ability to put objects into other container objects by providing a container typeclass and extending certain base commands.
## Installation
To install, import and add the `ContainerCmdSet` to `CharacterCmdSet` in your `default_cmdsets.py` file:
```python
from evennia.contrib.game_systems.containers import ContainerCmdSet
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# ...
def at_cmdset_creation(self):
# ...
self.add(ContainerCmdSet)
```
This will replace the default `look` and `get` commands with the container-friendly versions provided by the contrib as well as add a new `put` command.
## Usage
The contrib includes a `ContribContainer` typeclass which has all of the set-up necessary to be used as a container. To use, all you need to do is create an object in-game with that typeclass - it will automatically inherit anything you implemented in your base Object typeclass as well.
create bag:game_systems.containers.ContribContainer
The contrib's `ContribContainer` comes with a capacity limit of a maximum number of items it can hold. This can be changed per individual object.
In code:
```py
obj.capacity = 5
```
In game:
set box/capacity = 5
You can also make any other objects usable as containers by setting the `get_from` lock type on it.
lock mysterious box = get_from:true()
## Extending
The `ContribContainer` class is intended to be usable as-is, but you can also inherit from it for your own container classes to extend its functionality. Aside from having the container lock pre-set on object creation, it comes with three main additions:
### `capacity` property
`ContribContainer.capacity` is an `AttributeProperty` - meaning you can access it in code with `obj.capacity` and also set it in game with `set obj/capacity = 5` - which represents the capacity of the container as an integer. You can override this with a more complex representation of capacity on your own container classes.
### `at_pre_get_from` and `at_pre_put_in` methods
These two methods on `ContribContainer` are called as extra checks when attempting to either get an object from, or put an object in, a container. The contrib's `ContribContainer.at_pre_get_from` doesn't do any additional validation by default, while `ContribContainer.at_pre_put_in` does a simple capacity check.
You can override these methods on your own child class to do any additional capacity or access checks.
----
<small>This document page is generated from `evennia/contrib/game_systems/containers/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,62 @@
# Cooldowns
Contribution by owllex, 2021
Cooldowns are used to model rate-limited actions, like how often a
character can perform a given action; until a certain time has passed their
command can not be used again. This contrib provides a simple cooldown
handler that can be attached to any typeclass. A cooldown is a lightweight persistent
asynchronous timer that you can query to see if a certain time has yet passed.
Cooldowns are completely asynchronous and must be queried to know their
state. They do not fire callbacks, so are not a good fit for use cases
where something needs to happen on a specific schedule (use delay or
a TickerHandler for that instead).
See also the evennia [howto](../Howtos/Howto-Command-Cooldown.md) for more information
about the concept.
## Installation
To use, simply add the following property to the typeclass definition of any
object type that you want to support cooldowns. It will expose a new `cooldowns`
property that persists data to the object's attribute storage. You can set this
on your base `Object` typeclass to enable cooldown tracking on every kind of
object, or just put it on your `Character` typeclass.
By default the CooldownHandler will use the `cooldowns` property, but you can
customize this if desired by passing a different value for the `db_attribute`
parameter.
```python
from evennia.contrib.game_systems.cooldowns import CooldownHandler
from evennia.utils.utils import lazy_property
@lazy_property
def cooldowns(self):
return CooldownHandler(self, db_attribute="cooldowns")
```
## Example
Assuming you've installed cooldowns on your Character typeclasses, you can use a
cooldown to limit how often you can perform a command. The following code
snippet will limit the use of a Power Attack command to once every 10 seconds
per character.
```python
class PowerAttack(Command):
def func(self):
if self.caller.cooldowns.ready("power attack"):
self.do_power_attack()
self.caller.cooldowns.add("power attack", 10)
else:
self.caller.msg("That's not ready yet!")
```
----
<small>This document page is generated from `evennia/contrib/game_systems/cooldowns/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,306 @@
# Crafting system
Contribution by Griatch 2020
This implements a full crafting system. The principle is that of a 'recipe',
where you combine items (tagged as ingredients) create something new. The recipe can also
require certain (non-consumed) tools. An example would be to use the 'bread recipe' to
combine 'flour', 'water' and 'yeast' with an 'oven' to bake a 'loaf of bread'.
The recipe process can be understood like this:
ingredient(s) + tool(s) + recipe -> object(s)
Here, 'ingredients' are consumed by the crafting process, whereas 'tools' are
necessary for the process but will not be destroyed by it.
The included `craft` command works like this:
craft <recipe> [from <ingredient>,...] [using <tool>, ...]
## Examples
Using the `craft` command:
craft toy car from plank, wooden wheels, nails using saw, hammer
A recipe does not have to use tools or even multiple ingredients:
snow + snowball_recipe -> snowball
Conversely one could also imagine using tools without consumables, like
spell_book + wand + fireball_recipe -> fireball
The system is generic enough to be used also for adventure-like puzzles (but
one would need to change the command and determine the recipe on based on what
is being combined instead):
stick + string + hook -> makeshift_fishing_rod
makeshift_fishing_rod + storm_drain -> key
See the [sword example](evennia.contrib.game_systems.crafting.example_recipes) for an example
of how to design a recipe tree for crafting a sword from base elements.
## Installation and Usage
Import the `CmdCraft` command from evennia/contrib/crafting/crafting.py and
add it to your Character cmdset. Reload and the `craft` command will be
available to you:
craft <recipe> [from <ingredient>,...] [using <tool>, ...]
In code, you can craft using the
`evennia.contrib.game_systems.crafting.craft` function:
```python
from evennia.contrib.game_systems.crafting import craft
result = craft(caller, "recipename", *inputs)
```
Here, `caller` is the one doing the crafting and `*inputs` is any combination of
consumables and/or tool Objects. The system will identify which is which by the
[Tags](../Components/Tags.md) on them (see below) The `result` is always a list.
To use crafting you need recipes. Add a new variable to
`mygame/server/conf/settings.py`:
CRAFT_RECIPE_MODULES = ['world.recipes']
All top-level classes in these modules (whose name does not start with `_`) will
be parsed by Evennia as recipes to make available to the crafting system. Using
the above example, create `mygame/world/recipes.py` and add your recipies in
there:
A quick example (read on for more details):
```python
from evennia.contrib.game_systems.crafting import CraftingRecipe, CraftingValidationError
class RecipeBread(CraftingRecipe):
"""
Bread is good for making sandwitches!
"""
name = "bread" # used to identify this recipe in 'craft' command
tool_tags = ["bowl", "oven"]
consumable_tags = ["flour", "salt", "yeast", "water"]
output_prototypes = [
{"key": "Loaf of Bread",
"aliases": ["bread"],
"desc": "A nice load of bread.",
"typeclass": "typeclasses.objects.Food", # assuming this exists
"tags": [("bread", "crafting_material")] # this makes it usable in other recipes ...
}
]
def pre_craft(self, **kwargs):
# validates inputs etc. Raise `CraftingValidationError` if fails
def do_craft(self, **kwargs):
# performs the craft - report errors directly to user and return None (if
# failed) and the created object(s) if successful.
def post_craft(self, result, **kwargs):
# any post-crafting effects. Always called, even if do_craft failed (the
# result would be None then)
```
## Adding new recipes
A *recipe* is a class inheriting from
`evennia.contrib.game_systems.crafting.CraftingRecipe`. This class implements the
most common form of crafting - that using in-game objects. Each recipe is a
separate class which gets initialized with the consumables/tools you provide.
For the `craft` command to find your custom recipes, you need to tell Evennia
where they are. Add a new line to your `mygame/server/conf/settings.py` file,
with a list to any new modules with recipe classes.
```python
CRAFT_RECIPE_MODULES = ["world.myrecipes"]
```
(You need to reload after adding this). All global-level classes in these
modules (whose names don't start with underscore) are considered by the system
as viable recipes.
Here we assume you created `mygame/world/myrecipes.py` to match the above
example setting:
```python
# in mygame/world/myrecipes.py
from evennia.contrib.game_systems.crafting import CraftingRecipe
class WoodenPuppetRecipe(CraftingRecipe):
"""A puppet""""
name = "wooden puppet" # name to refer to this recipe as
tool_tags = ["knife"]
consumable_tags = ["wood"]
output_prototypes = [
{"key": "A carved wooden doll",
"typeclass": "typeclasses.objects.decorations.Toys",
"desc": "A small carved doll"}
]
```
This specifies which tags to look for in the inputs. It defines a
[Prototype](../Components/Prototypes.md) for the recipe to use to spawn the
result on the fly (a recipe could spawn more than one result if needed).
Instead of specifying the full prototype-dict, you could also just provide a
list of `prototype_key`s to existing prototypes you have.
After reloading the server, this recipe would now be available to use. To try it
we should create materials and tools to insert into the recipe.
The recipe analyzes inputs, looking for [Tags](../Components/Tags.md) with
specific tag-categories. The tag-category used can be set per-recipe using the
(`.consumable_tag_category` and `.tool_tag_category` respectively). The defaults
are `crafting_material` and `crafting_tool`. For
the puppet we need one object with the `wood` tag and another with the `knife`
tag:
```python
from evennia import create_object
knife = create_object(key="Hobby knife", tags=[("knife", "crafting_tool")])
wood = create_object(key="Piece of wood", tags[("wood", "crafting_material")])
```
Note that the objects can have any name, all that matters is the
tag/tag-category. This means if a "bayonet" also had the "knife" crafting tag,
it could also be used to carve a puppet. This is also potentially interesting
for use in puzzles and to allow users to experiment and find alternatives to
know ingredients.
By the way, there is also a simple shortcut for doing this:
```
tools, consumables = WoodenPuppetRecipe.seed()
```
The `seed` class-method will create simple dummy objects that fulfills the
recipe's requirements. This is great for testing.
Assuming these objects were put in our inventory, we could now craft using the
in-game command:
```bash
> craft wooden puppet from wood using hobby knife
```
In code we would do
```python
from evennia.contrib.game_systems.crafting import craft
puppet = craft(crafter, "wooden puppet", knife, wood)
```
In the call to `craft`, the order of `knife` and `wood` doesn't matter - the
recipe will sort out which is which based on their tags.
## Deeper customization of recipes
For customizing recipes further, it helps to understand how to use the
recipe-class directly:
```python
class MyRecipe(CraftingRecipe):
# ...
tools, consumables = MyRecipe.seed()
recipe = MyRecipe(crafter, *(tools + consumables))
result = recipe.craft()
```
This is useful for testing and allows you to use the class directly without
adding it to a module in `settings.CRAFTING_RECIPE_MODULES`.
Even without modifying more than the class properties, there are a lot of
options to set on the `CraftingRecipe` class. Easiest is to refer to the
[CraftingRecipe api
documentation](evennia.contrib.game_systems.crafting.crafting.CraftingRecipe). For example,
you can customize the validation-error messages, decide if the ingredients have
to be exactly right, if a failure still consumes the ingredients or not, and
much more.
For even more control you can override hooks in your own class:
- `pre_craft` - this should handle input validation and store its data in `.validated_consumables` and
`validated_tools` respectively. On error, this reports the error to the crafter and raises the
`CraftingValidationError`.
- `craft` - this will only be called if `pre_craft` finished without an exception. This should
return the result of the crafting, by spawnging the prototypes. Or the empty list if crafting
fails for some reason. This is the place to add skill-checks or random chance if you need it
for your game.
- `post_craft` - this receives the result from `craft` and handles error messages and also deletes
any consumables as needed. It may also modify the result before returning it.
- `msg` - this is a wrapper for `self.crafter.msg` and should be used to send messages to the
crafter. Centralizing this means you can also easily modify the sending style in one place later.
The class constructor (and the `craft` access function) takes optional `**kwargs`. These are passed
into each crafting hook. These are unused by default but could be used to customize things per-call.
### Skilled crafters
What the crafting system does not have out of the box is a 'skill' system - the
notion of being able to fail the craft if you are not skilled enough. Just how
skills work is game-dependent, so to add this you need to make your own recipe
parent class and have your recipes inherit from this.
```python
from random import randint
from evennia.contrib.game_systems.crafting import CraftingRecipe
class SkillRecipe(CraftingRecipe):
"""A recipe that considers skill"""
difficulty = 20
def craft(self, **kwargs):
"""The input is ok. Determine if crafting succeeds"""
# this is set at initialization
crafter = self.crafte
# let's assume the skill is stored directly on the crafter
# - the skill is 0..100.
crafting_skill = crafter.db.skill_crafting
# roll for success:
if randint(1, 100) <= (crafting_skill - self.difficulty):
# all is good, craft away
return super().craft()
else:
self.msg("You are not good enough to craft this. Better luck next time!")
return []
```
In this example we introduce a `.difficulty` for the recipe and makes a 'dice roll' to see
if we succed. We would of course make this a lot more immersive and detailed in a full game. In
principle you could customize each recipe just the way you want it, but you could also inherit from
a central parent like this to cut down on work.
The [sword recipe example module](evennia.contrib.game_systems.crafting.example_recipes) also shows an example
of a random skill-check being implemented in a parent and then inherited for multiple use.
## Even more customization
If you want to build something even more custom (maybe using different input types of validation logic)
you could also look at the `CraftingRecipe` parent class `CraftingRecipeBase`.
It implements just the minimum needed to be a recipe and for big changes you may be better off starting
from this rather than the more opinionated `CraftingRecipe`.
----
<small>This document page is generated from `evennia/contrib/game_systems/crafting/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,54 @@
# Custom gameime
Contrib by vlgeoff, 2017 - based on Griatch's core original
This reimplements the `evennia.utils.gametime` module but with a _custom_
calendar (unusual number of days per week/month/year etc) for your game world.
Like the original, it allows for scheduling events to happen at given
in-game times, but now taking this custom calendar into account.
## Installation
Import and use this in the same way as you would the normal
`evennia.utils.gametime` module.
Customize the calendar by adding a `TIME_UNITS` dict to your settings (see
example below).
## Usage:
```python
from evennia.contrib.base_systems import custom_gametime
gametime = custom_gametime.realtime_to_gametime(days=23)
# scedule an event to fire every in-game 10 hours
custom_gametime.schedule(callback, repeat=True, hour=10)
```
The calendar can be customized by adding the `TIME_UNITS` dictionary to your
settings file. This maps unit names to their length, expressed in the smallest
unit. Here's the default as an example:
TIME_UNITS = {
"sec": 1,
"min": 60,
"hr": 60 * 60,
"hour": 60 * 60,
"day": 60 * 60 * 24,
"week": 60 * 60 * 24 * 7,
"month": 60 * 60 * 24 * 7 * 4,
"yr": 60 * 60 * 24 * 7 * 4 * 12,
"year": 60 * 60 * 24 * 7 * 4 * 12, }
When using a custom calendar, these time unit names are used as kwargs to
the converter functions in this module. Even if your calendar uses other names
for months/weeks etc the system needs the default names internally.
----
<small>This document page is generated from `evennia/contrib/base_systems/custom_gametime/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,121 @@
# DebugPy VSCode debugger integration
Contribution by electroglyph, 2025
This registers an in-game command `debugpy` which starts the debugpy debugger and listens on port 5678.
For now this is only available for Visual Studio Code (VS Code).
If you are a JetBrains PyCharm user and would like to use this, make some noise at:
https://youtrack.jetbrains.com/issue/PY-63403/Support-debugpy
Credit for this goes to Moony on the Evennia Discord getting-help channel, thx Moony!
## Installation
This requires VS Code and debugpy, so make sure you're using VS Code.
From the venv where you installed Evennia run:
`pip install debugpy`
### Enable the command in Evennia
In your Evennia mygame folder, open up `/commands/default_cmdsets.py`
add `from evennia.contrib.utils.debugpy import CmdDebugPy` somewhere near the top.
in `CharacterCmdSet.at_cmdset_creation` add this under `super().at_cmdset_creation()`:
`self.add(CmdDebugPy)`
### Add "remote attach" option to VS Code debugger
Start VS Code and open your launch.json like this:
![screenshot](./vscode.png)
Add this to your configuration:
```json
{
"name": "Python Debugger: Remote Attach",
"justMyCode": false,
"type": "debugpy",
"request": "attach",
"connect": {
"host": "127.0.0.1",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "${workspaceFolder}"
}
]
},
```
Use `127.0.0.1` for the host if you are running Evennia from the same machine you'll be debugging from. Otherwise, if you want to debug a remote server, change host (and possibly remoteRoot mapping) as necessary.
Afterwards it should look something like this:
```json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
},
{
"name": "Python Debugger: Remote Attach",
"justMyCode": false,
"type": "debugpy",
"request": "attach",
"connect": {
"host": "127.0.0.1",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "${workspaceFolder}"
}
]
},
]
}
```
(notice the comma between the curly braces)
## Usage
Set a breakpoint in VS Code where you want the debugger to stop at.
In Evennia run `debugpy` command.
You should see "Waiting for debugger attach..."
Back in VS Code attach the debugger:
![screenshot](./attach.png)
Back in Evennia you should see "Debugger attached."
Now trigger the breakpoint you set and you'll be using a nice graphical debugger.
----
<small>This document page is generated from `evennia/contrib/utils/debugpy/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,148 @@
# Dice roller
Contribution by Griatch, 2012, 2023
A dice roller for any number and side of dice. Adds in-game dice rolling
(like `roll 2d10 + 1`) as well as conditionals (roll under/over/equal to a target)
and functions for rolling dice in code. Command also supports hidden or secret
rolls for use by a human game master.
## Installation:
Add the `CmdDice` command from this module to your character's cmdset
(and then restart the server):
```python
# in mygame/commands/default_cmdsets.py
# ...
from evennia.contrib.rpg import dice <---
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# ...
def at_cmdset_creation(self):
# ...
self.add(dice.CmdDice()) # <---
```
## Usage:
> roll 1d100 + 2
> roll 1d20
> roll 1d20 - 4
The result of the roll will be echoed to the room.
One can also specify a standard Python operator in order to specify
eventual target numbers and get results in a fair and guaranteed
unbiased way. For example:
> roll 2d6 + 2 < 8
Rolling this will inform all parties if roll was indeed below 8 or not.
> roll/hidden 1d100
Informs the room that the roll is being made without telling what the result
was.
> roll/secret 1d20
This a hidden roll that does not inform the room it happened.
## Rolling dice from code
You can specify the first argument as a string on standard RPG d-syntax (NdM,
where N is the number of dice to roll, and M is the number sides per dice):
```python
from evennia.contrib.rpg.dice import roll
roll("3d10 + 2")
```
You can also give a conditional (you'll then get a `True`/`False` back):
```python
roll("2d6 - 1 >= 10")
```
If you specify the first argument as an integer, it's interpret as the number of
dice to roll and you can then build the roll more explicitly. This can be
useful if you are using the roller together with some other system and want to
construct the roll from components.
```python
roll(dice, dicetype=6, modifier=None, conditional=None, return_tuple=False,
max_dicenum=10, max_dicetype=1000)
```
Here's how to roll `3d10 + 2` with explicit syntax:
```python
roll(3, 10, modifier=("+", 2))
```
Here's how to roll `2d6 - 1 >= 10` (you'll get back `True`/`False` back):
```python
roll(2, 6, modifier=("-", 1), conditional=(">=", 10))
```
### Dice pools and other variations
You can only roll one set of dice at a time. If your RPG requires you to roll multiple
sets of dice and combine them in more advanced ways, you can do so with multiple
`roll()` calls. Depending on what you need, you may just want to express this as
helper functions specific for your game.
Here's how to roll a D&D advantage roll (roll d20 twice, pick highest):
```python
from evennia.contrib.rpg.dice import roll
def roll_d20_with_advantage():
"""Get biggest result of two d20 rolls"""
return max(roll("d20"), roll("d20"))
```
Here's an example of a Free-League style dice pool, where you roll a pile of d6
and want to know how many 1s and sixes you get:
```python
from evennia.contrib.rpg.dice import roll
def roll_dice_pool(poolsize):
"""Return (number_of_ones, number_of_sixes)"""
results = [roll("1d6") for _ in range(poolsize)]
return results.count(1), results.count(6)
```
### Get all roll details
If you need the individual rolls (e.g. for a dice pool), set the `return_tuple` kwarg:
```python
roll("3d10 > 10", return_tuple=True)
(13, True, 3, (3, 4, 6)) # (result, outcome, diff, rolls)
```
The return is a tuple `(result, outcome, diff, rolls)`, where `result` is the
result of the roll, `outcome` is `True/False` if a conditional was
given (`None` otherwise), `diff` is the absolute difference between the
conditional and the result (`None` otherwise) and `rolls` is a tuple containing
the individual roll results.
----
<small>This document page is generated from `evennia/contrib/rpg/dice/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,37 @@
# Email-based login system
Contrib by Griatch, 2012
This is a variant of the login system that asks for an email-address
instead of a username to login. Note that it does not verify the email,
it just uses it as the identifier rather than a username.
This used to be the default Evennia login before replacing it with a
more standard username + password system (having to supply an email
for some reason caused a lot of confusion when people wanted to expand
on it. The email is not strictly needed internally, nor is any
confirmation email sent out anyway).
## Installation
To your settings file, add/edit the line:
```python
CMDSET_UNLOGGEDIN = "contrib.base_systems.email_login.UnloggedinCmdSet"
CONNECTION_SCREEN_MODULE = "contrib.base_systems.email_login.connection_screens"
```
That's it. Reload the server and reconnect to see it.
## Notes:
If you want to modify the way the connection screen looks, point
`CONNECTION_SCREEN_MODULE` to your own module. Use the default as a
guide (see also Evennia docs).
----
<small>This document page is generated from `evennia/contrib/base_systems/email_login/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,42 @@
# EvAdventure
Contrib by Griatch 2023-
```{warning}
NOTE - this tutorial is WIP and NOT complete yet! You will still learn
things from it, but don't expect perfection.
```
A complete example MUD using Evennia. This is the final result of what is
implemented if you follow [Part 3 of the Getting-Started tutorial](../Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Part3-Overview.md).
It's recommended that you follow the tutorial step by step and write your own
code. But if you prefer you can also pick apart or use this as a starting point
for your own game.
## Features
- Uses a MUD-version of the [Knave](https://rpggeek.com/rpg/50827/knave) old-school
fantasy ruleset by Ben Milton (classless and overall compatible with early
edition D&D), released under the Creative Commons Attribution (all uses,
including commercial are allowed
as long as attribution is given).
- Character creation using an editable character sheet
- Weapons, effects, healing and resting
- Two alternative combat systems (turn-based and twitch based)
- Magic (three spells)
- NPC/mobs with simple AI.
- Simple Quest system.
- Small game world.
- Coded using best Evennia practices, with unit tests.
## Installation
TODO
----
<small>This document page is generated from `evennia/contrib/tutorials/evadventure/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

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

View file

@ -0,0 +1,196 @@
# Extended Room
Contribution - Griatch 2012, vincent-lg 2019, Griatch 2023
This extends the normal `Room` typeclass to allow its description to change with
time-of-day and/or season as well as any other state (like flooded or dark).
Embedding `$state(burning, This place is on fire!)` in the description will
allow for changing the description based on room state. The room also supports
`details` for the player to look at in the room (without having to create a new
in-game object for each), as well as support for random echoes. The room
comes with a set of alternate commands for `look` and `@desc`, as well as new
commands `detail`, `roomstate` and `time`.
## Installation
Add the `ExtendedRoomCmdset` to the default character cmdset will add all
new commands for use.
In more detail, in `mygame/commands/default_cmdsets.py`:
```python
...
from evennia.contrib.grid import extended_room # <---
class CharacterCmdset(default_cmds.CharacterCmdSet):
...
def at_cmdset_creation(self):
super().at_cmdset_creation()
...
self.add(extended_room.ExtendedRoomCmdSet) # <---
```
Then reload to make the new commands available. Note that they only work
on rooms with the typeclass `ExtendedRoom`. Create new rooms with the right
typeclass or use the `typeclass` command to swap existing rooms. Note that since
this contrib overrides the `look` and `@desc` commands, you will need to add the
`extended_room.ExtendedRoomCmdSet` to the default character cmdset *after*
`super().at_cmdset_creation()`, or they will be overridden by the default look.
To dig a new extended room:
dig myroom:evennia.contrib.grid.extended_room.ExtendedRoom = north,south
To make all new rooms ExtendedRooms without having to specify it, make your
`Room` typeclass inherit from the `ExtendedRoom` and then reload:
```python
# in mygame/typeclasses/rooms.py
from evennia.contrib.grid.extended_room import ExtendedRoom
# ...
class Room(ObjectParent, ExtendedRoom):
# ...
```
## Features
### State-dependent description slots
By default, the normal `room.db.desc` description is used. You can however
add new state-ful descriptions with `room.add_desc(description,
room_state=roomstate)` or with the in-game command
```
@desc/roomstate [<description>]
```
For example
```
@desc/dark This room is pitch black.`.
```
These will be stored in Attributes `desc_<roomstate>`. To set the default,
fallback description, just use `@desc <description>`.
To activate a state on the room, use `room.add/remove_state(*roomstate)` or the in-game
command
```
roomstate <state> (use it again to toggle the state off)
```
For example
```
roomstate dark
```
There is one in-built, time-based state `season`. By default these are 'spring',
'summer', 'autumn' and 'winter'. The `room.get_season()` method returns the
current season based on the in-game time. By default they change with a 12-month
in-game time schedule. You can control them with
```
ExtendedRoom.months_per_year # default 12
ExtendedRoom.seasons_per year # a dict of {"season": (start, end), ...} where
# start/end are given in fractions of the whole year
```
To set a seasonal description, just set it as normal, with `room.add_desc` or
in-game with
```
@desc/winter This room is filled with snow.
@desc/autumn Red and yellow leaves cover the ground.
```
Normally the season changes with the in-game time, you can also 'force' a given
season by setting its state
```
roomstate winter
```
If you set the season manually like this, it won't change automatically again
until you unset it.
You can get the stateful description from the room with `room.get_stateful_desc()`.
### Changing parts of description based on state
All descriptions can have embedded `$state(roomstate, description)`
[FuncParser tags](../Components/FuncParser.md) embedded in them. Here is an example:
```py
room.add_desc("This a nice beach. "
"$state(empty, It is completely empty)"
"$state(full, It is full of people).", room_state="summer")
```
This is a summer-description with special embedded strings. If you set the room
with
> room.add_room_state("summer", "empty")
> room.get_stateful_desc()
This is a nice beach. It is completely empty.
> room.remove_room_state("empty")
> room.add_room_state("full")
> room.get_stateful_desc()
This is a nice beach. It is full of people.
There are four default time-of-day states that are meant to be used with these tags. The
room tracks and changes these automatically. By default they are 'morning',
'afternoon', 'evening' and 'night'. You can get the current time-slot with
`room.get_time_of_day`. You can control them with
```
ExtendedRoom.hours_per_day # default 24
ExtendedRoom.times_of_day # dict of {season: (start, end), ...} where
# the start/end are given as fractions of the day.
```
You use these inside descriptions as normal:
"A glade. $(morning, The morning sun shines down through the branches)."
### Details
_Details_ are "virtual" targets to look at in a room, without having to create a
new database instance for every thing. It's good to add more information to a
location. The details are stored as strings in a dictionary.
detail window = There is a window leading out.
detail rock = The rock has a text written on it: 'Do not dare lift me'.
When you are in the room you can then do `look window` or `look rock` and get
the matching detail-description. This requires the new custom `look` command.
### Random echoes
The `ExtendedRoom` supports random echoes. Just set them as an Attribute list
`room_messages`:
```
room.room_message_rate = 120 # in seconds. 0 to disable
room.db.room_messages = ["A car passes by.", "You hear the sound of car horns."]
room.start_repeat_broadcast_messages() # also a server reload works
```
These will start randomly echoing to the room every 120s.
### Extra commands
- `CmdExtendedRoomLook` (`look`) - look command supporting room details
- `CmdExtendedRoomDesc` (`@desc`) - desc command allowing to add stateful descs,
- `CmdExtendeRoomState` (`roomstate`) - toggle room states
- `CmdExtendedRoomDetail` (`detail`) - list and manipulate room details
- `CmdExtendedRoomGameTime` (`time`) - Shows the current time and season in the room.
----
<small>This document page is generated from `evennia/contrib/grid/extended_room/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,165 @@
# Easy fillable form
Contribution by Tim Ashley Jenkins, 2018
This module contains a function that generates an `EvMenu` for you - this
menu presents the player with a form of fields that can be filled
out in any order (e.g. for character generation or building). Each field's value can
be verified, with the function allowing easy checks for text and integer input,
minimum and maximum values / character lengths, or can even be verified by a custom
function. Once the form is submitted, the form's data is submitted as a dictionary
to any callable of your choice.
## Usage
The function that initializes the fillable form menu is fairly simple, and
includes the caller, the template for the form, and the callback(caller, result)
to which the form data will be sent to upon submission.
init_fill_field(formtemplate, caller, formcallback)
Form templates are defined as a list of dictionaries - each dictionary
represents a field in the form, and contains the data for the field's name and
behavior. For example, this basic form template will allow a player to fill out
a brief character profile:
PROFILE_TEMPLATE = [
{"fieldname":"Name", "fieldtype":"text"},
{"fieldname":"Age", "fieldtype":"number"},
{"fieldname":"History", "fieldtype":"text"},
]
This will present the player with an EvMenu showing this basic form:
```
Name:
Age:
History:
```
While in this menu, the player can assign a new value to any field with the
syntax <field> = <new value>, like so:
```
> name = Ashley
Field 'Name' set to: Ashley
```
Typing 'look' by itself will show the form and its current values.
```
> look
Name: Ashley
Age:
History:
```
Number fields require an integer input, and will reject any text that can't
be converted into an integer.
```
> age = youthful
Field 'Age' requires a number.
> age = 31
Field 'Age' set to: 31
```
Form data is presented as an EvTable, so text of any length will wrap cleanly.
```
> history = EVERY MORNING I WAKE UP AND OPEN PALM SLAM[...]
Field 'History' set to: EVERY MORNING I WAKE UP AND[...]
> look
Name: Ashley
Age: 31
History: EVERY MORNING I WAKE UP AND OPEN PALM SLAM A VHS INTO THE SLOT.
IT'S CHRONICLES OF RIDDICK AND RIGHT THEN AND THERE I START DOING
THE MOVES ALONGSIDE WITH THE MAIN CHARACTER, RIDDICK. I DO EVERY
MOVE AND I DO EVERY MOVE HARD.
```
When the player types 'submit' (or your specified submit command), the menu
quits and the form's data is passed to your specified function as a dictionary,
like so:
formdata = {"Name":"Ashley", "Age":31, "History":"EVERY MORNING I[...]"}
You can do whatever you like with this data in your function - forms can be used
to set data on a character, to help builders create objects, or for players to
craft items or perform other complicated actions with many variables involved.
The data that your form will accept can also be specified in your form template -
let's say, for example, that you won't accept ages under 18 or over 100. You can
do this by specifying "min" and "max" values in your field's dictionary:
```
PROFILE_TEMPLATE = [
{"fieldname":"Name", "fieldtype":"text"},
{"fieldname":"Age", "fieldtype":"number", "min":18, "max":100},
{"fieldname":"History", "fieldtype":"text"}
]
```
Now if the player tries to enter a value out of range, the form will not acept the
given value.
```
> age = 10
Field 'Age' reqiures a minimum value of 18.
> age = 900
Field 'Age' has a maximum value of 100.
```
Setting 'min' and 'max' for a text field will instead act as a minimum or
maximum character length for the player's input.
There are lots of ways to present the form to the player - fields can have default
values or show a custom message in place of a blank value, and player input can be
verified by a custom function, allowing for a great deal of flexibility. There
is also an option for 'bool' fields, which accept only a True / False input and
can be customized to represent the choice to the player however you like (E.G.
Yes/No, On/Off, Enabled/Disabled, etc.)
This module contains a simple example form that demonstrates all of the included
functionality - a command that allows a player to compose a message to another
online character and have it send after a custom delay. You can test it by
importing this module in your game's `default_cmdsets.py` module and adding
CmdTestMenu to your default character's command set.
## FIELD TEMPLATE KEYS:
### Required:
```
fieldname (str): Name of the field, as presented to the player.
fieldtype (str): Type of value required: 'text', 'number', or 'bool'.
```
### Optional:
- max (int): Maximum character length (if text) or value (if number).
- min (int): Minimum charater length (if text) or value (if number).
- truestr (str): String for a 'True' value in a bool field.
(E.G. 'On', 'Enabled', 'Yes')
- falsestr (str): String for a 'False' value in a bool field.
(E.G. 'Off', 'Disabled', 'No')
- default (str): Initial value (blank if not given).
- blankmsg (str): Message to show in place of value when field is blank.
- cantclear (bool): Field can't be cleared if True.
- required (bool): If True, form cannot be submitted while field is blank.
- verifyfunc (callable): Name of a callable used to verify input - takes
(caller, value) as arguments. If the function returns True,
the player's input is considered valid - if it returns False,
the input is rejected. Any other value returned will act as
the field's new value, replacing the player's input. This
allows for values that aren't strings or integers (such as
object dbrefs). For boolean fields, return '0' or '1' to set
the field to False or True.
----
<small>This document page is generated from `evennia/contrib/utils/fieldfill/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,84 @@
# Gendersub
Contribution by Griatch 2015
This is a simple gender-aware Character class for allowing users to
insert custom markers in their text to indicate gender-aware
messaging. It relies on a modified msg() and is meant as an
inspiration and starting point to how to do stuff like this.
An object can have the following genders:
- male (he/his)
- female (her/hers)
- neutral (it/its)
- ambiguous (they/them/their/theirs)
## Installation
Import and add the `SetGender` command to your default cmdset in
`mygame/commands/default_cmdset.py`:
```python
# mygame/commands/default_cmdsets.py
# ...
from evennia.contrib.game_systems.gendersub import SetGender # <---
# ...
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# ...
def at_cmdset_creation(self):
# ...
self.add(SetGender()) # <---
```
Make your `Character` inherit from `GenderCharacter`.
```python
# mygame/typeclasses/characters.py
# ...
from evennia.contrib.game_systems.gendersub import GenderCharacter # <---
class Character(GenderCharacter): # <---
# ...
```
Reload the server (`evennia reload` or `reload` from inside the game).
## Usage
When in use, messages can contain special tags to indicate pronouns gendered
based on the one being addressed. Capitalization will be retained.
- `|s`, `|S`: Subjective form: he, she, it, He, She, It, They
- `|o`, `|O`: Objective form: him, her, it, Him, Her, It, Them
- `|p`, `|P`: Possessive form: his, her, its, His, Her, Its, Their
- `|a`, `|A`: Absolute Possessive form: his, hers, its, His, Hers, Its, Theirs
For example,
```
char.msg("%s falls on |p face with a thud." % char.key)
"Tom falls on his face with a thud"
```
The default gender is "ambiguous" (they/them/their/theirs).
To use, have DefaultCharacter inherit from this, or change
setting.DEFAULT_CHARACTER to point to this class.
The `gender` command is used to set the gender. It needs to be added to the
default cmdset before it becomes available.
----
<small>This document page is generated from `evennia/contrib/game_systems/gendersub/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,79 @@
# In-game Git Integration
Contribution by helpme (2022)
A module to integrate a stripped-down version of git within the game, allowing developers to view their git status, change branches, and pull updated code of both their local mygame repo and Evennia core. After a successful pull or checkout, the git command will reload the game: Manual restarts may be required to to apply certain changes that would impact persistent scripts etc.
Once the contrib is set up, integrating remote changes is as simple as entering the following into your game:
```
git pull
```
The repositories you want to work with, be it only your local mygame repo, only Evennia core, or both, must be git directories for the command to function. If you are only interested in using this to get upstream Evennia changes, only the Evennia repository needs to be a git repository. [Get started with version control here.](https://www.evennia.com/docs/1.0-dev/Coding/Version-Control.html)
## Dependencies
This package requires the dependency "gitpython", a python library used to
interact with git repositories. To install, it's easiest to install Evennia's
extra requirements:
pip install evennia[extra]
If you installed with `git` you can also do
- `cd` to the root of the Evennia repository.
- `pip install --upgrade -e .[extra]`
## Installation
This utility adds a simple assortment of 'git' commands. Import the module into your commands and add it to your command set to make it available.
Specifically, in `mygame/commands/default_cmdsets.py`:
```python
...
from evennia.contrib.utils.git_integration import GitCmdSet # <---
class CharacterCmdset(default_cmds.Character_CmdSet):
...
def at_cmdset_creation(self):
...
self.add(GitCmdSet) # <---
```
Then `reload` to make the git command available.
## Usage
This utility will only work if the directory you wish to work with is a git directory. If they are not, you will be prompted to initiate your directory as a git repository using the following commands in your terminal:
```
git init
git remote add origin 'link to your repository'
```
By default, the git commands are only available to those with Developer permissions and higher. You can change this by overriding the command and setting its locks from "cmd:pperm(Developer)" to the lock of your choice.
The supported commands are:
* git status: An overview of your git repository, which files have been changed locally, and the commit you're on.
* git branch: What branches are available for you to check out.
* git checkout 'branch': Checkout a branch.
* git pull: Pull the latest code from your current branch.
* All of these commands are also available with 'evennia', to serve the same functionality related to your Evennia directory. So:
* git evennia status
* git evennia branch
* git evennia checkout 'branch'
* git evennia pull: Pull the latest Evennia code.
## Settings Used
The utility uses the existing GAME_DIR and EVENNIA_DIR settings from settings.py. You should not need to alter these if you have a standard directory setup, they ought to exist without any setup required from you.
----
<small>This document page is generated from `evennia/contrib/utils/git_integration/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,200 @@
# Godot Websocket
Contribution by ChrisLR, 2022
This contrib allows you to connect a Godot Client directly to your mud,
and display regular text with color in Godot's RichTextLabel using BBCode.
You can use Godot to provide advanced functionality with proper Evennia support.
## Installation
You need to add the following settings in your `settings.py` and restart evennia.
```python
PORTAL_SERVICES_PLUGIN_MODULES.append('evennia.contrib.base_systems.godotwebsocket.webclient')
GODOT_CLIENT_WEBSOCKET_PORT = 4008
GODOT_CLIENT_WEBSOCKET_CLIENT_INTERFACE = "127.0.0.1"
```
This will make evennia listen on the port 4008 for Godot.
You can change the port and interface as you want.
## Usage
The tl;dr of it is to connect using a Godot Websocket using the port defined above.
It will let you transfer data from Evennia to Godot, allowing you
to get styled text in a RichTextLabel with bbcode enabled or to handle
the extra data given from Evennia as needed.
This section assumes you have basic knowledge on how to use Godot.
You can read the following url for more details on Godot Websockets
and to implement a minimal client or look at the full example at the bottom of this page.
https://docs.godotengine.org/en/stable/tutorials/networking/websocket.html
The rest of this document will be for Godot 4.
Note that some of the code shown here is partially taken from official Godot Documentation
A very basic setup in godot would require
- One RichTextLabel Node to display the Evennia Output, ensure bbcode is enabled on it.
- One Node for your websocket client code with a new Script attached.
- One TextEdit Node to enter commands
- One Button Node to press and send the commands
- Controls for the layout, in this example I have used
Panel
VBoxContainer
RichTextLabel
HBoxContainer
TextEdit
Button
I will not go over how layout works but the documentation for them is easily accessible in the godot docs.
Open up the script for your client code.
We need to define the url leading to your mud, use the same values you have used in your Evennia Settings.
Next we write some basic code to get a connection going.
This will connect when the Scene is ready, poll and print the data when we receive it and close when the scene exits.
```
extends Node
# The URL we will connect to.
var websocket_url = "ws://127.0.0.1:4008"
var socket := WebSocketPeer.new()
func _ready():
if socket.connect_to_url(websocket_url) != OK:
print("Unable to connect.")
set_process(false)
func _process(_delta):
socket.poll()
match socket.get_ready_state():
WebSocketPeer.STATE_OPEN:
while socket.get_available_packet_count():
print(socket.get_packet().get_string_from_ascii())
WebSocketPeer.STATE_CLOSED:
var code = socket.get_close_code()
var reason = socket.get_close_reason()
print("WebSocket closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1])
set_process(false)
func _exit_tree():
socket.close()
```
At this point, you can start your evennia server, run godot and it should print a default reply.
After that you need to properly handle the data sent by evennia.
To do this, we will add a new function to dispatch the messages properly.
Here is an example
```
func _handle_data(data):
print(data) # Print for debugging
var data_array = JSON.parse_string(data)
# The first element can be used to see if its text
if data_array[0] == 'text':
# The second element contains the messages
for msg in data_array[1]: write_to_rtb(msg)
func write_to_rtb(msg):
output_label.append_text(msg)
```
The first element is the type, it will be `text` if it is a message
It can be anything you would provide to the Evennia `msg` function.
The second element will be the data related to the type of message, in this case it is a list of text to display.
Since it is parsed BBCode, we can add that directly to a RichTextLabel by calling its append_text method.
If you want anything better than fancy text in Godot, you will have
to leverage Evennia's OOB to send extra data.
You can [read more on OOB here](https://www.evennia.com/docs/latest/OOB.html#oob).
Now to send data, we connect the Button pressed Signal to a method,
read the label input and send it via the websocket, then clear the label.
```
func _on_button_pressed():
var msg = text_edit.text
var msg_arr = ['text', [msg], {}]
var msg_str = JSON.stringify(msg_arr)
socket.send_text(msg_str)
text_edit.text = ""
```
## Known Issues
- Sending SaverDicts and similar objects straight from Evennia .DB will cause issues,
cast them to dict() or list() before doing so.
## Full Example Script
```
extends Node
# The URL we will connect to.
var websocket_url = "ws://127.0.0.1:4008"
var socket := WebSocketPeer.new()
@onready var output_label = $"../Panel/VBoxContainer/RichTextLabel"
@onready var text_edit = $"../Panel/VBoxContainer/HBoxContainer/TextEdit"
func _ready():
if socket.connect_to_url(websocket_url) != OK:
print("Unable to connect.")
set_process(false)
func _process(_delta):
socket.poll()
match socket.get_ready_state():
WebSocketPeer.STATE_OPEN:
while socket.get_available_packet_count():
var data = socket.get_packet().get_string_from_ascii()
_handle_data(data)
WebSocketPeer.STATE_CLOSED:
var code = socket.get_close_code()
var reason = socket.get_close_reason()
print("WebSocket closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1])
set_process(false)
func _handle_data(data):
print(data) # Print for debugging
var data_array = JSON.parse_string(data)
# The first element can be used to see if its text
if data_array[0] == 'text':
# The second element contains the messages
for msg in data_array[1]: write_to_rtb(msg)
func write_to_rtb(msg):
output_label.append_text(msg)
func _on_button_pressed():
var msg = text_edit.text
var msg_arr = ['text', [msg], {}]
var msg_str = JSON.stringify(msg_arr)
socket.send_text(msg_str)
text_edit.text = ""
func _exit_tree():
socket.close()
```
----
<small>This document page is generated from `evennia/contrib/base_systems/godotwebsocket/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

View file

@ -0,0 +1,42 @@
# Health Bar
Contribution by Tim Ashley Jenkins, 2017
The function provided in this module lets you easily display visual
bars or meters as a colorful bar instead of just a number. A "health bar"
is merely the most obvious use for this, but the bar is highly customizable
and can be used for any sort of appropriate data besides player health.
Today's players may be more used to seeing statistics like health,
stamina, magic, and etc. displayed as bars rather than bare numerical
values, so using this module to present this data this way may make it
more accessible. Keep in mind, however, that players may also be using
a screen reader to connect to your game, which will not be able to
represent the colors of the bar in any way. By default, the values
represented are rendered as text inside the bar which can be read by
screen readers.
## Usage
No installation, just import and use `display_meter` from this
module:
```python
from evennia.contrib.rpg.health_bar import display_meter
# health is 23/100
health_bar = display_meter(23, 100)
caller.msg(prompt=health_bar)
```
The health bar will account for current values above the maximum or
below 0, rendering them as a completely full or empty bar with the
values displayed within.
----
<small>This document page is generated from `evennia/contrib/rpg/health_bar/README.md`. Changes to this
file will be overwritten, so edit that file rather than this one.</small>

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