Refactoring contribs
This commit is contained in:
parent
f5f75bd04d
commit
0ab1c30716
103 changed files with 3203 additions and 604 deletions
|
|
@ -1,24 +1,25 @@
|
|||
# AWSstorage system
|
||||
# AWSstorage system
|
||||
|
||||
Contrib by The Right Honourable Reverend (trhr) 2020
|
||||
|
||||
## What is this for?
|
||||
## What is this for?
|
||||
|
||||
This plugin migrates the Web-based portion of Evennia, namely images,
|
||||
javascript, and other items located inside staticfiles into Amazon AWS (S3) for hosting.
|
||||
javascript, and other items located inside staticfiles into Amazon AWS (S3) for
|
||||
hosting.
|
||||
|
||||
Files hosted on S3 are "in the cloud," and while your personal
|
||||
server may be sufficient for serving multimedia to a minimal number of users,
|
||||
the perfect use case for this plugin would be:
|
||||
|
||||
- Servers supporting heavy web-based traffic (webclient, etc) ...
|
||||
- Servers supporting heavy web-based traffic (webclient, etc) ...
|
||||
- With a sizable number of users ...
|
||||
- Where the users are globally distributed ...
|
||||
- Where multimedia files are served to users as a part of gameplay
|
||||
|
||||
Bottom line - if you're sending an image to a player every time they traverse a
|
||||
map, the bandwidth reduction of using this will be substantial. If not,
|
||||
probably skip this contrib.
|
||||
map, the bandwidth reduction of using this will be substantial. If not, probably
|
||||
skip this contrib.
|
||||
|
||||
## On costs
|
||||
|
||||
|
|
@ -35,14 +36,14 @@ pricing structure.
|
|||
This is a drop-in replacement that operates deeper than all of Evennia's code,
|
||||
so your existing code does not need to change at all to support it.
|
||||
|
||||
For example, when Evennia (or Django), tries to save a file permanently
|
||||
(say, an image uploaded by a user), the save (or load) communication follows the path:
|
||||
For example, when Evennia (or Django), tries to save a file permanently (say, an
|
||||
image uploaded by a user), the save (or load) communication follows the path:
|
||||
|
||||
Evennia -> Django
|
||||
Django -> Storage backend
|
||||
Storage backend -> file storage location (e.g. hard drive)
|
||||
Evennia -> Django
|
||||
Django -> Storage backend
|
||||
Storage backend -> file storage location (e.g. hard drive)
|
||||
|
||||
https://docs.djangoproject.com/en/3.0/ref/settings/#std:setting-STATICFILES_STORAGE
|
||||
[django docs](https://docs.djangoproject.com/en/3.0/ref/settings/#std:setting-STATICFILES_STORAGE)
|
||||
|
||||
This plugin, when enabled, overrides the default storage backend,
|
||||
which defaults to saving files at mygame/website/, instead,
|
||||
|
|
@ -111,7 +112,7 @@ Advanced Users: The second IAM statement, CreateBucket, is only needed
|
|||
for initial installation. You can remove it later, or you can
|
||||
create the bucket and set the ACL yourself before you continue.
|
||||
|
||||
## Dependencies
|
||||
## Dependencies
|
||||
|
||||
|
||||
This package requires the dependency "boto3 >= 1.4.4", the official
|
||||
|
|
@ -120,14 +121,14 @@ extra requirements;
|
|||
|
||||
- Activate your `virtualenv`
|
||||
- `cd` to the root of the Evennia repository. There should be an `requirements_extra.txt`
|
||||
file here.
|
||||
- `pip install -r requirements_extra.txt`
|
||||
file here.
|
||||
- `pip install -r requirements_extra.txt`
|
||||
|
||||
## Configure Evennia
|
||||
## Configure Evennia
|
||||
|
||||
Customize the variables defined below in `secret_settings.py`. No further
|
||||
configuration is needed. Note the three lines that you need to set to your
|
||||
actual values.
|
||||
actual values.
|
||||
|
||||
```python
|
||||
# START OF SECRET_SETTINGS.PY COPY/PASTE >>>
|
||||
|
|
@ -145,7 +146,7 @@ AWS_S3_OBJECT_PARAMETERS = { 'Expires': 'Thu, 31 Dec 2099 20:00:00 GMT',
|
|||
AWS_DEFAULT_ACL = 'public-read'
|
||||
AWS_S3_CUSTOM_DOMAIN = '%s.s3.amazonaws.com' % settings.AWS_BUCKET_NAME
|
||||
AWS_AUTO_CREATE_BUCKET = True
|
||||
STATICFILES_STORAGE = 'evennia.contrib.awsstorage.aws-s3-cdn.S3Boto3Storage'
|
||||
STATICFILES_STORAGE = 'evennia.contrib.base_systems.awsstorage.aws-s3-cdn.S3Boto3Storage'
|
||||
|
||||
# <<< END OF SECRET_SETTINGS.PY COPY/PASTE
|
||||
```
|
||||
|
|
@ -153,14 +154,14 @@ STATICFILES_STORAGE = 'evennia.contrib.awsstorage.aws-s3-cdn.S3Boto3Storage'
|
|||
You may also store these keys as environment variables of the same name.
|
||||
For advanced configuration, refer to the docs for django-storages.
|
||||
|
||||
After copying the above, run `evennia reboot`.
|
||||
After copying the above, run `evennia reboot`.
|
||||
|
||||
## Check that it works
|
||||
|
||||
Confirm that web assets are being served from S3 by visiting your website, then
|
||||
checking the source of any image (for instance, the logo). It should read
|
||||
`https://your-bucket-name.s3.amazonaws.com/path/to/file`. If so, the system
|
||||
works and you shouldn't need to do anything else.
|
||||
works and you shouldn't need to do anything else.
|
||||
|
||||
# Uninstallation
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""
|
||||
Intended to be a collecting folder for Django-specific contribs that do not have observable effects to players.
|
||||
AWS storage system contrib - trhr 2020
|
||||
|
||||
"""
|
||||
|
|
|
|||
127
evennia/contrib/base_systems/building_menu/README.md
Normal file
127
evennia/contrib/base_systems/building_menu/README.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# Building menu
|
||||
|
||||
Module containing the building menu system.
|
||||
|
||||
Evennia contributor: vincent-lg 2018
|
||||
|
||||
Building menus are in-game menus, not unlike `EvMenu` though using a
|
||||
different approach. Building menus have been specifically designed to edit
|
||||
information as a builder. Creating a building menu in a command allows
|
||||
builders quick-editing of a given object, like a room. If you follow the
|
||||
steps below to add the contrib, you will have access to an `@edit` command
|
||||
that will edit any default object offering to change its key and description.
|
||||
|
||||
## Install
|
||||
|
||||
1. Import the `GenericBuildingCmd` class from this contrib in your `mygame/commands/default_cmdset.py` file:
|
||||
|
||||
```python
|
||||
from evennia.base_systems.contrib.building_menu import GenericBuildingCmd
|
||||
```
|
||||
|
||||
2. Below, add the command in the `CharacterCmdSet`:
|
||||
|
||||
```python
|
||||
# ... These lines should exist in the file
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
super(CharacterCmdSet, self).at_cmdset_creation()
|
||||
# ... add the line below
|
||||
self.add(GenericBuildingCmd())
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The `edit` command will allow you to edit any object. You will need to
|
||||
specify the object name or ID as an argument. For instance: `edit here`
|
||||
will edit the current room. However, building menus can perform much more
|
||||
than this very simple example, read on for more details.
|
||||
|
||||
Building menus can be set to edit about anything. Here is an example of
|
||||
output you could obtain when editing the room:
|
||||
|
||||
```
|
||||
Editing the room: Limbo(#2)
|
||||
|
||||
[T]itle: the limbo room
|
||||
[D]escription
|
||||
This is the limbo room. You can easily change this default description,
|
||||
either by using the |y@desc/edit|n command, or simply by entering this
|
||||
menu (enter |yd|n).
|
||||
[E]xits:
|
||||
north to A parking(#4)
|
||||
[Q]uit this menu
|
||||
```
|
||||
|
||||
From there, you can open the title choice by pressing t. You can then
|
||||
change the room title by simply entering text, and go back to the
|
||||
main menu entering @ (all this is customizable). Press q to quit this menu.
|
||||
|
||||
The first thing to do is to create a new module and place a class
|
||||
inheriting from `BuildingMenu` in it.
|
||||
|
||||
```python
|
||||
from evennia.contrib.base_systems.building_menu import BuildingMenu
|
||||
|
||||
class RoomBuildingMenu(BuildingMenu):
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
Next, override the `init` method (not `__init__`!). You can add
|
||||
choices (like the title, description, and exits choices as seen above) by using
|
||||
the `add_choice` method.
|
||||
|
||||
```python
|
||||
class RoomBuildingMenu(BuildingMenu):
|
||||
def init(self, room):
|
||||
self.add_choice("title", "t", attr="key")
|
||||
|
||||
```
|
||||
|
||||
That will create the first choice, the title choice. If one opens your menu
|
||||
and enter t, she will be in the title choice. She can change the title
|
||||
(it will write in the room's `key` attribute) and then go back to the
|
||||
main menu using `@`.
|
||||
|
||||
`add_choice` has a lot of arguments and offers a great deal of
|
||||
flexibility. The most useful ones is probably the usage of callbacks,
|
||||
as you can set almost any argument in `add_choice` to be a callback, a
|
||||
function that you have defined above in your module. This function will be
|
||||
called when the menu element is triggered.
|
||||
|
||||
Notice that in order to edit a description, the best method to call isn't
|
||||
`add_choice`, but `add_choice_edit`. This is a convenient shortcut
|
||||
which is available to quickly open an `EvEditor` when entering this choice
|
||||
and going back to the menu when the editor closes.
|
||||
|
||||
```python
|
||||
class RoomBuildingMenu(BuildingMenu):
|
||||
def init(self, room):
|
||||
self.add_choice("title", "t", attr="key")
|
||||
self.add_choice_edit("description", key="d", attr="db.desc")
|
||||
|
||||
```
|
||||
|
||||
When you wish to create a building menu, you just need to import your
|
||||
class, create it specifying your intended caller and object to edit,
|
||||
then call `open`:
|
||||
|
||||
```python
|
||||
from <wherever> import RoomBuildingMenu
|
||||
|
||||
class CmdEdit(Command):
|
||||
|
||||
key = "redit"
|
||||
|
||||
def func(self):
|
||||
menu = RoomBuildingMenu(self.caller, self.caller.location)
|
||||
menu.open()
|
||||
|
||||
```
|
||||
|
||||
This is a very short introduction. For more details, see the [online
|
||||
tutorial](https://github.com/evennia/evennia/wiki/Building-menus) or read the
|
||||
heavily-documented code of the contrib itself.
|
||||
6
evennia/contrib/base_systems/building_menu/__init__.py
Normal file
6
evennia/contrib/base_systems/building_menu/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
Build-menu contrib - vincent-lg 2018
|
||||
|
||||
"""
|
||||
from .building_menu import GenericBuildingCmd # noqa
|
||||
from .building_menu import BuildingMenu # noqa
|
||||
|
|
@ -10,10 +10,12 @@ builders quick-editing of a given object, like a room. If you follow the
|
|||
steps below to add the contrib, you will have access to an `@edit` command
|
||||
that will edit any default object offering to change its key and description.
|
||||
|
||||
1. Import the `GenericBuildingCmd` class from this contrib in your `mygame/commands/default_cmdset.py` file:
|
||||
1. Import the `GenericBuildingCmd` class from this contrib in your
|
||||
`mygame/commands/default_cmdset.py` file:
|
||||
|
||||
```python
|
||||
from evennia.contrib.building_menu import GenericBuildingCmd
|
||||
from evennia.contrib.base_systems.building_menu import GenericBuildingCmd
|
||||
|
||||
```
|
||||
|
||||
2. Below, add the command in the `CharacterCmdSet`:
|
||||
|
|
@ -58,10 +60,11 @@ The first thing to do is to create a new module and place a class
|
|||
inheriting from `BuildingMenu` in it.
|
||||
|
||||
```python
|
||||
from evennia.contrib.building_menu import BuildingMenu
|
||||
from evennia.contrib.building_menu.building_menu import BuildingMenu
|
||||
|
||||
class RoomBuildingMenu(BuildingMenu):
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
Next, override the `init` method. You can add choices (like the title,
|
||||
57
evennia/contrib/base_systems/color_markups/README.md
Normal file
57
evennia/contrib/base_systems/color_markups/README.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Color markups
|
||||
|
||||
Contribution, Griatch 2017
|
||||
|
||||
Additional color markup styles for Evennia (extending or replacing the default
|
||||
`|r`, `|234` etc).
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Import the desired style variables from this module into
|
||||
mygame/server/conf/settings.py and add them to the settings variables below.
|
||||
Each are specified as a list, and multiple such lists can be added to each
|
||||
variable to support multiple formats. Note that list order affects which regexes
|
||||
are applied first. You must restart both Portal and Server for color tags to
|
||||
update.
|
||||
|
||||
Assign to the following settings variables (see below for example):
|
||||
|
||||
COLOR_ANSI_EXTRA_MAP - a mapping between regexes and ANSI colors
|
||||
COLOR_XTERM256_EXTRA_FG - regex for defining XTERM256 foreground colors
|
||||
COLOR_XTERM256_EXTRA_BG - regex for defining XTERM256 background colors
|
||||
COLOR_XTERM256_EXTRA_GFG - regex for defining XTERM256 grayscale foreground colors
|
||||
COLOR_XTERM256_EXTRA_GBG - regex for defining XTERM256 grayscale background colors
|
||||
COLOR_ANSI_BRIGHT_BG_EXTRA_MAP = ANSI does not support bright backgrounds; we fake
|
||||
this by mapping ANSI markup to matching bright XTERM256 backgrounds
|
||||
|
||||
COLOR_NO_DEFAULT - Set True/False. If False (default), extend the default
|
||||
markup, otherwise replace it completely.
|
||||
|
||||
## Example
|
||||
|
||||
To add the {- "curly-bracket" style, add the following to your settings file,
|
||||
then reboot both Server and Portal:
|
||||
|
||||
```python
|
||||
from evennia.contrib.base_systems import color_markups
|
||||
COLOR_ANSI_EXTRA_MAP = color_markups.CURLY_COLOR_ANSI_EXTRA_MAP
|
||||
COLOR_XTERM256_EXTRA_FG = color_markups.CURLY_COLOR_XTERM256_EXTRA_FG
|
||||
COLOR_XTERM256_EXTRA_BG = color_markups.CURLY_COLOR_XTERM256_EXTRA_BG
|
||||
COLOR_XTERM256_EXTRA_GFG = color_markups.CURLY_COLOR_XTERM256_EXTRA_GFG
|
||||
COLOR_XTERM256_EXTRA_GBG = color_markups.CURLY_COLOR_XTERM256_EXTRA_GBG
|
||||
COLOR_ANSI_BRIGHT_BG_EXTRA_MAP = color_markups.CURLY_COLOR_ANSI_BRIGHT_BG_EXTRA_MAP
|
||||
```
|
||||
|
||||
To add the `%c-` "mux/mush" style, add the following to your settings file, then
|
||||
reboot both Server and Portal:
|
||||
|
||||
```python
|
||||
from evennia.contrib import color_markups
|
||||
COLOR_ANSI_EXTRA_MAP = color_markups.MUX_COLOR_ANSI_EXTRA_MAP
|
||||
COLOR_XTERM256_EXTRA_FG = color_markups.MUX_COLOR_XTERM256_EXTRA_FG
|
||||
COLOR_XTERM256_EXTRA_BG = color_markups.MUX_COLOR_XTERM256_EXTRA_BG
|
||||
COLOR_XTERM256_EXTRA_GFG = color_markups.MUX_COLOR_XTERM256_EXTRA_GFG
|
||||
COLOR_XTERM256_EXTRA_GBG = color_markups.MUX_COLOR_XTERM256_EXTRA_GBG
|
||||
COLOR_ANSI_BRIGHT_BGS_EXTRA_MAP = color_markups.CURLY_COLOR_ANSI_BRIGHT_BGS_EXTRA_MAP
|
||||
```
|
||||
6
evennia/contrib/base_systems/color_markups/__init__.py
Normal file
6
evennia/contrib/base_systems/color_markups/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
Color markups contrib - Griatch 2017
|
||||
|
||||
"""
|
||||
|
||||
from .color_markups import * # noqa
|
||||
|
|
@ -30,7 +30,7 @@ Assign to the following settings variables:
|
|||
To add the {- "curly-bracket" style, add the following to your settings file, then reboot both
|
||||
Server and Portal:
|
||||
|
||||
from evennia.contrib import color_markups
|
||||
from evennia.contrib.base_systems import color_markups
|
||||
COLOR_ANSI_EXTRA_MAP = color_markups.CURLY_COLOR_ANSI_EXTRA_MAP
|
||||
COLOR_XTERM256_EXTRA_FG = color_markups.CURLY_COLOR_XTERM256_EXTRA_FG
|
||||
COLOR_XTERM256_EXTRA_BG = color_markups.CURLY_COLOR_XTERM256_EXTRA_BG
|
||||
|
|
@ -42,7 +42,7 @@ COLOR_ANSI_BRIGHT_BG_EXTRA_MAP = color_markups.CURLY_COLOR_ANSI_BRIGHT_BG_EXTRA_
|
|||
To add the %c- "mux/mush" style, add the following to your settings file, then reboot both Server
|
||||
and Portal:
|
||||
|
||||
from evennia.contrib import color_markups
|
||||
from evennia.contrib.base_systems import color_markups
|
||||
COLOR_ANSI_EXTRA_MAP = color_markups.MUX_COLOR_ANSI_EXTRA_MAP
|
||||
COLOR_XTERM256_EXTRA_FG = color_markups.MUX_COLOR_XTERM256_EXTRA_FG
|
||||
COLOR_XTERM256_EXTRA_BG = color_markups.MUX_COLOR_XTERM256_EXTRA_BG
|
||||
47
evennia/contrib/base_systems/custom_gametime/README.md
Normal file
47
evennia/contrib/base_systems/custom_gametime/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Custom gameime
|
||||
|
||||
Contrib - Griatch 2017, vlgeoff 2017
|
||||
|
||||
This reimplements the `evennia.utils.gametime` module but supporting a custom
|
||||
calendar for your game world. It allows for scheduling events to happen at given
|
||||
in-game times, taking this custom calendar into account.
|
||||
|
||||
## Installation
|
||||
|
||||
Import and use this in the same way as you would the normal
|
||||
`evennia.utils.gametime` module.
|
||||
|
||||
Customize the calendar by adding a `TIME_UNITS` dict to your settings (see
|
||||
example below).
|
||||
|
||||
|
||||
## Usage:
|
||||
|
||||
```python
|
||||
from evennia.contrib.base_systems import custom_gametime
|
||||
|
||||
gametime = custom_gametime.realtime_to_gametime(days=23)
|
||||
|
||||
# scedule an event to fire every in-game 10 hours
|
||||
custom_gametime.schedule(callback, repeat=True, hour=10)
|
||||
|
||||
```
|
||||
|
||||
The calendar can be customized by adding the `TIME_UNITS` dictionary to your
|
||||
settings file. This maps unit names to their length, expressed in the smallest
|
||||
unit. Here's the default as an example:
|
||||
|
||||
TIME_UNITS = {
|
||||
"sec": 1,
|
||||
"min": 60,
|
||||
"hr": 60 * 60,
|
||||
"hour": 60 * 60,
|
||||
"day": 60 * 60 * 24,
|
||||
"week": 60 * 60 * 24 * 7,
|
||||
"month": 60 * 60 * 24 * 7 * 4,
|
||||
"yr": 60 * 60 * 24 * 7 * 4 * 12,
|
||||
"year": 60 * 60 * 24 * 7 * 4 * 12, }
|
||||
|
||||
When using a custom calendar, these time unit names are used as kwargs to
|
||||
the converter functions in this module. Even if your calendar uses other names
|
||||
for months/weeks etc the system needs the default names internally.
|
||||
6
evennia/contrib/base_systems/custom_gametime/__init__.py
Normal file
6
evennia/contrib/base_systems/custom_gametime/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
Custom gametime contrib - Griatch, vlgeoff 2017
|
||||
|
||||
"""
|
||||
|
||||
from .custom_gametime import * # noqa
|
||||
30
evennia/contrib/base_systems/email_login/README.md
Normal file
30
evennia/contrib/base_systems/email_login/README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Email-based login system
|
||||
|
||||
Evennia contrib - Griatch 2012
|
||||
|
||||
This is a variant of the login system that requires an email-address
|
||||
instead of a username to login.
|
||||
|
||||
This used to be the default Evennia login before replacing it with a
|
||||
more standard username + password system (having to supply an email
|
||||
for some reason caused a lot of confusion when people wanted to expand
|
||||
on it. The email is not strictly needed internally, nor is any
|
||||
confirmation email sent out anyway).
|
||||
|
||||
## Installation
|
||||
|
||||
To your settings file, add/edit the line:
|
||||
|
||||
```python
|
||||
CMDSET_UNLOGGEDIN = "contrib.base_systems.email_login.UnloggedinCmdSet"
|
||||
CONNECTION_SCREEN_MODULE = "contrib.base_systems.email_login.connection_screens"
|
||||
|
||||
```
|
||||
|
||||
That's it. Reload the server and reconnect to see it.
|
||||
|
||||
## Notes:
|
||||
|
||||
If you want to modify the way the connection screen looks, point
|
||||
`CONNECTION_SCREEN_MODULE` to your own module. Use the default as a
|
||||
guide (see also Evennia docs).
|
||||
7
evennia/contrib/base_systems/email_login/__init__.py
Normal file
7
evennia/contrib/base_systems/email_login/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Email login contrib - Griatch 2012
|
||||
|
||||
"""
|
||||
|
||||
from .email_login import UnloggedinCmdSet # noqa
|
||||
from . import connection_screens # noqa
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Connection screen
|
||||
|
||||
This is the text to show the user when they first connect to the game (before
|
||||
they log in).
|
||||
|
||||
To change the login screen in this module, do one of the following:
|
||||
|
||||
- Define a function `connection_screen()`, taking no arguments. This will be
|
||||
called first and must return the full string to act as the connection screen.
|
||||
This can be used to produce more dynamic screens.
|
||||
- Alternatively, define a string variable in the outermost scope of this module
|
||||
with the connection string that should be displayed. If more than one such
|
||||
variable is given, Evennia will pick one of them at random.
|
||||
|
||||
The commands available to the user when the connection screen is shown
|
||||
are defined in evennia.default_cmds.UnloggedinCmdSet. The parsing and display
|
||||
of the screen is done by the unlogged-in "look" command.
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import utils
|
||||
|
||||
CONNECTION_SCREEN = """
|
||||
|b==============================================================|n
|
||||
Welcome to |g{}|n, version {}!
|
||||
|
||||
If you have an existing account, connect to it by typing:
|
||||
|wconnect <email> <password>|n
|
||||
If you need to create an account, type (without the <>'s):
|
||||
|wcreate <username> <email> <password>|n
|
||||
|
||||
Enter |whelp|n for more info. |wlook|n will re-show this screen.
|
||||
|b==============================================================|n""".format(
|
||||
settings.SERVERNAME, utils.get_evennia_version("short")
|
||||
)
|
||||
|
|
@ -16,10 +16,12 @@ confirmation email sent out anyway).
|
|||
|
||||
Installation is simple:
|
||||
|
||||
To your settings file, add/edit the line:
|
||||
To your settings file, add/edit settings as follows:
|
||||
|
||||
```python
|
||||
CMDSET_UNLOGGEDIN = "contrib.email_login.UnloggedinCmdSet"
|
||||
CMDSET_UNLOGGEDIN = "contrib.base_systems.email_login.email_login.UnloggedinCmdSet"
|
||||
CONNECTION_SCREEN_MODULE = "contrib.base_systems.email_login.connection_screens"
|
||||
|
||||
```
|
||||
|
||||
That's it. Reload the server and try to log in to see it.
|
||||
|
|
@ -2,54 +2,62 @@
|
|||
|
||||
Vincent Le Goff 2017
|
||||
|
||||
This contrib adds the system of in-game Python in Evennia, allowing immortals (or other trusted builders) to
|
||||
dynamically add features to individual objects. Using custom Python set in-game, every immortal or privileged users
|
||||
could have a specific room, exit, character, object or something else behave differently from its
|
||||
"cousins". For these familiar with the use of softcode in MU`*`, like SMAUG MudProgs, the ability to
|
||||
add arbitrary behavior to individual objects is a step toward freedom. Keep in mind, however, the
|
||||
warning below, and read it carefully before the rest of the documentation.
|
||||
This contrib adds the system of in-game Python in Evennia, allowing immortals
|
||||
(or other trusted builders) to dynamically add features to individual objects.
|
||||
Using custom Python set in-game, every immortal or privileged users could have a
|
||||
specific room, exit, character, object or something else behave differently from
|
||||
its "cousins". For these familiar with the use of softcode in MU`*`, like SMAUG
|
||||
MudProgs, the ability to add arbitrary behavior to individual objects is a step
|
||||
toward freedom. Keep in mind, however, the warning below, and read it carefully
|
||||
before the rest of the documentation.
|
||||
|
||||
## A WARNING REGARDING SECURITY
|
||||
|
||||
Evennia's in-game Python system will run arbitrary Python code without much restriction. Such a system is as
|
||||
powerful as potentially dangerous, and you will have to keep in mind these points before deciding to
|
||||
install it:
|
||||
Evennia's in-game Python system will run arbitrary Python code without much
|
||||
restriction. Such a system is as powerful as potentially dangerous, and you
|
||||
will have to keep in mind these points before deciding to install it:
|
||||
|
||||
1. Untrusted people can run Python code on your game server with this system. Be careful about who
|
||||
can use this system (see the permissions below).
|
||||
2. You can do all of this in Python outside the game. The in-game Python system is not to replace all your
|
||||
game feature.
|
||||
1. Untrusted people can run Python code on your game server with this system.
|
||||
Be careful about who can use this system (see the permissions below).
|
||||
2. You can do all of this in Python outside the game. The in-game Python system
|
||||
is not to replace all your game feature.
|
||||
|
||||
## Basic structure and vocabulary
|
||||
|
||||
- At the basis of the in-game Python system are **events**. An **event** defines the context in which we
|
||||
would like to call some arbitrary code. For instance, one event is
|
||||
defined on exits and will fire every time a character traverses through this exit. Events are described
|
||||
on a [typeclass](https://github.com/evennia/evennia/wiki/Typeclasses) (like
|
||||
[exits](https://github.com/evennia/evennia/wiki/Objects#exits) in our example). All objects inheriting
|
||||
from this typeclass will have access to this event.
|
||||
- **Callbacks** can be set on individual objects, on events defined in code. These **callbacks**
|
||||
can contain arbitrary code and describe a specific behavior for an object. When the event fires,
|
||||
all callbacks connected to this object's event are executed.
|
||||
- At the basis of the in-game Python system are **events**. An **event**
|
||||
defines the context in which we would like to call some arbitrary code. For
|
||||
instance, one event is defined on exits and will fire every time a character
|
||||
traverses through this exit. Events are described on a [typeclass](Typeclasses)
|
||||
([exits](Objects#exits) in our example). All objects inheriting from this
|
||||
typeclass will have access to this event.
|
||||
- **Callbacks** can be set on individual objects, on events defined in code.
|
||||
These **callbacks** can contain arbitrary code and describe a specific
|
||||
behavior for an object. When the event fires, all callbacks connected to this
|
||||
object's event are executed.
|
||||
|
||||
To see the system in context, when an object is picked up (using the default `get` command), a
|
||||
specific event is fired:
|
||||
To see the system in context, when an object is picked up (using the default
|
||||
`get` command), a specific event is fired:
|
||||
|
||||
1. The event "get" is set on objects (on the `Object` typeclass).
|
||||
2. When using the "get" command to pick up an object, this object's `at_get` hook is called.
|
||||
3. A modified hook of DefaultObject is set by the event system. This hook will execute (or call)
|
||||
the "get" event on this object.
|
||||
4. All callbacks tied to this object's "get" event will be executed in order. These callbacks act
|
||||
as functions containing Python code that you can write in-game, using specific variables that
|
||||
will be listed when you edit the callback itself.
|
||||
5. In individual callbacks, you can add multiple lines of Python code that will be fired at this
|
||||
point. In this example, the `character` variable will contain the character who has picked up
|
||||
the object, while `obj` will contain the object that was picked up.
|
||||
2. When using the "get" command to pick up an object, this object's `at_get`
|
||||
hook is called.
|
||||
3. A modified hook of DefaultObject is set by the event system. This hook will
|
||||
execute (or call) the "get" event on this object.
|
||||
4. All callbacks tied to this object's "get" event will be executed in order.
|
||||
These callbacks act as functions containing Python code that you can write
|
||||
in-game, using specific variables that will be listed when you edit the callback
|
||||
itself.
|
||||
5. In individual callbacks, you can add multiple lines of Python code that will
|
||||
be fired at this point. In this example, the `character` variable will
|
||||
contain the character who has picked up the object, while `obj` will contain the
|
||||
object that was picked up.
|
||||
|
||||
Following this example, if you create a callback "get" on the object "a sword", and put in it:
|
||||
Following this example, if you create a callback "get" on the object "a sword",
|
||||
and put in it:
|
||||
|
||||
```python
|
||||
character.msg("You have picked up {} and have completed this quest!".format(obj.get_display_name(character)))
|
||||
|
||||
```
|
||||
|
||||
When you pick up this object you should see something like:
|
||||
|
|
@ -59,11 +67,13 @@ When you pick up this object you should see something like:
|
|||
|
||||
## Installation
|
||||
|
||||
Being in a separate contrib, the in-game Python system isn't installed by default. You need to do it
|
||||
manually, following these steps:
|
||||
Being in a separate contrib, the in-game Python system isn't installed by
|
||||
default. You need to do it manually, following these steps:
|
||||
|
||||
This is the quick summary. Scroll down for more detailed help on each step.
|
||||
|
||||
1. Launch the main script (important!):
|
||||
```@py evennia.create_script("evennia.contrib.ingame_python.scripts.EventHandler")```
|
||||
```py evennia.create_script("evennia.contrib.base_systems.ingame_python.scripts.EventHandler")```
|
||||
2. Set the permissions (optional):
|
||||
- `EVENTS_WITH_VALIDATION`: a group that can edit callbacks, but will need approval (default to
|
||||
`None`).
|
||||
|
|
@ -72,7 +82,7 @@ manually, following these steps:
|
|||
- `EVENTS_VALIDATING`: a group that can validate callbacks (default to `"immortals"`).
|
||||
- `EVENTS_CALENDAR`: type of the calendar to be used (either `None`, `"standard"` or `"custom"`,
|
||||
default to `None`).
|
||||
3. Add the `@call` command.
|
||||
3. Add the `call` command.
|
||||
4. Inherit from the custom typeclasses of the in-game Python system.
|
||||
- `evennia.contrib.ingame_python.typeclasses.EventCharacter`: to replace `DefaultCharacter`.
|
||||
- `evennia.contrib.ingame_python.typeclasses.EventExit`: to replace `DefaultExit`.
|
||||
|
|
@ -89,7 +99,7 @@ that a 'callback' property is not defined. After performing step `1` the error w
|
|||
|
||||
To start the event script, you only need a single command, using `@py`.
|
||||
|
||||
@py evennia.create_script("evennia.contrib.ingame_python.scripts.EventHandler")
|
||||
py evennia.create_script("evennia.contrib.base_systems.ingame_python.scripts.EventHandler")
|
||||
|
||||
This command will create a global script (that is, a script independent from any object). This
|
||||
script will hold basic configuration, individual callbacks and so on. You may access it directly,
|
||||
|
|
@ -107,8 +117,7 @@ By default, callbacks can only be created by immortals: no one except the immort
|
|||
callbacks, and immortals don't need validation. It can easily be changed, either through settings
|
||||
or dynamically by changing permissions of users.
|
||||
|
||||
The events contrib adds three
|
||||
[permissions](https://github.com/evennia/evennia/wiki/Locks#permissions) in the settings. You can
|
||||
The ingame-python contrib adds three [permissions](Permissions)) in the settings. You can
|
||||
override them by changing the settings into your `server/conf/settings.py` file (see below for an
|
||||
example). The settings defined in the events contrib are:
|
||||
|
||||
|
|
@ -142,9 +151,7 @@ calendar you are using. By default, time-related events are disabled. You can
|
|||
`EVENTS_CALENDAR` to set it to:
|
||||
|
||||
- `"standard"`: the standard calendar, with standard days, months, years and so on.
|
||||
- `"custom"`: a custom calendar that will use the
|
||||
[custom_gametime](https://github.com/evennia/evennia/blob/master/evennia/contrib/custom_gametime.py)
|
||||
contrib to schedule events.
|
||||
- `"custom"`: a custom calendar that will use the `custom_gametime` contrib to schedule events.
|
||||
|
||||
This contrib defines two additional permissions that can be set on individual users:
|
||||
|
||||
|
|
@ -156,17 +163,17 @@ This contrib defines two additional permissions that can be set on individual us
|
|||
For instance, to give the right to edit callbacks without needing approval to the player 'kaldara',
|
||||
you might do something like:
|
||||
|
||||
@perm *kaldara = events_without_validation
|
||||
perm *kaldara = events_without_validation
|
||||
|
||||
To remove this same permission, just use the `/del` switch:
|
||||
|
||||
@perm/del *kaldara = events_without_validation
|
||||
perm/del *kaldara = events_without_validation
|
||||
|
||||
The rights to use the `@call` command are directly related to these permissions: by default, only
|
||||
The rights to use the `call` command are directly related to these permissions: by default, only
|
||||
users who have the `events_without_validation` permission or are in (or above) the group defined in
|
||||
the `EVENTS_WITH_VALIDATION` setting will be able to call the command (with different switches).
|
||||
|
||||
### Adding the `@call` command
|
||||
### Adding the `call` command
|
||||
|
||||
You also have to add the `@call` command to your Character CmdSet. This command allows your users
|
||||
to add, edit and delete callbacks in-game. In your `commands/default_cmdsets, it might look like
|
||||
|
|
@ -199,32 +206,34 @@ classes. For instance, in your `typeclasses/characters.py` module, you should c
|
|||
like this:
|
||||
|
||||
```python
|
||||
from evennia.contrib.ingame_python.typeclasses import EventCharacter
|
||||
from evennia.contrib.base_systems.ingame_python.typeclasses import EventCharacter
|
||||
|
||||
class Character(EventCharacter):
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
You should do the same thing for your rooms, exits and objects. Note that the in-game Python system works by
|
||||
overriding some hooks. Some of these features might not be accessible in your game if you don't
|
||||
call the parent methods when overriding hooks.
|
||||
You should do the same thing for your rooms, exits and objects. Note that the
|
||||
in-game Python system works by overriding some hooks. Some of these features
|
||||
might not be accessible in your game if you don't call the parent methods when
|
||||
overriding hooks.
|
||||
|
||||
## Using the `@call` command
|
||||
## Using the `call` command
|
||||
|
||||
The in-game Python system relies, to a great extent, on its `@call` command. Who can execute this command,
|
||||
and who can do what with it, will depend on your set of permissions.
|
||||
The in-game Python system relies, to a great extent, on its `call` command.
|
||||
Who can execute this command, and who can do what with it, will depend on your
|
||||
set of permissions.
|
||||
|
||||
The `@call` command allows to add, edit and delete callbacks on specific objects' events. The event
|
||||
The `call` command allows to add, edit and delete callbacks on specific objects' events. The event
|
||||
system can be used on most Evennia objects, mostly typeclassed objects (excluding players). The
|
||||
first argument of the `@call` command is the name of the object you want to edit. It can also be
|
||||
first argument of the `call` command is the name of the object you want to edit. It can also be
|
||||
used to know what events are available for this specific object.
|
||||
|
||||
### Examining callbacks and events
|
||||
|
||||
To see the events connected to an object, use the `@call` command and give the name or ID of the
|
||||
object to examine. For instance, @call here` to examine the events on your current location. Or
|
||||
`@call self` to see the events on yourself.
|
||||
To see the events connected to an object, use the `call` command and give the name or ID of the
|
||||
object to examine. For instance, `call here` to examine the events on your current location. Or
|
||||
`call self` to see the events on yourself.
|
||||
|
||||
This command will display a table, containing:
|
||||
|
||||
|
|
@ -233,7 +242,7 @@ This command will display a table, containing:
|
|||
second column.
|
||||
- A short help to tell you when the event is triggered in the third column.
|
||||
|
||||
If you execute `@call #1` for instance, you might see a table like this:
|
||||
If you execute `call #1` for instance, you might see a table like this:
|
||||
|
||||
```
|
||||
+------------------+---------+-----------------------------------------------+
|
||||
|
|
@ -292,7 +301,7 @@ If we want to prevent a character from traversing through this exit, the best ev
|
|||
|
||||
When we edit the event, we have some more information:
|
||||
|
||||
@call/add north = can_traverse
|
||||
call/add north = can_traverse
|
||||
|
||||
Can the character traverse through this exit?
|
||||
This event is called when a character is about to traverse this
|
||||
|
|
@ -308,7 +317,7 @@ Variables you can use in this event:
|
|||
The section dedicated to [eventfuncs](#the-eventfuncs) will elaborate on the `deny()` function and
|
||||
other eventfuncs. Let us say, for the time being, that it can prevent an action (in this case, it
|
||||
can prevent the character from traversing through this exit). In the editor that opened when you
|
||||
used `@call/add`, you can type something like:
|
||||
used `call/add`, you can type something like:
|
||||
|
||||
```python
|
||||
if character.id == 1:
|
||||
|
|
@ -320,11 +329,11 @@ else:
|
|||
|
||||
You can now enter `:wq` to leave the editor by saving the callback.
|
||||
|
||||
If you enter `@call north`, you should see that "can_traverse" now has an active callback. You can
|
||||
use `@call north = can_traverse` to see more details on the connected callbacks:
|
||||
If you enter `call north`, you should see that "can_traverse" now has an active callback. You can
|
||||
use `call north = can_traverse` to see more details on the connected callbacks:
|
||||
|
||||
```
|
||||
@call north = can_traverse
|
||||
call north = can_traverse
|
||||
+--------------+--------------+----------------+--------------+--------------+
|
||||
| Number | Author | Updated | Param | Valid |
|
||||
+~~~~~~~~~~~~~~+~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~+~~~~~~~~~~~~~~+
|
||||
|
|
@ -336,7 +345,7 @@ The left column contains callback numbers. You can use them to have even more i
|
|||
specific event. Here, for instance:
|
||||
|
||||
```
|
||||
@call north = can_traverse 1
|
||||
call north = can_traverse 1
|
||||
Callback can_traverse 1 of north:
|
||||
Created by XXXXX on 2017-04-02 17:58:05.
|
||||
Updated by XXXXX on 2017-04-02 18:02:50
|
||||
|
|
@ -360,11 +369,11 @@ the name of the object to edit and the equal sign:
|
|||
1. The name of the event (as seen above).
|
||||
2. A number, if several callbacks are connected at this location.
|
||||
|
||||
You can type `@call/edit <object> = <event name>` to see the callbacks that are linked at this
|
||||
You can type `call/edit <object> = <event name>` to see the callbacks that are linked at this
|
||||
location. If there is only one callback, it will be opened in the editor; if more are defined, you
|
||||
will be asked for a number to provide (for instance, `@call/edit north = can_traverse 2`).
|
||||
will be asked for a number to provide (for instance, `call/edit north = can_traverse 2`).
|
||||
|
||||
The command `@call` also provides a `/del` switch to remove a callback. It takes the same arguments
|
||||
The command `call` also provides a `/del` switch to remove a callback. It takes the same arguments
|
||||
as the `/edit` switch.
|
||||
|
||||
When removed, callbacks are logged, so an administrator can retrieve its content, assuming the
|
||||
|
|
@ -435,7 +444,7 @@ One example that will illustrate this system is the "msg_leave" event that can b
|
|||
This event can alter the message that will be sent to other characters when someone leaves through
|
||||
this exit.
|
||||
|
||||
@call/add down = msg_leave
|
||||
call/add down = msg_leave
|
||||
|
||||
Which should display:
|
||||
|
||||
|
|
@ -467,6 +476,7 @@ If you write something like this in your event:
|
|||
|
||||
```python
|
||||
message = "{character} falls into a hole in the ground!"
|
||||
|
||||
```
|
||||
|
||||
And if the character Wilfred takes this exit, others in the room will see:
|
||||
|
|
@ -488,18 +498,18 @@ For instance, let's say we want to create a cool voice-operated elevator. You e
|
|||
elevator and say the floor number... and the elevator moves in the right direction. In this case,
|
||||
we could create an callback with the parameter "one":
|
||||
|
||||
@call/add here = say one
|
||||
call/add here = say one
|
||||
|
||||
This callback will only fire when the user says a sentence that contains "one".
|
||||
|
||||
But what if we want to have a callback that would fire if the user says 1 or one? We can provide
|
||||
several parameters, separated by a comma.
|
||||
|
||||
@call/add here = say 1, one
|
||||
call/add here = say 1, one
|
||||
|
||||
Or, still more keywords:
|
||||
|
||||
@call/add here = say 1, one, ground
|
||||
call/add here = say 1, one, ground
|
||||
|
||||
This time, the user could say something like "take me to the ground floor" ("ground" is one of our
|
||||
keywords defined in the above callback).
|
||||
|
|
@ -524,11 +534,12 @@ a mandatory parameter, which is the time you expect this event to fire.
|
|||
For instance, let's add an event on this room that should trigger every day, at precisely 12:00 PM
|
||||
(the time is given as game time, not real time):
|
||||
|
||||
@call here = time 12:00
|
||||
call here = time 12:00
|
||||
|
||||
```python
|
||||
# This will be called every MUD day at 12:00 PM
|
||||
room.msg_contents("It's noon, time to have lunch!")
|
||||
|
||||
```
|
||||
|
||||
Now, at noon every MUD day, this event will fire and this callback will be executed. You can use
|
||||
|
|
@ -580,7 +591,7 @@ the next at regular times. Connecting exits (opening its doors), waiting a bit,
|
|||
rolling around and stopping at a different station. That's quite a complex set of callbacks, as it
|
||||
is, but let's only look at the part that opens and closes the doors:
|
||||
|
||||
@call/add here = time 10:00
|
||||
call/add here = time 10:00
|
||||
|
||||
```python
|
||||
# At 10:00 AM, the subway arrives in the room of ID 22.
|
||||
|
|
@ -617,7 +628,7 @@ This callback will:
|
|||
|
||||
And now, what should we have in "chain_1"?
|
||||
|
||||
@call/add here = chain_1
|
||||
call/add here = chain_1
|
||||
|
||||
```python
|
||||
# Close the doors
|
||||
|
|
@ -795,7 +806,7 @@ see a message about a "beautiful ant-hill".
|
|||
|
||||
### Adding new eventfuncs
|
||||
|
||||
Eventfuncs, like `deny(), are defined in `contrib/events/eventfuncs.py`. You can add your own
|
||||
Eventfuncs, like `deny()`, are defined in `contrib/events/eventfuncs.py`. You can add your own
|
||||
eventfuncs by creating a file named `eventfuncs.py` in your `world` directory. The functions
|
||||
defined in this file will be added as helpers.
|
||||
|
||||
|
|
@ -814,7 +825,7 @@ EVENTFUNCS_LOCATIONS = [
|
|||
|
||||
If you want to create events with parameters (if you create a "whisper" or "ask" command, for
|
||||
instance, and need to have some characters automatically react to words), you can set an additional
|
||||
argument in the tuple of events in your typeclass' ```_events``` class variable. This third argument
|
||||
argument in the tuple of events in your typeclass' `_events` class variable. This third argument
|
||||
must contain a callback that will be called to filter through the list of callbacks when the event
|
||||
fires. Two types of parameters are commonly used (but you can define more parameter types, although
|
||||
this is out of the scope of this documentation).
|
||||
|
|
@ -825,8 +836,9 @@ this is out of the scope of this documentation).
|
|||
The "say" command uses phrase parameters (you can set a "say" callback to fires if a phrase
|
||||
contains one specific word).
|
||||
|
||||
In both cases, you need to import a function from `evennia.contrib.ingame_python.utils` and use it as third
|
||||
parameter in your event definition.
|
||||
In both cases, you need to import a function from
|
||||
`evennia.contrib.base_systems.ingame_python.utils` and use it as third parameter in your
|
||||
event definition.
|
||||
|
||||
- `keyword_event` should be used for keyword parameters.
|
||||
- `phrase_event` should be used for phrase parameters.
|
||||
|
|
@ -834,7 +846,7 @@ parameter in your event definition.
|
|||
For example, here is the definition of the "say" event:
|
||||
|
||||
```python
|
||||
from evennia.contrib.ingame_python.utils import register_events, phrase_event
|
||||
from evennia.contrib.base_systems.ingame_python.utils import register_events, phrase_event
|
||||
# ...
|
||||
@register_events
|
||||
class SomeTypeclass:
|
||||
|
|
@ -865,5 +877,5 @@ The best way to do this is to use a custom setting, in your setting file
|
|||
EVENTS_DISABLED = True
|
||||
```
|
||||
|
||||
The in-game Python system will still be accessible (you will have access to the `@call` command, to debug),
|
||||
The in-game Python system will still be accessible (you will have access to the `call` command, to debug),
|
||||
but no event will be called automatically.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
"""
|
||||
In-game Python - vlgeoff 2017
|
||||
|
||||
"""
|
||||
|
||||
from . import callbackhandler
|
||||
from . import commands
|
||||
from . import eventfuncs
|
||||
from . import scripts
|
||||
from . import tests
|
||||
from . import typeclasses
|
||||
from . import utils
|
||||
|
|
@ -5,12 +5,11 @@ Module containing the commands of the in-game Python system.
|
|||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import Command
|
||||
from evennia.utils.ansi import raw
|
||||
from evennia.utils.eveditor import EvEditor
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.utils import class_from_module, time_format
|
||||
from evennia.contrib.ingame_python.utils import get_event_handler
|
||||
from evennia.contrib.base_systems.ingame_python.utils import get_event_handler
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ Eventfuncs are just Python functions that can be used inside of calllbacks.
|
|||
|
||||
"""
|
||||
|
||||
from evennia import ObjectDB, ScriptDB
|
||||
from evennia.contrib.ingame_python.utils import InterruptEvent
|
||||
from evennia import ObjectDB
|
||||
from evennia.contrib.base_systems.ingame_python.utils import InterruptEvent
|
||||
|
||||
|
||||
def deny():
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import traceback
|
|||
|
||||
from django.conf import settings
|
||||
from evennia import DefaultObject, DefaultScript, ChannelDB, ScriptDB
|
||||
from evennia import logger, ObjectDB
|
||||
from evennia import logger
|
||||
from evennia.utils.ansi import raw
|
||||
from evennia.utils.create import create_channel
|
||||
from evennia.utils.dbserialize import dbserialize
|
||||
from evennia.utils.utils import all_from_module, delay, pypath_to_realpath
|
||||
from evennia.contrib.ingame_python.callbackhandler import CallbackHandler
|
||||
from evennia.contrib.ingame_python.utils import get_next_wait, EVENTS, InterruptEvent
|
||||
from evennia.contrib.base_systems.ingame_python.callbackhandler import CallbackHandler
|
||||
from evennia.contrib.base_systems.ingame_python.utils import get_next_wait, EVENTS, InterruptEvent
|
||||
|
||||
# Constants
|
||||
RE_LINE_ERROR = re.compile(r'^ File "\<string\>", line (\d+)')
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ from evennia.objects.objects import ExitCommand
|
|||
from evennia.utils import ansi, utils
|
||||
from evennia.utils.create import create_object, create_script
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.contrib.ingame_python.commands import CmdCallback
|
||||
from evennia.contrib.ingame_python.callbackhandler import CallbackHandler
|
||||
from evennia.contrib.base_systems.ingame_python.commands import CmdCallback
|
||||
from evennia.contrib.base_systems.ingame_python.callbackhandler import CallbackHandler
|
||||
|
||||
# Force settings
|
||||
settings.EVENTS_CALENDAR = "standard"
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ default ones in evennia core.
|
|||
from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
|
||||
from evennia import ScriptDB
|
||||
from evennia.utils.utils import delay, inherits_from, lazy_property
|
||||
from evennia.contrib.ingame_python.callbackhandler import CallbackHandler
|
||||
from evennia.contrib.ingame_python.utils import register_events, time_event, phrase_event
|
||||
from evennia.contrib.base_systems.ingame_python.callbackhandler import CallbackHandler
|
||||
from evennia.contrib.base_systems.ingame_python.utils import register_events, time_event, phrase_event
|
||||
|
||||
# Character help
|
||||
CHARACTER_CAN_DELETE = """
|
||||
|
|
|
|||
|
|
@ -5,17 +5,15 @@ These functions are to be used by developers to customize events and callbacks.
|
|||
|
||||
"""
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import logger
|
||||
from evennia import ScriptDB
|
||||
from evennia.utils.create import create_script
|
||||
from evennia.utils.gametime import real_seconds_until as standard_rsu
|
||||
from evennia.utils.utils import class_from_module
|
||||
from evennia.contrib.custom_gametime import UNITS
|
||||
from evennia.contrib.custom_gametime import gametime_to_realtime
|
||||
from evennia.contrib.custom_gametime import real_seconds_until as custom_rsu
|
||||
from evennia.contrib.base_systems.custom_gametime import UNITS
|
||||
from evennia.contrib.base_systems.custom_gametime import gametime_to_realtime
|
||||
from evennia.contrib.base_systems.custom_gametime import real_seconds_until as custom_rsu
|
||||
|
||||
# Temporary storage for events waiting for the script to be started
|
||||
EVENTS = []
|
||||
|
|
|
|||
22
evennia/contrib/base_systems/menu_login/README.md
Normal file
22
evennia/contrib/base_systems/menu_login/README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Menu-based login system
|
||||
|
||||
Contribution - Vincent-lg 2016, Griatch 2019 (rework for modern EvMenu)
|
||||
|
||||
This changes the Evennia login to ask for the account name and password in
|
||||
sequence instead of requiring you to enter both at once. It uses EvMenu under
|
||||
the hood.
|
||||
|
||||
## Installation
|
||||
|
||||
To install, add this to `mygame/server/conf/settings.py`:
|
||||
|
||||
CMDSET_UNLOGGEDIN = "evennia.contrib.base_systems.menu_login.UnloggedinCmdSet"
|
||||
CONNECTION_SCREEN_MODULE = "contrib.base_systems.menu_login.connection_screens"
|
||||
|
||||
Reload the server and reconnect to see the changes.
|
||||
|
||||
## Notes
|
||||
|
||||
If you want to modify the way the connection screen looks, point
|
||||
`CONNECTION_SCREEN_MODULE` to your own module. Use the default as a
|
||||
guide (see also Evennia docs).
|
||||
7
evennia/contrib/base_systems/menu_login/__init__.py
Normal file
7
evennia/contrib/base_systems/menu_login/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Menu-login - Vinvent-lg 2016, Griatch 2019
|
||||
|
||||
"""
|
||||
|
||||
from .menu_login import UnloggedinCmdSet # noqa
|
||||
from .menu_login import connection_screens # noqa
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Connection screen
|
||||
|
||||
This is the text to show the user when they first connect to the game (before
|
||||
they log in).
|
||||
|
||||
To change the login screen in this module, do one of the following:
|
||||
|
||||
- Define a function `connection_screen()`, taking no arguments. This will be
|
||||
called first and must return the full string to act as the connection screen.
|
||||
This can be used to produce more dynamic screens.
|
||||
- Alternatively, define a string variable in the outermost scope of this module
|
||||
with the connection string that should be displayed. If more than one such
|
||||
variable is given, Evennia will pick one of them at random.
|
||||
|
||||
The commands available to the user when the connection screen is shown
|
||||
are defined in evennia.default_cmds.UnloggedinCmdSet. The parsing and display
|
||||
of the screen is done by the unlogged-in "look" command.
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import utils
|
||||
|
||||
CONNECTION_SCREEN = """
|
||||
|b==============================================================|n
|
||||
Welcome to |g{}|n, version {}!
|
||||
|
||||
Enter |wh|nelp for more info. |wlook|n will re-show this screen.
|
||||
|b==============================================================|n""".format(
|
||||
settings.SERVERNAME, utils.get_evennia_version("short")
|
||||
)
|
||||
|
|
@ -4,17 +4,17 @@ A login menu using EvMenu.
|
|||
Contribution - Vincent-lg 2016, Griatch 2019 (rework for modern EvMenu)
|
||||
|
||||
This changes the Evennia login to ask for the account name and password in
|
||||
sequence instead of requiring you to enter both at once.
|
||||
sequence instead of requiring you to enter both at once.
|
||||
|
||||
To install, add this line to the settings file (`mygame/server/conf/settings.py`):
|
||||
|
||||
CMDSET_UNLOGGEDIN = "evennia.contrib.menu_login.UnloggedinCmdSet"
|
||||
CMDSET_UNLOGGEDIN = "evennia.base_systems.contrib.menu_login.UnloggedinCmdSet"
|
||||
|
||||
Reload the server and the new connection method will be active. Note that you must
|
||||
independently change the connection screen to match this login style, by editing
|
||||
independently change the connection screen to match this login style, by editing
|
||||
`mygame/server/conf/connection_screens.py`.
|
||||
|
||||
This uses Evennia's menu system EvMenu and is triggered by a command that is
|
||||
This uses Evennia's menu system EvMenu and is triggered by a command that is
|
||||
called automatically when a new user connects.
|
||||
|
||||
"""
|
||||
|
|
@ -32,8 +32,7 @@ _ACCOUNT = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
|||
_GUEST = class_from_module(settings.BASE_GUEST_TYPECLASS)
|
||||
|
||||
_ACCOUNT_HELP = (
|
||||
"Enter the name you used to log into the game before, " "or a new account-name if you are new."
|
||||
)
|
||||
"Enter a new or existing login name.")
|
||||
_PASSWORD_HELP = (
|
||||
"Password should be a minimum of 8 characters (preferably longer) and "
|
||||
"can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."
|
||||
42
evennia/contrib/base_systems/mux_comms_cmds/README.md
Normal file
42
evennia/contrib/base_systems/mux_comms_cmds/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Legacy Comms-commands
|
||||
|
||||
Contribution - Griatch 2021
|
||||
|
||||
In Evennia 1.0, the old Channel commands (originally inspired by MUX) were
|
||||
replaced by the single `channel` command that performs all these function.
|
||||
That command is still required to talk on channels. This contrib (extracted
|
||||
from Evennia 0.9.5) reuses the channel-management of the base Channel command
|
||||
but breaks out its functionality into separate Commands with MUX-familiar names.
|
||||
|
||||
- `allcom` - `channel/all` and `channel`
|
||||
- `addcom` - `channel/alias`, `channel/sub` and `channel/unmute`
|
||||
- `delcom` - `channel/unalias`, `alias/unsub` and `channel/mute`
|
||||
- `cboot` - `channel/boot` (`channel/ban` and `/unban` not supported)
|
||||
- `cwho` - `channel/who`
|
||||
- `ccreate` - `channel/create`
|
||||
- `cdestroy` - `channel/destroy`
|
||||
- `clock` - `channel/lock`
|
||||
- `cdesc` - `channel/desc`
|
||||
|
||||
## Installation
|
||||
|
||||
- Import the `CmdSetLegacyComms` cmdset from this module into `mygame/commands/default_cmdsets.py`
|
||||
- Add it to the CharacterCmdSet's `at_cmdset_creation` method (see below).
|
||||
- Reload the server.
|
||||
|
||||
```python
|
||||
# in mygame/commands/default_cmdsets.py
|
||||
|
||||
# ..
|
||||
from evennia.contrib.base_systems.mux_comms_cmds import CmdSetLegacyComms # <----
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
# ...
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(CmdSetLegacyComms) # <----
|
||||
|
||||
```
|
||||
|
||||
Note that you will still be able to use the `channel` command; this is actually
|
||||
still used under the hood by these commands.
|
||||
6
evennia/contrib/base_systems/mux_comms_cmds/__init__.py
Normal file
6
evennia/contrib/base_systems/mux_comms_cmds/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
Mux-style comms commands - Griatch 2021
|
||||
|
||||
"""
|
||||
|
||||
from .mux_comms_cmds import CmdSetLegacyComms # noqa
|
||||
|
|
@ -30,8 +30,8 @@ Example:
|
|||
```python
|
||||
# in mygame/commands/default_cmdsets.py
|
||||
|
||||
# ...
|
||||
from evennia.contrib.legacy_comms import CmdSetLegacyComms # <----
|
||||
# ..
|
||||
from evennia.contrib.base_systems.mux_comms_cmds import CmdSetLegacyComms # <----
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
# ...
|
||||
|
|
@ -1,814 +0,0 @@
|
|||
"""
|
||||
Puzzles System - Provides a typeclass and commands for
|
||||
objects that can be combined (i.e. 'use'd) to produce
|
||||
new objects.
|
||||
|
||||
Evennia contribution - Henddher 2018
|
||||
|
||||
A Puzzle is a recipe of what objects (aka parts) must
|
||||
be combined by a player so a new set of objects
|
||||
(aka results) are automatically created.
|
||||
|
||||
Consider this simple Puzzle:
|
||||
|
||||
orange, mango, yogurt, blender = fruit smoothie
|
||||
|
||||
As a Builder:
|
||||
|
||||
@create/drop orange
|
||||
@create/drop mango
|
||||
@create/drop yogurt
|
||||
@create/drop blender
|
||||
@create/drop fruit smoothie
|
||||
|
||||
@puzzle smoothie, orange, mango, yogurt, blender = fruit smoothie
|
||||
...
|
||||
Puzzle smoothie(#1234) created successfuly.
|
||||
|
||||
@destroy/force orange, mango, yogurt, blender, fruit smoothie
|
||||
|
||||
@armpuzzle #1234
|
||||
Part orange is spawned at ...
|
||||
Part mango is spawned at ...
|
||||
....
|
||||
Puzzle smoothie(#1234) has been armed successfully
|
||||
|
||||
As Player:
|
||||
|
||||
use orange, mango, yogurt, blender
|
||||
...
|
||||
Genius, you blended all fruits to create a fruit smoothie!
|
||||
|
||||
Details:
|
||||
|
||||
Puzzles are created from existing objects. The given
|
||||
objects are introspected to create prototypes for the
|
||||
puzzle parts and results. These prototypes become the
|
||||
puzzle recipe. (See PuzzleRecipe and @puzzle
|
||||
command). Once the recipe is created, all parts and result
|
||||
can be disposed (i.e. destroyed).
|
||||
|
||||
At a later time, a Builder or a Script can arm the puzzle
|
||||
and spawn all puzzle parts in their respective
|
||||
locations (See @armpuzzle).
|
||||
|
||||
A regular player can collect the puzzle parts and combine
|
||||
them (See use command). If player has specified
|
||||
all pieces, the puzzle is considered solved and all
|
||||
its puzzle parts are destroyed while the puzzle results
|
||||
are spawened on their corresponding location.
|
||||
|
||||
Installation:
|
||||
|
||||
Add the PuzzleSystemCmdSet to all players.
|
||||
Alternatively:
|
||||
|
||||
@py self.cmdset.add('evennia.contrib.puzzles.PuzzleSystemCmdSet')
|
||||
|
||||
"""
|
||||
|
||||
import itertools
|
||||
from random import choice
|
||||
from evennia import create_script
|
||||
from evennia import CmdSet
|
||||
from evennia import DefaultScript
|
||||
from evennia import DefaultCharacter
|
||||
from evennia import DefaultRoom
|
||||
from evennia import DefaultExit
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.utils.utils import inherits_from
|
||||
from evennia.utils import search, utils, logger
|
||||
from evennia.prototypes.spawner import spawn
|
||||
|
||||
# Tag used by puzzles
|
||||
_PUZZLES_TAG_CATEGORY = "puzzles"
|
||||
_PUZZLES_TAG_RECIPE = "puzzle_recipe"
|
||||
# puzzle part and puzzle result
|
||||
_PUZZLES_TAG_MEMBER = "puzzle_member"
|
||||
|
||||
_PUZZLE_DEFAULT_FAIL_USE_MESSAGE = "You try to utilize %s but nothing happens ... something amiss?"
|
||||
_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = "You are a Genius!!!"
|
||||
_PUZZLE_DEFAULT_SUCCESS_USE_LOCATION_MESSAGE = "|c{caller}|n performs some kind of tribal dance and |y{result_names}|n seems to appear from thin air"
|
||||
|
||||
# ----------- UTILITY FUNCTIONS ------------
|
||||
|
||||
|
||||
def proto_def(obj, with_tags=True):
|
||||
"""
|
||||
Basic properties needed to spawn
|
||||
and compare recipe with candidate part
|
||||
"""
|
||||
protodef = {
|
||||
# TODO: Don't we need to honor ALL properties? attributes, contents, etc.
|
||||
"prototype_key": "%s(%s)" % (obj.key, obj.dbref),
|
||||
"key": obj.key,
|
||||
"typeclass": obj.typeclass_path,
|
||||
"desc": obj.db.desc,
|
||||
"location": obj.location,
|
||||
"home": obj.home,
|
||||
"locks": ";".join(obj.locks.all()),
|
||||
"permissions": obj.permissions.all()[:],
|
||||
}
|
||||
if with_tags:
|
||||
tags = obj.tags.all(return_key_and_category=True)
|
||||
tags = [(t[0], t[1], None) for t in tags]
|
||||
tags.append((_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY, None))
|
||||
protodef["tags"] = tags
|
||||
return protodef
|
||||
|
||||
|
||||
def maskout_protodef(protodef, mask):
|
||||
"""
|
||||
Returns a new protodef after removing protodef values based on mask
|
||||
"""
|
||||
protodef = dict(protodef)
|
||||
for m in mask:
|
||||
if m in protodef:
|
||||
protodef.pop(m)
|
||||
return protodef
|
||||
|
||||
|
||||
# Colorize the default success message
|
||||
def _colorize_message(msg):
|
||||
_i = 0
|
||||
_colors = ["|r", "|g", "|y"]
|
||||
_msg = []
|
||||
for l in msg:
|
||||
_msg += _colors[_i] + l
|
||||
_i = (_i + 1) % len(_colors)
|
||||
msg = "".join(_msg) + "|n"
|
||||
return msg
|
||||
|
||||
|
||||
_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = _colorize_message(_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE)
|
||||
|
||||
# ------------------------------------------
|
||||
|
||||
|
||||
class PuzzleRecipe(DefaultScript):
|
||||
"""
|
||||
Definition of a Puzzle Recipe
|
||||
"""
|
||||
|
||||
def save_recipe(self, puzzle_name, parts, results):
|
||||
self.db.puzzle_name = str(puzzle_name)
|
||||
self.db.parts = tuple(parts)
|
||||
self.db.results = tuple(results)
|
||||
self.db.mask = tuple()
|
||||
self.tags.add(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY)
|
||||
self.db.use_success_message = _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE
|
||||
self.db.use_success_location_message = _PUZZLE_DEFAULT_SUCCESS_USE_LOCATION_MESSAGE
|
||||
|
||||
|
||||
class CmdCreatePuzzleRecipe(MuxCommand):
|
||||
"""
|
||||
Creates a puzzle recipe. A puzzle consists of puzzle-parts that
|
||||
the player can 'use' together to create a specified result.
|
||||
|
||||
Usage:
|
||||
@puzzle name,<part1[,part2,...>] = <result1[,result2,...]>
|
||||
|
||||
Example:
|
||||
create/drop balloon
|
||||
create/drop glass of water
|
||||
create/drop water balloon
|
||||
@puzzle waterballon,balloon,glass of water = water balloon
|
||||
@del ballon, glass of water, water balloon
|
||||
@armpuzzle #1
|
||||
|
||||
Notes:
|
||||
Each part and result are objects that must (temporarily) exist and be placed in their
|
||||
corresponding location in order to create the puzzle. After the creation of the puzzle,
|
||||
these objects are not needed anymore and can be deleted. Components of the puzzle
|
||||
will be re-created by use of the `@armpuzzle` command later.
|
||||
|
||||
"""
|
||||
|
||||
key = "@puzzle"
|
||||
aliases = "@puzzlerecipe"
|
||||
locks = "cmd:perm(puzzle) or perm(Builder)"
|
||||
help_category = "Puzzles"
|
||||
|
||||
confirm = True
|
||||
default_confirm = "no"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
|
||||
if len(self.lhslist) < 2 or not self.rhs:
|
||||
string = "Usage: @puzzle name,<part1[,...]> = <result1[,...]>"
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
puzzle_name = self.lhslist[0]
|
||||
if len(puzzle_name) == 0:
|
||||
caller.msg("Invalid puzzle name %r." % puzzle_name)
|
||||
return
|
||||
|
||||
# if there is another puzzle with same name
|
||||
# warn user that parts and results will be
|
||||
# interchangable
|
||||
_puzzles = search.search_script_attribute(key="puzzle_name", value=puzzle_name)
|
||||
_puzzles = list(filter(lambda p: isinstance(p, PuzzleRecipe), _puzzles))
|
||||
if _puzzles:
|
||||
confirm = (
|
||||
"There are %d puzzles with the same name.\n" % len(_puzzles)
|
||||
+ "Its parts and results will be interchangeable.\n"
|
||||
+ "Continue yes/[no]? "
|
||||
)
|
||||
answer = ""
|
||||
while answer.strip().lower() not in ("y", "yes", "n", "no"):
|
||||
answer = yield (confirm)
|
||||
answer = self.default_confirm if answer == "" else answer
|
||||
if answer.strip().lower() in ("n", "no"):
|
||||
caller.msg("Cancelled: no puzzle created.")
|
||||
return
|
||||
|
||||
def is_valid_obj_location(obj):
|
||||
valid = True
|
||||
# Rooms are the only valid locations.
|
||||
# TODO: other valid locations could be added here.
|
||||
# Certain locations can be handled accordingly: e.g,
|
||||
# a part is located in a character's inventory,
|
||||
# perhaps will translate into the player character
|
||||
# having the part in his/her inventory while being
|
||||
# located in the same room where the builder was
|
||||
# located.
|
||||
# Parts and results may have different valid locations
|
||||
if not inherits_from(obj.location, DefaultRoom):
|
||||
caller.msg("Invalid location for %s" % (obj.key))
|
||||
valid = False
|
||||
return valid
|
||||
|
||||
def is_valid_part_location(part):
|
||||
return is_valid_obj_location(part)
|
||||
|
||||
def is_valid_result_location(part):
|
||||
return is_valid_obj_location(part)
|
||||
|
||||
def is_valid_inheritance(obj):
|
||||
valid = (
|
||||
not inherits_from(obj, DefaultCharacter)
|
||||
and not inherits_from(obj, DefaultRoom)
|
||||
and not inherits_from(obj, DefaultExit)
|
||||
)
|
||||
if not valid:
|
||||
caller.msg("Invalid typeclass for %s" % (obj))
|
||||
return valid
|
||||
|
||||
def is_valid_part(part):
|
||||
return is_valid_inheritance(part) and is_valid_part_location(part)
|
||||
|
||||
def is_valid_result(result):
|
||||
return is_valid_inheritance(result) and is_valid_result_location(result)
|
||||
|
||||
parts = []
|
||||
for objname in self.lhslist[1:]:
|
||||
obj = caller.search(objname)
|
||||
if not obj:
|
||||
return
|
||||
if not is_valid_part(obj):
|
||||
return
|
||||
parts.append(obj)
|
||||
|
||||
results = []
|
||||
for objname in self.rhslist:
|
||||
obj = caller.search(objname)
|
||||
if not obj:
|
||||
return
|
||||
if not is_valid_result(obj):
|
||||
return
|
||||
results.append(obj)
|
||||
|
||||
for part in parts:
|
||||
caller.msg("Part %s(%s)" % (part.name, part.dbref))
|
||||
|
||||
for result in results:
|
||||
caller.msg("Result %s(%s)" % (result.name, result.dbref))
|
||||
|
||||
proto_parts = [proto_def(obj) for obj in parts]
|
||||
proto_results = [proto_def(obj) for obj in results]
|
||||
|
||||
puzzle = create_script(PuzzleRecipe, key=puzzle_name, persistent=True)
|
||||
puzzle.save_recipe(puzzle_name, proto_parts, proto_results)
|
||||
puzzle.locks.add("control:id(%s) or perm(Builder)" % caller.dbref[1:])
|
||||
|
||||
caller.msg(
|
||||
"Puzzle |y'%s' |w%s(%s)|n has been created |gsuccessfully|n."
|
||||
% (puzzle.db.puzzle_name, puzzle.name, puzzle.dbref)
|
||||
)
|
||||
|
||||
caller.msg(
|
||||
"You may now dispose of all parts and results. \n"
|
||||
"Use @puzzleedit #{dbref} to customize this puzzle further. \n"
|
||||
"Use @armpuzzle #{dbref} to arm a new puzzle instance.".format(dbref=puzzle.dbref)
|
||||
)
|
||||
|
||||
|
||||
class CmdEditPuzzle(MuxCommand):
|
||||
"""
|
||||
Edits puzzle properties
|
||||
|
||||
Usage:
|
||||
@puzzleedit[/delete] <#dbref>
|
||||
@puzzleedit <#dbref>/use_success_message = <Custom message>
|
||||
@puzzleedit <#dbref>/use_success_location_message = <Custom message from {caller} producing {result_names}>
|
||||
@puzzleedit <#dbref>/mask = attr1[,attr2,...]>
|
||||
@puzzleedit[/addpart] <#dbref> = <obj[,obj2,...]>
|
||||
@puzzleedit[/delpart] <#dbref> = <obj[,obj2,...]>
|
||||
@puzzleedit[/addresult] <#dbref> = <obj[,obj2,...]>
|
||||
@puzzleedit[/delresult] <#dbref> = <obj[,obj2,...]>
|
||||
|
||||
Switches:
|
||||
addpart - adds parts to the puzzle
|
||||
delpart - removes parts from the puzzle
|
||||
addresult - adds results to the puzzle
|
||||
delresult - removes results from the puzzle
|
||||
delete - deletes the recipe. Existing parts and results aren't modified
|
||||
|
||||
mask - attributes to exclude during matching (e.g. location, desc, etc.)
|
||||
use_success_location_message containing {result_names} and {caller} will
|
||||
automatically be replaced with correct values. Both are optional.
|
||||
|
||||
When removing parts/results, it's possible to remove all.
|
||||
|
||||
"""
|
||||
|
||||
key = "@puzzleedit"
|
||||
locks = "cmd:perm(puzzleedit) or perm(Builder)"
|
||||
help_category = "Puzzles"
|
||||
|
||||
def func(self):
|
||||
self._USAGE = "Usage: @puzzleedit[/switches] <dbref>[/attribute = <value>]"
|
||||
caller = self.caller
|
||||
|
||||
if not self.lhslist:
|
||||
caller.msg(self._USAGE)
|
||||
return
|
||||
|
||||
if "/" in self.lhslist[0]:
|
||||
recipe_dbref, attr = self.lhslist[0].split("/")
|
||||
else:
|
||||
recipe_dbref = self.lhslist[0]
|
||||
|
||||
if not utils.dbref(recipe_dbref):
|
||||
caller.msg("A puzzle recipe's #dbref must be specified.\n" + self._USAGE)
|
||||
return
|
||||
|
||||
puzzle = search.search_script(recipe_dbref)
|
||||
if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe):
|
||||
caller.msg("%s(%s) is not a puzzle" % (puzzle[0].name, recipe_dbref))
|
||||
return
|
||||
|
||||
puzzle = puzzle[0]
|
||||
puzzle_name_id = "%s(%s)" % (puzzle.name, puzzle.dbref)
|
||||
|
||||
if "delete" in self.switches:
|
||||
if not (puzzle.access(caller, "control") or puzzle.access(caller, "delete")):
|
||||
caller.msg("You don't have permission to delete %s." % puzzle_name_id)
|
||||
return
|
||||
|
||||
puzzle.delete()
|
||||
caller.msg("%s was deleted" % puzzle_name_id)
|
||||
return
|
||||
|
||||
elif "addpart" in self.switches:
|
||||
objs = self._get_objs()
|
||||
if objs:
|
||||
added = self._add_parts(objs, puzzle)
|
||||
caller.msg("%s were added to parts" % (", ".join(added)))
|
||||
return
|
||||
|
||||
elif "delpart" in self.switches:
|
||||
objs = self._get_objs()
|
||||
if objs:
|
||||
removed = self._remove_parts(objs, puzzle)
|
||||
caller.msg("%s were removed from parts" % (", ".join(removed)))
|
||||
return
|
||||
|
||||
elif "addresult" in self.switches:
|
||||
objs = self._get_objs()
|
||||
if objs:
|
||||
added = self._add_results(objs, puzzle)
|
||||
caller.msg("%s were added to results" % (", ".join(added)))
|
||||
return
|
||||
|
||||
elif "delresult" in self.switches:
|
||||
objs = self._get_objs()
|
||||
if objs:
|
||||
removed = self._remove_results(objs, puzzle)
|
||||
caller.msg("%s were removed from results" % (", ".join(removed)))
|
||||
return
|
||||
|
||||
else:
|
||||
# edit attributes
|
||||
|
||||
if not (puzzle.access(caller, "control") or puzzle.access(caller, "edit")):
|
||||
caller.msg("You don't have permission to edit %s." % puzzle_name_id)
|
||||
return
|
||||
|
||||
if attr == "use_success_message":
|
||||
puzzle.db.use_success_message = self.rhs
|
||||
caller.msg(
|
||||
"%s use_success_message = %s\n"
|
||||
% (puzzle_name_id, puzzle.db.use_success_message)
|
||||
)
|
||||
return
|
||||
elif attr == "use_success_location_message":
|
||||
puzzle.db.use_success_location_message = self.rhs
|
||||
caller.msg(
|
||||
"%s use_success_location_message = %s\n"
|
||||
% (puzzle_name_id, puzzle.db.use_success_location_message)
|
||||
)
|
||||
return
|
||||
elif attr == "mask":
|
||||
puzzle.db.mask = tuple(self.rhslist)
|
||||
caller.msg("%s mask = %r\n" % (puzzle_name_id, puzzle.db.mask))
|
||||
return
|
||||
|
||||
def _get_objs(self):
|
||||
if not self.rhslist:
|
||||
self.caller.msg(self._USAGE)
|
||||
return
|
||||
objs = []
|
||||
for o in self.rhslist:
|
||||
obj = self.caller.search(o)
|
||||
if obj:
|
||||
objs.append(obj)
|
||||
return objs
|
||||
|
||||
def _add_objs_to(self, objs, to):
|
||||
"""Adds propto objs to the given set (parts or results)"""
|
||||
added = []
|
||||
toobjs = list(to[:])
|
||||
for obj in objs:
|
||||
protoobj = proto_def(obj)
|
||||
toobjs.append(protoobj)
|
||||
added.append(obj.key)
|
||||
return added, toobjs
|
||||
|
||||
def _remove_objs_from(self, objs, frm):
|
||||
"""Removes propto objs from the given set (parts or results)"""
|
||||
removed = []
|
||||
fromobjs = list(frm[:])
|
||||
for obj in objs:
|
||||
protoobj = proto_def(obj)
|
||||
if protoobj in fromobjs:
|
||||
fromobjs.remove(protoobj)
|
||||
removed.append(obj.key)
|
||||
return removed, fromobjs
|
||||
|
||||
def _add_parts(self, objs, puzzle):
|
||||
added, toobjs = self._add_objs_to(objs, puzzle.db.parts)
|
||||
puzzle.db.parts = tuple(toobjs)
|
||||
return added
|
||||
|
||||
def _remove_parts(self, objs, puzzle):
|
||||
removed, fromobjs = self._remove_objs_from(objs, puzzle.db.parts)
|
||||
puzzle.db.parts = tuple(fromobjs)
|
||||
return removed
|
||||
|
||||
def _add_results(self, objs, puzzle):
|
||||
added, toobjs = self._add_objs_to(objs, puzzle.db.results)
|
||||
puzzle.db.results = tuple(toobjs)
|
||||
return added
|
||||
|
||||
def _remove_results(self, objs, puzzle):
|
||||
removed, fromobjs = self._remove_objs_from(objs, puzzle.db.results)
|
||||
puzzle.db.results = tuple(fromobjs)
|
||||
return removed
|
||||
|
||||
|
||||
class CmdArmPuzzle(MuxCommand):
|
||||
"""
|
||||
Arms a puzzle by spawning all its parts.
|
||||
|
||||
Usage:
|
||||
@armpuzzle <puzzle #dbref>
|
||||
|
||||
Notes:
|
||||
Create puzzles with `@puzzle`; get list of
|
||||
defined puzzles using `@lspuzzlerecipes`.
|
||||
|
||||
"""
|
||||
|
||||
key = "@armpuzzle"
|
||||
locks = "cmd:perm(armpuzzle) or perm(Builder)"
|
||||
help_category = "Puzzles"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
|
||||
if self.args is None or not utils.dbref(self.args):
|
||||
caller.msg("A puzzle recipe's #dbref must be specified")
|
||||
return
|
||||
|
||||
puzzle = search.search_script(self.args)
|
||||
if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe):
|
||||
caller.msg("Invalid puzzle %r" % (self.args))
|
||||
return
|
||||
|
||||
puzzle = puzzle[0]
|
||||
caller.msg(
|
||||
"Puzzle Recipe %s(%s) '%s' found.\nSpawning %d parts ..."
|
||||
% (puzzle.name, puzzle.dbref, puzzle.db.puzzle_name, len(puzzle.db.parts))
|
||||
)
|
||||
|
||||
for proto_part in puzzle.db.parts:
|
||||
part = spawn(proto_part)[0]
|
||||
caller.msg(
|
||||
"Part %s(%s) spawned and placed at %s(%s)"
|
||||
% (part.name, part.dbref, part.location, part.location.dbref)
|
||||
)
|
||||
part.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY)
|
||||
part.db.puzzle_name = puzzle.db.puzzle_name
|
||||
|
||||
caller.msg("Puzzle armed |gsuccessfully|n.")
|
||||
|
||||
|
||||
def _lookups_parts_puzzlenames_protodefs(parts):
|
||||
# Create lookup dicts by part's dbref and by puzzle_name(tags)
|
||||
parts_dict = dict()
|
||||
puzzlename_tags_dict = dict()
|
||||
puzzle_ingredients = dict()
|
||||
for part in parts:
|
||||
parts_dict[part.dbref] = part
|
||||
protodef = proto_def(part, with_tags=False)
|
||||
# remove 'prototype_key' as it will prevent equality
|
||||
del protodef["prototype_key"]
|
||||
puzzle_ingredients[part.dbref] = protodef
|
||||
tags_categories = part.tags.all(return_key_and_category=True)
|
||||
for tag, category in tags_categories:
|
||||
if category != _PUZZLES_TAG_CATEGORY:
|
||||
continue
|
||||
if tag not in puzzlename_tags_dict:
|
||||
puzzlename_tags_dict[tag] = []
|
||||
puzzlename_tags_dict[tag].append(part.dbref)
|
||||
return parts_dict, puzzlename_tags_dict, puzzle_ingredients
|
||||
|
||||
|
||||
def _puzzles_by_names(names):
|
||||
# Find all puzzles by puzzle name (i.e. tag name)
|
||||
puzzles = []
|
||||
for puzzle_name in names:
|
||||
_puzzles = search.search_script_attribute(key="puzzle_name", value=puzzle_name)
|
||||
_puzzles = list(filter(lambda p: isinstance(p, PuzzleRecipe), _puzzles))
|
||||
if not _puzzles:
|
||||
continue
|
||||
else:
|
||||
puzzles.extend(_puzzles)
|
||||
return puzzles
|
||||
|
||||
|
||||
def _matching_puzzles(puzzles, puzzlename_tags_dict, puzzle_ingredients):
|
||||
# Check if parts can be combined to solve a puzzle
|
||||
matched_puzzles = dict()
|
||||
for puzzle in puzzles:
|
||||
puzzle_protoparts = list(puzzle.db.parts[:])
|
||||
puzzle_mask = puzzle.db.mask[:]
|
||||
# remove tags and prototype_key as they prevent equality
|
||||
for i, puzzle_protopart in enumerate(puzzle_protoparts[:]):
|
||||
del puzzle_protopart["tags"]
|
||||
del puzzle_protopart["prototype_key"]
|
||||
puzzle_protopart = maskout_protodef(puzzle_protopart, puzzle_mask)
|
||||
puzzle_protoparts[i] = puzzle_protopart
|
||||
|
||||
matched_dbrefparts = []
|
||||
parts_dbrefs = puzzlename_tags_dict[puzzle.db.puzzle_name]
|
||||
for part_dbref in parts_dbrefs:
|
||||
protopart = puzzle_ingredients[part_dbref]
|
||||
protopart = maskout_protodef(protopart, puzzle_mask)
|
||||
if protopart in puzzle_protoparts:
|
||||
puzzle_protoparts.remove(protopart)
|
||||
matched_dbrefparts.append(part_dbref)
|
||||
else:
|
||||
if len(puzzle_protoparts) == 0:
|
||||
matched_puzzles[puzzle.dbref] = matched_dbrefparts
|
||||
return matched_puzzles
|
||||
|
||||
|
||||
class CmdUsePuzzleParts(MuxCommand):
|
||||
"""
|
||||
Use an object, or a group of objects at once.
|
||||
|
||||
|
||||
Example:
|
||||
You look around you and see a pole, a long string, and a needle.
|
||||
|
||||
use pole, long string, needle
|
||||
|
||||
Genius! You built a fishing pole.
|
||||
|
||||
|
||||
Usage:
|
||||
use <obj1> [,obj2,...]
|
||||
"""
|
||||
|
||||
# Technical explanation
|
||||
"""
|
||||
Searches for all puzzles whose parts match the given set of objects. If there are matching
|
||||
puzzles, the result objects are spawned in their corresponding location if all parts have been
|
||||
passed in.
|
||||
"""
|
||||
|
||||
key = "use"
|
||||
aliases = "combine"
|
||||
locks = "cmd:pperm(use) or pperm(Player)"
|
||||
help_category = "Puzzles"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
|
||||
if not self.lhs:
|
||||
caller.msg("Use what?")
|
||||
return
|
||||
|
||||
many = "these" if len(self.lhslist) > 1 else "this"
|
||||
|
||||
# either all are parts, or abort finding matching puzzles
|
||||
parts = []
|
||||
partnames = self.lhslist[:]
|
||||
for partname in partnames:
|
||||
part = caller.search(
|
||||
partname,
|
||||
multimatch_string="Which %s. There are many.\n" % (partname),
|
||||
nofound_string="There is no %s around." % (partname),
|
||||
)
|
||||
|
||||
if not part:
|
||||
return
|
||||
|
||||
if not part.tags.get(_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY):
|
||||
|
||||
# not a puzzle part ... abort
|
||||
caller.msg("You have no idea how %s can be used" % (many))
|
||||
return
|
||||
|
||||
# a valid part
|
||||
parts.append(part)
|
||||
|
||||
# Create lookup dicts by part's dbref and by puzzle_name(tags)
|
||||
parts_dict, puzzlename_tags_dict, puzzle_ingredients = _lookups_parts_puzzlenames_protodefs(
|
||||
parts
|
||||
)
|
||||
|
||||
# Find all puzzles by puzzle name (i.e. tag name)
|
||||
puzzles = _puzzles_by_names(puzzlename_tags_dict.keys())
|
||||
|
||||
logger.log_info("PUZZLES %r" % ([(p.dbref, p.db.puzzle_name) for p in puzzles]))
|
||||
|
||||
# Create lookup dict of puzzles by dbref
|
||||
puzzles_dict = dict((puzzle.dbref, puzzle) for puzzle in puzzles)
|
||||
# Check if parts can be combined to solve a puzzle
|
||||
matched_puzzles = _matching_puzzles(puzzles, puzzlename_tags_dict, puzzle_ingredients)
|
||||
|
||||
if len(matched_puzzles) == 0:
|
||||
# TODO: we could use part.fail_message instead, if there was one
|
||||
# random part falls and lands on your feet
|
||||
# random part hits you square on the face
|
||||
caller.msg(_PUZZLE_DEFAULT_FAIL_USE_MESSAGE % (many))
|
||||
return
|
||||
|
||||
puzzletuples = sorted(matched_puzzles.items(), key=lambda t: len(t[1]), reverse=True)
|
||||
|
||||
logger.log_info("MATCHED PUZZLES %r" % (puzzletuples))
|
||||
|
||||
# sort all matched puzzles and pick largest one(s)
|
||||
puzzledbref, matched_dbrefparts = puzzletuples[0]
|
||||
nparts = len(matched_dbrefparts)
|
||||
puzzle = puzzles_dict[puzzledbref]
|
||||
largest_puzzles = list(itertools.takewhile(lambda t: len(t[1]) == nparts, puzzletuples))
|
||||
|
||||
# if there are more than one, choose one at random.
|
||||
# we could show the names of all those that can be resolved
|
||||
# but that would give away that there are other puzzles that
|
||||
# can be resolved with the same parts.
|
||||
# just hint how many.
|
||||
if len(largest_puzzles) > 1:
|
||||
caller.msg(
|
||||
"Your gears start turning and %d different ideas come to your mind ...\n"
|
||||
% (len(largest_puzzles))
|
||||
)
|
||||
puzzletuple = choice(largest_puzzles)
|
||||
puzzle = puzzles_dict[puzzletuple[0]]
|
||||
caller.msg("You try %s ..." % (puzzle.db.puzzle_name))
|
||||
|
||||
# got one, spawn its results
|
||||
result_names = []
|
||||
for proto_result in puzzle.db.results:
|
||||
result = spawn(proto_result)[0]
|
||||
result.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY)
|
||||
result.db.puzzle_name = puzzle.db.puzzle_name
|
||||
result_names.append(result.name)
|
||||
|
||||
# Destroy all parts used
|
||||
for dbref in matched_dbrefparts:
|
||||
parts_dict[dbref].delete()
|
||||
|
||||
result_names = ", ".join(result_names)
|
||||
caller.msg(puzzle.db.use_success_message)
|
||||
caller.location.msg_contents(
|
||||
puzzle.db.use_success_location_message.format(caller=caller, result_names=result_names),
|
||||
exclude=(caller,),
|
||||
)
|
||||
|
||||
|
||||
class CmdListPuzzleRecipes(MuxCommand):
|
||||
"""
|
||||
Searches for all puzzle recipes
|
||||
|
||||
Usage:
|
||||
@lspuzzlerecipes
|
||||
"""
|
||||
|
||||
key = "@lspuzzlerecipes"
|
||||
locks = "cmd:perm(lspuzzlerecipes) or perm(Builder)"
|
||||
help_category = "Puzzles"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
|
||||
recipes = search.search_script_tag(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY)
|
||||
|
||||
div = "-" * 60
|
||||
text = [div]
|
||||
msgf_recipe = "Puzzle |y'%s' %s(%s)|n"
|
||||
msgf_item = "%2s|c%15s|n: |w%s|n"
|
||||
for recipe in recipes:
|
||||
text.append(msgf_recipe % (recipe.db.puzzle_name, recipe.name, recipe.dbref))
|
||||
text.append("Success Caller message:\n" + recipe.db.use_success_message + "\n")
|
||||
text.append(
|
||||
"Success Location message:\n" + recipe.db.use_success_location_message + "\n"
|
||||
)
|
||||
text.append("Mask:\n" + str(recipe.db.mask) + "\n")
|
||||
text.append("Parts")
|
||||
for protopart in recipe.db.parts[:]:
|
||||
mark = "-"
|
||||
for k, v in protopart.items():
|
||||
text.append(msgf_item % (mark, k, v))
|
||||
mark = ""
|
||||
text.append("Results")
|
||||
for protoresult in recipe.db.results[:]:
|
||||
mark = "-"
|
||||
for k, v in protoresult.items():
|
||||
text.append(msgf_item % (mark, k, v))
|
||||
mark = ""
|
||||
else:
|
||||
text.append(div)
|
||||
text.append("Found |r%d|n puzzle(s)." % (len(recipes)))
|
||||
text.append(div)
|
||||
caller.msg("\n".join(text))
|
||||
|
||||
|
||||
class CmdListArmedPuzzles(MuxCommand):
|
||||
"""
|
||||
Searches for all armed puzzles
|
||||
|
||||
Usage:
|
||||
@lsarmedpuzzles
|
||||
"""
|
||||
|
||||
key = "@lsarmedpuzzles"
|
||||
locks = "cmd:perm(lsarmedpuzzles) or perm(Builder)"
|
||||
help_category = "Puzzles"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
|
||||
armed_puzzles = search.search_tag(_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY)
|
||||
|
||||
armed_puzzles = dict(
|
||||
(k, list(g)) for k, g in itertools.groupby(armed_puzzles, lambda ap: ap.db.puzzle_name)
|
||||
)
|
||||
|
||||
div = "-" * 60
|
||||
msgf_pznm = "Puzzle name: |y%s|n"
|
||||
msgf_item = "|m%25s|w(%s)|n at |c%25s|w(%s)|n"
|
||||
text = [div]
|
||||
for pzname, items in armed_puzzles.items():
|
||||
text.append(msgf_pznm % (pzname))
|
||||
for item in items:
|
||||
text.append(
|
||||
msgf_item % (item.name, item.dbref, item.location.name, item.location.dbref)
|
||||
)
|
||||
else:
|
||||
text.append(div)
|
||||
text.append("Found |r%d|n armed puzzle(s)." % (len(armed_puzzles)))
|
||||
text.append(div)
|
||||
caller.msg("\n".join(text))
|
||||
|
||||
|
||||
class PuzzleSystemCmdSet(CmdSet):
|
||||
"""
|
||||
CmdSet to create, arm and resolve Puzzles
|
||||
"""
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
super(PuzzleSystemCmdSet, self).at_cmdset_creation()
|
||||
|
||||
self.add(CmdCreatePuzzleRecipe())
|
||||
self.add(CmdEditPuzzle())
|
||||
self.add(CmdArmPuzzle())
|
||||
self.add(CmdListPuzzleRecipes())
|
||||
self.add(CmdListArmedPuzzles())
|
||||
self.add(CmdUsePuzzleParts())
|
||||
65
evennia/contrib/base_systems/unixcommand/README.md
Normal file
65
evennia/contrib/base_systems/unixcommand/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Unix-like Command style parent
|
||||
|
||||
Evennia contribution, Vincent Le Geoff 2017
|
||||
|
||||
This module contains a command class that allows for unix-style command syntax
|
||||
in-game, using --options, positional arguments and stuff like -n 10 etc
|
||||
similarly to a unix command. It might not the best syntax for the average player
|
||||
but can be really useful for builders when they need to have a single command do
|
||||
many things with many options. It uses the ArgumentParser from Python's standard
|
||||
library under the hood.
|
||||
|
||||
## Installation
|
||||
|
||||
To use, inherit `UnixCommand` from this module from your own commands. You need
|
||||
to override two methods:
|
||||
|
||||
- The `init_parser` method, which adds options to the parser. Note that you
|
||||
should normally *not* override the normal `parse` method when inheriting from
|
||||
`UnixCommand`.
|
||||
- The `func` method, called to execute the command once parsed (like any Command).
|
||||
|
||||
Here's a short example:
|
||||
|
||||
```python
|
||||
from evennia.contrib.base_systems.unixcommand import UnixCommand
|
||||
|
||||
|
||||
class CmdPlant(UnixCommand):
|
||||
|
||||
'''
|
||||
Plant a tree or plant.
|
||||
|
||||
This command is used to plant something in the room you are in.
|
||||
|
||||
Examples:
|
||||
plant orange -a 8
|
||||
plant strawberry --hidden
|
||||
plant potato --hidden --age 5
|
||||
|
||||
'''
|
||||
|
||||
key = "plant"
|
||||
|
||||
def init_parser(self):
|
||||
"Add the arguments to the parser."
|
||||
# 'self.parser' inherits `argparse.ArgumentParser`
|
||||
self.parser.add_argument("key",
|
||||
help="the key of the plant to be planted here")
|
||||
self.parser.add_argument("-a", "--age", type=int,
|
||||
default=1, help="the age of the plant to be planted")
|
||||
self.parser.add_argument("--hidden", action="store_true",
|
||||
help="should the newly-planted plant be hidden to players?")
|
||||
|
||||
def func(self):
|
||||
"func is called only if the parser succeeded."
|
||||
# 'self.opts' contains the parsed options
|
||||
key = self.opts.key
|
||||
age = self.opts.age
|
||||
hidden = self.opts.hidden
|
||||
self.msg("Going to plant '{}', age={}, hidden={}.".format(
|
||||
key, age, hidden))
|
||||
```
|
||||
|
||||
To see the full power of argparse and the types of supported options, visit
|
||||
[the documentation of argparse](https://docs.python.org/2/library/argparse.html).
|
||||
6
evennia/contrib/base_systems/unixcommand/__init__.py
Normal file
6
evennia/contrib/base_systems/unixcommand/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
Unix-like Command style - vlgeoff 2017
|
||||
|
||||
"""
|
||||
|
||||
from .unixcommand import UnixCommand # noqa
|
||||
|
|
@ -19,6 +19,9 @@ to override two methods:
|
|||
Here's a short example:
|
||||
|
||||
```python
|
||||
from evennia.contrib.base_systems.unixcommand import UnixCommand
|
||||
|
||||
|
||||
class CmdPlant(UnixCommand):
|
||||
|
||||
'''
|
||||
Loading…
Add table
Add a link
Reference in a new issue