Fix merge conflicts

This commit is contained in:
Griatch 2022-07-03 11:13:12 +02:00
commit f4c3db1151
52 changed files with 649 additions and 417 deletions

View file

@ -60,22 +60,22 @@ introduction][introduction] to read.
To learn how to get your hands on the code base, the [Getting
started][gettingstarted] page is the way to go. Otherwise you could
browse the [Documentation][wiki] or why not come join the [Evennia
browse the [Documentation][docs] or why not come join the [Evennia
Community forum][group] or join us in our [development chat][chat].
Welcome!
[homepage]: http://www.evennia.com
[gettingstarted]: http://github.com/evennia/evennia/wiki/Getting-Started
[wiki]: https://github.com/evennia/evennia/wiki
[homepage]: https://www.evennia.com
[gettingstarted]: https://www.evennia.com/docs/latest/Getting-Started.html
[docs]: https://www.evennia.com/docs/latest
[screenshot]: https://user-images.githubusercontent.com/294267/30773728-ea45afb6-a076-11e7-8820-49be2168a6b8.png
[logo]: https://github.com/evennia/evennia/blob/master/evennia/web/website/static/website/images/evennia_logo.png
[unittestciimg]: https://github.com/evennia/evennia/workflows/test-suite/badge.svg
[unittestcilink]: https://github.com/evennia/evennia/actions?query=workflow%3Atest-suite
[coverimg]: https://coveralls.io/repos/github/evennia/evennia/badge.svg?branch=master
[coverlink]: https://coveralls.io/github/evennia/evennia?branch=master
[introduction]: https://github.com/evennia/evennia/wiki/Evennia-Introduction
[license]: https://github.com/evennia/evennia/wiki/Licensing
[group]: https://groups.google.com/forum/#!forum/evennia
[chat]: http://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb
[introduction]: https://www.evennia.com/docs/latest/Evennia-Introduction.html
[license]: https://www.evennia.com/docs/latest/Licensing.html
[group]: https://github.com/evennia/evennia/discussions
[chat]: https://discord.gg/AJJpcRUhtF
[wikimudpage]: http://en.wikipedia.org/wiki/MUD

View file

@ -152,8 +152,8 @@ mv-local:
@echo "Documentation built (multiversion + autodocs)."
@echo "To see result, open evennia/docs/build/html/<version>/index.html in a browser."
# note - don't run the following manually, the result will clash with the result
# of the github actions!
# note - don't run deploy/release manually, the result will clash with the
# result of the github actions!
deploy:
make _multiversion-deploy
@echo "Documentation deployed."

View file

@ -17,6 +17,7 @@ git checkout gh-pages
# with the build/ directory available since this is not in git
# remove all but the build dir
# TODO don't delete old branches after 1.0 release; they will get harder and harder to rebuild
ls -Q | grep -v build | xargs rm -Rf
cp -Rf build/html/* .

View file

@ -3,6 +3,7 @@
sphinx==3.2.1
myst-parser==0.15.2
myst-parser[linkify]==0.15.2
Jinja2 < 3.1
# sphinx-multiversion with evennia fixes
git+https://github.com/evennia/sphinx-multiversion.git@evennia-mods#egg=sphinx-multiversion

View file

@ -10,14 +10,15 @@ the return from the function.
from evennia.utils.funcparser import FuncParser
def _power_callable(*args, **kwargs):
"""This will be callable as $square(number, power=<num>) in string"""
"""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, optionally containing `$func(...)` markers:
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).")
@ -71,7 +72,7 @@ You can apply inline function parsing to any string. The
from evennia.utils import funcparser
parser = FuncParser(callables, **default_kwargs)
parsed_string = parser.parser(input_string, raise_errors=False,
parsed_string = parser.parse(input_string, raise_errors=False,
escape=False, strip=False,
return_str=True, **reserved_kwargs)
@ -90,8 +91,12 @@ available to the parser as you parse strings with it. It can either be
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.
The other arguments to the parser:
`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,
@ -102,12 +107,14 @@ The other arguments to the parser:
- `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 `**default/reserved_keywords` are optional and allow you to pass custom data into _every_ function
call. This is great for including things like the current session or config options. Defaults can be
replaced if the user gives the same-named kwarg in the string's function call. Reserved kwargs are always passed,
ignoring defaults or what the user passed. In addition, the `funcparser` and `raise_errors`
reserved kwargs are always passed - the first is a back-reference to the `FuncParser` instance and the second
is the `raise_errors` boolean passed into `FuncParser.parse`.
- 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:
@ -158,7 +165,8 @@ 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`.
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."
@ -177,23 +185,66 @@ will be a string:
"There's a 22% chance of survival."
```
However, if you use the `parse_to_any` (or `parse(..., return_str=True)`) and _don't add any extra string around the outermost function call_,
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("$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
"There is a $format(beautiful meadow, with dandelions) to the west."
```
You can escape in various ways.
- Prepending with the escape character `\`
```python
"There is a $format(beautiful meadow\, with dandelions) to the west."
```
- Wrapping your strings in quotes. This works like Python, and you can nest
double and single quotes inside each other if so needed. The result will
be a verbatim string that contains everything but the outermost quotes.
```python
"There is a $format('beautiful meadow, with dandelions') to the west."
```
- If you want verbatim quotes in your string, you can escape them too.
```python
"There is a $format('beautiful meadow, with \'dandelions\'') to the west."
```
### Safe convertion of inputs
Since you don't know in which order users may use your callables, they should always check the types
of its inputs and convert to the type the callable needs. Note also that when converting from strings,
there are limits what inputs you can support. This is because FunctionParser strings are often 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.
Since you don't know in which order users may use your callables, they should
always check the types of its inputs and convert to the type the callable needs.
Note also that when converting from strings, there are limits what inputs you
can support. This is because FunctionParser strings can be used by
non-developer players/builders and some things (such as complex
classes/callables etc) are just not safe/possible to convert from string
representation.
In `evennia.utils.utils` is a helper called
[safe_convert_to_types](evennia.utils.utils.safe_convert_to_types). This function
@ -204,19 +255,24 @@ from evennia.utils.utils import safe_convert_to_types
def _process_callable(*args, **kwargs):
"""
A callable with a lot of custom options
$process(expression, local, extra=34, extra2=foo)
$process(expression, local, extra1=34, extra2=foo)
"""
args, kwargs = safe_convert_to_type(
(('py', 'py'), {'extra1': int, 'extra2': str}),
(('py', str), {'extra1': int, 'extra2': str}),
*args, **kwargs)
# args/kwargs should be correct types now
```
In other words, in the callable `$process(expression, local, extra1=..,
extra2=...)`, the first argument will be handled by the 'py' converter
(described below), the second will passed through regular Python `str`,
kwargs will be handled by `int` and `str` respectively. You can supply
your own converter function as long as it takes one argument and returns
the converted result.
In other words,
```python
@ -224,8 +280,7 @@ args, kwargs = safe_convert_to_type(
(tuple_of_arg_converters, dict_of_kwarg_converters), *args, **kwargs)
```
Each converter should be a callable taking one argument - this will be the arg/kwarg-value to convert. The
special converter `"py"` will try to convert a string argument to a Python structure with the help of the
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
@ -339,12 +394,12 @@ references to other objects accessible via these callables.
result of `you_obj.get_display_name(looker=receiver)`. This allows for a single string to echo differently
depending on who sees it, and also to reference other people in the same way.
- `$You([key])` - same as `$you` but always capitalized.
- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb
- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb
between 2nd person presens to 3rd person presence depending on who
sees the string. For example `"$You() $conj(smiles)".` will show as "You smile." and "Tom smiles." depending
on who sees it. This makes use of the tools in [evennia.utils.verb_conjugation](evennia.utils.verb_conjugation)
to do this, and only works for English verbs.
- `$pron(pronoun [,options])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically
- `$pron(pronoun [,options])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically
map pronouns (like his, herself, you, its etc) between 1st/2nd person to 3rd person.
### Example

View file

@ -115,7 +115,7 @@ Try to `look` at the box to see the (default) description.
The description you get is not very exciting. Let's add some flavor.
describe box = This is a large and very heavy box.
desc box = This is a large and very heavy box.
If you try the `get` command we will pick up the box. So far so good, but if we really want this to
be a large and heavy box, people should _not_ be able to run off with it that easily. To prevent
@ -155,10 +155,10 @@ later, in the [Commands tutorial](./Adding-Commands.md).
[Scripts](../../../Components/Scripts.md) are powerful out-of-character objects useful for many "under the hood" things.
One of their optional abilities is to do things on a timer. To try out a first script, let's put one
on ourselves. There is an example script in `evennia/contrib/tutorial_examples/bodyfunctions.py`
on ourselves. There is an example script in `evennia/contrib/tutorials/bodyfunctions/bodyfunctions.py`
that is called `BodyFunctions`. To add this to us we will use the `script` command:
script self = tutorial_examples.bodyfunctions.BodyFunctions
script self = tutorials.bodyfunctions.BodyFunctions
This string will tell Evennia to dig up the Python code at the place we indicate. It already knows
to look in the `contrib/` folder, so we don't have to give the full path.
@ -179,7 +179,7 @@ output every time it fires.
When you are tired of your character's "insights", kill the script with
script/stop self = tutorial_examples.bodyfunctions.BodyFunctions
script/stop self = tutorials.bodyfunctions.BodyFunctions
You create your own scripts in Python, outside the game; the path you give to `script` is literally
the Python path to your script file. The [Scripts](../../../Components/Scripts.md) page explains more details.
@ -199,7 +199,7 @@ named simply `Object`. Let's create an object that is a little more interesting.
Let's make us one of _those_!
create/drop button:tutorial_examples.red_button.RedButton
create/drop button:tutorials.red_button.RedButton
The same way we did with the Script Earler, we specify a "Python-path" to the Python code we want Evennia
to use for creating the object. There you go - one red button.
@ -301,7 +301,7 @@ The Command-help is something you modify in Python code. We'll get to that when
add Commands. But you can also add regular help entries, for example to explain something about
the history of your game world:
sethelp/add History = At the dawn of time ...
sethelp History = At the dawn of time ...
You will now find your new `History` entry in the `help` list and read your help-text with `help History`.

View file

@ -2,127 +2,119 @@
*A list of resources that may be useful for Evennia users and developers.*
## Official Evennia links
## Official Evennia resources
- [evennia.com](https://www.evennia.com) - Main Evennia portal page. Links to all corners of Evennia.
- [Evennia github page](https://github.com/evennia/evennia) - Download code and read documentation.
- [Evennia official chat
channel](https://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb)
- Our official IRC chat #evennia at irc.freenode.net:6667.
- [Evennia forums/mailing list](https://groups.google.com/group/evennia) - Web interface to our
google group.
- [Evennia development blog](https://evennia.blogspot.com/) - Musings from the lead developer.
- [Evennia's manual on ReadTheDocs](https://readthedocs.org/projects/evennia/) - Read and download
offline in html, PDF or epub formats.
- [Evennia Game Index](http://games.evennia.com/) - An automated listing of Evennia games.
----
- [Evennia development blog](https://evennia.blogspot.com/) - Musings from the lead developer.
- [Evennia on GitHub](https://github.com/evennia/evennia) - Download code and read documentation.
- [Evennia on Open Hub](https://www.openhub.net/p/6906)
- [Evennia on OpenHatch](https://openhatch.org/projects/Evennia)
- [Evennia on PyPi](https://pypi.python.org/pypi/Evennia-MUD-Server/)
- [Evennia subreddit](https://www.reddit.com/r/Evennia/) (not much there yet though)
## Third-party Evennia utilities and resources
## Evennia Community
*For publicly available games running on Evennia, add and find those in the [Evennia game
index](http://games.evennia.com) instead!*
- [Evennia Game Index](http://games.evennia.com/) - An automated listing of Evennia games.
- [Evennia official Discord channel](https://discord.gg/AJJpcRUhtF)
- [Evennia official forums](https://github.com/evennia/evennia/discussions) on Github Discussions.
- [Evennia subreddit](https://www.reddit.com/r/Evennia/)
- [Discord Evennia channel](https://discord.gg/NecFePw) - This is a fan-driven Discord channel with
a bridge to the official Evennia IRC channel.
## Third-party Evennia tools
---
- [Discord live blog](https://discordapp.com/channels/517176782357528616/517176782781415434) of the
_Blackbirds_ Evennia game project.
- [Unreal Engine Evennia plugin](https://www.unrealengine.com/marketplace/en-US/slug/evennia-plugin)
an in-progress Unreal plugin for integrating Evennia with Epic Games' Unreal Engine.
- [The dark net/March Hare MUD](https://github.com/thedarknet/evennia) from the 2019 [DEF CON
27](https://www.defcon.org/html/defcon-27/dc-27-index.html) hacker conference in Paris. This is an
Evennia game dir with batchcode to build the custom _Hackers_ style cyberspace zone with puzzles and
challenges [used during the conference](https://dcdark.net/home#).
- [Arx sources](https://github.com/Arx-Game/arxcode) - Open-source code release of the very popular
[Arx](https://play.arxmush.org/) Evennia game. [Here are instructions for installing](Arxcode-
installing-help)
- [Evennia-wiki](https://github.com/vincent-lg/evennia-wiki) - An Evennia-specific Wiki for your
website.
- [Evcolor](https://github.com/taladan/Pegasus/blob/origin/world/utilities/evcolor) - Optional
coloration for Evennia unit-test output.
- [Paxboards](https://github.com/aurorachain/paxboards) - Evennia bulletin board system (both for
telnet/web).
- [Encarnia sources](https://github.com/whitehorse-io/encarnia) - An open-sourced game dir for
Evennia with things like races, combat etc. [Summary
here](https://www.reddit.com/r/MUD/comments/6z6s3j/encarnia_an_evennia_python_mud_code_base_with/).
- [The world of Cool battles sources](https://github.com/FlutterSprite/coolbattles) - Open source
turn-based battle system for Evennia. It also has a [live demo](http://wcb.battlestudio.com/).
- [nextRPI](https://github.com/cluebyte/nextrpi) - A github project for making a toolbox for people
to make [RPI](https://www.topmudsites.com/forums/showthread.php?t=4804)-style Evennia games.
- [Muddery](https://github.com/muddery/muddery) - A mud framework under development, based on an
older fork of Evennia. It has some specific design goals for building and extending the game based
on input files.
- [vim-evennia](https://github.com/amfl/vim-evennia) - A mode for editing batch-build files (`.ev`)
files in the [vim](https://www.vim.org/) text editor (Emacs users can use [evennia-
- [Discord relay](https://github.com/InspectorCaracal/evennia-things/tree/main/discord_relay) - Two-way chat relays between Evennia channels and Discord channels.
- [docker-compose for Evennia](https://github.com/gtaylor/evennia-docker) - A quick-install setup for running Evennia in a [Docker container](https://www.docker.com/). (See [the official Evennia docs](https://www.evennia.com/docs/latest/Running-Evennia-in-Docker.html) for more details on running Evennia with Docker.)
- [Evcolor](https://github.com/taladan/Pegasus/blob/origin/world/utilities/evcolor) - Optional coloration for Evennia unit-test output.
- [Evennia-wiki](https://github.com/vincent-lg/evennia-wiki) - An Evennia-specific Wiki for your website.
- [nextRPI](https://github.com/cluebyte/nextrpi) - A github project for making a toolbox for people to make [RPI](https://www.topmudsites.com/forums/showthread.php?t=4804)-style Evennia games.
- [Paxboards](https://github.com/aurorachain/paxboards) - Evennia bulletin board system (both for telnet/web).
- [Unreal Engine Evennia plugin](https://www.unrealengine.com/marketplace/en-US/slug/evennia-plugin) an in-progress Unreal plugin for integrating Evennia with Epic Games' Unreal Engine.
- [vim-evennia](https://github.com/amfl/vim-evennia) - A mode for editing batch-build files (`.ev`) files in the [vim](https://www.vim.org/) text editor (Emacs users can use [evennia-
mode.el](https://github.com/evennia/evennia/blob/master/evennia/utils/evennia-mode.el)).
- [The world of Cool battles sources](https://github.com/FlutterSprite/coolbattles) - Open source turn-based battle system for an older version of Evennia.
- [Other Evennia-related repos on github](https://github.com/search?p=1&q=evennia)
----
- [EvCast video series](https://www.youtube.com/playlist?list=PLyYMNttpc-SX1hvaqlUNmcxrhmM64pQXl) -
Tutorial videos explaining installing Evennia, basic Python etc.
- [Evennia-docker](https://github.com/gtaylor/evennia-docker) - Evennia in a [Docker
container](https://www.docker.com/) for quick install and deployment in just a few commands.
- [Evennia's docs in Chinese](http://www.evenniacn.com/) - A translated mirror of a slightly older
Evennia version. Announcement [here](https://groups.google.com/forum/#!topic/evennia/3AXS8ZTzJaA).
- [Evennia for MUSHers](https://musoapbox.net/topic/1150/evennia-for-mushers) - An article describing
Evennia for those used to the MUSH way of doing things.
- *[Language Understanding for Text games using Deep reinforcement
learning](http://news.mit.edu/2015/learning-language-playing-computer-games-0924#_msocom_1)*
([PDF](https://people.csail.mit.edu/karthikn/pdfs/mud-play15.pdf)) - MIT research paper using Evennia
to train AIs.
## Other useful mud development resources
## Evennia-Based Projects
- [ROM area reader](https://github.com/ctoth/area_reader) - Parser for converting ROM area files to
Python objects.
- [Gossip MUD chat network](https://gossip.haus/)
### Code bases
- [Arx sources](https://github.com/Arx-Game/arxcode) - Open-source code release of the very popular [Arx](https://play.arxmush.org/) Evennia game. [Here are instructions for installing](https://www.evennia.com/docs/1.0-dev/Howtos/Arxcode-Installation.html)
- [Encarnia sources](https://github.com/whitehorse-io/encarnia) - An open-sourced game dir for an older version of Evennia with things like races, combat etc. [Summary here](https://www.reddit.com/r/MUD/comments/6z6s3j/encarnia_an_evennia_python_mud_code_base_with/).
- [The dark net/March Hare MUD](https://github.com/thedarknet/evennia) from the 2019 [DEF CON 27](https://www.defcon.org/html/defcon-27/dc-27-index.html) hacker conference in Paris. This is an Evennia game dir with batchcode to build the custom _Hackers_ style cyberspace zone with puzzles and challenges [used during the conference](https://dcdark.net/home#).
- [Muddery](https://github.com/muddery/muddery) - A mud framework under development, based on an older fork of Evennia. It has some specific design goals for building and extending the game based on input files.
## General MUD forums and discussions
### Other
- [MUD Coder's Guild](https://mudcoders.com/) - A blog and [associated Slack
channel](https://slack.mudcoders.com/) with discussions on MUD development.
- [MuSoapbox](https://www.musoapbox.net/) - Very active Mu* game community mainly focused on MUSH-type gaming.
- [Imaginary Realities](http://journal.imaginary-realities.com/) - An e-magazine on game and MUD
design that has several articles about Evennia. There is also an
[archive of older issues](http://disinterest.org/resource/imaginary-realities/)
from 1998-2001 that are still very relevant.
- [Optional Realities](http://optionalrealities.com/) - Mud development discussion forums that has
regular articles on MUD development focused on roleplay-intensive games. After a HD crash it's not
as content-rich as it once was.
- [MudLab](http://mudlab.org/) - Mud design discussion forum
- [MudConnector](http://www.mudconnect.com/) - Mud listing and forums
- [MudBytes](http://www.mudbytes.net/) - Mud listing and forums
- [Top Mud Sites](http://www.topmudsites.com/) - Mud listing and forums
- [Planet Mud-Dev](http://planet-muddev.disinterest.org/) - A blog aggregator following blogs of
current MUD development (including Evennia) around the 'net. Worth to put among your RSS
subscriptions.
- Mud Dev mailing list archive ([mirror](http://www.disinterest.org/resource/MUD-Dev/)) -
Influential mailing list active 1996-2004. Advanced game design discussions.
- [Discord live blog](https://discordapp.com/channels/517176782357528616/517176782781415434) of the _Blackbirds_ Evennia game project.
- [Evennia for MUSHers](https://musoapbox.net/topic/1150/evennia-for-mushers) - An article describing Evennia for those used to the MUSH way of doing things.
- *[Language Understanding for Text games using Deep reinforcement learning](http://news.mit.edu/2015/learning-language-playing-computer-games-0924#_msocom_1)*
([PDF](https://people.csail.mit.edu/karthikn/pdfs/mud-play15.pdf)) - MIT research paper using Evennia to train AIs.
----
## General MU* resources
### Tools
- [ROM area reader](https://github.com/ctoth/area_reader) - Parser for converting ROM area files to Python objects.
### Informational
- [Imaginary Realities unofficial archive](http://tharsis-gate.org/articles/imaginary.html) - An e-magazine on game and MUD design that has several articles about Evennia.
- [Lost Library of MOO](https://www.hayseed.net/MOO/) - Archive of scientific articles on mudding (in particular moo).
- [Mud Client/Server Interaction](http://cryosphere.net/mud-protocol.html) - A page on classic MUD telnet protocols.
- [Mud-dev wiki](http://mud-dev.wikidot.com/) - A (very) slowly growing resource on MUD creation.
- [Mud Client/Server Interaction](http://cryosphere.net/mud-protocol.html) - A page on classic MUD
telnet protocols.
- [Mud Tech's fun/cool but ...](https://gc-taylor.com/blog/2013/01/08/mud-tech-funcool-dont-forget-ship-damned-thing/) -
Greg Taylor gives good advice on mud design.
- [Lost Library of MOO](https://www.hayseed.net/MOO/) - Archive of scientific articles on mudding (in
particular moo).
- [Nick Gammon's hints thread](http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=5959) -
Contains a very useful list of things to think about when starting your new MUD.
- [Lost Garden](http://www.lostgarden.com/) - A game development blog with long and interesting
articles (not MUD-specific)
- [What Games Are](http://whatgamesare.com/) - A blog about general game design (not MUD-specific)
- [The Alexandrian](https://thealexandrian.net/) - A blog about tabletop roleplaying and board games,
but with lots of general discussion about rule systems and game balance that could be applicable
also for MUDs.
- [Raph Koster's laws of game design](https://www.raphkoster.com/games/laws-of-online-world-design/the-laws-of-online-world-design/) -
thought-provoking guidelines and things to think about when designing a virtual multiplayer world
(Raph is known for *Ultima Online* among other things).
- [Mud Tech's fun/cool but ...](https://gc-taylor.com/blog/2013/01/08/mud-tech-funcool-dont-forget-ship-damned-thing/) - Greg Taylor gives good advice on mud design.
- [Nick Gammon's hints thread](http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=5959) - Contains a very useful list of things to think about when starting your new MUD.
- [Raph Koster's laws of game design](https://www.raphkoster.com/games/laws-of-online-world-design/the-laws-of-online-world-design/) - thought-provoking guidelines and things to think about when designing a virtual multiplayer world (Raph is known for *Ultima Online* among other things).
## Literature
### Community
- [Grapevine](https://grapevine.haus/) - MUD listings and inter-game chat network
- [MUD Coder's Guild](https://mudcoders.com/) - A blog and [associated Slack channel](https://slack.mudcoders.com/) with discussions on MUD development.
- [MudBytes](http://www.mudbytes.net/) - MUD listing and forums
- [MudConnector](http://www.mudconnect.com/) - MUD listing and forums
- [MudLab](http://mudlab.org/) - MUD design discussion forum
- [MuSoapbox](https://musoapbox.net/) - MU* forum mainly focused on MUSH-type gaming.
- [Top Mud Sites](http://www.topmudsites.com/) - MUD listing and forums
----
## General Game-Dev Resources
### Tools
- [GIT](https://git-scm.com/)
- [Documentation](https://git-scm.com/documentation)
- [Learn GIT in 15 minutes](https://try.github.io/levels/1/challenges/1) (interactive tutorial)
### Frameworks
- [Django's homepage](https://www.djangoproject.com/)
- [Documentation](https://docs.djangoproject.com/en)
- [Code](https://code.djangoproject.com/)
- [Twisted homepage](https://twistedmatrix.com/)
- [Documentation](https://twistedmatrix.com/documents/current/core/howto/index.html)
- [Code](https://twistedmatrix.com/trac/browser)
### Learning Python
- [Python Website](https://www.python.org/)
- [Documentation](https://www.python.org/doc/)
- [Tutorial](https://docs.python.org/tut/tut.html)
- [Library Reference](https://docs.python.org/lib/lib.html)
- [Language Reference](https://docs.python.org/ref/ref.html)
- [Python tips and tricks](https://www.siafoo.net/article/52)
- [Jetbrains Python academy](https://hyperskill.org/onboarding?track=python) - free online programming curriculum for different skill levels
### Blogs
- [Lost Garden](https://lostgarden.home.blog/) - A game development blog with long and interesting articles (not MUD-specific)
- [What Games Are](http://whatgamesare.com/) - A blog about general game design (not MUD-specific)
- [The Alexandrian](https://thealexandrian.net/) - A blog about tabletop roleplaying and board games, but with lots of general discussion about rule systems and game balance that could be applicable also for MUDs.
### Literature
- Richard Bartle *Designing Virtual Worlds*
([amazon page](https://www.amazon.com/Designing-Virtual-Worlds-Richard-Bartle/dp/0131018167)) -
@ -148,29 +140,3 @@ Contains a very useful list of things to think about when starting your new MUD.
economic theory. Written in 1730 but the translation is annotated and the essay is actually very
easy to follow also for a modern reader. Required reading if you think of implementing a sane game
economic system.
## Frameworks
- [Django's homepage](https://www.djangoproject.com/)
- [Documentation](https://docs.djangoproject.com/en)
- [Code](https://code.djangoproject.com/)
- [Twisted homepage](https://twistedmatrix.com/)
- [Documentation](https://twistedmatrix.com/documents/current/core/howto/index.html)
- [Code](https://twistedmatrix.com/trac/browser)
## Tools
- [GIT](https://git-scm.com/)
- [Documentation](https://git-scm.com/documentation)
- [Learn GIT in 15 minutes](https://try.github.io/levels/1/challenges/1) (interactive tutorial)
## Python Info
- [Python Website](https://www.python.org/)
- [Documentation](https://www.python.org/doc/)
- [Tutorial](https://docs.python.org/tut/tut.html)
- [Library Reference](https://docs.python.org/lib/lib.html)
- [Language Reference](https://docs.python.org/ref/ref.html)
- [Python tips and tricks](https://www.siafoo.net/article/52)
- [Jetbrains Python academy](https://hyperskill.org/onboarding?track=python) -
free online programming curriculum for different skill levels

View file

@ -678,7 +678,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
typeclass (str, optional): Typeclass to use for this character. If
not given, use settings.BASE_CHARACTER_TYPECLASS.
permissions (list, optional): If not given, use the account's permissions.
ip (str, optiona): The client IP creating this character. Will fall back to the
ip (str, optional): The client IP creating this character. Will fall back to the
one stored for the account if not given.
kwargs (any): Other kwargs will be used in the create_call.
Returns:
@ -955,7 +955,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
kwargs (any): Other keyword arguments will be added to the
found command object instance as variables before it
executes. This is unused by default Evennia but may be
used to set flags and change operating paramaters for
used to set flags and change operating parameters for
commands at run-time.
"""
@ -1433,7 +1433,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
if _MULTISESSION_MODE == 0:
# in this mode we should have only one character available. We
# try to auto-connect to our last conneted object, if any
# try to auto-connect to our last connected object, if any
try:
self.puppet_object(session, self.db._last_puppet)
except RuntimeError:
@ -1460,7 +1460,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
"""
Called by the login process if a user account is targeted correctly
but provided with an invalid password. By default it does nothing,
but exists to be overriden.
but exists to be overridden.
Args:
session (session): Session logging in.
@ -1703,7 +1703,7 @@ class DefaultGuest(DefaultAccount):
Gets or creates a Guest account object.
Keyword Args:
ip (str, optional): IP address of requestor; used for ban checking,
ip (str, optional): IP address of requester; used for ban checking,
throttling and logging
Returns:

View file

@ -450,9 +450,7 @@ class CmdSetHandler(object):
"""
if "permanent" in kwargs:
logger.log_dep(
"obj.cmdset.add() kwarg 'permanent' has changed name to 'persistent'."
)
logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed name to 'persistent'.")
persistent = kwargs["permanent"] if persistent is False else persistent
if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)):

View file

@ -1071,7 +1071,7 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS):
exitname, backshort = self.directions[exitshort]
backname = self.directions[backshort][0]
# if we recieved a typeclass for the exit, add it to the alias(short name)
# if we received a typeclass for the exit, add it to the alias(short name)
if ":" in self.lhs:
# limit to only the first : character
exit_typeclass = ":" + self.lhs.split(":", 1)[-1]
@ -1665,7 +1665,7 @@ class CmdSetAttribute(ObjManipCommand):
def split_nested_attr(self, attr):
"""
Yields tuples of (possible attr name, nested keys on that attr).
For performance, this is biased to the deepest match, but allows compatability
For performance, this is biased to the deepest match, but allows compatibility
with older attrs that might have been named with `[]`'s.
> list(split_nested_attr("nested['asdf'][0]"))
@ -2219,11 +2219,13 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
old_typeclass_path = obj.typeclass_path
if reset:
answer = yield("|yNote that this will reset the object back to its typeclass' default state, "
"removing any custom locks/perms/attributes etc that may have been added "
"by an explicit create_object call. Use `update` or type/force instead in order "
"to keep such data. "
"Continue [Y]/N?|n")
answer = yield (
"|yNote that this will reset the object back to its typeclass' default state, "
"removing any custom locks/perms/attributes etc that may have been added "
"by an explicit create_object call. Use `update` or type/force instead in order "
"to keep such data. "
"Continue [Y]/N?|n"
)
if answer.upper() in ("N", "NO"):
caller.msg("Aborted.")
return
@ -2830,7 +2832,7 @@ class CmdExamine(ObjManipCommand):
objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj)
objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset)
objdata[
f"Commands vailable to {obj.key} (result of Merged Cmdset(s))"
f"Commands available to {obj.key} (result of Merged Cmdset(s))"
] = self.format_current_cmds(obj, current_cmdset)
if self.object_type == "script":
objdata["Description"] = self.format_script_desc(obj)
@ -3473,7 +3475,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
caller.msg("\n".join(msgs))
if "delete" not in self.switches:
if script and script.pk:
ScriptEvMore(caller, [script], session=self.session)
ScriptEvMore(caller, [script], session=self.session)
else:
caller.msg("Script was deleted automatically.")
else:
@ -4029,7 +4031,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
)
return
try:
# we homogenize the protoype first, to be more lenient with free-form
# we homogenize the prototype first, to be more lenient with free-form
protlib.validate_prototype(protlib.homogenize_prototype(prototype))
except RuntimeError as err:
self.caller.msg(str(err))

View file

@ -1818,7 +1818,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
"""
Link an Evennia channel to an exteral Grapevine channel
Link an Evennia channel to an external Grapevine channel
Usage:
grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>

View file

@ -67,7 +67,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
help <topic>/<subtopic>/<subsubtopic> ...
Use the 'help' command alone to see an index of all help topics, organized
by category.eSome big topics may offer additional sub-topics.
by category. Some big topics may offer additional sub-topics.
"""
@ -138,7 +138,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
click_topics=True,
):
"""This visually formats the help entry.
This method can be overriden to customize the way a help
This method can be overridden to customize the way a help
entry is displayed.
Args:

View file

@ -107,8 +107,7 @@ class TestGeneral(BaseEvenniaCommandTest):
def test_nick_list(self):
self.call(general.CmdNick(), "/list", "No nicks defined.")
self.call(general.CmdNick(), "test1 = Hello",
"Inputline-nick 'test1' mapped to 'Hello'.")
self.call(general.CmdNick(), "test1 = Hello", "Inputline-nick 'test1' mapped to 'Hello'.")
self.call(general.CmdNick(), "/list", "Defined Nicks:")
def test_get_and_drop(self):
@ -1295,7 +1294,8 @@ class TestBuilding(BaseEvenniaCommandTest):
"Obj2 = evennia.objects.objects.DefaultExit",
"Obj2 changed typeclass from evennia.objects.objects.DefaultObject "
"to evennia.objects.objects.DefaultExit.",
cmdstring="swap", inputs=["yes"],
cmdstring="swap",
inputs=["yes"],
)
self.call(building.CmdTypeclass(), "/list Obj", "Core typeclasses")
self.call(
@ -1332,7 +1332,7 @@ class TestBuilding(BaseEvenniaCommandTest):
"/reset/force Obj=evennia.objects.objects.DefaultObject",
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
"All object creation hooks were run. All old attributes where deleted before the swap.",
inputs=["yes"]
inputs=["yes"],
)
from evennia.prototypes.prototypes import homogenize_prototype
@ -1359,7 +1359,7 @@ class TestBuilding(BaseEvenniaCommandTest):
"typeclasses.objects.Object.\nOnly the at_object_creation hook was run "
"(update mode). Attributes set before swap were not removed\n"
"(use `swap` or `type/reset` to clear all). Prototype 'replaced_obj' was "
"successfully applied over the object type."
"successfully applied over the object type.",
)
assert self.obj1.db.desc == "protdesc"

View file

@ -17,8 +17,10 @@ def get_component_class(component_name):
subclasses = Component.__subclasses__()
component_class = next((sc for sc in subclasses if sc.name == component_name), None)
if component_class is None:
message = f"Component named {component_name} has not been found. " \
f"Make sure it has been imported before being used."
message = (
f"Component named {component_name} has not been found. "
f"Make sure it has been imported before being used."
)
raise Exception(message)
return component_class

View file

@ -13,6 +13,7 @@ class Component:
Each Component must supply the name, it is used as a slot name but also part of the attribute key.
"""
name = ""
def __init__(self, host=None):

View file

@ -26,7 +26,7 @@ class DBField(AttributeProperty):
db_fields = getattr(owner, "_db_fields", None)
if db_fields is None:
db_fields = {}
setattr(owner, '_db_fields', db_fields)
setattr(owner, "_db_fields", db_fields)
db_fields[name] = self
@ -50,7 +50,7 @@ class NDBField(NAttributeProperty):
ndb_fields = getattr(owner, "_ndb_fields", None)
if ndb_fields is None:
ndb_fields = {}
setattr(owner, '_ndb_fields', ndb_fields)
setattr(owner, "_ndb_fields", ndb_fields)
ndb_fields[name] = self
@ -64,6 +64,7 @@ class TagField:
Default value of a tag is added when the component is registered.
Tags are removed if the component itself is removed.
"""
def __init__(self, default=None, enforce_single=False):
self._category_key = None
self._default = default
@ -78,7 +79,7 @@ class TagField:
tag_fields = getattr(owner, "_tag_fields", None)
if tag_fields is None:
tag_fields = {}
setattr(owner, '_tag_fields', tag_fields)
setattr(owner, "_tag_fields", tag_fields)
tag_fields[name] = self
def __get__(self, instance, owner):

View file

@ -16,6 +16,7 @@ class ComponentProperty:
Defaults can be overridden for this typeclass by passing kwargs
"""
def __init__(self, component_name, **kwargs):
"""
Initializes the descriptor
@ -49,6 +50,7 @@ class ComponentHandler:
It lets you add or remove components and will load components as needed.
It stores the list of registered components on the host .db with component_names as key.
"""
def __init__(self, host):
self.host = host
self._loaded_components = {}
@ -124,7 +126,9 @@ class ComponentHandler:
self.host.signals.remove_object_listeners_and_responders(component)
del self._loaded_components[component_name]
else:
message = f"Cannot remove {component_name} from {self.host.name} as it is not registered."
message = (
f"Cannot remove {component_name} from {self.host.name} as it is not registered."
)
raise ComponentIsNotRegistered(message)
def remove_by_name(self, name):
@ -199,7 +203,9 @@ class ComponentHandler:
self._set_component(component_instance)
self.host.signals.add_object_listeners_and_responders(component_instance)
else:
message = f"Could not initialize runtime component {component_name} of {self.host.name}"
message = (
f"Could not initialize runtime component {component_name} of {self.host.name}"
)
raise ComponentDoesNotExist(message)
def _set_component(self, component):

View file

@ -15,9 +15,11 @@ def as_listener(func=None, signal_name=None):
signal_name (str): The name of the signal to listen to, defaults to function name.
"""
if not func and signal_name:
def wrapper(func):
func._listener_signal_name = signal_name
return func
return wrapper
signal_name = func.__name__
@ -35,9 +37,11 @@ def as_responder(func=None, signal_name=None):
signal_name (str): The name of the signal to respond to, defaults to function name.
"""
if not func and signal_name:
def wrapper(func):
func._responder_signal_name = signal_name
return func
return wrapper
signal_name = func.__name__
@ -177,12 +181,12 @@ class SignalsHandler(object):
"""
type_host = type(obj)
for att_name, att_obj in type_host.__dict__.items():
listener_signal_name = getattr(att_obj, '_listener_signal_name', None)
listener_signal_name = getattr(att_obj, "_listener_signal_name", None)
if listener_signal_name:
callback = getattr(obj, att_name)
self.add_listener(signal_name=listener_signal_name, callback=callback)
responder_signal_name = getattr(att_obj, '_responder_signal_name', None)
responder_signal_name = getattr(att_obj, "_responder_signal_name", None)
if responder_signal_name:
callback = getattr(obj, att_name)
self.add_responder(signal_name=responder_signal_name, callback=callback)
@ -196,12 +200,12 @@ class SignalsHandler(object):
"""
type_host = type(obj)
for att_name, att_obj in type_host.__dict__.items():
listener_signal_name = getattr(att_obj, '_listener_signal_name', None)
listener_signal_name = getattr(att_obj, "_listener_signal_name", None)
if listener_signal_name:
callback = getattr(obj, att_name)
self.remove_listener(signal_name=listener_signal_name, callback=callback)
responder_signal_name = getattr(att_obj, '_responder_signal_name', None)
responder_signal_name = getattr(att_obj, "_responder_signal_name", None)
if responder_signal_name:
callback = getattr(obj, att_name)
self.remove_responder(signal_name=responder_signal_name, callback=callback)

View file

@ -56,7 +56,7 @@ class TestComponents(EvenniaTest):
def test_character_can_register_runtime_component(self):
rct = RuntimeComponentTestC.create(self.char1)
self.char1.components.add(rct)
test_c = self.char1.components.get('test_c')
test_c = self.char1.components.get("test_c")
assert test_c
assert test_c.my_int == 6
@ -110,7 +110,7 @@ class TestComponents(EvenniaTest):
assert handler.get("test_c") is rct
def test_can_access_component_regular_get(self):
assert self.char1.cmp.test_a is self.char1.components.get('test_a')
assert self.char1.cmp.test_a is self.char1.components.get("test_a")
def test_returns_none_with_regular_get_when_no_attribute(self):
assert self.char1.cmp.does_not_exist is None
@ -127,7 +127,7 @@ class TestComponents(EvenniaTest):
def test_host_has_added_component_tags(self):
rct = RuntimeComponentTestC.create(self.char1)
self.char1.components.add(rct)
test_c = self.char1.components.get('test_c')
test_c = self.char1.components.get("test_c")
assert self.char1.tags.has(key="test_c", category="components")
assert self.char1.tags.has(key="added_value", category="test_c::added_tag")
@ -162,7 +162,7 @@ class TestComponents(EvenniaTest):
assert not self.char1.tags.has(key="added_value", category="test_c::added_tag")
def test_component_tags_only_hold_one_value_when_enforce_single(self):
test_b = self.char1.components.get('test_b')
test_b = self.char1.components.get("test_b")
test_b.single_tag = "first_value"
test_b.single_tag = "second value"
@ -171,7 +171,7 @@ class TestComponents(EvenniaTest):
assert not self.char1.tags.has(key="first_value", category="test_b::single_tag")
def test_component_tags_default_value_is_overridden_when_enforce_single(self):
test_b = self.char1.components.get('test_b')
test_b = self.char1.components.get("test_b")
test_b.default_single_tag = "second value"
assert self.char1.tags.has(key="second value", category="test_b::default_single_tag")
@ -179,12 +179,14 @@ class TestComponents(EvenniaTest):
assert not self.char1.tags.has(key="first_value", category="test_b::default_single_tag")
def test_component_tags_support_multiple_values_by_default(self):
test_b = self.char1.components.get('test_b')
test_b = self.char1.components.get("test_b")
test_b.multiple_tags = "first value"
test_b.multiple_tags = "second value"
test_b.multiple_tags = "third value"
assert all(val in test_b.multiple_tags for val in ("first value", "second value", "third value"))
assert all(
val in test_b.multiple_tags for val in ("first value", "second value", "third value")
)
assert self.char1.tags.has(key="first value", category="test_b::multiple_tags")
assert self.char1.tags.has(key="second value", category="test_b::multiple_tags")
assert self.char1.tags.has(key="third value", category="test_b::multiple_tags")
@ -193,11 +195,11 @@ class TestComponents(EvenniaTest):
class CharWithSignal(ComponentHolderMixin, DefaultCharacter):
@signals.as_listener
def my_signal(self):
setattr(self, 'my_signal_is_called', True)
setattr(self, "my_signal_is_called", True)
@signals.as_listener
def my_other_signal(self):
setattr(self, 'my_other_signal_is_called', True)
setattr(self, "my_other_signal_is_called", True)
@signals.as_responder
def my_response(self):
@ -213,11 +215,11 @@ class ComponentWithSignal(Component):
@signals.as_listener
def my_signal(self):
setattr(self, 'my_signal_is_called', True)
setattr(self, "my_signal_is_called", True)
@signals.as_listener
def my_other_signal(self):
setattr(self, 'my_other_signal_is_called', True)
setattr(self, "my_other_signal_is_called", True)
@signals.as_responder
def my_response(self):
@ -236,14 +238,15 @@ class TestComponentSignals(BaseEvenniaTest):
def setUp(self):
super().setUp()
self.char1 = create.create_object(
CharWithSignal, key="Char",
CharWithSignal,
key="Char",
)
def test_host_can_register_as_listener(self):
self.char1.signals.trigger("my_signal")
assert self.char1.my_signal_is_called
assert not getattr(self.char1, 'my_other_signal_is_called', None)
assert not getattr(self.char1, "my_other_signal_is_called", None)
def test_host_can_register_as_responder(self):
responses = self.char1.signals.query("my_response")
@ -258,7 +261,7 @@ class TestComponentSignals(BaseEvenniaTest):
component = char.cmp.test_signal_a
assert component.my_signal_is_called
assert not getattr(component, 'my_other_signal_is_called', None)
assert not getattr(component, "my_other_signal_is_called", None)
def test_component_can_register_as_responder(self):
char = self.char1

View file

@ -328,4 +328,4 @@ class GametimeScript(DefaultScript):
callback()
seconds = real_seconds_until(**self.db.gametime)
self.start(interval=seconds,force_restart=True)
self.start(interval=seconds, force_restart=True)

View file

@ -284,7 +284,7 @@ def parse_language(speaker, emote):
# the key is simply the running match in the emote
key = f"##{imatch}"
# replace say with ref markers in emote
emote = "{start}{{{key}}}{end}".format( start=emote[:istart], key=key, end=emote[iend:] )
emote = "{start}{{{key}}}{end}".format(start=emote[:istart], key=key, end=emote[iend:])
mapping[key] = (langname, saytext)
if errors:
@ -339,18 +339,18 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
"""
# build a list of candidates with all possible referrable names
# include 'me' keyword for self-ref
candidate_map = [(sender, 'me')]
candidate_map = [(sender, "me")]
for obj in candidates:
# check if sender has any recogs for obj and add
if hasattr(sender, "recog"):
if recog := sender.recog.get(obj):
candidate_map.append((obj, recog))
candidate_map.append((obj, recog))
# check if obj has an sdesc and add
if hasattr(obj, "sdesc"):
candidate_map.append((obj, obj.sdesc.get()))
# if no sdesc, include key plus aliases instead
else:
candidate_map.extend( [(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()] )
candidate_map.extend([(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()])
# escape mapping syntax on the form {#id} if it exists already in emote,
# if so it is replaced with just "id".
@ -374,31 +374,40 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
match_index = marker_match.start()
# split the emote string at the reference marker, to process everything after it
head = string[:match_index]
tail = string[match_index+1:]
tail = string[match_index + 1 :]
if search_mode:
# match the candidates against the whole search string after the marker
rquery = "".join([r"\b(" + re.escape(word.strip(punctuation)) + r").*" for word in iter(tail.split())])
matches = ((re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map)
rquery = "".join(
[
r"\b(" + re.escape(word.strip(punctuation)) + r").*"
for word in iter(tail.split())
]
)
matches = (
(re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map
)
# filter out any non-matching candidates
bestmatches = [(obj, match.group()) for match, obj, text in matches if match]
else:
# to find the longest match, we start from the marker and lengthen the
# to find the longest match, we start from the marker and lengthen the
# match query one word at a time.
word_list = []
bestmatches = []
# preserve punctuation when splitting
tail = re.split('(\W)', tail)
tail = re.split("(\W)", tail)
iend = 0
for i, item in enumerate(tail):
# don't add non-word characters to the search query
if not item.isalpha():
continue
continue
word_list.append(item)
rquery = "".join([r"\b(" + re.escape(word) + r").*" for word in word_list])
# match candidates against the current set of words
matches = ((re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map)
matches = (
(re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map
)
matches = [(obj, match.group()) for match, obj, text in matches if match]
if len(matches) == 0:
# no matches at this length, keep previous iteration as best
@ -411,7 +420,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
# save search string
matched_text = "".join(tail[1:iend])
# recombine remainder of emote back into a string
tail = "".join(tail[iend+1:])
tail = "".join(tail[iend + 1 :])
nmatches = len(bestmatches)
@ -549,18 +558,18 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
if "anonymous_add" in kwargs:
anonymous_add = kwargs.pop("anonymous_add")
# make sure to catch all possible self-refs
self_refs = [f"{skey}{ref}" for ref in ('t','^','v','~','')]
self_refs = [f"{skey}{ref}" for ref in ("t", "^", "v", "~", "")]
if anonymous_add and not any(1 for tag in obj_mapping if tag in self_refs):
# no self-reference in the emote - add it
if anonymous_add == "first":
# add case flag for initial caps
skey += 't'
skey += "t"
# don't put a space after the self-ref if it's a possessive emote
femote = "{key}{emote}" if emote.startswith("'") else "{key} {emote}"
else:
# add it to the end
femote = "{emote} [{key}]"
emote = femote.format( key="{{"+ skey +"}}", emote=emote )
emote = femote.format(key="{{" + skey + "}}", emote=emote)
obj_mapping[skey] = sender
# broadcast emote to everyone
@ -663,7 +672,9 @@ class SdescHandler:
if len(cleaned_sdesc) > max_length:
raise SdescError(
"Short desc can max be {} chars long (was {} chars).".format(max_length, len(cleaned_sdesc))
"Short desc can max be {} chars long (was {} chars).".format(
max_length, len(cleaned_sdesc)
)
)
# store to attributes
@ -682,7 +693,6 @@ class SdescHandler:
return self.sdesc or self.obj.key
class RecogHandler:
"""
This handler manages the recognition mapping
@ -758,7 +768,9 @@ class RecogHandler:
if len(cleaned_recog) > max_length:
raise RecogError(
"Recog string cannot be longer than {} chars (was {} chars)".format(max_length, len(cleaned_recog))
"Recog string cannot be longer than {} chars (was {} chars)".format(
max_length, len(cleaned_recog)
)
)
# mapping #dbref:obj
@ -866,7 +878,7 @@ class CmdEmote(RPCommand): # replaces the main emote
emote = self.args
targets = self.caller.location.contents
if not emote.endswith((".", "?", "!", '"')): # If emote is not punctuated or speech,
emote += "." # add a full-stop for good measure.
emote += "." # add a full-stop for good measure.
send_emote(self.caller, targets, emote, anonymous_add="first")
@ -1132,7 +1144,11 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
if forget_mode:
# remove existing recog
caller.recog.remove(obj)
caller.msg("You will now know them only as '{}'.".format( obj.get_display_name(caller, noid=True) ))
caller.msg(
"You will now know them only as '{}'.".format(
obj.get_display_name(caller, noid=True)
)
)
else:
# set recog
sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
@ -1216,9 +1232,10 @@ class ContribRPObject(DefaultObject):
This class is meant as a mix-in or parent for objects in an
rp-heavy game. It implements the base functionality for poses.
"""
@lazy_property
def sdesc(self):
return SdescHandler(self)
return SdescHandler(self)
def at_object_creation(self):
"""
@ -1409,19 +1426,18 @@ class ContribRPObject(DefaultObject):
def get_posed_sdesc(self, sdesc, **kwargs):
"""
Displays the object with its current pose string.
Returns:
pose (str): A string containing the object's sdesc and
current or default pose.
"""
# get the current pose, or default if no pose is set
pose = self.db.pose or self.db.pose_default
# return formatted string, or sdesc as fallback
return f"{sdesc} {pose}" if pose else sdesc
def get_display_name(self, looker, **kwargs):
"""
Displays the name of the object in a viewer-aware manner.
@ -1448,8 +1464,8 @@ class ContribRPObject(DefaultObject):
is privileged to control said object.
"""
ref = kwargs.get("ref","~")
ref = kwargs.get("ref", "~")
if looker == self:
# always show your own key
sdesc = self.key
@ -1460,13 +1476,12 @@ class ContribRPObject(DefaultObject):
except AttributeError:
# use own sdesc as a fallback
sdesc = self.sdesc.get()
# add dbref is looker has control access and `noid` is not set
if self.access(looker, access_type="control") and not kwargs.get("noid",False):
sdesc = f"{sdesc}(#{self.id})"
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
# add dbref is looker has control access and `noid` is not set
if self.access(looker, access_type="control") and not kwargs.get("noid", False):
sdesc = f"{sdesc}(#{self.id})"
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
def return_appearance(self, looker):
"""
@ -1475,7 +1490,7 @@ class ContribRPObject(DefaultObject):
Args:
looker (Object): Object doing the looking.
Returns:
string (str): A string containing the name, appearance and contents
of the object.
@ -1553,11 +1568,11 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
characters stand out from other objects.
"""
ref = kwargs.get("ref","~")
ref = kwargs.get("ref", "~")
if looker == self:
# process your key as recog since you recognize yourself
sdesc = self.process_recog(self.key,self)
sdesc = self.process_recog(self.key, self)
else:
try:
# get the sdesc looker should see, with formatting
@ -1567,12 +1582,11 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
sdesc = self.sdesc.get()
# add dbref is looker has control access and `noid` is not set
if self.access(looker, access_type="control") and not kwargs.get("noid",False):
if self.access(looker, access_type="control") and not kwargs.get("noid", False):
sdesc = f"{sdesc}(#{self.id})"
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
def at_object_creation(self):
"""
Called at initial creation.
@ -1631,7 +1645,6 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
return sdesc
def process_sdesc(self, sdesc, obj, **kwargs):
"""
Allows to customize how your sdesc is displayed (primarily by
@ -1713,7 +1726,4 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
the evennia.contrib.rpg.rplanguage module.
"""
return "{label}|w{text}|n".format(
label=f"|W({language})" if language else "",
text=text
)
return "{label}|w{text}|n".format(label=f"|W({language})" if language else "", text=text)

View file

@ -165,17 +165,19 @@ class TestRPSystem(BaseEvenniaTest):
)
def test_get_sdesc(self):
looker = self.speaker # Sender
target = self.receiver1 # Receiver1
looker.sdesc.add(sdesc0) # A nice sender of emotes
target.sdesc.add(sdesc1) # The first receiver of emotes.
looker = self.speaker # Sender
target = self.receiver1 # Receiver1
looker.sdesc.add(sdesc0) # A nice sender of emotes
target.sdesc.add(sdesc1) # The first receiver of emotes.
# sdesc with no processing
self.assertEqual(looker.get_sdesc(target), "The first receiver of emotes.")
# sdesc with processing
self.assertEqual(looker.get_sdesc(target, process=True), "|bThe first receiver of emotes.|n")
looker.recog.add(target, recog01) # Mr Receiver
self.assertEqual(
looker.get_sdesc(target, process=True), "|bThe first receiver of emotes.|n"
)
looker.recog.add(target, recog01) # Mr Receiver
# recog with no processing
self.assertEqual(looker.get_sdesc(target), "Mr Receiver")
@ -233,7 +235,7 @@ class TestRPSystem(BaseEvenniaTest):
self.out1,
"|bA nice sender of emotes|n looks at |mReceiver1|n. Then, "
"|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice."
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
)
self.assertEqual(
self.out2,

View file

@ -321,7 +321,6 @@ class TestTraitStatic(_TraitHandlerBase):
self.trait.mult = 0.75
self.assertEqual(self._get_values(), (5, 1, 0.75, 4.5))
def test_delete(self):
"""Deleting resets to default."""
self.trait.mult = 2.0
@ -362,7 +361,14 @@ class TestTraitCounter(_TraitHandlerBase):
def _get_values(self):
"""Get (base, mod, mult, value, min, max)."""
return (self.trait.base, self.trait.mod, self.trait.mult, self.trait.value, self.trait.min, self.trait.max)
return (
self.trait.base,
self.trait.mod,
self.trait.mult,
self.trait.value,
self.trait.min,
self.trait.max,
)
def test_init(self):
self.assertEqual(
@ -634,7 +640,14 @@ class TestTraitGauge(_TraitHandlerBase):
def _get_values(self):
"""Get (base, mod, mult, value, min, max)."""
return (self.trait.base, self.trait.mod, self.trait.mult, self.trait.value, self.trait.min, self.trait.max)
return (
self.trait.base,
self.trait.mod,
self.trait.mult,
self.trait.value,
self.trait.min,
self.trait.max,
)
def test_init(self):
self.assertEqual(

View file

@ -1161,7 +1161,9 @@ class StaticTrait(Trait):
def __str__(self):
status = "{value:11}".format(value=self.value)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(name=self.name, status=status, mod=self.mod, mult=self.mult)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(
name=self.name, status=status, mod=self.mod, mult=self.mult
)
# Helpers
@property
@ -1322,16 +1324,16 @@ class CounterTrait(Trait):
now = time()
tdiff = now - self._data["last_update"]
current += rate * tdiff
value = (current + self.mod)
value = current + self.mod
# we must make sure so we don't overstep our bounds
# even if .mod is included
if self._passed_ratetarget(value):
current = (self._data["ratetarget"] - self.mod)
current = self._data["ratetarget"] - self.mod
self._stop_timer()
elif not self._within_boundaries(value):
current = (self._enforce_boundaries(value) - self.mod)
current = self._enforce_boundaries(value) - self.mod
self._stop_timer()
else:
self._data["last_update"] = now
@ -1571,7 +1573,9 @@ class GaugeTrait(CounterTrait):
def __str__(self):
status = "{value:4} / {base:4}".format(value=self.value, base=self.base)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(name=self.name, status=status, mod=self.mod, mult=self.mult)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(
name=self.name, status=status, mod=self.mod, mult=self.mult
)
@property
def base(self):
@ -1621,7 +1625,7 @@ class GaugeTrait(CounterTrait):
if value is None:
self._data["min"] = self.default_keys["min"]
elif type(value) in (int, float):
self._data["min"] = min(value, (self.base + self.mod) * self.mult)
self._data["min"] = min(value, (self.base + self.mod) * self.mult)
@property
def max(self):
@ -1644,7 +1648,7 @@ class GaugeTrait(CounterTrait):
def current(self):
"""The `current` value of the gauge."""
return self._update_current(
self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult))
self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult))
)
@current.setter
@ -1655,7 +1659,7 @@ class GaugeTrait(CounterTrait):
@current.deleter
def current(self):
"Resets current back to 'full'"
self._data["current"] = (self.base + self.mod) * self.mult
self._data["current"] = (self.base + self.mod) * self.mult
@property
def value(self):

View file

@ -1,6 +1,6 @@
# world/
This folder is meant as a miscellanous folder for all that other stuff
This folder is meant as a miscellaneous folder for all that other stuff
related to the game. Code which are not commands or typeclasses go
here, like custom economy systems, combat code, batch-files etc.

View file

@ -28,7 +28,7 @@ Possible keywords are:
- `prototype_key` - the name of the prototype. This is required for db-prototypes,
for module-prototypes, the global variable name of the dict is used instead
- `prototype_parent` - string pointing to parent prototype if any. Prototype inherits
in a similar way as classes, with children overriding values in their partents.
in a similar way as classes, with children overriding values in their parents.
- `key` - string, the main object identifier.
- `typeclass` - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`.
- `location` - this should be a valid object or #dbref.
@ -42,7 +42,7 @@ Possible keywords are:
of the shorter forms, defaults are used for the rest.
- `tags` - Tags, as a list of tuples `(tag,)`, `(tag, category)` or `(tag, category, data)`.
- Any other keywords are interpreted as Attributes with no category or lock.
These will internally be added to `attrs` (eqivalent to `(attrname, value)`.
These will internally be added to `attrs` (equivalent to `(attrname, value)`.
See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info.

View file

@ -12,9 +12,13 @@ import re
# since we use them (e.g. as command names).
# Lunr's default ignore-word list is found here:
# https://github.com/yeraydiazdiaz/lunr.py/blob/master/lunr/stop_word_filter.py
_LUNR_STOP_WORD_FILTER_EXCEPTIONS = (
["about", "might", "get", "who", "say"] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS
)
_LUNR_STOP_WORD_FILTER_EXCEPTIONS = [
"about",
"might",
"get",
"who",
"say",
] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS
_LUNR = None

View file

@ -473,6 +473,7 @@ def tag(accessing_obj, accessed_obj, *args, **kwargs):
category = args[1] if len(args) > 1 else None
return bool(accessing_obj.tags.get(tagkey, category=category))
def is_ooc(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -489,13 +490,14 @@ def is_ooc(accessing_obj, accessed_obj, *args, **kwargs):
session = accessed_obj.session
except AttributeError:
session = account.sessions.get()[0] # note-this doesn't work well
# for high multisession mode. We may need
# to change to sessiondb to resolve this
# for high multisession mode. We may need
# to change to sessiondb to resolve this
try:
return not account.get_puppet(session)
except TypeError:
return not session.get_puppet()
def objtag(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:

View file

@ -528,10 +528,10 @@ def search_prototype(
"""
# This will load the prototypes the first time they are searched
loaded = getattr(load_module_prototypes, '_LOADED', False)
loaded = getattr(load_module_prototypes, "_LOADED", False)
if not loaded:
load_module_prototypes()
setattr(load_module_prototypes, '_LOADED', True)
setattr(load_module_prototypes, "_LOADED", True)
# prototype keys are always in lowecase
if key:

View file

@ -2316,9 +2316,11 @@ def main():
if option in ("makemessages", "compilemessages"):
# some commands don't require the presence of a game directory to work
need_gamedir = False
if CURRENT_DIR != EVENNIA_LIB:
print("You must stand in the evennia/evennia/ folder (where the 'locale/' "
"folder is located) to run this command.")
if CURRENT_DIR != EVENNIA_LIB:
print(
"You must stand in the evennia/evennia/ folder (where the 'locale/' "
"folder is located) to run this command."
)
sys.exit()
if option in ("shell", "check", "makemigrations", "createsuperuser", "shell_plus"):

View file

@ -226,6 +226,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
or option == naws.NAWS
or option == MCCP
or option == mssp.MSSP
or option == ECHO
or option == suppress_ga.SUPPRESS_GA
)
@ -236,6 +237,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
or option == naws.NAWS
or option == MCCP
or option == mssp.MSSP
or option == ECHO
or option == suppress_ga.SUPPRESS_GA
)

View file

@ -423,6 +423,7 @@ class Evennia:
logger.log_msg("Evennia Server successfully restarted in 'reset' mode.")
elif mode == "shutdown":
from evennia.objects.models import ObjectDB
self.at_server_cold_start()
# clear eventual lingering session storages
ObjectDB.objects.clear_all_sessids()

View file

@ -683,7 +683,7 @@ class ServerSessionHandler(SessionHandler):
Get a unique list of connected and logged-in Accounts.
Returns:
accounts (list): All conected Accounts (which may be fewer than the
accounts (list): All connected Accounts (which may be fewer than the
amount of Sessions due to multi-playing).
"""

View file

@ -197,7 +197,6 @@ class TestServer(TestCase):
class TestInitHooks(TestCase):
def setUp(self):
from evennia.utils import create

View file

@ -16,15 +16,18 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
avoid running the large number of tests defined by Django
"""
def setup_test_environment(self, **kwargs):
# the portal looping call starts before the unit-test suite so we
# can't mock it - instead we stop it before starting the test - otherwise
# we'd get unclean reactor errors across test boundaries.
from evennia.server.portal.portal import PORTAL
PORTAL.maintenance_task.stop()
# initialize evennia itself
import evennia
evennia._init()
from django.conf import settings
@ -37,6 +40,7 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
# remove testing flag after suite has run
from django.conf import settings
settings._TEST_ENVIRONMENT = False
super().teardown_test_environment(**kwargs)

View file

@ -218,13 +218,15 @@ class AttributeProperty:
"""
value = self._default
try:
value = self.at_get(getattr(instance, self.attrhandler_name).get(
key=self._key,
default=self._default,
category=self._category,
strattr=self._strattr,
raise_exception=self._autocreate,
))
value = self.at_get(
getattr(instance, self.attrhandler_name).get(
key=self._key,
default=self._default,
category=self._category,
strattr=self._strattr,
raise_exception=self._autocreate,
)
)
except AttributeError:
if self._autocreate:
# attribute didn't exist and autocreate is set

View file

@ -96,6 +96,7 @@ class Tag(models.Model):
# Handlers making use of the Tags model
#
class TagProperty:
"""
Tag property descriptor. Allows for setting tags on an object as Django-like 'fields'
@ -112,6 +113,7 @@ class TagProperty:
mytag2 = TagProperty(category="tagcategory")
"""
taghandler_name = "tags"
def __init__(self, category=None, data=None):
@ -134,10 +136,7 @@ class TagProperty:
"""
try:
return getattr(instance, self.taghandler_name).get(
key=self._key,
category=self._category,
return_list=False,
raise_exception=True
key=self._key, category=self._category, return_list=False, raise_exception=True
)
except AttributeError:
self.__set__(instance, self._category)
@ -150,9 +149,7 @@ class TagProperty:
self._category = category
(
getattr(instance, self.taghandler_name).add(
key=self._key,
category=self._category,
data=self._data
key=self._key, category=self._category, data=self._data
)
)
@ -430,8 +427,15 @@ class TagHandler(object):
return ret[0] if len(ret) == 1 else ret
def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False,
raise_exception=False):
def get(
self,
key=None,
default=None,
category=None,
return_tagobj=False,
return_list=False,
raise_exception=False,
):
"""
Get the tag for the given key, category or combination of the two.
@ -613,6 +617,7 @@ class AliasProperty(TagProperty):
bob = AliasProperty()
"""
taghandler_name = "aliases"
@ -636,6 +641,7 @@ class PermissionProperty(TagProperty):
myperm = PermissionProperty()
"""
taghandler_name = "permissions"

View file

@ -145,7 +145,9 @@ class TestTypedObjectManager(BaseEvenniaTest):
def test_get_tag_with_any_including_nones(self):
self.obj1.tags.add("tagA", "categoryA")
self.assertEqual(
self._manager("get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB", None], match="any"),
self._manager(
"get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB", None], match="any"
),
[self.obj1],
)

View file

@ -23,7 +23,7 @@ from collections import deque, OrderedDict, defaultdict
from collections.abc import MutableSequence, MutableSet, MutableMapping
try:
from pickle import dumps, loads
from pickle import dumps, loads, UnpicklingError
except ImportError:
from pickle import dumps, loads
from django.core.exceptions import ObjectDoesNotExist
@ -633,12 +633,12 @@ def to_pickle(data):
# not one of the base types
if hasattr(item, "__serialize_dbobjs__"):
# Allows custom serialization of any dbobjects embedded in
# the item that Evennia will otherwise not found (these would
# the item that Evennia will otherwise not find (these would
# otherwise lead to an error). Use the dbserialize helper from
# this method.
try:
item.__serialize_dbobjs__()
except TypeError:
except TypeError as err:
# we catch typerrors so we can handle both classes (requiring
# classmethods) and instances
pass
@ -725,9 +725,13 @@ def from_pickle(data, db_obj=None):
# use the dbunserialize helper in this module.
try:
item.__deserialize_dbobjs__()
except TypeError:
except (TypeError, UnpicklingError):
# handle recoveries both of classes (requiring classmethods
# or instances
# or instances. Unpickling errors can happen when re-loading the
# data from cache (because the hidden entity was already
# deserialized and stored back on the object, unpickling it
# again fails). TODO: Maybe one could avoid this retry in a
# more graceful way?
pass
return item

View file

@ -1255,12 +1255,12 @@ class EvMenu:
min_rows = 4
# split the items into columns
split = max(min_rows, ceil(len(table)/ncols))
split = max(min_rows, ceil(len(table) / ncols))
max_end = len(table)
cols_list = []
for icol in range(ncols):
start = icol*split
end = min(start+split,max_end)
start = icol * split
end = min(start + split, max_end)
cols_list.append(EvColumn(*table[start:end]))
return str(EvTable(table=cols_list, border="none"))

View file

@ -181,7 +181,7 @@ class EvMore(object):
justify (bool, optional): If set, auto-justify long lines. This must be turned
off for fixed-width or formatted output, like tables. It's force-disabled
if `inp` is an EvTable.
justify_kwargs (dict, optional): Keywords for the justifiy function. Used only
justify_kwargs (dict, optional): Keywords for the justify function. Used only
if `justify` is True. If this is not set, default arguments will be used.
exit_on_lastpage (bool, optional): If reaching the last page without the
page being completely filled, exit pager immediately. If unset,
@ -507,7 +507,7 @@ class EvMore(object):
def page_formatter(self, page):
"""
Page formatter. Every page passes through this method. Override
it to customize behvaior per-page. A common use is to generate a new
it to customize behavior per-page. A common use is to generate a new
EvTable for every page (this is more efficient than to generate one huge
EvTable across many pages and feed it into EvMore all at once).

View file

@ -85,8 +85,8 @@ class _ParsedFunc:
# state storage
fullstr: str = ""
infuncstr: str = ""
single_quoted: bool = False
double_quoted: bool = False
single_quoted: int = -1
double_quoted: int = -1
current_kwarg: str = ""
open_lparens: int = 0
open_lsquate: int = 0
@ -319,8 +319,8 @@ class FuncParser:
# parsing state
callstack = []
single_quoted = False
double_quoted = False
single_quoted = -1
double_quoted = -1
open_lparens = 0 # open (
open_lsquare = 0 # open [
open_lcurly = 0 # open {
@ -331,6 +331,7 @@ class FuncParser:
curr_func = None
fullstr = "" # final string
infuncstr = "" # string parts inside the current level of $funcdef (including $)
literal_infuncstr = False
for char in string:
@ -374,12 +375,13 @@ class FuncParser:
curr_func.open_lcurly = open_lcurly
current_kwarg = ""
infuncstr = ""
single_quoted = False
double_quoted = False
single_quoted = -1
double_quoted = -1
open_lparens = 0
open_lsquare = 0
open_lcurly = 0
exec_return = ""
literal_infuncstr = False
callstack.append(curr_func)
# start a new func
@ -402,19 +404,41 @@ class FuncParser:
infuncstr += str(exec_return)
exec_return = ""
if char == "'": # note that this is the same as "\'"
if char == "'" and double_quoted < 0: # note that this is the same as "\'"
# a single quote - flip status
single_quoted = not single_quoted
infuncstr += char
if single_quoted == 0:
infuncstr = infuncstr[1:]
single_quoted = -1
elif single_quoted > 0:
prefix = infuncstr[0:single_quoted]
infuncstr = prefix + infuncstr[single_quoted + 1 :]
single_quoted = -1
else:
infuncstr += char
infuncstr = infuncstr.strip()
single_quoted = len(infuncstr) - 1
literal_infuncstr = True
continue
if char == '"': # note that this is the same as '\"'
if char == '"' and single_quoted < 0: # note that this is the same as '\"'
# a double quote = flip status
double_quoted = not double_quoted
infuncstr += char
if double_quoted == 0:
infuncstr = infuncstr[1:]
double_quoted = -1
elif double_quoted > 0:
prefix = infuncstr[0:double_quoted]
infuncstr = prefix + infuncstr[double_quoted + 1 :]
double_quoted = -1
else:
infuncstr += char
infuncstr = infuncstr.strip()
double_quoted = len(infuncstr) - 1
literal_infuncstr = True
continue
if double_quoted or single_quoted:
if double_quoted >= 0 or single_quoted >= 0:
# inside a string definition - this escapes everything else
infuncstr += char
continue
@ -478,12 +502,15 @@ class FuncParser:
else:
curr_func.args.append(exec_return)
else:
if not literal_infuncstr:
infuncstr = infuncstr.strip()
# store a string instead
if current_kwarg:
curr_func.kwargs[current_kwarg] = infuncstr.strip()
elif infuncstr.strip():
curr_func.kwargs[current_kwarg] = infuncstr
elif literal_infuncstr or infuncstr.strip():
# don't store the empty string
curr_func.args.append(infuncstr.strip())
curr_func.args.append(infuncstr)
# note that at this point either exec_return or infuncstr will
# be empty. We need to store the full string so we can print
@ -494,6 +521,7 @@ class FuncParser:
current_kwarg = ""
exec_return = ""
infuncstr = ""
literal_infuncstr = False
if char == ")":
# closing the function list - this means we have a
@ -537,6 +565,7 @@ class FuncParser:
if return_str:
exec_return = ""
infuncstr = ""
literal_infuncstr = False
continue
infuncstr += char

View file

@ -67,7 +67,7 @@ class TimeScript(DefaultScript):
callback(*args, **kwargs)
seconds = real_seconds_until(**self.db.gametime)
self.start(interval=seconds,force_restart=True)
self.start(interval=seconds, force_restart=True)
# Access functions

View file

@ -50,6 +50,7 @@ def _log(msg, logfunc, prefix="", **kwargs):
# log call functions (each has legacy aliases)
def log_info(msg, **kwargs):
"""
Logs any generic debugging/informative info that should appear in the log.
@ -62,6 +63,7 @@ def log_info(msg, **kwargs):
"""
_log(msg, log.info, **kwargs)
info = log_info
log_infomsg = log_info
log_msg = log_info
@ -79,6 +81,7 @@ def log_warn(msg, **kwargs):
"""
_log(msg, log.warn, **kwargs)
warn = log_warn
warning = log_warn
log_warnmsg = log_warn
@ -120,6 +123,7 @@ def log_trace(msg=None, **kwargs):
if msg:
_log(msg, log.error, prefix="!!", **kwargs)
log_tracemsg = log_trace
exception = log_trace
critical = log_trace
@ -156,6 +160,7 @@ def log_sec(msg, **kwargs):
"""
_log(msg, log.info, prefix="SS", **kwargs)
sec = log_sec
security = log_sec
log_secmsg = log_sec
@ -174,12 +179,12 @@ def log_server(msg, **kwargs):
_log(msg, log.info, prefix="Server", **kwargs)
class GetLogObserver:
"""
Sets up how the system logs are formatted.
"""
component_prefix = ""
event_levels = {
twisted_logger.LogLevel.debug: "??",
@ -207,8 +212,7 @@ class GetLogObserver:
event["log_format"] = str(event.get("log_format", ""))
component_prefix = self.component_prefix or ""
log_msg = twisted_logger.formatEventAsClassicLogText(
event,
formatTime=lambda e: twisted_logger.formatTime(e, _TIME_FORMAT)
event, formatTime=lambda e: twisted_logger.formatTime(e, _TIME_FORMAT)
)
return f"{component_prefix}{log_msg}"
@ -218,14 +222,15 @@ class GetLogObserver:
# Called by server/portal on startup
class GetPortalLogObserver(GetLogObserver):
component_prefix = "|Portal| "
class GetServerLogObserver(GetLogObserver):
component_prefix = ""
# logging overrides
@ -352,6 +357,7 @@ class WeeklyLogFile(logfile.DailyLogFile):
self.lastDate = max(self.lastDate, self.toDate())
self.size += len(data)
# Arbitrary file logger

View file

@ -96,7 +96,6 @@ DEFAULT_SETTING_RESETS = dict(
"evennia.game_template.server.conf.prototypefuncs",
],
BASE_GUEST_TYPECLASS="evennia.accounts.accounts.DefaultGuest",
# a special setting boolean _TEST_ENVIRONMENT is set by the test runner
# while the test suite is running.
)

View file

@ -15,9 +15,7 @@ class TestDbSerialize(TestCase):
"""
def setUp(self):
self.obj = DefaultObject(
db_key="Tester",
)
self.obj = DefaultObject(db_key="Tester")
self.obj.save()
def test_constants(self):
@ -117,3 +115,61 @@ class TestDbSerialize(TestCase):
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3]})
self.obj.db.test |= {"b": [5, 6]}
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3], "b": [5, 6]})
class _InvalidContainer:
"""Container not saveable in Attribute (if obj is dbobj, it 'hides' it)"""
def __init__(self, obj):
self.hidden_obj = obj
class _ValidContainer(_InvalidContainer):
"""Container possible to save in Attribute (handles hidden dbobj explicitly)"""
def __serialize_dbobjs__(self):
self.hidden_obj = dbserialize.dbserialize(self.hidden_obj)
def __deserialize_dbobjs__(self):
self.hidden_obj = dbserialize.dbunserialize(self.hidden_obj)
class DbObjWrappers(TestCase):
"""
Test the `__serialize_dbobjs__` and `__deserialize_dbobjs__` methods.
"""
def setUp(self):
super().setUp()
self.dbobj1 = DefaultObject(db_key="Tester1")
self.dbobj1.save()
self.dbobj2 = DefaultObject(db_key="Tester2")
self.dbobj2.save()
def test_dbobj_hidden_obj__fail(self):
with self.assertRaises(TypeError):
self.dbobj1.db.testarg = _InvalidContainer(self.dbobj1)
def test_consecutive_fetch(self):
con = _ValidContainer(self.dbobj2)
self.dbobj1.db.testarg = con
attrobj = self.dbobj1.attributes.get("testarg", return_obj=True)
self.assertEqual(attrobj.value, con)
self.assertEqual(attrobj.value, con)
self.assertEqual(attrobj.value.hidden_obj, self.dbobj2)
def test_dbobj_hidden_obj__success(self):
con = _ValidContainer(self.dbobj2)
self.dbobj1.db.testarg = con
# accessing the same data twice
res1 = self.dbobj1.db.testarg
res2 = self.dbobj1.db.testarg
self.assertEqual(res1, res2)
self.assertEqual(res1, con)
self.assertEqual(res2, con)
self.assertEqual(res1.hidden_obj, self.dbobj2)
self.assertEqual(res2.hidden_obj, self.dbobj2)

View file

@ -44,6 +44,7 @@ def _double_callable(*args, **kwargs):
def _eval_callable(*args, **kwargs):
if args:
return simple_eval(args[0])
return ""
@ -113,25 +114,25 @@ class TestFuncParser(TestCase):
("$foo() Test noargs5", "_test() Test noargs5"),
("Test args1 $foo(a,b,c)", "Test args1 _test(a, b, c)"),
("Test args2 $bar(foo, bar, too)", "Test args2 _test(foo, bar, too)"),
("Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, ' too')"),
("Test args4 $foo('')", "Test args4 _test('')"),
('Test args4 $foo("")', 'Test args4 _test("")'),
(r"Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, too)"),
("Test args4 $foo('')", "Test args4 _test()"),
('Test args4 $foo("")', "Test args4 _test()"),
("Test args5 $foo(\(\))", "Test args5 _test(())"),
("Test args6 $foo(\()", "Test args6 _test(()"),
("Test args7 $foo(())", "Test args7 _test(())"),
("Test args8 $foo())", "Test args8 _test())"),
("Test args9 $foo(=)", "Test args9 _test(=)"),
("Test args10 $foo(\,)", "Test args10 _test(,)"),
("Test args10 $foo(',')", "Test args10 _test(',')"),
("Test args10 $foo(',')", "Test args10 _test(,)"),
("Test args11 $foo(()", "Test args11 $foo(()"), # invalid syntax
(
"Test kwarg1 $bar(foo=1, bar='foo', too=ere)",
"Test kwarg1 _test(foo=1, bar='foo', too=ere)",
"Test kwarg1 _test(foo=1, bar=foo, too=ere)",
),
("Test kwarg2 $bar(foo,bar,too=ere)", "Test kwarg2 _test(foo, bar, too=ere)"),
("test kwarg3 $foo(foo = bar, bar = ere )", "test kwarg3 _test(foo=bar, bar=ere)"),
(
"test kwarg4 $foo(foo =' bar ',\" bar \"= ere )",
r"test kwarg4 $foo(foo =\' bar \',\" bar \"= ere )",
"test kwarg4 _test(foo=' bar ', \" bar \"=ere)",
),
(
@ -180,22 +181,29 @@ class TestFuncParser(TestCase):
("Test clr $clr(r, This is a red string!)", "Test clr |rThis is a red string!|n"),
("Test eval1 $eval(21 + 21 - 10)", "Test eval1 32"),
("Test eval2 $eval((21 + 21) / 2)", "Test eval2 21.0"),
("Test eval3 $eval('21' + 'foo' + 'bar')", "Test eval3 21foobar"),
("Test eval4 $eval('21' + '$repl()' + '' + str(10 // 2))", "Test eval4 21rr5"),
("Test eval5 $eval('21' + '\$repl()' + '' + str(10 // 2))", "Test eval5 21$repl()5"),
("Test eval6 $eval('$repl(a)' + '$repl(b)')", "Test eval6 rarrbr"),
("Test eval3 $eval(\"'21' + 'foo' + 'bar'\")", "Test eval3 21foobar"),
(r"Test eval4 $eval(\'21\' + \'$repl()\' + \"''\" + str(10 // 2))", "Test eval4 21rr5"),
(
r"Test eval5 $eval(\'21\' + \'\$repl()\' + \'\' + str(10 // 2))",
"Test eval5 21$repl()5",
),
("Test eval6 $eval(\"'$repl(a)' + '$repl(b)'\")", "Test eval6 rarrbr"),
("Test type1 $typ([1,2,3,4])", "Test type1 <class 'list'>"),
("Test type2 $typ((1,2,3,4))", "Test type2 <class 'tuple'>"),
("Test type3 $typ({1,2,3,4})", "Test type3 <class 'set'>"),
("Test type4 $typ({1:2,3:4})", "Test type4 <class 'dict'>"),
("Test type5 $typ(1), $typ(1.0)", "Test type5 <class 'int'>, <class 'float'>"),
("Test type6 $typ('1'), $typ(\"1.0\")", "Test type6 <class 'str'>, <class 'str'>"),
(
"Test type6 $typ(\"'1'\"), $typ('\"1.0\"')",
"Test type6 <class 'str'>, <class 'str'>",
),
("Test add1 $add(1, 2)", "Test add1 3"),
("Test add2 $add([1,2,3,4], [5,6])", "Test add2 [1, 2, 3, 4, 5, 6]"),
("Test literal1 $sum($lit([1,2,3,4,5,6]))", "Test literal1 21"),
("Test literal2 $typ($lit(1))", "Test literal2 <class 'int'>"),
("Test literal3 $typ($lit(1)aaa)", "Test literal3 <class 'str'>"),
("Test literal4 $typ(aaa$lit(1))", "Test literal4 <class 'str'>"),
("Test spider's thread", "Test spider's thread"),
]
)
def test_parse(self, string, expected):
@ -258,7 +266,11 @@ class TestFuncParser(TestCase):
self.assertEqual([1, 2, 3, 4], ret)
self.assertTrue(isinstance(ret, list))
ret = self.parser.parse_to_any("$lit('')")
ret = self.parser.parse_to_any("$lit(\"''\")")
self.assertEqual("", ret)
self.assertTrue(isinstance(ret, str))
ret = self.parser.parse_to_any(r"$lit(\'\')")
self.assertEqual("", ret)
self.assertTrue(isinstance(ret, str))
@ -398,6 +410,8 @@ class TestDefaultCallables(TestCase):
("There is $int2str(1) murderer, but $int2str(12) suspects.",
"There is one murderer, but twelve suspects."),
("There is $an(thing) here", "There is a thing here"),
("Some $eval(\"'-'*20\")Hello", "Some --------------------Hello"),
('$crop("spider\'s silk", 5)', "spide"),
]
)
def test_other_callables(self, string, expected):
@ -462,15 +476,16 @@ class TestDefaultCallables(TestCase):
self.parser.parse(
"this should be $pad('''escaped,''' and '''instead,''' cropped $crop(with a long,5) text., 80)"
),
"this should be '''escaped,''' and '''instead,''' cropped with text. ",
"this should be escaped, and instead, cropped with text. ",
)
def test_escaped2(self):
raw_str = 'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'
expected = "this should be escaped, and instead, cropped with text. "
result = self.parser.parse(raw_str)
self.assertEqual(
self.parser.parse(
'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'
),
'this should be """escaped,""" and """instead,""" cropped with text. ',
result,
expected,
)

View file

@ -0,0 +1,50 @@
from evennia.scripts.scripts import DefaultScript
from evennia.utils.test_resources import EvenniaTest
from evennia.utils.search import search_script_attribute, search_script_tag
class TestSearch(EvenniaTest):
def test_search_script_tag(self):
"""Check that a script can be found by its tag."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag")
found = search_script_tag("a-tag")
self.assertEqual(len(found), 1, errors)
self.assertEqual(script.key, found[0].key, errors)
def test_search_script_tag_category(self):
"""Check that a script can be found by its tag and category."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag", category="a-category")
found = search_script_tag("a-tag", category="a-category")
self.assertEqual(len(found), 1, errors)
self.assertEqual(script.key, found[0].key, errors)
def test_search_script_tag_wrong_category(self):
"""Check that a script cannot be found by the wrong category."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag", category="a-category")
found = search_script_tag("a-tag", category="wrong-category")
self.assertEqual(len(found), 0, errors)
def test_search_script_tag_wrong(self):
"""Check that a script cannot be found by the wrong tag."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag", category="a-category")
found = search_script_tag("wrong-tag", category="a-category")
self.assertEqual(len(found), 0, errors)
def test_search_script_attribute(self):
"""Check that a script can be found by its attributes."""
script, errors = DefaultScript.create("a-script")
script.db.an_attribute = "some value"
found = search_script_attribute(key="an_attribute", value="some value")
self.assertEqual(len(found), 1, errors)
self.assertEqual(script.key, found[0].key, errors)
def test_search_script_attribute_wrong(self):
"""Check that a script cannot be found by wrong value of its attributes."""
script, errors = DefaultScript.create("a-script")
script.db.an_attribute = "some value"
found = search_script_attribute(key="an_attribute", value="wrong value")
self.assertEqual(len(found), 0, errors)

View file

@ -12,7 +12,9 @@ class TestText2Html(TestCase):
self.assertEqual("foo", parser.format_styles("foo"))
self.assertEqual(
'<span class="color-001">red</span>foo',
parser.format_styles(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"),
parser.format_styles(
ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"
),
)
self.assertEqual(
'<span class="bgcolor-001">red</span>foo',
@ -31,33 +33,15 @@ class TestText2Html(TestCase):
)
self.assertEqual(
'a <span class="underline">red</span>foo',
parser.format_styles(
"a "
+ ansi.ANSI_UNDERLINE
+ "red"
+ ansi.ANSI_NORMAL
+ "foo"
),
parser.format_styles("a " + ansi.ANSI_UNDERLINE + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'a <span class="blink">red</span>foo',
parser.format_styles(
"a "
+ ansi.ANSI_BLINK
+ "red"
+ ansi.ANSI_NORMAL
+ "foo"
),
parser.format_styles("a " + ansi.ANSI_BLINK + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'a <span class="bgcolor-007 color-000">red</span>foo',
parser.format_styles(
"a "
+ ansi.ANSI_INVERSE
+ "red"
+ ansi.ANSI_NORMAL
+ "foo"
),
parser.format_styles("a " + ansi.ANSI_INVERSE + "red" + ansi.ANSI_NORMAL + "foo"),
)
def test_remove_bells(self):
@ -65,13 +49,7 @@ class TestText2Html(TestCase):
self.assertEqual("foo", parser.remove_bells("foo"))
self.assertEqual(
"a red" + ansi.ANSI_NORMAL + "foo",
parser.remove_bells(
"a "
+ ansi.ANSI_BEEP
+ "red"
+ ansi.ANSI_NORMAL
+ "foo"
),
parser.remove_bells("a " + ansi.ANSI_BEEP + "red" + ansi.ANSI_NORMAL + "foo"),
)
def test_remove_backspaces(self):
@ -160,20 +138,20 @@ class TestText2Html(TestCase):
self.assertEqual(
text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"),
'<span class="blink bgcolor-006">'
'Hello'
"Hello"
'</span><span class="underline color-009">'
'W'
"W"
'</span><span class="underline color-010">'
'o'
"o"
'</span><span class="underline color-011">'
'r'
"r"
'</span><span class="underline color-012">'
'l'
"l"
'</span><span class="underline color-013">'
'd'
"d"
'</span><span class="underline color-014">'
'!'
"!"
'</span><span class="underline bgcolor-002 color-014">'
'!'
'</span>',
"!"
"</span>",
)

View file

@ -234,9 +234,9 @@ class TextToHTMLparser(object):
for i, substr in enumerate(str_list):
# reset all current styling
if substr == ANSI_NORMAL and not clean:
# replace with close existing tag
str_list[i] = "</span>"
if substr == ANSI_NORMAL:
# close any existing span if necessary
str_list[i] = "</span>" if not clean else ""
# reset to defaults
classes = []
clean = True

View file

@ -819,7 +819,7 @@ def latinify(string, default="?", pure_ascii=False):
This is used as a last resort when normal encoding does not work.
Arguments:
string (str): A string to convert to 'safe characters' convertable
string (str): A string to convert to 'safe characters' convertible
to an latin-1 bytestring later.
default (str, optional): Characters resisting mapping will be replaced
with this character or string. The intent is to apply an encode operation
@ -1078,7 +1078,7 @@ def delay(timedelay, callback, *args, **kwargs):
Keep in mind that persistent tasks arguments and callback should not
use memory references.
If persistent is set to True the delay function will return an int
which is the task's id itended for use with TASK_HANDLER's do_task
which is the task's id intended for use with TASK_HANDLER's do_task
and remove methods.
All persistent tasks whose time delays have passed will be called on server startup.
@ -1531,12 +1531,12 @@ def class_from_module(path, defaultpaths=None, fallback=None):
defaultpaths (iterable, optional): If a direct import from `path` fails,
try subsequent imports by prepending those paths to `path`.
fallback (str): If all other attempts fail, use this path as a fallback.
This is intended as a last-resport. In the example of Evennia
This is intended as a last-resort. In the example of Evennia
loading, this would be a path to a default parent class in the
evennia repo itself.
Returns:
class (Class): An uninstatiated class recovered from path.
class (Class): An uninstantiated class recovered from path.
Raises:
ImportError: If all loading failed.
@ -1675,7 +1675,7 @@ def string_partial_matching(alternatives, inp, ret_index=True):
Matching is made from the start of each subword in each
alternative. Case is not important. So e.g. "bi sh sw" or just
"big" or "shiny" or "sw" will match "Big shiny sword". Scoring is
done to allow to separate by most common demoninator. You will get
done to allow to separate by most common denominator. You will get
multiple matches returned if appropriate.
Args:
@ -1749,7 +1749,7 @@ def format_table(table, extra_space=1):
ftable = format_table([[1,2,3], [4,5,6]])
string = ""
for ir, row in enumarate(ftable):
for ir, row in enumerate(ftable):
if ir == 0:
# make first row white
string += "\\n|w" + "".join(row) + "|n"
@ -2695,6 +2695,7 @@ def copy_word_case(base_word, new_word):
+ excess
)
def run_in_main_thread(function_or_method, *args, **kwargs):
"""
Force a callable to execute in the main Evennia thread. This is only relevant when

View file

@ -14,6 +14,7 @@ autobahn >= 20.7.1, < 21.0.0
lunr == 0.6.0
simpleeval <= 1.0
uritemplate == 4.1.1
Jinja2 < 3.1
# try to resolve dependency issue in py3.7
attrs >= 19.2.0