mirror of
https://github.com/evennia/evennia.git
synced 2026-03-21 23:36:30 +01:00
Updated HTML docs.
This commit is contained in:
parent
59e50f3fa5
commit
06bc3c8bcd
663 changed files with 2 additions and 61705 deletions
|
|
@ -1,107 +0,0 @@
|
|||
# Accounts
|
||||
|
||||
|
||||
All *users* (real people) that starts a game [Session](./Sessions.md) on Evennia are doing so through an
|
||||
object called *Account*. The Account object has no in-game representation, it represents a unique
|
||||
game account. In order to actually get on the game the Account must *puppet* an [Object](./Objects.md)
|
||||
(normally a [Character](./Objects.md#characters)).
|
||||
|
||||
Exactly how many Sessions can interact with an Account and its Puppets at once is determined by
|
||||
Evennia's [MULTISESSION_MODE](./Sessions.md#multisession-mode) setting.
|
||||
|
||||
Apart from storing login information and other account-specific data, the Account object is what is
|
||||
chatting on [Channels](./Channels.md). It is also a good place to store [Permissions](./Locks.md) to be
|
||||
consistent between different in-game characters as well as configuration options. The Account
|
||||
object also has its own [CmdSet](./Command-Sets.md), the `AccountCmdSet`.
|
||||
|
||||
Logged into default evennia, you can use the `ooc` command to leave your current
|
||||
[character](./Objects.md) and go into OOC mode. You are quite limited in this mode, basically it works
|
||||
like a simple chat program. It acts as a staging area for switching between Characters (if your
|
||||
game supports that) or as a safety mode if your Character gets deleted. Use `ic` to attempt to
|
||||
(re)puppet a Character.
|
||||
|
||||
Note that the Account object can have, and often does have, a different set of
|
||||
[Permissions](./Permissions.md) from the Character they control. Normally you should put your
|
||||
permissions on the Account level - this will overrule permissions set on the Character level. For
|
||||
the permissions of the Character to come into play the default `quell` command can be used. This
|
||||
allows for exploring the game using a different permission set (but you can't escalate your
|
||||
permissions this way - for hierarchical permissions like `Builder`, `Admin` etc, the *lower* of the
|
||||
permissions on the Character/Account will always be used).
|
||||
|
||||
## How to create your own Account types
|
||||
|
||||
You will usually not want more than one Account typeclass for all new accounts (but you could in
|
||||
principle create a system that changes an account's typeclass dynamically).
|
||||
|
||||
An Evennia Account is, per definition, a Python class that includes `evennia.DefaultAccount` among
|
||||
its parents. In `mygame/typeclasses/accounts.py` there is an empty class ready for you to modify.
|
||||
Evennia defaults to using this (it inherits directly from `DefaultAccount`).
|
||||
|
||||
Here's an example of modifying the default Account class in code:
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/accounts.py
|
||||
|
||||
from evennia import DefaultAccount
|
||||
|
||||
class Account(DefaultAccount): # [...]
|
||||
|
||||
at_account_creation(self): "this is called only once, when account is first created"
|
||||
self.db.real_name = None # this is set later self.db.real_address = None # "
|
||||
self.db.config_1 = True # default config self.db.config_2 = False # "
|
||||
self.db.config_3 = 1 # "
|
||||
|
||||
# ... whatever else our game needs to know ``` Reload the server with `reload`.
|
||||
|
||||
```
|
||||
|
||||
... However, if you use `examine *self` (the asterisk makes you examine your Account object rather
|
||||
than your Character), you won't see your new Attributes yet. This is because `at_account_creation`
|
||||
is only called the very *first* time the Account is called and your Account object already exists
|
||||
(any new Accounts that connect will see them though). To update yourself you need to make sure to
|
||||
re-fire the hook on all the Accounts you have already created. Here is an example of how to do this
|
||||
using `py`:
|
||||
|
||||
|
||||
``` py [account.at_account_creation() for account in evennia.managers.accounts.all()] ```
|
||||
|
||||
You should now see the Attributes on yourself.
|
||||
|
||||
|
||||
> If you wanted Evennia to default to a completely *different* Account class located elsewhere, you
|
||||
> must point Evennia to it. Add `BASE_ACCOUNT_TYPECLASS` to your settings file, and give the python
|
||||
> path to your custom class as its value. By default this points to `typeclasses.accounts.Account`,
|
||||
> the empty template we used above.
|
||||
|
||||
|
||||
## Properties on Accounts
|
||||
|
||||
Beyond those properties assigned to all typeclassed objects (see [Typeclasses](./Typeclasses.md)), the
|
||||
Account also has the following custom properties:
|
||||
|
||||
- `user` - a unique link to a `User` Django object, representing the logged-in user.
|
||||
- `obj` - an alias for `character`.
|
||||
- `name` - an alias for `user.username`
|
||||
- `sessions` - an instance of
|
||||
[ObjectSessionHandler](github:evennia.objects.objects#objectsessionhandler)
|
||||
managing all connected Sessions (physical connections) this object listens to (Note: In older
|
||||
versions of Evennia, this was a list). The so-called `session-id` (used in many places) is found
|
||||
as
|
||||
a property `sessid` on each Session instance.
|
||||
- `is_superuser` (bool: True/False) - if this account is a superuser.
|
||||
|
||||
Special handlers:
|
||||
- `cmdset` - This holds all the current [Commands](./Commands.md) of this Account. By default these are
|
||||
the commands found in the cmdset defined by `settings.CMDSET_ACCOUNT`.
|
||||
- `nicks` - This stores and handles [Nicks](./Nicks.md), in the same way as nicks it works on Objects.
|
||||
For Accounts, nicks are primarily used to store custom aliases for
|
||||
[Channels](./Channels.md).
|
||||
|
||||
Selection of special methods (see `evennia.DefaultAccount` for details):
|
||||
- `get_puppet` - get a currently puppeted object connected to the Account and a given session id, if
|
||||
any.
|
||||
- `puppet_object` - connect a session to a puppetable Object.
|
||||
- `unpuppet_object` - disconnect a session from a puppetable Object.
|
||||
- `msg` - send text to the Account
|
||||
- `execute_cmd` - runs a command as if this Account did it.
|
||||
- `search` - search for Accounts.
|
||||
|
|
@ -1,613 +0,0 @@
|
|||
# Attributes
|
||||
|
||||
```{code-block}
|
||||
:caption: In-game
|
||||
> set obj/myattr = "test"
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In-code, using the .db wrapper
|
||||
obj.db.foo = [1, 2, 3, "bar"]
|
||||
value = obj.db.foo
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In-code, using the .attributes handler
|
||||
obj.attributes.add("myattr", 1234, category="bar")
|
||||
value = attributes.get("myattr", category="bar")
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In-code, using `AttributeProperty` at class level
|
||||
from evennia import DefaultObject
|
||||
from evennia import AttributeProperty
|
||||
|
||||
class MyObject(DefaultObject):
|
||||
foo = AttributeProperty(default=[1, 2, 3, "bar"])
|
||||
myattr = AttributeProperty(100, category='bar')
|
||||
|
||||
```
|
||||
|
||||
_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a server reboot. An Attribute can store pretty much any
|
||||
Python data structure and data type, like numbers, strings, lists, dicts etc. You can also
|
||||
store (references to) database objects like characters and rooms.
|
||||
|
||||
- [What can be stored in an Attribute](#what-types-of-data-can-i-save-in-an-attribute) is a must-read to avoid being surprised, also for experienced developers. Attributes can store _almost_ everything
|
||||
but you need to know the quirks.
|
||||
- [NAttributes](#in-memory-attributes-nattributes) are the in-memory, non-persistent
|
||||
siblings of Attributes.
|
||||
- [Managing Attributes In-game](#managing-attributes-in-game) for in-game builder commands.
|
||||
|
||||
## Managing Attributes in Code
|
||||
|
||||
Attributes are usually handled in code. All [Typeclassed](./Typeclasses.md) entities
|
||||
([Accounts](./Accounts.md), [Objects](./Objects.md), [Scripts](./Scripts.md) and
|
||||
[Channels](./Channels.md)) can (and usually do) have Attributes associated with them. There
|
||||
are three ways to manage Attributes, all of which can be mixed.
|
||||
|
||||
- [Using the `.db` property shortcut](#using-db)
|
||||
- [Using the `.attributes` manager (`AttributeManager`)](#using-attributes)
|
||||
- [Using `AttributeProperty` for assigning Attributes in a way similar to Django fields](#using-attributeproperty)
|
||||
|
||||
### Using .db
|
||||
|
||||
The simplest way to get/set Attributes is to use the `.db` shortcut. This allows for setting and getting Attributes that lack a _category_ (having category `None`)
|
||||
|
||||
```python
|
||||
import evennia
|
||||
|
||||
obj = evennia.create_object(key="Foo")
|
||||
|
||||
obj.db.foo1 = 1234
|
||||
obj.db.foo2 = [1, 2, 3, 4]
|
||||
obj.db.weapon = "sword"
|
||||
obj.db.self_reference = obj # stores a reference to the obj
|
||||
|
||||
# (let's assume a rose exists in-game)
|
||||
rose = evennia.search_object(key="rose")[0] # returns a list, grab 0th element
|
||||
rose.db.has_thorns = True
|
||||
|
||||
# retrieving
|
||||
val1 = obj.db.foo1
|
||||
val2 = obj.db.foo2
|
||||
weap = obj.db.weapon
|
||||
myself = obj.db.self_reference # retrieve reference from db, get object back
|
||||
|
||||
is_ouch = rose.db.has_thorns
|
||||
|
||||
# this will return None, not AttributeError!
|
||||
not_found = obj.db.jiwjpowiwwerw
|
||||
|
||||
# returns all Attributes on the object
|
||||
obj.db.all
|
||||
|
||||
# delete an Attribute
|
||||
del obj.db.foo2
|
||||
```
|
||||
Trying to access a non-existing Attribute will never lead to an `AttributeError`. Instead
|
||||
you will get `None` back. The special `.db.all` will return a list of all Attributes on
|
||||
the object. You can replace this with your own Attribute `all` if you want, it will replace the
|
||||
default `all` functionality until you delete it again.
|
||||
|
||||
### Using .attributes
|
||||
|
||||
If you want to group your Attribute in a category, or don't know the name of the Attribute beforehand, you can make use of
|
||||
the [AttributeHandler](evennia.typeclasses.attributes.AttributeHandler), available as `.attributes` on all typeclassed entities. With no extra keywords, this is identical to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally):
|
||||
|
||||
```python
|
||||
is_ouch = rose.attributes.get("has_thorns")
|
||||
|
||||
obj.attributes.add("helmet", "Knight's helmet")
|
||||
helmet = obj.attributes.get("helmet")
|
||||
|
||||
# you can give space-separated Attribute-names (can't do that with .db)
|
||||
obj.attributes.add("my game log", "long text about ...")
|
||||
```
|
||||
|
||||
By using a category you can separate same-named Attributes on the same object to help organization.
|
||||
|
||||
```python
|
||||
# store (let's say we have gold_necklace and ringmail_armor from before)
|
||||
obj.attributes.add("neck", gold_necklace, category="clothing")
|
||||
obj.attributes.add("neck", ringmail_armor, category="armor")
|
||||
|
||||
# retrieve later - we'll get back gold_necklace and ringmail_armor
|
||||
neck_clothing = obj.attributes.get("neck", category="clothing")
|
||||
neck_armor = obj.attributes.get("neck", category="armor")
|
||||
```
|
||||
|
||||
If you don't specify a category, the Attribute's `category` will be `None` and can thus also be found via `.db`. `None` is considered a category of its own, so you won't find `None`-category Attributes mixed with Attributes having categories.
|
||||
|
||||
Here are the methods of the `AttributeHandler`. See
|
||||
the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for more details.
|
||||
|
||||
- `has(...)` - this checks if the object has an Attribute with this key. This is equivalent
|
||||
to doing `obj.db.attrname` except you can also check for a specific `category.
|
||||
- `get(...)` - this retrieves the given Attribute. You can also provide a `default` value to return
|
||||
if the Attribute is not defined (instead of None). By supplying an
|
||||
`accessing_object` to the call one can also make sure to check permissions before modifying
|
||||
anything. The `raise_exception` kwarg allows you to raise an `AttributeError` instead of returning
|
||||
`None` when you access a non-existing `Attribute`. The `strattr` kwarg tells the system to store
|
||||
the Attribute as a raw string rather than to pickle it. While an optimization this should usually
|
||||
not be used unless the Attribute is used for some particular, limited purpose.
|
||||
- `add(...)` - this adds a new Attribute to the object. An optional [lockstring](./Locks.md) can be
|
||||
supplied here to restrict future access and also the call itself may be checked against locks.
|
||||
- `remove(...)` - Remove the given Attribute. This can optionally be made to check for permission
|
||||
before performing the deletion. - `clear(...)` - removes all Attributes from object.
|
||||
- `all(category=None)` - returns all Attributes (of the given category) attached to this object.
|
||||
|
||||
Examples:
|
||||
|
||||
```python
|
||||
try:
|
||||
# raise error if Attribute foo does not exist
|
||||
val = obj.attributes.get("foo", raise_exception=True):
|
||||
except AttributeError:
|
||||
# ...
|
||||
|
||||
# return default value if foo2 doesn't exist
|
||||
val2 = obj.attributes.get("foo2", default=[1, 2, 3, "bar"])
|
||||
|
||||
# delete foo if it exists (will silently fail if unset, unless
|
||||
# raise_exception is set)
|
||||
obj.attributes.remove("foo")
|
||||
|
||||
# view all clothes on obj
|
||||
all_clothes = obj.attributes.all(category="clothes")
|
||||
```
|
||||
|
||||
### Using AttributeProperty
|
||||
|
||||
The third way to set up an Attribute is to use an `AttributeProperty`. This
|
||||
is done on the _class level_ of your typeclass and allows you to treat Attributes a bit like Django database Fields. Unlike using `.db` and `.attributes`, an `AttributeProperty` can't be created on the fly, you must assign it in the class code.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/characters.py
|
||||
|
||||
from evennia import DefaultCharacter
|
||||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
|
||||
strength = AttributeProperty(10, category='stat')
|
||||
constitution = AttributeProperty(11, category='stat')
|
||||
agility = AttributeProperty(12, category='stat')
|
||||
magic = AttributeProperty(13, category='stat')
|
||||
|
||||
sleepy = AttributeProperty(False, autocreate=False)
|
||||
poisoned = AttributeProperty(False, autocreate=False)
|
||||
|
||||
def at_object_creation(self):
|
||||
# ...
|
||||
```
|
||||
|
||||
When a new instance of the class is created, new `Attributes` will be created with the value and category given.
|
||||
|
||||
With `AttributeProperty`'s set up like this, one can access the underlying `Attribute` like a regular property on the created object:
|
||||
|
||||
```python
|
||||
char = create_object(Character)
|
||||
|
||||
char.strength # returns 10
|
||||
char.agility = 15 # assign a new value (category remains 'stat')
|
||||
|
||||
char.db.magic # returns None (wrong category)
|
||||
char.attributes.get("agility", category="stat") # returns 15
|
||||
|
||||
char.db.sleepy # returns None because autocreate=False (see below)
|
||||
|
||||
```
|
||||
|
||||
```{warning}
|
||||
Be careful to not assign AttributeProperty's to names of properties and methods already existing on the class, like 'key' or 'at_object_creation'. That could lead to very confusing errors.
|
||||
```
|
||||
|
||||
The `autocreate=False` (default is `True`) used for `sleepy` and `poisoned` is worth a closer explanation. When `False`, _no_ Attribute will be auto-created for these AttributProperties unless they are _explicitly_ set.
|
||||
The advantage of not creating an Attribute is that the default value given to `AttributeProperty` is returned with no database access unless you change it. This also means that if you want to change the default later, all entities previously create will inherit the new default.
|
||||
The drawback is that without a database precense you can't find the Attribute via `.db` and `.attributes.get` (or by querying for it in other ways in the database):
|
||||
|
||||
```python
|
||||
char.sleepy # returns False, no db access
|
||||
|
||||
char.db.sleepy # returns None - no Attribute exists
|
||||
char.attributes.get("sleepy") # returns None too
|
||||
|
||||
char.sleepy = True # now an Attribute is created
|
||||
char.db.sleepy # now returns True!
|
||||
char.attributes.get("sleepy") # now returns True
|
||||
|
||||
char.sleepy # now returns True, involves db access
|
||||
|
||||
```
|
||||
|
||||
You can e.g. `del char.strength` to set the value back to the default (the value defined
|
||||
in the `AttributeProperty`).
|
||||
|
||||
See the [AttributeProperty API](evennia.typeclasses.attributes.AttributeProperty) for more details on how to create it with special options, like giving access-restrictions.
|
||||
|
||||
|
||||
## Managing Attributes in-game
|
||||
|
||||
Attributes are mainly used by code. But one can also allow the builder to use Attributes to
|
||||
'turn knobs' in-game. For example a builder could want to manually tweak the "level" Attribute of an
|
||||
enemy NPC to lower its difficuly.
|
||||
|
||||
When setting Attributes this way, you are severely limited in what can be stored - this is because
|
||||
giving players (even builders) the ability to store arbitrary Python would be a severe security
|
||||
problem.
|
||||
|
||||
In game you can set an Attribute like this:
|
||||
|
||||
set myobj/foo = "bar"
|
||||
|
||||
To view, do
|
||||
|
||||
set myobj/foo
|
||||
|
||||
or see them together with all object-info with
|
||||
|
||||
examine myobj
|
||||
|
||||
The first `set`-example will store a new Attribute `foo` on the object `myobj` and give it the
|
||||
value "bar".
|
||||
You can store numbers, booleans, strings, tuples, lists and dicts this way. But if
|
||||
you store a list/tuple/dict they must be proper Python structures and may _only_ contain strings
|
||||
or numbers. If you try to insert an unsupported structure, the input will be converted to a
|
||||
string.
|
||||
|
||||
set myobj/mybool = True
|
||||
set myobj/mybool = True
|
||||
set myobj/mytuple = (1, 2, 3, "foo")
|
||||
set myobj/mylist = ["foo", "bar", 2]
|
||||
set myobj/mydict = {"a": 1, "b": 2, 3: 4}
|
||||
set mypobj/mystring = [1, 2, foo] # foo is invalid Python (no quotes)
|
||||
|
||||
For the last line you'll get a warning and the value instead will be saved as a string `"[1, 2, foo]"`.
|
||||
|
||||
## Locking and checking Attributes
|
||||
|
||||
While the `set` command is limited to builders, individual Attributes are usually not
|
||||
locked down. You may want to lock certain sensitive Attributes, in particular for games
|
||||
where you allow player building. You can add such limitations by adding a [lock string](./Locks.md)
|
||||
to your Attribute. A NAttribute have no locks.
|
||||
|
||||
The relevant lock types are
|
||||
|
||||
- `attrread` - limits who may read the value of the Attribute
|
||||
- `attredit` - limits who may set/change this Attribute
|
||||
|
||||
You must use the `AttributeHandler` to assign the lockstring to the Attribute:
|
||||
|
||||
```python
|
||||
lockstring = "attread:all();attredit:perm(Admins)"
|
||||
obj.attributes.add("myattr", "bar", lockstring=lockstring)"
|
||||
```
|
||||
|
||||
If you already have an Attribute and want to add a lock in-place you can do so
|
||||
by having the `AttributeHandler` return the `Attribute` object itself (rather than
|
||||
its value) and then assign the lock to it directly:
|
||||
|
||||
```python
|
||||
lockstring = "attread:all();attredit:perm(Admins)"
|
||||
obj.attributes.get("myattr", return_obj=True).locks.add(lockstring)
|
||||
```
|
||||
|
||||
Note the `return_obj` keyword which makes sure to return the `Attribute` object so its LockHandler
|
||||
could be accessed.
|
||||
|
||||
A lock is no good if nothing checks it -- and by default Evennia does not check locks on Attributes.
|
||||
To check the `lockstring` you provided, make sure you include `accessing_obj` and set
|
||||
`default_access=False` as you make a `get` call.
|
||||
|
||||
```python
|
||||
# in some command code where we want to limit
|
||||
# setting of a given attribute name on an object
|
||||
attr = obj.attributes.get(attrname,
|
||||
return_obj=True,
|
||||
accessing_obj=caller,
|
||||
default=None,
|
||||
default_access=False)
|
||||
if not attr:
|
||||
caller.msg("You cannot edit that Attribute!")
|
||||
return
|
||||
# edit the Attribute here
|
||||
```
|
||||
|
||||
The same keywords are available to use with `obj.attributes.set()` and `obj.attributes.remove()`,
|
||||
those will check for the `attredit` lock type.
|
||||
|
||||
## What types of data can I save in an Attribute?
|
||||
|
||||
The database doesn't know anything about Python objects, so Evennia must *serialize* Attribute
|
||||
values into a string representation before storing it to the database. This is done using the
|
||||
[pickle](https://docs.python.org/library/pickle.html) module of Python.
|
||||
|
||||
> The only exception is if you use the `strattr` keyword of the
|
||||
`AttributeHandler` to save to the `strvalue` field of the Attribute. In that case you can _only_ save
|
||||
*strings* and those will not be pickled).
|
||||
|
||||
### Storing single objects
|
||||
|
||||
With a single object, we mean anything that is *not iterable*, like numbers,
|
||||
strings or custom class instances without the `__iter__` method.
|
||||
|
||||
* You can generally store any non-iterable Python entity that can be _pickled_.
|
||||
* Single database objects/typeclasses can be stored, despite them normally not
|
||||
being possible to pickle. Evennia will convert them to an internal
|
||||
representation using theihr classname, database-id and creation-date with a
|
||||
microsecond precision. When retrieving, the object instance will be re-fetched
|
||||
from the database using this information.
|
||||
* If you 'hide' a db-obj as a property on a custom class, Evennia will not be
|
||||
able to find it to serialize it. For that you need to help it out (see below).
|
||||
|
||||
```{code-block} python
|
||||
:caption: Valid assignments
|
||||
|
||||
# Examples of valid single-value attribute data:
|
||||
obj.db.test1 = 23
|
||||
obj.db.test1 = False
|
||||
# a database object (will be stored as an internal representation)
|
||||
obj.db.test2 = myobj
|
||||
```
|
||||
|
||||
As mentioned, Evennia will not be able to automatically serialize db-objects
|
||||
'hidden' in arbitrary properties on an object. This will lead to an error
|
||||
when saving the Attribute.
|
||||
|
||||
```{code-block} python
|
||||
:caption: Invalid, 'hidden' dbobject
|
||||
# example of storing an invalid, "hidden" dbobject in Attribute
|
||||
class Container:
|
||||
def __init__(self, mydbobj):
|
||||
# no way for Evennia to know this is a database object!
|
||||
self.mydbobj = mydbobj
|
||||
|
||||
# let's assume myobj is a db-object
|
||||
container = Container(myobj)
|
||||
obj.db.mydata = container # will raise error!
|
||||
|
||||
```
|
||||
|
||||
By adding two methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` to the
|
||||
object you want to save, you can pre-serialize and post-deserialize all 'hidden'
|
||||
objects before Evennia's main serializer gets to work. Inside these methods, use Evennia's
|
||||
[evennia.utils.dbserialize.dbserialize](evennia.utils.dbserialize.dbserialize) and
|
||||
[dbunserialize](evennia.utils.dbserialize.dbunserialize) functions to safely
|
||||
serialize the db-objects you want to store.
|
||||
|
||||
```{code-block} python
|
||||
:caption: Fixing an invalid 'hidden' dbobj for storing in Attribute
|
||||
|
||||
from evennia.utils import dbserialize # important
|
||||
|
||||
class Container:
|
||||
def __init__(self, mydbobj):
|
||||
# A 'hidden' db-object
|
||||
self.mydbobj = mydbobj
|
||||
|
||||
def __serialize_dbobjs__(self):
|
||||
"""This is called before serialization and allows
|
||||
us to custom-handle those 'hidden' dbobjs"""
|
||||
self.mydbobj = dbserialize.dbserialize(self.mydbobj
|
||||
|
||||
def __deserialize_dbobjs__(self):
|
||||
"""This is called after deserialization and allows you to
|
||||
restore the 'hidden' dbobjs you serialized before"""
|
||||
if isinstance(self.mydbobj, bytes):
|
||||
# make sure to check if it's bytes before trying dbunserialize
|
||||
self.mydbobj = dbserialize.dbunserialize(self.mydbobj)
|
||||
|
||||
# let's assume myobj is a db-object
|
||||
container = Container(myobj)
|
||||
obj.db.mydata = container # will now work fine!
|
||||
```
|
||||
|
||||
> Note the extra check in `__deserialize_dbobjs__` to make sure the thing you
|
||||
> are deserializing is a `bytes` object. This is needed because the Attribute's
|
||||
> cache reruns deserializations in some situations when the data was already
|
||||
> once deserialized. If you see errors in the log saying
|
||||
> `Could not unpickle data for storage: ...`, the reason is
|
||||
> likely that you forgot to add this check.
|
||||
|
||||
|
||||
### Storing multiple objects
|
||||
|
||||
This means storing objects in a collection of some kind and are examples of *iterables*, pickle-able
|
||||
entities you can loop over in a for-loop. Attribute-saving supports the following iterables:
|
||||
|
||||
* [Tuples](https://docs.python.org/2/library/functions.html#tuple), like `(1,2,"test", <dbobj>)`.
|
||||
* [Lists](https://docs.python.org/2/tutorial/datastructures.html#more-on-lists), like `[1,2,"test", <dbobj>]`.
|
||||
* [Dicts](https://docs.python.org/2/tutorial/datastructures.html#dictionaries), like `{1:2, "test":<dbobj>]`.
|
||||
* [Sets](https://docs.python.org/2/tutorial/datastructures.html#sets), like `{1,2,"test",<dbobj>}`.
|
||||
* [collections.OrderedDict](https://docs.python.org/2/library/collections.html#collections.OrderedDict),
|
||||
like `OrderedDict((1,2), ("test", <dbobj>))`.
|
||||
* [collections.Deque](https://docs.python.org/2/library/collections.html#collections.deque), like `deque((1,2,"test",<dbobj>))`.
|
||||
* *Nestings* of any combinations of the above, like lists in dicts or an OrderedDict of tuples, each
|
||||
containing dicts, etc.
|
||||
* All other iterables (i.e. entities with the `__iter__` method) will be converted to a *list*.
|
||||
Since you can use any combination of the above iterables, this is generally not much of a
|
||||
limitation.
|
||||
|
||||
Any entity listed in the [Single object](./Attributes.md#storing-single-objects) section above can be
|
||||
stored in the iterable.
|
||||
|
||||
> As mentioned in the previous section, database entities (aka typeclasses) are not possible to
|
||||
> pickle. So when storing an iterable, Evennia must recursively traverse the iterable *and all its
|
||||
> nested sub-iterables* in order to find eventual database objects to convert. This is a very fast
|
||||
> process but for efficiency you may want to avoid too deeply nested structures if you can.
|
||||
|
||||
```python
|
||||
# examples of valid iterables to store
|
||||
obj.db.test3 = [obj1, 45, obj2, 67]
|
||||
# a dictionary
|
||||
obj.db.test4 = {'str':34, 'dex':56, 'agi':22, 'int':77}
|
||||
# a mixed dictionary/list
|
||||
obj.db.test5 = {'members': [obj1,obj2,obj3], 'enemies':[obj4,obj5]}
|
||||
# a tuple with a list in it
|
||||
obj.db.test6 = (1, 3, 4, 8, ["test", "test2"], 9)
|
||||
# a set
|
||||
obj.db.test7 = set([1, 2, 3, 4, 5])
|
||||
# in-situ manipulation
|
||||
obj.db.test8 = [1, 2, {"test":1}]
|
||||
obj.db.test8[0] = 4
|
||||
obj.db.test8[2]["test"] = 5
|
||||
# test8 is now [4,2,{"test":5}]
|
||||
```
|
||||
|
||||
Note that if make some advanced iterable object, and store an db-object on it in
|
||||
a way such that it is _not_ returned by iterating over it, you have created a
|
||||
'hidden' db-object. See [the previous section](#storing-single-objects) for how
|
||||
to tell Evennia how to serialize such hidden objects safely.
|
||||
|
||||
|
||||
### Retrieving Mutable objects
|
||||
|
||||
A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can
|
||||
be modified in-place after they were created, which is everything except tuples) are handled by
|
||||
custom objects called `_SaverList`, `_SaverDict` etc. These `_Saver...` classes behave just like the
|
||||
normal variant except that they are aware of the database and saves to it whenever new data gets
|
||||
assigned to them. This is what allows you to do things like `self.db.mylist[7] = val` and be sure
|
||||
that the new version of list is saved. Without this you would have to load the list into a temporary
|
||||
variable, change it and then re-assign it to the Attribute in order for it to save.
|
||||
|
||||
There is however an important thing to remember. If you retrieve your mutable iterable into another
|
||||
variable, e.g. `mylist2 = obj.db.mylist`, your new variable (`mylist2`) will *still* be a
|
||||
`_SaverList`. This means it will continue to save itself to the database whenever it is updated!
|
||||
|
||||
```python
|
||||
obj.db.mylist = [1, 2, 3, 4]
|
||||
mylist = obj.db.mylist
|
||||
|
||||
mylist[3] = 5 # this will also update database
|
||||
|
||||
print(mylist) # this is now [1, 2, 3, 5]
|
||||
print(obj.db.mylist) # now also [1, 2, 3, 5]
|
||||
```
|
||||
|
||||
When you extract your mutable Attribute data into a variable like `mylist`, think of it as getting a _snapshot_
|
||||
of the variable. If you update the snapshot, it will save to the database, but this change _will not propagate to
|
||||
any other snapshots you may have done previously_.
|
||||
|
||||
```python
|
||||
obj.db.mylist = [1, 2, 3, 4]
|
||||
mylist1 = obj.db.mylist
|
||||
mylist2 = obj.db.mylist
|
||||
mylist1[3] = 5
|
||||
|
||||
print(mylist1) # this is now [1, 2, 3, 5]
|
||||
print(obj.db.mylist) # also updated to [1, 2, 3, 5]
|
||||
|
||||
print(mylist2) # still [1, 2, 3, 4] !
|
||||
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
Remember, the complexities of this section only relate to *mutable* iterables - things you can update
|
||||
in-place, like lists and dicts. [Immutable](https://en.wikipedia.org/wiki/Immutable) objects (strings,
|
||||
numbers, tuples etc) are already disconnected from the database from the onset.
|
||||
```
|
||||
|
||||
To avoid confusion with mutable Attributes, only work with one variable (snapshot) at a time and save
|
||||
back the results as needed.
|
||||
|
||||
You can also choose to "disconnect" the Attribute entirely from the
|
||||
database with the help of the `.deserialize()` method:
|
||||
|
||||
```python
|
||||
obj.db.mylist = [1, 2, 3, 4, {1: 2}]
|
||||
mylist = obj.db.mylist.deserialize()
|
||||
```
|
||||
|
||||
The result of this operation will be a structure only consisting of normal Python mutables (`list`
|
||||
instead of `_SaverList`, `dict` instead of `_SaverDict` and so on). If you update it, you need to
|
||||
explicitly save it back to the Attribute for it to save.
|
||||
|
||||
## Properties of Attributes
|
||||
|
||||
An `Attribute` object is stored in the database. It has the following properties:
|
||||
|
||||
- `key` - the name of the Attribute. When doing e.g. `obj.db.attrname = value`, this property is set
|
||||
to `attrname`.
|
||||
- `value` - this is the value of the Attribute. This value can be anything which can be pickled -
|
||||
objects, lists, numbers or what have you (see
|
||||
[this section](./Attributes.md#what-types-of-data-can-i-save-in-an-attribute) for more info). In the
|
||||
example
|
||||
`obj.db.attrname = value`, the `value` is stored here.
|
||||
- `category` - this is an optional property that is set to None for most Attributes. Setting this
|
||||
allows to use Attributes for different functionality. This is usually not needed unless you want
|
||||
to use Attributes for very different functionality ([Nicks](./Nicks.md) is an example of using
|
||||
Attributes in this way). To modify this property you need to use the [Attribute Handler](#attributes)
|
||||
- `strvalue` - this is a separate value field that only accepts strings. This severely limits the
|
||||
data possible to store, but allows for easier database lookups. This property is usually not used
|
||||
except when re-using Attributes for some other purpose ([Nicks](./Nicks.md) use it). It is only
|
||||
accessible via the [Attribute Handler](#attributes).
|
||||
|
||||
There are also two special properties:
|
||||
|
||||
- `attrtype` - this is used internally by Evennia to separate [Nicks](./Nicks.md), from Attributes (Nicks
|
||||
use Attributes behind the scenes).
|
||||
- `model` - this is a *natural-key* describing the model this Attribute is attached to. This is on
|
||||
the form *appname.modelclass*, like `objects.objectdb`. It is used by the Attribute and
|
||||
NickHandler to quickly sort matches in the database. Neither this nor `attrtype` should normally
|
||||
need to be modified.
|
||||
|
||||
Non-database attributes are not stored in the database and have no equivalence
|
||||
to `category` nor `strvalue`, `attrtype` or `model`.
|
||||
|
||||
## In-memory Attributes (NAttributes)
|
||||
|
||||
_NAttributes_ (short of Non-database Attributes) mimic Attributes in most things except they
|
||||
are **non-persistent** - they will _not_ survive a server reload.
|
||||
|
||||
- Instead of `.db` use `.ndb`.
|
||||
- Instead of `.attributes` use `.nattributes`
|
||||
- Instead of `AttributeProperty`, use `NAttributeProperty`.
|
||||
|
||||
```python
|
||||
rose.ndb.has_thorns = True
|
||||
is_ouch = rose.ndb.has_thorns
|
||||
|
||||
rose.nattributes.add("has_thorns", True)
|
||||
is_ouch = rose.nattributes.get("has_thorns")
|
||||
```
|
||||
|
||||
Differences between `Attributes` and `NAttributes`:
|
||||
|
||||
- `NAttribute`s are always wiped on a server reload.
|
||||
- They only exist in memory and never involve the database at all, making them faster to
|
||||
access and edit than `Attribute`s.
|
||||
- `NAttribute`s can store _any_ Python structure (and database object) without limit.
|
||||
- They can _not_ be set with the standard `set` command (but they are visible with `examine`)
|
||||
|
||||
There are some important reasons we recommend using `ndb` to store temporary data rather than
|
||||
the simple alternative of just storing a variable directly on an object:
|
||||
|
||||
- NAttributes are tracked by Evennia and will not be purged in various cache-cleanup operations
|
||||
the server may do. So using them guarantees that they'll remain available at least as long as
|
||||
the server lives.
|
||||
- It's a consistent style - `.db/.attributes` and `.ndb/.nattributes` makes for clean-looking code
|
||||
where it's clear how long-lived (or not) your data is to be.
|
||||
|
||||
### Persistent vs non-persistent
|
||||
|
||||
So *persistent* data means that your data will survive a server reboot, whereas with
|
||||
*non-persistent* data it will not ...
|
||||
|
||||
... So why would you ever want to use non-persistent data? The answer is, you don't have to. Most of
|
||||
the time you really want to save as much as you possibly can. Non-persistent data is potentially
|
||||
useful in a few situations though.
|
||||
|
||||
- You are worried about database performance. Since Evennia caches Attributes very aggressively,
|
||||
this is not an issue unless you are reading *and* writing to your Attribute very often (like many
|
||||
times per second). Reading from an already cached Attribute is as fast as reading any Python
|
||||
property. But even then this is not likely something to worry about: Apart from Evennia's own
|
||||
caching, modern database systems themselves also cache data very efficiently for speed. Our
|
||||
default
|
||||
database even runs completely in RAM if possible, alleviating much of the need to write to disk
|
||||
during heavy loads.
|
||||
- A more valid reason for using non-persistent data is if you *want* to lose your state when logging
|
||||
off. Maybe you are storing throw-away data that are re-initialized at server startup. Maybe you
|
||||
are implementing some caching of your own. Or maybe you are testing a buggy [Script](./Scripts.md) that
|
||||
does potentially harmful stuff to your character object. With non-persistent storage you can be
|
||||
sure that whatever is messed up, it's nothing a server reboot can't clear up.
|
||||
- `NAttribute`s have no restrictions at all on what they can store, since they
|
||||
don't need to worry about being saved to the database - they work very well for temporary storage.
|
||||
- You want to implement a fully or partly *non-persistent world*. Who are we to argue with your
|
||||
grand vision!
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
# Batch Code Processor
|
||||
|
||||
|
||||
For an introduction and motivation to using batch processors, see [here](./Batch-Processors.md). This
|
||||
page describes the Batch-*code* processor. The Batch-*command* one is covered [here](Batch-Command-
|
||||
Processor).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The batch-code processor is a superuser-only function, invoked by
|
||||
|
||||
> @batchcode path.to.batchcodefile
|
||||
|
||||
Where `path.to.batchcodefile` is the path to a *batch-code file*. Such a file should have a name
|
||||
ending in "`.py`" (but you shouldn't include that in the path). The path is given like a python path
|
||||
relative to a folder you define to hold your batch files, set by `BATCH_IMPORT_PATH` in your
|
||||
settings. Default folder is (assuming your game is called "mygame") `mygame/world/`. So if you want
|
||||
to run the example batch file in `mygame/world/batch_code.py`, you could simply use
|
||||
|
||||
> @batchcode batch_code
|
||||
|
||||
This will try to run through the entire batch file in one go. For more gradual, *interactive*
|
||||
control you can use the `/interactive` switch. The switch `/debug` will put the processor in
|
||||
*debug* mode. Read below for more info.
|
||||
|
||||
## The batch file
|
||||
|
||||
A batch-code file is a normal Python file. The difference is that since the batch processor loads
|
||||
and executes the file rather than importing it, you can reliably update the file, then call it
|
||||
again, over and over and see your changes without needing to `@reload` the server. This makes for
|
||||
easy testing. In the batch-code file you have also access to the following global variables:
|
||||
|
||||
- `caller` - This is a reference to the object running the batchprocessor.
|
||||
- `DEBUG` - This is a boolean that lets you determine if this file is currently being run in debug-
|
||||
mode or not. See below how this can be useful.
|
||||
|
||||
Running a plain Python file through the processor will just execute the file from beginning to end.
|
||||
If you want to get more control over the execution you can use the processor's *interactive* mode.
|
||||
This runs certain code blocks on their own, rerunning only that part until you are happy with it. In
|
||||
order to do this you need to add special markers to your file to divide it up into smaller chunks.
|
||||
These take the form of comments, so the file remains valid Python.
|
||||
|
||||
Here are the rules of syntax of the batch-code `*.py` file.
|
||||
|
||||
- `#CODE` as the first on a line marks the start of a *code* block. It will last until the beginning
|
||||
of another marker or the end of the file. Code blocks contain functional python code. Each `#CODE`
|
||||
block will be run in complete isolation from other parts of the file, so make sure it's self-
|
||||
contained.
|
||||
- `#HEADER` as the first on a line marks the start of a *header* block. It lasts until the next
|
||||
marker or the end of the file. This is intended to hold imports and variables you will need for all
|
||||
other blocks .All python code defined in a header block will always be inserted at the top of every
|
||||
`#CODE` blocks in the file. You may have more than one `#HEADER` block, but that is equivalent to
|
||||
having one big one. Note that you can't exchange data between code blocks, so editing a header-
|
||||
variable in one code block won't affect that variable in any other code block!
|
||||
- `#INSERT path.to.file` will insert another batchcode (Python) file at that position.
|
||||
- A `#` that is not starting a `#HEADER`, `#CODE` or `#INSERT` instruction is considered a comment.
|
||||
- Inside a block, normal Python syntax rules apply. For the sake of indentation, each block acts as
|
||||
a separate python module.
|
||||
|
||||
Below is a version of the example file found in `evennia/contrib/tutorial_examples/`.
|
||||
|
||||
```python
|
||||
#
|
||||
# This is an example batch-code build file for Evennia.
|
||||
#
|
||||
|
||||
#HEADER
|
||||
|
||||
# This will be included in all other #CODE blocks
|
||||
|
||||
from evennia import create_object, search_object
|
||||
from evennia.contrib.tutorial_examples import red_button
|
||||
from typeclasses.objects import Object
|
||||
|
||||
limbo = search_object('Limbo')[0]
|
||||
|
||||
|
||||
#CODE
|
||||
|
||||
red_button = create_object(red_button.RedButton, key="Red button",
|
||||
location=limbo, aliases=["button"])
|
||||
|
||||
# caller points to the one running the script
|
||||
caller.msg("A red button was created.")
|
||||
|
||||
# importing more code from another batch-code file
|
||||
#INSERT batch_code_insert
|
||||
|
||||
#CODE
|
||||
|
||||
table = create_object(Object, key="Blue Table", location=limbo)
|
||||
chair = create_object(Object, key="Blue Chair", location=limbo)
|
||||
|
||||
string = f"A {table} and {chair} were created."
|
||||
if DEBUG:
|
||||
table.delete()
|
||||
chair.delete()
|
||||
string += " Since debug was active, they were deleted again."
|
||||
caller.msg(string)
|
||||
```
|
||||
|
||||
This uses Evennia's Python API to create three objects in sequence.
|
||||
|
||||
## Debug mode
|
||||
|
||||
Try to run the example script with
|
||||
|
||||
> @batchcode/debug tutorial_examples.example_batch_code
|
||||
|
||||
The batch script will run to the end and tell you it completed. You will also get messages that the
|
||||
button and the two pieces of furniture were created. Look around and you should see the button
|
||||
there. But you won't see any chair nor a table! This is because we ran this with the `/debug`
|
||||
switch, which is directly visible as `DEBUG==True` inside the script. In the above example we
|
||||
handled this state by deleting the chair and table again.
|
||||
|
||||
The debug mode is intended to be used when you test out a batchscript. Maybe you are looking for
|
||||
bugs in your code or try to see if things behave as they should. Running the script over and over
|
||||
would then create an ever-growing stack of chairs and tables, all with the same name. You would have
|
||||
to go back and painstakingly delete them later.
|
||||
|
||||
## Interactive mode
|
||||
|
||||
Interactive mode works very similar to the [batch-command processor counterpart](Batch-Command-
|
||||
Processor). It allows you more step-wise control over how the batch file is executed. This is useful
|
||||
for debugging or for picking and choosing only particular blocks to run. Use `@batchcode` with the
|
||||
`/interactive` flag to enter interactive mode.
|
||||
|
||||
> @batchcode/interactive tutorial_examples.batch_code
|
||||
|
||||
You should see the following:
|
||||
|
||||
01/02: red_button = create_object(red_button.RedButton, [...] (hh for help)
|
||||
|
||||
This shows that you are on the first `#CODE` block, the first of only two commands in this batch
|
||||
file. Observe that the block has *not* actually been executed at this point!
|
||||
|
||||
To take a look at the full code snippet you are about to run, use `ll` (a batch-processor version of
|
||||
`look`).
|
||||
|
||||
```python
|
||||
from evennia.utils import create, search
|
||||
from evennia.contrib.tutorial_examples import red_button
|
||||
from typeclasses.objects import Object
|
||||
|
||||
limbo = search.objects(caller, 'Limbo', global_search=True)[0]
|
||||
|
||||
red_button = create.create_object(red_button.RedButton, key="Red button",
|
||||
location=limbo, aliases=["button"])
|
||||
|
||||
# caller points to the one running the script
|
||||
caller.msg("A red button was created.")
|
||||
```
|
||||
|
||||
Compare with the example code given earlier. Notice how the content of `#HEADER` has been pasted at
|
||||
the top of the `#CODE` block. Use `pp` to actually execute this block (this will create the button
|
||||
and give you a message). Use `nn` (next) to go to the next command. Use `hh` for a list of commands.
|
||||
|
||||
If there are tracebacks, fix them in the batch file, then use `rr` to reload the file. You will
|
||||
still be at the same code block and can rerun it easily with `pp` as needed. This makes for a simple
|
||||
debug cycle. It also allows you to rerun individual troublesome blocks - as mentioned, in a large
|
||||
batch file this can be very useful (don't forget the `/debug` mode either).
|
||||
|
||||
Use `nn` and `bb` (next and back) to step through the file; e.g. `nn 12` will jump 12 steps forward
|
||||
(without processing any blocks in between). All normal commands of Evennia should work too while
|
||||
working in interactive mode.
|
||||
|
||||
## Limitations and Caveats
|
||||
|
||||
The batch-code processor is by far the most flexible way to build a world in Evennia. There are
|
||||
however some caveats you need to keep in mind.
|
||||
|
||||
### Safety
|
||||
Or rather the lack of it. There is a reason only *superusers* are allowed to run the batch-code
|
||||
processor by default. The code-processor runs **without any Evennia security checks** and allows
|
||||
full access to Python. If an untrusted party could run the code-processor they could execute
|
||||
arbitrary python code on your machine, which is potentially a very dangerous thing. If you want to
|
||||
allow other users to access the batch-code processor you should make sure to run Evennia as a
|
||||
separate and very limited-access user on your machine (i.e. in a 'jail'). By comparison, the batch-
|
||||
command processor is much safer since the user running it is still 'inside' the game and can't
|
||||
really do anything outside what the game commands allow them to.
|
||||
|
||||
### No communication between code blocks
|
||||
Global variables won't work in code batch files, each block is executed as stand-alone environments.
|
||||
`#HEADER` blocks are literally pasted on top of each `#CODE` block so updating some header-variable
|
||||
in your block will not make that change available in another block. Whereas a python execution
|
||||
limitation, allowing this would also lead to very hard-to-debug code when using the interactive mode
|
||||
- this would be a classical example of "spaghetti code".
|
||||
|
||||
The main practical issue with this is when building e.g. a room in one code block and later want to
|
||||
connect that room with a room you built in the current block. There are two ways to do this:
|
||||
|
||||
- Perform a database search for the name of the room you created (since you cannot know in advance
|
||||
which dbref it got assigned). The problem is that a name may not be unique (you may have a lot of "A
|
||||
dark forest" rooms). There is an easy way to handle this though - use [Tags](./Tags.md) or *Aliases*. You
|
||||
can assign any number of tags and/or aliases to any object. Make sure that one of those tags or
|
||||
aliases is unique to the room (like "room56") and you will henceforth be able to always uniquely
|
||||
search and find it later.
|
||||
- Use the `caller` global property as an inter-block storage. For example, you could have a
|
||||
dictionary of room references in an `ndb`:
|
||||
```python
|
||||
#HEADER
|
||||
if caller.ndb.all_rooms is None:
|
||||
caller.ndb.all_rooms = {}
|
||||
|
||||
#CODE
|
||||
# create and store the castle
|
||||
castle = create_object("rooms.Room", key="Castle")
|
||||
caller.ndb.all_rooms["castle"] = castle
|
||||
|
||||
#CODE
|
||||
# in another node we want to access the castle
|
||||
castle = caller.ndb.all_rooms.get("castle")
|
||||
```
|
||||
Note how we check in `#HEADER` if `caller.ndb.all_rooms` doesn't already exist before creating the
|
||||
dict. Remember that `#HEADER` is copied in front of every `#CODE` block. Without that `if` statement
|
||||
we'd be wiping the dict every block!
|
||||
|
||||
### Don't treat a batchcode file like any Python file
|
||||
Despite being a valid Python file, a batchcode file should *only* be run by the batchcode processor.
|
||||
You should not do things like define Typeclasses or Commands in them, or import them into other
|
||||
code. Importing a module in Python will execute base level of the module, which in the case of your
|
||||
average batchcode file could mean creating a lot of new objects every time.
|
||||
### Don't let code rely on the batch-file's real file path
|
||||
|
||||
When you import things into your batchcode file, don't use relative imports but always import with
|
||||
paths starting from the root of your game directory or evennia library. Code that relies on the
|
||||
batch file's "actual" location *will fail*. Batch code files are read as text and the strings
|
||||
executed. When the code runs it has no knowledge of what file those strings where once a part of.
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
# Batch Command Processor
|
||||
|
||||
|
||||
For an introduction and motivation to using batch processors, see [here](./Batch-Processors.md). This
|
||||
page describes the Batch-*command* processor. The Batch-*code* one is covered [here](Batch-Code-
|
||||
Processor).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The batch-command processor is a superuser-only function, invoked by
|
||||
|
||||
> @batchcommand path.to.batchcmdfile
|
||||
|
||||
Where `path.to.batchcmdfile` is the path to a *batch-command file* with the "`.ev`" file ending.
|
||||
This path is given like a python path relative to a folder you define to hold your batch files, set
|
||||
with `BATCH_IMPORT_PATH` in your settings. Default folder is (assuming your game is in the `mygame`
|
||||
folder) `mygame/world`. So if you want to run the example batch file in
|
||||
`mygame/world/batch_cmds.ev`, you could use
|
||||
|
||||
> @batchcommand batch_cmds
|
||||
|
||||
A batch-command file contains a list of Evennia in-game commands separated by comments. The
|
||||
processor will run the batch file from beginning to end. Note that *it will not stop if commands in
|
||||
it fail* (there is no universal way for the processor to know what a failure looks like for all
|
||||
different commands). So keep a close watch on the output, or use *Interactive mode* (see below) to
|
||||
run the file in a more controlled, gradual manner.
|
||||
|
||||
## The batch file
|
||||
|
||||
The batch file is a simple plain-text file containing Evennia commands. Just like you would write
|
||||
them in-game, except you have more freedom with line breaks.
|
||||
|
||||
Here are the rules of syntax of an `*.ev` file. You'll find it's really, really simple:
|
||||
|
||||
- All lines having the `#` (hash)-symbol *as the first one on the line* are considered *comments*.
|
||||
All non-comment lines are treated as a command and/or their arguments.
|
||||
- Comment lines have an actual function -- they mark the *end of the previous command definition*.
|
||||
So never put two commands directly after one another in the file - separate them with a comment, or
|
||||
the second of the two will be considered an argument to the first one. Besides, using plenty of
|
||||
comments is good practice anyway.
|
||||
- A line that starts with the word `#INSERT` is a comment line but also signifies a special
|
||||
instruction. The syntax is `#INSERT <path.batchfile>` and tries to import a given batch-cmd file
|
||||
into this one. The inserted batch file (file ending `.ev`) will run normally from the point of the
|
||||
`#INSERT` instruction.
|
||||
- Extra whitespace in a command definition is *ignored*. - A completely empty line translates in to
|
||||
a line break in texts. Two empty lines thus means a new paragraph (this is obviously only relevant
|
||||
for commands accepting such formatting, such as the `@desc` command).
|
||||
- The very last command in the file is not required to end with a comment.
|
||||
- You *cannot* nest another `@batchcommand` statement into your batch file. If you want to link many
|
||||
batch-files together, use the `#INSERT` batch instruction instead. You also cannot launch the
|
||||
`@batchcode` command from your batch file, the two batch processors are not compatible.
|
||||
|
||||
Below is a version of the example file found in `evennia/contrib/tutorial_examples/batch_cmds.ev`.
|
||||
|
||||
```bash
|
||||
#
|
||||
# This is an example batch build file for Evennia.
|
||||
#
|
||||
|
||||
# This creates a red button
|
||||
@create button:tutorial_examples.red_button.RedButton
|
||||
# (This comment ends input for @create)
|
||||
# Next command. Let's create something.
|
||||
@set button/desc =
|
||||
This is a large red button. Now and then
|
||||
it flashes in an evil, yet strangely tantalizing way.
|
||||
|
||||
A big sign sits next to it. It says:
|
||||
|
||||
|
||||
-----------
|
||||
|
||||
Press me!
|
||||
|
||||
-----------
|
||||
|
||||
|
||||
... It really begs to be pressed! You
|
||||
know you want to!
|
||||
|
||||
# This inserts the commands from another batch-cmd file named
|
||||
# batch_insert_file.ev.
|
||||
#INSERT examples.batch_insert_file
|
||||
|
||||
|
||||
# (This ends the @set command). Note that single line breaks
|
||||
# and extra whitespace in the argument are ignored. Empty lines
|
||||
# translate into line breaks in the output.
|
||||
# Now let's place the button where it belongs (let's say limbo #2 is
|
||||
# the evil lair in our example)
|
||||
@teleport #2
|
||||
# (This comments ends the @teleport command.)
|
||||
# Now we drop it so others can see it.
|
||||
# The very last command in the file needs not be ended with #.
|
||||
drop button
|
||||
```
|
||||
|
||||
To test this, run `@batchcommand` on the file:
|
||||
|
||||
> @batchcommand contrib.tutorial_examples.batch_cmds
|
||||
|
||||
A button will be created, described and dropped in Limbo. All commands will be executed by the user
|
||||
calling the command.
|
||||
|
||||
> Note that if you interact with the button, you might find that its description changes, loosing
|
||||
your custom-set description above. This is just the way this particular object works.
|
||||
|
||||
## Interactive mode
|
||||
|
||||
Interactive mode allows you to more step-wise control over how the batch file is executed. This is
|
||||
useful for debugging and also if you have a large batch file and is only updating a small part of it
|
||||
-- running the entire file again would be a waste of time (and in the case of `@create`-ing objects
|
||||
you would to end up with multiple copies of same-named objects, for example). Use `@batchcommand`
|
||||
with the `/interactive` flag to enter interactive mode.
|
||||
|
||||
> @batchcommand/interactive tutorial_examples.batch_cmds
|
||||
|
||||
You will see this:
|
||||
|
||||
01/04: @create button:tutorial_examples.red_button.RedButton (hh for help)
|
||||
|
||||
This shows that you are on the `@create` command, the first out of only four commands in this batch
|
||||
file. Observe that the command `@create` has *not* been actually processed at this point!
|
||||
|
||||
To take a look at the full command you are about to run, use `ll` (a batch-processor version of
|
||||
`look`). Use `pp` to actually process the current command (this will actually `@create` the button)
|
||||
-- and make sure it worked as planned. Use `nn` (next) to go to the next command. Use `hh` for a
|
||||
list of commands.
|
||||
|
||||
If there are errors, fix them in the batch file, then use `rr` to reload the file. You will still be
|
||||
at the same command and can rerun it easily with `pp` as needed. This makes for a simple debug
|
||||
cycle. It also allows you to rerun individual troublesome commands - as mentioned, in a large batch
|
||||
file this can be very useful. Do note that in many cases, commands depend on the previous ones (e.g.
|
||||
if `@create` in the example above had failed, the following commands would have had nothing to
|
||||
operate on).
|
||||
|
||||
Use `nn` and `bb` (next and back) to step through the file; e.g. `nn 12` will jump 12 steps forward
|
||||
(without processing any command in between). All normal commands of Evennia should work too while
|
||||
working in interactive mode.
|
||||
|
||||
## Limitations and Caveats
|
||||
|
||||
The batch-command processor is great for automating smaller builds or for testing new commands and
|
||||
objects repeatedly without having to write so much. There are several caveats you have to be aware
|
||||
of when using the batch-command processor for building larger, complex worlds though.
|
||||
|
||||
The main issue is that when you run a batch-command script you (*you*, as in your superuser
|
||||
character) are actually moving around in the game creating and building rooms in sequence, just as
|
||||
if you had been entering those commands manually, one by one. You have to take this into account
|
||||
when creating the file, so that you can 'walk' (or teleport) to the right places in order.
|
||||
|
||||
This also means there are several pitfalls when designing and adding certain types of objects. Here
|
||||
are some examples:
|
||||
|
||||
- *Rooms that change your [Command Set](./Command-Sets.md)*: Imagine that you build a 'dark' room, which
|
||||
severely limits the cmdsets of those entering it (maybe you have to find the light switch to
|
||||
proceed). In your batch script you would create this room, then teleport to it - and promptly be
|
||||
shifted into the dark state where none of your normal build commands work ...
|
||||
- *Auto-teleportation*: Rooms that automatically teleport those that enter them to another place
|
||||
(like a trap room, for example). You would be teleported away too.
|
||||
- *Mobiles*: If you add aggressive mobs, they might attack you, drawing you into combat. If they
|
||||
have AI they might even follow you around when building - or they might move away from you before
|
||||
you've had time to finish describing and equipping them!
|
||||
|
||||
The solution to all these is to plan ahead. Make sure that superusers are never affected by whatever
|
||||
effects are in play. Add an on/off switch to objects and make sure it's always set to *off* upon
|
||||
creation. It's all doable, one just needs to keep it in mind.
|
||||
|
||||
## Assorted notes
|
||||
|
||||
The fact that you build as 'yourself' can also be considered an advantage however, should you ever
|
||||
decide to change the default command to allow others than superusers to call the processor. Since
|
||||
normal access-checks are still performed, a malevolent builder with access to the processor should
|
||||
not be able to do all that much damage (this is the main drawback of the [Batch Code
|
||||
Processor](./Batch-Code-Processor.md))
|
||||
|
||||
- [GNU Emacs](https://www.gnu.org/software/emacs/) users might find it interesting to use emacs'
|
||||
*evennia mode*. This is an Emacs major mode found in `evennia/utils/evennia-mode.el`. It offers
|
||||
correct syntax highlighting and indentation with `<tab>` when editing `.ev` files in Emacs. See the
|
||||
header of that file for installation instructions.
|
||||
- [VIM](https://www.vim.org/) users can use amfl's [vim-evennia](https://github.com/amfl/vim-evennia)
|
||||
mode instead, see its readme for install instructions.
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
# Batch Processors
|
||||
|
||||
|
||||
Building a game world is a lot of work, especially when starting out. Rooms should be created,
|
||||
descriptions have to be written, objects must be detailed and placed in their proper places. In many
|
||||
traditional MUD setups you had to do all this online, line by line, over a telnet session.
|
||||
|
||||
Evennia already moves away from much of this by shifting the main coding work to external Python
|
||||
modules. But also building would be helped if one could do some or all of it externally. Enter
|
||||
Evennia's *batch processors* (there are two of them). The processors allows you, as a game admin, to
|
||||
build your game completely offline in normal text files (*batch files*) that the processors
|
||||
understands. Then, when you are ready, you use the processors to read it all into Evennia (and into
|
||||
the database) in one go.
|
||||
|
||||
You can of course still build completely online should you want to - this is certainly the easiest
|
||||
way to go when learning and for small build projects. But for major building work, the advantages of
|
||||
using the batch-processors are many:
|
||||
- It's hard to compete with the comfort of a modern desktop text editor; Compared to a traditional
|
||||
MUD line input, you can get much better overview and many more features. Also, accidentally pressing
|
||||
Return won't immediately commit things to the database.
|
||||
- You might run external spell checkers on your batch files. In the case of one of the batch-
|
||||
processors (the one that deals with Python code), you could also run external debuggers and code
|
||||
analyzers on your file to catch problems before feeding it to Evennia.
|
||||
- The batch files (as long as you keep them) are records of your work. They make a natural starting
|
||||
point for quickly re-building your world should you ever decide to start over.
|
||||
- If you are an Evennia developer, using a batch file is a fast way to setup a test-game after
|
||||
having reset the database.
|
||||
- The batch files might come in useful should you ever decide to distribute all or part of your
|
||||
world to others.
|
||||
|
||||
|
||||
There are two batch processors, the Batch-*command* processor and the Batch-*code* processor. The
|
||||
first one is the simpler of the two. It doesn't require any programming knowledge - you basically
|
||||
just list in-game commands in a text file. The code-processor on the other hand is much more
|
||||
powerful but also more complex - it lets you use Evennia's API to code your world in full-fledged
|
||||
Python code.
|
||||
|
||||
- The [Batch Command Processor](./Batch-Command-Processor.md)
|
||||
- The [Batch Code Processor](./Batch-Code-Processor.md)
|
||||
|
||||
If you plan to use international characters in your batchfiles you are wise to read about *file
|
||||
encodings* below.
|
||||
|
||||
## A note on File Encodings
|
||||
|
||||
As mentioned, both the processors take text files as input and then proceed to process them. As long
|
||||
as you stick to the standard [ASCII](https://en.wikipedia.org/wiki/Ascii) character set (which means
|
||||
the normal English characters, basically) you should not have to worry much about this section.
|
||||
|
||||
Many languages however use characters outside the simple `ASCII` table. Common examples are various
|
||||
apostrophes and umlauts but also completely different symbols like those of the greek or cyrillic
|
||||
alphabets.
|
||||
|
||||
First, we should make it clear that Evennia itself handles international characters just fine. It
|
||||
(and Django) uses [unicode](https://en.wikipedia.org/wiki/Unicode) strings internally.
|
||||
|
||||
The problem is that when reading a text file like the batchfile, we need to know how to decode the
|
||||
byte-data stored therein to universal unicode. That means we need an *encoding* (a mapping) for how
|
||||
the file stores its data. There are many, many byte-encodings used around the world, with opaque
|
||||
names such as `Latin-1`, `ISO-8859-3` or `ARMSCII-8` to pick just a few examples. Problem is that
|
||||
it's practially impossible to determine which encoding was used to save a file just by looking at it
|
||||
(it's just a bunch of bytes!). You have to *know*.
|
||||
|
||||
With this little introduction it should be clear that Evennia can't guess but has to *assume* an
|
||||
encoding when trying to load a batchfile. The text editor and Evennia must speak the same "language"
|
||||
so to speak. Evennia will by default first try the international `UTF-8` encoding, but you can have
|
||||
Evennia try any sequence of different encodings by customizing the `ENCODINGS` list in your settings
|
||||
file. Evennia will use the first encoding in the list that do not raise any errors. Only if none
|
||||
work will the server give up and return an error message.
|
||||
|
||||
You can often change the text editor encoding (this depends on your editor though), otherwise you
|
||||
need to add the editor's encoding to Evennia's `ENCODINGS` list. If you are unsure, write a test
|
||||
file with lots of non-ASCII letters in the editor of your choice, then import to make sure it works
|
||||
as it should.
|
||||
|
||||
More help with encodings can be found in the entry [Text Encodings](../Concepts/Text-Encodings.md) and also in the
|
||||
Wikipedia article [here](https://en.wikipedia.org/wiki/Text_encodings).
|
||||
|
||||
**A footnote for the batch-code processor**: Just because *Evennia* can parse your file and your
|
||||
fancy special characters, doesn't mean that *Python* allows their use. Python syntax only allows
|
||||
international characters inside *strings*. In all other source code only `ASCII` set characters are
|
||||
allowed.
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
# Bootstrap Components and Utilities
|
||||
|
||||
Bootstrap provides many utilities and components you can use when customizing Evennia's web
|
||||
presence. We'll go over a few examples here that you might find useful.
|
||||
> Please take a look at either [the basic web tutorial](../Howtos/Beginner-Tutorial/Part5/Add-a-simple-new-web-page.md) or
|
||||
>[the web character view tutorial](../Howtos/Web-Character-View-Tutorial.md)
|
||||
> to get a feel for how to add pages to Evennia's website to test these examples.
|
||||
|
||||
## General Styling
|
||||
Bootstrap provides base styles for your site. These can be customized through CSS, but the default
|
||||
styles are intended to provide a consistent, clean look for sites.
|
||||
|
||||
### Color
|
||||
Most elements can be styled with default colors. [Take a look at the documentation](https://getbootstrap.com/docs/4.0/utilities/colors/) to learn more about these colors
|
||||
- suffice to say, adding a class of text-* or bg-*, for instance, text-primary, sets the text color
|
||||
or background color.
|
||||
|
||||
### Borders
|
||||
Simply adding a class of 'border' to an element adds a border to the element. For more in-depth
|
||||
info, please [read the documentation on borders.](https://getbootstrap.com/docs/4.0/utilities/borders/).
|
||||
```
|
||||
<span class="border border-dark"></span>
|
||||
```
|
||||
You can also easily round corners just by adding a class.
|
||||
```
|
||||
<img src="..." class="rounded" />
|
||||
```
|
||||
|
||||
### Spacing
|
||||
Bootstrap provides classes to easily add responsive margin and padding. Most of the time, you might
|
||||
like to add margins or padding through CSS itself - however these classes are used in the default
|
||||
Evennia site. [Take a look at the docs](https://getbootstrap.com/docs/4.0/utilities/spacing/) to
|
||||
learn more.
|
||||
|
||||
***
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
[Buttons](https://getbootstrap.com/docs/4.0/components/buttons/) in Bootstrap are very easy to use -
|
||||
button styling can be added to `<button>`, `<a>`, and `<input>` elements.
|
||||
```
|
||||
<a class="btn btn-primary" href="#" role="button">I'm a Button</a>
|
||||
<button class="btn btn-primary" type="submit">Me too!</button>
|
||||
<input class="btn btn-primary" type="button" value="Button">
|
||||
<input class="btn btn-primary" type="submit" value="Also a Button">
|
||||
<input class="btn btn-primary" type="reset" value="Button as Well">
|
||||
```
|
||||
### Cards
|
||||
[Cards](https://getbootstrap.com/docs/4.0/components/card/) provide a container for other elements
|
||||
that stands out from the rest of the page. The "Accounts", "Recently Connected", and "Database
|
||||
Stats" on the default webpage are all in cards. Cards provide quite a bit of formatting options -
|
||||
the following is a simple example, but read the documentation or look at the site's source for more.
|
||||
```
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Card title</h4>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
|
||||
<p class="card-text">Fancy, isn't it?</p>
|
||||
<a href="#" class="card-link">Card link</a>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Jumbotron
|
||||
[Jumbotrons](https://getbootstrap.com/docs/4.0/components/jumbotron/) are useful for featuring an
|
||||
image or tagline for your game. They can flow with the rest of your content or take up the full
|
||||
width of the page - Evennia's base site uses the former.
|
||||
```
|
||||
<div class="jumbotron jumbotron-fluid">
|
||||
<div class="container">
|
||||
<h1 class="display-3">Full Width Jumbotron</h1>
|
||||
<p class="lead">Look at the source of the default Evennia page for a regular Jumbotron</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Forms
|
||||
[Forms](https://getbootstrap.com/docs/4.0/components/forms/) are highly customizable with Bootstrap.
|
||||
For a more in-depth look at how to use forms and their styles in your own Evennia site, please read
|
||||
over [the web character gen tutorial.](../Howtos/Web-Character-Generation.md)
|
||||
|
|
@ -1,380 +0,0 @@
|
|||
# Channels
|
||||
|
||||
In a multiplayer game, players often need other means of in-game communication
|
||||
than moving to the same room and use `say` or `emote`.
|
||||
|
||||
_Channels_ allows Evennia's to act as a fancy chat program. When a player is
|
||||
connected to a channel, sending a message to it will automatically distribute
|
||||
it to every other subscriber.
|
||||
|
||||
Channels can be used both for chats between [Accounts](./Accounts.md) and between
|
||||
[Objects](./Objects.md) (usually Characters). Chats could be both OOC
|
||||
(out-of-character) or IC (in-charcter) in nature. Some examples:
|
||||
|
||||
- A support channel for contacting staff (OOC)
|
||||
- A general chat for discussing anything and foster community (OOC)
|
||||
- Admin channel for private staff discussions (OOC)
|
||||
- Private guild channels for planning and organization (IC/OOC depending on game)
|
||||
- Cyberpunk-style retro chat rooms (IC)
|
||||
- In-game radio channels (IC)
|
||||
- Group telephathy (IC)
|
||||
- Walkie talkies (IC)
|
||||
|
||||
```{versionchanged} 1.0
|
||||
|
||||
Channel system changed to use a central 'channel' command and nicks instead of
|
||||
auto-generated channel-commands and -cmdset. ChannelHandler was removed.
|
||||
|
||||
```
|
||||
|
||||
## Using channels in-game
|
||||
|
||||
In the default command set, channels are all handled via the mighty
|
||||
[channel
|
||||
command](evennia.commands.default.comms.CmdChannel), `channel` (or
|
||||
`chan`). By default, this command will assume all entities dealing with
|
||||
channels are `Accounts`.
|
||||
|
||||
### Viewing and joining channels
|
||||
|
||||
channel - shows your subscriptions
|
||||
channel/all - shows all subs available to you
|
||||
channel/who - shows who subscribes to this channel
|
||||
|
||||
To join/unsub a channel do
|
||||
|
||||
channel/sub channelname
|
||||
channel/unsub channelname
|
||||
|
||||
If you temporarily don't want to hear the channel for a while (without actually
|
||||
unsubscribing), you can mute it:
|
||||
|
||||
channel/mute channelname
|
||||
channel/unmute channelname
|
||||
|
||||
### Chat on channels
|
||||
|
||||
To speak on a channel, do
|
||||
|
||||
channel public Hello world!
|
||||
|
||||
If the channel-name has spaces in it, you need to use a '`=`':
|
||||
|
||||
channel rest room = Hello world!
|
||||
|
||||
Now, this is more to type than we'd like, so when you join a channel, the
|
||||
system automatically sets up an personal alias so you can do this instead:
|
||||
|
||||
public Hello world
|
||||
|
||||
```{warning}
|
||||
|
||||
This shortcut will not work if the channel-name has spaces in it.
|
||||
So channels with long names should make sure to provide a one-word alias as
|
||||
well.
|
||||
```
|
||||
|
||||
Any user can make up their own channel aliases:
|
||||
|
||||
channel/alias public = foo;bar
|
||||
|
||||
You can now just do
|
||||
|
||||
foo Hello world!
|
||||
bar Hello again!
|
||||
|
||||
And even remove the default one if they don't want to use it
|
||||
|
||||
channel/unalias public
|
||||
public Hello (gives a command-not-found error now)
|
||||
|
||||
But you can also use your alias with the `channel` command:
|
||||
|
||||
channel foo Hello world!
|
||||
|
||||
> What happens when aliasing is that a [nick](./Nicks.md) is created that maps your
|
||||
> alias + argument onto calling the `channel` command. So when you enter `foo hello`,
|
||||
> what the server sees is actually `channel foo = hello`. The system is also
|
||||
> clever enough to know that whenever you search for channels, your channel-nicks
|
||||
> should also be considered so as to convert your input to an existing channel name.
|
||||
|
||||
You can check if you missed channel conversations by viewing the channel's
|
||||
scrollback with
|
||||
|
||||
channel/history public
|
||||
|
||||
This retrieves the last 20 lines of text (also from a time when you were
|
||||
offline). You can step further back by specifying how many lines back to start:
|
||||
|
||||
channel/history public = 30
|
||||
|
||||
This again retrieve 20 lines, but starting 30 lines back (so you'll get lines
|
||||
30-50 counting backwards).
|
||||
|
||||
|
||||
### Channel administration
|
||||
|
||||
To create/destroy a new channel you can do
|
||||
|
||||
channel/create channelname;alias;alias = description
|
||||
channel/destroy channelname
|
||||
|
||||
Aliases are optional but can be good for obvious shortcuts everyone may want to
|
||||
use. The description is used in channel-listings. You will automatically join a
|
||||
channel you created and will be controlling it. You can also use `channel/desc` to
|
||||
change the description on a channel you wnn later.
|
||||
|
||||
If you control a channel you can also kick people off it:
|
||||
|
||||
channel/boot mychannel = annoyinguser123 : stop spamming!
|
||||
|
||||
The last part is an optional reason to send to the user before they are booted.
|
||||
You can give a comma-separated list of channels to kick the same user from all
|
||||
those channels at once. The user will be unsubbed from the channel and all
|
||||
their aliases will be wiped. But they can still rejoin if they like.
|
||||
|
||||
channel/ban mychannel = annoyinguser123
|
||||
channel/ban - view bans
|
||||
channel/unban mychannel = annoyinguser123
|
||||
|
||||
Banning adds the user to the channels blacklist. This means they will not be
|
||||
able to _rejoin_ if you boot them. You will need to run `channel/boot` to
|
||||
actually kick them out.
|
||||
|
||||
See the [Channel command](evennia.commands.default.comms.CmdChannel) api
|
||||
docs (and in-game help) for more details.
|
||||
|
||||
Admin-level users can also modify channel's [locks](./Locks.md):
|
||||
|
||||
channel/lock buildchannel = listen:all();send:perm(Builders)
|
||||
|
||||
Channels use three lock-types by default:
|
||||
|
||||
- `listen` - who may listen to the channel. Users without this access will not
|
||||
even be able to join the channel and it will not appear in listings for them.
|
||||
- `send` - who may send to the channel.
|
||||
- `control` - this is assigned to you automatically when you create the channel. With
|
||||
control over the channel you can edit it, boot users and do other management tasks.
|
||||
|
||||
|
||||
#### Restricting channel administration
|
||||
|
||||
By default everyone can use the channel command ([evennia.commands.default.comms.CmdChannel](evennia.commands.default.comms.CmdChannel))
|
||||
to create channels and will then control the channels they created (to boot/ban
|
||||
people etc). If you as a developer does not want regular players to do this
|
||||
(perhaps you want only staff to be able to spawn new channels), you can
|
||||
override the `channel` command and change its `locks` property.
|
||||
|
||||
The default `help` command has the following `locks` property:
|
||||
|
||||
```python
|
||||
locks = "cmd:not perm(channel_banned); admin:all(); manage:all(); changelocks: perm(Admin)"
|
||||
```
|
||||
|
||||
This is a regular [lockstring](./Locks.md).
|
||||
|
||||
- `cmd: pperm(channel_banned)` - The `cmd` locktype is the standard one used for all Commands.
|
||||
an accessing object failing this will not even know that the command exists. The `pperm()` lockfunc
|
||||
checks an on-account [Permission](Building Permissions) 'channel_banned' - and the `not` means
|
||||
that if they _have_ that 'permission' they are cut off from using the `channel` command. You usually
|
||||
don't need to change this lock.
|
||||
- `admin:all()` - this is a lock checked in the `channel` command itself. It controls access to the
|
||||
`/boot`, `/ban` and `/unban` switches (by default letting everyone use them).
|
||||
- `manage:all()` - this controls access to the `/create`, `/destroy`, `/desc` switches.
|
||||
- `changelocks: perm(Admin)` - this controls access to the `/lock` and `/unlock` switches. By
|
||||
default this is something only [Admins](Building Permissions) can change.
|
||||
|
||||
> Note - while `admin:all()` and `manage:all()` will let everyone use these switches, users
|
||||
> will still only be able to admin or destroy channels they actually control!
|
||||
|
||||
If you only want (say) Builders and higher to be able to create and admin
|
||||
channels you could override the `help` command and change the lockstring to:
|
||||
|
||||
```python
|
||||
# in for example mygame/commands/commands.py
|
||||
|
||||
from evennia import default_cmds
|
||||
|
||||
class MyCustomChannelCmd(default_cmds.CmdChannel):
|
||||
locks = "cmd: not pperm(channel_banned);admin:perm(Builder);manage:perm(Builder);changelocks:perm(Admin)"
|
||||
|
||||
```
|
||||
|
||||
Add this custom command to your default cmdset and regular users wil now get an
|
||||
access-denied error when trying to use use these switches.
|
||||
|
||||
## Allowing Characters to use Channels
|
||||
|
||||
The default `channel` command ([evennia.commands.default.comms.CmdChannel](evennia.commands.default.comms.CmdChannel))
|
||||
sits in the `Account` [command set](./Command-Sets.md). It is set up such that it will
|
||||
always operate on `Accounts`, even if you were to add it to the
|
||||
`CharacterCmdSet`.
|
||||
|
||||
It's a one-line change to make this command accept non-account callers. But for
|
||||
convenience we provide a version for Characters/Objects. Just import
|
||||
[evennia.commands.default.comms.CmdObjectChannel](evennia.commands.default.comms.CmdObjectChannel)
|
||||
and inherit from that instead.
|
||||
|
||||
## Customizing channel output and behavior
|
||||
|
||||
When distributing a message, the channel will call a series of hooks on itself
|
||||
and (more importantly) on each recipient. So you can customize things a lot by
|
||||
just modifying hooks on your normal Object/Account typeclasses.
|
||||
|
||||
Internally, the message is sent with
|
||||
`channel.msg(message, senders=sender, bypass_mute=False, **kwargs)`, where
|
||||
`bypass_mute=True` means the message ignores muting (good for alerts or if you
|
||||
delete the channel etc) and `**kwargs` are any extra info you may want to pass
|
||||
to the hooks. The `senders` (it's always only one in the default implementation
|
||||
but could in principle be multiple) and `bypass_mute` are part of the `kwargs`
|
||||
below:
|
||||
|
||||
1. `channel.at_pre_msg(message, **kwargs)`
|
||||
2. For each recipient:
|
||||
- `message = recipient.at_pre_channel_msg(message, channel, **kwargs)` -
|
||||
allows for the message to be tweaked per-receiver (for example coloring it depending
|
||||
on the users' preferences). If this method returns `False/None`, that
|
||||
recipient is skipped.
|
||||
- `recipient.channel_msg(message, channel, **kwargs)` - actually sends to recipient.
|
||||
- `recipient.at_post_channel_msg(message, channel, **kwargs)` - any post-receive effects.
|
||||
3. `channel.at_post_channel_msg(message, **kwargs)`
|
||||
|
||||
Note that `Accounts` and `Objects` both have their have separate sets of hooks.
|
||||
So make sure you modify the set actually used by your subcribers (or both).
|
||||
Default channels all use `Account` subscribers.
|
||||
|
||||
## Channels in code
|
||||
|
||||
For most common changes, the default channel, the recipient hooks and possibly
|
||||
overriding the `channel` command will get you very far. But you can also tweak
|
||||
channels themselves.
|
||||
|
||||
Channels are [Typeclassed](./Typeclasses.md) entities. This means they are
|
||||
persistent in the database, can have [attributes](./Attributes.md) and [Tags](./Tags.md)
|
||||
and can be easily extended.
|
||||
|
||||
To change which channel typeclass Evennia uses for default commands, change
|
||||
`settings.BASE_CHANNEL_TYPECLASS`. The base command class is
|
||||
[`evennia.comms.comms.DefaultChannel`](evennia.comms.comms.DefaultChannel).
|
||||
There is an empty child class in `mygame/typeclasses/channels.py`, same
|
||||
as for other typelass-bases.
|
||||
|
||||
In code you create a new channel with `evennia.create_channel` or
|
||||
`Channel.create`:
|
||||
|
||||
```python
|
||||
from evennia import create_channel, search_object
|
||||
from typeclasses.channels import Channel
|
||||
|
||||
channel = create_channel("my channel", aliases=["mychan"], locks=..., typeclass=...)
|
||||
# alternative
|
||||
channel = Channel.create("my channel", aliases=["mychan"], locks=...)
|
||||
|
||||
# connect to it
|
||||
me = search_object(key="Foo")[0]
|
||||
channel.connect(me)
|
||||
|
||||
# send to it (this will trigger the channel_msg hooks described earlier)
|
||||
channel.msg("Hello world!", senders=me)
|
||||
|
||||
# view subscriptions (the SubscriptionHandler handles all subs under the hood)
|
||||
channel.subscriptions.has(me) # check we subbed
|
||||
channel.subscriptions.all() # get all subs
|
||||
channel.subscriptions.online() # get only subs currently online
|
||||
channel.subscriptions.clear() # unsub all
|
||||
|
||||
# leave channel
|
||||
channel.disconnect(me)
|
||||
|
||||
# permanently delete channel (will unsub everyone)
|
||||
channel.delete()
|
||||
|
||||
```
|
||||
|
||||
The Channel's `.connect` method will accept both `Account` and `Object` subscribers
|
||||
and will handle them transparently.
|
||||
|
||||
The channel has many more hooks, both hooks shared with all typeclasses as well
|
||||
as special ones related to muting/banning etc. See the channel class for
|
||||
details.
|
||||
|
||||
## Channel logging
|
||||
|
||||
```{versionchanged} 0.7
|
||||
|
||||
Channels changed from using Msg to TmpMsg and optional log files.
|
||||
```
|
||||
```{versionchanged} 1.0
|
||||
|
||||
Channels stopped supporting Msg and TmpMsg, using only log files.
|
||||
```
|
||||
|
||||
The channel messages are not stored in the database. A channel is instead
|
||||
always logged to a regular text log-file
|
||||
`mygame/server/logs/channel_<channelname>.log`. This is where `channels/history channelname`
|
||||
gets its data from. A channel's log will rotate when it grows too big, which
|
||||
thus also automatically limits the max amount of history a user can view with
|
||||
`/history`.
|
||||
|
||||
The log file name is set on the channel class as the `log_file` property. This
|
||||
is a string that takes the formatting token `{channelname}` to be replaced with
|
||||
the (lower-case) name of the channel. By default the log is written to in the
|
||||
channel's `at_post_channel_msg` method.
|
||||
|
||||
|
||||
### Properties on Channels
|
||||
|
||||
Channels have all the standard properties of a Typeclassed entity (`key`,
|
||||
`aliases`, `attributes`, `tags`, `locks` etc). This is not an exhaustive list;
|
||||
see the [Channel api docs](evennia.comms.comms.DefaultChannel) for details.
|
||||
|
||||
- `send_to_online_only` - this class boolean defaults to `True` and is a
|
||||
sensible optimization since people offline people will not see the message anyway.
|
||||
- `log_file` - this is a string that determines the name of the channel log file. Default
|
||||
is `"channel_{channelname}.log"`. The log file will appear in `settings.LOG_DIR` (usually
|
||||
`mygame/server/logs/`). You should usually not change this.
|
||||
- `channel_prefix_string` - this property is a string to easily change how
|
||||
the channel is prefixed. It takes the `channelname` format key. Default is `"[{channelname}] "`
|
||||
and produces output like `[public] ...`.
|
||||
- `subscriptions` - this is the [SubscriptionHandler](evennia.comms.models.SubscriptionHandler), which
|
||||
has methods `has`, `add`, `remove`, `all`, `clear` and also `online` (to get
|
||||
only actually online channel-members).
|
||||
- `wholist`, `mutelist`, `banlist` are properties that return a list of subscribers,
|
||||
as well as who are currently muted or banned.
|
||||
- `channel_msg_nick_pattern` - this is a regex pattern for performing the in-place nick
|
||||
replacement (detect that `channelalias <msg` means that you want to send a message to a channel).
|
||||
This pattern accepts an `{alias}` formatting marker. Don't mess with this unless you really
|
||||
want to change how channels work.
|
||||
- `channel_msg_nick_replacement` - this is a string on the [nick replacement
|
||||
- form](./Nicks.md). It accepts the `{channelname}` formatting tag. This is strongly tied to the
|
||||
`channel` command and is by default `channel {channelname} = $1`.
|
||||
|
||||
Notable `Channel` hooks:
|
||||
|
||||
- `at_pre_channel_msg(message, **kwargs)` - called before sending a message, to
|
||||
modify it. Not used by default.
|
||||
- `msg(message, senders=..., bypass_mute=False, **kwargs)` - send the message onto
|
||||
the channel. The `**kwargs` are passed on into the other call hooks (also on the recipient).
|
||||
- `at_post_channel_msg(message, **kwargs)` - by default this is used to store the message
|
||||
to the log file.
|
||||
- `channel_prefix(message)` - this is called to allow the channel to prefix. This is called
|
||||
by the object/account when they build the message, so if wanting something else one can
|
||||
also just remove that call.
|
||||
- every channel message. By default it just returns `channel_prefix_string`.
|
||||
- `has_connection(subscriber)` - shortcut to check if an entity subscribes to
|
||||
this channel.
|
||||
- `mute/unmute(subscriber)` - this mutes the channel for this user.
|
||||
- `ban/unban(subscriber)` - adds/remove user from banlist.
|
||||
- `connect/disconnect(subscriber)` - adds/removes a subscriber.
|
||||
- `add_user_channel_alias(user, alias, **kwargs)` - sets up a user-nick for this channel. This is
|
||||
what maps e.g. `alias <msg>` to `channel channelname = <msg>`.
|
||||
- `remove_user_channel_alias(user, alias, **kwargs)` - remove an alias. Note that this is
|
||||
a class-method that will happily remove found channel-aliases from the user linked to _any_
|
||||
channel, not only from the channel the method is called on.
|
||||
- `pre_join_channel(subscriber)` - if this returns `False`, connection will be refused.
|
||||
- `post_join_channel(subscriber)` - by default this sets up a users's channel-nicks/aliases.
|
||||
- `pre_leave_channel(subscriber)` - if this returns `False`, the user is not allowed to leave.
|
||||
- `post_leave_channel(subscriber)` - this will clean up any channel aliases/nicks of the user.
|
||||
- `delete` the standard typeclass-delete mechanism will also automatically un-subscribe all
|
||||
subscribers (and thus wipe all their aliases).
|
||||
|
||||
|
|
@ -1,297 +0,0 @@
|
|||
# Coding Utils
|
||||
|
||||
|
||||
Evennia comes with many utilities to help with common coding tasks. Most are accessible directly
|
||||
from the flat API, otherwise you can find them in the `evennia/utils/` folder.
|
||||
|
||||
## Searching
|
||||
|
||||
A common thing to do is to search for objects. There it's easiest to use the `search` method defined
|
||||
on all objects. This will search for objects in the same location and inside the self object:
|
||||
|
||||
```python
|
||||
obj = self.search(objname)
|
||||
```
|
||||
|
||||
The most common time one needs to do this is inside a command body. `obj =
|
||||
self.caller.search(objname)` will search inside the caller's (typically, the character that typed
|
||||
the command) `.contents` (their "inventory") and `.location` (their "room").
|
||||
|
||||
Give the keyword `global_search=True` to extend search to encompass entire database. Aliases will
|
||||
also be matched by this search. You will find multiple examples of this functionality in the default
|
||||
command set.
|
||||
|
||||
If you need to search for objects in a code module you can use the functions in
|
||||
`evennia.utils.search`. You can access these as shortcuts `evennia.search_*`.
|
||||
|
||||
```python
|
||||
from evennia import search_object
|
||||
obj = search_object(objname)
|
||||
```
|
||||
|
||||
- [`evennia.search_account`](evennia.accounts.manager.AccountDBManager.search_account)
|
||||
- [`evennia.search_object`](evennia.objects.manager.ObjectDBManager.search_object)
|
||||
- [`evennia.search(object)_by_tag`](evennia.utils.search.search_tag)
|
||||
- [`evennia.search_script`](evennia.scripts.manager.ScriptDBManager.search_script)
|
||||
- [`evennia.search_channel`](evennia.comms.managers.ChannelDBManager.search_channel)
|
||||
- [`evennia.search_message`](evennia.comms.managers.MsgManager.search_message)
|
||||
- [`evennia.search_help`](evennia.help.manager.HelpEntryManager.search_help)
|
||||
|
||||
Note that these latter methods will always return a `list` of results, even if the list has one or
|
||||
zero entries.
|
||||
|
||||
## Create
|
||||
|
||||
Apart from the in-game build commands (`@create` etc), you can also build all of Evennia's game
|
||||
entities directly in code (for example when defining new create commands).
|
||||
```python
|
||||
import evennia
|
||||
|
||||
myobj = evennia.create_objects("game.gamesrc.objects.myobj.MyObj", key="MyObj")
|
||||
```
|
||||
|
||||
- [`evennia.create_account`](evennia.utils.create.create_account)
|
||||
- [`evennia.create_object`](evennia.utils.create.create_object)
|
||||
- [`evennia.create_script`](evennia.utils.create.create_script)
|
||||
- [`evennia.create_channel`](evennia.utils.create.create_channel)
|
||||
- [`evennia.create_help_entry`](evennia.utils.create.create_help_entry)
|
||||
- [`evennia.create_message`](evennia.utils.create.create_message)
|
||||
|
||||
Each of these create-functions have a host of arguments to further customize the created entity. See
|
||||
`evennia/utils/create.py` for more information.
|
||||
|
||||
## Logging
|
||||
|
||||
Normally you can use Python `print` statements to see output to the terminal/log. The `print`
|
||||
statement should only be used for debugging though. For producion output, use the `logger` which
|
||||
will create proper logs either to terminal or to file.
|
||||
|
||||
```python
|
||||
from evennia import logger
|
||||
#
|
||||
logger.log_err("This is an Error!")
|
||||
logger.log_warn("This is a Warning!")
|
||||
logger.log_info("This is normal information")
|
||||
logger.log_dep("This feature is deprecated")
|
||||
```
|
||||
|
||||
There is a special log-message type, `log_trace()` that is intended to be called from inside a
|
||||
traceback - this can be very useful for relaying the traceback message back to log without having it
|
||||
kill the server.
|
||||
|
||||
```python
|
||||
try:
|
||||
# [some code that may fail...]
|
||||
except Exception:
|
||||
logger.log_trace("This text will show beneath the traceback itself.")
|
||||
```
|
||||
|
||||
The `log_file` logger, finally, is a very useful logger for outputting arbitrary log messages. This
|
||||
is a heavily optimized asynchronous log mechanism using
|
||||
[threads](https://en.wikipedia.org/wiki/Thread_%28computing%29) to avoid overhead. You should be
|
||||
able to use it for very heavy custom logging without fearing disk-write delays.
|
||||
|
||||
```python
|
||||
logger.log_file(message, filename="mylog.log")
|
||||
```
|
||||
|
||||
If not an absolute path is given, the log file will appear in the `mygame/server/logs/` directory.
|
||||
If the file already exists, it will be appended to. Timestamps on the same format as the normal
|
||||
Evennia logs will be automatically added to each entry. If a filename is not specified, output will
|
||||
be written to a file `game/logs/game.log`.
|
||||
|
||||
## Time Utilities
|
||||
### Game time
|
||||
|
||||
Evennia tracks the current server time. You can access this time via the `evennia.gametime`
|
||||
shortcut:
|
||||
|
||||
```python
|
||||
from evennia import gametime
|
||||
|
||||
# all the functions below return times in seconds).
|
||||
|
||||
# total running time of the server
|
||||
runtime = gametime.runtime()
|
||||
# time since latest hard reboot (not including reloads)
|
||||
uptime = gametime.uptime()
|
||||
# server epoch (its start time)
|
||||
server_epoch = gametime.server_epoch()
|
||||
|
||||
# in-game epoch (this can be set by `settings.TIME_GAME_EPOCH`.
|
||||
# If not, the server epoch is used.
|
||||
game_epoch = gametime.game_epoch()
|
||||
# in-game time passed since time started running
|
||||
gametime = gametime.gametime()
|
||||
# in-game time plus game epoch (i.e. the current in-game
|
||||
# time stamp)
|
||||
gametime = gametime.gametime(absolute=True)
|
||||
# reset the game time (back to game epoch)
|
||||
gametime.reset_gametime()
|
||||
|
||||
```
|
||||
|
||||
The setting `TIME_FACTOR` determines how fast/slow in-game time runs compared to the real world. The
|
||||
setting `TIME_GAME_EPOCH` sets the starting game epoch (in seconds). The functions from the
|
||||
`gametime` module all return their times in seconds. You can convert this to whatever units of time
|
||||
you desire for your game. You can use the `@time` command to view the server time info.
|
||||
|
||||
You can also *schedule* things to happen at specific in-game times using the
|
||||
[gametime.schedule](evennia.utils.gametime.schedule) function:
|
||||
|
||||
```python
|
||||
import evennia
|
||||
|
||||
def church_clock:
|
||||
limbo = evennia.search_object(key="Limbo")
|
||||
limbo.msg_contents("The church clock chimes two.")
|
||||
|
||||
gametime.schedule(church_clock, hour=2)
|
||||
```
|
||||
|
||||
### utils.time_format()
|
||||
|
||||
This function takes a number of seconds as input (e.g. from the `gametime` module above) and
|
||||
converts it to a nice text output in days, hours etc. It's useful when you want to show how old
|
||||
something is. It converts to four different styles of output using the *style* keyword:
|
||||
|
||||
- style 0 - `5d:45m:12s` (standard colon output)
|
||||
- style 1 - `5d` (shows only the longest time unit)
|
||||
- style 2 - `5 days, 45 minutes` (full format, ignores seconds)
|
||||
- style 3 - `5 days, 45 minutes, 12 seconds` (full format, with seconds)
|
||||
|
||||
### utils.delay()
|
||||
|
||||
```python
|
||||
from evennia import utils
|
||||
|
||||
def _callback(obj, text):
|
||||
obj.msg(text)
|
||||
|
||||
# wait 10 seconds before sending "Echo!" to obj (which we assume is defined)
|
||||
deferred = utils.delay(10, _callback, obj, "Echo!", persistent=False)
|
||||
|
||||
# code here will run immediately, not waiting for the delay to fire!
|
||||
|
||||
```
|
||||
|
||||
This creates an asynchronous delayed call. It will fire the given callback function after the given
|
||||
number of seconds. This is a very light wrapper over a Twisted
|
||||
[Deferred](https://twistedmatrix.com/documents/current/core/howto/defer.html). Normally this is run
|
||||
non-persistently, which means that if the server is `@reload`ed before the delay is over, the
|
||||
callback will never run (the server forgets it). If setting `persistent` to True, the delay will be
|
||||
stored in the database and survive a `@reload` - but for this to work it is susceptible to the same
|
||||
limitations incurred when saving to an [Attribute](./Attributes.md).
|
||||
|
||||
The `deferred` return object can usually be ignored, but calling its `.cancel()` method will abort
|
||||
the delay prematurely.
|
||||
|
||||
`utils.delay` is the lightest form of delayed call in Evennia. For other way to create time-bound
|
||||
tasks, see the [TickerHandler](./TickerHandler.md) and [Scripts](./Scripts.md).
|
||||
|
||||
> Note that many delayed effects can be achieved without any need for an active timer. For example
|
||||
if you have a trait that should recover a point every 5 seconds you might just need its value when
|
||||
it's needed, but checking the current time and calculating on the fly what value it should have.
|
||||
|
||||
## Object Classes
|
||||
### utils.inherits_from()
|
||||
|
||||
This useful function takes two arguments - an object to check and a parent. It returns `True` if
|
||||
object inherits from parent *at any distance* (as opposed to Python's in-built `is_instance()` that
|
||||
will only catch immediate dependence). This function also accepts as input any combination of
|
||||
classes, instances or python-paths-to-classes.
|
||||
|
||||
Note that Python code should usually work with [duck
|
||||
typing](https://en.wikipedia.org/wiki/Duck_typing). But in Evennia's case it can sometimes be useful
|
||||
to check if an object inherits from a given [Typeclass](./Typeclasses.md) as a way of identification. Say
|
||||
for example that we have a typeclass *Animal*. This has a subclass *Felines* which in turn has a
|
||||
subclass *HouseCat*. Maybe there are a bunch of other animal types too, like horses and dogs. Using
|
||||
`inherits_from` will allow you to check for all animals in one go:
|
||||
|
||||
```python
|
||||
from evennia import utils
|
||||
if (utils.inherits_from(obj, "typeclasses.objects.animals.Animal"):
|
||||
obj.msg("The bouncer stops you in the door. He says: 'No talking animals allowed.'")
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Text utilities
|
||||
|
||||
In a text game, you are naturally doing a lot of work shuffling text back and forth. Here is a *non-
|
||||
complete* selection of text utilities found in `evennia/utils/utils.py` (shortcut `evennia.utils`).
|
||||
If nothing else it can be good to look here before starting to develop a solution of your own.
|
||||
|
||||
### utils.fill()
|
||||
|
||||
This flood-fills a text to a given width (shuffles the words to make each line evenly wide). It also
|
||||
indents as needed.
|
||||
|
||||
```python
|
||||
outtxt = fill(intxt, width=78, indent=4)
|
||||
```
|
||||
|
||||
### utils.crop()
|
||||
|
||||
This function will crop a very long line, adding a suffix to show the line actually continues. This
|
||||
can be useful in listings when showing multiple lines would mess up things.
|
||||
|
||||
```python
|
||||
intxt = "This is a long text that we want to crop."
|
||||
outtxt = crop(intxt, width=19, suffix="[...]")
|
||||
# outtxt is now "This is a long text[...]"
|
||||
```
|
||||
|
||||
### utils.dedent()
|
||||
|
||||
This solves what may at first glance appear to be a trivial problem with text - removing
|
||||
indentations. It is used to shift entire paragraphs to the left, without disturbing any further
|
||||
formatting they may have. A common case for this is when using Python triple-quoted strings in code
|
||||
- they will retain whichever indentation they have in the code, and to make easily-readable source
|
||||
code one usually don't want to shift the string to the left edge.
|
||||
|
||||
```python
|
||||
#python code is entered at a given indentation
|
||||
intxt = """
|
||||
This is an example text that will end
|
||||
up with a lot of whitespace on the left.
|
||||
It also has indentations of
|
||||
its own."""
|
||||
outtxt = dedent(intxt)
|
||||
# outtxt will now retain all internal indentation
|
||||
# but be shifted all the way to the left.
|
||||
```
|
||||
|
||||
Normally you do the dedent in the display code (this is for example how the help system homogenizes
|
||||
help entries).
|
||||
|
||||
### to_str() and to_bytes()
|
||||
|
||||
Evennia supplies two utility functions for converting text to the correct
|
||||
encodings. `to_str()` and `to_bytes()`. Unless you are adding a custom protocol and
|
||||
need to send byte-data over the wire, `to_str` is the only one you'll need.
|
||||
|
||||
The difference from Python's in-built `str()` and `bytes()` operators are that
|
||||
the Evennia ones makes use of the `ENCODINGS` setting and will try very hard to
|
||||
never raise a traceback but instead echo errors through logging. See
|
||||
[here](../Concepts/Text-Encodings.md) for more info.
|
||||
|
||||
### Ansi Coloring Tools
|
||||
- [evennia.utils.ansi](evennia.utils.ansi)
|
||||
|
||||
## Display utilities
|
||||
### Making ascii tables
|
||||
|
||||
The [EvTable](evennia.utils.evtable.EvTable) class (`evennia/utils/evtable.py`) can be used
|
||||
to create correctly formatted text tables. There is also
|
||||
[EvForm](evennia.utils.evform.EvForm) (`evennia/utils/evform.py`). This reads a fixed-format
|
||||
text template from a file in order to create any level of sophisticated ascii layout. Both evtable
|
||||
and evform have lots of options and inputs so see the header of each module for help.
|
||||
|
||||
The third-party [PrettyTable](https://code.google.com/p/prettytable/) module is also included in
|
||||
Evennia. PrettyTable is considered deprecated in favor of EvTable since PrettyTable cannot handle
|
||||
ANSI colour. PrettyTable can be found in `evennia/utils/prettytable/`. See its homepage above for
|
||||
instructions.
|
||||
|
||||
### Menus
|
||||
- [evennia.EvMenu](evennia.utils.evmenu.EvMenu)
|
||||
|
|
@ -1,376 +0,0 @@
|
|||
# Command Sets
|
||||
|
||||
|
||||
Command Sets are intimately linked with [Commands](./Commands.md) and you should be familiar with
|
||||
Commands before reading this page. The two pages were split for ease of reading.
|
||||
|
||||
A *Command Set* (often referred to as a CmdSet or cmdset) is the basic unit for storing one or more
|
||||
*Commands*. A given Command can go into any number of different command sets. Storing Command
|
||||
classes in a command set is the way to make commands available to use in your game.
|
||||
|
||||
When storing a CmdSet on an object, you will make the commands in that command set available to the
|
||||
object. An example is the default command set stored on new Characters. This command set contains
|
||||
all the useful commands, from `look` and `inventory` to `@dig` and `@reload`
|
||||
([permissions](./Permissions.md) then limit which players may use them, but that's a separate
|
||||
topic).
|
||||
|
||||
When an account enters a command, cmdsets from the Account, Character, its location, and elsewhere
|
||||
are pulled together into a *merge stack*. This stack is merged together in a specific order to
|
||||
create a single "merged" cmdset, representing the pool of commands available at that very moment.
|
||||
|
||||
An example would be a `Window` object that has a cmdset with two commands in it: `look through
|
||||
window` and `open window`. The command set would be visible to players in the room with the window,
|
||||
allowing them to use those commands only there. You could imagine all sorts of clever uses of this,
|
||||
like a `Television` object which had multiple commands for looking at it, switching channels and so
|
||||
on. The tutorial world included with Evennia showcases a dark room that replaces certain critical
|
||||
commands with its own versions because the Character cannot see.
|
||||
|
||||
If you want a quick start into defining your first commands and using them with command sets, you
|
||||
can head over to the [Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) which steps through things
|
||||
without the explanations.
|
||||
|
||||
## Defining Command Sets
|
||||
|
||||
A CmdSet is, as most things in Evennia, defined as a Python class inheriting from the correct parent
|
||||
(`evennia.CmdSet`, which is a shortcut to `evennia.commands.cmdset.CmdSet`). The CmdSet class only
|
||||
needs to define one method, called `at_cmdset_creation()`. All other class parameters are optional,
|
||||
but are used for more advanced set manipulation and coding (see the [merge rules](Command-
|
||||
Sets#merge-rules) section).
|
||||
|
||||
```python
|
||||
# file mygame/commands/mycmdset.py
|
||||
|
||||
from evennia import CmdSet
|
||||
|
||||
# this is a theoretical custom module with commands we
|
||||
# created previously: mygame/commands/mycommands.py
|
||||
from commands import mycommands
|
||||
|
||||
class MyCmdSet(CmdSet):
|
||||
def at_cmdset_creation(self):
|
||||
"""
|
||||
The only thing this method should need
|
||||
to do is to add commands to the set.
|
||||
"""
|
||||
self.add(mycommands.MyCommand1())
|
||||
self.add(mycommands.MyCommand2())
|
||||
self.add(mycommands.MyCommand3())
|
||||
```
|
||||
|
||||
The CmdSet's `add()` method can also take another CmdSet as input. In this case all the commands
|
||||
from that CmdSet will be appended to this one as if you added them line by line:
|
||||
|
||||
```python
|
||||
def at_cmdset_creation():
|
||||
...
|
||||
self.add(AdditionalCmdSet) # adds all command from this set
|
||||
...
|
||||
```
|
||||
|
||||
If you added your command to an existing cmdset (like to the default cmdset), that set is already
|
||||
loaded into memory. You need to make the server aware of the code changes:
|
||||
|
||||
```
|
||||
@reload
|
||||
```
|
||||
|
||||
You should now be able to use the command.
|
||||
|
||||
If you created a new, fresh cmdset, this must be added to an object in order to make the commands
|
||||
within available. A simple way to temporarily test a cmdset on yourself is use the `@py` command to
|
||||
execute a python snippet:
|
||||
|
||||
```python
|
||||
@py self.cmdset.add('commands.mycmdset.MyCmdSet')
|
||||
```
|
||||
|
||||
This will stay with you until you `@reset` or `@shutdown` the server, or you run
|
||||
|
||||
```python
|
||||
@py self.cmdset.delete('commands.mycmdset.MyCmdSet')
|
||||
```
|
||||
|
||||
In the example above, a specific Cmdset class is removed. Calling `delete` without arguments will
|
||||
remove the latest added cmdset.
|
||||
|
||||
> Note: Command sets added using `cmdset.add` are, by default, *not* persistent in the database.
|
||||
|
||||
If you want the cmdset to survive a reload, you can do:
|
||||
|
||||
```
|
||||
@py self.cmdset.add(commands.mycmdset.MyCmdSet, persistent=True)
|
||||
```
|
||||
|
||||
Or you could add the cmdset as the *default* cmdset:
|
||||
|
||||
```
|
||||
@py self.cmdset.add_default(commands.mycmdset.MyCmdSet)
|
||||
```
|
||||
|
||||
An object can only have one "default" cmdset (but can also have none). This is meant as a safe fall-
|
||||
back even if all other cmdsets fail or are removed. It is always persistent and will not be affected
|
||||
by `cmdset.delete()`. To remove a default cmdset you must explicitly call `cmdset.remove_default()`.
|
||||
|
||||
Command sets are often added to an object in its `at_object_creation` method. For more examples of
|
||||
adding commands, read the [Step by step tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md). Generally you can
|
||||
customize which command sets are added to your objects by using `self.cmdset.add()` or
|
||||
`self.cmdset.add_default()`.
|
||||
|
||||
> Important: Commands are identified uniquely by key *or* alias (see [Commands](./Commands.md)). If any
|
||||
overlap exists, two commands are considered identical. Adding a Command to a command set that
|
||||
already has an identical command will *replace* the previous command. This is very important. You
|
||||
must take this behavior into account when attempting to overload any default Evennia commands with
|
||||
your own. Otherwise, you may accidentally "hide" your own command in your command set when adding a
|
||||
new one that has a matching alias.
|
||||
|
||||
### Properties on Command Sets
|
||||
|
||||
There are several extra flags that you can set on CmdSets in order to modify how they work. All are
|
||||
optional and will be set to defaults otherwise. Since many of these relate to *merging* cmdsets,
|
||||
you might want to read the [Adding and Merging Command Sets](./Command-Sets.md#adding-and-merging-
|
||||
command-sets) section for some of these to make sense.
|
||||
|
||||
- `key` (string) - an identifier for the cmdset. This is optional, but should be unique. It is used
|
||||
for display in lists, but also to identify special merging behaviours using the `key_mergetype`
|
||||
dictionary below.
|
||||
- `mergetype` (string) - allows for one of the following string values: "*Union*", "*Intersect*",
|
||||
"*Replace*", or "*Remove*".
|
||||
- `priority` (int) - This defines the merge order of the merge stack - cmdsets will merge in rising
|
||||
order of priority with the highest priority set merging last. During a merger, the commands from the
|
||||
set with the higher priority will have precedence (just what happens depends on the [merge
|
||||
type](./Command-Sets.md#adding-and-merging-command-sets)). If priority is identical, the order in the
|
||||
merge stack determines preference. The priority value must be greater or equal to `-100`. Most in-
|
||||
game sets should usually have priorities between `0` and `100`. Evennia default sets have priorities
|
||||
as follows (these can be changed if you want a different distribution):
|
||||
- EmptySet: `-101` (should be lower than all other sets)
|
||||
- SessionCmdSet: `-20`
|
||||
- AccountCmdSet: `-10`
|
||||
- CharacterCmdSet: `0`
|
||||
- ExitCmdSet: ` 101` (generally should always be available)
|
||||
- ChannelCmdSet: `101` (should usually always be available) - since exits never accept
|
||||
arguments, there is no collision between exits named the same as a channel even though the commands
|
||||
"collide".
|
||||
- `key_mergetype` (dict) - a dict of `key:mergetype` pairs. This allows this cmdset to merge
|
||||
differently with certain named cmdsets. If the cmdset to merge with has a `key` matching an entry in
|
||||
`key_mergetype`, it will not be merged according to the setting in `mergetype` but according to the
|
||||
mode in this dict. Please note that this is more complex than it may seem due to the [merge
|
||||
order](./Command-Sets.md#adding-and-merging-command-sets) of command sets. Please review that section
|
||||
before using `key_mergetype`.
|
||||
- `duplicates` (bool/None default `None`) - this determines what happens when merging same-priority
|
||||
cmdsets containing same-key commands together. The`dupicate` option will *only* apply when merging
|
||||
the cmdset with this option onto one other cmdset with the same priority. The resulting cmdset will
|
||||
*not* retain this `duplicate` setting.
|
||||
- `None` (default): No duplicates are allowed and the cmdset being merged "onto" the old one
|
||||
will take precedence. The result will be unique commands. *However*, the system will assume this
|
||||
value to be `True` for cmdsets on Objects, to avoid dangerous clashes. This is usually the safe bet.
|
||||
- `False`: Like `None` except the system will not auto-assume any value for cmdsets defined on
|
||||
Objects.
|
||||
- `True`: Same-named, same-prio commands will merge into the same cmdset. This will lead to a
|
||||
multimatch error (the user will get a list of possibilities in order to specify which command they
|
||||
meant). This is is useful e.g. for on-object cmdsets (example: There is a `red button` and a `green
|
||||
button` in the room. Both have a `press button` command, in cmdsets with the same priority. This
|
||||
flag makes sure that just writing `press button` will force the Player to define just which object's
|
||||
command was intended).
|
||||
- `no_objs` this is a flag for the cmdhandler that builds the set of commands available at every
|
||||
moment. It tells the handler not to include cmdsets from objects around the account (nor from rooms
|
||||
or inventory) when building the merged set. Exit commands will still be included. This option can
|
||||
have three values:
|
||||
- `None` (default): Passthrough of any value set explicitly earlier in the merge stack. If never
|
||||
set explicitly, this acts as `False`.
|
||||
- `True`/`False`: Explicitly turn on/off. If two sets with explicit `no_objs` are merged,
|
||||
priority determines what is used.
|
||||
- `no_exits` - this is a flag for the cmdhandler that builds the set of commands available at every
|
||||
moment. It tells the handler not to include cmdsets from exits. This flag can have three values:
|
||||
- `None` (default): Passthrough of any value set explicitly earlier in the merge stack. If
|
||||
never set explicitly, this acts as `False`.
|
||||
- `True`/`False`: Explicitly turn on/off. If two sets with explicit `no_exits` are merged,
|
||||
priority determines what is used.
|
||||
- `no_channels` (bool) - this is a flag for the cmdhandler that builds the set of commands available
|
||||
at every moment. It tells the handler not to include cmdsets from available in-game channels. This
|
||||
flag can have three values:
|
||||
- `None` (default): Passthrough of any value set explicitly earlier in the merge stack. If
|
||||
never set explicitly, this acts as `False`.
|
||||
- `True`/`False`: Explicitly turn on/off. If two sets with explicit `no_channels` are merged,
|
||||
priority determines what is used.
|
||||
|
||||
## Command Sets Searched
|
||||
|
||||
When a user issues a command, it is matched against the [merged](./Command-Sets.md#adding-and-merging-
|
||||
command-sets) command sets available to the player at the moment. Which those are may change at any
|
||||
time (such as when the player walks into the room with the `Window` object described earlier).
|
||||
|
||||
The currently valid command sets are collected from the following sources:
|
||||
|
||||
- The cmdsets stored on the currently active [Session](./Sessions.md). Default is the empty
|
||||
`SessionCmdSet` with merge priority `-20`.
|
||||
- The cmdsets defined on the [Account](./Accounts.md). Default is the AccountCmdSet with merge priority
|
||||
`-10`.
|
||||
- All cmdsets on the Character/Object (assuming the Account is currently puppeting such a
|
||||
Character/Object). Merge priority `0`.
|
||||
- The cmdsets of all objects carried by the puppeted Character (checks the `call` lock). Will not be
|
||||
included if `no_objs` option is active in the merge stack.
|
||||
- The cmdsets of the Character's current location (checks the `call` lock). Will not be included if
|
||||
`no_objs` option is active in the merge stack.
|
||||
- The cmdsets of objects in the current location (checks the `call` lock). Will not be included if
|
||||
`no_objs` option is active in the merge stack.
|
||||
- The cmdsets of Exits in the location. Merge priority `+101`. Will not be included if `no_exits`
|
||||
*or* `no_objs` option is active in the merge stack.
|
||||
- The [channel](./Channels.md) cmdset containing commands for posting to all channels the account
|
||||
or character is currently connected to. Merge priority `+101`. Will not be included if `no_channels`
|
||||
option is active in the merge stack.
|
||||
|
||||
Note that an object does not *have* to share its commands with its surroundings. A Character's
|
||||
cmdsets should not be shared for example, or all other Characters would get multi-match errors just
|
||||
by being in the same room. The ability of an object to share its cmdsets is managed by its `call`
|
||||
[lock](./Locks.md). For example, [Character objects](./Objects.md) defaults to `call:false()` so that any
|
||||
cmdsets on them can only be accessed by themselves, not by other objects around them. Another
|
||||
example might be to lock an object with `call:inside()` to only make their commands available to
|
||||
objects inside them, or `cmd:holds()` to make their commands available only if they are held.
|
||||
|
||||
## Adding and Merging Command Sets
|
||||
|
||||
*Note: This is an advanced topic. It's very useful to know about, but you might want to skip it if
|
||||
this is your first time learning about commands.*
|
||||
|
||||
CmdSets have the special ability that they can be *merged* together into new sets. Which of the
|
||||
ingoing commands end up in the merged set is defined by the *merge rule* and the relative
|
||||
*priorities* of the two sets. Removing the latest added set will restore things back to the way it
|
||||
was before the addition.
|
||||
|
||||
CmdSets are non-destructively stored in a stack inside the cmdset handler on the object. This stack
|
||||
is parsed to create the "combined" cmdset active at the moment. CmdSets from other sources are also
|
||||
included in the merger such as those on objects in the same room (like buttons to press) or those
|
||||
introduced by state changes (such as when entering a menu). The cmdsets are all ordered after
|
||||
priority and then merged together in *reverse order*. That is, the higher priority will be merged
|
||||
"onto" lower-prio ones. By defining a cmdset with a merge-priority between that of two other sets,
|
||||
you will make sure it will be merged in between them.
|
||||
The very first cmdset in this stack is called the *Default cmdset* and is protected from accidental
|
||||
deletion. Running `obj.cmdset.delete()` will never delete the default set. Instead one should add
|
||||
new cmdsets on top of the default to "hide" it, as described below. Use the special
|
||||
`obj.cmdset.delete_default()` only if you really know what you are doing.
|
||||
|
||||
CmdSet merging is an advanced feature useful for implementing powerful game effects. Imagine for
|
||||
example a player entering a dark room. You don't want the player to be able to find everything in
|
||||
the room at a glance - maybe you even want them to have a hard time to find stuff in their backpack!
|
||||
You can then define a different CmdSet with commands that override the normal ones. While they are
|
||||
in the dark room, maybe the `look` and `inv` commands now just tell the player they cannot see
|
||||
anything! Another example would be to offer special combat commands only when the player is in
|
||||
combat. Or when being on a boat. Or when having taken the super power-up. All this can be done on
|
||||
the fly by merging command sets.
|
||||
|
||||
### Merge Rules
|
||||
|
||||
Basic rule is that command sets are merged in *reverse priority order*. That is, lower-prio sets are
|
||||
merged first and higher prio sets are merged "on top" of them. Think of it like a layered cake with
|
||||
the highest priority on top.
|
||||
|
||||
To further understand how sets merge, we need to define some examples. Let's call the first command
|
||||
set **A** and the second **B**. We assume **B** is the command set already active on our object and
|
||||
we will merge **A** onto **B**. In code terms this would be done by `object.cdmset.add(A)`.
|
||||
Remember, B is already active on `object` from before.
|
||||
|
||||
We let the **A** set have higher priority than **B**. A priority is simply an integer number. As
|
||||
seen in the list above, Evennia's default cmdsets have priorities in the range `-101` to `120`. You
|
||||
are usually safe to use a priority of `0` or `1` for most game effects.
|
||||
|
||||
In our examples, both sets contain a number of commands which we'll identify by numbers, like `A1,
|
||||
A2` for set **A** and `B1, B2, B3, B4` for **B**. So for that example both sets contain commands
|
||||
with the same keys (or aliases) "1" and "2" (this could for example be "look" and "get" in the real
|
||||
game), whereas commands 3 and 4 are unique to **B**. To describe a merge between these sets, we
|
||||
would write `A1,A2 + B1,B2,B3,B4 = ?` where `?` is a list of commands that depend on which merge
|
||||
type **A** has, and which relative priorities the two sets have. By convention, we read this
|
||||
statement as "New command set **A** is merged onto the old command set **B** to form **?**".
|
||||
|
||||
Below are the available merge types and how they work. Names are partly borrowed from [Set
|
||||
theory](https://en.wikipedia.org/wiki/Set_theory).
|
||||
|
||||
- **Union** (default) - The two cmdsets are merged so that as many commands as possible from each
|
||||
cmdset ends up in the merged cmdset. Same-key commands are merged by priority.
|
||||
|
||||
# Union
|
||||
A1,A2 + B1,B2,B3,B4 = A1,A2,B3,B4
|
||||
|
||||
- **Intersect** - Only commands found in *both* cmdsets (i.e. which have the same keys) end up in
|
||||
the merged cmdset, with the higher-priority cmdset replacing the lower one's commands.
|
||||
|
||||
# Intersect
|
||||
A1,A3,A5 + B1,B2,B4,B5 = A1,A5
|
||||
|
||||
- **Replace** - The commands of the higher-prio cmdset completely replaces the lower-priority
|
||||
cmdset's commands, regardless of if same-key commands exist or not.
|
||||
|
||||
# Replace
|
||||
A1,A3 + B1,B2,B4,B5 = A1,A3
|
||||
|
||||
- **Remove** - The high-priority command sets removes same-key commands from the lower-priority
|
||||
cmdset. They are not replaced with anything, so this is a sort of filter that prunes the low-prio
|
||||
set using the high-prio one as a template.
|
||||
|
||||
# Remove
|
||||
A1,A3 + B1,B2,B3,B4,B5 = B2,B4,B5
|
||||
|
||||
Besides `priority` and `mergetype`, a command-set also takes a few other variables to control how
|
||||
they merge:
|
||||
|
||||
- `duplicates` (bool) - determines what happens when two sets of equal priority merge. Default is
|
||||
that the new set in the merger (i.e. **A** above) automatically takes precedence. But if
|
||||
*duplicates* is true, the result will be a merger with more than one of each name match. This will
|
||||
usually lead to the player receiving a multiple-match error higher up the road, but can be good for
|
||||
things like cmdsets on non-player objects in a room, to allow the system to warn that more than one
|
||||
'ball' in the room has the same 'kick' command defined on it and offer a chance to select which
|
||||
ball to kick ... Allowing duplicates only makes sense for *Union* and *Intersect*, the setting is
|
||||
ignored for the other mergetypes.
|
||||
- `key_mergetypes` (dict) - allows the cmdset to define a unique mergetype for particular cmdsets,
|
||||
identified by their cmdset `key`. Format is `{CmdSetkey:mergetype}`. Example:
|
||||
`{'Myevilcmdset','Replace'}` which would make sure for this set to always use 'Replace' on the
|
||||
cmdset with the key `Myevilcmdset` only, no matter what the main `mergetype` is set to.
|
||||
|
||||
> Warning: The `key_mergetypes` dictionary *can only work on the cmdset we merge onto*. When using
|
||||
`key_mergetypes` it is thus important to consider the merge priorities - you must make sure that you
|
||||
pick a priority *between* the cmdset you want to detect and the next higher one, if any. That is, if
|
||||
we define a cmdset with a high priority and set it to affect a cmdset that is far down in the merge
|
||||
stack, we would not "see" that set when it's time for us to merge. Example: Merge stack is
|
||||
`A(prio=-10), B(prio=-5), C(prio=0), D(prio=5)`. We now merge a cmdset `E(prio=10)` onto this stack,
|
||||
with a `key_mergetype={"B":"Replace"}`. But priorities dictate that we won't be merged onto B, we
|
||||
will be merged onto E (which is a merger of the lower-prio sets at this point). Since we are merging
|
||||
onto E and not B, our `key_mergetype` directive won't trigger. To make sure it works we must make
|
||||
sure we merge onto B. Setting E's priority to, say, -4 will make sure to merge it onto B and affect
|
||||
it appropriately.
|
||||
|
||||
More advanced cmdset example:
|
||||
|
||||
```python
|
||||
from commands import mycommands
|
||||
|
||||
class MyCmdSet(CmdSet):
|
||||
|
||||
key = "MyCmdSet"
|
||||
priority = 4
|
||||
mergetype = "Replace"
|
||||
key_mergetypes = {'MyOtherCmdSet':'Union'}
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"""
|
||||
The only thing this method should need
|
||||
to do is to add commands to the set.
|
||||
"""
|
||||
self.add(mycommands.MyCommand1())
|
||||
self.add(mycommands.MyCommand2())
|
||||
self.add(mycommands.MyCommand3())
|
||||
```
|
||||
|
||||
### Assorted Notes
|
||||
|
||||
It is very important to remember that two commands are compared *both* by their `key` properties
|
||||
*and* by their `aliases` properties. If either keys or one of their aliases match, the two commands
|
||||
are considered the *same*. So consider these two Commands:
|
||||
|
||||
- A Command with key "kick" and alias "fight"
|
||||
- A Command with key "punch" also with an alias "fight"
|
||||
|
||||
During the cmdset merging (which happens all the time since also things like channel commands and
|
||||
exits are merged in), these two commands will be considered *identical* since they share alias. It
|
||||
means only one of them will remain after the merger. Each will also be compared with all other
|
||||
commands having any combination of the keys and/or aliases "kick", "punch" or "fight".
|
||||
|
||||
... So avoid duplicate aliases, it will only cause confusion.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# Command System
|
||||
|
||||
- [Commands](./Commands.md)
|
||||
- [Command Sets](./Command-Sets.md)
|
||||
- [Command Auto-help](./Help-System.md#command-auto-help-system)
|
||||
|
||||
See also:
|
||||
- [Default Commands](./Default-Commands.md)
|
||||
- [Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md)
|
||||
|
|
@ -1,690 +0,0 @@
|
|||
# Commands
|
||||
|
||||
|
||||
Commands are intimately linked to [Command Sets](./Command-Sets.md) and you need to read that page too to
|
||||
be familiar with how the command system works. The two pages were split for easy reading.
|
||||
|
||||
The basic way for users to communicate with the game is through *Commands*. These can be commands
|
||||
directly related to the game world such as *look*, *get*, *drop* and so on, or administrative
|
||||
commands such as *examine* or *@dig*.
|
||||
|
||||
The [default commands](./Default-Commands.md) coming with Evennia are 'MUX-like' in that they use @
|
||||
for admin commands, support things like switches, syntax with the '=' symbol etc, but there is
|
||||
nothing that prevents you from implementing a completely different command scheme for your game. You
|
||||
can find the default commands in `evennia/commands/default`. You should not edit these directly -
|
||||
they will be updated by the Evennia team as new features are added. Rather you should look to them
|
||||
for inspiration and inherit your own designs from them.
|
||||
|
||||
There are two components to having a command running - the *Command* class and the
|
||||
[Command Set](./Command-Sets.md) (command sets were split into a separate wiki page for ease of reading).
|
||||
|
||||
1. A *Command* is a python class containing all the functioning code for what a command does - for
|
||||
example, a *get* command would contain code for picking up objects.
|
||||
1. A *Command Set* (often referred to as a CmdSet or cmdset) is like a container for one or more
|
||||
Commands. A given Command can go into any number of different command sets. Only by putting the
|
||||
command set on a character object you will make all the commands therein available to use by that
|
||||
character. You can also store command sets on normal objects if you want users to be able to use the
|
||||
object in various ways. Consider a "Tree" object with a cmdset defining the commands *climb* and
|
||||
*chop down*. Or a "Clock" with a cmdset containing the single command *check time*.
|
||||
|
||||
This page goes into full detail about how to use Commands. To fully use them you must also read the
|
||||
page detailing [Command Sets](./Command-Sets.md). There is also a step-by-step
|
||||
[Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) that will get you started quickly without the
|
||||
extra explanations.
|
||||
|
||||
## Defining Commands
|
||||
|
||||
All commands are implemented as normal Python classes inheriting from the base class `Command`
|
||||
(`evennia.Command`). You will find that this base class is very "bare". The default commands of
|
||||
Evennia actually inherit from a child of `Command` called `MuxCommand` - this is the class that
|
||||
knows all the mux-like syntax like `/switches`, splitting by "=" etc. Below we'll avoid mux-
|
||||
specifics and use the base `Command` class directly.
|
||||
|
||||
```python
|
||||
# basic Command definition
|
||||
from evennia import Command
|
||||
|
||||
class MyCmd(Command):
|
||||
"""
|
||||
This is the help-text for the command
|
||||
"""
|
||||
key = "mycommand"
|
||||
def parse(self):
|
||||
# parsing the command line here
|
||||
def func(self):
|
||||
# executing the command here
|
||||
```
|
||||
|
||||
Here is a minimalistic command with no custom parsing:
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
|
||||
class CmdEcho(Command):
|
||||
key = "echo"
|
||||
|
||||
def func(self):
|
||||
# echo the caller's input back to the caller
|
||||
self.caller.msg(f"Echo: {self.args}")
|
||||
|
||||
```
|
||||
|
||||
You define a new command by assigning a few class-global properties on your inherited class and
|
||||
overloading one or two hook functions. The full gritty mechanic behind how commands work are found
|
||||
towards the end of this page; for now you only need to know that the command handler creates an
|
||||
instance of this class and uses that instance whenever you use this command - it also dynamically
|
||||
assigns the new command instance a few useful properties that you can assume to always be available.
|
||||
|
||||
### Who is calling the command?
|
||||
|
||||
In Evennia there are three types of objects that may call the command. It is important to be aware
|
||||
of this since this will also assign appropriate `caller`, `session`, `sessid` and `account`
|
||||
properties on the command body at runtime. Most often the calling type is `Session`.
|
||||
|
||||
* A [Session](./Sessions.md). This is by far the most common case when a user is entering a command in
|
||||
their client.
|
||||
* `caller` - this is set to the puppeted [Object](./Objects.md) if such an object exists. If no
|
||||
puppet is found, `caller` is set equal to `account`. Only if an Account is not found either (such as
|
||||
before being logged in) will this be set to the Session object itself.
|
||||
* `session` - a reference to the [Session](./Sessions.md) object itself.
|
||||
* `sessid` - `sessid.id`, a unique integer identifier of the session.
|
||||
* `account` - the [Account](./Accounts.md) object connected to this Session. None if not logged in.
|
||||
* An [Account](./Accounts.md). This only happens if `account.execute_cmd()` was used. No Session
|
||||
information can be obtained in this case.
|
||||
* `caller` - this is set to the puppeted Object if such an object can be determined (without
|
||||
Session info this can only be determined in `MULTISESSION_MODE=0` or `1`). If no puppet is found,
|
||||
this is equal to `account`.
|
||||
* `session` - `None*`
|
||||
* `sessid` - `None*`
|
||||
* `account` - Set to the Account object.
|
||||
* An [Object](./Objects.md). This only happens if `object.execute_cmd()` was used (for example by an
|
||||
NPC).
|
||||
* `caller` - This is set to the calling Object in question.
|
||||
* `session` - `None*`
|
||||
* `sessid` - `None*`
|
||||
* `account` - `None`
|
||||
|
||||
> `*)`: There is a way to make the Session available also inside tests run directly on Accounts and
|
||||
Objects, and that is to pass it to `execute_cmd` like so: `account.execute_cmd("...",
|
||||
session=<Session>)`. Doing so *will* make the `.session` and `.sessid` properties available in the
|
||||
command.
|
||||
|
||||
### Properties assigned to the command instance at run-time
|
||||
|
||||
Let's say account *Bob* with a character *BigGuy* enters the command *look at sword*. After the
|
||||
system having successfully identified this as the "look" command and determined that BigGuy really
|
||||
has access to a command named `look`, it chugs the `look` command class out of storage and either
|
||||
loads an existing Command instance from cache or creates one. After some more checks it then assigns
|
||||
it the following properties:
|
||||
|
||||
- `caller` - The character BigGuy, in this example. This is a reference to the object executing the
|
||||
command. The value of this depends on what type of object is calling the command; see the previous
|
||||
section.
|
||||
- `session` - the [Session](./Sessions.md) Bob uses to connect to the game and control BigGuy (see also
|
||||
previous section).
|
||||
- `sessid` - the unique id of `self.session`, for quick lookup.
|
||||
- `account` - the [Account](./Accounts.md) Bob (see previous section).
|
||||
- `cmdstring` - the matched key for the command. This would be *look* in our example.
|
||||
- `args` - this is the rest of the string, except the command name. So if the string entered was
|
||||
*look at sword*, `args` would be " *at sword*". Note the space kept - Evennia would correctly
|
||||
interpret `lookat sword` too. This is useful for things like `/switches` that should not use space.
|
||||
In the `MuxCommand` class used for default commands, this space is stripped. Also see the
|
||||
`arg_regex` property if you want to enforce a space to make `lookat sword` give a command-not-found
|
||||
error.
|
||||
- `obj` - the game [Object](./Objects.md) on which this command is defined. This need not be the caller,
|
||||
but since `look` is a common (default) command, this is probably defined directly on *BigGuy* - so
|
||||
`obj` will point to BigGuy. Otherwise `obj` could be an Account or any interactive object with
|
||||
commands defined on it, like in the example of the "check time" command defined on a "Clock" object.
|
||||
- `cmdset` - this is a reference to the merged CmdSet (see below) from which this command was
|
||||
matched. This variable is rarely used, it's main use is for the [auto-help system](Help-
|
||||
System#command-auto-help-system) (*Advanced note: the merged cmdset need NOT be the same as
|
||||
`BigGuy.cmdset`. The merged set can be a combination of the cmdsets from other objects in the room,
|
||||
for example*).
|
||||
- `raw_string` - this is the raw input coming from the user, without stripping any surrounding
|
||||
whitespace. The only thing that is stripped is the ending newline marker.
|
||||
|
||||
#### Other useful utility methods:
|
||||
|
||||
- `.get_help(caller, cmdset)` - Get the help entry for this command. By default the arguments are
|
||||
not
|
||||
used, but they could be used to implement alternate help-display systems.
|
||||
- `.client_width()` - Shortcut for getting the client's screen-width. Note that not all clients will
|
||||
truthfully report this value - that case the `settings.DEFAULT_SCREEN_WIDTH` will be returned.
|
||||
- `.styled_table(*args, **kwargs)` - This returns an [EvTable](module-
|
||||
evennia.utils.evtable) styled based on the
|
||||
session calling this command. The args/kwargs are the same as for EvTable, except styling defaults
|
||||
are set.
|
||||
- `.styled_header`, `_footer`, `separator` - These will produce styled decorations for
|
||||
display to the user. They are useful for creating listings and forms with colors adjustable per-
|
||||
user.
|
||||
|
||||
### Defining your own command classes
|
||||
|
||||
Beyond the properties Evennia always assigns to the command at run-time (listed above), your job is
|
||||
to define the following class properties:
|
||||
|
||||
- `key` (string) - the identifier for the command, like `look`. This should (ideally) be unique. A
|
||||
key can consist of more than one word, like "press button" or "pull left lever". Note that *both*
|
||||
`key` and `aliases` below determine the identity of a command. So two commands are considered if
|
||||
either matches. This is important for merging cmdsets described below.
|
||||
- `aliases` (optional list) - a list of alternate names for the command (`["glance", "see", "l"]`).
|
||||
Same name rules as for `key` applies.
|
||||
- `locks` (string) - a [lock definition](./Locks.md), usually on the form `cmd:<lockfuncs>`. Locks is a
|
||||
rather big topic, so until you learn more about locks, stick to giving the lockstring `"cmd:all()"`
|
||||
to make the command available to everyone (if you don't provide a lock string, this will be assigned
|
||||
for you).
|
||||
- `help_category` (optional string) - setting this helps to structure the auto-help into categories.
|
||||
If none is set, this will be set to *General*.
|
||||
- `save_for_next` (optional boolean). This defaults to `False`. If `True`, a copy of this command
|
||||
object (along with any changes you have done to it) will be stored by the system and can be accessed
|
||||
by the next command by retrieving `self.caller.ndb.last_cmd`. The next run command will either clear
|
||||
or replace the storage.
|
||||
- `arg_regex` (optional raw string): Used to force the parser to limit itself and tell it when the
|
||||
command-name ends and arguments begin (such as requiring this to be a space or a /switch). This is
|
||||
done with a regular expression. [See the arg_regex section](./Commands.md#on-arg_regex) for the details.
|
||||
- `auto_help` (optional boolean). Defaults to `True`. This allows for turning off the
|
||||
[auto-help system](./Help-System.md#command-auto-help-system) on a per-command basis. This could be useful if you
|
||||
either want to write your help entries manually or hide the existence of a command from `help`'s
|
||||
generated list.
|
||||
- `is_exit` (bool) - this marks the command as being used for an in-game exit. This is, by default,
|
||||
set by all Exit objects and you should not need to set it manually unless you make your own Exit
|
||||
system. It is used for optimization and allows the cmdhandler to easily disregard this command when
|
||||
the cmdset has its `no_exits` flag set.
|
||||
- `is_channel` (bool)- this marks the command as being used for an in-game channel. This is, by
|
||||
default, set by all Channel objects and you should not need to set it manually unless you make your
|
||||
own Channel system. is used for optimization and allows the cmdhandler to easily disregard this
|
||||
command when its cmdset has its `no_channels` flag set.
|
||||
- `msg_all_sessions` (bool): This affects the behavior of the `Command.msg` method. If unset
|
||||
(default), calling `self.msg(text)` from the Command will always only send text to the Session that
|
||||
actually triggered this Command. If set however, `self.msg(text)` will send to all Sessions relevant
|
||||
to the object this Command sits on. Just which Sessions receives the text depends on the object and
|
||||
the server's `MULTISESSION_MODE`.
|
||||
|
||||
You should also implement at least two methods, `parse()` and `func()` (You could also implement
|
||||
`perm()`, but that's not needed unless you want to fundamentally change how access checks work).
|
||||
|
||||
- `at_pre_cmd()` is called very first on the command. If this function returns anything that
|
||||
evaluates to `True` the command execution is aborted at this point.
|
||||
- `parse()` is intended to parse the arguments (`self.args`) of the function. You can do this in any
|
||||
way you like, then store the result(s) in variable(s) on the command object itself (i.e. on `self`).
|
||||
To take an example, the default mux-like system uses this method to detect "command switches" and
|
||||
store them as a list in `self.switches`. Since the parsing is usually quite similar inside a command
|
||||
scheme you should make `parse()` as generic as possible and then inherit from it rather than re-
|
||||
implementing it over and over. In this way, the default `MuxCommand` class implements a `parse()`
|
||||
for all child commands to use.
|
||||
- `func()` is called right after `parse()` and should make use of the pre-parsed input to actually
|
||||
do whatever the command is supposed to do. This is the main body of the command. The return value
|
||||
from this method will be returned from the execution as a Twisted Deferred.
|
||||
- `at_post_cmd()` is called after `func()` to handle eventual cleanup.
|
||||
|
||||
Finally, you should always make an informative [doc
|
||||
string](https://www.python.org/dev/peps/pep-0257/#what-is-a-docstring) (`__doc__`) at the top of
|
||||
your class. This string is dynamically read by the [Help System](./Help-System.md) to create the help
|
||||
entry for this command. You should decide on a way to format your help and stick to that.
|
||||
|
||||
Below is how you define a simple alternative "`smile`" command:
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
|
||||
class CmdSmile(Command):
|
||||
"""
|
||||
A smile command
|
||||
|
||||
Usage:
|
||||
smile [at] [<someone>]
|
||||
grin [at] [<someone>]
|
||||
|
||||
Smiles to someone in your vicinity or to the room
|
||||
in general.
|
||||
|
||||
(This initial string (the __doc__ string)
|
||||
is also used to auto-generate the help
|
||||
for this command)
|
||||
"""
|
||||
|
||||
key = "smile"
|
||||
aliases = ["smile at", "grin", "grin at"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "General"
|
||||
|
||||
def parse(self):
|
||||
"Very trivial parser"
|
||||
self.target = self.args.strip()
|
||||
|
||||
def func(self):
|
||||
"This actually does things"
|
||||
caller = self.caller
|
||||
|
||||
if not self.target or self.target == "here":
|
||||
string = f"{caller.key} smiles"
|
||||
else:
|
||||
target = caller.search(self.target)
|
||||
if not target:
|
||||
return
|
||||
string = f"{caller.key} smiles at {target.key}"
|
||||
|
||||
caller.location.msg_contents(string)
|
||||
|
||||
```
|
||||
|
||||
The power of having commands as classes and to separate `parse()` and `func()`
|
||||
lies in the ability to inherit functionality without having to parse every
|
||||
command individually. For example, as mentioned the default commands all
|
||||
inherit from `MuxCommand`. `MuxCommand` implements its own version of `parse()`
|
||||
that understands all the specifics of MUX-like commands. Almost none of the
|
||||
default commands thus need to implement `parse()` at all, but can assume the
|
||||
incoming string is already split up and parsed in suitable ways by its parent.
|
||||
|
||||
Before you can actually use the command in your game, you must now store it
|
||||
within a *command set*. See the [Command Sets](./Command-Sets.md) page.
|
||||
|
||||
### Command prefixes
|
||||
|
||||
Historically, many MU* servers used to use prefix, such as `@` or `&` to signify that
|
||||
a command is used for administration or requires staff privileges. The problem with this is that
|
||||
newcomers to MU often find such extra symbols confusing. Evennia allows commands that can be
|
||||
accessed both with- or without such a prefix.
|
||||
|
||||
CMD_IGNORE_PREFIXES = "@&/+`
|
||||
|
||||
This is a setting consisting of a string of characters. Each is a prefix that will be considered
|
||||
a skippable prefix - _if the command is still unique in its cmdset when skipping the prefix_.
|
||||
|
||||
So if you wanted to write `@look` instead of `look` you can do so - the `@` will be ignored. But If
|
||||
we added an actual `@look` command (with a `key` or alias `@look`) then we would need to use the
|
||||
`@` to separate between the two.
|
||||
|
||||
This is also used in the default commands. For example, `@open` is a building
|
||||
command that allows you to create new exits to link two rooms together. Its `key` is set to `@open`,
|
||||
including the `@` (no alias is set). By default you can use both `@open` and `open` for
|
||||
this command. But "open" is a pretty common word and let's say a developer adds a new `open` command
|
||||
for opening a door. Now `@open` and `open` are two different commands and the `@` must be used to
|
||||
separate them.
|
||||
|
||||
> The `help` command will prefer to show all command names without prefix if
|
||||
> possible. Only if there is a collision, will the prefix be shown in the help system.
|
||||
|
||||
### On arg_regex
|
||||
|
||||
The command parser is very general and does not require a space to end your command name. This means
|
||||
that the alias `:` to `emote` can be used like `:smiles` without modification. It also means
|
||||
`getstone` will get you the stone (unless there is a command specifically named `getstone`, then
|
||||
that will be used). If you want to tell the parser to require a certain separator between the
|
||||
command name and its arguments (so that `get stone` works but `getstone` gives you a 'command not
|
||||
found' error) you can do so with the `arg_regex` property.
|
||||
|
||||
The `arg_regex` is a [raw regular expression string](https://docs.python.org/library/re.html). The
|
||||
regex will be compiled by the system at runtime. This allows you to customize how the part
|
||||
*immediately following* the command name (or alias) must look in order for the parser to match for
|
||||
this command. Some examples:
|
||||
|
||||
- `commandname argument` (`arg_regex = r"\s.+"`): This forces the parser to require the command name
|
||||
to be followed by one or more spaces. Whatever is entered after the space will be treated as an
|
||||
argument. However, if you'd forget the space (like a command having no arguments), this would *not*
|
||||
match `commandname`.
|
||||
- `commandname` or `commandname argument` (`arg_regex = r"\s.+|$"`): This makes both `look` and
|
||||
`look me` work but `lookme` will not.
|
||||
- `commandname/switches arguments` (`arg_regex = r"(?:^(?:\s+|\/).*$)|^$"`. If you are using
|
||||
Evennia's `MuxCommand` Command parent, you may wish to use this since it will allow `/switche`s to
|
||||
work as well as having or not having a space.
|
||||
|
||||
The `arg_regex` allows you to customize the behavior of your commands. You can put it in the parent
|
||||
class of your command to customize all children of your Commands. However, you can also change the
|
||||
base default behavior for all Commands by modifying `settings.COMMAND_DEFAULT_ARG_REGEX`.
|
||||
|
||||
## Exiting a command
|
||||
|
||||
Normally you just use `return` in one of your Command class' hook methods to exit that method. That
|
||||
will however still fire the other hook methods of the Command in sequence. That's usually what you
|
||||
want but sometimes it may be useful to just abort the command, for example if you find some
|
||||
unacceptable input in your parse method. To exit the command this way you can raise
|
||||
`evennia.InterruptCommand`:
|
||||
|
||||
```python
|
||||
from evennia import InterruptCommand
|
||||
|
||||
class MyCommand(Command):
|
||||
|
||||
# ...
|
||||
|
||||
def parse(self):
|
||||
# ...
|
||||
# if this fires, `func()` and `at_post_cmd` will not
|
||||
# be called at all
|
||||
raise InterruptCommand()
|
||||
|
||||
```
|
||||
|
||||
## Pauses in commands
|
||||
|
||||
Sometimes you want to pause the execution of your command for a little while before continuing -
|
||||
maybe you want to simulate a heavy swing taking some time to finish, maybe you want the echo of your
|
||||
voice to return to you with an ever-longer delay. Since Evennia is running asynchronously, you
|
||||
cannot use `time.sleep()` in your commands (or anywhere, really). If you do, the *entire game* will
|
||||
be frozen for everyone! So don't do that. Fortunately, Evennia offers a really quick syntax for
|
||||
making pauses in commands.
|
||||
|
||||
In your `func()` method, you can use the `yield` keyword. This is a Python keyword that will freeze
|
||||
the current execution of your command and wait for more before processing.
|
||||
|
||||
> Note that you *cannot* just drop `yield` into any code and expect it to pause. Evennia will only
|
||||
pause for you if you `yield` inside the Command's `func()` method. Don't expect it to work anywhere
|
||||
else.
|
||||
|
||||
Here's an example of a command using a small pause of five seconds between messages:
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
|
||||
class CmdWait(Command):
|
||||
"""
|
||||
A dummy command to show how to wait
|
||||
|
||||
Usage:
|
||||
wait
|
||||
|
||||
"""
|
||||
|
||||
key = "wait"
|
||||
locks = "cmd:all()"
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"""Command execution."""
|
||||
self.msg("Beginner-Tutorial to wait ...")
|
||||
yield 5
|
||||
self.msg("... This shows after 5 seconds. Waiting ...")
|
||||
yield 2
|
||||
self.msg("... And now another 2 seconds have passed.")
|
||||
```
|
||||
|
||||
The important line is the `yield 5` and `yield 2` lines. It will tell Evennia to pause execution
|
||||
here and not continue until the number of seconds given has passed.
|
||||
|
||||
There are two things to remember when using `yield` in your Command's `func` method:
|
||||
|
||||
1. The paused state produced by the `yield` is not saved anywhere. So if the server reloads in the
|
||||
middle of your command pausing, it will *not* resume when the server comes back up - the remainder
|
||||
of the command will never fire. So be careful that you are not freezing the character or account in
|
||||
a way that will not be cleared on reload.
|
||||
2. If you use `yield` you may not also use `return <values>` in your `func` method. You'll get an
|
||||
error explaining this. This is due to how Python generators work. You can however use a "naked"
|
||||
`return` just fine. Usually there is no need for `func` to return a value, but if you ever do need
|
||||
to mix `yield` with a final return value in the same `func`, look at
|
||||
[twisted.internet.defer.returnValue](https://twistedmatrix.com/documents/current/api/twisted.internet.defer.html#returnValue).
|
||||
|
||||
## Asking for user input
|
||||
|
||||
The `yield` keyword can also be used to ask for user input. Again you can't
|
||||
use Python's `input` in your command, for it would freeze Evennia for
|
||||
everyone while waiting for that user to input their text. Inside a Command's
|
||||
`func` method, the following syntax can also be used:
|
||||
|
||||
```python
|
||||
answer = yield("Your question")
|
||||
```
|
||||
|
||||
Here's a very simple example:
|
||||
|
||||
```python
|
||||
class CmdConfirm(Command):
|
||||
|
||||
"""
|
||||
A dummy command to show confirmation.
|
||||
|
||||
Usage:
|
||||
confirm
|
||||
|
||||
"""
|
||||
|
||||
key = "confirm"
|
||||
|
||||
def func(self):
|
||||
answer = yield("Are you sure you want to go on?")
|
||||
if answer.strip().lower() in ("yes", "y"):
|
||||
self.msg("Yes!")
|
||||
else:
|
||||
self.msg("No!")
|
||||
```
|
||||
|
||||
This time, when the user enters the 'confirm' command, she will be asked if she wants to go on.
|
||||
Entering 'yes' or "y" (regardless of case) will give the first reply, otherwise the second reply
|
||||
will show.
|
||||
|
||||
> Note again that the `yield` keyword does not store state. If the game reloads while waiting for
|
||||
the user to answer, the user will have to start over. It is not a good idea to use `yield` for
|
||||
important or complex choices, a persistent [EvMenu](./EvMenu.md) might be more appropriate in this case.
|
||||
|
||||
## System commands
|
||||
|
||||
*Note: This is an advanced topic. Skip it if this is your first time learning about commands.*
|
||||
|
||||
There are several command-situations that are exceptional in the eyes of the server. What happens if
|
||||
the account enters an empty string? What if the 'command' given is infact the name of a channel the
|
||||
user wants to send a message to? Or if there are multiple command possibilities?
|
||||
|
||||
Such 'special cases' are handled by what's called *system commands*. A system command is defined
|
||||
in the same way as other commands, except that their name (key) must be set to one reserved by the
|
||||
engine (the names are defined at the top of `evennia/commands/cmdhandler.py`). You can find (unused)
|
||||
implementations of the system commands in `evennia/commands/default/system_commands.py`. Since these
|
||||
are not (by default) included in any `CmdSet` they are not actually used, they are just there for
|
||||
show. When the special situation occurs, Evennia will look through all valid `CmdSet`s for your
|
||||
custom system command. Only after that will it resort to its own, hard-coded implementation.
|
||||
|
||||
Here are the exceptional situations that triggers system commands. You can find the command keys
|
||||
they use as properties on `evennia.syscmdkeys`:
|
||||
|
||||
- No input (`syscmdkeys.CMD_NOINPUT`) - the account just pressed return without any input. Default
|
||||
is to do nothing, but it can be useful to do something here for certain implementations such as line
|
||||
editors that interpret non-commands as text input (an empty line in the editing buffer).
|
||||
- Command not found (`syscmdkeys.CMD_NOMATCH`) - No matching command was found. Default is to
|
||||
display the "Huh?" error message.
|
||||
- Several matching commands where found (`syscmdkeys.CMD_MULTIMATCH`) - Default is to show a list of
|
||||
matches.
|
||||
- User is not allowed to execute the command (`syscmdkeys.CMD_NOPERM`) - Default is to display the
|
||||
"Huh?" error message.
|
||||
- Channel (`syscmdkeys.CMD_CHANNEL`) - This is a [Channel](./Channels.md) name of a channel you are
|
||||
subscribing to - Default is to relay the command's argument to that channel. Such commands are
|
||||
created by the Comm system on the fly depending on your subscriptions.
|
||||
- New session connection (`syscmdkeys.CMD_LOGINSTART`). This command name should be put in the
|
||||
`settings.CMDSET_UNLOGGEDIN`. Whenever a new connection is established, this command is always
|
||||
called on the server (default is to show the login screen).
|
||||
|
||||
Below is an example of redefining what happens when the account doesn't provide any input (e.g. just
|
||||
presses return). Of course the new system command must be added to a cmdset as well before it will
|
||||
work.
|
||||
|
||||
```python
|
||||
from evennia import syscmdkeys, Command
|
||||
|
||||
class MyNoInputCommand(Command):
|
||||
"Usage: Just press return, I dare you"
|
||||
key = syscmdkeys.CMD_NOINPUT
|
||||
def func(self):
|
||||
self.caller.msg("Don't just press return like that, talk to me!")
|
||||
```
|
||||
|
||||
## Dynamic Commands
|
||||
|
||||
*Note: This is an advanced topic.*
|
||||
|
||||
Normally Commands are created as fixed classes and used without modification. There are however
|
||||
situations when the exact key, alias or other properties is not possible (or impractical) to pre-
|
||||
code ([Exits](./Commands.md#exits) is an example of this).
|
||||
|
||||
To create a command with a dynamic call signature, first define the command body normally in a class
|
||||
(set your `key`, `aliases` to default values), then use the following call (assuming the command
|
||||
class you created is named `MyCommand`):
|
||||
|
||||
```python
|
||||
cmd = MyCommand(key="newname",
|
||||
aliases=["test", "test2"],
|
||||
locks="cmd:all()",
|
||||
...)
|
||||
```
|
||||
|
||||
*All* keyword arguments you give to the Command constructor will be stored as a property on the
|
||||
command object. This will overload existing properties defined on the parent class.
|
||||
|
||||
Normally you would define your class and only overload things like `key` and `aliases` at run-time.
|
||||
But you could in principle also send method objects (like `func`) as keyword arguments in order to
|
||||
make your command completely customized at run-time.
|
||||
|
||||
## Exits
|
||||
|
||||
*Note: This is an advanced topic.*
|
||||
|
||||
Exits are examples of the use of a [Dynamic Command](./Commands.md#dynamic-commands).
|
||||
|
||||
The functionality of [Exit](./Objects.md) objects in Evennia is not hard-coded in the engine. Instead
|
||||
Exits are normal [typeclassed](./Typeclasses.md) objects that auto-create a [CmdSet](./Command-Sets.md) on
|
||||
themselves when they load. This cmdset has a single dynamically created Command with the same
|
||||
properties (key, aliases and locks) as the Exit object itself. When entering the name of the exit,
|
||||
this dynamic exit-command is triggered and (after access checks) moves the Character to the exit's
|
||||
destination.
|
||||
Whereas you could customize the Exit object and its command to achieve completely different
|
||||
behaviour, you will usually be fine just using the appropriate `traverse_*` hooks on the Exit
|
||||
object. But if you are interested in really changing how things work under the hood, check out
|
||||
`evennia/objects/objects.py` for how the `Exit` typeclass is set up.
|
||||
|
||||
## Command instances are re-used
|
||||
|
||||
*Note: This is an advanced topic that can be skipped when first learning about Commands.*
|
||||
|
||||
A Command class sitting on an object is instantiated once and then re-used. So if you run a command
|
||||
from object1 over and over you are in fact running the same command instance over and over (if you
|
||||
run the same command but sitting on object2 however, it will be a different instance). This is
|
||||
usually not something you'll notice, since every time the Command-instance is used, all the relevant
|
||||
properties on it will be overwritten. But armed with this knowledge you can implement some of the
|
||||
more exotic command mechanism out there, like the command having a 'memory' of what you last entered
|
||||
so that you can back-reference the previous arguments etc.
|
||||
|
||||
> Note: On a server reload, all Commands are rebuilt and memory is flushed.
|
||||
|
||||
To show this in practice, consider this command:
|
||||
|
||||
```python
|
||||
class CmdTestID(Command):
|
||||
key = "testid"
|
||||
|
||||
def func(self):
|
||||
|
||||
if not hasattr(self, "xval"):
|
||||
self.xval = 0
|
||||
self.xval += 1
|
||||
|
||||
self.caller.msg(f"Command memory ID: {id(self)} (xval={self.xval})")
|
||||
|
||||
```
|
||||
|
||||
Adding this to the default character cmdset gives a result like this in-game:
|
||||
|
||||
```
|
||||
> testid
|
||||
Command memory ID: 140313967648552 (xval=1)
|
||||
> testid
|
||||
Command memory ID: 140313967648552 (xval=2)
|
||||
> testid
|
||||
Command memory ID: 140313967648552 (xval=3)
|
||||
```
|
||||
|
||||
Note how the in-memory address of the `testid` command never changes, but `xval` keeps ticking up.
|
||||
|
||||
## Dynamically created commands
|
||||
|
||||
*This is also an advanced topic.*
|
||||
|
||||
Commands can also be created and added to a cmdset on the fly. Creating a class instance with a
|
||||
keyword argument, will assign that keyword argument as a property on this paricular command:
|
||||
|
||||
```
|
||||
class MyCmdSet(CmdSet):
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
self.add(MyCommand(myvar=1, foo="test")
|
||||
|
||||
```
|
||||
|
||||
This will start the `MyCommand` with `myvar` and `foo` set as properties (accessable as `self.myvar`
|
||||
and `self.foo`). How they are used is up to the Command. Remember however the discussion from the
|
||||
previous section - since the Command instance is re-used, those properties will *remain* on the
|
||||
command as long as this cmdset and the object it sits is in memory (i.e. until the next reload).
|
||||
Unless `myvar` and `foo` are somehow reset when the command runs, they can be modified and that
|
||||
change will be remembered for subsequent uses of the command.
|
||||
|
||||
|
||||
## How commands actually work
|
||||
|
||||
*Note: This is an advanced topic mainly of interest to server developers.*
|
||||
|
||||
Any time the user sends text to Evennia, the server tries to figure out if the text entered
|
||||
corresponds to a known command. This is how the command handler sequence looks for a logged-in user:
|
||||
|
||||
1. A user enters a string of text and presses enter.
|
||||
2. The user's Session determines the text is not some protocol-specific control sequence or OOB
|
||||
command, but sends it on to the command handler.
|
||||
3. Evennia's *command handler* analyzes the Session and grabs eventual references to Account and
|
||||
eventual puppeted Characters (these will be stored on the command object later). The *caller*
|
||||
property is set appropriately.
|
||||
4. If input is an empty string, resend command as `CMD_NOINPUT`. If no such command is found in
|
||||
cmdset, ignore.
|
||||
5. If command.key matches `settings.IDLE_COMMAND`, update timers but don't do anything more.
|
||||
6. The command handler gathers the CmdSets available to *caller* at this time:
|
||||
- The caller's own currently active CmdSet.
|
||||
- CmdSets defined on the current account, if caller is a puppeted object.
|
||||
- CmdSets defined on the Session itself.
|
||||
- The active CmdSets of eventual objects in the same location (if any). This includes commands
|
||||
on [Exits](./Objects.md#exits).
|
||||
- Sets of dynamically created *System commands* representing available
|
||||
[Communications](./Channels.md)
|
||||
7. All CmdSets *of the same priority* are merged together in groups. Grouping avoids order-
|
||||
dependent issues of merging multiple same-prio sets onto lower ones.
|
||||
8. All the grouped CmdSets are *merged* in reverse priority into one combined CmdSet according to
|
||||
each set's merge rules.
|
||||
9. Evennia's *command parser* takes the merged cmdset and matches each of its commands (using its
|
||||
key and aliases) against the beginning of the string entered by *caller*. This produces a set of
|
||||
candidates.
|
||||
10. The *cmd parser* next rates the matches by how many characters they have and how many percent
|
||||
matches the respective known command. Only if candidates cannot be separated will it return multiple
|
||||
matches.
|
||||
- If multiple matches were returned, resend as `CMD_MULTIMATCH`. If no such command is found in
|
||||
cmdset, return hard-coded list of matches.
|
||||
- If no match was found, resend as `CMD_NOMATCH`. If no such command is found in cmdset, give
|
||||
hard-coded error message.
|
||||
11. If a single command was found by the parser, the correct command object is plucked out of
|
||||
storage. This usually doesn't mean a re-initialization.
|
||||
12. It is checked that the caller actually has access to the command by validating the *lockstring*
|
||||
of the command. If not, it is not considered as a suitable match and `CMD_NOMATCH` is triggered.
|
||||
13. If the new command is tagged as a channel-command, resend as `CMD_CHANNEL`. If no such command
|
||||
is found in cmdset, use hard-coded implementation.
|
||||
14. Assign several useful variables to the command instance (see previous sections).
|
||||
15. Call `at_pre_command()` on the command instance.
|
||||
16. Call `parse()` on the command instance. This is fed the remainder of the string, after the name
|
||||
of the command. It's intended to pre-parse the string into a form useful for the `func()` method.
|
||||
17. Call `func()` on the command instance. This is the functional body of the command, actually
|
||||
doing useful things.
|
||||
18. Call `at_post_command()` on the command instance.
|
||||
|
||||
## Assorted notes
|
||||
|
||||
The return value of `Command.func()` is a Twisted
|
||||
[deferred](https://twistedmatrix.com/documents/current/core/howto/defer.html).
|
||||
Evennia does not use this return value at all by default. If you do, you must
|
||||
thus do so asynchronously, using callbacks.
|
||||
|
||||
```python
|
||||
# in command class func()
|
||||
def callback(ret, caller):
|
||||
caller.msg(f"Returned is {ret}")
|
||||
deferred = self.execute_command("longrunning")
|
||||
deferred.addCallback(callback, self.caller)
|
||||
```
|
||||
|
||||
This is probably not relevant to any but the most advanced/exotic designs (one might use it to
|
||||
create a "nested" command structure for example).
|
||||
|
||||
The `save_for_next` class variable can be used to implement state-persistent commands. For example
|
||||
it can make a command operate on "it", where it is determined by what the previous command operated
|
||||
on.
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
# Core Components
|
||||
|
||||
These are the 'building blocks' out of which Evennia is built. This documentation is complementary to, and often goes deeper than, the doc-strings of each component in the [API](../Evennia-API.md).
|
||||
|
||||
## Database entites
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Typeclasses.md
|
||||
Sessions.md
|
||||
Accounts.md
|
||||
Objects.md
|
||||
Scripts.md
|
||||
Channels.md
|
||||
Msg.md
|
||||
Attributes.md
|
||||
Nicks.md
|
||||
Tags.md
|
||||
Prototypes.md
|
||||
Help-System.md
|
||||
Permissions.md
|
||||
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Command-System.md
|
||||
Commands.md
|
||||
Command-Sets.md
|
||||
Default-Commands.md
|
||||
Connection-Screen.md
|
||||
Batch-Processors.md
|
||||
Batch-Code-Processor.md
|
||||
Batch-Command-Processor.md
|
||||
```
|
||||
|
||||
|
||||
## Utils and tools
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Coding-Utils.md
|
||||
EvEditor.md
|
||||
EvForm.md
|
||||
EvMenu.md
|
||||
EvMore.md
|
||||
EvTable.md
|
||||
FuncParser.md
|
||||
MonitorHandler.md
|
||||
TickerHandler.md
|
||||
Locks.md
|
||||
Signals.md
|
||||
```
|
||||
|
||||
## Web components
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Website.md
|
||||
Web-API.md
|
||||
Web-Admin.md
|
||||
```
|
||||
|
||||
## Server and network
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Portal-And-Server.md
|
||||
Inputfuncs.md
|
||||
Outputfuncs.md
|
||||
Server.md
|
||||
Setup/Server-Conf.md
|
||||
Webserver.md
|
||||
Webclient.md
|
||||
Bootstrap-Components-and-Utilities.md
|
||||
Signals.md
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# Connection Screen
|
||||
|
||||
|
||||
When you first connect to your game you are greeted by Evennia's default connection screen.
|
||||
|
||||
|
||||
==============================================================
|
||||
Welcome to Evennia, version Beta-ra4d24e8a3cab+!
|
||||
|
||||
If you have an existing account, connect to it by typing:
|
||||
connect <username> <password>
|
||||
If you need to create an account, type (without the <>'s):
|
||||
create <username> <password>
|
||||
|
||||
If you have spaces in your username, enclose it in quotes.
|
||||
Enter help for more info. look will re-show this screen.
|
||||
==============================================================
|
||||
|
||||
Effective, but not very exciting. You will most likely want to change this to be more unique for
|
||||
your game. This is simple:
|
||||
|
||||
1. Edit `mygame/server/conf/connection_screens.py`.
|
||||
1. [Reload](../Setup/Start-Stop-Reload.md) Evennia.
|
||||
|
||||
Evennia will look into this module and locate all *globally defined strings* in it. These strings
|
||||
are used as the text in your connection screen and are shown to the user at startup. If more than
|
||||
one such string/screen is defined in the module, a *random* screen will be picked from among those
|
||||
available.
|
||||
|
||||
## Commands available at the Connection Screen
|
||||
|
||||
You can also customize the [Commands](./Commands.md) available to use while the connection screen is
|
||||
shown (`connect`, `create` etc). These commands are a bit special since when the screen is running
|
||||
the account is not yet logged in. A command is made available at the login screen by adding them to
|
||||
`UnloggedinCmdSet` in `mygame/commands/default_cmdset.py`. See [Commands](./Commands.md) and the
|
||||
tutorial section on how to add new commands to a default command set.
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
# Default Commands
|
||||
|
||||
The full set of default Evennia commands currently contains 88 commands in 9 source
|
||||
files. Our policy for adding default commands is outlined [here](../Concepts/Using-MUX-as-a-Standard.md). The
|
||||
[Commands](./Commands.md) documentation explains how Commands work as well as how to make new or customize
|
||||
existing ones.
|
||||
|
||||
> Note that this page is auto-generated. Report problems to the [issue tracker](github:issues).
|
||||
|
||||
```{note}
|
||||
Some game-states add their own Commands which are not listed here. Examples include editing a text
|
||||
with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using the
|
||||
[Batch-Processor](./Batch-Processors.md)'s interactive mode.
|
||||
```
|
||||
|
||||
- [**@about** [@version]](CmdAbout) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@accounts** [@account]](CmdAccounts) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@alias** [setobjalias]](CmdSetObjAlias) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@channel** [@chan, @channels]](CmdChannel) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**@cmdsets**](CmdListCmdSets) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@copy**](CmdCopy) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@cpattr**](CmdCpAttr) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@create**](CmdCreate) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@desc**](CmdDesc) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@destroy** [@del, @delete]](CmdDestroy) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@dig**](CmdDig) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@examine** [@ex, @exam]](CmdExamine) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Building_)
|
||||
- [**@find** [@locate, @search]](CmdFind) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@link**](CmdLink) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@lock** [@locks]](CmdLock) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@mvattr**](CmdMvAttr) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@name** [@rename]](CmdName) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@objects**](CmdObjects) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@open**](CmdOpen) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@py** [@!]](CmdPy) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _System_)
|
||||
- [**@reload** [@restart]](CmdReload) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _System_)
|
||||
- [**@reset** [@reboot]](CmdReset) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _System_)
|
||||
- [**@scripts** [@script]](CmdScripts) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@server** [@serverload]](CmdServerLoad) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@service** [@services]](CmdService) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@set**](CmdSetAttribute) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@sethome**](CmdSetHome) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@shutdown**](CmdShutdown) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _System_)
|
||||
- [**@spawn** [@olc]](CmdSpawn) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@tag** [@tags]](CmdTag) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@tasks** [@delays, @task]](CmdTasks) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@teleport** [@tel]](CmdTeleport) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@tickers**](CmdTickers) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@time** [@uptime]](CmdTime) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _System_)
|
||||
- [**@tunnel** [@tun]](CmdTunnel) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@typeclass** [@parent, @swap, @type, @typeclasses, @update]](CmdTypeclass) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**@wipe**](CmdWipe) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**__unloggedin_look_command** [l, look]](CmdUnconnectedLook) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**access** [groups, hierarchy]](CmdAccess) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**batchcode** [batchcodes]](CmdBatchCode) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**batchcommands** [batchcmd, batchcommand]](CmdBatchCommands) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**charcreate**](CmdCharCreate) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**chardelete**](CmdCharDelete) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**color**](CmdColorTest) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**connect** [co, con, conn]](CmdUnconnectedConnect) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**create** [cr, cre]](CmdUnconnectedCreate) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**drop**](CmdDrop) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**encoding** [encode]](CmdUnconnectedEncoding) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**get** [grab]](CmdGet) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**give**](CmdGive) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**grapevine2chan**](CmdGrapevine2Chan) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**help** [?]](CmdHelp) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**help** [?, h]](CmdUnconnectedHelp) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**home**](CmdHome) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**ic** [puppet]](CmdIC) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**info**](CmdUnconnectedInfo) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**inventory** [i, inv]](CmdInventory) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**irc2chan**](CmdIRC2Chan) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**ircstatus**](CmdIRCStatus) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**look** [l, ls]](CmdOOCLook) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**look** [l, ls]](CmdLook) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**nick** [nickname, nicks]](CmdNick) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**ooc** [unpuppet]](CmdOOC) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**option** [options]](CmdOption) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**page** [tell]](CmdPage) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**password**](CmdPassword) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**pose** [:, emote]](CmdPose) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**quell** [unquell]](CmdQuell) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**quit**](CmdQuit) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**quit** [q, qu]](CmdUnconnectedQuit) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**rss2chan**](CmdRSS2Chan) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _Comms_)
|
||||
- [**say** [", ']](CmdSay) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**screenreader**](CmdUnconnectedScreenreader) (cmdset: [UnloggedinCmdSet](UnloggedinCmdSet), help-category: _General_)
|
||||
- [**sessions**](CmdSessions) (cmdset: [SessionCmdSet](SessionCmdSet), help-category: _General_)
|
||||
- [**setdesc**](CmdSetDesc) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**sethelp**](CmdSetHelp) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**style**](CmdStyle) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
- [**unlink**](CmdUnLink) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _Building_)
|
||||
- [**whisper**](CmdWhisper) (cmdset: [CharacterCmdSet](CharacterCmdSet), help-category: _General_)
|
||||
- [**who** [doing]](CmdWho) (cmdset: [AccountCmdSet](AccountCmdSet), help-category: _General_)
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
# EvEditor
|
||||
|
||||
|
||||
Evennia offers a powerful in-game line editor in `evennia.utils.eveditor.EvEditor`. This editor,
|
||||
mimicking the well-known VI line editor. It offers line-by-line editing, undo/redo, line deletes,
|
||||
search/replace, fill, dedent and more.
|
||||
|
||||
## Launching the editor
|
||||
|
||||
The editor is created as follows:
|
||||
|
||||
```python
|
||||
from evennia.utils.eveditor import EvEditor
|
||||
|
||||
EvEditor(caller,
|
||||
loadfunc=None, savefunc=None, quitfunc=None,
|
||||
key="")
|
||||
```
|
||||
|
||||
- `caller` (Object or Account): The user of the editor.
|
||||
- `loadfunc` (callable, optional): This is a function called when the editor is first started. It
|
||||
is called with `caller` as its only argument. The return value from this function is used as the
|
||||
starting text in the editor buffer.
|
||||
- `savefunc` (callable, optional): This is called when the user saves their buffer in the editor is
|
||||
called with two arguments, `caller` and `buffer`, where `buffer` is the current buffer.
|
||||
- `quitfunc` (callable, optional): This is called when the user quits the editor. If given, all
|
||||
cleanup and exit messages to the user must be handled by this function.
|
||||
- `key` (str, optional): This text will be displayed as an identifier and reminder while editing.
|
||||
It has no other mechanical function.
|
||||
- `persistent` (default `False`): if set to `True`, the editor will survive a reboot.
|
||||
|
||||
## Example of usage
|
||||
|
||||
This is an example command for setting a specific Attribute using the editor.
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
from evennia.utils import eveditor
|
||||
|
||||
class CmdSetTestAttr(Command):
|
||||
"""
|
||||
Set the "test" Attribute using
|
||||
the line editor.
|
||||
|
||||
Usage:
|
||||
settestattr
|
||||
|
||||
"""
|
||||
key = "settestattr"
|
||||
def func(self):
|
||||
"Set up the callbacks and launch the editor"
|
||||
def load(caller):
|
||||
"get the current value"
|
||||
return caller.attributes.get("test")
|
||||
def save(caller, buffer):
|
||||
"save the buffer"
|
||||
caller.attributes.add("test", buffer)
|
||||
def quit(caller):
|
||||
"Since we define it, we must handle messages"
|
||||
caller.msg("Editor exited")
|
||||
key = f"{self.caller}/test"
|
||||
# launch the editor
|
||||
eveditor.EvEditor(self.caller,
|
||||
loadfunc=load, savefunc=save, quitfunc=quit,
|
||||
key=key)
|
||||
```
|
||||
|
||||
## Persistent editor
|
||||
|
||||
If you set the `persistent` keyword to `True` when creating the editor, it will remain open even
|
||||
when reloading the game. In order to be persistent, an editor needs to have its callback functions
|
||||
(`loadfunc`, `savefunc` and `quitfunc`) as top-level functions defined in the module. Since these
|
||||
functions will be stored, Python will need to find them.
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
from evennia.utils import eveditor
|
||||
|
||||
def load(caller):
|
||||
"get the current value"
|
||||
return caller.attributes.get("test")
|
||||
|
||||
def save(caller, buffer):
|
||||
"save the buffer"
|
||||
caller.attributes.add("test", buffer)
|
||||
|
||||
def quit(caller):
|
||||
"Since we define it, we must handle messages"
|
||||
caller.msg("Editor exited")
|
||||
|
||||
class CmdSetTestAttr(Command):
|
||||
"""
|
||||
Set the "test" Attribute using
|
||||
the line editor.
|
||||
|
||||
Usage:
|
||||
settestattr
|
||||
|
||||
"""
|
||||
key = "settestattr"
|
||||
def func(self):
|
||||
"Set up the callbacks and launch the editor"
|
||||
key = f"{self.caller}/test"
|
||||
# launch the editor
|
||||
eveditor.EvEditor(self.caller,
|
||||
loadfunc=load, savefunc=save, quitfunc=quit,
|
||||
key=key, persistent=True)
|
||||
```
|
||||
|
||||
## Line editor usage
|
||||
|
||||
The editor mimics the `VIM` editor as best as possible. The below is an excerpt of the return from
|
||||
the in-editor help command (`:h`).
|
||||
|
||||
```
|
||||
<txt> - any non-command is appended to the end of the buffer.
|
||||
: <l> - view buffer or only line <l>
|
||||
:: <l> - view buffer without line numbers or other parsing
|
||||
::: - print a ':' as the only character on the line...
|
||||
:h - this help.
|
||||
|
||||
:w - save the buffer (don't quit)
|
||||
:wq - save buffer and quit
|
||||
:q - quit (will be asked to save if buffer was changed)
|
||||
:q! - quit without saving, no questions asked
|
||||
|
||||
:u - (undo) step backwards in undo history
|
||||
:uu - (redo) step forward in undo history
|
||||
:UU - reset all changes back to initial state
|
||||
|
||||
:dd <l> - delete line <n>
|
||||
:dw <l> <w> - delete word or regex <w> in entire buffer or on line <l>
|
||||
:DD - clear buffer
|
||||
|
||||
:y <l> - yank (copy) line <l> to the copy buffer
|
||||
:x <l> - cut line <l> and store it in the copy buffer
|
||||
:p <l> - put (paste) previously copied line directly after <l>
|
||||
:i <l> <txt> - insert new text <txt> at line <l>. Old line will move down
|
||||
:r <l> <txt> - replace line <l> with text <txt>
|
||||
:I <l> <txt> - insert text at the beginning of line <l>
|
||||
:A <l> <txt> - append text after the end of line <l>
|
||||
|
||||
:s <l> <w> <txt> - search/replace word or regex <w> in buffer or on line <l>
|
||||
|
||||
:f <l> - flood-fill entire buffer or line <l>
|
||||
:fi <l> - indent entire buffer or line <l>
|
||||
:fd <l> - de-indent entire buffer or line <l>
|
||||
|
||||
:echo - turn echoing of the input on/off (helpful for some clients)
|
||||
|
||||
Legend:
|
||||
<l> - line numbers, or range lstart:lend, e.g. '3:7'.
|
||||
<w> - one word or several enclosed in quotes.
|
||||
<txt> - longer string, usually not needed to be enclosed in quotes.
|
||||
```
|
||||
|
||||
## The EvEditor to edit code
|
||||
|
||||
The `EvEditor` is also used to edit some Python code in Evennia. The `@py` command supports an
|
||||
`/edit` switch that will open the EvEditor in code mode. This mode isn't significantly different
|
||||
from the standard one, except it handles automatic indentation of blocks and a few options to
|
||||
control this behavior.
|
||||
|
||||
- `:<` to remove a level of indentation for the future lines.
|
||||
- `:+` to add a level of indentation for the future lines.
|
||||
- `:=` to disable automatic indentation altogether.
|
||||
|
||||
Automatic indentation is there to make code editing more simple. Python needs correct indentation,
|
||||
not as an aesthetic addition, but as a requirement to determine beginning and ending of blocks. The
|
||||
EvEditor will try to guess the next level of indentation. If you type a block "if", for instance,
|
||||
the EvEditor will propose you an additional level of indentation at the next line. This feature
|
||||
cannot be perfect, however, and sometimes, you will have to use the above options to handle
|
||||
indentation.
|
||||
|
||||
`:=` can be used to turn automatic indentation off completely. This can be very useful when trying
|
||||
to paste several lines of code that are already correctly indented, for instance.
|
||||
|
||||
To see the EvEditor in code mode, you can use the `@py/edit` command. Type in your code (on one or
|
||||
several lines). You can then use the `:w` option (save without quitting) and the code you have
|
||||
typed will be executed. The `:!` will do the same thing. Executing code while not closing the
|
||||
editor can be useful if you want to test the code you have typed but add new lines after your test.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# EvForm
|
||||
|
||||
[Docstring in evennia/utils/evform.py](evennia.utils.evform)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,37 +0,0 @@
|
|||
# EvMore
|
||||
|
||||
|
||||
When sending a very long text to a user client, it might scroll beyond of the height of the client
|
||||
window. The `evennia.utils.evmore.EvMore` class gives the user the in-game ability to only view one
|
||||
page of text at a time. It is usually used via its access function, `evmore.msg`.
|
||||
|
||||
The name comes from the famous unix pager utility *more* which performs just this function.
|
||||
|
||||
## Using EvMore
|
||||
|
||||
To use the pager, just pass the long text through it:
|
||||
|
||||
```python
|
||||
from evennia.utils import evmore
|
||||
|
||||
evmore.msg(receiver, long_text)
|
||||
```
|
||||
Where receiver is an [Object](./Objects.md) or a [Account](./Accounts.md). If the text is longer than the
|
||||
client's screen height (as determined by the NAWS handshake or by `settings.CLIENT_DEFAULT_HEIGHT`)
|
||||
the pager will show up, something like this:
|
||||
|
||||
>[...]
|
||||
aute irure dolor in reprehenderit in voluptate velit
|
||||
esse cillum dolore eu fugiat nulla pariatur. Excepteur
|
||||
sint occaecat cupidatat non proident, sunt in culpa qui
|
||||
officia deserunt mollit anim id est laborum.
|
||||
|
||||
>(**more** [1/6] retur**n**|**b**ack|**t**op|**e**nd|**a**bort)
|
||||
|
||||
|
||||
where the user will be able to hit the return key to move to the next page, or use the suggested
|
||||
commands to jump to previous pages, to the top or bottom of the document as well as abort the
|
||||
paging.
|
||||
|
||||
The pager takes several more keyword arguments for controlling the message output. See the
|
||||
[evmore-API](github:evennia.utils.evmore) for more info.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# EvTable
|
||||
|
||||
[Docstring in evennia/utils/evtable.py](evennia.utils.evtable)
|
||||
|
|
@ -1,446 +0,0 @@
|
|||
# The Inline Function Parser
|
||||
|
||||
The [FuncParser](evennia.utils.funcparser.FuncParser) extracts and executes
|
||||
'inline functions'
|
||||
embedded in a string on the form `$funcname(args, kwargs)`. Under the hood, this will
|
||||
lead to a call to a Python function you control. The inline function call will be replaced by
|
||||
the return from the function.
|
||||
|
||||
```python
|
||||
from evennia.utils.funcparser import FuncParser
|
||||
|
||||
def _power_callable(*args, **kwargs):
|
||||
"""This will be callable as $pow(number, power=<num>) in string"""
|
||||
pow = int(kwargs.get('power', 2))
|
||||
return float(args[0]) ** pow
|
||||
|
||||
# create a parser and tell it that '$pow' means using _power_callable
|
||||
parser = FuncParser({"pow": _power_callable})
|
||||
|
||||
```
|
||||
Next, just pass a string into the parser, containing `$func(...)` markers:
|
||||
|
||||
```python
|
||||
parser.parse("We have that 4 x 4 x 4 is $pow(4, power=3).")
|
||||
"We have that 4 x 4 x 4 is 64."
|
||||
```
|
||||
|
||||
Normally the return is always converted to a string but you can also get the actual data type from the call:
|
||||
|
||||
```python
|
||||
parser.parse_to_any("$pow(4)")
|
||||
16
|
||||
```
|
||||
|
||||
To show a `$func()` verbatim in your code without parsing it, escape it as either `$$func()` or `\$func()`:
|
||||
|
||||
|
||||
```python
|
||||
parser.parse("This is an escaped $$pow(4) and so is this \$pow(3)")
|
||||
"This is an escaped $pow(4) and so is this $pow(3)"
|
||||
```
|
||||
|
||||
## Uses in default Evennia
|
||||
|
||||
The FuncParser can be applied to any string. Out of the box it's applied in a few situations:
|
||||
|
||||
- _Outgoing messages_. All messages sent from the server is processed through FuncParser and every
|
||||
callable is provided the [Session](./Sessions.md) of the object receiving the message. This potentially
|
||||
allows a message to be modified on the fly to look different for different recipients.
|
||||
- _Prototype values_. A [Prototype](./Prototypes.md) dict's values are run through the parser such that every
|
||||
callable gets a reference to the rest of the prototype. In the Prototype ORM, this would allow builders
|
||||
to safely call functions to set non-string values to prototype values, get random values, reference
|
||||
other fields of the prototype, and more.
|
||||
- _Actor-stance in messages to others_. In the
|
||||
[Object.msg_contents](evennia.objects.objects.DefaultObject.msg_contents) method,
|
||||
the outgoing string is parsed for special `$You()` and `$conj()` callables to decide if a given recipient
|
||||
should see "You" or the character's name.
|
||||
|
||||
```{important}
|
||||
The inline-function parser is not intended as a 'softcode' programming language. It does not
|
||||
have things like loops and conditionals, for example. While you could in principle extend it to
|
||||
do very advanced things and allow builders a lot of power, all-out coding is something
|
||||
Evennia expects you to do in a proper text editor, outside of the game, not from inside it.
|
||||
```
|
||||
|
||||
## Using the FuncParser
|
||||
|
||||
You can apply inline function parsing to any string. The
|
||||
[FuncParser](evennia.utils.funcparser.FuncParser) is imported as `evennia.utils.funcparser`.
|
||||
|
||||
```python
|
||||
from evennia.utils import funcparser
|
||||
|
||||
parser = FuncParser(callables, **default_kwargs)
|
||||
parsed_string = parser.parse(input_string, raise_errors=False,
|
||||
escape=False, strip=False,
|
||||
return_str=True, **reserved_kwargs)
|
||||
|
||||
# callables can also be passed as paths to modules
|
||||
parser = FuncParser(["game.myfuncparser_callables", "game.more_funcparser_callables"])
|
||||
```
|
||||
|
||||
Here, `callables` points to a collection of normal Python functions (see next section) for you to make
|
||||
available to the parser as you parse strings with it. It can either be
|
||||
- A `dict` of `{"functionname": callable, ...}`. This allows you do pick and choose exactly which callables
|
||||
to include and how they should be named. Do you want a callable to be available under more than one name?
|
||||
Just add it multiple times to the dict, with a different key.
|
||||
- A `module` or (more commonly) a `python-path` to a module. This module can define a dict
|
||||
`FUNCPARSER_CALLABLES = {"funcname": callable, ...}` - this will be imported and used like the `dict` above.
|
||||
If no such variable is defined, _every_ top-level function in the module (whose name doesn't start with
|
||||
an underscore `_`) will be considered a suitable callable. The name of the function will be the `$funcname`
|
||||
by which it can be called.
|
||||
- A `list` of modules/paths. This allows you to pull in modules from many sources for your parsing.
|
||||
- The `**default` kwargs are optional kwargs that will be passed to _all_
|
||||
callables every time this parser is used - unless the user overrides it explicitly in
|
||||
their call. This is great for providing sensible standards that the user can
|
||||
tweak as needed.
|
||||
|
||||
`FuncParser.parse` takes further arguments, and can vary for every string parsed.
|
||||
|
||||
- `raise_errors` - By default, any errors from a callable will be quietly ignored and the result
|
||||
will be that the failing function call will show verbatim. If `raise_errors` is set,
|
||||
then parsing will stop and whatever exception happened will be raised. It'd be up to you to handle
|
||||
this properly.
|
||||
- `escape` - Returns a string where every `$func(...)` has been escaped as `\$func()`.
|
||||
- `strip` - Remove all `$func(...)` calls from string (as if each returned `''`).
|
||||
- `return_str` - When `True` (default), `parser` always returns a string. If `False`, it may return
|
||||
the return value of a single function call in the string. This is the same as using the `.parse_to_any`
|
||||
method.
|
||||
- The `**reserved_keywords` are _always_ passed to every callable in the string.
|
||||
They override any `**defaults` given when instantiating the parser and cannot
|
||||
be overridden by the user - if they enter the same kwarg it will be ignored.
|
||||
This is great for providing the current session, settings etc.
|
||||
- The `funcparser` and `raise_errors`
|
||||
are always added as reserved keywords - the first is a
|
||||
back-reference to the `FuncParser` instance and the second
|
||||
is the `raise_errors` boolean given to `FuncParser.parse`.
|
||||
|
||||
Here's an example of using the default/reserved keywords:
|
||||
|
||||
```python
|
||||
def _test(*args, **kwargs):
|
||||
# do stuff
|
||||
return something
|
||||
|
||||
parser = funcparser.FuncParser({"test": _test}, mydefault=2)
|
||||
result = parser.parse("$test(foo, bar=4)", myreserved=[1, 2, 3])
|
||||
```
|
||||
Here the callable will be called as
|
||||
|
||||
```python
|
||||
_test('foo', bar='4', mydefault=2, myreserved=[1, 2, 3],
|
||||
funcparser=<FuncParser>, raise_errors=False)
|
||||
```
|
||||
|
||||
The `mydefault=2` kwarg could be overwritten if we made the call as `$test(mydefault=...)`
|
||||
but `myreserved=[1, 2, 3]` will _always_ be sent as-is and will override a call `$test(myreserved=...)`.
|
||||
The `funcparser`/`raise_errors` kwargs are also always included as reserved kwargs.
|
||||
|
||||
## Defining custom callables
|
||||
|
||||
All callables made available to the parser must have the following signature:
|
||||
|
||||
```python
|
||||
def funcname(*args, **kwargs):
|
||||
# ...
|
||||
return something
|
||||
```
|
||||
|
||||
> The `*args` and `**kwargs` must always be included. If you are unsure how `*args` and `**kwargs` work in Python,
|
||||
> [read about them here](https://www.digitalocean.com/community/tutorials/how-to-use-args-and-kwargs-in-python-3).
|
||||
|
||||
The input from the innermost `$funcname(...)` call in your callable will always be a `str`. Here's
|
||||
an example of an `$toint` function; it converts numbers to integers.
|
||||
|
||||
"There's a $toint(22.0)% chance of survival."
|
||||
|
||||
What will enter the `$toint` callable (as `args[0]`) is the _string_ `"22.0"`. The function is responsible
|
||||
for converting this to a number so that we can convert it to an integer. We must also properly handle invalid
|
||||
inputs (like non-numbers).
|
||||
|
||||
If you want to mark an error, raise `evennia.utils.funcparser.ParsingError`. This stops the entire parsing
|
||||
of the string and may or may not raise the exception depending on what you set `raise_errors` to when you
|
||||
created the parser.
|
||||
|
||||
However, if you _nest_ functions, the return of the innermost function may be something other than
|
||||
a string. Let's introduce the `$eval` function, which evaluates simple expressions using
|
||||
Python's `literal_eval` and/or `simple_eval`. It returns whatever data type it
|
||||
evaluates to.
|
||||
|
||||
"There's a $toint($eval(10 * 2.2))% chance of survival."
|
||||
|
||||
Since the `$eval` is the innermost call, it will get a string as input - the string `"10 * 2.2"`.
|
||||
It evaluates this and returns the `float` `22.0`. This time the outermost `$toint` will be called with
|
||||
this `float` instead of with a string.
|
||||
|
||||
> It's important to safely validate your inputs since users may end up nesting your callables in any order.
|
||||
> See the next section for useful tools to help with this.
|
||||
|
||||
In these examples, the result will be embedded in the larger string, so the result of the entire parsing
|
||||
will be a string:
|
||||
|
||||
```python
|
||||
parser.parse(above_string)
|
||||
"There's a 22% chance of survival."
|
||||
```
|
||||
|
||||
However, if you use the `parse_to_any` (or `parse(..., return_str=False)`) and
|
||||
_don't add any extra string around the outermost function call_,
|
||||
you'll get the return type of the outermost callable back:
|
||||
|
||||
```python
|
||||
parser.parse_to_any("$toint($eval(10 * 2.2)")
|
||||
22
|
||||
parser.parse_to_any("the number $toint($eval(10 * 2.2).")
|
||||
"the number 22"
|
||||
parser.parse_to_any("$toint($eval(10 * 2.2)%")
|
||||
"22%"
|
||||
```
|
||||
|
||||
### Escaping special character
|
||||
|
||||
When entering funcparser callables in strings, it looks like a regular
|
||||
function call inside a string:
|
||||
|
||||
```python
|
||||
"This is a $myfunc(arg1, arg2, kwarg=foo)."
|
||||
```
|
||||
|
||||
Commas (`,`) and equal-signs (`=`) are considered to separate the arguments and
|
||||
kwargs. In the same way, the right parenthesis (`)`) closes the argument list.
|
||||
Sometimes you want to include commas in the argument without it breaking the
|
||||
argument list.
|
||||
|
||||
```python
|
||||
"The $format(forest's smallest meadow, with dandelions) is to the west."
|
||||
```
|
||||
|
||||
You can escape in various ways.
|
||||
|
||||
- Prepending special characters like `,` and `=` with the escape character `\`
|
||||
|
||||
```python
|
||||
"The $format(forest's smallest meadow\, with dandelions) is to the west."
|
||||
```
|
||||
|
||||
- Wrapping your strings in double quotes. Unlike in raw Python, you
|
||||
can't escape with single quotes `'` since these could also be apostrophes (like
|
||||
`forest's` above). The result will be a verbatim string that contains
|
||||
everything but the outermost double quotes.
|
||||
|
||||
```python
|
||||
'The $format("forest's smallest meadow, with dandelions") is to the west.'
|
||||
```
|
||||
- If you want verbatim double-quotes to appear in your string, you can escape
|
||||
them with `\"` in turn.
|
||||
|
||||
```python
|
||||
'The $format("forest's smallest meadow, with \"dandelions\"') is to the west.'
|
||||
```
|
||||
|
||||
### Safe convertion of inputs
|
||||
|
||||
Since you don't know in which order users may use your callables, they should
|
||||
always check the types of its inputs and convert to the type the callable needs.
|
||||
Note also that when converting from strings, there are limits what inputs you
|
||||
can support. This is because FunctionParser strings can be used by
|
||||
non-developer players/builders and some things (such as complex
|
||||
classes/callables etc) are just not safe/possible to convert from string
|
||||
representation.
|
||||
|
||||
In `evennia.utils.utils` is a helper called
|
||||
[safe_convert_to_types](evennia.utils.utils.safe_convert_to_types). This function
|
||||
automates the conversion of simple data types in a safe way:
|
||||
|
||||
```python
|
||||
from evennia.utils.utils import safe_convert_to_types
|
||||
|
||||
def _process_callable(*args, **kwargs):
|
||||
"""
|
||||
$process(expression, local, extra1=34, extra2=foo)
|
||||
|
||||
"""
|
||||
args, kwargs = safe_convert_to_type(
|
||||
(('py', str), {'extra1': int, 'extra2': str}),
|
||||
*args, **kwargs)
|
||||
|
||||
# args/kwargs should be correct types now
|
||||
|
||||
```
|
||||
|
||||
In other words, in the callable `$process(expression, local, extra1=..,
|
||||
extra2=...)`, the first argument will be handled by the 'py' converter
|
||||
(described below), the second will passed through regular Python `str`,
|
||||
kwargs will be handled by `int` and `str` respectively. You can supply
|
||||
your own converter function as long as it takes one argument and returns
|
||||
the converted result.
|
||||
|
||||
In other words,
|
||||
|
||||
```python
|
||||
args, kwargs = safe_convert_to_type(
|
||||
(tuple_of_arg_converters, dict_of_kwarg_converters), *args, **kwargs)
|
||||
```
|
||||
|
||||
The special converter `"py"` will try to convert a string argument to a Python structure with the help of the
|
||||
following tools (which you may also find useful to experiment with on your own):
|
||||
|
||||
- [ast.literal_eval](https://docs.python.org/3.8/library/ast.html#ast.literal_eval) is an in-built Python
|
||||
function. It
|
||||
_only_ supports strings, bytes, numbers, tuples, lists, dicts, sets, booleans and `None`. That's
|
||||
it - no arithmetic or modifications of data is allowed. This is good for converting individual values and
|
||||
lists/dicts from the input line to real Python objects.
|
||||
- [simpleeval](https://pypi.org/project/simpleeval/) is a third-party tool included with Evennia. This
|
||||
allows for evaluation of simple (and thus safe) expressions. One can operate on numbers and strings
|
||||
with `+-/*` as well as do simple comparisons like `4 > 3` and more. It does _not_ accept more complex
|
||||
containers like lists/dicts etc, so this and `literal_eval` are complementary to each other.
|
||||
|
||||
```{warning}
|
||||
It may be tempting to run use Python's in-built ``eval()`` or ``exec()`` functions as converters since
|
||||
these are able to convert any valid Python source code to Python. NEVER DO THIS unless you really, really
|
||||
know that ONLY developers will ever modify the string going into the callable. The parser is intended
|
||||
for untrusted users (if you were trusted you'd have access to Python already). Letting untrusted users
|
||||
pass strings to ``eval``/``exec`` is a MAJOR security risk. It allows the caller to run arbitrary
|
||||
Python code on your server. This is the path to maliciously deleted hard drives. Just don't do it and
|
||||
sleep better at night.
|
||||
```
|
||||
|
||||
## Default callables
|
||||
|
||||
These are some example callables you can import and add your parser. They are divided into
|
||||
global-level dicts in `evennia.utils.funcparser`. Just import the dict(s) and merge/add one or
|
||||
more to them when you create your `FuncParser` instance to have those callables be available.
|
||||
|
||||
### `evennia.utils.funcparser.FUNCPARSER_CALLABLES`
|
||||
|
||||
These are the 'base' callables.
|
||||
|
||||
- `$eval(expression)` ([code](evennia.utils.funcparser.funcparser_callable_eval)) -
|
||||
this uses `literal_eval` and `simple_eval` (see previous section) attemt to convert a string expression
|
||||
to a python object. This handles e.g. lists of literals `[1, 2, 3]` and simple expressions like `"1 + 2"`.
|
||||
- `$toint(number)` ([code](evennia.utils.funcparser.funcparser_callable_toint)) -
|
||||
always converts an output to an integer, if possible.
|
||||
- `$add/sub/mult/div(obj1, obj2)` ([code](evennia.utils.funcparser.funcparser_callable_add)) -
|
||||
this adds/subtracts/multiplies and divides to elements together. While simple addition could be done with
|
||||
`$eval`, this could for example be used also to add two lists together, which is not possible with `eval`;
|
||||
for example `$add($eval([1,2,3]), $eval([4,5,6])) -> [1, 2, 3, 4, 5, 6]`.
|
||||
- `$round(float, significant)` ([code](evennia.utils.funcparser.funcparser_callable_round)) -
|
||||
rounds an input float into the number of provided significant digits. For example `$round(3.54343, 3) -> 3.543`.
|
||||
- `$random([start, [end]])` ([code](evennia.utils.funcparser.funcparser_callable_random)) -
|
||||
this works like the Python `random()` function, but will randomize to an integer value if both start/end are
|
||||
integers. Without argument, will return a float between 0 and 1.
|
||||
- `$randint([start, [end]])` ([code](evennia.utils.funcparser.funcparser_callable_randint)) -
|
||||
works like the `randint()` python function and always returns an integer.
|
||||
- `$choice(list)` ([code](evennia.utils.funcparser.funcparser_callable_choice)) -
|
||||
the input will automatically be parsed the same way as `$eval` and is expected to be an iterable. A random
|
||||
element of this list will be returned.
|
||||
- `$pad(text[, width, align, fillchar])` ([code](evennia.utils.funcparser.funcparser_callable_pad)) -
|
||||
this will pad content. `$pad("Hello", 30, c, -)` will lead to a text centered in a 30-wide block surrounded by `-`
|
||||
characters.
|
||||
- `$crop(text, width=78, suffix='[...]')` ([code](evennia.utils.funcparser.funcparser_callable_crop)) -
|
||||
this will crop a text longer than the width, by default ending it with a `[...]`-suffix that also fits within
|
||||
the width. If no width is given, the client width or `settings.DEFAULT_CLIENT_WIDTH` will be used.
|
||||
- `$space(num)` ([code](evennia.utils.funcparser.funcparser_callable_space)) -
|
||||
this will insert `num` spaces.
|
||||
- `$just(string, width=40, align=c, indent=2)` ([code](evennia.utils.funcparser.funcparser_callable_justify)) -
|
||||
justifies the text to a given width, aligning it left/right/center or 'f' for full (spread text across width).
|
||||
- `$ljust` - shortcut to justify-left. Takes all other kwarg of `$just`.
|
||||
- `$rjust` - shortcut to right justify.
|
||||
- `$cjust` - shortcut to center justify.
|
||||
- `$clr(startcolor, text[, endcolor])` ([code](evennia.utils.funcparser.funcparser_callable_clr)) -
|
||||
color text. The color is given with one or two characters without the preceeding `|`. If no endcolor is
|
||||
given, the string will go back to neutral, so `$clr(r, Hello)` is equivalent to `|rHello|n`.
|
||||
|
||||
### `evennia.utils.funcparser.SEARCHING_CALLABLES`
|
||||
|
||||
These are callables that requires access-checks in order to search for objects. So they require some
|
||||
extra reserved kwargs to be passed when running the parser:
|
||||
|
||||
```python
|
||||
|
||||
parser.parse_to_any(string, caller=<object or account>, access="control", ...)
|
||||
|
||||
```
|
||||
The `caller` is required, it's the the object to do the access-check for. The `access` kwarg is the
|
||||
[lock type](./Locks.md) to check, default being `"control"`.
|
||||
|
||||
- `$search(query,type=account|script,return_list=False)` ([code](evennia.utils.funcparser.funcparser_callable_search)) -
|
||||
this will look up and try to match an object by key or alias. Use the `type` kwarg to
|
||||
search for `account` or `script` instead. By default this will return nothing if there are more than one
|
||||
match; if `return_list` is `True` a list of 0, 1 or more matches will be returned instead.
|
||||
- `$obj(query)`, `$dbref(query)` - legacy aliases for `$search`.
|
||||
- `$objlist(query)` - legacy alias for `$search`, always returning a list.
|
||||
|
||||
|
||||
### `evennia.utils.funcparser.ACTOR_STANCE_CALLABLES`
|
||||
|
||||
These are used to implement actor-stance emoting. They are used by the
|
||||
[DefaultObject.msg_contents](evennia.objects.objects.DefaultObject.msg_contents) method
|
||||
by default. You can read a lot more about this on the page
|
||||
[Change messages per receiver](../Concepts/Change-Messages-Per-Receiver.md).
|
||||
|
||||
On the parser side, all these inline functions require extra kwargs be passed into the parser
|
||||
(done by `msg_contents` by default):
|
||||
|
||||
```python
|
||||
parser.parse(string, caller=<obj>, receiver=<obj>, mapping={'key': <obj>, ...})
|
||||
```
|
||||
|
||||
Here the `caller` is the one sending the message and `receiver` the one to see it. The `mapping` contains
|
||||
references to other objects accessible via these callables.
|
||||
|
||||
- `$you([key])` ([code](evennia.utils.funcparser.funcparser_callable_you)) -
|
||||
if no `key` is given, this represents the `caller`, otherwise an object from `mapping`
|
||||
will be used. As this message is sent to different recipients, the `receiver` will change and this will
|
||||
be replaced either with the string `you` (if you and the receiver is the same entity) or with the
|
||||
result of `you_obj.get_display_name(looker=receiver)`. This allows for a single string to echo differently
|
||||
depending on who sees it, and also to reference other people in the same way.
|
||||
- `$You([key])` - same as `$you` but always capitalized.
|
||||
- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb
|
||||
between 2nd person presens to 3rd person presence depending on who
|
||||
sees the string. For example `"$You() $conj(smiles)".` will show as "You smile." and "Tom smiles." depending
|
||||
on who sees it. This makes use of the tools in [evennia.utils.verb_conjugation](evennia.utils.verb_conjugation)
|
||||
to do this, and only works for English verbs.
|
||||
- `$pron(pronoun [,options])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically
|
||||
map pronouns (like his, herself, you, its etc) between 1st/2nd person to 3rd person.
|
||||
|
||||
### Example
|
||||
|
||||
Here's an example of including the default callables together with two custom ones.
|
||||
|
||||
```python
|
||||
from evennia.utils import funcparser
|
||||
from evennia.utils import gametime
|
||||
|
||||
def _dashline(*args, **kwargs):
|
||||
if args:
|
||||
return f"\n-------- {args[0]} --------"
|
||||
return ''
|
||||
|
||||
def _uptime(*args, **kwargs):
|
||||
return gametime.uptime()
|
||||
|
||||
callables = {
|
||||
"dashline": _dashline,
|
||||
"uptime": _uptime,
|
||||
**funcparser.FUNCPARSER_CALLABLES,
|
||||
**funcparser.ACTOR_STANCE_CALLABLES,
|
||||
**funcparser.SEARCHING_CALLABLES
|
||||
}
|
||||
|
||||
parser = funcparser.FuncParser(callables)
|
||||
|
||||
string = "This is the current uptime:$dashline($toint($uptime()) seconds)"
|
||||
result = parser.parse(string)
|
||||
|
||||
```
|
||||
|
||||
Above we define two callables `_dashline` and `_uptime` and map them to names `"dashline"` and `"uptime"`,
|
||||
which is what we then can call as `$header` and `$uptime` in the string. We also have access to
|
||||
all the defaults (like `$toint()`).
|
||||
|
||||
The parsed result of the above would be something like this:
|
||||
|
||||
This is the current uptime:
|
||||
------- 343 seconds -------
|
||||
|
|
@ -1,407 +0,0 @@
|
|||
# Help System
|
||||
|
||||
Evennia has an extensive help system covering both command-help and regular
|
||||
free-form help documentation. It supports subtopics and if failing to find a
|
||||
match it will provide suggestsions, first from alternative topics and then by
|
||||
finding mentions of the search term in help entries.
|
||||
|
||||
|
||||
> help theatre
|
||||
|
||||
```
|
||||
------------------------------------------------------------------------------
|
||||
Help for The theatre (aliases: the hub, curtains)
|
||||
|
||||
The theatre is at the centre of the city, both literally and figuratively ...
|
||||
(A lot more text about it follows ...)
|
||||
|
||||
Subtopics:
|
||||
theatre/lore
|
||||
theatre/layout
|
||||
theatre/dramatis personae
|
||||
------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
> help evennia
|
||||
|
||||
```
|
||||
------------------------------------------------------------------------------
|
||||
No help found
|
||||
|
||||
There is no help topic matching 'evennia'.
|
||||
... But matches where found within the help texts of the suggestions below.
|
||||
|
||||
Suggestions:
|
||||
grapevine2chan, about, irc2chan
|
||||
-----------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
## Using the help system from in-game
|
||||
|
||||
The help system is accessed in-game by use of the `help` command:
|
||||
|
||||
help <topic>
|
||||
|
||||
Sub-topics are accessed as `help <topic>/<subtopic>/...`.
|
||||
|
||||
Creating a new help entry from in-game is done with
|
||||
|
||||
sethelp <topic>[;aliases] [,category] [,lockstring] = <text>
|
||||
|
||||
For example
|
||||
|
||||
sethelp The Gods;pantheon, Lore = In the beginning all was dark ...
|
||||
|
||||
Use the `/edit` switch to open the EvEditor for more convenient in-game writing
|
||||
(but note that devs can also create help entries outside the game using their
|
||||
regular code editor, see below).
|
||||
|
||||
> You can also create help entries as Python modules, outside of the game. See
|
||||
> _FileHelp_ entries below.
|
||||
|
||||
## Sources of help entries
|
||||
|
||||
Evennia collects help entries from three sources:
|
||||
|
||||
- _Auto-generated command help_ - this is literally the doc-strings of
|
||||
the [Command classes](./Commands.md). The idea is that the command docs are
|
||||
easier to maintain and keep up-to-date if the developer can change them at the
|
||||
same time as they do the code.
|
||||
- _Database-stored help entries_ - These are created in-game (using the
|
||||
default `sethelp` command as exemplified in the previous section).
|
||||
- _File-stored help entries_ - These are created outside the game, as dicts in
|
||||
normal Python modules. They allows developers to write and maintain their help
|
||||
files using a proper text editor.
|
||||
|
||||
### The Help Entry
|
||||
|
||||
All help entries (no matter the source) have the following properties:
|
||||
|
||||
- `key` - This is the main topic-name. For Commands, this is literally the
|
||||
command's `key`.
|
||||
- `aliases` - Alternate names for the help entry. This can be useful if the main
|
||||
name is hard to remember.
|
||||
- `help_category` - The general grouping of the entry. This is optional. If not
|
||||
given it will use the default category given by
|
||||
`settings.COMMAND_DEFAULT_HELP_CATEGORY` for Commands and
|
||||
`settings.DEFAULT_HELP_CATEGORY` for file+db help entries.
|
||||
- `locks` - Lock string (for commands) or LockHandler (all help entries).
|
||||
This defines who may read this entry. See the next section.
|
||||
- `tags` - This is not used by default, but could be used to further organize
|
||||
help entries.
|
||||
- `text` - The actual help entry text. This will be dedented and stripped of
|
||||
extra space at beginning and end.
|
||||
|
||||
A `text` that scrolls off the screen will automatically be paginated by
|
||||
the [EvMore](./EvMore.md) pager (you can control this with
|
||||
`settings.HELP_MORE_ENABLED=False`). If you use EvMore and want to control
|
||||
exactly where the pager should break the page, mark the break with the control
|
||||
character `\f`.
|
||||
|
||||
#### Subtopics
|
||||
|
||||
```{versionadded} 1.0
|
||||
```
|
||||
|
||||
Rather than making a very long help entry, the `text` may also be broken up
|
||||
into _subtopics_. A list of the next level of subtopics are shown below the
|
||||
main help text and allows the user to read more about some particular detail
|
||||
that wouldn't fit in the main text.
|
||||
|
||||
Subtopics use a markup slightly similar to markdown headings. The top level
|
||||
heading must be named `# subtopics` (non case-sensitive) and the following
|
||||
headers must be sub-headings to this (so `## subtopic name` etc). All headings
|
||||
are non-case sensitive (the help command will format them). The topics can be
|
||||
nested at most to a depth of 5 (which is probably too many levels already). The
|
||||
parser uses fuzzy matching to find the subtopic, so one does not have to type
|
||||
it all out exactly.
|
||||
|
||||
Below is an example of a `text` with sub topics.
|
||||
|
||||
```
|
||||
The theatre is the heart of the city, here you can find ...
|
||||
(This is the main help text, what you get with `help theatre`)
|
||||
|
||||
# subtopics
|
||||
|
||||
## lore
|
||||
|
||||
The theatre holds many mysterious things...
|
||||
(`help theatre/lore`)
|
||||
|
||||
### the grand opening
|
||||
|
||||
The grand opening is the name for a mysterious event where ghosts appeared ...
|
||||
(`this is a subsub-topic to lore, accessible as `help theatre/lore/grand` or
|
||||
any other partial match).
|
||||
|
||||
### the Phantom
|
||||
|
||||
Deep under the theatre, rumors has it a monster hides ...
|
||||
(another subsubtopic, accessible as `help theatre/lore/phantom`)
|
||||
|
||||
## layout
|
||||
|
||||
The theatre is a two-story building situated at ...
|
||||
(`help theatre/layout`)
|
||||
|
||||
## dramatis personae
|
||||
|
||||
There are many interesting people prowling the halls of the theatre ...
|
||||
(`help theatre/dramatis` or `help theathre/drama` or `help theatre/personae` would work)
|
||||
|
||||
### Primadonna Ada
|
||||
|
||||
Everyone knows the primadonna! She is ...
|
||||
(A subtopic under dramatis personae, accessible as `help theatre/drama/ada` etc)
|
||||
|
||||
### The gatekeeper
|
||||
|
||||
He always keeps an eye on the door and ...
|
||||
(`help theatre/drama/gate`)
|
||||
|
||||
```
|
||||
### Command Auto-help system
|
||||
|
||||
The auto-help system uses the `__doc__` strings of your command classes and
|
||||
formats this to a nice- looking help entry. This makes for a very easy way to
|
||||
keep the help updated - just document your commands well and updating the help
|
||||
file is just a `reload` away.
|
||||
|
||||
Example (from a module with command definitions):
|
||||
|
||||
```python
|
||||
class CmdMyCmd(Command):
|
||||
"""
|
||||
mycmd - my very own command
|
||||
|
||||
Usage:
|
||||
mycmd[/switches] <args>
|
||||
|
||||
Switches:
|
||||
test - test the command
|
||||
run - do something else
|
||||
|
||||
This is my own command that does this and that.
|
||||
|
||||
"""
|
||||
# [...]
|
||||
|
||||
locks = "cmd:all();read:all()" # default
|
||||
help_category = "General" # default
|
||||
auto_help = True # default
|
||||
|
||||
# [...]
|
||||
```
|
||||
|
||||
The text at the very top of the command class definition is the class'
|
||||
`__doc__`-string and will be shown to users looking for help. Try to use a
|
||||
consistent format - all default commands are using the structure shown above.
|
||||
|
||||
You can limit access to the help entry by the `view` and/or `read` locks on the
|
||||
Command. See [the section below](./Help-System.md#locking-help-entries) for details.
|
||||
|
||||
You should also supply the `help_category` class property if you can; this helps
|
||||
to group help entries together for people to more easily find them. See the
|
||||
`help` command in-game to see the default categories. If you don't specify the
|
||||
category, `settings.COMMAND_DEFAULT_HELP_CATEGORY` (default is "General") is
|
||||
used.
|
||||
|
||||
If you don't want your command to be picked up by the auto-help system at all
|
||||
(like if you want to write its docs manually using the info in the next section
|
||||
or you use a [cmdset](./Command-Sets.md) that has its own help functionality) you
|
||||
can explicitly set `auto_help` class property to `False` in your command
|
||||
definition.
|
||||
|
||||
Alternatively, you can keep the advantages of *auto-help* in commands, but
|
||||
control the display of command helps. You can do so by overriding the command's
|
||||
`get_help(caller, cmdset)` method. By default, this method will return the
|
||||
class docstring. You could modify it to add custom behavior: the text returned
|
||||
by this method will be displayed to the character asking for help in this
|
||||
command.
|
||||
|
||||
### Database-help entries
|
||||
|
||||
These are most commonly created in-game using the `sethelp` command. If you need to create one
|
||||
manually, you can do so with `evennia.create_help_entry()`:
|
||||
|
||||
```python
|
||||
|
||||
from evennia import create_help_entry
|
||||
entry = create_help_entry("emote",
|
||||
"Emoting is important because ...",
|
||||
category="Roleplaying", locks="view:all()")
|
||||
```
|
||||
|
||||
The entity being created is a [evennia.help.models.HelpEntry](evennia.help.models.HelpEntry)
|
||||
object. This is _not_ a [Typeclassed](./Typeclasses.md) entity and is not meant to
|
||||
be modified to any great degree. It holds the properties listed earlier. The
|
||||
text is stored in a field `entrytext`. It does not provide a `get_help` method
|
||||
like commands, stores and returns the `entrytext` directly.
|
||||
|
||||
You can search for (db-)-`HelpEntry` objects using `evennia.search_help` but note that
|
||||
this will not return the two other types of help entries.
|
||||
|
||||
### File-help entries
|
||||
|
||||
```{versionadded} 1.0
|
||||
```
|
||||
|
||||
File-help entries are created by the game development team outside of the game. The
|
||||
help entries are defined in normal Python modules (`.py` file ending) containing
|
||||
a `dict` to represent each entry. They require a server `reload` before any changes
|
||||
apply.
|
||||
|
||||
- Evennia will look through all modules given by
|
||||
`settings.FILE_HELP_ENTRY_MODULES`. This should be a list of python-paths for
|
||||
Evennia to import.
|
||||
- If this module contains a top-level variable `HELP_ENTRY_DICTS`, this will be
|
||||
imported and must be a `list` of help-entry dicts.
|
||||
- If no `HELP_ENTRY_DICTS` list is found, _every_ top-level variable in the
|
||||
module that is a `dict` will be read as a help entry. The variable-names will
|
||||
be ignored in this case.
|
||||
|
||||
If you add multiple modules to be read, same-keyed help entries added later in
|
||||
the list will override coming before.
|
||||
|
||||
Each entry dict must define keys to match that needed by all help entries.
|
||||
Here's an example of a help module:
|
||||
|
||||
```python
|
||||
|
||||
# in a module pointed to by settings.FILE_HELP_ENTRY_MODULES
|
||||
|
||||
HELP_ENTRY_DICTS = [
|
||||
{
|
||||
"key": "The Gods", # case-insensitive, can be searched by 'gods' too
|
||||
"aliases": ['pantheon', 'religion']
|
||||
"category": "Lore",
|
||||
"locks": "read:all()", # optional
|
||||
"text": '''
|
||||
The gods formed the world ...
|
||||
|
||||
# Subtopics
|
||||
|
||||
## Pantheon
|
||||
|
||||
The pantheon consists of 40 gods that ...
|
||||
|
||||
### God of love
|
||||
|
||||
The most prominent god is ...
|
||||
|
||||
### God of war
|
||||
|
||||
Also known as 'the angry god', this god is known to ...
|
||||
|
||||
'''
|
||||
},
|
||||
{
|
||||
"key": "The mortals",
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
The help entry text will be dedented and will retain paragraphs. You should try
|
||||
to keep your strings a reasonable width (it will look better). Just reload the
|
||||
server and the file-based help entries will be available to view.
|
||||
|
||||
## Entry priority
|
||||
|
||||
Should you have clashing help-entries between the three types of available
|
||||
entries, the priority is
|
||||
|
||||
Command-auto-help > Db-help > File-help
|
||||
|
||||
So if you create a db-help entry 'foo', it will replace any file-based help
|
||||
entry 'foo'. But if there is a Command 'foo', that's the help you'll get when
|
||||
you enter `help foo`.
|
||||
|
||||
The reasoning for this is that commands must always be understood in order to
|
||||
play the game. Meanwhile db-based help can be kept up-to-date from in-game
|
||||
builders and may be less 'static' than the file-based ones.
|
||||
|
||||
The `sethelp` command (which only deals with creating db-based help entries)
|
||||
will warn you if a new help entry might shadow/be shadowed by a
|
||||
same/similar-named command or file-based help entry.
|
||||
|
||||
## Locking help entries
|
||||
|
||||
The default `help` command gather all available commands and help entries
|
||||
together so they can be searched or listed. By setting locks on the command/help
|
||||
entry one can limit who can read help about it.
|
||||
|
||||
- Commands failing the normal `cmd`-lock will be removed before even getting
|
||||
to the help command. In this case the other two lock types below are ignored.
|
||||
- The `view` access type determines if the command/help entry should be visible in
|
||||
the main help index. If not given, it is assumed everyone can view.
|
||||
- The `read` access type determines if the command/help entry can be actually read.
|
||||
If a `read` lock is given and `view` is not, the `read`-lock is assumed to
|
||||
apply to `view`-access as well (so if you can't read the help entry it will
|
||||
also not show up in the index). If `read`-lock is not given, it's assume
|
||||
everyone can read the help entry.
|
||||
|
||||
For Commands you set the help-related locks the same way you would any lock:
|
||||
|
||||
```python
|
||||
class MyCommand(Command):
|
||||
"""
|
||||
<docstring for command>
|
||||
"""
|
||||
key = "mycommand"
|
||||
# everyone can use the command, builders can view it in the help index
|
||||
# but only devs can actually read the help (a weird setup for sure!)
|
||||
locks = "cmd:all();view:perm(Builders);read:perm(Developers)
|
||||
|
||||
```
|
||||
|
||||
Db-help entries and File-Help entries work the same way (except the `cmd`-type
|
||||
lock is not used. A file-help example:
|
||||
|
||||
```python
|
||||
help_entry = {
|
||||
# ...
|
||||
locks = "read:perm(Developer)",
|
||||
# ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```{versionchanged} 1.0
|
||||
Changed the old 'view' lock to control the help-index inclusion and added
|
||||
the new 'read' lock-type to control access to the entry itself.
|
||||
```
|
||||
|
||||
## Customizing the look of the help system
|
||||
|
||||
This is done almost exclusively by overriding the `help` command
|
||||
[evennia.commands.default.help.CmdHelp](evennia.commands.default.help.CmdHelp).
|
||||
|
||||
Since the available commands may vary from moment to moment, `help` is
|
||||
responsible for collating the three sources of help-entries (commands/db/file)
|
||||
together and search through them on the fly. It also does all the formatting of
|
||||
the output.
|
||||
|
||||
To make it easier to tweak the look, the parts of the code that changes the
|
||||
visual presentation and entity searching has been broken out into separate
|
||||
methods on the command class. Override these in your version of `help` to change
|
||||
the display or tweak as you please. See the api link above for details.
|
||||
|
||||
## Technical notes
|
||||
|
||||
Since it needs to search so different types of data, the help system has to
|
||||
collect all possibilities in memory before searching through the entire set. It
|
||||
uses the [Lunr](https://github.com/yeraydiazdiaz/lunr.py) search engine to
|
||||
search through the main bulk of help entries. Lunr is a mature engine used for
|
||||
web-pages and produces much more sensible results than previous solutions.
|
||||
|
||||
Once the main entry has been found, subtopics are then searched with
|
||||
simple `==`, `startswith` and `in` matching (there are so relatively few of them
|
||||
at that point).
|
||||
|
||||
```{versionchanged} 1.0
|
||||
Replaced the old bag-of-words algorithm with lunr package.
|
||||
|
||||
```
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
# Inputfuncs
|
||||
|
||||
|
||||
An *inputfunc* is an Evennia function that handles a particular input (an [inputcommand](../Concepts/OOB.md)) from
|
||||
the client. The inputfunc is the last destination for the inputcommand along the [ingoing message
|
||||
path](../Concepts/Messagepath.md#the-ingoing-message-path). The inputcommand always has the form `(commandname,
|
||||
(args), {kwargs})` and Evennia will use this to try to find and call an inputfunc on the form
|
||||
|
||||
```python
|
||||
def commandname(session, *args, **kwargs):
|
||||
# ...
|
||||
|
||||
```
|
||||
Or, if no match was found, it will call an inputfunc named "default" on this form
|
||||
|
||||
```python
|
||||
def default(session, cmdname, *args, **kwargs):
|
||||
# cmdname is the name of the mismatched inputcommand
|
||||
|
||||
```
|
||||
|
||||
## Adding your own inputfuncs
|
||||
|
||||
This is simple. Add a function on the above form to `mygame/server/conf/inputfuncs.py`. Your
|
||||
function must be in the global, outermost scope of that module and not start with an underscore
|
||||
(`_`) to be recognized as an inputfunc. Reload the server. That's it. To overload a default
|
||||
inputfunc (see below), just add a function with the same name.
|
||||
|
||||
The modules Evennia looks into for inputfuncs are defined in the list `settings.INPUT_FUNC_MODULES`.
|
||||
This list will be imported from left to right and later imported functions will replace earlier
|
||||
ones.
|
||||
|
||||
## Default inputfuncs
|
||||
|
||||
Evennia defines a few default inputfuncs to handle the common cases. These are defined in
|
||||
`evennia/server/inputfuncs.py`.
|
||||
|
||||
### text
|
||||
|
||||
- Input: `("text", (textstring,), {})`
|
||||
- Output: Depends on Command triggered
|
||||
|
||||
This is the most common of inputcommands, and the only one supported by every traditional mud. The
|
||||
argument is usually what the user sent from their command line. Since all text input from the user
|
||||
like this is considered a [Command](./Commands.md), this inputfunc will do things like nick-replacement
|
||||
and then pass on the input to the central Commandhandler.
|
||||
|
||||
### echo
|
||||
|
||||
- Input: `("echo", (args), {})`
|
||||
- Output: `("text", ("Echo returns: %s" % args), {})`
|
||||
|
||||
This is a test input, which just echoes the argument back to the session as text. Can be used for
|
||||
testing custom client input.
|
||||
|
||||
### default
|
||||
|
||||
The default function, as mentioned above, absorbs all non-recognized inputcommands. The default one
|
||||
will just log an error.
|
||||
|
||||
### client_options
|
||||
|
||||
- Input: `("client_options, (), {key:value, ...})`
|
||||
- Output:
|
||||
- normal: None
|
||||
- get: `("client_options", (), {key:value, ...})`
|
||||
|
||||
This is a direct command for setting protocol options. These are settable with the `@option`
|
||||
command, but this offers a client-side way to set them. Not all connection protocols makes use of
|
||||
all flags, but here are the possible keywords:
|
||||
|
||||
- get (bool): If this is true, ignore all other kwargs and immediately return the current settings
|
||||
as an outputcommand `("client_options", (), {key=value, ...})`-
|
||||
- client (str): A client identifier, like "mushclient".
|
||||
- version (str): A client version
|
||||
- ansi (bool): Supports ansi colors
|
||||
- xterm256 (bool): Supports xterm256 colors or not
|
||||
- mxp (bool): Supports MXP or not
|
||||
- utf-8 (bool): Supports UTF-8 or not
|
||||
- screenreader (bool): Screen-reader mode on/off
|
||||
- mccp (bool): MCCP compression on/off
|
||||
- screenheight (int): Screen height in lines
|
||||
- screenwidth (int): Screen width in characters
|
||||
- inputdebug (bool): Debug input functions
|
||||
- nomarkup (bool): Strip all text tags
|
||||
- raw (bool): Leave text tags unparsed
|
||||
|
||||
> Note that there are two GMCP aliases to this inputfunc - `hello` and `supports_set`, which means
|
||||
it will be accessed via the GMCP `Hello` and `Supports.Set` instructions assumed by some clients.
|
||||
|
||||
### get_client_options
|
||||
|
||||
- Input: `("get_client_options, (), {key:value, ...})`
|
||||
- Output: `("client_options, (), {key:value, ...})`
|
||||
|
||||
This is a convenience wrapper that retrieves the current options by sending "get" to
|
||||
`client_options` above.
|
||||
|
||||
### get_inputfuncs
|
||||
|
||||
- Input: `("get_inputfuncs", (), {})`
|
||||
- Output: `("get_inputfuncs", (), {funcname:docstring, ...})`
|
||||
|
||||
Returns an outputcommand on the form `("get_inputfuncs", (), {funcname:docstring, ...})` - a list of
|
||||
all the available inputfunctions along with their docstrings.
|
||||
|
||||
### login
|
||||
|
||||
> Note: this is currently experimental and not very well tested.
|
||||
|
||||
- Input: `("login", (username, password), {})`
|
||||
- Output: Depends on login hooks
|
||||
|
||||
This performs the inputfunc version of a login operation on the current Session.
|
||||
|
||||
### get_value
|
||||
|
||||
Input: `("get_value", (name, ), {})`
|
||||
Output: `("get_value", (value, ), {})`
|
||||
|
||||
Retrieves a value from the Character or Account currently controlled by this Session. Takes one
|
||||
argument, This will only accept particular white-listed names, you'll need to overload the function
|
||||
to expand. By default the following values can be retrieved:
|
||||
|
||||
- "name" or "key": The key of the Account or puppeted Character.
|
||||
- "location": Name of the current location, or "None".
|
||||
- "servername": Name of the Evennia server connected to.
|
||||
|
||||
### repeat
|
||||
|
||||
- Input: `("repeat", (), {"callback":funcname,
|
||||
"interval": secs, "stop": False})`
|
||||
- Output: Depends on the repeated function. Will return `("text", (repeatlist),{}` with a list of
|
||||
accepted names if given an unfamiliar callback name.
|
||||
|
||||
This will tell evennia to repeatedly call a named function at a given interval. Behind the scenes
|
||||
this will set up a [Ticker](./TickerHandler.md). Only previously acceptable functions are possible to
|
||||
repeat-call in this way, you'll need to overload this inputfunc to add the ones you want to offer.
|
||||
By default only two example functions are allowed, "test1" and "test2", which will just echo a text
|
||||
back at the given interval. Stop the repeat by sending `"stop": True` (note that you must include
|
||||
both the callback name and interval for Evennia to know what to stop).
|
||||
|
||||
### unrepeat
|
||||
|
||||
- Input: `("unrepeat", (), ("callback":funcname,
|
||||
"interval": secs)`
|
||||
- Output: None
|
||||
|
||||
This is a convenience wrapper for sending "stop" to the `repeat` inputfunc.
|
||||
|
||||
### monitor
|
||||
|
||||
- Input: `("monitor", (), ("name":field_or_argname, stop=False)`
|
||||
- Output (on change): `("monitor", (), {"name":name, "value":value})`
|
||||
|
||||
This sets up on-object monitoring of Attributes or database fields. Whenever the field or Attribute
|
||||
changes in any way, the outputcommand will be sent. This is using the
|
||||
[MonitorHandler](./MonitorHandler.md) behind the scenes. Pass the "stop" key to stop monitoring. Note
|
||||
that you must supply the name also when stopping to let the system know which monitor should be
|
||||
cancelled.
|
||||
|
||||
Only fields/attributes in a whitelist are allowed to be used, you have to overload this function to
|
||||
add more. By default the following fields/attributes can be monitored:
|
||||
|
||||
- "name": The current character name
|
||||
- "location": The current location
|
||||
- "desc": The description Argument
|
||||
|
||||
## unmonitor
|
||||
|
||||
- Input: `("unmonitor", (), {"name":name})`
|
||||
- Output: None
|
||||
|
||||
A convenience wrapper that sends "stop" to the `monitor` function.
|
||||
|
|
@ -1,387 +0,0 @@
|
|||
# Locks
|
||||
|
||||
|
||||
For most games it is a good idea to restrict what people can do. In Evennia such restrictions are
|
||||
applied and checked by something called *locks*. All Evennia entities ([Commands](./Commands.md),
|
||||
[Objects](./Objects.md), [Scripts](./Scripts.md), [Accounts](./Accounts.md), [Help System](./Help-System.md),
|
||||
[messages](./Msg.md) and [channels](./Channels.md)) are accessed through locks.
|
||||
|
||||
A lock can be thought of as an "access rule" restricting a particular use of an Evennia entity.
|
||||
Whenever another entity wants that kind of access the lock will analyze that entity in different
|
||||
ways to determine if access should be granted or not. Evennia implements a "lockdown" philosophy -
|
||||
all entities are inaccessible unless you explicitly define a lock that allows some or full access.
|
||||
|
||||
Let's take an example: An object has a lock on itself that restricts how people may "delete" that
|
||||
object. Apart from knowing that it restricts deletion, the lock also knows that only players with
|
||||
the specific ID of, say, `34` are allowed to delete it. So whenever a player tries to run `delete`
|
||||
on the object, the `delete` command makes sure to check if this player is really allowed to do so.
|
||||
It calls the lock, which in turn checks if the player's id is `34`. Only then will it allow `delete`
|
||||
to go on with its job.
|
||||
|
||||
## Setting and checking a lock
|
||||
|
||||
The in-game command for setting locks on objects is `lock`:
|
||||
|
||||
> lock obj = <lockstring>
|
||||
|
||||
The `<lockstring>` is a string of a certain form that defines the behaviour of the lock. We will go
|
||||
into more detail on how `<lockstring>` should look in the next section.
|
||||
|
||||
Code-wise, Evennia handles locks through what is usually called `locks` on all relevant entities.
|
||||
This is a handler that allows you to add, delete and check locks.
|
||||
|
||||
```python
|
||||
myobj.locks.add(<lockstring>)
|
||||
```
|
||||
|
||||
One can call `locks.check()` to perform a lock check, but to hide the underlying implementation all
|
||||
objects also have a convenience function called `access`. This should preferably be used. In the
|
||||
example below, `accessing_obj` is the object requesting the 'delete' access whereas `obj` is the
|
||||
object that might get deleted. This is how it would look (and does look) from inside the `delete`
|
||||
command:
|
||||
|
||||
```python
|
||||
if not obj.access(accessing_obj, 'delete'):
|
||||
accessing_obj.msg("Sorry, you may not delete that.")
|
||||
return
|
||||
```
|
||||
|
||||
## Defining locks
|
||||
|
||||
Defining a lock (i.e. an access restriction) in Evennia is done by adding simple strings of lock
|
||||
definitions to the object's `locks` property using `obj.locks.add()`.
|
||||
|
||||
Here are some examples of lock strings (not including the quotes):
|
||||
|
||||
```python
|
||||
delete:id(34) # only allow obj #34 to delete
|
||||
edit:all() # let everyone edit
|
||||
# only those who are not "very_weak" or are Admins may pick this up
|
||||
get: not attr(very_weak) or perm(Admin)
|
||||
```
|
||||
|
||||
Formally, a lockstring has the following syntax:
|
||||
|
||||
```python
|
||||
access_type: [NOT] lockfunc1([arg1,..]) [AND|OR] [NOT] lockfunc2([arg1,...]) [...]
|
||||
```
|
||||
|
||||
where `[]` marks optional parts. `AND`, `OR` and `NOT` are not case sensitive and excess spaces are
|
||||
ignored. `lockfunc1, lockfunc2` etc are special _lock functions_ available to the lock system.
|
||||
|
||||
So, a lockstring consists of the type of restriction (the `access_type`), a colon (`:`) and then an
|
||||
expression involving function calls that determine what is needed to pass the lock. Each function
|
||||
returns either `True` or `False`. `AND`, `OR` and `NOT` work as they do normally in Python. If the
|
||||
total result is `True`, the lock is passed.
|
||||
|
||||
You can create several lock types one after the other by separating them with a semicolon (`;`) in
|
||||
the lockstring. The string below yields the same result as the previous example:
|
||||
|
||||
delete:id(34);edit:all();get: not attr(very_weak) or perm(Admin)
|
||||
|
||||
|
||||
### Valid access_types
|
||||
|
||||
An `access_type`, the first part of a lockstring, defines what kind of capability a lock controls,
|
||||
such as "delete" or "edit". You may in principle name your `access_type` anything as long as it is
|
||||
unique for the particular object. The name of the access types is not case-sensitive.
|
||||
|
||||
If you want to make sure the lock is used however, you should pick `access_type` names that you (or
|
||||
the default command set) actually checks for, as in the example of `delete` above that uses the
|
||||
'delete' `access_type`.
|
||||
|
||||
Below are the access_types checked by the default commandset.
|
||||
|
||||
- [Commands](./Commands.md)
|
||||
- `cmd` - this defines who may call this command at all.
|
||||
- [Objects](./Objects.md):
|
||||
- `control` - who is the "owner" of the object. Can set locks, delete it etc. Defaults to the
|
||||
creator of the object.
|
||||
- `call` - who may call Object-commands stored on this Object except for the Object itself. By
|
||||
default, Objects share their Commands with anyone in the same location (e.g. so you can 'press' a
|
||||
`Button` object in the room). For Characters and Mobs (who likely only use those Commands for
|
||||
themselves and don't want to share them) this should usually be turned off completely, using
|
||||
something like `call:false()`.
|
||||
- `examine` - who may examine this object's properties.
|
||||
- `delete` - who may delete the object.
|
||||
- `edit` - who may edit properties and attributes of the object.
|
||||
- `view` - if the `look` command will display/list this object in descriptions
|
||||
and if you will be able to see its description. Note that if
|
||||
you target it specifically by name, the system will still find it, just
|
||||
not be able to look at it. See `search` lock to completely hide the item.
|
||||
- `search` - this controls if the object can be found with the
|
||||
`DefaultObject.search` method (usually referred to with `caller.search`
|
||||
in Commands). This is how to create entirely 'undetectable' in-game objects.
|
||||
If not setting this lock excplicitly, all objects are assumed searchable.
|
||||
Note that if you are aiming to make some _permanently invisible game system,
|
||||
using a [Script](./Scripts.md) is a better bet.
|
||||
- `get`- who may pick up the object and carry it around.
|
||||
- `puppet` - who may "become" this object and control it as their "character".
|
||||
- `attrcreate` - who may create new attributes on the object (default True)
|
||||
- [Characters](./Objects.md#characters):
|
||||
- Same as for Objects
|
||||
- [Exits](./Objects.md#exits):
|
||||
- Same as for Objects
|
||||
- `traverse` - who may pass the exit.
|
||||
- [Accounts](./Accounts.md):
|
||||
- `examine` - who may examine the account's properties.
|
||||
- `delete` - who may delete the account.
|
||||
- `edit` - who may edit the account's attributes and properties.
|
||||
- `msg` - who may send messages to the account.
|
||||
- `boot` - who may boot the account.
|
||||
- [Attributes](./Attributes.md): (only checked by `obj.secure_attr`)
|
||||
- `attrread` - see/access attribute
|
||||
- `attredit` - change/delete attribute
|
||||
- [Channels](./Channels.md):
|
||||
- `control` - who is administrating the channel. This means the ability to delete the channel,
|
||||
boot listeners etc.
|
||||
- `send` - who may send to the channel.
|
||||
- `listen` - who may subscribe and listen to the channel.
|
||||
- [HelpEntry](./Help-System.md):
|
||||
- `examine` - who may view this help entry (usually everyone)
|
||||
- `edit` - who may edit this help entry.
|
||||
|
||||
So to take an example, whenever an exit is to be traversed, a lock of the type *traverse* will be
|
||||
checked. Defining a suitable lock type for an exit object would thus involve a lockstring `traverse:
|
||||
<lock functions>`.
|
||||
|
||||
### Custom access_types
|
||||
|
||||
As stated above, the `access_type` part of the lock is simply the 'name' or 'type' of the lock. The
|
||||
text is an arbitrary string that must be unique for an object. If adding a lock with the same
|
||||
`access_type` as one that already exists on the object, the new one override the old one.
|
||||
|
||||
For example, if you wanted to create a bulletin board system and wanted to restrict who can either
|
||||
read a board or post to a board. You could then define locks such as:
|
||||
|
||||
```python
|
||||
obj.locks.add("read:perm(Player);post:perm(Admin)")
|
||||
```
|
||||
|
||||
This will create a 'read' access type for Characters having the `Player` permission or above and a
|
||||
'post' access type for those with `Admin` permissions or above (see below how the `perm()` lock
|
||||
function works). When it comes time to test these permissions, simply check like this (in this
|
||||
example, the `obj` may be a board on the bulletin board system and `accessing_obj` is the player
|
||||
trying to read the board):
|
||||
|
||||
```python
|
||||
if not obj.access(accessing_obj, 'read'):
|
||||
accessing_obj.msg("Sorry, you may not read that.")
|
||||
return
|
||||
```
|
||||
|
||||
### Lock functions
|
||||
|
||||
A lock function is a normal Python function put in a place Evennia looks for such functions. The
|
||||
modules Evennia looks at is the list `settings.LOCK_FUNC_MODULES`. *All functions* in any of those
|
||||
modules will automatically be considered a valid lock function. The default ones are found in
|
||||
`evennia/locks/lockfuncs.py` and you can start adding your own in `mygame/server/conf/lockfuncs.py`.
|
||||
You can append the setting to add more module paths. To replace a default lock function, just add
|
||||
your own with the same name.
|
||||
|
||||
A lock function must always accept at least two arguments - the *accessing object* (this is the
|
||||
object wanting to get access) and the *accessed object* (this is the object with the lock). Those
|
||||
two are fed automatically as the first two arguments to the function when the lock is checked. Any
|
||||
arguments explicitly given in the lock definition will appear as extra arguments.
|
||||
|
||||
```python
|
||||
# A simple example lock function. Called with e.g. `id(34)`. This is
|
||||
# defined in, say mygame/server/conf/lockfuncs.py
|
||||
|
||||
def id(accessing_obj, accessed_obj, *args, **kwargs):
|
||||
if args:
|
||||
wanted_id = args[0]
|
||||
return accessing_obj.id == wanted_id
|
||||
return False
|
||||
```
|
||||
|
||||
The above could for example be used in a lock function like this:
|
||||
|
||||
```python
|
||||
# we have `obj` and `owner_object` from before
|
||||
obj.locks.add(f"edit: id({owner_object.id})")
|
||||
```
|
||||
|
||||
We could check if the "edit" lock is passed with something like this:
|
||||
|
||||
```python
|
||||
# as part of a Command's func() method, for example
|
||||
if not obj.access(caller, "edit"):
|
||||
caller.msg("You don't have access to edit this!")
|
||||
return
|
||||
```
|
||||
|
||||
In this example, everyone except the `caller` with the right `id` will get the error.
|
||||
|
||||
> (Using the `*` and `**` syntax causes Python to magically put all extra arguments into a list
|
||||
`args` and all keyword arguments into a dictionary `kwargs` respectively. If you are unfamiliar with
|
||||
how `*args` and `**kwargs` work, see the Python manuals).
|
||||
|
||||
Some useful default lockfuncs (see `src/locks/lockfuncs.py` for more):
|
||||
|
||||
- `true()/all()` - give access to everyone
|
||||
- `false()/none()/superuser()` - give access to none. Superusers bypass the check entirely and are
|
||||
thus the only ones who will pass this check.
|
||||
- `perm(perm)` - this tries to match a given `permission` property, on an Account firsthand, on a
|
||||
Character second. See [below](./Permissions.md).
|
||||
- `perm_above(perm)` - like `perm` but requires a "higher" permission level than the one given.
|
||||
- `id(num)/dbref(num)` - checks so the access_object has a certain dbref/id.
|
||||
- `attr(attrname)` - checks if a certain [Attribute](./Attributes.md) exists on accessing_object.
|
||||
- `attr(attrname, value)` - checks so an attribute exists on accessing_object *and* has the given
|
||||
value.
|
||||
- `attr_gt(attrname, value)` - checks so accessing_object has a value larger (`>`) than the given
|
||||
value.
|
||||
- `attr_ge, attr_lt, attr_le, attr_ne` - corresponding for `>=`, `<`, `<=` and `!=`.
|
||||
- `holds(objid)` - checks so the accessing objects contains an object of given name or dbref.
|
||||
- `inside()` - checks so the accessing object is inside the accessed object (the inverse of
|
||||
`holds()`).
|
||||
- `pperm(perm)`, `pid(num)/pdbref(num)` - same as `perm`, `id/dbref` but always looks for
|
||||
permissions and dbrefs of *Accounts*, not on Characters.
|
||||
- `serversetting(settingname, value)` - Only returns True if Evennia has a given setting or a
|
||||
setting set to a given value.
|
||||
|
||||
## Checking simple strings
|
||||
|
||||
Sometimes you don't really need to look up a certain lock, you just want to check a lockstring. A
|
||||
common use is inside Commands, in order to check if a user has a certain permission. The lockhandler
|
||||
has a method `check_lockstring(accessing_obj, lockstring, bypass_superuser=False)` that allows this.
|
||||
|
||||
```python
|
||||
# inside command definition
|
||||
if not self.caller.locks.check_lockstring(self.caller, "dummy:perm(Admin)"):
|
||||
self.caller.msg("You must be an Admin or higher to do this!")
|
||||
return
|
||||
```
|
||||
|
||||
Note here that the `access_type` can be left to a dummy value since this method does not actually do
|
||||
a Lock lookup.
|
||||
|
||||
## Default locks
|
||||
|
||||
Evennia sets up a few basic locks on all new objects and accounts (if we didn't, noone would have
|
||||
any access to anything from the start). This is all defined in the root [Typeclasses](./Typeclasses.md)
|
||||
of the respective entity, in the hook method `basetype_setup()` (which you usually don't want to
|
||||
edit unless you want to change how basic stuff like rooms and exits store their internal variables).
|
||||
This is called once, before `at_object_creation`, so just put them in the latter method on your
|
||||
child object to change the default. Also creation commands like `create` changes the locks of
|
||||
objects you create - for example it sets the `control` lock_type so as to allow you, its creator, to
|
||||
control and delete the object.
|
||||
|
||||
|
||||
## More Lock definition examples
|
||||
|
||||
examine: attr(eyesight, excellent) or perm(Builders)
|
||||
|
||||
You are only allowed to do *examine* on this object if you have 'excellent' eyesight (that is, has
|
||||
an Attribute `eyesight` with the value `excellent` defined on yourself) or if you have the
|
||||
"Builders" permission string assigned to you.
|
||||
|
||||
open: holds('the green key') or perm(Builder)
|
||||
|
||||
This could be called by the `open` command on a "door" object. The check is passed if you are a
|
||||
Builder or has the right key in your inventory.
|
||||
|
||||
cmd: perm(Builders)
|
||||
|
||||
Evennia's command handler looks for a lock of type `cmd` to determine if a user is allowed to even
|
||||
call upon a particular command or not. When you define a command, this is the kind of lock you must
|
||||
set. See the default command set for lots of examples. If a character/account don't pass the `cmd`
|
||||
lock type the command will not even appear in their `help` list.
|
||||
|
||||
cmd: not perm(no_tell)
|
||||
|
||||
"Permissions" can also be used to block users or implement highly specific bans. The above example
|
||||
would be be added as a lock string to the `tell` command. This will allow everyone *not* having the
|
||||
"permission" `no_tell` to use the `tell` command. You could easily give an account the "permission"
|
||||
`no_tell` to disable their use of this particular command henceforth.
|
||||
|
||||
|
||||
```python
|
||||
dbref = caller.id
|
||||
lockstring = "control:id(%s);examine:perm(Builders);delete:id(%s) or perm(Admin);get:all()" %
|
||||
(dbref, dbref)
|
||||
new_obj.locks.add(lockstring)
|
||||
```
|
||||
|
||||
This is how the `create` command sets up new objects. In sequence, this permission string sets the
|
||||
owner of this object be the creator (the one running `create`). Builders may examine the object
|
||||
whereas only Admins and the creator may delete it. Everyone can pick it up.
|
||||
|
||||
## A complete example of setting locks on an object
|
||||
|
||||
Assume we have two objects - one is ourselves (not superuser) and the other is an [Object](./Objects.md)
|
||||
called `box`.
|
||||
|
||||
> create/drop box
|
||||
> desc box = "This is a very big and heavy box."
|
||||
|
||||
We want to limit which objects can pick up this heavy box. Let's say that to do that we require the
|
||||
would-be lifter to to have an attribute *strength* on themselves, with a value greater than 50. We
|
||||
assign it to ourselves to begin with.
|
||||
|
||||
> set self/strength = 45
|
||||
|
||||
Ok, so for testing we made ourselves strong, but not strong enough. Now we need to look at what
|
||||
happens when someone tries to pick up the the box - they use the `get` command (in the default set).
|
||||
This is defined in `evennia/commands/default/general.py`. In its code we find this snippet:
|
||||
|
||||
```python
|
||||
if not obj.access(caller, 'get'):
|
||||
if obj.db.get_err_msg:
|
||||
caller.msg(obj.db.get_err_msg)
|
||||
else:
|
||||
caller.msg("You can't get that.")
|
||||
return
|
||||
```
|
||||
|
||||
So the `get` command looks for a lock with the type *get* (not so surprising). It also looks for an
|
||||
[Attribute](./Attributes.md) on the checked object called _get_err_msg_ in order to return a customized
|
||||
error message. Sounds good! Let's start by setting that on the box:
|
||||
|
||||
> set box/get_err_msg = You are not strong enough to lift this box.
|
||||
|
||||
Next we need to craft a Lock of type *get* on our box. We want it to only be passed if the accessing
|
||||
object has the attribute *strength* of the right value. For this we would need to create a lock
|
||||
function that checks if attributes have a value greater than a given value. Luckily there is already
|
||||
such a one included in evennia (see `evennia/locks/lockfuncs.py`), called `attr_gt`.
|
||||
|
||||
So the lock string will look like this: `get:attr_gt(strength, 50)`. We put this on the box now:
|
||||
|
||||
lock box = get:attr_gt(strength, 50)
|
||||
|
||||
Try to `get` the object and you should get the message that we are not strong enough. Increase your
|
||||
strength above 50 however and you'll pick it up no problem. Done! A very heavy box!
|
||||
|
||||
If you wanted to set this up in python code, it would look something like this:
|
||||
|
||||
```python
|
||||
|
||||
from evennia import create_object
|
||||
|
||||
# create, then set the lock
|
||||
box = create_object(None, key="box")
|
||||
box.locks.add("get:attr_gt(strength, 50)")
|
||||
|
||||
# or we can assign locks in one go right away
|
||||
box = create_object(None, key="box", locks="get:attr_gt(strength, 50)")
|
||||
|
||||
# set the attributes
|
||||
box.db.desc = "This is a very big and heavy box."
|
||||
box.db.get_err_msg = "You are not strong enough to lift this box."
|
||||
|
||||
# one heavy box, ready to withstand all but the strongest...
|
||||
```
|
||||
|
||||
## On Django's permission system
|
||||
|
||||
Django also implements a comprehensive permission/security system of its own. The reason we don't
|
||||
use that is because it is app-centric (app in the Django sense). Its permission strings are of the
|
||||
form `appname.permstring` and it automatically adds three of them for each database model in the app
|
||||
- for the app evennia/object this would be for example 'object.create', 'object.admin' and
|
||||
'object.edit'. This makes a lot of sense for a web application, not so much for a MUD, especially
|
||||
when we try to hide away as much of the underlying architecture as possible.
|
||||
|
||||
The django permissions are not completely gone however. We use it for validating passwords during
|
||||
login. It is also used exclusively for managing Evennia's web-based admin site, which is a graphical
|
||||
front-end for the database of Evennia. You edit and assign such permissions directly from the web
|
||||
interface. It's stand-alone from the permissions described above.
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# MonitorHandler
|
||||
|
||||
|
||||
The *MonitorHandler* is a system for watching changes in properties or Attributes on objects. A
|
||||
monitor can be thought of as a sort of trigger that responds to change.
|
||||
|
||||
The main use for the MonitorHandler is to report changes to the client; for example the client
|
||||
Session may ask Evennia to monitor the value of the Characer's `health` attribute and report
|
||||
whenever it changes. This way the client could for example update its health bar graphic as needed.
|
||||
|
||||
## Using the MonitorHandler
|
||||
|
||||
The MontorHandler is accessed from the singleton `evennia.MONITOR_HANDLER`. The code for the handler
|
||||
is in `evennia.scripts.monitorhandler`.
|
||||
|
||||
Here's how to add a new monitor:
|
||||
|
||||
```python
|
||||
from evennia import MONITOR_HANDLER
|
||||
|
||||
MONITOR_HANDLER.add(obj, fieldname, callback,
|
||||
idstring="", persistent=False, **kwargs)
|
||||
|
||||
```
|
||||
|
||||
- `obj` ([Typeclassed](./Typeclasses.md) entity) - the object to monitor. Since this must be
|
||||
typeclassed, it means you can't monitor changes on [Sessions](./Sessions.md) with the monitorhandler, for
|
||||
example.
|
||||
- `fieldname` (str) - the name of a field or [Attribute](./Attributes.md) on `obj`. If you want to
|
||||
monitor a database field you must specify its full name, including the starting `db_` (like
|
||||
`db_key`, `db_location` etc). Any names not starting with `db_` are instead assumed to be the names
|
||||
of Attributes. This difference matters, since the MonitorHandler will automatically know to watch
|
||||
the `db_value` field of the Attribute.
|
||||
- `callback`(callable) - This will be called as `callback(fieldname=fieldname, obj=obj, **kwargs)`
|
||||
when the field updates.
|
||||
- `idstring` (str) - this is used to separate multiple monitors on the same object and fieldname.
|
||||
This is required in order to properly identify and remove the monitor later. It's also used for
|
||||
saving it.
|
||||
- `persistent` (bool) - if True, the monitor will survive a server reboot.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from evennia import MONITOR_HANDLER as monitorhandler
|
||||
|
||||
def _monitor_callback(fieldname="", obj=None, **kwargs):
|
||||
# reporting callback that works both
|
||||
# for db-fields and Attributes
|
||||
if fieldname.startswith("db_"):
|
||||
new_value = getattr(obj, fieldname)
|
||||
else: # an attribute
|
||||
new_value = obj.attributes.get(fieldname)
|
||||
obj.msg(f"{obj.key}.{fieldname} changed to '{new_value}'.")
|
||||
|
||||
# (we could add _some_other_monitor_callback here too)
|
||||
|
||||
# monitor Attribute (assume we have obj from before)
|
||||
monitorhandler.add(obj, "desc", _monitor_callback)
|
||||
|
||||
# monitor same db-field with two different callbacks (must separate by id_string)
|
||||
monitorhandler.add(obj, "db_key", _monitor_callback, id_string="foo")
|
||||
monitorhandler.add(obj, "db_key", _some_other_monitor_callback, id_string="bar")
|
||||
|
||||
```
|
||||
|
||||
A monitor is uniquely identified by the combination of the *object instance* it is monitoring, the
|
||||
*name* of the field/attribute to monitor on that object and its `idstring` (`obj` + `fieldname` +
|
||||
`idstring`). The `idstring` will be the empty string unless given explicitly.
|
||||
|
||||
So to "un-monitor" the above you need to supply enough information for the system to uniquely find
|
||||
the monitor to remove:
|
||||
|
||||
```
|
||||
monitorhandler.remove(obj, "desc")
|
||||
monitorhandler.remove(obj, "db_key", idstring="foo")
|
||||
monitorhandler.remove(obj, "db_key", idstring="bar")
|
||||
```
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
# Msg
|
||||
|
||||
The [Msg](evennia.comms.models.Msg) object represents a database-saved
|
||||
piece of communication. Think of it as a discrete piece of email - it contains
|
||||
a message, some metadata and will always have a sender and one or more
|
||||
recipients.
|
||||
|
||||
Once created, a Msg is normally not changed. It is persitently saved in the
|
||||
database. This allows for comprehensive logging of communications. Here are some
|
||||
good uses for `Msg` objects:
|
||||
|
||||
- page/tells (the `page` command is how Evennia uses them out of the box)
|
||||
- messages in a bulletin board
|
||||
- game-wide email stored in 'mailboxes'.
|
||||
|
||||
|
||||
```{important}
|
||||
|
||||
A `Msg` does not have any in-game representation. So if you want to use them
|
||||
to represent in-game mail/letters, the physical letters would never be
|
||||
visible in a room (possible to steal, spy on etc) unless you make your
|
||||
spy-system access the Msgs directly (or go to the trouble of spawning an
|
||||
actual in-game letter-object based on the Msg)
|
||||
|
||||
|
||||
```
|
||||
|
||||
```{versionchanged} 1.0
|
||||
Channels dropped Msg-support. Now only used in `page` command by default.
|
||||
```
|
||||
|
||||
## Msg in code
|
||||
|
||||
The Msg is intended to be used exclusively in code, to build other game systems. It is _not_
|
||||
a [Typeclassed](./Typeclasses.md) entity, which means it cannot (easily) be overridden. It
|
||||
doesn't support Attributes (but it _does_ support [Tags](./Tags.md)). It tries to be lean
|
||||
and small since a new one is created for every message.
|
||||
|
||||
You create a new message with `evennia.create_message`:
|
||||
|
||||
```python
|
||||
from evennia import create_message
|
||||
message = create_message(senders, message, receivers,
|
||||
locks=..., tags=..., header=...)
|
||||
```
|
||||
|
||||
You can search for `Msg` objects in various ways:
|
||||
|
||||
|
||||
```python
|
||||
from evennia import search_message, Msg
|
||||
|
||||
# args are optional. Only a single sender/receiver should be passed
|
||||
messages = search_message(sender=..., receiver=..., freetext=..., dbref=...)
|
||||
|
||||
# get all messages for a given sender/receiver
|
||||
messages = Msg.objects.get_msg_by_sender(sender)
|
||||
messages = Msg.objects.get_msg_by_receiver(recipient)
|
||||
|
||||
```
|
||||
|
||||
### Properties on Msg
|
||||
|
||||
- `senders` - there must always be at least one sender. This is a set of
|
||||
- [Account](./Accounts.md), [Object](./Objects.md), [Script](./Scripts.md)
|
||||
or `str` in any combination (but usually a message only targets one type).
|
||||
Using a `str` for a sender indicates it's an 'external' sender and
|
||||
and can be used to point to a sender that is not a typeclassed entity. This is not used by default
|
||||
and what this would be depends on the system (it could be a unique id or a
|
||||
python-path, for example). While most systems expect a single sender, it's
|
||||
possible to have any number of them.
|
||||
- `receivers` - these are the ones to see the Msg. These are again any combination of
|
||||
[Account](./Accounts.md), [Object](./Objects.md) or [Script](./Scripts.md) or `str` (an 'external' receiver).
|
||||
It's in principle possible to have zero receivers but most usages of Msg expects one or more.
|
||||
- `header` - this is an optional text field that can contain meta-information about the message. For
|
||||
an email-like system it would be the subject line. This can be independently searched, making
|
||||
this a powerful place for quickly finding messages.
|
||||
- `message` - the actual text being sent.
|
||||
- `date_sent` - this is auto-set to the time the Msg was created (and thus presumably sent).
|
||||
- `locks` - the Evennia [lock handler](./Locks.md). Use with `locks.add()` etc and check locks with `msg.access()`
|
||||
like for all other lockable entities. This can be used to limit access to the contents
|
||||
of the Msg. The default lock-type to check is `'read'`.
|
||||
- `hide_from` - this is an optional list of [Accounts](./Accounts.md) or [Objects](./Objects.md) that
|
||||
will not see this Msg. This relationship is available mainly for optimization
|
||||
reasons since it allows quick filtering of messages not intended for a given
|
||||
target.
|
||||
|
||||
|
||||
## TempMsg
|
||||
|
||||
[evennia.comms.models.TempMsg](evennia.comms.models.TempMsg) is an object
|
||||
that implements the same API as the regular `Msg`, but which has no database
|
||||
component (and thus cannot be searched). It's meant to plugged into systems
|
||||
expecting a `Msg` but where you just want to process the message without saving
|
||||
it.
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
# Nicks
|
||||
|
||||
|
||||
*Nicks*, short for *Nicknames* is a system allowing an object (usually a [Account](./Accounts.md)) to
|
||||
assign custom replacement names for other game entities.
|
||||
|
||||
Nicks are not to be confused with *Aliases*. Setting an Alias on a game entity actually changes an
|
||||
inherent attribute on that entity, and everyone in the game will be able to use that alias to
|
||||
address the entity thereafter. A *Nick* on the other hand, is used to map a different way *you
|
||||
alone* can refer to that entity. Nicks are also commonly used to replace your input text which means
|
||||
you can create your own aliases to default commands.
|
||||
|
||||
Default Evennia use Nicks in three flavours that determine when Evennia actually tries to do the
|
||||
substitution.
|
||||
|
||||
- inputline - replacement is attempted whenever you write anything on the command line. This is the
|
||||
default.
|
||||
- objects - replacement is only attempted when referring to an object
|
||||
- accounts - replacement is only attempted when referring an account
|
||||
|
||||
Here's how to use it in the default command set (using the `nick` command):
|
||||
|
||||
nick ls = look
|
||||
|
||||
This is a good one for unix/linux users who are accustomed to using the `ls` command in their daily
|
||||
life. It is equivalent to `nick/inputline ls = look`.
|
||||
|
||||
nick/object mycar2 = The red sports car
|
||||
|
||||
With this example, substitutions will only be done specifically for commands expecting an object
|
||||
reference, such as
|
||||
|
||||
look mycar2
|
||||
|
||||
becomes equivalent to "`look The red sports car`".
|
||||
|
||||
nick/accounts tom = Thomas Johnsson
|
||||
|
||||
This is useful for commands searching for accounts explicitly:
|
||||
|
||||
@find *tom
|
||||
|
||||
One can use nicks to speed up input. Below we add ourselves a quicker way to build red buttons. In
|
||||
the future just writing *rb* will be enough to execute that whole long string.
|
||||
|
||||
nick rb = @create button:examples.red_button.RedButton
|
||||
|
||||
Nicks could also be used as the start for building a "recog" system suitable for an RP mud.
|
||||
|
||||
nick/account Arnold = The mysterious hooded man
|
||||
|
||||
The nick replacer also supports unix-style *templating*:
|
||||
|
||||
nick build $1 $2 = @create/drop $1;$2
|
||||
|
||||
This will catch space separated arguments and store them in the the tags `$1` and `$2`, to be
|
||||
inserted in the replacement string. This example allows you to do `build box crate` and have Evennia
|
||||
see `@create/drop box;crate`. You may use any `$` numbers between 1 and 99, but the markers must
|
||||
match between the nick pattern and the replacement.
|
||||
|
||||
> If you want to catch "the rest" of a command argument, make sure to put a `$` tag *with no spaces
|
||||
to the right of it* - it will then receive everything up until the end of the line.
|
||||
|
||||
You can also use [shell-type wildcards](http://www.linfo.org/wildcard.html):
|
||||
|
||||
- \* - matches everything.
|
||||
- ? - matches a single character.
|
||||
- [seq] - matches everything in the sequence, e.g. [xyz] will match both x, y and z
|
||||
- [!seq] - matches everything *not* in the sequence. e.g. [!xyz] will match all but x,y z.
|
||||
|
||||
## Coding with nicks
|
||||
|
||||
Nicks are stored as the `Nick` database model and are referred from the normal Evennia
|
||||
[object](./Objects.md) through the `nicks` property - this is known as the *NickHandler*. The NickHandler
|
||||
offers effective error checking, searches and conversion.
|
||||
|
||||
```python
|
||||
# A command/channel nick:
|
||||
obj.nicks.add("greetjack", "tell Jack = Hello pal!")
|
||||
|
||||
# An object nick:
|
||||
obj.nicks.add("rose", "The red flower", nick_type="object")
|
||||
|
||||
# An account nick:
|
||||
obj.nicks.add("tom", "Tommy Hill", nick_type="account")
|
||||
|
||||
# My own custom nick type (handled by my own game code somehow):
|
||||
obj.nicks.add("hood", "The hooded man", nick_type="my_identsystem")
|
||||
|
||||
# get back the translated nick:
|
||||
full_name = obj.nicks.get("rose", nick_type="object")
|
||||
|
||||
# delete a previous set nick
|
||||
object.nicks.remove("rose", nick_type="object")
|
||||
```
|
||||
|
||||
In a command definition you can reach the nick handler through `self.caller.nicks`. See the `nick`
|
||||
command in `evennia/commands/default/general.py` for more examples.
|
||||
|
||||
As a last note, The Evennia [channel](./Channels.md) alias systems are using nicks with the
|
||||
`nick_type="channel"` in order to allow users to create their own custom aliases to channels.
|
||||
|
||||
## Advanced note
|
||||
|
||||
Internally, nicks are [Attributes](./Attributes.md) saved with the `db_attrype` set to "nick" (normal
|
||||
Attributes has this set to `None`).
|
||||
|
||||
The nick stores the replacement data in the Attribute.db_value field as a tuple with four fields
|
||||
`(regex_nick, template_string, raw_nick, raw_template)`. Here `regex_nick` is the converted regex
|
||||
representation of the `raw_nick` and the `template-string` is a version of the `raw_template`
|
||||
prepared for efficient replacement of any `$`- type markers. The `raw_nick` and `raw_template` are
|
||||
basically the unchanged strings you enter to the `nick` command (with unparsed `$` etc).
|
||||
|
||||
If you need to access the tuple for some reason, here's how:
|
||||
|
||||
```python
|
||||
tuple = obj.nicks.get("nickname", return_tuple=True)
|
||||
# or, alternatively
|
||||
tuple = obj.nicks.get("nickname", return_obj=True).value
|
||||
```
|
||||
|
|
@ -1,238 +0,0 @@
|
|||
# Objects
|
||||
|
||||
|
||||
All in-game objects in Evennia, be it characters, chairs, monsters, rooms or hand grenades are
|
||||
represented by an Evennia *Object*. Objects form the core of Evennia and is probably what you'll
|
||||
spend most time working with. Objects are [Typeclassed](./Typeclasses.md) entities.
|
||||
|
||||
An Evennia Object is, by definition, a Python class that includes
|
||||
[evennia.objects.objects.DefaultObject](evennia.objects.objects.DefaultObject) among its
|
||||
parents. Evennia defines several subclasses of `DefaultObject`:
|
||||
|
||||
- [evennia.objects.objects.DefaultCharacter](evennia.objects.objects.DefaultCharacter) -
|
||||
the normal in-game Character, controlled by a player.
|
||||
- [evennia.objects.objects.DefaultRoom](evennia.objects.objects.DefaultRoom) - a location in the game world.
|
||||
- [evennia.objects.objects.DefaultExit](evennia.objects.objects.DefaultExit) - an entity that (usually) sits
|
||||
in a room and represents a one-way connection to another location.
|
||||
|
||||
You will usually not use the `Default*` parents themselves. In `mygame/typeclasses/` there are
|
||||
convenient subclasses to use. They are empty, and thus identical to
|
||||
the defaults. Tweaking them is one of the main ways to customize you game!
|
||||
|
||||
- `mygame.typeclasses.objects.Object` (inherits from `DefaultObject`)
|
||||
- `mygame.typeclasses.characters.Character` (inherits from `DefaultCharacter`)
|
||||
- `mygame.typeclasses.rooms.Room` (inherits from `DefaultRoom`)
|
||||
- `mygame.typeclasses.exits.Exit` (inherits from `DefaultExit`)
|
||||
|
||||
## How to create your own object types
|
||||
|
||||
You can easily add your own in-game behavior by either modifying one of the typeclasses in
|
||||
your game dir or by inheriting from them.
|
||||
|
||||
You can put your new typeclass directly in the relevant parent
|
||||
module, or you could organize your code in some other way. Here we assume we make a new module
|
||||
`mygame/typeclasses/flowers.py`:
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/flowers.py
|
||||
|
||||
from typeclasses.objects import Object
|
||||
|
||||
class Rose(Object):
|
||||
"""
|
||||
This creates a simple rose object
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"this is called only once, when object is first created"
|
||||
# add a persistent attribute 'desc'
|
||||
# to object (silly example).
|
||||
self.db.desc = "This is a pretty rose with thorns."
|
||||
```
|
||||
|
||||
Now you just need to point to the class *Rose* with the `create` command
|
||||
to make a new rose:
|
||||
|
||||
@create/drop MyRose:flowers.Rose
|
||||
|
||||
What the `create` command actually *does* is to use the [evennia.create_object](evennia.utils.create.create_object)
|
||||
function. You can do the same thing yourself in code:
|
||||
|
||||
```python
|
||||
from evennia import create_object
|
||||
new_rose = create_object("typeclasses.flowers.Rose", key="MyRose")
|
||||
```
|
||||
|
||||
(The `create` command will auto-append the most likely path to your typeclass, if you enter the
|
||||
call manually you have to give the full path to the class. The `create.create_object` function is
|
||||
powerful and should be used for all coded object creating (so this is what you use when defining
|
||||
your own building commands).
|
||||
|
||||
This particular Rose class doesn't really do much, all it does it make sure the attribute
|
||||
`desc`(which is what the `look` command looks for) is pre-set, which is pretty pointless since you
|
||||
will usually want to change this at build time (using the `desc` command or using the
|
||||
[Spawner](./Prototypes.md)).
|
||||
|
||||
## Adding common functionality
|
||||
|
||||
`Object`, `Character`, `Room` and `Exit` also inherit from `mygame.typeclasses.objects.ObjectParent`.
|
||||
This is an empty 'mixin' class. Optionally, you can modify this class if you want to easily add some _common_ functionality to all
|
||||
your Objects, Characters, Rooms and Exits at once. You can still customize each subclass separately (see the Python
|
||||
docs on [multiple inheritance](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance) for details).
|
||||
|
||||
For example:
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/objects.py
|
||||
# ...
|
||||
|
||||
from evennia.objects.objects import DefaultObject
|
||||
|
||||
class ObjectParent:
|
||||
def at_pre_get(self, getter, **kwargs):
|
||||
# make all entities by default un-pickable
|
||||
return False
|
||||
|
||||
class Object(ObjectParent, DefaultObject):
|
||||
# replaces at_pre_get with its own
|
||||
def at_pre_get(self, getter, **kwargs):
|
||||
return True
|
||||
|
||||
# each in their respective modules ...
|
||||
|
||||
class Character(ObjectParent, DefaultCharacter):
|
||||
# will inherit at_pre_get from ObjectParent
|
||||
pass
|
||||
|
||||
class Exit(ObjectParent, DefaultExit):
|
||||
# Overrides and uses the DefaultExit version of at_pre_get instead
|
||||
def at_pre_get(self, getter, **kwargs):
|
||||
return DefaultExit.at_pre_get(self, getter, **kwargs)
|
||||
|
||||
```
|
||||
|
||||
## Properties and functions on Objects
|
||||
|
||||
Beyond the properties assigned to all [typeclassed](./Typeclasses.md) objects (see that page for a list
|
||||
of those), the Object also has the following custom properties:
|
||||
|
||||
- `aliases` - a handler that allows you to add and remove aliases from this object. Use
|
||||
`aliases.add()` to add a new alias and `aliases.remove()` to remove one.
|
||||
- `location` - a reference to the object currently containing this object.
|
||||
- `home` is a backup location. The main motivation is to have a safe place to move the object to if
|
||||
its `location` is destroyed. All objects should usually have a home location for safety.
|
||||
- `destination` - this holds a reference to another object this object links to in some way. Its
|
||||
main use is for [Exits](./Objects.md#exits), it's otherwise usually unset.
|
||||
- `nicks` - as opposed to aliases, a [Nick](./Nicks.md) holds a convenient nickname replacement for a
|
||||
real name, word or sequence, only valid for this object. This mainly makes sense if the Object is
|
||||
used as a game character - it can then store briefer shorts, example so as to quickly reference game
|
||||
commands or other characters. Use nicks.add(alias, realname) to add a new one.
|
||||
- `account` - this holds a reference to a connected [Account](./Accounts.md) controlling this object (if
|
||||
any). Note that this is set also if the controlling account is *not* currently online - to test if
|
||||
an account is online, use the `has_account` property instead.
|
||||
- `sessions` - if `account` field is set *and the account is online*, this is a list of all active
|
||||
sessions (server connections) to contact them through (it may be more than one if multiple
|
||||
connections are allowed in settings).
|
||||
- `has_account` - a shorthand for checking if an *online* account is currently connected to this
|
||||
object.
|
||||
- `contents` - this returns a list referencing all objects 'inside' this object (i,e. which has this
|
||||
object set as their `location`).
|
||||
- `exits` - this returns all objects inside this object that are *Exits*, that is, has the
|
||||
`destination` property set.
|
||||
|
||||
The last two properties are special:
|
||||
|
||||
- `cmdset` - this is a handler that stores all [command sets](./Command-Sets.md) defined on the
|
||||
object (if any).
|
||||
- `scripts` - this is a handler that manages [Scripts](./Scripts.md) attached to the object (if any).
|
||||
|
||||
The Object also has a host of useful utility functions. See the function headers in
|
||||
`src/objects/objects.py` for their arguments and more details.
|
||||
|
||||
- `msg()` - this function is used to send messages from the server to an account connected to this
|
||||
object.
|
||||
- `msg_contents()` - calls `msg` on all objects inside this object.
|
||||
- `search()` - this is a convenient shorthand to search for a specific object, at a given location
|
||||
or globally. It's mainly useful when defining commands (in which case the object executing the
|
||||
command is named `caller` and one can do `caller.search()` to find objects in the room to operate
|
||||
on).
|
||||
- `execute_cmd()` - Lets the object execute the given string as if it was given on the command line.
|
||||
- `move_to` - perform a full move of this object to a new location. This is the main move method
|
||||
and will call all relevant hooks, do all checks etc.
|
||||
- `clear_exits()` - will delete all [Exits](./Objects.md#exits) to *and* from this object.
|
||||
- `clear_contents()` - this will not delete anything, but rather move all contents (except Exits) to
|
||||
their designated `Home` locations.
|
||||
- `delete()` - deletes this object, first calling `clear_exits()` and
|
||||
`clear_contents()`.
|
||||
|
||||
The Object Typeclass defines many more *hook methods* beyond `at_object_creation`. Evennia calls
|
||||
these hooks at various points. When implementing your custom objects, you will inherit from the
|
||||
base parent and overload these hooks with your own custom code. See `evennia.objects.objects` for an
|
||||
updated list of all the available hooks or the [API for DefaultObject here](evennia.objects.objects.DefaultObject).
|
||||
|
||||
## Subclasses of `Object`
|
||||
|
||||
There are three special subclasses of *Object* in default Evennia - *Characters*, *Rooms* and
|
||||
*Exits*. The reason they are separated is because these particular object types are fundamental,
|
||||
something you will always need and in some cases requires some extra attention in order to be
|
||||
recognized by the game engine (there is nothing stopping you from redefining them though). In
|
||||
practice they are all pretty similar to the base Object.
|
||||
|
||||
### Characters
|
||||
|
||||
Characters are objects controlled by [Accounts](./Accounts.md). When a new Account
|
||||
logs in to Evennia for the first time, a new `Character` object is created and
|
||||
the Account object is assigned to the `account` attribute. A `Character` object
|
||||
must have a [Default Commandset](./Command-Sets.md) set on itself at
|
||||
creation, or the account will not be able to issue any commands! If you just
|
||||
inherit your own class from `evennia.DefaultCharacter` and make sure to use
|
||||
`super()` to call the parent methods you should be fine. In
|
||||
`mygame/typeclasses/characters.py` is an empty `Character` class ready for you
|
||||
to modify.
|
||||
|
||||
### Rooms
|
||||
|
||||
*Rooms* are the root containers of all other objects. The only thing really separating a room from
|
||||
any other object is that they have no `location` of their own and that default commands like `@dig`
|
||||
creates objects of this class - so if you want to expand your rooms with more functionality, just
|
||||
inherit from `ev.DefaultRoom`. In `mygame/typeclasses/rooms.py` is an empty `Room` class ready for
|
||||
you to modify.
|
||||
|
||||
### Exits
|
||||
|
||||
*Exits* are objects connecting other objects (usually *Rooms*) together. An object named *North* or
|
||||
*in* might be an exit, as well as *door*, *portal* or *jump out the window*. An exit has two things
|
||||
that separate them from other objects. Firstly, their *destination* property is set and points to a
|
||||
valid object. This fact makes it easy and fast to locate exits in the database. Secondly, exits
|
||||
define a special [Transit Command](./Commands.md) on themselves when they are created. This command is
|
||||
named the same as the exit object and will, when called, handle the practicalities of moving the
|
||||
character to the Exits's *destination* - this allows you to just enter the name of the exit on its
|
||||
own to move around, just as you would expect.
|
||||
|
||||
The exit functionality is all defined on the Exit typeclass, so you could in principle completely
|
||||
change how exits work in your game (it's not recommended though, unless you really know what you are
|
||||
doing). Exits are [locked](./Locks.md) using an access_type called *traverse* and also make use of a few
|
||||
hook methods for giving feedback if the traversal fails. See `evennia.DefaultExit` for more info.
|
||||
In `mygame/typeclasses/exits.py` there is an empty `Exit` class for you to modify.
|
||||
|
||||
The process of traversing an exit is as follows:
|
||||
|
||||
1. The traversing `obj` sends a command that matches the Exit-command name on the Exit object. The
|
||||
[cmdhandler](./Commands.md) detects this and triggers the command defined on the Exit. Traversal always
|
||||
involves the "source" (the current location) and the `destination` (this is stored on the Exit
|
||||
object).
|
||||
1. The Exit command checks the `traverse` lock on the Exit object
|
||||
1. The Exit command triggers `at_traverse(obj, destination)` on the Exit object.
|
||||
1. In `at_traverse`, `object.move_to(destination)` is triggered. This triggers the following hooks,
|
||||
in order:
|
||||
1. `obj.at_pre_move(destination)` - if this returns False, move is aborted.
|
||||
1. `origin.at_pre_leave(obj, destination)`
|
||||
1. `obj.announce_move_from(destination)`
|
||||
1. Move is performed by changing `obj.location` from source location to `destination`.
|
||||
1. `obj.announce_move_to(source)`
|
||||
1. `destination.at_object_receive(obj, source)`
|
||||
1. `obj.at_post_move(source)`
|
||||
1. On the Exit object, `at_post_traverse(obj, source)` is triggered.
|
||||
|
||||
If the move fails for whatever reason, the Exit will look for an Attribute `err_traverse` on itself
|
||||
and display this as an error message. If this is not found, the Exit will instead call
|
||||
`at_failed_traverse(obj)` on itself.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Outputfuncs
|
||||
|
||||
TODO. For now info about outputfuncs are found in [OOB](../Concepts/OOB.md).
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
# Permissions
|
||||
|
||||
A *permission* is simply a text string stored in the handler `permissions` on `Objects`
|
||||
and `Accounts`. Think of it as a specialized sort of [Tag](./Tags.md) - one specifically dedicated
|
||||
to access checking. They are thus often tightly coupled to [Locks](./Locks.md).
|
||||
Permission strings are not case-sensitive, so "Builder" is the same as "builder"
|
||||
etc.
|
||||
|
||||
Permissions are used as a convenient way to structure access levels and
|
||||
hierarchies. It is set by the `perm` command and checked by the
|
||||
`PermissionHandler.check` method as well as by the specially the `perm()` and
|
||||
`pperm()` [lock functions](./Locks.md).
|
||||
|
||||
All new accounts are given a default set of permissions defined by
|
||||
`settings.PERMISSION_ACCOUNT_DEFAULT`.
|
||||
|
||||
## Managing Permissions
|
||||
|
||||
In-game, you use the `perm` command to add and remove permissions
|
||||
j
|
||||
perm/account Tommy = Builders
|
||||
perm/account/del Tommy = Builders
|
||||
|
||||
Note the use of the `/account` switch. It means you assign the permission to the
|
||||
[Accounts](./Accounts.md) Tommy instead of any [Character](./Objects.md) that also
|
||||
happens to be named "Tommy".
|
||||
|
||||
There can be reasons for putting permissions on Objects (especially NPCS), but
|
||||
for granting powers to players, you should usually put the permission on the
|
||||
`Account` - this guarantees that they are kept, *regardless*
|
||||
of which Character they are currently puppeting. This is especially important to
|
||||
remember when assigning permissions from the *hierarchy tree* (see below), as an
|
||||
Account's permissions will overrule that of its character. So to be sure to
|
||||
avoid confusion you should generally put hierarchy permissions on the Account,
|
||||
not on their Characters (but see also [quelling](#quelling)).
|
||||
|
||||
In code, you add/remove Permissions via the `PermissionHandler`, which sits on all
|
||||
typeclassed entities as the property `.permissions`:
|
||||
|
||||
```python
|
||||
account.permissions.add("Builders")
|
||||
account.permissions.add("cool_guy")
|
||||
obj.permissions.add("Blacksmith")
|
||||
obj.permissions.remove("Blacksmith")
|
||||
```
|
||||
|
||||
|
||||
## The permission hierarchy
|
||||
|
||||
Selected permission strings can be organized in a *permission hierarchy* by editing the tuple
|
||||
`settings.PERMISSION_HIERARCHY`. Evennia's default permission hierarchy is as follows
|
||||
(in increasing order of power):
|
||||
|
||||
Player # can chat and send tells (default level) (lowest)
|
||||
Helper # can edit help files
|
||||
Builder # can edit the world
|
||||
Admin # can administrate accounts
|
||||
Developer # like superuser but affected by locks (highest)
|
||||
|
||||
(Besides being case-insensitive, hierarchical permissions also understand the
|
||||
plural form, so you could use `Developers` and `Developer` interchangeably).
|
||||
|
||||
> There is also a `Guest` level below `Player` that is only active if `settings.GUEST_ENABLED` is
|
||||
set. The Guest is is never part of `settings.PERMISSION_HIERARCHY`.
|
||||
|
||||
When checking a hierarchical permission (using one of the methods to follow),
|
||||
you will pass checks for your level and all *below* you. That is, even if the
|
||||
check explicitly checks for "Builder" level access, you will actually pass if you have
|
||||
one of "Builder", "Admin" or "Developer". By contrast, if you check for a
|
||||
non-hierarchical permission, like "Blacksmith" you *must* have exactly
|
||||
that permission to pass.
|
||||
|
||||
## Checking permissions
|
||||
|
||||
It's important to note that you check for the permission of a *puppeted*
|
||||
[Object](./Objects.md) (like a Character), the check will always first use the
|
||||
permissions of any `Account` connected to that Object before checking for
|
||||
permissions on the Object. In the case of hierarchical permissions (Admins,
|
||||
Builders etc), the Account permission will always be used (this stops an Account
|
||||
from escalating their permission by puppeting a high-level Character). If the
|
||||
permission looked for is not in the hierarchy, an exact match is required, first
|
||||
on the Account and if not found there (or if no Account is connected), then on
|
||||
the Object itself.
|
||||
|
||||
### Checking with obj.permissions.check()
|
||||
|
||||
The simplest way to check if an entity has a permission is to check its
|
||||
_PermissionHandler_, stored as `.permissions` on all typeclassed entities.
|
||||
|
||||
if obj.permissions.check("Builder"):
|
||||
# allow builder to do stuff
|
||||
|
||||
if obj.permissions.check("Blacksmith", "Warrior"):
|
||||
# do stuff for blacksmiths OR warriors
|
||||
|
||||
if obj.permissions.check("Blacksmith", "Warrior", require_all=True):
|
||||
# only for those that are both blacksmiths AND warriors
|
||||
|
||||
Using the `.check` method is the way to go, it will take hierarchical
|
||||
permissions into account, check accounts/sessions etc.
|
||||
|
||||
```{warning}
|
||||
|
||||
Don't confuse `.permissions.check()` with `.permissions.has()`. The .has()
|
||||
method checks if a string is defined specifically on that PermissionHandler.
|
||||
It will not consider permission-hierarchy, puppeting etc. `.has` can be useful
|
||||
if you are manipulating permissions, but use `.check` for access checking.
|
||||
|
||||
```
|
||||
|
||||
### Lock funcs
|
||||
|
||||
While the `PermissionHandler` offers a simple way to check perms, [Lock
|
||||
strings](./Locks.md) offers a mini-language for describing how something is accessed.
|
||||
The `perm()` _lock function_ is the main tool for using Permissions in locks.
|
||||
|
||||
Let's say we have a `red_key` object. We also have red chests that we want to
|
||||
unlock with this key.
|
||||
|
||||
perm red_key = unlocks_red_chests
|
||||
|
||||
This gives the `red_key` object the permission "unlocks_red_chests". Next we
|
||||
lock our red chests:
|
||||
|
||||
lock red chest = unlock:perm(unlocks_red_chests)
|
||||
|
||||
When trying to unlock the red chest with this key, the chest Typeclass could
|
||||
then take the key and do an access check:
|
||||
|
||||
```python
|
||||
# in some typeclass file where chest is defined
|
||||
|
||||
class TreasureChest(Object):
|
||||
|
||||
# ...
|
||||
|
||||
def open_chest(self, who, tried_key):
|
||||
|
||||
if not chest.access(who, tried_key, "unlock"):
|
||||
who.msg("The key does not fit!")
|
||||
return
|
||||
else:
|
||||
who.msg("The key fits! The chest opens.")
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
There are several variations to the default `perm` lockfunc:
|
||||
|
||||
- `perm_above` - requires a hierarchical permission *higher* than the one
|
||||
provided. Example: `"edit: perm_above(Player)"`
|
||||
- `pperm` - looks *only* for permissions on `Accounts`, never at any puppeted
|
||||
objects (regardless of hierarchical perm or not).
|
||||
- `pperm_above` - like `perm_above`, but for Accounts only.
|
||||
|
||||
### Some examples
|
||||
|
||||
Adding permissions and checking with locks
|
||||
|
||||
```python
|
||||
account.permissions.add("Builder")
|
||||
account.permissions.add("cool_guy")
|
||||
account.locks.add("enter:perm_above(Player) and perm(cool_guy)")
|
||||
account.access(obj1, "enter") # this returns True!
|
||||
```
|
||||
|
||||
An example of a puppet with a connected account:
|
||||
|
||||
```python
|
||||
account.permissions.add("Player")
|
||||
puppet.permissions.add("Builders")
|
||||
puppet.permissions.add("cool_guy")
|
||||
obj2.locks.add("enter:perm_above(Accounts) and perm(cool_guy)")
|
||||
|
||||
obj2.access(puppet, "enter") # this returns False, since puppet permission
|
||||
# is lower than Account's perm, and perm takes
|
||||
# precedence.
|
||||
```
|
||||
|
||||
## Superusers
|
||||
|
||||
There is normally only one *superuser* account and that is the one first created
|
||||
when starting Evennia (User #1). This is sometimes known as the "Owner" or "God"
|
||||
user. A superuser has more than full access - it completely *bypasses* all
|
||||
locks and will always pass the `PermissionHandler.check()` check. This allows
|
||||
for the superuser to always have access to everything in an emergency. But it
|
||||
could also hide any eventual errors you might have made in your lock definitions. So
|
||||
when trying out game systems you should either use quelling (see below) or make
|
||||
a second Developer-level character that does not bypass such checks.
|
||||
|
||||
## Quelling
|
||||
|
||||
The `quell` command can be used to enforce the `perm()` lockfunc to ignore
|
||||
permissions on the Account and instead use the permissions on the Character
|
||||
only. This can be used e.g. by staff to test out things with a lower permission
|
||||
level. Return to the normal operation with `unquell`. Note that quelling will
|
||||
use the smallest of any hierarchical permission on the Account or Character, so
|
||||
one cannot escalate one's Account permission by quelling to a high-permission
|
||||
Character. Also the superuser can quell their powers this way, making them
|
||||
affectable by locks.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# Portal And Server
|
||||
|
||||
|
||||
Evennia consists of two processes, known as *Portal* and *Server*. They can be controlled from
|
||||
inside the game or from the command line as described [here](../Setup/Start-Stop-Reload.md).
|
||||
|
||||
If you are new to the concept, the main purpose of separating the two is to have accounts connect to
|
||||
the Portal but keep the MUD running on the Server. This way one can restart/reload the game (the
|
||||
Server part) without Accounts getting disconnected.
|
||||
|
||||

|
||||
|
||||
The Server and Portal are glued together via an AMP (Asynchronous Messaging Protocol) connection.
|
||||
This allows the two programs to communicate seamlessly.
|
||||
|
|
@ -1,334 +0,0 @@
|
|||
# Spawner and Prototypes
|
||||
|
||||
|
||||
The *spawner* is a system for defining and creating individual objects from a base template called a
|
||||
*prototype*. It is only designed for use with in-game [Objects](./Objects.md), not any other type of
|
||||
entity.
|
||||
|
||||
The normal way to create a custom object in Evennia is to make a [Typeclass](./Typeclasses.md). If you
|
||||
haven't read up on Typeclasses yet, think of them as normal Python classes that save to the database
|
||||
behind the scenes. Say you wanted to create a "Goblin" enemy. A common way to do this would be to
|
||||
first create a `Mobile` typeclass that holds everything common to mobiles in the game, like generic
|
||||
AI, combat code and various movement methods. A `Goblin` subclass is then made to inherit from
|
||||
`Mobile`. The `Goblin` class adds stuff unique to goblins, like group-based AI (because goblins are
|
||||
smarter in a group), the ability to panic, dig for gold etc.
|
||||
|
||||
But now it's time to actually start to create some goblins and put them in the world. What if we
|
||||
wanted those goblins to not all look the same? Maybe we want grey-skinned and green-skinned goblins
|
||||
or some goblins that can cast spells or which wield different weapons? We *could* make subclasses of
|
||||
`Goblin`, like `GreySkinnedGoblin` and `GoblinWieldingClub`. But that seems a bit excessive (and a
|
||||
lot of Python code for every little thing). Using classes can also become impractical when wanting
|
||||
to combine them - what if we want a grey-skinned goblin shaman wielding a spear - setting up a web
|
||||
of classes inheriting each other with multiple inheritance can be tricky.
|
||||
|
||||
This is what the *prototype* is for. It is a Python dictionary that describes these per-instance
|
||||
changes to an object. The prototype also has the advantage of allowing an in-game builder to
|
||||
customize an object without access to the Python backend. Evennia also allows for saving and
|
||||
searching prototypes so other builders can find and use (and tweak) them later. Having a library of
|
||||
interesting prototypes is a good reasource for builders. The OLC system allows for creating, saving,
|
||||
loading and manipulating prototypes using a menu system.
|
||||
|
||||
The *spawner* takes a prototype and uses it to create (spawn) new, custom objects.
|
||||
|
||||
## Using the OLC
|
||||
|
||||
Enter the `olc` command or `@spawn/olc` to enter the prototype wizard. This is a menu system for
|
||||
creating, loading, saving and manipulating prototypes. It's intended to be used by in-game builders
|
||||
and will give a better understanding of prototypes in general. Use `help` on each node of the menu
|
||||
for more information. Below are further details about how prototypes work and how they are used.
|
||||
|
||||
## The prototype
|
||||
|
||||
The prototype dictionary can either be created for you by the OLC (see above), be written manually
|
||||
in a Python module (and then referenced by the `@spawn` command/OLC), or created on-the-fly and
|
||||
manually loaded into the spawner function or `@spawn` command.
|
||||
|
||||
The dictionary defines all possible database-properties of an Object. It has a fixed set of allowed
|
||||
keys. When preparing to store the prototype in the database (or when using the OLC), some
|
||||
of these keys are mandatory. When just passing a one-time prototype-dict to the spawner the system
|
||||
is
|
||||
more lenient and will use defaults for keys not explicitly provided.
|
||||
|
||||
In dictionary form, a prototype can look something like this:
|
||||
|
||||
```python
|
||||
{
|
||||
"prototype_key": "house"
|
||||
"key": "Large house"
|
||||
"typeclass": "typeclasses.rooms.house.House"
|
||||
}
|
||||
```
|
||||
If you wanted to load it into the spawner in-game you could just put all on one line:
|
||||
|
||||
@spawn {"prototype_key="house", "key": "Large house", ...}
|
||||
|
||||
> Note that the prototype dict as given on the command line must be a valid Python structure -
|
||||
so you need to put quotes around strings etc. For security reasons, a dict inserted from-in game
|
||||
cannot have any
|
||||
other advanced Python functionality, such as executable code, `lambda` etc. If builders are supposed
|
||||
to be able to use such features, you need to offer them through [$protfuncs](Spawner-and-
|
||||
Prototypes#protfuncs), embedded runnable functions that you have full control to check and vet
|
||||
before running.
|
||||
|
||||
### Prototype keys
|
||||
|
||||
All keys starting with `prototype_` are for book keeping.
|
||||
|
||||
- `prototype_key` - the 'name' of the prototype, used for referencing the prototype
|
||||
when spawning and inheritance. If defining a prototype in a module and this
|
||||
not set, it will be auto-set to the name of the prototype's variable in the module.
|
||||
- `prototype_parent` - If given, this should be the `prototype_key` of another prototype stored in
|
||||
the system or available in a module. This makes this prototype *inherit* the keys from the
|
||||
parent and only override what is needed. Give a tuple `(parent1, parent2, ...)` for multiple
|
||||
left-right inheritance. If this is not given, a `typeclass` should usually be defined (below).
|
||||
- `prototype_desc` - this is optional and used when listing the prototype in in-game listings.
|
||||
- `protototype_tags` - this is optional and allows for tagging the prototype in order to find it
|
||||
easier later.
|
||||
- `prototype_locks` - two lock types are supported: `edit` and `spawn`. The first lock restricts
|
||||
the copying and editing of the prototype when loaded through the OLC. The second determines who
|
||||
may use the prototype to create new objects.
|
||||
|
||||
|
||||
The remaining keys determine actual aspects of the objects to spawn from this prototype:
|
||||
|
||||
- `key` - the main object identifier. Defaults to "Spawned Object *X*", where *X* is a random
|
||||
integer.
|
||||
- `typeclass` - A full python-path (from your gamedir) to the typeclass you want to use. If not
|
||||
set, the `prototype_parent` should be
|
||||
defined, with `typeclass` defined somewhere in the parent chain. When creating a one-time
|
||||
prototype
|
||||
dict just for spawning, one could omit this - `settings.BASE_OBJECT_TYPECLASS` will be used
|
||||
instead.
|
||||
- `location` - this should be a `#dbref`.
|
||||
- `home` - a valid `#dbref`. Defaults to `location` or `settings.DEFAULT_HOME` if location does not
|
||||
exist.
|
||||
- `destination` - a valid `#dbref`. Only used by exits.
|
||||
- `permissions` - list of permission strings, like `["Accounts", "may_use_red_door"]`
|
||||
- `locks` - a [lock-string](./Locks.md) like `"edit:all();control:perm(Builder)"`
|
||||
- `aliases` - list of strings for use as aliases
|
||||
- `tags` - list [Tags](./Tags.md). These are given as tuples `(tag, category, data)`.
|
||||
- `attrs` - list of [Attributes](./Attributes.md). These are given as tuples `(attrname, value,
|
||||
category, lockstring)`
|
||||
- Any other keywords are interpreted as non-category [Attributes](./Attributes.md) and their values.
|
||||
This is convenient for simple Attributes - use `attrs` for full control of Attributes.
|
||||
|
||||
#### More on prototype inheritance
|
||||
|
||||
- A prototype can inherit by defining a `prototype_parent` pointing to the name
|
||||
(`prototype_key` of another prototype). If a list of `prototype_keys`, this
|
||||
will be stepped through from left to right, giving priority to the first in
|
||||
the list over those appearing later. That is, if your inheritance is
|
||||
`prototype_parent = ('A', 'B,' 'C')`, and all parents contain colliding keys,
|
||||
then the one from `A` will apply.
|
||||
- The prototype keys that start with `prototype_*` are all unique to each
|
||||
prototype. They are _never_ inherited from parent to child.
|
||||
- The prototype fields `'attr': [(key, value, category, lockstring),...]`
|
||||
and `'tags': [(key, category, data), ...]` are inherited in a _complementary_
|
||||
fashion. That means that only colliding key+category matches will be replaced, not the entire list.
|
||||
Remember that the category `None` is also considered a valid category!
|
||||
- Adding an Attribute as a simple `key:value` will under the hood be translated
|
||||
into an Attribute tuple `(key, value, None, '')` and may replace an Attribute
|
||||
in the parent if it the same key and a `None` category.
|
||||
- All other keys (`permissions`, `destination`, `aliases` etc) are completely
|
||||
_replaced_ by the child's value if given. For the parent's value to be
|
||||
retained, the child must not define these keys at all.
|
||||
|
||||
### Prototype values
|
||||
|
||||
The prototype supports values of several different types.
|
||||
|
||||
It can be a hard-coded value:
|
||||
|
||||
```python
|
||||
{"key": "An ugly goblin", ...}
|
||||
|
||||
```
|
||||
|
||||
It can also be a *callable*. This callable is called without arguments whenever the prototype is
|
||||
used to
|
||||
spawn a new object:
|
||||
|
||||
```python
|
||||
{"key": _get_a_random_goblin_name, ...}
|
||||
|
||||
```
|
||||
|
||||
By use of Python `lambda` one can wrap the callable so as to make immediate settings in the
|
||||
prototype:
|
||||
|
||||
```python
|
||||
{"key": lambda: random.choice(("Urfgar", "Rick the smelly", "Blargh the foul", ...)), ...}
|
||||
|
||||
```
|
||||
|
||||
#### Protfuncs
|
||||
|
||||
Finally, the value can be a *prototype function* (*Protfunc*). These look like simple function calls
|
||||
that you embed in strings and that has a `$` in front, like
|
||||
|
||||
```python
|
||||
{"key": "$choice(Urfgar, Rick the smelly, Blargh the foul)",
|
||||
"attrs": {"desc": "This is a large $red(and very red) demon. "
|
||||
"He has $randint(2,5) skulls in a chain around his neck."}
|
||||
```
|
||||
At execution time, the place of the protfunc will be replaced with the result of that protfunc being
|
||||
called (this is always a string). A protfunc is a [FuncParser function](./FuncParser.md) run
|
||||
every time the prototype is used to spawn a new object.
|
||||
|
||||
Here is how a protfunc is defined (same as an inlinefunc).
|
||||
|
||||
```python
|
||||
# this is a silly example, you can just color the text red with |r directly!
|
||||
def red(*args, **kwargs):
|
||||
"""
|
||||
Usage: $red(<text>)
|
||||
Returns the same text you entered, but red.
|
||||
"""
|
||||
if not args or len(args) > 1:
|
||||
raise ValueError("Must have one argument, the text to color red!")
|
||||
return f"|r{args[0]}|n"
|
||||
```
|
||||
|
||||
> Note that we must make sure to validate input and raise `ValueError` if that fails. Also, it is
|
||||
*not* possible to use keywords in the call to the protfunc (so something like `$echo(text,
|
||||
align=left)` is invalid). The `kwargs` requred is for internal evennia use and not used at all for
|
||||
protfuncs (only by inlinefuncs).
|
||||
|
||||
To make this protfunc available to builders in-game, add it to a new module and add the path to that
|
||||
module to `settings.PROT_FUNC_MODULES`:
|
||||
|
||||
```python
|
||||
# in mygame/server/conf/settings.py
|
||||
|
||||
PROT_FUNC_MODULES += ["world.myprotfuncs"]
|
||||
|
||||
```
|
||||
All *global callables* in your added module will be considered a new protfunc. To avoid this (e.g.
|
||||
to have helper functions that are not protfuncs on their own), name your function something starting
|
||||
with `_`.
|
||||
|
||||
The default protfuncs available out of the box are defined in `evennia/prototypes/profuncs.py`. To
|
||||
override the ones available, just add the same-named function in your own protfunc module.
|
||||
|
||||
| Protfunc | Description |
|
||||
| --- | --- |
|
||||
| `$random()` | Returns random value in range [0, 1) |
|
||||
| `$randint(start, end)` | Returns random value in range [start, end] |
|
||||
| `$left_justify(<text>)` | Left-justify text |
|
||||
| `$right_justify(<text>)` | Right-justify text to screen width |
|
||||
| `$center_justify(<text>)` | Center-justify text to screen width |
|
||||
| `$full_justify(<text>)` | Spread text across screen width by adding spaces |
|
||||
| `$protkey(<name>)` | Returns value of another key in this prototype (self-reference) |
|
||||
| `$add(<value1>, <value2>)` | Returns value1 + value2. Can also be lists, dicts etc |
|
||||
| `$sub(<value1>, <value2>)` | Returns value1 - value2 |
|
||||
| `$mult(<value1>, <value2>)` | Returns value1 * value2 |
|
||||
| `$div(<value1>, <value2>)` | Returns value2 / value1 |
|
||||
| `$toint(<value>)` | Returns value converted to integer (or value if not possible) |
|
||||
| `$eval(<code>)` | Returns result of [literal-eval](https://docs.python.org/2/library/ast.html#ast.literal_eval) of code string. Only simple python expressions. |
|
||||
| `$obj(<query>)` | Returns object #dbref searched globally by key, tag or #dbref. Error if more than one found. |
|
||||
| `$objlist(<query>)` | Like `$obj`, except always returns a list of zero, one or more results. |
|
||||
| `$dbref(dbref)` | Returns argument if it is formed as a #dbref (e.g. #1234), otherwise error. |
|
||||
|
||||
For developers with access to Python, using protfuncs in prototypes is generally not useful. Passing
|
||||
real Python functions is a lot more powerful and flexible. Their main use is to allow in-game
|
||||
builders to
|
||||
do limited coding/scripting for their prototypes without giving them direct access to raw Python.
|
||||
|
||||
## Storing prototypes
|
||||
|
||||
A prototype can be defined and stored in two ways, either in the database or as a dict in a module.
|
||||
|
||||
### Database prototypes
|
||||
|
||||
Stored as [Scripts](./Scripts.md) in the database. These are sometimes referred to as *database-
|
||||
prototypes* This is the only way for in-game builders to modify and add prototypes. They have the
|
||||
advantage of being easily modifiable and sharable between builders but you need to work with them
|
||||
using in-game tools.
|
||||
|
||||
### Module-based prototypes
|
||||
|
||||
These prototypes are defined as dictionaries assigned to global variables in one of the modules
|
||||
defined in `settings.PROTOTYPE_MODULES`. They can only be modified from outside the game so they are
|
||||
are necessarily "read-only" from in-game and cannot be modified (but copies of them could be made
|
||||
into database-prototypes). These were the only prototypes available before Evennia 0.8. Module based
|
||||
prototypes can be useful in order for developers to provide read-only "starting" or "base"
|
||||
prototypes to build from or if they just prefer to work offline in an external code editor.
|
||||
|
||||
By default `mygame/world/prototypes.py` is set up for you to add your own prototypes. *All global
|
||||
dicts* in this module will be considered by Evennia to be a prototype. You could also tell Evennia
|
||||
to look for prototypes in more modules if you want:
|
||||
|
||||
```python
|
||||
# in mygame/server/conf.py
|
||||
|
||||
PROTOTYPE_MODULES = += ["world.myownprototypes", "combat.prototypes"]
|
||||
|
||||
```
|
||||
|
||||
Here is an example of a prototype defined in a module:
|
||||
|
||||
```python
|
||||
# in a module Evennia looks at for prototypes,
|
||||
# (like mygame/world/prototypes.py)
|
||||
|
||||
ORC_SHAMAN = {"key":"Orc shaman",
|
||||
"typeclass": "typeclasses.monsters.Orc",
|
||||
"weapon": "wooden staff",
|
||||
"health": 20}
|
||||
```
|
||||
|
||||
> Note that in the example above, `"ORC_SHAMAN"` will become the `prototype_key` of this prototype.
|
||||
> It's the only case when `prototype_key` can be skipped in a prototype. However, if `prototype_key`
|
||||
> was given explicitly, that would take precedence. This is a legacy behavior and it's recommended
|
||||
> that you always add `prototype_key` to be consistent.
|
||||
|
||||
|
||||
## Using @spawn
|
||||
|
||||
The spawner can be used from inside the game through the Builder-only `@spawn` command. Assuming the
|
||||
"goblin" typeclass is available to the system (either as a database-prototype or read from module),
|
||||
you can spawn a new goblin with
|
||||
|
||||
@spawn goblin
|
||||
|
||||
You can also specify the prototype directly as a valid Python dictionary:
|
||||
|
||||
@spawn {"prototype_key": "shaman", \
|
||||
"key":"Orc shaman", \
|
||||
"prototype_parent": "goblin", \
|
||||
"weapon": "wooden staff", \
|
||||
"health": 20}
|
||||
|
||||
> Note: The `@spawn` command is more lenient about the prototype dictionary than shown here. So you
|
||||
can for example skip the `prototype_key` if you are just testing a throw-away prototype. A random
|
||||
hash will be used to please the validation. You could also skip `prototype_parent/typeclass` - then
|
||||
the typeclass given by `settings.BASE_OBJECT_TYPECLASS` will be used.
|
||||
|
||||
## Using evennia.prototypes.spawner()
|
||||
|
||||
In code you access the spawner mechanism directly via the call
|
||||
|
||||
```python
|
||||
new_objects = evennia.prototypes.spawner.spawn(*prototypes)
|
||||
```
|
||||
|
||||
All arguments are prototype dictionaries. The function will return a
|
||||
matching list of created objects. Example:
|
||||
|
||||
```python
|
||||
obj1, obj2 = evennia.prototypes.spawner.spawn({"key": "Obj1", "desc": "A test"},
|
||||
{"key": "Obj2", "desc": "Another test"})
|
||||
```
|
||||
> Hint: Same as when using `@spawn`, when spawning from a one-time prototype dict like this, you can
|
||||
skip otherwise required keys, like `prototype_key` or `typeclass`/`prototype_parent`. Defaults will
|
||||
be used.
|
||||
|
||||
Note that no `location` will be set automatically when using `evennia.prototypes.spawner.spawn()`,
|
||||
you
|
||||
have to specify `location` explicitly in the prototype dict.
|
||||
|
||||
If the prototypes you supply are using `prototype_parent` keywords, the spawner will read prototypes
|
||||
from modules
|
||||
in `settings.PROTOTYPE_MODULES` as well as those saved to the database to determine the body of
|
||||
available parents. The `spawn` command takes many optional keywords, you can find its definition [in
|
||||
the api docs](github:evennia.prototypes.spawner#spawn).
|
||||
|
|
@ -1,437 +0,0 @@
|
|||
# Scripts
|
||||
|
||||
[Script API reference](evennia.scripts.scripts)
|
||||
|
||||
*Scripts* are the out-of-character siblings to the in-character
|
||||
[Objects](./Objects.md). Scripts are so flexible that the name "Script" is a bit limiting
|
||||
in itself - but we had to pick _something_ to name them. Other possible names
|
||||
(depending on what you'd use them for) would be `OOBObjects`, `StorageContainers` or `TimerObjects`.
|
||||
|
||||
If you ever consider creating an [Object](./Objects.md) with a `None`-location just to store some game data,
|
||||
you should really be using a Script instead.
|
||||
|
||||
- Scripts are full [Typeclassed](./Typeclasses.md) entities - they have [Attributes](./Attributes.md) and
|
||||
can be modified in the same way. But they have _no in-game existence_, so no
|
||||
location or command-execution like [Objects](./Objects.md) and no connection to a particular
|
||||
player/session like [Accounts](./Accounts.md). This means they are perfectly suitable for acting
|
||||
as database-storage backends for game _systems_: Storing the current state of the economy,
|
||||
who is involved in the current fight, tracking an ongoing barter and so on. They are great as
|
||||
persistent system handlers.
|
||||
- Scripts have an optional _timer component_. This means that you can set up the script
|
||||
to tick the `at_repeat` hook on the Script at a certain interval. The timer can be controlled
|
||||
independently of the rest of the script as needed. This component is optional
|
||||
and complementary to other timing functions in Evennia, like
|
||||
[evennia.utils.delay](evennia.utils.utils.delay) and
|
||||
[evennia.utils.repeat](evennia.utils.utils.repeat).
|
||||
- Scripts can _attach_ to Objects and Accounts via e.g. `obj.scripts.add/remove`. In the
|
||||
script you can then access the object/account as `self.obj` or `self.account`. This can be used to
|
||||
dynamically extend other typeclasses but also to use the timer component to affect the parent object
|
||||
in various ways. For historical reasons, a Script _not_ attached to an object is referred to as a
|
||||
_Global_ Script.
|
||||
|
||||
```{versionchanged} 1.0
|
||||
In previus Evennia versions, stopping the Script's timer also meant deleting the Script object.
|
||||
Starting with this version, the timer can be start/stopped separately and `.delete()` must be called
|
||||
on the Script explicitly to delete it.
|
||||
|
||||
```
|
||||
|
||||
## In-game command examples
|
||||
|
||||
There are two main commands controlling scripts in the default cmdset:
|
||||
|
||||
The `addscript` command is used for attaching scripts to existing objects:
|
||||
|
||||
> addscript obj = bodyfunctions.BodyFunctions
|
||||
|
||||
The `scripts` command is used to view all scripts and perform operations on them:
|
||||
|
||||
> scripts
|
||||
> scripts/stop bodyfunctions.BodyFunctions
|
||||
> scripts/start #244
|
||||
> scripts/pause #11
|
||||
> scripts/delete #566
|
||||
|
||||
```{versionchanged} 1.0
|
||||
The `addscript` command used to be only `script` which was easy to confuse with `scripts`.
|
||||
```
|
||||
|
||||
## Code examples
|
||||
|
||||
Here are some examples of working with Scripts in-code (more details to follow in later
|
||||
sections).
|
||||
|
||||
Create a new script:
|
||||
```python
|
||||
new_script = evennia.create_script(key="myscript", typeclass=...)
|
||||
```
|
||||
|
||||
Create script with timer component:
|
||||
|
||||
```python
|
||||
# (note that this will call `timed_script.at_repeat` which is empty by default)
|
||||
timed_script = evennia.create_script(key="Timed script",
|
||||
interval=34, # seconds <=0 means off
|
||||
start_delay=True, # wait interval before first call
|
||||
autostart=True) # start timer (else needing .start() )
|
||||
|
||||
# manipulate the script's timer
|
||||
timed_script.stop()
|
||||
timed_script.start()
|
||||
timed_script.pause()
|
||||
timed_script.unpause()
|
||||
```
|
||||
|
||||
Attach script to another object:
|
||||
|
||||
```python
|
||||
myobj.scripts.add(new_script)
|
||||
myobj.scripts.add(evennia.DefaultScript)
|
||||
all_scripts_on_obj = myobj.scripts.all()
|
||||
```
|
||||
|
||||
Search/find scripts in various ways:
|
||||
|
||||
```python
|
||||
# regular search (this is always a list, also if there is only one match)
|
||||
list_of_myscripts = evennia.search_script("myscript")
|
||||
|
||||
# search through Evennia's GLOBAL_SCRIPTS container (based on
|
||||
# script's key only)
|
||||
from evennia import GLOBAL_SCRIPTS
|
||||
|
||||
myscript = GLOBAL_SCRIPTS.myscript
|
||||
GLOBAL_SCRIPTS.get("Timed script").db.foo = "bar"
|
||||
```
|
||||
|
||||
Delete the Script (this will also stop its timer):
|
||||
|
||||
```python
|
||||
new_script.delete()
|
||||
timed_script.delete()
|
||||
```
|
||||
|
||||
## Defining new Scripts
|
||||
|
||||
A Script is defined as a class and is created in the same way as other
|
||||
[typeclassed](./Typeclasses.md) entities. The parent class is `evennia.DefaultScript`.
|
||||
|
||||
|
||||
### Simple storage script
|
||||
|
||||
In `mygame/typeclasses/scripts.py` is an empty `Script` class already set up. You
|
||||
can use this as a base for your own scripts.
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/scripts.py
|
||||
|
||||
from evennia import DefaultScript
|
||||
|
||||
class Script(DefaultScript):
|
||||
# stuff common for all your scripts goes here
|
||||
|
||||
class MyScript(Script):
|
||||
def at_script_creation(self):
|
||||
"""Called once, when script is first created"""
|
||||
self.key = "myscript"
|
||||
self.db.foo = "bar"
|
||||
|
||||
```
|
||||
|
||||
Once created, this simple Script could act as a global storage:
|
||||
|
||||
```python
|
||||
evennia.create_script('typeclasses.scripts.MyScript')
|
||||
|
||||
# from somewhere else
|
||||
|
||||
myscript = evennia.search_script("myscript").first()
|
||||
bar = myscript.db.foo
|
||||
myscript.db.something_else = 1000
|
||||
|
||||
```
|
||||
|
||||
Note that if you give keyword arguments to `create_script` you can override the values
|
||||
you set in your `at_script_creation`:
|
||||
|
||||
```python
|
||||
|
||||
evennia.create_script('typeclasses.scripts.MyScript', key="another name",
|
||||
attributes=[("foo", "bar-alternative")])
|
||||
|
||||
|
||||
```
|
||||
|
||||
See the [create_script](evennia.utils.create.create_script) and
|
||||
[search_script](evennia.utils.search.search_script) API documentation for more options
|
||||
on creating and finding Scripts.
|
||||
|
||||
|
||||
## Timed Scripts
|
||||
|
||||
There are several properties one can set on the Script to control its timer component.
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/scripts.py
|
||||
|
||||
class TimerScript(Script):
|
||||
|
||||
def at_script_creation(self):
|
||||
self.key = "myscript"
|
||||
self.desc = "An example script"
|
||||
self.interval = 60 # 1 min repeat
|
||||
|
||||
def at_repeat(self):
|
||||
# do stuff every minute
|
||||
|
||||
```
|
||||
|
||||
This example will call `at_repeat` every minute. The `create_script` function has an `autostart=True` keyword
|
||||
set by default - this means the script's timer component will be started automatically. Otherwise
|
||||
`.start()` must be called separately.
|
||||
|
||||
Supported properties are:
|
||||
|
||||
- `key` (str): The name of the script. This makes it easier to search for it later. If it's a script
|
||||
attached to another object one can also get all scripts off that object and get the script that way.
|
||||
- `desc` (str): Note - not `.db.desc`! This is a database field on the Script shown in script listings
|
||||
to help identifying what does what.
|
||||
- `interval` (int): The amount of time (in seconds) between every 'tick' of the timer. Note that
|
||||
it's generally bad practice to use sub-second timers for anything in a text-game - the player will
|
||||
not be able to appreciate the precision (and if you print it, it will just spam the screen). For
|
||||
calculations you can pretty much always do them on-demand, or at a much slower interval without the
|
||||
player being the wiser.
|
||||
- `start_delay` (bool): If timer should start right away or wait `interval` seconds first.
|
||||
- `repeats` (int): If >0, the timer will only run this many times before stopping. Otherwise the
|
||||
number of repeats are infinite. If set to 1, the Script mimics a `delay` action.
|
||||
- `persistent` (bool): This defaults to `True` and means the timer will survive a server reload/reboot.
|
||||
If not, a reload will have the timer come back in a stopped state. Setting this to `False` will _not_
|
||||
delete the Script object itself (use `.delete()` for this).
|
||||
|
||||
The timer component is controlled with methods on the Script class:
|
||||
|
||||
- `.at_repeat()` - this method is called every `interval` seconds while the timer is
|
||||
active.
|
||||
- `.is_valid()` - this method is called by the timer just before `at_repeat()`. If it returns `False`
|
||||
the timer is immediately stopped.
|
||||
- `.start()` - start/update the timer. If keyword arguments are given, they can be used to
|
||||
change `interval`, `start_delay` etc on the fly. This calls the `.at_start()` hook.
|
||||
This is also called after a server reload assuming the timer was not previously stopped.
|
||||
- `.update()` - legacy alias for `.start`.
|
||||
- `.stop()` - stops and resets the timer. This calls the `.at_stop()` hook.
|
||||
- `.pause()` - pauses the timer where it is, storing its current position. This calls
|
||||
the `.at_pause(manual_pause=True)` hook. This is also called on a server reload/reboot,
|
||||
at which time the `manual_pause` will be `False`.
|
||||
- `.unpause()` - unpause a previously paused script. This will call the `at_start` hook.
|
||||
- `.time_until_next_repeat()` - get the time until next time the timer fires.
|
||||
- `.remaining_repeats()` - get the number of repeats remaining, or `None` if repeats are infinite.
|
||||
- `.reset_callcount()` - this resets the repeat counter to start over from 0. Only useful if `repeats>0`.
|
||||
- `.force_repeat()` - this prematurely forces `at_repeat` to be called right away. Doing so will reset the
|
||||
countdown so that next call will again happen after `interval` seconds.
|
||||
|
||||
### Script timers vs delay/repeat
|
||||
|
||||
If the _only_ goal is to get a repeat/delay effect, the
|
||||
[evennia.utils.delay](evennia.utils.utils.delay) and
|
||||
[evennia.utils.repeat](evennia.utils.utils.repeat) functions
|
||||
should generally be considered first. A Script is a lot 'heavier' to create/delete on the fly.
|
||||
In fact, for making a single delayed call (`script.repeats==1`), the `utils.delay` call is
|
||||
probably always the better choice.
|
||||
|
||||
For repeating tasks, the `utils.repeat` is optimized for quick repeating of a large number of objects. It
|
||||
uses the TickerHandler under the hood. Its subscription-based model makes it very efficient to
|
||||
start/stop the repeating action for an object. The side effect is however that all objects set to tick
|
||||
at a given interval will _all do so at the same time_. This may or may not look strange in-game depending
|
||||
on the situation. By contrast the Script uses its own ticker that will operate independently from the
|
||||
tickers of all other Scripts.
|
||||
|
||||
It's also worth noting that once the script object has _already been created_,
|
||||
starting/stopping/pausing/unpausing the timer has very little overhead. The pause/unpause and update
|
||||
methods of the script also offers a bit more fine-control than using `utils.delays/repeat`.
|
||||
|
||||
## Script attached to another object
|
||||
|
||||
Scripts can be attached to an [Account](./Accounts.md) or (more commonly) an [Object](./Objects.md).
|
||||
If so, the 'parent object' will be available to the script as either `.obj` or `.account`.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/scripts.py
|
||||
# Script class is defined at the top of this module
|
||||
|
||||
import random
|
||||
|
||||
class Weather(Script):
|
||||
"""
|
||||
A timer script that displays weather info. Meant to
|
||||
be attached to a room.
|
||||
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
self.key = "weather_script"
|
||||
self.desc = "Gives random weather messages."
|
||||
self.interval = 60 * 5 # every 5 minutes
|
||||
|
||||
def at_repeat(self):
|
||||
"called every self.interval seconds."
|
||||
rand = random.random()
|
||||
if rand < 0.5:
|
||||
weather = "A faint breeze is felt."
|
||||
elif rand < 0.7:
|
||||
weather = "Clouds sweep across the sky."
|
||||
else:
|
||||
weather = "There is a light drizzle of rain."
|
||||
# send this message to everyone inside the object this
|
||||
# script is attached to (likely a room)
|
||||
self.obj.msg_contents(weather)
|
||||
```
|
||||
|
||||
If attached to a room, this Script will randomly report some weather
|
||||
to everyone in the room every 5 minutes.
|
||||
|
||||
```python
|
||||
myroom.scripts.add(scripts.Weather)
|
||||
```
|
||||
|
||||
> Note that `typeclasses` in your game dir is added to the setting `TYPECLASS_PATHS`.
|
||||
> Therefore we don't need to give the full path (`typeclasses.scripts.Weather`
|
||||
> but only `scripts.Weather` above.
|
||||
|
||||
You can also attach the script as part of creating it:
|
||||
|
||||
```python
|
||||
create_script('typeclasses.weather.Weather', obj=myroom)
|
||||
```
|
||||
|
||||
## Other Script methods
|
||||
|
||||
A Script has all the properties of a typeclassed object, such as `db` and `ndb`(see
|
||||
[Typeclasses](./Typeclasses.md)). Setting `key` is useful in order to manage scripts (delete them by name
|
||||
etc). These are usually set up in the Script's typeclass, but can also be assigned on the fly as
|
||||
keyword arguments to `evennia.create_script`.
|
||||
|
||||
- `at_script_creation()` - this is only called once - when the script is first created.
|
||||
- `at_server_reload()` - this is called whenever the server is warm-rebooted (e.g. with the
|
||||
`reload` command). It's a good place to save non-persistent data you might want to survive a
|
||||
reload.
|
||||
- `at_server_shutdown()` - this is called when a system reset or systems shutdown is invoked.
|
||||
- `at_server_start()` - this is called when the server comes back (from reload/shutdown/reboot). It
|
||||
can be usuful for initializations and caching of non-persistent data when starting up a script's
|
||||
functionality.
|
||||
- `at_repeat()`
|
||||
- `at_start()`
|
||||
- `at_pause()`
|
||||
- `at_stop()`
|
||||
- `delete()` - same as for other typeclassed entities, this will delete the Script. Of note is that
|
||||
it will also stop the timer (if it runs), leading to the `at_stop` hook being called.
|
||||
|
||||
In addition, Scripts support [Attributes](./Attributes.md), [Tags](./Tags.md) and [Locks](./Locks.md) etc like other
|
||||
Typeclassed entities.
|
||||
|
||||
See also the methods involved in controlling a [Timed Script](#timed-scripts) above.
|
||||
|
||||
## The GLOBAL_SCRIPTS container
|
||||
|
||||
A Script not attached to another entity is commonly referred to as a _Global_ script since it't available
|
||||
to access from anywhere. This means they need to be searched for in order to be used.
|
||||
|
||||
Evennia supplies a convenient "container" `evennia.GLOBAL_SCRIPTS` to help organize your global
|
||||
scripts. All you need is the Script's `key`.
|
||||
|
||||
|
||||
```python
|
||||
from evennia import GLOBAL_SCRIPTS
|
||||
|
||||
# access as a property on the container, named the same as the key
|
||||
my_script = GLOBAL_SCRIPTS.my_script
|
||||
# needed if there are spaces in name or name determined on the fly
|
||||
another_script = GLOBAL_SCRIPTS.get("another script")
|
||||
# get all global scripts (this returns a Django Queryset)
|
||||
all_scripts = GLOBAL_SCRIPTS.all()
|
||||
# you can operate directly on the script
|
||||
GLOBAL_SCRIPTS.weather.db.current_weather = "Cloudy"
|
||||
|
||||
```
|
||||
|
||||
```{warning}
|
||||
Note that global scripts appear as properties on `GLOBAL_SCRIPTS` based on their `key`.
|
||||
If you were to create two global scripts with the same `key` (even with different typeclasses),
|
||||
the `GLOBAL_SCRIPTS` container will only return one of them (which one depends on order in
|
||||
the database). Best is to organize your scripts so that this does not happen. Otherwise, use
|
||||
`evennia.search_scripts` to get exactly the script you want.
|
||||
```
|
||||
|
||||
There are two ways to make a script appear as a property on `GLOBAL_SCRIPTS`:
|
||||
|
||||
1. Manually create a new global script with a `key` using `create_script`.
|
||||
2. Define the script's properties in the `GLOBAL_SCRIPTS` settings variable. This tells Evennia
|
||||
that it should check if a script with that `key` exists and if not, create it for you.
|
||||
This is very useful for scripts that must always exist and/or should be auto-created
|
||||
when your server restarts. If you use this method, you must make sure all
|
||||
script keys are globally unique.
|
||||
|
||||
Here's how to tell Evennia to manage the script in settings:
|
||||
|
||||
```python
|
||||
# in mygame/server/conf/settings.py
|
||||
|
||||
GLOBAL_SCRIPTS = {
|
||||
"my_script": {
|
||||
"typeclass": "typeclasses.scripts.Weather",
|
||||
"repeats": -1,
|
||||
"interval": 50,
|
||||
"desc": "Weather script"
|
||||
},
|
||||
"storagescript": {}
|
||||
}
|
||||
```
|
||||
|
||||
Above we add two scripts with keys `myscript` and `storagescript`respectively. The following dict
|
||||
can be empty - the `settings.BASE_SCRIPT_TYPECLASS` will then be used. Under the hood, the provided
|
||||
dict (along with the `key`) will be passed into `create_script` automatically, so
|
||||
all the [same keyword arguments as for create_script](evennia.utils.create.create_script) are
|
||||
supported here.
|
||||
|
||||
```{warning}
|
||||
Before setting up Evennia to manage your script like this, make sure that your Script typeclass
|
||||
does not have any critical errors (test it separately). If there are, you'll see errors in your log
|
||||
and your Script will temporarily fall back to being a `DefaultScript` type.
|
||||
```
|
||||
|
||||
Moreover, a script defined this way is *guaranteed* to exist when you try to access it:
|
||||
|
||||
```python
|
||||
from evennia import GLOBAL_SCRIPTS
|
||||
# Delete the script
|
||||
GLOBAL_SCRIPTS.storagescript.delete()
|
||||
# running the `scripts` command now will show no storagescript
|
||||
# but below it's automatically recreated again!
|
||||
storage = GLOBAL_SCRIPTS.storagescript
|
||||
```
|
||||
|
||||
That is, if the script is deleted, next time you get it from `GLOBAL_SCRIPTS`, Evennia will use the
|
||||
information in settings to recreate it for you on the fly.
|
||||
|
||||
|
||||
## Hints: Dealing with Script Errors
|
||||
|
||||
Errors inside a timed, executing script can sometimes be rather terse or point to
|
||||
parts of the execution mechanism that is hard to interpret. One way to make it
|
||||
easier to debug scripts is to import Evennia's native logger and wrap your
|
||||
functions in a try/catch block. Evennia's logger can show you where the
|
||||
traceback occurred in your script.
|
||||
|
||||
```python
|
||||
|
||||
from evennia.utils import logger
|
||||
|
||||
class Weather(Script):
|
||||
|
||||
# [...]
|
||||
|
||||
def at_repeat(self):
|
||||
|
||||
try:
|
||||
# [...]
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Server component
|
||||
|
||||
TODO: This is currently in [Portal-and-Server](./Portal-And-Server.md).
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
# Sessions
|
||||
|
||||
|
||||
An Evennia *Session* represents one single established connection to the server. Depending on the
|
||||
Evennia session, it is possible for a person to connect multiple times, for example using different
|
||||
clients in multiple windows. Each such connection is represented by a session object.
|
||||
|
||||
A session object has its own [cmdset](./Command-Sets.md), usually the "unloggedin" cmdset. This is what
|
||||
is used to show the login screen and to handle commands to create a new account (or
|
||||
[Account](./Accounts.md) in evennia lingo) read initial help and to log into the game with an existing
|
||||
account. A session object can either be "logged in" or not. Logged in means that the user has
|
||||
authenticated. When this happens the session is associated with an Account object (which is what
|
||||
holds account-centric stuff). The account can then in turn puppet any number of objects/characters.
|
||||
|
||||
> Warning: A Session is not *persistent* - it is not a [Typeclass](./Typeclasses.md) and has no
|
||||
connection to the database. The Session will go away when a user disconnects and you will lose any
|
||||
custom data on it if the server reloads. The `.db` handler on Sessions is there to present a uniform
|
||||
API (so you can assume `.db` exists even if you don't know if you receive an Object or a Session),
|
||||
but this is just an alias to `.ndb`. So don't store any data on Sessions that you can't afford to
|
||||
lose in a reload. You have been warned.
|
||||
|
||||
## Properties on Sessions
|
||||
|
||||
Here are some important properties available on (Server-)Sessions
|
||||
|
||||
- `sessid` - The unique session-id. This is an integer starting from 1.
|
||||
- `address` - The connected client's address. Different protocols give different information here.
|
||||
- `logged_in` - `True` if the user authenticated to this session.
|
||||
- `account` - The [Account](./Accounts.md) this Session is attached to. If not logged in yet, this is
|
||||
`None`.
|
||||
- `puppet` - The [Character/Object](./Objects.md) currently puppeted by this Account/Session combo. If
|
||||
not logged in or in OOC mode, this is `None`.
|
||||
- `ndb` - The [Non-persistent Attribute](./Attributes.md) handler.
|
||||
- `db` - As noted above, Sessions don't have regular Attributes. This is an alias to `ndb`.
|
||||
- `cmdset` - The Session's [CmdSetHandler](./Command-Sets.md)
|
||||
|
||||
Session statistics are mainly used internally by Evennia.
|
||||
|
||||
- `conn_time` - How long this Session has been connected
|
||||
- `cmd_last` - Last active time stamp. This will be reset by sending `idle` keepalives.
|
||||
- `cmd_last_visible` - last active time stamp. This ignores `idle` keepalives and representes the
|
||||
last time this session was truly visibly active.
|
||||
- `cmd_total` - Total number of Commands passed through this Session.
|
||||
|
||||
|
||||
## Multisession mode
|
||||
|
||||
The number of sessions possible to connect to a given account at the same time and how it works is
|
||||
given by the `MULTISESSION_MODE` setting:
|
||||
|
||||
* `MULTISESSION_MODE=0`: One session per account. When connecting with a new session the old one is
|
||||
disconnected. This is the default mode and emulates many classic mud code bases. In default Evennia,
|
||||
this mode also changes how the `create account` Command works - it will automatically create a
|
||||
Character with the *same name* as the Account. When logging in, the login command is also modified
|
||||
to have the player automatically puppet that Character. This makes the distinction between Account
|
||||
and Character minimal from the player's perspective.
|
||||
* `MULTISESSION_MODE=1`: Many sessions per account, input/output from/to each session is treated the
|
||||
same. For the player this means they can connect to the game from multiple clients and see the same
|
||||
output in all of them. The result of a command given in one client (that is, through one Session)
|
||||
will be returned to *all* connected Sessions/clients with no distinction. This mode will have the
|
||||
Session(s) auto-create and puppet a Character in the same way as mode 0.
|
||||
* `MULTISESSION_MODE=2`: Many sessions per account, one character per session. In this mode,
|
||||
puppeting an Object/Character will link the puppet back only to the particular Session doing the
|
||||
puppeting. That is, input from that Session will make use of the CmdSet of that Object/Character and
|
||||
outgoing messages (such as the result of a `look`) will be passed back only to that puppeting
|
||||
Session. If another Session tries to puppet the same Character, the old Session will automatically
|
||||
un-puppet it. From the player's perspective, this will mean that they can open separate game clients
|
||||
and play a different Character in each using one game account.
|
||||
This mode will *not* auto-create a Character and *not* auto-puppet on login like in modes 0 and 1.
|
||||
Instead it changes how the account-cmdsets's `OOCLook` command works so as to show a simple
|
||||
'character select' menu.
|
||||
* `MULTISESSION_MODE=3`: Many sessions per account *and* character. This is the full multi-puppeting
|
||||
mode, where multiple sessions may not only connect to the player account but multiple sessions may
|
||||
also puppet a single character at the same time. From the user's perspective it means one can open
|
||||
multiple client windows, some for controlling different Characters and some that share a Character's
|
||||
input/output like in mode 1. This mode otherwise works the same as mode 2.
|
||||
|
||||
> Note that even if multiple Sessions puppet one Character, there is only ever one instance of that
|
||||
Character.
|
||||
|
||||
## Returning data to the session
|
||||
|
||||
When you use `msg()` to return data to a user, the object on which you call the `msg()` matters. The
|
||||
`MULTISESSION_MODE` also matters, especially if greater than 1.
|
||||
|
||||
For example, if you use `account.msg("hello")` there is no way for evennia to know which session it
|
||||
should send the greeting to. In this case it will send it to all sessions. If you want a specific
|
||||
session you need to supply its session to the `msg` call (`account.msg("hello",
|
||||
session=mysession)`).
|
||||
|
||||
On the other hand, if you call the `msg()` message on a puppeted object, like
|
||||
`character.msg("hello")`, the character already knows the session that controls it - it will
|
||||
cleverly auto-add this for you (you can specify a different session if you specifically want to send
|
||||
stuff to another session).
|
||||
|
||||
Finally, there is a wrapper for `msg()` on all command classes: `command.msg()`. This will
|
||||
transparently detect which session was triggering the command (if any) and redirects to that session
|
||||
(this is most often what you want). If you are having trouble redirecting to a given session,
|
||||
`command.msg()` is often the safest bet.
|
||||
|
||||
You can get the `session` in two main ways:
|
||||
* [Accounts](./Accounts.md) and [Objects](./Objects.md) (including Characters) have a `sessions` property.
|
||||
This is a *handler* that tracks all Sessions attached to or puppeting them. Use e.g.
|
||||
`accounts.sessions.get()` to get a list of Sessions attached to that entity.
|
||||
* A Command instance has a `session` property that always points back to the Session that triggered
|
||||
it (it's always a single one). It will be `None` if no session is involved, like when a mob or
|
||||
script triggers the Command.
|
||||
|
||||
## Customizing the Session object
|
||||
|
||||
When would one want to customize the Session object? Consider for example a character creation
|
||||
system: You might decide to keep this on the out-of-character level. This would mean that you create
|
||||
the character at the end of some sort of menu choice. The actual char-create cmdset would then
|
||||
normally be put on the account. This works fine as long as you are `MULTISESSION_MODE` below 2.
|
||||
For higher modes, replacing the Account cmdset will affect *all* your connected sessions, also those
|
||||
not involved in character creation. In this case you want to instead put the char-create cmdset on
|
||||
the Session level - then all other sessions will keep working normally despite you creating a new
|
||||
character in one of them.
|
||||
|
||||
By default, the session object gets the `commands.default_cmdsets.UnloggedinCmdSet` when the user
|
||||
first connects. Once the session is authenticated it has *no* default sets. To add a "logged-in"
|
||||
cmdset to the Session, give the path to the cmdset class with `settings.CMDSET_SESSION`. This set
|
||||
will then henceforth always be present as soon as the account logs in.
|
||||
|
||||
To customize further you can completely override the Session with your own subclass. To replace the
|
||||
default Session class, change `settings.SERVER_SESSION_CLASS` to point to your custom class. This is
|
||||
a dangerous practice and errors can easily make your game unplayable. Make sure to take heed of the
|
||||
[original](https://github.com/evennia/evennia/blob/master/evennia/server/session.py) and make your
|
||||
changes carefully.
|
||||
|
||||
## Portal and Server Sessions
|
||||
|
||||
*Note: This is considered an advanced topic. You don't need to know this on a first read-through.*
|
||||
|
||||
Evennia is split into two parts, the [Portal and the Server](./Portal-And-Server.md). Each side tracks
|
||||
its own Sessions, syncing them to each other.
|
||||
|
||||
The "Session" we normally refer to is actually the `ServerSession`. Its counter-part on the Portal
|
||||
side is the `PortalSession`. Whereas the server sessions deal with game states, the portal session
|
||||
deals with details of the connection-protocol itself. The two are also acting as backups of critical
|
||||
data such as when the server reboots.
|
||||
|
||||
New Account connections are listened for and handled by the Portal using the [protocols](Portal-And-
|
||||
Server) it understands (such as telnet, ssh, webclient etc). When a new connection is established, a
|
||||
`PortalSession` is created on the Portal side. This session object looks different depending on
|
||||
which protocol is used to connect, but all still have a minimum set of attributes that are generic
|
||||
to all
|
||||
sessions.
|
||||
|
||||
These common properties are piped from the Portal, through the AMP connection, to the Server, which
|
||||
is now informed a new connection has been established. On the Server side, a `ServerSession` object
|
||||
is created to represent this. There is only one type of `ServerSession`; It looks the same
|
||||
regardless of how the Account connects.
|
||||
|
||||
From now on, there is a one-to-one match between the `ServerSession` on one side of the AMP
|
||||
connection and the `PortalSession` on the other. Data arriving to the Portal Session is sent on to
|
||||
its mirror Server session and vice versa.
|
||||
|
||||
During certain situations, the portal- and server-side sessions are
|
||||
"synced" with each other:
|
||||
- The Player closes their client, killing the Portal Session. The Portal syncs with the Server to
|
||||
make sure the corresponding Server Session is also deleted.
|
||||
- The Player quits from inside the game, killing the Server Session. The Server then syncs with the
|
||||
Portal to make sure to close the Portal connection cleanly.
|
||||
- The Server is rebooted/reset/shutdown - The Server Sessions are copied over ("saved") to the
|
||||
Portal side. When the Server comes back up, this data is returned by the Portal so the two are again
|
||||
in sync. This way an Account's login status and other connection-critical things can survive a
|
||||
server reboot (assuming the Portal is not stopped at the same time, obviously).
|
||||
|
||||
## Sessionhandlers
|
||||
|
||||
Both the Portal and Server each have a *sessionhandler* to manage the connections. These handlers
|
||||
are global entities contain all methods for relaying data across the AMP bridge. All types of
|
||||
Sessions hold a reference to their respective Sessionhandler (the property is called
|
||||
`sessionhandler`) so they can relay data. See [protocols](../Concepts/Custom-Protocols.md) for more info
|
||||
on building new protocols.
|
||||
|
||||
To get all Sessions in the game (i.e. all currently connected clients), you access the server-side
|
||||
Session handler, which you get by
|
||||
```
|
||||
from evennia.server.sessionhandler import SESSION_HANDLER
|
||||
```
|
||||
> Note: The `SESSION_HANDLER` singleton has an older alias `SESSIONS` that is commonly seen in
|
||||
various places as well.
|
||||
|
||||
See the
|
||||
[sessionhandler.py](https://github.com/evennia/evennia/blob/master/evennia/server/sessionhandler.py)
|
||||
module for details on the capabilities of the `ServerSessionHandler`.
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
# Signals
|
||||
|
||||
|
||||
_This is feature available from evennia 0.9 and onward_.
|
||||
|
||||
There are multiple ways for you to plug in your own functionality into Evennia.
|
||||
The most common way to do so is through *hooks* - methods on typeclasses that
|
||||
gets called at particular events. Hooks are great when you want a game entity
|
||||
to behave a certain way when something happens to it. _Signals_ complements
|
||||
hooks for cases when you want to easily attach new functionality without
|
||||
overriding things on the typeclass.
|
||||
|
||||
When certain events happen in Evennia, a _Signal_ is fired. The idea is that
|
||||
you can "attach" any number of event-handlers to these signals. You can attach
|
||||
any number of handlers and they'll all fire whenever any entity triggers the
|
||||
signal.
|
||||
|
||||
Evennia uses the [Django Signal system](https://docs.djangoproject.com/en/2.2/topics/signals/).
|
||||
|
||||
|
||||
## Attaching a handler to a signal
|
||||
|
||||
First you create your handler
|
||||
|
||||
```python
|
||||
|
||||
def myhandler(sender, **kwargs):
|
||||
# do stuff
|
||||
|
||||
```
|
||||
|
||||
The `**kwargs` is mandatory. Then you attach it to the signal of your choice:
|
||||
|
||||
```python
|
||||
from evennia.server import signals
|
||||
|
||||
signals.SIGNAL_OBJECT_POST_CREATE.connect(myhandler)
|
||||
|
||||
```
|
||||
|
||||
This particular signal fires after (post) an Account has connected to the game.
|
||||
When that happens, `myhandler` will fire with the `sender` being the Account that just connected.
|
||||
|
||||
If you want to respond only to the effects of a specific entity you can do so
|
||||
like this:
|
||||
|
||||
```python
|
||||
from evennia import search_account
|
||||
from evennia import signals
|
||||
|
||||
account = search_account("foo")[0]
|
||||
signals.SIGNAL_ACCOUNT_POST_CONNECT.connect(myhandler, account)
|
||||
```
|
||||
|
||||
## Available signals
|
||||
|
||||
All signals (including some django-specific defaults) are available in the module
|
||||
`evennia.server.signals`
|
||||
(with a shortcut `evennia.signals`). Signals are named by the sender type. So `SIGNAL_ACCOUNT_*`
|
||||
returns
|
||||
`Account` instances as senders, `SIGNAL_OBJECT_*` returns `Object`s etc. Extra keywords (kwargs)
|
||||
should
|
||||
be extracted from the `**kwargs` dict in the signal handler.
|
||||
|
||||
- `SIGNAL_ACCOUNT_POST_CREATE` - this is triggered at the very end of `Account.create()`. Note that
|
||||
calling `evennia.create.create_account` (which is called internally by `Account.create`) will
|
||||
*not*
|
||||
trigger this signal. This is because using `Account.create()` is expected to be the most commonly
|
||||
used way for users to themselves create accounts during login. It passes and extra kwarg `ip` with
|
||||
the client IP of the connecting account.
|
||||
- `SIGNAL_ACCOUNT_POST_LOGIN` - this will always fire when the account has authenticated. Sends
|
||||
extra kwarg `session` with the new [Session](./Sessions.md) object involved.
|
||||
- `SIGNAL_ACCCOUNT_POST_FIRST_LOGIN` - this fires just before `SIGNAL_ACCOUNT_POST_LOGIN` but only
|
||||
if
|
||||
this is the *first* connection done (that is, if there are no previous sessions connected). Also
|
||||
passes the `session` along as a kwarg.
|
||||
- `SIGNAL_ACCOUNT_POST_LOGIN_FAIL` - sent when someone tried to log into an account by failed.
|
||||
Passes
|
||||
the `session` as an extra kwarg.
|
||||
- `SIGNAL_ACCOUNT_POST_LOGOUT` - always fires when an account logs off, no matter if other sessions
|
||||
remain or not. Passes the disconnecting `session` along as a kwarg.
|
||||
- `SIGNAL_ACCOUNT_POST_LAST_LOGOUT` - fires before `SIGNAL_ACCOUNT_POST_LOGOUT`, but only if this is
|
||||
the *last* Session to disconnect for that account. Passes the `session` as a kwarg.
|
||||
- `SIGNAL_OBJECT_POST_PUPPET` - fires when an account puppets this object. Extra kwargs `session`
|
||||
and `account` represent the puppeting entities.
|
||||
`SIGNAL_OBJECT_POST_UNPUPPET` - fires when the sending object is unpuppeted. Extra kwargs are
|
||||
`session` and `account`.
|
||||
- `SIGNAL_ACCOUNT_POST_RENAME` - triggered by the setting of `Account.username`. Passes extra
|
||||
kwargs `old_name`, `new_name`.
|
||||
- `SIGNAL_TYPED_OBJECT_POST_RENAME` - triggered when any Typeclassed entity's `key` is changed.
|
||||
Extra
|
||||
kwargs passed are `old_key` and `new_key`.
|
||||
- `SIGNAL_SCRIPT_POST_CREATE` - fires when a script is first created, after any hooks.
|
||||
- `SIGNAL_CHANNEL_POST_CREATE` - fires when a Channel is first created, after any hooks.
|
||||
- `SIGNAL_HELPENTRY_POST_CREATE` - fires when a help entry is first created.
|
||||
|
||||
The `evennia.signals` module also gives you conveneient access to the default Django signals (these
|
||||
use a
|
||||
different naming convention).
|
||||
|
||||
- `pre_save` - fired when any database entitiy's `.save` method fires, before any saving has
|
||||
happened.
|
||||
- `post_save` - fires after saving a database entity.
|
||||
- `pre_delete` - fires just before a database entity is deleted.
|
||||
- `post_delete` - fires after a database entity was deleted.
|
||||
- `pre_init` - fires before a typeclass' `__init__` method (which in turn
|
||||
happens before the `at_init` hook fires).
|
||||
- `post_init` - triggers at the end of `__init__` (still before the `at_init` hook).
|
||||
|
||||
These are highly specialized Django signals that are unlikely to be useful to most users. But
|
||||
they are included here for completeness.
|
||||
|
||||
- `m2m_changed` - fires after a Many-to-Many field (like `db_attributes`) changes.
|
||||
- `pre_migrate` - fires before database migration starts with `evennia migrate`.
|
||||
- `post_migrate` - fires after database migration finished.
|
||||
- `request_started` - sent when HTTP request begins.
|
||||
- `request_finished` - sent when HTTP request ends.
|
||||
- `settings_changed` - sent when changing settings due to `@override_settings`
|
||||
decorator (only relevant for unit testing)
|
||||
- `template_rendered` - sent when test system renders http template (only useful for unit tests).
|
||||
- `connection_creation` - sent when making initial connection to database.
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
# Tags
|
||||
|
||||
```{code-block}
|
||||
:caption: In game
|
||||
> tag obj = tagname
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In code, using .tags (TagHandler)
|
||||
|
||||
obj.tags.add("mytag", category="foo")
|
||||
obj.tags.get("mytag", category="foo")
|
||||
```
|
||||
|
||||
```{code-block} python
|
||||
:caption: In code, using TagProperty (auto-assign tag to all instances of the class)
|
||||
|
||||
from evennia import DefaultObject
|
||||
from evennia import TagProperty
|
||||
class Sword(DefaultObject):
|
||||
can_be_wielded = TagProperty(category='combat')
|
||||
has_sharp_edge = TagProperty(category='combat')
|
||||
|
||||
```
|
||||
|
||||
_Tags_ are short text lables one can 'hang' on objects in order to organize, group and quickly find out their properties. An Evennia entity can be tagged by any number of tags. They are more efficient than [Attributes](./Attributes.md) since on the database-side, Tags are _shared_ between all objects with that particular tag. A tag does not carry a value in itself; it either sits on the entity
|
||||
|
||||
Above, the tags inform us that the `Sword` is both sharp and can be wielded. If that's all they do, they could just be a normal Python flag. When tags become important is if there are a lot of objects with different combinations of tags. Maybe you have a magical spell that dulls _all_ sharp-edged objects in the castle - whether sword, dagger, spear or kitchen knife! You can then just grab all objects with the `has_sharp_edge` tag.
|
||||
Another example would be a weather script affecting all rooms tagged as `outdoors` or finding all characters tagged with `belongs_to_fighter_guild`.
|
||||
|
||||
In Evennia, Tags are technically also used to implement `Aliases` (alternative names for objects) and `Permissions` (simple strings for [Locks](./Locks.md) to check for).
|
||||
|
||||
|
||||
## Properties of Tags (and Aliases and Permissions)
|
||||
|
||||
Tags are *unique*. This means that there is only ever one Tag object with a given key and category.
|
||||
|
||||
> Not specifying a category (default) gives the tag a category of `None`, which is also considered a
|
||||
unique key + category combination.
|
||||
|
||||
When Tags are assigned to game entities, these entities are actually sharing the same Tag. This
|
||||
means that Tags are not suitable for storing information about a single object - use an
|
||||
[Attribute](./Attributes.md) for this instead. Tags are a lot more limited than Attributes but this also
|
||||
makes them very quick to lookup in the database - this is the whole point.
|
||||
|
||||
Tags have the following properties, stored in the database:
|
||||
|
||||
- **key** - the name of the Tag. This is the main property to search for when looking up a Tag.
|
||||
- **category** - this category allows for retrieving only specific subsets of tags used for
|
||||
different purposes. You could have one category of tags for "zones", another for "outdoor
|
||||
locations", for example. If not given, the category will be `None`, which is also considered a
|
||||
separate, default, category.
|
||||
- **data** - this is an optional text field with information about the tag. Remember that Tags are
|
||||
shared between entities, so this field cannot hold any object-specific information. Usually it would
|
||||
be used to hold info about the group of entities the Tag is tagging - possibly used for contextual
|
||||
help like a tool tip. It is not used by default.
|
||||
|
||||
There are also two special properties. These should usually not need to be changed or set, it is
|
||||
used internally by Evennia to implement various other uses it makes of the `Tag` object:
|
||||
- **model** - this holds a *natural-key* description of the model object that this tag deals with,
|
||||
on the form *application.modelclass*, for example `objects.objectdb`. It used by the TagHandler of
|
||||
each entity type for correctly storing the data behind the scenes.
|
||||
- **tagtype** - this is a "top-level category" of sorts for the inbuilt children of Tags, namely
|
||||
*Aliases* and *Permissions*. The Taghandlers using this special field are especially intended to
|
||||
free up the *category* property for any use you desire.
|
||||
|
||||
## Adding/Removing Tags
|
||||
|
||||
You can tag any *typeclassed* object, namely [Objects](./Objects.md), [Accounts](./Accounts.md),
|
||||
[Scripts](./Scripts.md) and [Channels](./Channels.md). General tags are added by the *Taghandler*. The
|
||||
tag handler is accessed as a property `tags` on the relevant entity:
|
||||
|
||||
```python
|
||||
mychair.tags.add("furniture")
|
||||
mychair.tags.add("furniture", category="luxurious")
|
||||
myroom.tags.add("dungeon#01")
|
||||
myscript.tags.add("weather", category="climate")
|
||||
myaccount.tags.add("guestaccount")
|
||||
|
||||
mychair.tags.all() # returns a list of Tags
|
||||
mychair.tags.remove("furniture")
|
||||
mychair.tags.clear()
|
||||
```
|
||||
|
||||
Adding a new tag will either create a new Tag or re-use an already existing one. Note that there are
|
||||
_two_ "furniture" tags, one with a `None` category, and one with the "luxurious" category.
|
||||
|
||||
When using `remove`, the `Tag` is not deleted but are just disconnected from the tagged object. This
|
||||
makes for very quick operations. The `clear` method removes (disconnects) all Tags from the object.
|
||||
You can also use the default `@tag` command:
|
||||
|
||||
@tag mychair = furniture
|
||||
|
||||
This tags the chair with a 'furniture' Tag (the one with a `None` category).
|
||||
|
||||
## Searching for objects with a given tag
|
||||
|
||||
Usually tags are used as a quick way to find tagged database entities. You can retrieve all objects
|
||||
with a given Tag like this in code:
|
||||
|
||||
```python
|
||||
import evennia
|
||||
|
||||
# all methods return Querysets
|
||||
|
||||
# search for objects
|
||||
objs = evennia.search_tag("furniture")
|
||||
objs2 = evennia.search_tag("furniture", category="luxurious")
|
||||
dungeon = evennia.search_tag("dungeon#01")
|
||||
forest_rooms = evennia.search_tag(category="forest")
|
||||
forest_meadows = evennia.search_tag("meadow", category="forest")
|
||||
magic_meadows = evennia.search_tag("meadow", category="magical")
|
||||
|
||||
# search for scripts
|
||||
weather = evennia.search_tag_script("weather")
|
||||
climates = evennia.search_tag_script(category="climate")
|
||||
|
||||
# search for accounts
|
||||
accounts = evennia.search_tag_account("guestaccount")
|
||||
```
|
||||
|
||||
> Note that searching for just "furniture" will only return the objects tagged with the "furniture"
|
||||
tag that
|
||||
has a category of `None`. We must explicitly give the category to get the "luxurious" furniture.
|
||||
|
||||
Using any of the `search_tag` variants will all return [Django
|
||||
Querysets](https://docs.djangoproject.com/en/2.1/ref/models/querysets/), including if you only have
|
||||
one match. You can treat querysets as lists and iterate over them, or continue building search
|
||||
queries with them.
|
||||
|
||||
Remember when searching that not setting a category means setting it to `None` - this does *not*
|
||||
mean that category is undefined, rather `None` is considered the default, unnamed category.
|
||||
|
||||
```python
|
||||
import evennia
|
||||
|
||||
myobj1.tags.add("foo") # implies category=None
|
||||
myobj2.tags.add("foo", category="bar")
|
||||
|
||||
# this returns a queryset with *only* myobj1
|
||||
objs = evennia.search_tag("foo")
|
||||
|
||||
# these return a queryset with *only* myobj2
|
||||
objs = evennia.search_tag("foo", category="bar")
|
||||
# or
|
||||
objs = evennia.search_tag(category="bar")
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
There is also an in-game command that deals with assigning and using ([Object-](./Objects.md)) tags:
|
||||
|
||||
@tag/search furniture
|
||||
|
||||
## Using Aliases and Permissions
|
||||
|
||||
Aliases and Permissions are implemented using normal TagHandlers that simply save Tags with a
|
||||
different `tagtype`. These handlers are named `aliases` and `permissions` on all Objects. They are
|
||||
used in the same way as Tags above:
|
||||
|
||||
```python
|
||||
boy.aliases.add("rascal")
|
||||
boy.permissions.add("Builders")
|
||||
boy.permissions.remove("Builders")
|
||||
|
||||
all_aliases = boy.aliases.all()
|
||||
```
|
||||
|
||||
and so on. Similarly to how `@tag` works in-game, there is also the `@perm` command for assigning
|
||||
permissions and `@alias` command for aliases.
|
||||
|
||||
## Assorted notes
|
||||
|
||||
Generally, tags are enough on their own for grouping objects. Having no tag `category` is perfectly
|
||||
fine and the normal operation. Simply adding a new Tag for grouping objects is often better than
|
||||
making a new category. So think hard before deciding you really need to categorize your Tags.
|
||||
|
||||
That said, tag categories can be useful if you build some game system that uses tags. You can then
|
||||
use tag categories to make sure to separate tags created with this system from any other tags
|
||||
created elsewhere. You can then supply custom search methods that *only* find objects tagged with
|
||||
tags of that category. An example of this
|
||||
is found in the [Zone tutorial](../Concepts/Zones.md).
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
# TickerHandler
|
||||
|
||||
|
||||
One way to implement a dynamic MUD is by using "tickers", also known as "heartbeats". A ticker is a
|
||||
timer that fires ("ticks") at a given interval. The tick triggers updates in various game systems.
|
||||
|
||||
## About Tickers
|
||||
|
||||
Tickers are very common or even unavoidable in other mud code bases. Certain code bases are even
|
||||
hard-coded to rely on the concept of the global 'tick'. Evennia has no such notion - the decision to
|
||||
use tickers is very much up to the need of your game and which requirements you have. The "ticker
|
||||
recipe" is just one way of cranking the wheels.
|
||||
|
||||
The most fine-grained way to manage the flow of time is of course to use [Scripts](./Scripts.md). Many
|
||||
types of operations (weather being the classic example) are however done on multiple objects in the
|
||||
same way at regular intervals, and for this, storing separate Scripts on each object is inefficient.
|
||||
The way to do this is to use a ticker with a "subscription model" - let objects sign up to be
|
||||
triggered at the same interval, unsubscribing when the updating is no longer desired.
|
||||
|
||||
Evennia offers an optimized implementation of the subscription model - the *TickerHandler*. This is
|
||||
a singleton global handler reachable from `evennia.TICKER_HANDLER`. You can assign any *callable* (a
|
||||
function or, more commonly, a method on a database object) to this handler. The TickerHandler will
|
||||
then call this callable at an interval you specify, and with the arguments you supply when adding
|
||||
it. This continues until the callable un-subscribes from the ticker. The handler survives a reboot
|
||||
and is highly optimized in resource usage.
|
||||
|
||||
Here is an example of importing `TICKER_HANDLER` and using it:
|
||||
|
||||
```python
|
||||
# we assume that obj has a hook "at_tick" defined on itself
|
||||
from evennia import TICKER_HANDLER as tickerhandler
|
||||
|
||||
tickerhandler.add(20, obj.at_tick)
|
||||
```
|
||||
|
||||
That's it - from now on, `obj.at_tick()` will be called every 20 seconds.
|
||||
|
||||
You can also import function and tick that:
|
||||
|
||||
```python
|
||||
from evennia import TICKER_HANDLER as tickerhandler
|
||||
from mymodule import myfunc
|
||||
|
||||
tickerhandler.add(30, myfunc)
|
||||
```
|
||||
|
||||
Removing (stopping) the ticker works as expected:
|
||||
|
||||
```python
|
||||
tickerhandler.remove(20, obj.at_tick)
|
||||
tickerhandler.remove(30, myfunc)
|
||||
```
|
||||
|
||||
Note that you have to also supply `interval` to identify which subscription to remove. This is
|
||||
because the TickerHandler maintains a pool of tickers and a given callable can subscribe to be
|
||||
ticked at any number of different intervals.
|
||||
|
||||
The full definition of the `tickerhandler.add` method is
|
||||
|
||||
```python
|
||||
tickerhandler.add(interval, callback,
|
||||
idstring="", persistent=True, *args, **kwargs)
|
||||
```
|
||||
|
||||
Here `*args` and `**kwargs` will be passed to `callback` every `interval` seconds. If `persistent`
|
||||
is `False`, this subscription will not survive a server reload.
|
||||
|
||||
Tickers are identified and stored by making a key of the callable itself, the ticker-interval, the
|
||||
`persistent` flag and the `idstring` (the latter being an empty string when not given explicitly).
|
||||
|
||||
Since the arguments are not included in the ticker's identification, the `idstring` must be used to
|
||||
have a specific callback triggered multiple times on the same interval but with different arguments:
|
||||
|
||||
```python
|
||||
tickerhandler.add(10, obj.update, "ticker1", True, 1, 2, 3)
|
||||
tickerhandler.add(10, obj.update, "ticker2", True, 4, 5)
|
||||
```
|
||||
|
||||
> Note that, when we want to send arguments to our callback within a ticker handler, we need to
|
||||
specify `idstring` and `persistent` before, unless we call our arguments as keywords, which would
|
||||
often be more readable:
|
||||
|
||||
```python
|
||||
tickerhandler.add(10, obj.update, caller=self, value=118)
|
||||
```
|
||||
|
||||
If you add a ticker with exactly the same combination of callback, interval and idstring, it will
|
||||
overload the existing ticker. This identification is also crucial for later removing (stopping) the
|
||||
subscription:
|
||||
|
||||
```python
|
||||
tickerhandler.remove(10, obj.update, idstring="ticker1")
|
||||
tickerhandler.remove(10, obj.update, idstring="ticker2")
|
||||
```
|
||||
|
||||
The `callable` can be on any form as long as it accepts the arguments you give to send to it in
|
||||
`TickerHandler.add`.
|
||||
|
||||
> Note that everything you supply to the TickerHandler will need to be pickled at some point to be
|
||||
saved into the database. Most of the time the handler will correctly store things like database
|
||||
objects, but the same restrictions as for [Attributes](./Attributes.md) apply to what the TickerHandler
|
||||
may store.
|
||||
|
||||
When testing, you can stop all tickers in the entire game with `tickerhandler.clear()`. You can also
|
||||
view the currently subscribed objects with `tickerhandler.all()`.
|
||||
|
||||
See the [Weather Tutorial](../Howtos/Weather-Tutorial.md) for an example of using the TickerHandler.
|
||||
|
||||
### When *not* to use TickerHandler
|
||||
|
||||
Using the TickerHandler may sound very useful but it is important to consider when not to use it.
|
||||
Even if you are used to habitually relying on tickers for everything in other code bases, stop and
|
||||
think about what you really need it for. This is the main point:
|
||||
|
||||
> You should *never* use a ticker to catch *changes*.
|
||||
|
||||
Think about it - you might have to run the ticker every second to react to the change fast enough.
|
||||
Most likely nothing will have changed at a given moment. So you are doing pointless calls (since
|
||||
skipping the call gives the same result as doing it). Making sure nothing's changed might even be
|
||||
computationally expensive depending on the complexity of your system. Not to mention that you might
|
||||
need to run the check *on every object in the database*. Every second. Just to maintain status quo
|
||||
...
|
||||
|
||||
Rather than checking over and over on the off-chance that something changed, consider a more
|
||||
proactive approach. Could you implement your rarely changing system to *itself* report when its
|
||||
status changes? It's almost always much cheaper/efficient if you can do things "on demand". Evennia
|
||||
itself uses hook methods for this very reason.
|
||||
|
||||
So, if you consider a ticker that will fire very often but which you expect to have no effect 99% of
|
||||
the time, consider handling things things some other way. A self-reporting on-demand solution is
|
||||
usually cheaper also for fast-updating properties. Also remember that some things may not need to be
|
||||
updated until someone actually is examining or using them - any interim changes happening up to that
|
||||
moment are pointless waste of computing time.
|
||||
|
||||
The main reason for needing a ticker is when you want things to happen to multiple objects at the
|
||||
same time without input from something else.
|
||||
|
|
@ -1,352 +0,0 @@
|
|||
# Typeclasses
|
||||
|
||||
*Typeclasses* form the core of Evennia's data storage. It allows Evennia to represent any number of
|
||||
different game entities as Python classes, without having to modify the database schema for every
|
||||
new type.
|
||||
|
||||
In Evennia the most important game entities, [Accounts](./Accounts.md), [Objects](./Objects.md),
|
||||
[Scripts](./Scripts.md) and [Channels](./Channels.md) are all Python classes inheriting, at
|
||||
varying distance, from `evennia.typeclasses.models.TypedObject`. In the documentation we refer to
|
||||
these objects as being "typeclassed" or even "being a typeclass".
|
||||
|
||||
This is how the inheritance looks for the typeclasses in Evennia:
|
||||
|
||||
```
|
||||
TypedObject
|
||||
_________________|_________________________________
|
||||
| | | |
|
||||
1: AccountDB ObjectDB ScriptDB ChannelDB
|
||||
| | | |
|
||||
2: DefaultAccount DefaultObject DefaultScript DefaultChannel
|
||||
| DefaultCharacter | |
|
||||
| DefaultRoom | |
|
||||
| DefaultExit | |
|
||||
| | | |
|
||||
3: Account Object Script Channel
|
||||
Character
|
||||
Room
|
||||
Exit
|
||||
```
|
||||
|
||||
- **Level 1** above is the "database model" level. This describes the database tables and fields
|
||||
(this is technically a [Django model](https://docs.djangoproject.com/en/2.2/topics/db/models/)).
|
||||
- **Level 2** is where we find Evennia's default implementations of the various game entities, on
|
||||
top of the database. These classes define all the hook methods that Evennia calls in various
|
||||
situations. `DefaultObject` is a little special since it's the parent for `DefaultCharacter`,
|
||||
`DefaultRoom` and `DefaultExit`. They are all grouped under level 2 because they all represents
|
||||
defaults to build from.
|
||||
- **Level 3**, finally, holds empty template classes created in your game directory. This is the
|
||||
level you are meant to modify and tweak as you please, overloading the defaults as befits your game.
|
||||
The templates inherit directly from their defaults, so `Object` inherits from `DefaultObject` and
|
||||
`Room` inherits from `DefaultRoom`.
|
||||
|
||||
The `typeclass/list` command will provide a list of all typeclasses known to
|
||||
Evennia. This can be useful for getting a feel for what is available. Note
|
||||
however that if you add a new module with a class in it but do not import that
|
||||
module from anywhere, the `typeclass/list` will not find it. To make it known
|
||||
to Evennia you must import that module from somewhere.
|
||||
|
||||
|
||||
## Difference between typeclasses and classes
|
||||
|
||||
All Evennia classes inheriting from class in the table above share one important feature and two
|
||||
important limitations. This is why we don't simply call them "classes" but "typeclasses".
|
||||
|
||||
1. A typeclass can save itself to the database. This means that some properties (actually not that
|
||||
many) on the class actually represents database fields and can only hold very specific data types.
|
||||
This is detailed [below](./Typeclasses.md#about-typeclass-properties).
|
||||
1. Due to its connection to the database, the typeclass' name must be *unique* across the _entire_
|
||||
server namespace. That is, there must never be two same-named classes defined anywhere. So the below
|
||||
code would give an error (since `DefaultObject` is now globally found both in this module and in the
|
||||
default library):
|
||||
|
||||
```python
|
||||
from evennia import DefaultObject as BaseObject
|
||||
class DefaultObject(BaseObject):
|
||||
pass
|
||||
```
|
||||
|
||||
1. A typeclass' `__init__` method should normally not be overloaded. This has mostly to do with the
|
||||
fact that the `__init__` method is not called in a predictable way. Instead Evennia suggest you use
|
||||
the `at_*_creation` hooks (like `at_object_creation` for Objects) for setting things the very first
|
||||
time the typeclass is saved to the database or the `at_init` hook which is called every time the
|
||||
object is cached to memory. If you know what you are doing and want to use `__init__`, it *must*
|
||||
both accept arbitrary keyword arguments and use `super` to call its parent::
|
||||
|
||||
```python
|
||||
def __init__(self, **kwargs):
|
||||
# my content
|
||||
super().__init__(**kwargs)
|
||||
# my content
|
||||
```
|
||||
|
||||
Apart from this, a typeclass works like any normal Python class and you can
|
||||
treat it as such.
|
||||
|
||||
|
||||
## Creating a new typeclass
|
||||
|
||||
It's easy to work with Typeclasses. Either you use an existing typeclass or you create a new Python
|
||||
class inheriting from an existing typeclass. Here is an example of creating a new type of Object:
|
||||
|
||||
```python
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Furniture(DefaultObject):
|
||||
# this defines what 'furniture' is, like
|
||||
# storing who sits on it or something.
|
||||
pass
|
||||
|
||||
```
|
||||
|
||||
You can now create a new `Furniture` object in two ways. First (and usually not the most
|
||||
convenient) way is to create an instance of the class and then save it manually to the database:
|
||||
|
||||
```python
|
||||
chair = Furniture(db_key="Chair")
|
||||
chair.save()
|
||||
|
||||
```
|
||||
|
||||
To use this you must give the database field names as keywords to the call. Which are available
|
||||
depends on the entity you are creating, but all start with `db_*` in Evennia. This is a method you
|
||||
may be familiar with if you know Django from before.
|
||||
|
||||
It is recommended that you instead use the `create_*` functions to create typeclassed entities:
|
||||
|
||||
|
||||
```python
|
||||
from evennia import create_object
|
||||
|
||||
chair = create_object(Furniture, key="Chair")
|
||||
# or (if your typeclass is in a module furniture.py)
|
||||
chair = create_object("furniture.Furniture", key="Chair")
|
||||
```
|
||||
|
||||
The `create_object` (`create_account`, `create_script` etc) takes the typeclass as its first
|
||||
argument; this can both be the actual class or the python path to the typeclass as found under your
|
||||
game directory. So if your `Furniture` typeclass sits in `mygame/typeclasses/furniture.py`, you
|
||||
could point to it as `typeclasses.furniture.Furniture`. Since Evennia will itself look in
|
||||
`mygame/typeclasses`, you can shorten this even further to just `furniture.Furniture`. The create-
|
||||
functions take a lot of extra keywords allowing you to set things like [Attributes](./Attributes.md) and
|
||||
[Tags](./Tags.md) all in one go. These keywords don't use the `db_*` prefix. This will also automatically
|
||||
save the new instance to the database, so you don't need to call `save()` explicitly.
|
||||
|
||||
## About typeclass properties
|
||||
|
||||
An example of a database field is `db_key`. This stores the "name" of the entity you are modifying
|
||||
and can thus only hold a string. This is one way of making sure to update the `db_key`:
|
||||
|
||||
```python
|
||||
chair.db_key = "Table"
|
||||
chair.save()
|
||||
|
||||
print(chair.db_key)
|
||||
<<< Table
|
||||
```
|
||||
|
||||
That is, we change the chair object to have the `db_key` "Table", then save this to the database.
|
||||
However, you almost never do things this way; Evennia defines property wrappers for all the database
|
||||
fields. These are named the same as the field, but without the `db_` part:
|
||||
|
||||
```python
|
||||
chair.key = "Table"
|
||||
|
||||
print(chair.key)
|
||||
<<< Table
|
||||
|
||||
```
|
||||
|
||||
The `key` wrapper is not only shorter to write, it will make sure to save the field for you, and
|
||||
does so more efficiently by levering sql update mechanics under the hood. So whereas it is good to
|
||||
be aware that the field is named `db_key` you should use `key` as much as you can.
|
||||
|
||||
Each typeclass entity has some unique fields relevant to that type. But all also share the
|
||||
following fields (the wrapper name without `db_` is given):
|
||||
|
||||
- `key` (str): The main identifier for the entity, like "Rose", "myscript" or "Paul". `name` is an
|
||||
alias.
|
||||
- `date_created` (datetime): Time stamp when this object was created.
|
||||
- `typeclass_path` (str): A python path pointing to the location of this (type)class
|
||||
|
||||
There is one special field that doesn't use the `db_` prefix (it's defined by Django):
|
||||
|
||||
- `id` (int): the database id (database ref) of the object. This is an ever-increasing, unique
|
||||
integer. It can also be accessed as `dbid` (database ID) or `pk` (primary key). The `dbref` property
|
||||
returns the string form "#id".
|
||||
|
||||
The typeclassed entity has several common handlers:
|
||||
|
||||
- `tags` - the [TagHandler](./Tags.md) that handles tagging. Use `tags.add()` , `tags.get()` etc.
|
||||
- `locks` - the [LockHandler](./Locks.md) that manages access restrictions. Use `locks.add()`,
|
||||
`locks.get()` etc.
|
||||
- `attributes` - the [AttributeHandler](./Attributes.md) that manages Attributes on the object. Use
|
||||
`attributes.add()`
|
||||
etc.
|
||||
- `db` (DataBase) - a shortcut property to the AttributeHandler; allowing `obj.db.attrname = value`
|
||||
- `nattributes` - the [Non-persistent AttributeHandler](./Attributes.md) for attributes not saved in the
|
||||
database.
|
||||
- `ndb` (NotDataBase) - a shortcut property to the Non-peristent AttributeHandler. Allows
|
||||
`obj.ndb.attrname = value`
|
||||
|
||||
|
||||
Each of the typeclassed entities then extend this list with their own properties. Go to the
|
||||
respective pages for [Objects](./Objects.md), [Scripts](./Scripts.md), [Accounts](./Accounts.md) and
|
||||
[Channels](./Channels.md) for more info. It's also recommended that you explore the available
|
||||
entities using [Evennia's flat API](../Evennia-API.md) to explore which properties and methods they have
|
||||
available.
|
||||
|
||||
## Overloading hooks
|
||||
|
||||
The way to customize typeclasses is usually to overload *hook methods* on them. Hooks are methods
|
||||
that Evennia call in various situations. An example is the `at_object_creation` hook on `Objects`,
|
||||
which is only called once, the very first time this object is saved to the database. Other examples
|
||||
are the `at_login` hook of Accounts and the `at_repeat` hook of Scripts.
|
||||
|
||||
## Querying for typeclasses
|
||||
|
||||
Most of the time you search for objects in the database by using convenience methods like the
|
||||
`caller.search()` of [Commands](./Commands.md) or the search functions like `evennia.search_objects`.
|
||||
|
||||
You can however also query for them directly using [Django's query
|
||||
language](https://docs.djangoproject.com/en/1.7/topics/db/queries/). This makes use of a _database
|
||||
manager_ that sits on all typeclasses, named `objects`. This manager holds methods that allow
|
||||
database searches against that particular type of object (this is the way Django normally works
|
||||
too). When using Django queries, you need to use the full field names (like `db_key`) to search:
|
||||
|
||||
```python
|
||||
matches = Furniture.objects.get(db_key="Chair")
|
||||
|
||||
```
|
||||
|
||||
It is important that this will *only* find objects inheriting directly from `Furniture` in your
|
||||
database. If there was a subclass of `Furniture` named `Sitables` you would not find any chairs
|
||||
derived from `Sitables` with this query (this is not a Django feature but special to Evennia). To
|
||||
find objects from subclasses Evennia instead makes the `get_family` and `filter_family` query
|
||||
methods available:
|
||||
|
||||
```python
|
||||
# search for all furnitures and subclasses of furnitures
|
||||
# whose names starts with "Chair"
|
||||
matches = Furniture.objects.filter_family(db_key__startswith="Chair")
|
||||
|
||||
```
|
||||
|
||||
To make sure to search, say, all `Scripts` *regardless* of typeclass, you need to query from the
|
||||
database model itself. So for Objects, this would be `ObjectDB` in the diagram above. Here's an
|
||||
example for Scripts:
|
||||
|
||||
```python
|
||||
from evennia import ScriptDB
|
||||
matches = ScriptDB.objects.filter(db_key__contains="Combat")
|
||||
```
|
||||
|
||||
When querying from the database model parent you don't need to use `filter_family` or `get_family` -
|
||||
you will always query all children on the database model.
|
||||
|
||||
## Updating existing typeclass instances
|
||||
|
||||
If you already have created instances of Typeclasses, you can modify the *Python code* at any time -
|
||||
due to how Python inheritance works your changes will automatically be applied to all children once
|
||||
you have reloaded the server.
|
||||
|
||||
However, database-saved data, like `db_*` fields, [Attributes](./Attributes.md), [Tags](./Tags.md) etc, are
|
||||
not themselves embedded into the class and will *not* be updated automatically. This you need to
|
||||
manage yourself, by searching for all relevant objects and updating or adding the data:
|
||||
|
||||
```python
|
||||
# add a worth Attribute to all existing Furniture
|
||||
for obj in Furniture.objects.all():
|
||||
# this will loop over all Furniture instances
|
||||
obj.db.worth = 100
|
||||
```
|
||||
|
||||
A common use case is putting all Attributes in the `at_*_creation` hook of the entity, such as
|
||||
`at_object_creation` for `Objects`. This is called every time an object is created - and only then.
|
||||
This is usually what you want but it does mean already existing objects won't get updated if you
|
||||
change the contents of `at_object_creation` later. You can fix this in a similar way as above
|
||||
(manually setting each Attribute) or with something like this:
|
||||
|
||||
```python
|
||||
# Re-run at_object_creation only on those objects not having the new Attribute
|
||||
for obj in Furniture.objects.all():
|
||||
if not obj.db.worth:
|
||||
obj.at_object_creation()
|
||||
```
|
||||
|
||||
The above examples can be run in the command prompt created by `evennia shell`. You could also run
|
||||
it all in-game using `@py`. That however requires you to put the code (including imports) as one
|
||||
single line using `;` and [list
|
||||
comprehensions](http://www.secnetix.de/olli/Python/list_comprehensions.hawk), like this (ignore the
|
||||
line break, that's only for readability in the wiki):
|
||||
|
||||
```
|
||||
@py from typeclasses.furniture import Furniture;
|
||||
[obj.at_object_creation() for obj in Furniture.objects.all() if not obj.db.worth]
|
||||
```
|
||||
|
||||
It is recommended that you plan your game properly before starting to build, to avoid having to
|
||||
retroactively update objects more than necessary.
|
||||
|
||||
## Swap typeclass
|
||||
|
||||
If you want to swap an already existing typeclass, there are two ways to do so: From in-game and via
|
||||
code. From inside the game you can use the default `@typeclass` command:
|
||||
|
||||
```
|
||||
@typeclass objname = path.to.new.typeclass
|
||||
```
|
||||
|
||||
There are two important switches to this command:
|
||||
- `/reset` - This will purge all existing Attributes on the object and re-run the creation hook
|
||||
(like `at_object_creation` for Objects). This assures you get an object which is purely of this new
|
||||
class.
|
||||
- `/force` - This is required if you are changing the class to be *the same* class the object
|
||||
already has - it's a safety check to avoid user errors. This is usually used together with `/reset`
|
||||
to re-run the creation hook on an existing class.
|
||||
|
||||
In code you instead use the `swap_typeclass` method which you can find on all typeclassed entities:
|
||||
|
||||
```python
|
||||
obj_to_change.swap_typeclass(new_typeclass_path, clean_attributes=False,
|
||||
run_start_hooks="all", no_default=True, clean_cmdsets=False)
|
||||
```
|
||||
|
||||
The arguments to this method are described [in the API docs
|
||||
here](github:evennia.typeclasses.models#typedobjectswap_typeclass).
|
||||
|
||||
|
||||
## How typeclasses actually work
|
||||
|
||||
*This is considered an advanced section.*
|
||||
|
||||
Technically, typeclasses are [Django proxy
|
||||
models](https://docs.djangoproject.com/en/1.7/topics/db/models/#proxy-models). The only database
|
||||
models that are "real" in the typeclass system (that is, are represented by actual tables in the
|
||||
database) are `AccountDB`, `ObjectDB`, `ScriptDB` and `ChannelDB` (there are also
|
||||
[Attributes](./Attributes.md) and [Tags](./Tags.md) but they are not typeclasses themselves). All the
|
||||
subclasses of them are "proxies", extending them with Python code without actually modifying the
|
||||
database layout.
|
||||
|
||||
Evennia modifies Django's proxy model in various ways to allow them to work without any boiler plate
|
||||
(for example you don't need to set the Django "proxy" property in the model `Meta` subclass, Evennia
|
||||
handles this for you using metaclasses). Evennia also makes sure you can query subclasses as well as
|
||||
patches django to allow multiple inheritance from the same base class.
|
||||
|
||||
## Caveats
|
||||
|
||||
Evennia uses the *idmapper* to cache its typeclasses (Django proxy models) in memory. The idmapper
|
||||
allows things like on-object handlers and properties to be stored on typeclass instances and to not
|
||||
get lost as long as the server is running (they will only be cleared on a Server reload). Django
|
||||
does not work like this by default; by default every time you search for an object in the database
|
||||
you'll get a *different* instance of that object back and anything you stored on it that was not in
|
||||
the database would be lost. The bottom line is that Evennia's Typeclass instances subside in memory
|
||||
a lot longer than vanilla Django model instance do.
|
||||
|
||||
There is one caveat to consider with this, and that relates to [making your own models](New-
|
||||
Models): Foreign relationships to typeclasses are cached by Django and that means that if you were
|
||||
to change an object in a foreign relationship via some other means than via that relationship, the
|
||||
object seeing the relationship may not reliably update but will still see its old cached version.
|
||||
Due to typeclasses staying so long in memory, stale caches of such relationships could be more
|
||||
visible than common in Django. See the [closed issue #1098 and its
|
||||
comments](https://github.com/evennia/evennia/issues/1098) for examples and solutions.
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
# Evennia REST API
|
||||
|
||||
Evennia makes its database accessible via a REST API found on
|
||||
[http://localhost:4001/api](http://localhost:4001/api) if running locally with
|
||||
default setup. The API allows you to retrieve, edit and create resources from
|
||||
outside the game, for example with your own custom client or game editor.
|
||||
|
||||
While you can view and learn about the api in the web browser, it is really
|
||||
meant to be accessed in code, by other programs.
|
||||
|
||||
The API is using [Django Rest Framework][drf]. This automates the process
|
||||
of setting up _views_ (Python code) to process the result of web requests.
|
||||
The process of retrieving data is similar to that explained on the
|
||||
[Webserver](./Webserver.md) page, except the views will here return [JSON][json]
|
||||
data for the resource you want. You can also _send_ such JSON data
|
||||
in order to update the database from the outside.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
To activate the API, add this to your settings file.
|
||||
|
||||
REST_API_ENABLED = True
|
||||
|
||||
The main controlling setting is `REST_FRAMEWORK`, which is a dict. The keys
|
||||
`DEFAULT_LIST_PERMISSION` and `DEFAULT_CREATE_PERMISSIONS` control who may
|
||||
view and create new objects via the api respectively. By default, users with
|
||||
['Builder'-level permission](./Permissions.md) or higher may access both actions.
|
||||
|
||||
While the api is meant to be expanded upon, Evennia supplies several operations
|
||||
out of the box. If you click the `Autodoc` button in the upper right of the `/api`
|
||||
website you'll get a fancy graphical presentation of the available endpoints.
|
||||
|
||||
Here is an example of calling the api in Python using the standard `requests` library.
|
||||
|
||||
>>> import requests
|
||||
>>> response = requests.get("https://www.mygame.com/api", auth=("MyUsername", "password123"))
|
||||
>>> response.json()
|
||||
{'accounts': 'http://www.mygame.com/api/accounts/',
|
||||
'objects': 'http://www.mygame.com/api/objects/',
|
||||
'characters': 'http://www.mygame.comg/api/characters/',
|
||||
'exits': 'http://www.mygame.com/api/exits/',
|
||||
'rooms': 'http://www.mygame.com/api/rooms/',
|
||||
'scripts': 'http://www.mygame.com/api/scripts/'
|
||||
'helpentries': 'http://www.mygame.com/api/helpentries/' }
|
||||
|
||||
To list a specific type of object:
|
||||
|
||||
>>> response = requests.get("https://www.mygame.com/api/objects",
|
||||
auth=("Myusername", "password123"))
|
||||
>>> response.json()
|
||||
{
|
||||
"count": 125,
|
||||
"next": "https://www.mygame.com/api/objects/?limit=25&offset=25",
|
||||
"previous": null,
|
||||
"results" : [{"db_key": "A rusty longsword", "id": 57, "db_location": 213, ...}]}
|
||||
|
||||
In the above example, it now displays the objects inside the "results" array,
|
||||
while it has a "count" value for the number of total objects, and "next" and
|
||||
"previous" links for the next and previous page, if any. This is called
|
||||
[pagination][pagination], and the link displays "limit" and "offset" as query
|
||||
parameters that can be added to the url to control the output.
|
||||
|
||||
|
||||
Other query parameters can be defined as [filters][filters] which allow you to
|
||||
further narrow the results. For example, to only get accounts with developer
|
||||
permissions:
|
||||
|
||||
>>> response = requests.get("https://www.mygame.com/api/accounts/?permission=developer",
|
||||
auth=("MyUserName", "password123"))
|
||||
>>> response.json()
|
||||
{
|
||||
"count": 1,
|
||||
"results": [{"username": "bob",...}]
|
||||
}
|
||||
|
||||
Now suppose that you want to use the API to create an [Object](./Objects.md):
|
||||
|
||||
>>> data = {"db_key": "A shiny sword"}
|
||||
>>> response = requests.post("https://www.mygame.com/api/objects",
|
||||
data=data, auth=("Anotherusername", "mypassword"))
|
||||
>>> response.json()
|
||||
{"db_key": "A shiny sword", "id": 214, "db_location": None, ...}
|
||||
|
||||
|
||||
Here we made a HTTP POST request to the `/api/objects` endpoint with the `db_key`
|
||||
we wanted. We got back info for the newly created object. You can now make
|
||||
another request with PUT (replace everything) or PATCH (replace only what you
|
||||
provide). By providing the id to the endpoint (`/api/objects/214`),
|
||||
we make sure to update the right sword:
|
||||
|
||||
>>> data = {"db_key": "An even SHINIER sword", "db_location": 50}
|
||||
>>> response = requests.put("https://www.mygame.com/api/objects/214",
|
||||
data=data, auth=("Anotherusername", "mypassword"))
|
||||
>>> response.json()
|
||||
{"db_key": "An even SHINIER sword", "id": 214, "db_location": 50, ...}
|
||||
|
||||
|
||||
In most cases, you won't be making API requests to the backend with Python,
|
||||
but with Javascript from some frontend application.
|
||||
There are many Javascript libraries which are meant to make this process
|
||||
easier for requests from the frontend, such as [AXIOS][axios], or using
|
||||
the native [Fetch][fetch].
|
||||
|
||||
## Customizing the API
|
||||
|
||||
Overall, reading up on [Django Rest Framework ViewSets](https://www.django-rest-framework.org/api-guide/viewsets) and
|
||||
other parts of their documentation is required for expanding and
|
||||
customizing the API.
|
||||
|
||||
Check out the [Website](./Website.md) page for help on how to override code, templates
|
||||
and static files.
|
||||
- API templates (for the web-display) is located in `evennia/web/api/templates/rest_framework/` (it must
|
||||
be named such to allow override of the original REST framework templates).
|
||||
- Static files is in `evennia/web/api/static/rest_framework/`
|
||||
- The api code is located in `evennia/web/api/` - the `url.py` file here is responsible for
|
||||
collecting all view-classes.
|
||||
|
||||
Contrary to other web components, there is no pre-made urls.py set up for
|
||||
`mygame/web/api/`. This is because the registration of models with the api is
|
||||
strongly integrated with the REST api functionality. Easiest is probably to
|
||||
copy over `evennia/web/api/urls.py` and modify it in place.
|
||||
|
||||
|
||||
[wiki-api]: https://en.wikipedia.org/wiki/Application_programming_interface
|
||||
[drf]: https://www.django-rest-framework.org/
|
||||
[pagination]: https://www.django-rest-framework.org/api-guide/pagination/
|
||||
[filters]: https://www.django-rest-framework.org/api-guide/filtering/#filtering
|
||||
[json]: https://en.wikipedia.org/wiki/JSON
|
||||
[crud]: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete
|
||||
[serializers]: https://www.django-rest-framework.org/api-guide/serializers/
|
||||
[ajax]: https://en.wikipedia.org/wiki/Ajax_(programming)
|
||||
[rest]: https://en.wikipedia.org/wiki/Representational_state_transfer
|
||||
[requests]: https://requests.readthedocs.io/en/master/
|
||||
[axios]: https://github.com/axios/axios
|
||||
[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
# The Web Admin
|
||||
|
||||
The Evennia _Web admin_ is a customized [Django admin site](https://docs.djangoproject.com/en/3.2/ref/contrib/admin/)
|
||||
used for manipulating the game database using a graphical interface. You
|
||||
have to be logged into the site to use it. It then appears as an `Admin` link
|
||||
the top of your website. You can also go to [http://localhost:4001/admin](http://localhost:4001/admin) when
|
||||
running locally.
|
||||
|
||||
Almost all actions done in the admin can also be done in-game by use of Admin-
|
||||
or Builder-commands.
|
||||
|
||||
## Usage
|
||||
|
||||
The admin is pretty self-explanatory - you can see lists of each object type,
|
||||
create new instances of each type and also add new Attributes/tags them. The
|
||||
admin frontpage will give a summary of all relevant entities and how they are
|
||||
used.
|
||||
|
||||
There are a few use cases that requires some additional explanation though.
|
||||
|
||||
### Adding objects to Attributes
|
||||
|
||||
The `value` field of an Attribute is pickled into a special form. This is usually not
|
||||
something you need to worry about (the admin will pickle/unpickle) the value
|
||||
for you), _except_ if you want to store a database-object in an attribute. Such
|
||||
objects are actually stored as a `tuple` with object-unique data.
|
||||
|
||||
1. Find the object you want to add to the Attribute. At the bottom of the first section
|
||||
you'll find the field _Serialized string_. This string shows a Python tuple like
|
||||
|
||||
('__packed_dbobj__', ('objects', 'objectdb'), '2021:05:15-08:59:30:624660', 358)
|
||||
|
||||
Mark and copy this tuple-string to your clipboard exactly as it stands (parentheses and all).
|
||||
2. Go to the entity that should have the new Attribute and create the Attribute. In its `value`
|
||||
field, paste the tuple-string you copied before. Save!
|
||||
3. If you want to store multiple objects in, say, a list, you can do so by literally
|
||||
typing a python list `[tuple, tuple, tuple, ...]` where you paste in the serialized
|
||||
tuple-strings with commas. At some point it's probably easier to do this in code though ...
|
||||
|
||||
### Linking Accounts and Characters
|
||||
|
||||
In `MULTISESSION_MODE` 0 or 1, each connection can have one Account and one
|
||||
Character, usually with the same name. Normally this is done by the user
|
||||
creating a new account and logging in - a matching Character will then be
|
||||
created for them. You can however also do so manually in the admin:
|
||||
|
||||
1. First create the complete Account in the admin.
|
||||
2. Next, create the Object (usually of `Character` typeclass) and name it the same
|
||||
as the Account. It also needs a command-set. The default CharacterCmdset is a good bet.
|
||||
3. In the `Puppeting Account` field, select the Account.
|
||||
4. Make sure to save everything.
|
||||
5. Click the `Link to Account` button (this will only work if you saved first). This will
|
||||
add the needed locks and Attributes to the Account to allow them to immediately
|
||||
connect to the Character when they next log in. This will (where possible):
|
||||
- Set `account.db._last_puppet` to the Character.
|
||||
- Add Character to `account.db._playabel_characters` list.
|
||||
- Add/extend the `puppet:` lock on the Character to include `puppet:pid(<Character.id>)`
|
||||
|
||||
### Building with the Admin
|
||||
|
||||
It's possible (if probably not very practical at scale) to build and describe
|
||||
rooms in the Admin.
|
||||
|
||||
1. Create an `Object` of a Room-typeclass with a suitable room-name.
|
||||
2. Set an Attribute 'desc' on the room - the value of this Attribute is the
|
||||
room's description.
|
||||
3. Add `Tags` of `type` 'alias' to add room-aliases (no type for regular tags)
|
||||
|
||||
Exits:
|
||||
|
||||
1. Exits are `Objects` of an `Exit` typeclass, so create one.
|
||||
2. The exit has `Location` of the room you just created.
|
||||
3. Set `Destination` set to where the exit leads to.
|
||||
4. Set a 'desc' Attribute, this is shown if someone looks at the exit.
|
||||
5. `Tags` of `type` 'alias' are alternative names users can use to go through
|
||||
this exit.
|
||||
|
||||
## Grant others access to the admin
|
||||
|
||||
The access to the admin is controlled by the `Staff status` flag on the
|
||||
Account. Without this flag set, even superusers will not even see the admin
|
||||
link on the web page. The staff-status has no in-game equivalence.
|
||||
|
||||
|
||||
Only Superusers can change the `Superuser status` flag, and grant new
|
||||
permissions to accounts. The superuser is the only permission level that is
|
||||
also relevant in-game. `User Permissions` and `Groups` found on the `Account`
|
||||
admin page _only_ affects the admin - they have no connection to the in-game
|
||||
[Permissions](./Permissions.md) (Player, Builder, Admin etc).
|
||||
|
||||
For a staffer with `Staff status` to be able to actually do anything, the
|
||||
superuser must grant at least some permissions for them on their Account. This
|
||||
can also be good in order to limit mistakes. It can be a good idea to not allow
|
||||
the `Can delete Account` permission, for example.
|
||||
|
||||
```{important}
|
||||
|
||||
If you grant staff-status and permissions to an Account and they still cannot
|
||||
access the admin's content, try reloading the server.
|
||||
|
||||
```
|
||||
|
||||
```{warning}
|
||||
|
||||
If a staff member has access to the in-game ``py`` command, they can just as
|
||||
well have their admin ``Superuser status`` set too. The reason is that ``py``
|
||||
grants them all the power they need to set the ``is_superuser`` flag on their
|
||||
account manually. There is a reason access to the ``py`` command must be
|
||||
considered carefully ...
|
||||
|
||||
```
|
||||
|
||||
## Customizing the web admin
|
||||
|
||||
Customizing the admin is a big topic and something beyond the scope of this
|
||||
documentation. See the [official Django docs](https://docs.djangoproject.com/en/3.2/ref/contrib/admin/) for
|
||||
the details. This is just a brief summary.
|
||||
|
||||
See the [Website](./Website.md) page for an overview of the components going into
|
||||
generating a web page. The Django admin uses the same principle except that
|
||||
Django provides a lot of tools to automate the admin-generation for us.
|
||||
|
||||
Admin templates are found in `evennia/web/templates/admin/` but you'll find
|
||||
this is relatively empty. This is because most of the templates are just
|
||||
inherited directly from their original location in the Django package
|
||||
(`django/contrib/admin/templates/`). So if you wanted to override one you'd have
|
||||
to copy it from _there_ into your `mygame/templates/admin/` folder. Same is true
|
||||
for CSS files.
|
||||
|
||||
The admin site's backend code (the views) is found in `evennia/web/admin/`. It
|
||||
is organized into `admin`-classes, like `ObjectAdmin`, `AccountAdmin` etc.
|
||||
These automatically use the underlying database models to generate useful views
|
||||
for us without us havint go code the forms etc ourselves.
|
||||
|
||||
The top level `AdminSite` (the admin configuration referenced in django docs)
|
||||
is found in `evennia/web/utils/adminsite.py`.
|
||||
|
||||
|
||||
### Change the title of the admin
|
||||
|
||||
By default the admin's title is `Evennia web admin`. To change this, add the
|
||||
following to your `mygame/web/urls.py`:
|
||||
|
||||
```python
|
||||
# in mygame/web/urls.py
|
||||
|
||||
# ...
|
||||
|
||||
from django.conf.admin import site
|
||||
|
||||
#...
|
||||
|
||||
site.site_header = "My great game admin"
|
||||
|
||||
|
||||
```
|
||||
|
||||
Reload the server and the admin's title header will have changed.
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
# Web Client
|
||||
|
||||
Evennia comes with a MUD client accessible from a normal web browser. During development you can try
|
||||
it at `http://localhost:4001/webclient`. The client consists of several parts, all under
|
||||
`evennia/web`:
|
||||
|
||||
`templates/webclient/webclient.html` and `templates/webclient/base.html` are the very simplistic
|
||||
django html templates describing the webclient layout.
|
||||
|
||||
`static/webclient/js/evennia.js` is the main evennia javascript library. This handles all
|
||||
communication between Evennia and the client over websockets and via AJAX/COMET if the browser can't
|
||||
handle websockets. It will make the Evennia object available to the javascript namespace, which
|
||||
offers methods for sending and receiving data to/from the server transparently. This is intended to
|
||||
be used also if swapping out the gui front end.
|
||||
|
||||
`static/webclient/js/webclient_gui.js` is the default plugin manager. It adds the `plugins` and
|
||||
`plugin_manager` objects to the javascript namespace, coordinates the GUI operations between the
|
||||
various plugins, and uses the Evennia object library for all in/out.
|
||||
|
||||
`static/webclient/js/plugins` provides a default set of plugins that implement a "telnet-like"
|
||||
interface, and a couple of example plugins to show how you could implement new plugin features.
|
||||
|
||||
`static/webclient/css/webclient.css` is the CSS file for the client; it also defines things like how
|
||||
to display ANSI/Xterm256 colors etc.
|
||||
|
||||
The server-side webclient protocols are found in `evennia/server/portal/webclient.py` and
|
||||
`webclient_ajax.py` for the two types of connections. You can't (and should not need to) modify
|
||||
these.
|
||||
|
||||
## Customizing the web client
|
||||
|
||||
Like was the case for the website, you override the webclient from your game directory. You need to
|
||||
add/modify a file in the matching directory locations within your project's `mygame/web/` directories.
|
||||
These directories are NOT directly used by the web server when the game is running, the
|
||||
server copies everything web related in the Evennia folder over to `mygame/server/.static/` and then
|
||||
copies in all of your `mygame/web/` files. This can cause some cases were you edit a file, but it doesn't
|
||||
seem to make any difference in the servers behavior. **Before doing anything else, try shutting
|
||||
down the game and running `evennia collectstatic` from the command line then start it back up, clear
|
||||
your browser cache, and see if your edit shows up.**
|
||||
|
||||
Example: To change the list of in-use plugins, you need to override base.html by copying
|
||||
`evennia/web/templates/webclient/base.html` to
|
||||
`mygame/web/templates/webclient/base.html` and editing it to add your new plugin.
|
||||
|
||||
# Evennia Web Client API (from evennia.js)
|
||||
* `Evennia.init( opts )`
|
||||
* `Evennia.connect()`
|
||||
* `Evennia.isConnected()`
|
||||
* `Evennia.msg( cmdname, args, kwargs, callback )`
|
||||
* `Evennia.emit( cmdname, args, kwargs )`
|
||||
* `log()`
|
||||
|
||||
# Plugin Manager API (from webclient_gui.js)
|
||||
* `options` Object, Stores key/value 'state' that can be used by plugins to coordinate behavior.
|
||||
* `plugins` Object, key/value list of the all the loaded plugins.
|
||||
* `plugin_handler` Object
|
||||
* `plugin_handler.add("name", plugin)`
|
||||
* `plugin_handler.onSend(string)`
|
||||
|
||||
# Plugin callbacks API
|
||||
* `init()` -- The only required callback
|
||||
* `boolean onKeydown(event)` This plugin listens for Keydown events
|
||||
* `onBeforeUnload()` This plugin does something special just before the webclient page/tab is
|
||||
closed.
|
||||
* `onLoggedIn(args, kwargs)` This plugin does something when the webclient first logs in.
|
||||
* `onGotOptions(args, kwargs)` This plugin does something with options sent from the server.
|
||||
* `boolean onText(args, kwargs)` This plugin does something with messages sent from the server.
|
||||
* `boolean onPrompt(args, kwargs)` This plugin does something when the server sends a prompt.
|
||||
* `boolean onUnknownCmd(cmdname, args, kwargs)` This plugin does something with "unknown commands".
|
||||
* `onConnectionClose(args, kwargs)` This plugin does something when the webclient disconnects from
|
||||
the server.
|
||||
* `newstring onSend(string)` This plugin examines/alters text that other plugins generate. **Use
|
||||
with caution**
|
||||
|
||||
The order of the plugins defined in `base.html` is important. All the callbacks for each plugin
|
||||
will be executed in that order. Functions marked "boolean" above must return true/false. Returning
|
||||
true will short-circuit the execution, so no other plugins lower in the base.html list will have
|
||||
their callback for this event called. This enables things like the up/down arrow keys for the
|
||||
history.js plugin to always occur before the default_in.js plugin adds that key to the current input
|
||||
buffer.
|
||||
|
||||
# Example/Default Plugins (plugins/*.js)
|
||||
* `clienthelp.js` Defines onOptionsUI from the options2 plugin. This is a mostly empty plugin to
|
||||
add some "How To" information for your game.
|
||||
* `default_in.js` Defines onKeydown. <enter> key or mouse clicking the arrow will send the currently
|
||||
typed text.
|
||||
* `default_out.js` Defines onText, onPrompt, and onUnknownCmd. Generates HTML output for the user.
|
||||
* `default_unload.js` Defines onBeforeUnload. Prompts the user to confirm that they meant to
|
||||
leave/close the game.
|
||||
* `font.js` Defines onOptionsUI. The plugin adds the ability to select your font and font size.
|
||||
* `goldenlayout_default_config.js` Not actually a plugin, defines a global variable that
|
||||
goldenlayout uses to determine its window layout, known tag routing, etc.
|
||||
* `goldenlayout.js` Defines onKeydown, onText and custom functions. A very powerful "tabbed" window
|
||||
manager for drag-n-drop windows, text routing and more.
|
||||
* `history.js` Defines onKeydown and onSend. Creates a history of past sent commands, and uses arrow
|
||||
keys to peruse.
|
||||
* `hotbuttons.js` Defines onGotOptions. A Disabled-by-default plugin that defines a button bar with
|
||||
user-assignable commands.
|
||||
* `html.js` A basic plugin to allow the client to handle "raw html" messages from the server, this
|
||||
allows the server to send native HTML messages like >div style='s'<styled text>/div<
|
||||
* `iframe.js` Defines onOptionsUI. A goldenlayout-only plugin to create a restricted browsing sub-
|
||||
window for a side-by-side web/text interface, mostly an example of how to build new HTML
|
||||
"components" for goldenlayout.
|
||||
* `message_routing.js` Defines onOptionsUI, onText, onKeydown. This goldenlayout-only plugin
|
||||
implements regex matching to allow users to "tag" arbitrary text that matches, so that it gets
|
||||
routed to proper windows. Similar to "Spawn" functions for other clients.
|
||||
* `multimedia.js` An basic plugin to allow the client to handle "image" "audio" and "video" messages
|
||||
from the server and display them as inline HTML.
|
||||
* `notifications.js` Defines onText. Generates browser notification events for each new message
|
||||
while the tab is hidden.
|
||||
* `oob.js` Defines onSend. Allows the user to test/send Out Of Band json messages to the server.
|
||||
* `options.js` Defines most callbacks. Provides a popup-based UI to coordinate options settings with the server.
|
||||
* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the options/settings tab.
|
||||
Integrates with other plugins via the custom onOptionsUI callback.
|
||||
* `popups.js` Provides default popups/Dialog UI for other plugins to use.
|
||||
* `text2html.js` Provides a new message handler type: `text2html`, similar to the multimedia and html
|
||||
plugins. This plugin provides a way to offload rendering the regular pipe-styled ASCII messages
|
||||
to the client. This allows the server to do less work, while also allowing the client a place to
|
||||
customize this conversion process. To use this plugin you will need to override the current commands
|
||||
in Evennia, changing any place where a raw text output message is generated and turn it into a
|
||||
`text2html` message. For example: `target.msg("my text")` becomes: `target.msg(text2html=("my text"))`
|
||||
(even better, use a webclient pane routing tag: `target.msg(text2html=("my text", {"type": "sometag"}))`)
|
||||
`text2html` messages should format and behave identically to the server-side generated text2html() output.
|
||||
|
||||
# A side note on html messages vrs text2html messages
|
||||
|
||||
So...lets say you have a desire to make your webclient output more like standard webpages...
|
||||
For telnet clients, you could collect a bunch of text lines together, with ASCII formatted borders, etc.
|
||||
Then send the results to be rendered client-side via the text2html plugin.
|
||||
|
||||
But for webclients, you could format a message directly with the html plugin to render the whole thing as an
|
||||
HTML table, like so:
|
||||
```
|
||||
# Server Side Python Code:
|
||||
|
||||
if target.is_webclient():
|
||||
# This can be styled however you like using CSS, just add the CSS file to web/static/webclient/css/...
|
||||
table = [
|
||||
"<table>",
|
||||
"<tr><td>1</td><td>2</td><td>3</td></tr>",
|
||||
"<tr><td>4</td><td>5</td><td>6</td></tr>",
|
||||
"</table>"
|
||||
]
|
||||
target.msg( html=( "".join(table), {"type": "mytag"}) )
|
||||
else:
|
||||
# This will use the client to render this as "plain, simple" ASCII text, the same
|
||||
# as if it was rendered server-side via the Portal's text2html() functions
|
||||
table = [
|
||||
"#############",
|
||||
"# 1 # 2 # 3 #",
|
||||
"#############",
|
||||
"# 4 # 5 # 6 #",
|
||||
"#############"
|
||||
]
|
||||
target.msg( html2html=( "\n".join(table), {"type": "mytag"}) )
|
||||
```
|
||||
|
||||
# Writing your own Plugins
|
||||
|
||||
So, you love the functionality of the webclient, but your game has specific
|
||||
types of text that need to be separated out into their own space, visually.
|
||||
The Goldenlayout plugin framework can help with this.
|
||||
|
||||
## GoldenLayout
|
||||
|
||||
GoldenLayout is a web framework that allows web developers and their users to create their own
|
||||
tabbed/windowed layouts. Windows/tabs can be click-and-dragged from location to location by
|
||||
clicking on their titlebar and dragging until the "frame lines" appear. Dragging a window onto
|
||||
another window's titlebar will create a tabbed "Stack". The Evennia goldenlayout plugin defines 3
|
||||
basic types of window: The Main window, input windows and non-main text output windows. The Main
|
||||
window and the first input window are unique in that they can't be "closed".
|
||||
|
||||
The most basic customization is to provide your users with a default layout other than just one Main
|
||||
output and the one starting input window. This is done by modifying your server's
|
||||
goldenlayout_default_config.js.
|
||||
|
||||
Start by creating a new
|
||||
`mygame/web/static/webclient/js/plugins/goldenlayout_default_config.js` file, and adding
|
||||
the following JSON variable:
|
||||
|
||||
```
|
||||
var goldenlayout_config = {
|
||||
content: [{
|
||||
type: 'column',
|
||||
content: [{
|
||||
type: 'row',
|
||||
content: [{
|
||||
type: 'column',
|
||||
content: [{
|
||||
type: 'component',
|
||||
componentName: 'Main',
|
||||
isClosable: false,
|
||||
tooltip: 'Main - drag to desired position.',
|
||||
componentState: {
|
||||
cssClass: 'content',
|
||||
types: 'untagged',
|
||||
updateMethod: 'newlines',
|
||||
},
|
||||
}, {
|
||||
type: 'component',
|
||||
componentName: 'input',
|
||||
id: 'inputComponent',
|
||||
height: 10,
|
||||
tooltip: 'Input - The last input in the layout is always the default.',
|
||||
}, {
|
||||
type: 'component',
|
||||
componentName: 'input',
|
||||
id: 'inputComponent',
|
||||
height: 10,
|
||||
isClosable: false,
|
||||
tooltip: 'Input - The last input in the layout is always the default.',
|
||||
}]
|
||||
},{
|
||||
type: 'column',
|
||||
content: [{
|
||||
type: 'component',
|
||||
componentName: 'evennia',
|
||||
componentId: 'evennia',
|
||||
title: 'example',
|
||||
height: 60,
|
||||
isClosable: false,
|
||||
componentState: {
|
||||
types: 'some-tag-here',
|
||||
updateMethod: 'newlines',
|
||||
},
|
||||
}, {
|
||||
type: 'component',
|
||||
componentName: 'evennia',
|
||||
componentId: 'evennia',
|
||||
title: 'sheet',
|
||||
isClosable: false,
|
||||
componentState: {
|
||||
types: 'sheet',
|
||||
updateMethod: 'replace',
|
||||
},
|
||||
}],
|
||||
}],
|
||||
}]
|
||||
}]
|
||||
};
|
||||
```
|
||||
This is a bit ugly, but hopefully, from the indentation, you can see that it creates a side-by-side
|
||||
(2-column) interface with 3 windows down the left side (The Main and 2 inputs) and a pair of windows
|
||||
on the right side for extra outputs. Any text tagged with "some-tag-here" will flow to the bottom
|
||||
of the "example" window, and any text tagged "sheet" will replace the text already in the "sheet"
|
||||
window.
|
||||
|
||||
Note: GoldenLayout gets VERY confused and will break if you create two windows with the "Main"
|
||||
componentName.
|
||||
|
||||
Now, let's say you want to display text on each window using different CSS. This is where new
|
||||
goldenlayout "components" come in. Each component is like a blueprint that gets stamped out when
|
||||
you create a new instance of that component, once it is defined, it won't be easily altered. You
|
||||
will need to define a new component, preferably in a new plugin file, and then add that into your
|
||||
page (either dynamically to the DOM via javascript, or by including the new plugin file into the
|
||||
base.html).
|
||||
|
||||
First up, follow the directions in Customizing the Web Client section above to override the
|
||||
base.html.
|
||||
|
||||
Next, add the new plugin to your copy of base.html:
|
||||
```
|
||||
<script src={% static "webclient/js/plugins/myplugin.js" %} language="javascript"
|
||||
type="text/javascript"></script>
|
||||
```
|
||||
Remember, plugins are load-order dependent, so make sure the new `<script>` tag comes before the
|
||||
goldenlayout.js
|
||||
|
||||
Next, create a new plugin file `mygame/web/static/webclient/js/plugins/myplugin.js` and
|
||||
edit it.
|
||||
|
||||
```
|
||||
let myplugin = (function () {
|
||||
//
|
||||
//
|
||||
var postInit = function() {
|
||||
var myLayout = window.plugins['goldenlayout'].getGL();
|
||||
|
||||
// register our component and replace the default messagewindow
|
||||
myLayout.registerComponent( 'mycomponent', function (container, componentState) {
|
||||
let mycssdiv = $('<div>').addClass('myCSS');
|
||||
mycssdiv.attr('types', 'mytag');
|
||||
mycssdiv.attr('update_method', 'newlines');
|
||||
mycssdiv.appendTo( container.getElement() );
|
||||
});
|
||||
|
||||
console.log("MyPlugin Initialized.");
|
||||
}
|
||||
|
||||
return {
|
||||
init: function () {},
|
||||
postInit: postInit,
|
||||
}
|
||||
})();
|
||||
window.plugin_handler.add("myplugin", myplugin);
|
||||
```
|
||||
You can then add "mycomponent" to an item's componentName in your goldenlayout_default_config.js.
|
||||
|
||||
Make sure to stop your server, evennia collectstatic, and restart your server. Then make sure to clear your browser cache before loading the webclient page.
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
# Webserver
|
||||
|
||||
When Evennia starts it also spins up its own Twisted-based web server. The
|
||||
webserver is responsible for serving the html pages of the game's website. It
|
||||
can also serve static resources like images and music.
|
||||
|
||||
The webclient runs as part of the [Server](./Portal-And-Server.md) process of
|
||||
Evennia. This means that it can directly access cached objects modified
|
||||
in-game, and there is no risk of working with objects that are temporarily
|
||||
out-of-sync in the database.
|
||||
|
||||
The webserver runs on Twisted and is meant to be used in a production
|
||||
environment. It leverages the Django web framework and provides:
|
||||
|
||||
- A [Game Website](./Website.md) - this is what you see when you go to
|
||||
`localhost:4001`. The look of the website is meant to be customized to your
|
||||
game. Users logged into the website will be auto-logged into the game if they
|
||||
do so with the webclient since they share the same login credentials (there
|
||||
is no way to safely do auto-login with telnet clients).
|
||||
- The [Web Admin](./Web-Admin.md) is based on the Django web admin and allows you to
|
||||
edit the game database in a graphical interface.
|
||||
- The [Webclient](./Webclient.md) page is served by the webserver, but the actual
|
||||
game communication (sending/receiving data) is done by the javascript client
|
||||
on the page opening a websocket connection directly to Evennia's Portal.
|
||||
- The [Evennia REST-API](./Web-API.md) allows for accessing the database from outside the game
|
||||
(only if `REST_API_ENABLED=True).
|
||||
|
||||
|
||||
## Basic Webserver data flow
|
||||
|
||||
1. A user enters an url in their browser (or clicks a button). This leads to
|
||||
the browser sending a _HTTP request_ to the server containing an url-path
|
||||
(like for `https://localhost:4001/`, the part of the url we need to consider
|
||||
`/`). Other possibilities would be `/admin/`, `/login/`, `/channels/` etc.
|
||||
2. evennia (through Django) will make use of the regular expressions registered
|
||||
in the `urls.py` file. This acts as a rerouter to _views_, which are
|
||||
regular Python functions or callable classes able to process the incoming
|
||||
request (think of these as similar to the right Evennia Command being
|
||||
selected to handle your input - views are like Commands in this sense). In
|
||||
the case of `/` we reroute to a view handling the main index-page of the
|
||||
website.
|
||||
3. The view code will prepare all the data needed by the web page. For the default
|
||||
index page, this means gather the game statistics so you can see how many
|
||||
are currently connected to the game etc.
|
||||
4. The view will next fetch a _template_. A template is a HTML-document with special
|
||||
'placeholder' tags (written as `{{...}}` or `{% ... %}` usually). These
|
||||
placeholders allow the view to inject dynamic content into the HTML and make
|
||||
the page customized to the current situation. For the index page, it means
|
||||
injecting the current player-count in the right places of the html page. This
|
||||
is called 'rendering' the template. The result is a complete HTML page.
|
||||
5. (The view can also pull in a _form_ to customize user-input in a similar way.)
|
||||
6. The finished HTML page is packed into a _HTTP response_ and returned to the
|
||||
web browser, which can now display the page!
|
||||
|
||||
### A note on the webclient
|
||||
|
||||
The web browser can also execute code directly without talking to the Server.
|
||||
This code must be written/loaded into the web page and is written using the
|
||||
Javascript programming language (there is no way around this, it is what web
|
||||
browsers understand). Executing Javascript is something the web browser does,
|
||||
it operates independently from Evennia. Small snippets of javascript can be
|
||||
used on a page to have buttons react, make small animations etc that doesn't
|
||||
require the server.
|
||||
|
||||
In the case of the [Webclient](./Webclient.md), Evennia will load the Webclient page
|
||||
as above, but the page then initiates Javascript code (a lot of it) responsible
|
||||
for actually displaying the client GUI, allows you to resize windows etc.
|
||||
|
||||
After it starts, the webclient 'calls home' and spins up a
|
||||
[websocket](https://en.wikipedia.org/wiki/WebSocket) link to the Evennia Portal - this
|
||||
is how all data is then exchanged. So after the initial loading of the
|
||||
webclient page, the above sequence doesn't happen again until close the tab and
|
||||
come back or you reload it manually in your browser.
|
||||
|
|
@ -1,420 +0,0 @@
|
|||
# Game website
|
||||
|
||||
When Evennia starts it will also start a [Webserver](./Webserver.md) as part of the
|
||||
[Server](./Portal-And-Server.md) process. This uses [Django](https://docs.djangoproject.com)
|
||||
to present a simple but functional default game website. With the default setup,
|
||||
open your browser to [localhost:4001](http://localhost:4001) or [127.0.0.1:4001](http://127.0.0.1:4001)
|
||||
to see it.
|
||||
|
||||
The website allows existing players to log in using an account-name and
|
||||
password they previously used to register with the game. If a user logs in with
|
||||
the [Webclient](./Webclient.md) they will also log into the website and vice-versa.
|
||||
So if you are logged into the website, opening the webclient will automatically
|
||||
log you into the game as that account.
|
||||
|
||||
The default website shows a "Welcome!" page with a few links to useful
|
||||
resources. It also shows some statistics about how many players are currently
|
||||
connected.
|
||||
|
||||
In the top menu you can find
|
||||
- _Home_ - Get back to front page.
|
||||
- _Documentation_ - A link to the latest stable Evennia documentation.
|
||||
- _Characters_ - This is a demo of connecting in-game characters to the website.
|
||||
It will display a list of all entities of the
|
||||
_typeclasses.characters.Character` typeclass and allow you to view their
|
||||
description with an optional image. The list is only available to logged-in
|
||||
users.
|
||||
- _Channels_ - This is a demo of connecting in-game chats to the website. It will
|
||||
show a list of all channels available to you and allow you to view the latest
|
||||
discussions. Most channels require logging in, but the `Public` channel can
|
||||
also be viewed by non-loggedin users.
|
||||
- _Help_ - This ties the in-game [Help system](./Help-System.md) to the website. All
|
||||
database-based help entries that are publicly available or accessible to your
|
||||
account can be read. This is a good way to present a body of help for people
|
||||
to read outside of the game.
|
||||
- _Play Online_ - This opens the [Webclient](./Webclient.md) in the browser.
|
||||
- _Admin_ The [Web admin](Web admin) will only show if you are logged in.
|
||||
- _Log in/out_ - Allows you to authenticate using the same credentials you use
|
||||
in the game.
|
||||
- _Register_ - Allows you to register a new account. This is the same as
|
||||
creating a new account upon first logging into the game).
|
||||
|
||||
## Modifying the default Website
|
||||
|
||||
You can modify and override all aspects of the web site from your game dir.
|
||||
You'll mostly be doing so in your settings file
|
||||
(`mygame/server/conf/settings.py` and in the gamedir's `web/folder`
|
||||
(`mygame/web/` if your game folder is `mygame/`).
|
||||
|
||||
> When testing your modifications, it's a good idea to add `DEBUG = True` to
|
||||
> your settings file. This will give you nice informative tracebacks directly
|
||||
> in your browser instead of generic 404 or 500 error pages. Just remember that
|
||||
> DEBUG mode leaks memory (for retaining debug info) and is *not* safe to use
|
||||
> for a production game!
|
||||
|
||||
As explained on the [Webserver](./Webserver.md) page, the process for getting a web
|
||||
page is
|
||||
|
||||
1. Web browser sends HTTP request to server with an URL
|
||||
2. `urls.py` uses regex to match that URL to a _view_ (a Python function or callable class).
|
||||
3. The correct Python view is loaded and executes.
|
||||
4. The view pulls in a _template_, a HTML document with placeholder markers in it,
|
||||
and fills those in as needed (it may also use a _form_ to customize user-input in the same way).
|
||||
A HTML page may also in turn point to static resources (usually CSS, sometimes images etc).
|
||||
5. The rendered HTML page is returned to the browser as a HTTP response. If
|
||||
the HTML page requires static resources are requested, the browser will
|
||||
fetch those separately before displaying it to the user.
|
||||
|
||||
If you look at the [evennia/web/](github:develop/evennia/web) directory you'll find the following
|
||||
structure (leaving out stuff not relevant to the website):
|
||||
|
||||
```
|
||||
evennia/web/
|
||||
...
|
||||
static/
|
||||
website/
|
||||
css/
|
||||
(css style files)
|
||||
images/
|
||||
(images to show)
|
||||
|
||||
templates/
|
||||
website/
|
||||
(html files)
|
||||
|
||||
website/
|
||||
urls.py
|
||||
views/
|
||||
(python files related to website)
|
||||
|
||||
urls.py
|
||||
|
||||
```
|
||||
|
||||
The top-level `web/urls.py` file 'includes' the `web/website/urls.py` file -
|
||||
that way all the website-related url-handling is kept in the same place.
|
||||
|
||||
This is the layout of the `mygame/web/` folder relevant for the website:
|
||||
|
||||
```
|
||||
mygame/web/
|
||||
...
|
||||
static/
|
||||
website/
|
||||
css/
|
||||
images/
|
||||
|
||||
templates/
|
||||
website/
|
||||
|
||||
website/
|
||||
urls.py
|
||||
views/
|
||||
|
||||
urls.py
|
||||
|
||||
```
|
||||
|
||||
```{versionchanged} 1.0
|
||||
|
||||
Game folders created with older versions of Evennia will lack most of this
|
||||
convenient `mygame/web/` layout. If you use a game dir from an older version,
|
||||
you should copy over the missing `evennia/game_template/web/` folders from
|
||||
there, as well as the main urls.py file.
|
||||
|
||||
```
|
||||
|
||||
As you can see, the `mygame/web/` folder is a copy of the `evennia/web/` folder
|
||||
structure except the `mygame` folders are mostly empty.
|
||||
|
||||
For static- and template-files, Evennia will _first_
|
||||
look in `mygame/static` and `mygame/templates` before going to the default
|
||||
locations in `evennia/web/`. So override these resources, you just need to put
|
||||
a file with the same name in the right spot under `mygame/web/` (and then
|
||||
reload the server). Easiest is often to copy the original over and modify it.
|
||||
|
||||
Overridden views (Python modules) also need an additional tweak to the
|
||||
`website/urls.py` file - you must make sure to repoint the url to the new
|
||||
version rather than it using the original.
|
||||
|
||||
## Examples of commom web changes
|
||||
|
||||
```{important}
|
||||
|
||||
Django is a very mature web-design framework. There are endless
|
||||
internet-tutorials, courses and books available to explain how to use Django.
|
||||
So these examples only serve as a first primer to get you started.
|
||||
|
||||
```
|
||||
|
||||
### Change Title and blurb
|
||||
|
||||
The website's title and blurb are simply changed by tweaking
|
||||
`settings.SERVERNAME` and `settings.GAME_SLOGAN`. Your settings file is in
|
||||
`mygame/server/conf/settings.py`, just set/add
|
||||
|
||||
SERVERNAME = "My Awesome Game"
|
||||
GAME_SLOGAN = "The best game in the world"
|
||||
|
||||
### Change the Logo
|
||||
|
||||
The Evennia googly-eyed snake logo is probably not what you want for your game.
|
||||
The template looks for a file `web/static/website/images/evennia_logo.png`. Just
|
||||
plop your own PNG logo (64x64 pixels large) in there and name it the same.
|
||||
|
||||
|
||||
### Change front page HTML
|
||||
|
||||
The front page of the website is usually referred to as the 'index' in HTML
|
||||
parlance.
|
||||
|
||||
The frontpage template is found in `evennia/web/templates/website/index.html`.
|
||||
Just copy this to the equivalent place in `mygame/web/`. Modify it there and
|
||||
reload the server to see your changes.
|
||||
|
||||
Django templates has a few special features that separate them from normal HTML
|
||||
documents - they contain a special templating language marked with `{% ... %}` and
|
||||
`{{ ... }}`.
|
||||
|
||||
Some important things to know:
|
||||
|
||||
- `{% extends "base.html" %}` - This is equivalent to a Python
|
||||
`from othermodule import *` statement, but for templates. It allows a given template
|
||||
to use everything from the imported (extended) template, but also to override anything
|
||||
it wants to change. This makes it easy to keep all pages looking the same and avoids
|
||||
a lot of boiler plate.
|
||||
- `{% block blockname %}...{% endblock %}` - Blocks are inheritable, named pieces of code
|
||||
that are modified in one place and then used elsewhere. This works a bit in reverse to
|
||||
normal inheritance, because it's commonly in such a way that `base.html` defines an empty
|
||||
block, let's say `contents`: `{% block contents %}{% endblock %}` but makes sure to put
|
||||
that _in the right place_, say in the main body, next to the sidebar etc. Then each page
|
||||
does `{% extends "base.html %"}` and makes their own `{% block contents} <actual content> {% endblock %}`.
|
||||
Their `contents` block will now override the empty one in `base.html` and appear in the right
|
||||
place in the document, without the extending template having to specifying everything else
|
||||
around it!
|
||||
- `{{ ... }}` are 'slots' usually embedded inside HTML tags or content. They reference a
|
||||
_context_ (basically a dict) that the Python _view_ makes available to it.
|
||||
Keys on the context are accessed with dot-notation, so if you provide a
|
||||
context `{"stats": {"hp": 10, "mp": 5}}` to your template, you could access
|
||||
that as `{{ stats.hp }}` to display `10` at that location to display `10` at
|
||||
that location.
|
||||
|
||||
This allows for template inheritance (making it easier to make all
|
||||
pages look the same without rewriting the same thing over and over)
|
||||
|
||||
There's a lot more information to be found in the [Django template language documentation](https://docs.djangoproject.com/en/3.2/ref/templates/language/).
|
||||
|
||||
### Change webpage colors and styling
|
||||
|
||||
You can tweak the [CSS](https://en.wikipedia.org/wiki/Cascading_Style_Sheets) of the entire
|
||||
website. If you investigate the `evennia/web/templates/website/base.html` file you'll see that we
|
||||
use the [Bootstrap
|
||||
4](https://getbootstrap.com/docs/4.6/getting-started/introduction/) toolkit.
|
||||
|
||||
Much structural HTML functionality is actually coming from bootstrap, so you
|
||||
will often be able to just add bootstrap CSS classes to elements in the HTML
|
||||
file to get various effects like text-centering or similar.
|
||||
|
||||
The website's custom CSS is found in
|
||||
`evennia/web/static/website/css/website.css` but we also look for a (currently
|
||||
empty) `custom.css` in the same location. You can override either, but it may
|
||||
be easier to revert your changes if you only add things to `custom.css`.
|
||||
|
||||
Copy the CSS file you want to modify to the corresponding location in `mygame/web`.
|
||||
Modify it and reload the server to see your changes.
|
||||
|
||||
You can also apply static files without reloading, but running this in the
|
||||
terminal:
|
||||
|
||||
evennia collectstatic --no-input
|
||||
|
||||
(this is run automatically when reloading the server).
|
||||
|
||||
> Note that before you see new CSS files applied you may need to refresh your
|
||||
> browser without cache (Ctrl-F5 in Firefox, for example).
|
||||
|
||||
As an example, add/copy `custom.css` to `mygame/web/static/website/css/` and
|
||||
add the following:
|
||||
|
||||
|
||||
```css
|
||||
|
||||
.navbar {
|
||||
background-color: #7a3d54;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #7a3d54;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Reload and your website now has a red theme!
|
||||
|
||||
> Hint: Learn to use your web browser's [Developer tools](https://torquemag.io/2020/06/browser-developer-tools-tutorial/).
|
||||
> These allow you to tweak CSS 'live' to find a look you like and copy it into
|
||||
> the .css file only when you want to make the changes permanent.
|
||||
|
||||
|
||||
### Change front page functionality
|
||||
|
||||
The logic is all in the view. To find where the index-page view is found, we
|
||||
look in `evennia/web/website/urls.py`. Here we find the following line:
|
||||
|
||||
```python
|
||||
# in evennia/web/website/urls.py
|
||||
|
||||
...
|
||||
# website front page
|
||||
path("", index.EvenniaIndexView.as_view(), name="index"),
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
The first `""` is the empty url - root - what you get if you just enter `localhost:4001/`
|
||||
with no extra path. As expected, this leads to the index page. By looking at the imports
|
||||
we find the view is in in `evennia/web/website/views/index.py`.
|
||||
|
||||
Copy this file to the corresponding location in `mygame/web`. Then tweak your `mygame/web/website/urls.py`
|
||||
file to point to the new file:
|
||||
|
||||
```python
|
||||
# in mygame/web/website/urls.py
|
||||
|
||||
# ...
|
||||
|
||||
from web.website.views import index
|
||||
|
||||
urlpatterns = [
|
||||
path("", index.EvenniaIndexView.as_view(), name="index")
|
||||
|
||||
]
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
So we just import `index` from the new location and point to it. After a reload
|
||||
the front page will now redirect to use your copy rather than the original.
|
||||
|
||||
The frontpage view is a class `EvenniaIndexView`. This is a [Django class-based view](https://docs.djangoproject.com/en/3.2/topics/class-based-views/).
|
||||
It's a little less visible what happens in a class-based view than in a function (since
|
||||
the class implements a lot of functionality as methods), but it's powerful and
|
||||
much easier to extend/modify.
|
||||
|
||||
The class property `template_name` sets the location of the template used under
|
||||
the `templates/` folder. So `website/index.html` points to
|
||||
`web/templates/website/index.html` (as we already explored above.
|
||||
|
||||
The `get_context_data` is a convenient method for providing the context for the
|
||||
template. In the index-page's case we want the game stats (number of recent
|
||||
players etc). These are then made available to use in `{{ ... }}` slots in the
|
||||
template as described in the previous section.
|
||||
|
||||
### Change other website pages
|
||||
|
||||
The other sub pages are handled in the same way - copy the template or static
|
||||
resource to the right place, or copy the view and repoint your `website/urls.py` to
|
||||
your copy. Just remember to reload.
|
||||
|
||||
## Adding a new web page
|
||||
|
||||
### Using Flat Pages
|
||||
|
||||
The absolutely simplest way to add a new web page is to use the `Flat Pages`
|
||||
app available in the [Web Admin](./Web-Admin.md). The page will appear with the same
|
||||
styling as the rest of the site.
|
||||
|
||||
For the `Flat pages` module to work you must first set up a _Site_ (or
|
||||
domain) to use. You only need to this once.
|
||||
|
||||
- Go to the Web admin and select `Sites`. If your
|
||||
game is at `mygreatgame.com`, that's the domain you need to add. For local
|
||||
experimentation, add the domain `localhost:4001`. Note the `id` of the domain
|
||||
(look at the url when you click on the new domain, if it's for example
|
||||
`http://localhost:4001/admin/sites/site/2/change/`, then the id is `2`).
|
||||
- Now add the line `SITE_ID = <id>` to your settings file.
|
||||
|
||||
Next you create new pages easily.
|
||||
|
||||
- Go the `Flat Pages` web admin and choose to add a new flat page.
|
||||
- Set the url. If you want the page to appear as e.g. `localhost:4001/test/`, then
|
||||
add `/test/` here. You need to add both leading and trailing slashes.
|
||||
- Set `Title` to the name of the page.
|
||||
- The `Content` is the HTML content of the body of the page. Go wild!
|
||||
- Finally pick the `Site` you made before, and save.
|
||||
- (in the advanced section you can make it so that you have to login to see the page etc).
|
||||
|
||||
You can now go to `localhost:4001/test/` and see your new page!
|
||||
|
||||
### Add Custom new page
|
||||
|
||||
The `Flat Pages` page doesn't allow for (much) dynamic content and customization. For
|
||||
this you need to add the needed components yourself.
|
||||
|
||||
Let's see how to make a `/test/` page from scratch.
|
||||
|
||||
- Add a new `test.html` file under `mygame/web/templates/website/`. Easiest is to base
|
||||
this off an existing file. Make sure to `{% extend base.html %}` if you want to
|
||||
get the same styling as the rest of your site.
|
||||
- Add a new view `testview.py` under `mygame/web/website/views/` (don't name it `test.py` or
|
||||
Django/Evennia will think it contains unit tests). Add a view there to process
|
||||
your page. This is a minimal view to start from (read much more [in the Django docs](https://docs.djangoproject.com/en/3.2/topics/class-based-views/)):
|
||||
|
||||
```python
|
||||
# mygame/web/website/views/testview.py
|
||||
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
class MyTestView(TemplateView):
|
||||
template_name = "website/test.html"
|
||||
|
||||
|
||||
```
|
||||
|
||||
- Finally, point to your view from the `mygame/web/website/urls.py`:
|
||||
|
||||
```python
|
||||
# in mygame/web/website/urls.py
|
||||
|
||||
# ...
|
||||
from web.website.views import testview
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
# we can skip the initial / here
|
||||
path("test/", testview.MyTestView.as_view())
|
||||
]
|
||||
|
||||
```
|
||||
- Reload the server and your new page is available. You can now continue to add
|
||||
all sorts of advanced dynamic content through your view and template!
|
||||
|
||||
|
||||
## User forms
|
||||
|
||||
All the pages created so far deal with _presenting_ information to the user.
|
||||
It's also possible for the user to _input_ data on the page through _forms_. An
|
||||
example would be a page of fields and sliders you fill in to create a
|
||||
character, with a big 'Submit' button at the bottom.
|
||||
|
||||
Firstly, this must be represented in HTML. The `<form> ... </form>` is a
|
||||
standard HTML element you need to add to your template. It also has some other
|
||||
requirements, such as `<input>` and often Javascript components as well (but
|
||||
usually Django will help with this). If you are unfamiliar with how HTML forms
|
||||
work, [read about them here](https://docs.djangoproject.com/en/3.2/topics/forms/#html-forms).
|
||||
|
||||
The basic gist of it is that when you click to 'submit' the form, a POST HTML
|
||||
request will be sent to the server containing the data the user entered. It's
|
||||
now up to the server to make sure the data makes sense (validation) and then
|
||||
process the input somehow (like creating a new character).
|
||||
|
||||
On the backend side, we need to specify the logic for validating and processing
|
||||
the form data. This is done by the `Form` [Django class](https://docs.djangoproject.com/en/3.2/topics/forms/#forms-in-django).
|
||||
This specifies _fields_ on itself that define how to validate that piece of data.
|
||||
|
||||
The form is then linked into the view-class by adding `form_class = MyFormClass` to
|
||||
the view (next to `template_name`).
|
||||
|
||||
There are several example forms in `evennia/web/website/forms.py`. It's also a good
|
||||
idea to read [Building a form in Django](https://docs.djangoproject.com/en/3.2/topics/forms/#building-a-form-in-django)
|
||||
on the Django website - it covers all you need.
|
||||
Loading…
Add table
Add a link
Reference in a new issue