Refactoring contribs

This commit is contained in:
Griatch 2021-12-18 18:02:37 +01:00
parent f5f75bd04d
commit 0ab1c30716
103 changed files with 3203 additions and 604 deletions

View file

@ -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

View file

@ -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
"""

View 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.

View file

@ -0,0 +1,6 @@
"""
Build-menu contrib - vincent-lg 2018
"""
from .building_menu import GenericBuildingCmd # noqa
from .building_menu import BuildingMenu # noqa

View file

@ -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,

View 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
```

View file

@ -0,0 +1,6 @@
"""
Color markups contrib - Griatch 2017
"""
from .color_markups import * # noqa

View file

@ -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

View 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.

View file

@ -0,0 +1,6 @@
"""
Custom gametime contrib - Griatch, vlgeoff 2017
"""
from .custom_gametime import * # noqa

View 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).

View file

@ -0,0 +1,7 @@
"""
Email login contrib - Griatch 2012
"""
from .email_login import UnloggedinCmdSet # noqa
from . import connection_screens # noqa

View file

@ -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")
)

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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)

View file

@ -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():

View file

@ -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+)')

View file

@ -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"

View file

@ -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 = """

View file

@ -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 = []

View 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).

View 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

View file

@ -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")
)

View file

@ -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."

View 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.

View file

@ -0,0 +1,6 @@
"""
Mux-style comms commands - Griatch 2021
"""
from .mux_comms_cmds import CmdSetLegacyComms # noqa

View file

@ -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):
# ...

View file

@ -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())

View 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).

View file

@ -0,0 +1,6 @@
"""
Unix-like Command style - vlgeoff 2017
"""
from .unixcommand import UnixCommand # noqa

View file

@ -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):
'''