mirror of
https://github.com/evennia/evennia.git
synced 2026-03-17 05:16:31 +01:00
Reorganize docs into flat folder layout
This commit is contained in:
parent
106558cec0
commit
892d8efb93
135 changed files with 34 additions and 1180 deletions
107
docs/source/Component/Accounts.md
Normal file
107
docs/source/Component/Accounts.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Accounts
|
||||
|
||||
|
||||
All *users* (real people) that starts a game [Session](Sessions) 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)
|
||||
(normally a [Character](Objects#Character)).
|
||||
|
||||
Exactly how many Sessions can interact with an Account and its Puppets at once is determined by
|
||||
Evennia's [MULTISESSION_MODE](Sessions#Multisession-mode) setting.
|
||||
|
||||
Apart from storing login information and other account-specific data, the Account object is what is
|
||||
chatting on [Channels](Communications). It is also a good place to store [Permissions](Locks) to be
|
||||
consistent between different in-game characters as well as configuration options. The Account
|
||||
object also has its own [CmdSet](Command-Sets), the `AccountCmdSet`.
|
||||
|
||||
Logged into default evennia, you can use the `ooc` command to leave your current
|
||||
[character](Objects) 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](Locks#Permissions) 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)), 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) 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), in the same way as nicks it works on Objects.
|
||||
For Accounts, nicks are primarily used to store custom aliases for
|
||||
[Channels](Communications#Channels).
|
||||
|
||||
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.
|
||||
395
docs/source/Component/Attributes.md
Normal file
395
docs/source/Component/Attributes.md
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
# Attributes
|
||||
|
||||
|
||||
When performing actions in Evennia it is often important that you store data for later. If you write
|
||||
a menu system, you have to keep track of the current location in the menu tree so that the player
|
||||
can give correct subsequent commands. If you are writing a combat system, you might have a
|
||||
combattant's next roll get easier dependent on if their opponent failed. Your characters will
|
||||
probably need to store roleplaying-attributes like strength and agility. And so on.
|
||||
|
||||
[Typeclassed](Typeclasses) game entities ([Accounts](Accounts), [Objects](Objects),
|
||||
[Scripts](Scripts) and [Channels](Communications)) always have *Attributes* associated with them.
|
||||
Attributes are used to store any type of data 'on' such entities. This is different from storing
|
||||
data in properties already defined on entities (such as `key` or `location`) - these have very
|
||||
specific names and require very specific types of data (for example you couldn't assign a python
|
||||
*list* to the `key` property no matter how hard you tried). `Attributes` come into play when you
|
||||
want to assign arbitrary data to arbitrary names.
|
||||
|
||||
**Attributes are _not_ secure by default and any player may be able to change them unless you
|
||||
[prevent this behavior](Attributes#locking-and-checking-attributes).**
|
||||
|
||||
## The .db and .ndb shortcuts
|
||||
|
||||
To save persistent data on a Typeclassed object you normally use the `db` (DataBase) operator. Let's
|
||||
try to save some data to a *Rose* (an [Object](Objects)):
|
||||
|
||||
```python
|
||||
# saving
|
||||
rose.db.has_thorns = True
|
||||
# getting it back
|
||||
is_ouch = rose.db.has_thorns
|
||||
|
||||
```
|
||||
|
||||
This looks like any normal Python assignment, but that `db` makes sure that an *Attribute* is
|
||||
created behind the scenes and is stored in the database. Your rose will continue to have thorns
|
||||
throughout the life of the server now, until you deliberately remove them.
|
||||
|
||||
To be sure to save **non-persistently**, i.e. to make sure NOT to create a database entry, you use
|
||||
`ndb` (NonDataBase). It works in the same way:
|
||||
|
||||
```python
|
||||
# saving
|
||||
rose.ndb.has_thorns = True
|
||||
# getting it back
|
||||
is_ouch = rose.ndb.has_thorns
|
||||
```
|
||||
|
||||
Technically, `ndb` has nothing to do with `Attributes`, despite how similar they look. No
|
||||
`Attribute` object is created behind the scenes when using `ndb`. In fact the database is not
|
||||
invoked at all since we are not interested in persistence. There is however an important reason to
|
||||
use `ndb` to store data rather than to just store variables direct on entities - `ndb`-stored data
|
||||
is tracked by the server and will not be purged in various cache-cleanup operations Evennia may do
|
||||
while it runs. Data stored on `ndb` (as well as `db`) will also be easily listed by example the
|
||||
`@examine` command.
|
||||
|
||||
You can also `del` properties on `db` and `ndb` as normal. This will for example delete an
|
||||
`Attribute`:
|
||||
|
||||
```python
|
||||
del rose.db.has_thorns
|
||||
```
|
||||
|
||||
Both `db` and `ndb` defaults to offering an `all` property on themselves. This returns all
|
||||
associated attributes or non-persistent properties.
|
||||
|
||||
```python
|
||||
list_of_all_rose_attributes = rose.db.all
|
||||
list_of_all_rose_ndb_attrs = rose.ndb.all
|
||||
```
|
||||
|
||||
If you use `all` as the name of an attribute, this will be used instead. Later deleting your custom
|
||||
`all` will return the default behaviour.
|
||||
|
||||
## The AttributeHandler
|
||||
|
||||
The `.db` and `.ndb` properties are very convenient but if you don't know the name of the Attribute
|
||||
beforehand they cannot be used. Behind the scenes `.db` actually accesses the `AttributeHandler`
|
||||
which sits on typeclassed entities as the `.attributes` property. `.ndb` does the same for the
|
||||
`.nattributes` property.
|
||||
|
||||
The handlers have normal access methods that allow you to manage and retrieve `Attributes` and
|
||||
`NAttributes`:
|
||||
|
||||
- `has('attrname')` - this checks if the object has an Attribute with this key. This is equivalent
|
||||
to doing `obj.db.attrname`.
|
||||
- `get(...)` - this retrieves the given Attribute. Normally the `value` property of the Attribute is
|
||||
returned, but the method takes keywords for returning the Attribute object itself. By supplying an
|
||||
`accessing_object` to the call one can also make sure to check permissions before modifying
|
||||
anything.
|
||||
- `add(...)` - this adds a new Attribute to the object. An optional [lockstring](Locks) 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(...)` - returns all Attributes (of the given category) attached to this object.
|
||||
|
||||
See [this section](Attributes#locking-and-checking-attributes) for more about locking down Attribute
|
||||
access and editing. The `Nattribute` offers no concept of access control.
|
||||
|
||||
Some examples:
|
||||
|
||||
```python
|
||||
import evennia
|
||||
obj = evennia.search_object("MyObject")
|
||||
|
||||
obj.attributes.add("test", "testvalue")
|
||||
print(obj.db.test) # prints "testvalue"
|
||||
print(obj.attributes.get("test")) # "
|
||||
print(obj.attributes.all()) # prints [<AttributeObject>]
|
||||
obj.attributes.remove("test")
|
||||
```
|
||||
|
||||
|
||||
## 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#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) is an example of using
|
||||
Attributes
|
||||
in this way). To modify this property you need to use the [Attribute
|
||||
Handler](Attributes#The_Attribute_Handler).
|
||||
- `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) use it). It is only
|
||||
accessible via the [Attribute Handler](Attributes#The_Attribute_Handler).
|
||||
|
||||
There are also two special properties:
|
||||
|
||||
- `attrtype` - this is used internally by Evennia to separate [Nicks](Nicks), 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 have no equivalence to `category` nor `strvalue`, `attrtype` or `model`.
|
||||
|
||||
## 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) 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.
|
||||
- NAttributes have no restrictions at all on what they can store (see next section), 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!
|
||||
|
||||
## What types of data can I save in an Attribute?
|
||||
|
||||
> None of the following affects NAttributes, which does not invoke the database at all. There are no
|
||||
> restrictions to what can be stored in a NAttribute.
|
||||
|
||||
The database doesn't know anything about Python objects, so Evennia must *serialize* Attribute
|
||||
values into a string representation in order to store it to the database. This is done using the
|
||||
`pickle` 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* which will not be pickled).
|
||||
|
||||
It's important to note that when you access the data in an Attribute you are *always* de-serializing
|
||||
it from the database representation every time. This is because we allow for storing
|
||||
database-entities in Attributes too. If we cached it as its Python form, we might end up with
|
||||
situations where the database entity was deleted since we last accessed the Attribute.
|
||||
De-serializing data with a database-entity in it means querying the database for that object and
|
||||
making sure it still exists (otherwise it will be set to `None`). Performance-wise this is usually
|
||||
not a big deal. But if you are accessing the Attribute as part of some big loop or doing a large
|
||||
amount of reads/writes you should first extract it to a temporary variable, operate on *that* and
|
||||
then save the result back to the Attribute. If you are storing a more complex structure like a
|
||||
`dict` or a `list` you should make sure to "disconnect" it from the database before looping over it,
|
||||
as mentioned in the [Retrieving Mutable Objects](Attributes#retrieving-mutable-objects) section
|
||||
below.
|
||||
|
||||
### 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](http://docs.python.org/library/pickle.html).
|
||||
* Single database objects/typeclasses can be stored as any other in the Attribute. These can
|
||||
normally *not* be pickled, but Evennia will behind the scenes convert them to an internal
|
||||
representation using their classname, database-id and creation-date with a microsecond precision,
|
||||
guaranteeing you get the same object back when you access the Attribute later.
|
||||
* If you *hide* a database object inside a non-iterable custom class (like stored as a variable
|
||||
inside it), Evennia will not know it's there and won't convert it safely. Storing classes with
|
||||
such hidden database objects is *not* supported and will lead to errors!
|
||||
|
||||
```python
|
||||
# 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
|
||||
|
||||
# example of an invalid, "hidden" dbobject
|
||||
class Invalid(object):
|
||||
def __init__(self, dbobj):
|
||||
# no way for Evennia to know this is a dbobj
|
||||
self.dbobj = dbobj
|
||||
invalid = Invalid(myobj)
|
||||
obj.db.invalid = invalid # will cause error!
|
||||
```
|
||||
|
||||
### 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#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}]
|
||||
```
|
||||
|
||||
### 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) # this is also [1,2,3,5]
|
||||
```
|
||||
|
||||
To "disconnect" your extracted mutable variable from the database you simply need to convert the
|
||||
`_Saver...` iterable to a normal Python structure. So to convert a `_SaverList`, you use the
|
||||
`list()` function, for a `_SaverDict` you use `dict()` and so on.
|
||||
|
||||
```python
|
||||
obj.db.mylist = [1,2,3,4]
|
||||
mylist = list(obj.db.mylist) # convert to normal list
|
||||
mylist[3] = 5
|
||||
print(mylist) # this is now [1,2,3,5]
|
||||
print(obj.db.mylist) # this is still [1,2,3,4]
|
||||
```
|
||||
|
||||
A further problem comes with *nested mutables*, like a dict containing lists of dicts or something
|
||||
like that. Each of these nested mutables would be `_Saver*` structures connected to the database and
|
||||
disconnecting the outermost one of them would not disconnect those nested within. To make really
|
||||
sure you disonnect a nested structure entirely from the database, Evennia provides a special
|
||||
function `evennia.utils.dbserialize.deserialize`:
|
||||
|
||||
```
|
||||
from evennia.utils.dbserialize import deserialize
|
||||
|
||||
decoupled_mutables = deserialize(nested_mutables)
|
||||
|
||||
```
|
||||
|
||||
The result of this operation will be a structure only consisting of normal Python mutables (`list`
|
||||
instead of `_SaverList` and so on).
|
||||
|
||||
|
||||
Remember, this is only valid for *mutable* iterables.
|
||||
[Immutable](http://en.wikipedia.org/wiki/Immutable) objects (strings, numbers, tuples etc) are
|
||||
already disconnected from the database from the onset.
|
||||
|
||||
```python
|
||||
obj.db.mytup = (1,2,[3,4])
|
||||
obj.db.mytup[0] = 5 # this fails since tuples are immutable
|
||||
|
||||
# this works but will NOT update database since outermost is a tuple
|
||||
obj.db.mytup[2][1] = 5
|
||||
print(obj.db.mytup[2][1]) # this still returns 4, not 5
|
||||
|
||||
mytup1 = obj.db.mytup # mytup1 is already disconnected from database since outermost
|
||||
# iterable is a tuple, so we can edit the internal list as we want
|
||||
# without affecting the database.
|
||||
```
|
||||
|
||||
> Attributes will fetch data fresh from the database whenever you read them, so
|
||||
> if you are performing big operations on a mutable Attribute property (such as looping over a list
|
||||
> or dict) you should make sure to "disconnect" the Attribute's value first and operate on this
|
||||
> rather than on the Attribute. You can gain dramatic speed improvements to big loops this
|
||||
> way.
|
||||
|
||||
|
||||
## Locking and checking Attributes
|
||||
|
||||
Attributes are normally not locked down by default, but you can easily change that for individual
|
||||
Attributes (like those that may be game-sensitive in games with user-level building).
|
||||
|
||||
First you need to set a *lock string* on your Attribute. Lock strings are specified [Locks](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 cannot use the `db` handler to modify Attribute object (such as setting a lock on them) - The
|
||||
`db` handler will return the Attribute's *value*, not the Attribute object itself. Instead you use
|
||||
the AttributeHandler and set it to return the object instead of the value:
|
||||
|
||||
```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.
|
||||
You have to add a check to your commands/code wherever it fits (such as before setting an
|
||||
Attribute).
|
||||
|
||||
```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.
|
||||
229
docs/source/Component/Batch-Code-Processor.md
Normal file
229
docs/source/Component/Batch-Code-Processor.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# Batch Code Processor
|
||||
|
||||
|
||||
For an introduction and motivation to using batch processors, see [here](Batch-Processors). 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 = "A %s and %s were created."
|
||||
if DEBUG:
|
||||
table.delete()
|
||||
chair.delete()
|
||||
string += " Since debug was active, " \
|
||||
"they were deleted again."
|
||||
caller.msg(string % (table, chair))
|
||||
```
|
||||
|
||||
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) 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.
|
||||
182
docs/source/Component/Batch-Command-Processor.md
Normal file
182
docs/source/Component/Batch-Command-Processor.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# Batch Command Processor
|
||||
|
||||
|
||||
For an introduction and motivation to using batch processors, see [here](Batch-Processors). 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)*: 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))
|
||||
|
||||
- [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](http://www.vim.org/) users can use amfl's [vim-evennia](https://github.com/amfl/vim-evennia)
|
||||
mode instead, see its readme for install instructions.
|
||||
82
docs/source/Component/Batch-Processors.md
Normal file
82
docs/source/Component/Batch-Processors.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# 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)
|
||||
- The [Batch Code Processor](Batch-Code-Processor)
|
||||
|
||||
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](http://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](http://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](Text-Encodings) and also in the
|
||||
Wikipedia article [here](http://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.
|
||||
82
docs/source/Component/Bootstrap-Components-and-Utilities.md
Normal file
82
docs/source/Component/Bootstrap-Components-and-Utilities.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# 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](Add-a-simple-new-web-page) or [the web
|
||||
character view tutorial](Web-Character-View-Tutorial)
|
||||
> 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.](Web-Character-Generation)
|
||||
297
docs/source/Component/Coding-Utils.md
Normal file
297
docs/source/Component/Coding-Utils.md
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
# 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](../wiki/evennia.accounts.manager#accountdbmanagersearch_account)
|
||||
- [evennia.search_object](../wiki/evennia.objects.manager#objectdbmanagersearch_object)
|
||||
- [evennia.search_object_by_tag](../wiki/evennia.utils.search#search_object_by_tag)
|
||||
- [evennia.search_script](../wiki/evennia.scripts.manager#scriptdbmanagersearch_script)
|
||||
- [evennia.search_channel](../wiki/evennia.comms.managers#channeldbmanagersearch_channel)
|
||||
- [evennia.search_message](../wiki/evennia.comms.managers#msgmanagersearch_message)
|
||||
- [evennia.search_help](../wiki/evennia.help.manager#helpentrymanagersearch_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](../wiki/evennia.utils.create#create_account)
|
||||
- [evennia.create_object](../wiki/evennia.utils.create#create_object)
|
||||
- [evennia.create_script](../wiki/evennia.utils.create#create_script)
|
||||
- [evennia.create_channel](../wiki/evennia.utils.create#create_channel)
|
||||
- [evennia.create_help_entry](../wiki/evennia.utils.create#create_help_entry)
|
||||
- [evennia.create_message](../wiki/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](github: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).
|
||||
|
||||
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) and [Scripts](Scripts).
|
||||
|
||||
> 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](http://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) 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](Text-Encodings) for more info.
|
||||
|
||||
### Ansi Coloring Tools
|
||||
- [evennia.ansi](api:evennia.utils.ansi)
|
||||
|
||||
## Display utilities
|
||||
### Making ascii tables
|
||||
|
||||
The [EvTable](github:evennia.utils.evtable#evtable) class (`evennia/utils/evtable.py`) can be used
|
||||
to create correctly formatted text tables. There is also
|
||||
[EvForm](github: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](github:evennia.utils.evmenu#evmenu)
|
||||
376
docs/source/Component/Command-Sets.md
Normal file
376
docs/source/Component/Command-Sets.md
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
# Command Sets
|
||||
|
||||
|
||||
Command Sets are intimately linked with [Commands](Commands) 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](Locks#Permissions) 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](Adding-Command-Tutorial) 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, permanent=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](Adding-Command-Tutorial). 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)). 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#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#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#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#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). Default is the empty
|
||||
`SessionCmdSet` with merge priority `-20`.
|
||||
- The cmdsets defined on the [Account](Accounts). 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](Communications) 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). For example, [Character objects](Objects) 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](http://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.
|
||||
663
docs/source/Component/Commands.md
Normal file
663
docs/source/Component/Commands.md
Normal file
|
|
@ -0,0 +1,663 @@
|
|||
# Commands
|
||||
|
||||
|
||||
Commands are intimately linked to [Command Sets](Command-Sets) 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-Command-Help) 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) (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). There is also a step-by-step [Adding Command
|
||||
Tutorial](Adding-Command-Tutorial) 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("Echo: {}".format(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). 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) 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) object itself.
|
||||
* `sessid` - `sessid.id`, a unique integer identifier of the session.
|
||||
* `account` - the [Account](Accounts) object connected to this Session. None if not logged in.
|
||||
* An [Account](Accounts). 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). 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) 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) 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) 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](api:evennia.utils#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), 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#on-arg_regex) for the details.
|
||||
- `auto_help` (optional boolean). Defaults to `True`. This allows for turning off the [auto-help
|
||||
system](Help-System#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](http://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) 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) page.
|
||||
|
||||
### 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](http://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("Starting 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) 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](Communications) 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#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#Dynamic_Commands).
|
||||
|
||||
The functionality of [Exit](Objects) objects in Evennia is not hard-coded in the engine. Instead
|
||||
Exits are normal [typeclassed](Typeclasses) objects that auto-create a [CmdSet](Commands#CmdSets) 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("Command memory ID: {} (xval={})".format(id(self), 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#Exits).
|
||||
- Sets of dynamically created *System commands* representing available
|
||||
[Communications](Communications#Channels).
|
||||
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](http://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("Returned is %s" % 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.
|
||||
113
docs/source/Component/Communications.md
Normal file
113
docs/source/Component/Communications.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Communications
|
||||
|
||||
|
||||
Apart from moving around in the game world and talking, players might need other forms of
|
||||
communication. This is offered by Evennia's `Comm` system. Stock evennia implements a 'MUX-like'
|
||||
system of channels, but there is nothing stopping you from changing things to better suit your
|
||||
taste.
|
||||
|
||||
Comms rely on two main database objects - `Msg` and `Channel`. There is also the `TempMsg` which
|
||||
mimics the API of a `Msg` but has no connection to the database.
|
||||
|
||||
## Msg
|
||||
|
||||
The `Msg` object is the basic unit of communication in Evennia. A message works a little like an
|
||||
e-mail; it always has a sender (a [Account](Accounts)) and one or more recipients. The recipients
|
||||
may be either other Accounts, or a *Channel* (see below). You can mix recipients to send the message
|
||||
to both Channels and Accounts if you like.
|
||||
|
||||
Once created, a `Msg` is normally not changed. It is peristently saved in the database. This allows
|
||||
for comprehensive logging of communications. This could be useful for allowing senders/receivers to
|
||||
have 'mailboxes' with the messages they want to keep.
|
||||
|
||||
### Properties defined on `Msg`
|
||||
|
||||
- `senders` - this is a reference to one or many [Account](Accounts) or [Objects](Objects) (normally
|
||||
*Characters*) sending the message. This could also be an *External Connection* such as a message
|
||||
coming in over IRC/IMC2 (see below). There is usually only one sender, but the types can also be
|
||||
mixed in any combination.
|
||||
- `receivers` - a list of target [Accounts](Accounts), [Objects](Objects) (usually *Characters*) or
|
||||
*Channels* to send the message to. The types of receivers can be mixed in any combination.
|
||||
- `header` - this is a text field for storing a title or header for the message.
|
||||
- `message` - the actual text being sent.
|
||||
- `date_sent` - when message was sent (auto-created).
|
||||
- `locks` - a [lock definition](Locks).
|
||||
- `hide_from` - this can optionally hold a list of objects, accounts or channels to hide this `Msg`
|
||||
from. This relationship is stored in the database primarily for optimization reasons, allowing for
|
||||
quickly post-filter out messages not intended for a given target. There is no in-game methods for
|
||||
setting this, it's intended to be done in code.
|
||||
|
||||
You create new messages in code using `evennia.create_message` (or
|
||||
`evennia.utils.create.create_message.`)
|
||||
|
||||
## TempMsg
|
||||
|
||||
`evennia.comms.models` also has `TempMsg` which mimics the API of `Msg` but is not connected to the
|
||||
database. TempMsgs are used by Evennia for channel messages by default. They can be used for any
|
||||
system expecting a `Msg` but when you don't actually want to save anything.
|
||||
|
||||
## Channels
|
||||
|
||||
Channels are [Typeclassed](Typeclasses) entities, which mean they can be easily extended and their
|
||||
functionality modified. To change which channel typeclass Evennia uses, change
|
||||
settings.BASE_CHANNEL_TYPECLASS.
|
||||
|
||||
Channels act as generic distributors of messages. Think of them as "switch boards" redistributing
|
||||
`Msg` or `TempMsg` objects. Internally they hold a list of "listening" objects and any `Msg` (or
|
||||
`TempMsg`) sent to the channel will be distributed out to all channel listeners. Channels have
|
||||
[Locks](Locks) to limit who may listen and/or send messages through them.
|
||||
|
||||
The *sending* of text to a channel is handled by a dynamically created [Command](Commands) that
|
||||
always have the same name as the channel. This is created for each channel by the global
|
||||
`ChannelHandler`. The Channel command is added to the Account's cmdset and normal command locks are
|
||||
used to determine which channels are possible to write to. When subscribing to a channel, you can
|
||||
then just write the channel name and the text to send.
|
||||
|
||||
The default ChannelCommand (which can be customized by pointing `settings.CHANNEL_COMMAND_CLASS` to
|
||||
your own command), implements a few convenient features:
|
||||
|
||||
- It only sends `TempMsg` objects. Instead of storing individual entries in the database it instead
|
||||
dumps channel output a file log in `server/logs/channel_<channelname>.log`. This is mainly for
|
||||
practical reasons - we find one rarely need to query individual Msg objects at a later date. Just
|
||||
stupidly dumping the log to a file also means a lot less database overhead.
|
||||
- It adds a `/history` switch to view the 20 last messages in the channel. These are read from the
|
||||
end of the log file. One can also supply a line number to start further back in the file (but always
|
||||
20 entries at a time). It's used like this:
|
||||
|
||||
> public/history
|
||||
> public/history 35
|
||||
|
||||
|
||||
There are two default channels created in stock Evennia - `MudInfo` and `Public`. `MudInfo`
|
||||
receives server-related messages meant for Admins whereas `Public` is open to everyone to chat on
|
||||
(all new accounts are automatically joined to it when logging in, it is useful for asking
|
||||
questions). The default channels are defined by the `DEFAULT_CHANNELS` list (see
|
||||
`evennia/settings_default.py` for more details).
|
||||
|
||||
You create new channels with `evennia.create_channel` (or `evennia.utils.create.create_channel`).
|
||||
|
||||
In code, messages are sent to a channel using the `msg` or `tempmsg` methods of channels:
|
||||
|
||||
channel.msg(msgobj, header=None, senders=None, persistent=True)
|
||||
|
||||
The argument `msgobj` can be either a string, a previously constructed `Msg` or a `TempMsg` - in the
|
||||
latter cases all the following keywords are ignored since the message objects already contains all
|
||||
this information. If `msgobj` is a string, the other keywords are used for creating a new `Msg` or
|
||||
`TempMsg` on the fly, depending on if `persistent` is set or not. By default, a `TempMsg` is emitted
|
||||
for channel communication (since the default ChannelCommand instead logs to a file).
|
||||
|
||||
```python
|
||||
# assume we have a 'sender' object and a channel named 'mychan'
|
||||
|
||||
# manually sending a message to a channel
|
||||
mychan.msg("Hello!", senders=[sender])
|
||||
```
|
||||
|
||||
### Properties defined on `Channel`
|
||||
|
||||
- `key` - main name for channel
|
||||
- `aliases` - alternative native names for channels
|
||||
- `desc` - optional description of channel (seen in listings)
|
||||
- `keep_log` (bool) - if the channel should store messages (default)
|
||||
- `locks` - A [lock definition](Locks). Channels normally use the access_types `send, control` and
|
||||
`listen`.
|
||||
36
docs/source/Component/Connection-Screen.md
Normal file
36
docs/source/Component/Connection-Screen.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# 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](Start-Stop-Reload) 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) 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) and the
|
||||
tutorial section on how to add new commands to a default command set.
|
||||
2641
docs/source/Component/Default-Command-Help.md
Normal file
2641
docs/source/Component/Default-Command-Help.md
Normal file
File diff suppressed because it is too large
Load diff
181
docs/source/Component/EvEditor.md
Normal file
181
docs/source/Component/EvEditor.md
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
# 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.set("test", buffer)
|
||||
def quit(caller):
|
||||
"Since we define it, we must handle messages"
|
||||
caller.msg("Editor exited")
|
||||
key = "%s/test" % self.caller
|
||||
# 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.set("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 = "%s/test" % self.caller
|
||||
# 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.
|
||||
1001
docs/source/Component/EvMenu.md
Normal file
1001
docs/source/Component/EvMenu.md
Normal file
File diff suppressed because it is too large
Load diff
37
docs/source/Component/EvMore.md
Normal file
37
docs/source/Component/EvMore.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# 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) or a [Account](Accounts). 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.
|
||||
122
docs/source/Component/Help-System.md
Normal file
122
docs/source/Component/Help-System.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Help System
|
||||
|
||||
|
||||
An important part of Evennia is the online help system. This allows the players and staff alike to
|
||||
learn how to use the game's commands as well as other information pertinent to the game. The help
|
||||
system has many different aspects, from the normal editing of help entries from inside the game, to
|
||||
auto-generated help entries during code development using the *auto-help system*.
|
||||
|
||||
## Viewing the help database
|
||||
|
||||
The main command is `help`:
|
||||
|
||||
help [searchstring]
|
||||
|
||||
This will show a list of help entries, ordered after categories. You will find two sections,
|
||||
*Command help entries* and *Other help entries* (initially you will only have the first one). You
|
||||
can use help to get more info about an entry; you can also give partial matches to get suggestions.
|
||||
If you give category names you will only be shown the topics in that category.
|
||||
|
||||
|
||||
## Command Auto-help system
|
||||
|
||||
A common item that requires help entries are in-game commands. Keeping these entries up-to-date with
|
||||
the actual source code functionality can be a chore. Evennia's commands are therefore auto-
|
||||
documenting straight from the sources through its *auto-help system*. Only commands that you and
|
||||
your character can actually currently use are picked up by the auto-help system. That means an admin
|
||||
will see a considerably larger amount of help topics than a normal player when using the default
|
||||
`help` command.
|
||||
|
||||
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. There is no need to manually
|
||||
create and maintain help database entries for commands; as long as you keep the docstrings updated
|
||||
your help will be dynamically updated for you as well.
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
# [...]
|
||||
|
||||
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 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, "General" is assumed.
|
||||
|
||||
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) 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()` 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 all help entries not involving commands (this is handled automatically by the [Command
|
||||
Auto-help system](Help-System#command-auto-help-system)). Non-automatic help entries describe how
|
||||
your particular game is played - its rules, world descriptions and so on.
|
||||
|
||||
A help entry consists of four parts:
|
||||
|
||||
- The *topic*. This is the name of the help entry. This is what players search for when they are
|
||||
looking for help. The topic can contain spaces and also partial matches will be found.
|
||||
- The *help category*. Examples are *Administration*, *Building*, *Comms* or *General*. This is an
|
||||
overall grouping of similar help topics, used by the engine to give a better overview.
|
||||
- The *text* - the help text itself, of any length.
|
||||
- locks - a [lock definition](Locks). This can be used to limit access to this help entry, maybe
|
||||
because it's staff-only or otherwise meant to be restricted. Help commands check for `access_type`s
|
||||
`view` and `edit`. An example of a lock string would be `view:perm(Builders)`.
|
||||
|
||||
You can create new help entries in code by using `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()")
|
||||
```
|
||||
|
||||
From inside the game those with the right permissions can use the `@sethelp` command to add and
|
||||
modify help entries.
|
||||
|
||||
> @sethelp/add emote = The emote command is ...
|
||||
|
||||
Using `@sethelp` you can add, delete and append text to existing entries. By default new entries
|
||||
will go in the *General* help category. You can change this using a different form of the `@sethelp`
|
||||
command:
|
||||
|
||||
> @sethelp/add emote, Roleplaying = Emoting is important because ...
|
||||
|
||||
If the category *Roleplaying* did not already exist, it is created and will appear in the help
|
||||
index.
|
||||
|
||||
You can, finally, define a lock for the help entry by following the category with a [lock
|
||||
definition](Locks):
|
||||
|
||||
> @sethelp/add emote, Roleplaying, view:all() = Emoting is ...
|
||||
174
docs/source/Component/Inputfuncs.md
Normal file
174
docs/source/Component/Inputfuncs.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# Inputfuncs
|
||||
|
||||
|
||||
An *inputfunc* is an Evennia function that handles a particular input (an [inputcommand](OOB)) from
|
||||
the client. The inputfunc is the last destination for the inputcommand along the [ingoing message
|
||||
path](Messagepath#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), 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). 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) 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.
|
||||
495
docs/source/Component/Locks.md
Normal file
495
docs/source/Component/Locks.md
Normal file
|
|
@ -0,0 +1,495 @@
|
|||
# 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),
|
||||
[Objects](Objects), [Scripts](Scripts), [Accounts](Accounts), [Help System](Help-System),
|
||||
[messages](Communications#Msg) and [channels](Communications#Channels)) 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)
|
||||
- `cmd` - this defines who may call this command at all.
|
||||
- [Objects](Objects):
|
||||
- `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
|
||||
- `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#Characters):
|
||||
- Same as for Objects
|
||||
- [Exits](Objects#Exits):
|
||||
- Same as for Objects
|
||||
- `traverse` - who may pass the exit.
|
||||
- [Accounts](Accounts):
|
||||
- `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): (only checked by `obj.secure_attr`)
|
||||
- `attrread` - see/access attribute
|
||||
- `attredit` - change/delete attribute
|
||||
- [Channels](Communications#Channels):
|
||||
- `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):
|
||||
- `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("edit: id(%i)" % 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](Locks#permissions).
|
||||
- `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) 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)
|
||||
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.
|
||||
|
||||
# Permissions
|
||||
|
||||
> This section covers the underlying code use of permissions. If you just want to learn how to
|
||||
practically assign permissions in-game, refer to the [Building Permissions](Building-Permissions)
|
||||
page, which details how you use the `perm` command.
|
||||
|
||||
A *permission* is simply a list of text strings stored in the handler `permissions` on `Objects`
|
||||
and `Accounts`. Permissions can be used as a convenient way to structure access levels and
|
||||
hierarchies. It is set by the `perm` command. Permissions are especially handled by the `perm()` and
|
||||
`pperm()` lock functions listed above.
|
||||
|
||||
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)
|
||||
|
||||
What this lock will expect is to the fed the actual key object. The `perm()` lock function will
|
||||
check the permissions set on the key and only return true if the permission is the one given.
|
||||
|
||||
Finally we need to actually check this lock somehow. Let's say the chest has an command `open <key>`
|
||||
sitting on itself. Somewhere in its code the command needs to figure out which key you are using and
|
||||
test if this key has the correct permission:
|
||||
|
||||
```python
|
||||
# self.obj is the chest
|
||||
# and used_key is the key we used as argument to
|
||||
# the command. The self.caller is the one trying
|
||||
# to unlock the chest
|
||||
if not self.obj.access(used_key, "unlock"):
|
||||
self.caller.msg("The key does not fit!")
|
||||
return
|
||||
```
|
||||
|
||||
All new accounts are given a default set of permissions defined by
|
||||
`settings.PERMISSION_ACCOUNT_DEFAULT`.
|
||||
|
||||
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:
|
||||
|
||||
Developer # like superuser but affected by locks
|
||||
Admin # can administrate accounts
|
||||
Builder # can edit the world
|
||||
Helper # can edit help files
|
||||
Player # can chat and send tells (default level)
|
||||
|
||||
(Also the plural form works, so you could use `Developers` etc too).
|
||||
|
||||
> There is also a `Guest` level below `Player` that is only active if `settings.GUEST_ENABLED` is
|
||||
set. This is never part of `settings.PERMISSION_HIERARCHY`.
|
||||
|
||||
The main use of this is that if you use the lock function `perm()` mentioned above, a lock check for
|
||||
a particular permission in the hierarchy will *also* grant access to those with *higher* hierarchy
|
||||
access. So if you have the permission "Admin" you will also pass a lock defined as `perm(Builder)`
|
||||
or any of those levels below "Admin".
|
||||
|
||||
When doing an access check from an [Object](Objects) or Character, the `perm()` lock function 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.
|
||||
|
||||
Here is how you use `perm` to give an account more permissions:
|
||||
|
||||
perm/account Tommy = Builders
|
||||
perm/account/del Tommy = Builders # remove it again
|
||||
|
||||
Note the use of the `/account` switch. It means you assign the permission to the
|
||||
[Accounts](Accounts) Tommy instead of any [Character](Objects) that also happens to be named
|
||||
"Tommy".
|
||||
|
||||
Putting permissions on the *Account* 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* - as mentioned above, 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](Locks#Quelling)).
|
||||
|
||||
Below is an example of an object without any connected account
|
||||
|
||||
```python
|
||||
obj1.permissions = ["Builders", "cool_guy"]
|
||||
obj2.locks.add("enter:perm_above(Accounts) and perm(cool_guy)")
|
||||
|
||||
obj2.access(obj1, "enter") # this returns True!
|
||||
```
|
||||
|
||||
And one example of a puppet with a connected account:
|
||||
|
||||
```python
|
||||
account.permissions.add("Accounts")
|
||||
puppet.permissions.add("Builders", "cool_guy")
|
||||
obj2.locks.add("enter:perm_above(Accounts) and perm(cool_guy)")
|
||||
|
||||
obj2.access(puppet, "enter") # this returns False!
|
||||
```
|
||||
|
||||
## 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 so no checks are even run. This allows for the
|
||||
superuser to always have access to everything in an emergency. But it also hides 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 so your locks get tested correctly.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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)
|
||||
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) 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.
|
||||
79
docs/source/Component/MonitorHandler.md
Normal file
79
docs/source/Component/MonitorHandler.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# 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) entity) - the object to monitor. Since this must be
|
||||
typeclassed, it means you can't monitor changes on [Sessions](Sessions) with the monitorhandler, for
|
||||
example.
|
||||
- `fieldname` (str) - the name of a field or [Attribute](Attributes) 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("%s.%s changed to '%s'." % \
|
||||
(obj.key, fieldname, 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")
|
||||
```
|
||||
124
docs/source/Component/Nicks.md
Normal file
124
docs/source/Component/Nicks.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# Nicks
|
||||
|
||||
|
||||
*Nicks*, short for *Nicknames* is a system allowing an object (usually a [Account](Accounts)) 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) 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](Communications) 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) 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
|
||||
```
|
||||
185
docs/source/Component/Objects.md
Normal file
185
docs/source/Component/Objects.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# 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) entities.
|
||||
|
||||
## How to create your own object types
|
||||
|
||||
An Evennia Object is, per definition, a Python class that includes `evennia.DefaultObject` among its
|
||||
parents. In `mygame/typeclasses/objects.py` there is already a class `Object` that inherits from
|
||||
`DefaultObject` and that you can inherit from. You can put your new typeclass directly in that
|
||||
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."
|
||||
```
|
||||
|
||||
You could save this in the `mygame/typeclasses/objects.py` (then you'd not need to import `Object`)
|
||||
or you can put it in a new module. Let's say we do the latter, making a module
|
||||
`typeclasses/flowers.py`. 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 `evennia.create_object`. 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). Check out the `ev.create_*` functions for how to build other entities
|
||||
like [Scripts](Scripts)).
|
||||
|
||||
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](Spawner-and-Prototypes)). The `Object` typeclass offers many more hooks that is available
|
||||
to use though - see next section.
|
||||
|
||||
## Properties and functions on Objects
|
||||
|
||||
Beyond the properties assigned to all [typeclassed](Typeclasses) 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#Exits), it's otherwise usually unset.
|
||||
- `nicks` - as opposed to aliases, a [Nick](Nicks) 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) 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](Commands#Command_Sets) defined on the
|
||||
object (if any).
|
||||
- `scripts` - this is a handler that manages [Scripts](Scripts) 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#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](api: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). 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](Commands#Command_Sets) 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) 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) 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) 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_before_move(destination)` - if this returns False, move is aborted.
|
||||
1. `origin.at_before_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_after_move(source)`
|
||||
1. On the Exit object, `at_after_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.
|
||||
15
docs/source/Component/Portal-And-Server.md
Normal file
15
docs/source/Component/Portal-And-Server.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# 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](Start-Stop-Reload).
|
||||
|
||||
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.
|
||||
366
docs/source/Component/Scripts.md
Normal file
366
docs/source/Component/Scripts.md
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
# Scripts
|
||||
|
||||
|
||||
*Scripts* are the out-of-character siblings to the in-character
|
||||
[Objects](Objects). Scripts are so flexible that the "Script" is a bit limiting
|
||||
- we had to pick something to name them after all. Other possible names
|
||||
(depending on what you'd use them for) would be `OOBObjects`,
|
||||
`StorageContainers` or `TimerObjects`.
|
||||
|
||||
Scripts can be used for many different things in Evennia:
|
||||
|
||||
- They can attach to Objects to influence them in various ways - or exist
|
||||
independently of any one in-game entity (so-called *Global Scripts*).
|
||||
- They can work as timers and tickers - anything that may change with Time. But
|
||||
they can also have no time dependence at all. Note though that if all you want
|
||||
is just to have an object method called repeatedly, you should consider using
|
||||
the [TickerHandler](TickerHandler) which is more limited but is specialized on
|
||||
just this task.
|
||||
- They can describe State changes. A Script is an excellent platform for
|
||||
hosting a persistent, but unique system handler. For example, a Script could be
|
||||
used as the base to track the state of a turn-based combat system. Since
|
||||
Scripts can also operate on a timer they can also update themselves regularly
|
||||
to perform various actions.
|
||||
- They can act as data stores for storing game data persistently in the database
|
||||
(thanks to its ability to have [Attributes](Attributes)).
|
||||
- They can be used as OOC stores for sharing data between groups of objects, for
|
||||
example for tracking the turns in a turn-based combat system or barter exchange.
|
||||
|
||||
Scripts are [Typeclassed](Typeclasses) entities and are manipulated in a similar
|
||||
way to how it works for other such Evennia entities:
|
||||
|
||||
```python
|
||||
# create a new script
|
||||
new_script = evennia.create_script(key="myscript", typeclass=...)
|
||||
|
||||
# search (this is always a list, also if there is only one match)
|
||||
list_of_myscript = evennia.search_script("myscript")
|
||||
|
||||
```
|
||||
|
||||
## Defining new Scripts
|
||||
|
||||
A Script is defined as a class and is created in the same way as other
|
||||
[typeclassed](Typeclasses) entities. The class has several properties
|
||||
to control the timer-component of the scripts. These are all _optional_ -
|
||||
leaving them out will just create a Script with no timer components (useful to act as
|
||||
a database store or to hold a persistent game system, for example).
|
||||
|
||||
This you can do for example in the module
|
||||
`evennia/typeclasses/scripts.py`. Below is an example Script
|
||||
Typeclass.
|
||||
|
||||
```python
|
||||
from evennia import DefaultScript
|
||||
|
||||
class MyScript(DefaultScript):
|
||||
|
||||
def at_script_creation(self):
|
||||
self.key = "myscript"
|
||||
self.interval = 60 # 1 min repeat
|
||||
|
||||
def at_repeat(self):
|
||||
# do stuff every minute
|
||||
```
|
||||
|
||||
In `mygame/typeclasses/scripts.py` is the `Script` class which inherits from `DefaultScript`
|
||||
already. This is provided as your own base class to do with what you like: You can tweak `Script` if
|
||||
you want to change the default behavior and it is usually convenient to inherit from this instead.
|
||||
Here's an example:
|
||||
|
||||
```python
|
||||
# for example in 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
|
||||
self.persistent = True # will survive reload
|
||||
|
||||
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 we put this script on a room, it will randomly report some weather
|
||||
to everyone in the room every 5 minutes.
|
||||
|
||||
To activate it, just add it to the script handler (`scripts`) on an
|
||||
[Room](Objects). That object becomes `self.obj` in the example above. Here we
|
||||
put it on a room called `myroom`:
|
||||
|
||||
```
|
||||
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 create scripts using the `evennia.create_script` function:
|
||||
|
||||
```python
|
||||
from evennia import create_script
|
||||
create_script('typeclasses.weather.Weather', obj=myroom)
|
||||
```
|
||||
|
||||
Note that if you were to give a keyword argument to `create_script`, that would
|
||||
override the default value in your Typeclass. So for example, here is an instance
|
||||
of the weather script that runs every 10 minutes instead (and also not survive
|
||||
a server reload):
|
||||
|
||||
```python
|
||||
create_script('typeclasses.weather.Weather', obj=myroom,
|
||||
persistent=False, interval=10*60)
|
||||
```
|
||||
|
||||
From in-game you can use the `@script` command to launch the Script on things:
|
||||
|
||||
```
|
||||
@script here = typeclasses.scripts.Weather
|
||||
```
|
||||
|
||||
You can conveniently view and kill running Scripts by using the `@scripts`
|
||||
command in-game.
|
||||
|
||||
## Properties and functions defined on Scripts
|
||||
|
||||
A Script has all the properties of a typeclassed object, such as `db` and `ndb`(see
|
||||
[Typeclasses](Typeclasses)). 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`.
|
||||
|
||||
- `desc` - an optional description of the script's function. Seen in script listings.
|
||||
- `interval` - how often the script should run. If `interval == 0` (default), this script has no
|
||||
timing component, will not repeat and will exist forever. This is useful for Scripts used for
|
||||
storage or acting as bases for various non-time dependent game systems.
|
||||
- `start_delay` - (bool), if we should wait `interval` seconds before firing for the first time or
|
||||
not.
|
||||
- `repeats` - How many times we should repeat, assuming `interval > 0`. If repeats is set to `<= 0`,
|
||||
the script will repeat indefinitely. Note that *each* firing of the script (including the first one)
|
||||
counts towards this value. So a `Script` with `start_delay=False` and `repeats=1` will start,
|
||||
immediately fire and shut down right away.
|
||||
- `persistent`- if this script should survive a server *reset* or server *shutdown*. (You don't need
|
||||
to set this for it to survive a normal reload - the script will be paused and seamlessly restart
|
||||
after the reload is complete).
|
||||
|
||||
There is one special property:
|
||||
|
||||
- `obj` - the [Object](Objects) this script is attached to (if any). You should not need to set
|
||||
this manually. If you add the script to the Object with `myobj.scripts.add(myscriptpath)` or give
|
||||
`myobj` as an argument to the `utils.create.create_script` function, the `obj` property will be set
|
||||
to `myobj` for you.
|
||||
|
||||
It's also imperative to know the hook functions. Normally, overriding
|
||||
these are all the customization you'll need to do in Scripts. You can
|
||||
find longer descriptions of these in `src/scripts/scripts.py`.
|
||||
|
||||
- `at_script_creation()` - this is usually where the script class sets things like `interval` and
|
||||
`repeats`; things that control how the script runs. It is only called once - when the script is
|
||||
first created.
|
||||
- `is_valid()` - determines if the script should still be running or not. This is called when
|
||||
running `obj.scripts.validate()`, which you can run manually, but which is also called by Evennia
|
||||
during certain situations such as reloads. This is also useful for using scripts as state managers.
|
||||
If the method returns `False`, the script is stopped and cleanly removed.
|
||||
- `at_start()` - this is called when the script starts or is unpaused. For persistent scripts this
|
||||
is at least once ever server startup. Note that this will *always* be called right away, also if
|
||||
`start_delay` is `True`.
|
||||
- `at_repeat()` - this is called every `interval` seconds, or not at all. It is called right away at
|
||||
startup, unless `start_delay` is `True`, in which case the system will wait `interval` seconds
|
||||
before calling.
|
||||
- `at_stop()` - this is called when the script stops for whatever reason. It's a good place to do
|
||||
custom cleanup.
|
||||
- `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.
|
||||
|
||||
Running methods (usually called automatically by the engine, but possible to also invoke manually)
|
||||
|
||||
- `start()` - this will start the script. This is called automatically whenever you add a new script
|
||||
to a handler. `at_start()` will be called.
|
||||
- `stop()` - this will stop the script and delete it. Removing a script from a handler will stop it
|
||||
automatically. `at_stop()` will be called.
|
||||
- `pause()` - this pauses a running script, rendering it inactive, but not deleting it. All
|
||||
properties are saved and timers can be resumed. This is called automatically when the server reloads
|
||||
and will *not* lead to the *at_stop()* hook being called. This is a suspension of the script, not a
|
||||
change of state.
|
||||
- `unpause()` - resumes a previously paused script. The `at_start()` hook *will* be called to allow
|
||||
it to reclaim its internal state. Timers etc are restored to what they were before pause. The server
|
||||
automatically unpauses all paused scripts after a server reload.
|
||||
- `force_repeat()` - this will forcibly step the script, regardless of when it would otherwise have
|
||||
fired. The timer will reset and the `at_repeat()` hook is called as normal. This also counts towards
|
||||
the total number of repeats, if limited.
|
||||
- `time_until_next_repeat()` - for timed scripts, this returns the time in seconds until it next
|
||||
fires. Returns `None` if `interval==0`.
|
||||
- `remaining_repeats()` - if the Script should run a limited amount of times, this tells us how many
|
||||
are currently left.
|
||||
- `reset_callcount(value=0)` - this allows you to reset the number of times the Script has fired. It
|
||||
only makes sense if `repeats > 0`.
|
||||
- `restart(interval=None, repeats=None, start_delay=None)` - this method allows you to restart the
|
||||
Script in-place with different run settings. If you do, the `at_stop` hook will be called and the
|
||||
Script brought to a halt, then the `at_start` hook will be called as the Script starts up with your
|
||||
(possibly changed) settings. Any keyword left at `None` means to not change the original setting.
|
||||
|
||||
|
||||
## Global Scripts
|
||||
|
||||
A script does not have to be connected to an in-game object. If not it is
|
||||
called a *Global script*. You can create global scripts by simply not supplying an object to store
|
||||
it on:
|
||||
|
||||
```python
|
||||
# adding a global script
|
||||
from evennia import create_script
|
||||
create_script("typeclasses.globals.MyGlobalEconomy",
|
||||
key="economy", persistent=True, obj=None)
|
||||
```
|
||||
Henceforth you can then get it back by searching for its key or other identifier with
|
||||
`evennia.search_script`. In-game, the `scripts` command will show all scripts.
|
||||
|
||||
Evennia supplies a convenient "container" called `GLOBAL_SCRIPTS` that can offer an easy
|
||||
way to access global scripts. If you know the name (key) of the script you can get it like so:
|
||||
|
||||
```python
|
||||
from evennia import GLOBAL_SCRIPTS
|
||||
|
||||
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 Queryset)
|
||||
all_scripts = GLOBAL_SCRIPTS.all()
|
||||
# you can operate directly on the script
|
||||
GLOBAL_SCRIPTS.weather.db.current_weather = "Cloudy"
|
||||
|
||||
```
|
||||
|
||||
> Note that global scripts appear as properties on `GLOBAL_SCRIPTS` based on their `key`.
|
||||
If you were to create two global scripts with the same `key` (even with different typeclasses),
|
||||
the `GLOBAL_SCRIPTS` container will only return one of them (which one depends on order in
|
||||
the database). Best is to organize your scripts so that this does not happen. Otherwise, use
|
||||
`evennia.search_script` to get exactly the script you want.
|
||||
|
||||
There are two ways to make a script appear as a property on `GLOBAL_SCRIPTS`. The first is
|
||||
to manually create a new global script with `create_script` as mentioned above. Often you want this
|
||||
to happen automatically when the server starts though. For this you can use the setting
|
||||
`GLOBAL_SCRIPTS`:
|
||||
|
||||
```python
|
||||
GLOBAL_SCRIPTS = {
|
||||
"my_script": {
|
||||
"typeclass": "scripts.Weather",
|
||||
"repeats": -1,
|
||||
"interval": 50,
|
||||
"desc": "Weather script"
|
||||
"persistent": True
|
||||
},
|
||||
"storagescript": {
|
||||
"typeclass": "scripts.Storage",
|
||||
"persistent": True
|
||||
}
|
||||
}
|
||||
```
|
||||
Here the key (`myscript` and `storagescript` above) is required, all other fields are optional. If
|
||||
`typeclass` is not given, a script of type `settings.BASE_SCRIPT_TYPECLASS` is assumed. The keys
|
||||
related to timing and intervals are only needed if the script is timed.
|
||||
|
||||
Evennia will use the information in `settings.GLOBAL_SCRIPTS` to automatically create and start
|
||||
these
|
||||
scripts when the server starts (unless they already exist, based on their `key`). You need to reload
|
||||
the server before the setting is read and new scripts become available. You can then find the `key`
|
||||
you gave as properties on `evennia.GLOBAL_SCRIPTS`
|
||||
(such as `evennia.GLOBAL_SCRIPTS.storagescript`).
|
||||
|
||||
> Note: Make sure that your Script typeclass does not have any critical errors. If so, 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
|
||||
# first stop the script
|
||||
GLOBAL_SCRIPTS.storagescript.stop()
|
||||
# running the `scripts` command now will show no storagescript
|
||||
# but below now it's recreated again!
|
||||
storage = GLOBAL_SCRIPTS.storagescript
|
||||
```
|
||||
That is, if the script is deleted, next time you get it from `GLOBAL_SCRIPTS`, it will use the
|
||||
information
|
||||
in settings to recreate it for you.
|
||||
|
||||
> Note that if your goal with the Script is to store persistent data, you should set it as
|
||||
`persistent=True`, either in `settings.GLOBAL_SCRIPTS` or in the Scripts typeclass. Otherwise any
|
||||
data you wanted to store on it will be gone (since a new script of the same name is restarted
|
||||
instead).
|
||||
|
||||
## Dealing with Errors
|
||||
|
||||
Errors inside an 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(DefaultScript):
|
||||
|
||||
# [...]
|
||||
|
||||
def at_repeat(self):
|
||||
|
||||
try:
|
||||
# [...] code as above
|
||||
except Exception:
|
||||
# logs the error
|
||||
logger.log_trace()
|
||||
|
||||
```
|
||||
|
||||
## Example of a timed script
|
||||
|
||||
In-game you can try out scripts using the `@script` command. In the
|
||||
`evennia/contrib/tutorial_examples/bodyfunctions.py` is a little example script
|
||||
that makes you do little 'sounds' at random intervals. Try the following to apply an
|
||||
example time-based script to your character.
|
||||
|
||||
> @script self = bodyfunctions.BodyFunctions
|
||||
|
||||
> Note: Since `evennia/contrib/tutorial_examples` is in the default setting
|
||||
> `TYPECLASS_PATHS`, we only need to specify the final part of the path,
|
||||
> that is, `bodyfunctions.BodyFunctions`.
|
||||
|
||||
If you want to inflict your flatulence script on another person, place or
|
||||
thing, try something like the following:
|
||||
|
||||
> @py self.location.search('matt').scripts.add('bodyfunctions.BodyFunctions')
|
||||
|
||||
Here's how you stop it on yourself.
|
||||
|
||||
> @script/stop self = bodyfunctions.BodyFunctions
|
||||
|
||||
This will kill the script again. You can use the `@scripts` command to list all
|
||||
active scripts in the game, if any (there are none by default).
|
||||
|
||||
|
||||
For another example of a Script in use, check out the [Turn Based Combat System
|
||||
tutorial](https://github.com/evennia/evennia/wiki/Turn%20based%20Combat%20System).
|
||||
104
docs/source/Component/Server-Conf.md
Normal file
104
docs/source/Component/Server-Conf.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Server Conf
|
||||
|
||||
|
||||
Evennia runs out of the box without any changes to its settings. But there are several important
|
||||
ways to customize the server and expand it with your own plugins.
|
||||
|
||||
## Settings file
|
||||
|
||||
The "Settings" file referenced throughout the documentation is the file
|
||||
`mygame/server/conf/settings.py`. This is automatically created on the first run of `evennia --init`
|
||||
(see the [Getting Started](Getting-Started) page).
|
||||
|
||||
Your new `settings.py` is relatively bare out of the box. Evennia's core settings file is actually
|
||||
[evennia/settings_default.py](https://github.com/evennia/evennia/blob/master/evennia/settings_default.py)
|
||||
and is considerably more extensive (it is also heavily documented so you should refer to this file
|
||||
directly for the available settings).
|
||||
|
||||
Since `mygame/server/conf/settings.py` is a normal Python module, it simply imports
|
||||
`evennia/settings_default.py` into itself at the top.
|
||||
|
||||
This means that if any setting you want to change were to depend on some *other* default setting,
|
||||
you might need to copy & paste both in order to change them and get the effect you want (for most
|
||||
commonly changed settings, this is not something you need to worry about).
|
||||
|
||||
You should never edit `evennia/settings_default.py`. Rather you should copy&paste the select
|
||||
variables you want to change into your `settings.py` and edit them there. This will overload the
|
||||
previously imported defaults.
|
||||
|
||||
> Warning: It may be tempting to copy everything from `settings_default.py` into your own settings
|
||||
file. There is a reason we don't do this out of the box though: it makes it directly clear what
|
||||
changes you did. Also, if you limit your copying to the things you really need you will directly be
|
||||
able to take advantage of upstream changes and additions to Evennia for anything you didn't
|
||||
customize.
|
||||
|
||||
In code, the settings is accessed through
|
||||
|
||||
```python
|
||||
from django.conf import settings
|
||||
# or (shorter):
|
||||
from evennia import settings
|
||||
# example:
|
||||
servername = settings.SERVER_NAME
|
||||
```
|
||||
|
||||
Each setting appears as a property on the imported `settings` object. You can also explore all
|
||||
possible options with `evennia.settings_full` (this also includes advanced Django defaults that are
|
||||
not touched in default Evennia).
|
||||
|
||||
> It should be pointed out that when importing `settings` into your code like this, it will be *read
|
||||
only*. You *cannot* edit your settings from your code! The only way to change an Evennia setting is
|
||||
to edit `mygame/server/conf/settings.py` directly. You also generally need to restart the server
|
||||
(possibly also the Portal) before a changed setting becomes available.
|
||||
|
||||
## Other files in the `server/conf` directory
|
||||
|
||||
Apart from the main `settings.py` file,
|
||||
|
||||
- `at_initial_setup.py` - this allows you to add a custom startup method to be called (only) the
|
||||
very first time Evennia starts (at the same time as user #1 and Limbo is created). It can be made to
|
||||
start your own global scripts or set up other system/world-related things your game needs to have
|
||||
running from the start.
|
||||
- `at_server_startstop.py` - this module contains two functions that Evennia will call every time
|
||||
the Server starts and stops respectively - this includes stopping due to reloading and resetting as
|
||||
well as shutting down completely. It's a useful place to put custom startup code for handlers and
|
||||
other things that must run in your game but which has no database persistence.
|
||||
- `connection_screens.py` - all global string variables in this module are interpreted by Evennia as
|
||||
a greeting screen to show when an Account first connects. If more than one string variable is
|
||||
present in the module a random one will be picked.
|
||||
- `inlinefuncs.py` - this is where you can define custom [Inline functions](TextTags#inlinefuncs).
|
||||
- `inputfuncs.py` - this is where you define custom [Input functions](Inputfuncs) to handle data
|
||||
from the client.
|
||||
- `lockfuncs.py` - this is one of many possible modules to hold your own "safe" *lock functions* to
|
||||
make available to Evennia's [Locks](Locks).
|
||||
- `mssp.py` - this holds meta information about your game. It is used by MUD search engines (which
|
||||
you often have to register with) in order to display what kind of game you are running along with
|
||||
statistics such as number of online accounts and online status.
|
||||
- `oobfuncs.py` - in here you can define custom [OOB functions](OOB).
|
||||
- `portal_services_plugin.py` - this allows for adding your own custom services/protocols to the
|
||||
Portal. It must define one particular function that will be called by Evennia at startup. There can
|
||||
be any number of service plugin modules, all will be imported and used if defined. More info can be
|
||||
found [here](http://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols).
|
||||
- `server_services_plugin.py` - this is equivalent to the previous one, but used for adding new
|
||||
services to the Server instead. More info can be found
|
||||
[here](http://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols).
|
||||
|
||||
Some other Evennia systems can be customized by plugin modules but has no explicit template in
|
||||
`conf/`:
|
||||
|
||||
- *cmdparser.py* - a custom module can be used to totally replace Evennia's default command parser.
|
||||
All this does is to split the incoming string into "command name" and "the rest". It also handles
|
||||
things like error messages for no-matches and multiple-matches among other things that makes this
|
||||
more complex than it sounds. The default parser is *very* generic, so you are most often best served
|
||||
by modifying things further down the line (on the command parse level) than here.
|
||||
- *at_search.py* - this allows for replacing the way Evennia handles search results. It allows to
|
||||
change how errors are echoed and how multi-matches are resolved and reported (like how the default
|
||||
understands that "2-ball" should match the second "ball" object if there are two of them in the
|
||||
room).
|
||||
|
||||
## ServerConf
|
||||
|
||||
There is a special database model called `ServerConf` that stores server internal data and settings
|
||||
such as current account count (for interfacing with the webserver), startup status and many other
|
||||
things. It's rarely of use outside the server core itself but may be good to
|
||||
know about if you are an Evennia developer.
|
||||
188
docs/source/Component/Sessions.md
Normal file
188
docs/source/Component/Sessions.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# 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), 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) 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) 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) this Session is attached to. If not logged in yet, this is
|
||||
`None`.
|
||||
- `puppet` - The [Character/Object](Objects) currently puppeted by this Account/Session combo. If
|
||||
not logged in or in OOC mode, this is `None`.
|
||||
- `ndb` - The [Non-persistent Attribute](Attributes) handler.
|
||||
- `db` - As noted above, Sessions don't have regular Attributes. This is an alias to `ndb`.
|
||||
- `cmdset` - The Session's [CmdSetHandler](Command-Sets)
|
||||
|
||||
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) and [Objects](Objects) (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). 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](Custom-Protocols) 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`.
|
||||
121
docs/source/Component/Signals.md
Normal file
121
docs/source/Component/Signals.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# 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) 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.
|
||||
327
docs/source/Component/Spawner-and-Prototypes.md
Normal file
327
docs/source/Component/Spawner-and-Prototypes.md
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
# 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), not any other type of
|
||||
entity.
|
||||
|
||||
The normal way to create a custom object in Evennia is to make a [Typeclass](Typeclasses). 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. While this can sometimes be skipped (such as when
|
||||
defining a prototype in a module or feeding a prototype-dict manually to the spawner function),
|
||||
it's good
|
||||
practice to try to include this. It is used for book-keeping and storing of the prototype so you
|
||||
can find it later.
|
||||
- `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) like `"edit:all();control:perm(Builder)"`
|
||||
- `aliases` - list of strings for use as aliases
|
||||
- `tags` - list [Tags](Tags). These are given as tuples `(tag, category, data)`.
|
||||
- `attrs` - list of [Attributes](Attributes). These are given as tuples `(attrname, value,
|
||||
category, lockstring)`
|
||||
- Any other keywords are interpreted as non-category [Attributes](Attributes) and their values.
|
||||
This is
|
||||
convenient for simple Attributes - use `attrs` for full control of Attributes.
|
||||
|
||||
Deprecated as of Evennia 0.8:
|
||||
|
||||
- `ndb_<name>` - sets the value of a non-persistent attribute (`"ndb_"` is stripped from the name).
|
||||
This is simply not useful in a prototype and is deprecated.
|
||||
- `exec` - This accepts a code snippet or a list of code snippets to run. This should not be used -
|
||||
use callables or [$protfuncs](Spawner-and-Prototypes#protfuncs) instead (see below).
|
||||
|
||||
### 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 works in much the same way as an
|
||||
[InlineFunc](TextTags#inline-functions) - they are actually
|
||||
parsed using the same parser - except protfuncs are run every time the prototype is used to spawn a
|
||||
new object (whereas an inlinefunc is called when a text is returned to the user).
|
||||
|
||||
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 "|r{}|n".format(args[0])
|
||||
```
|
||||
|
||||
> 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) 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).
|
||||
169
docs/source/Component/Tags.md
Normal file
169
docs/source/Component/Tags.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# Tags
|
||||
|
||||
|
||||
A common task of a game designer is to organize and find groups of objects and do operations on
|
||||
them. A classic example is to have a weather script affect all "outside" rooms. Another would be for
|
||||
a player casting a magic spell that affects every location "in the dungeon", but not those
|
||||
"outside". Another would be to quickly find everyone joined with a particular guild or everyone
|
||||
currently dead.
|
||||
|
||||
*Tags* are short text labels that you attach to objects so as to easily be able to retrieve and
|
||||
group them. An Evennia entity can be tagged with any number of Tags. On the database side, Tag
|
||||
entities are *shared* between all objects with that tag. This makes them very efficient but also
|
||||
fundamentally different from [Attributes](Attributes), each of which always belongs to one *single*
|
||||
object.
|
||||
|
||||
In Evennia, Tags are technically also used to implement `Aliases` (alternative names for objects)
|
||||
and `Permissions` (simple strings for [Locks](Locks) 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) 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), [Accounts](Accounts),
|
||||
[Scripts](Scripts) and [Channels](Communications). 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)) 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](Zones).
|
||||
136
docs/source/Component/TickerHandler.md
Normal file
136
docs/source/Component/TickerHandler.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# 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). 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) 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](Weather-Tutorial) 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.
|
||||
353
docs/source/Component/Typeclasses.md
Normal file
353
docs/source/Component/Typeclasses.md
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
# Typeclasses
|
||||
|
||||
|
||||
*Typeclasses* form the core of Evennia 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), [Objects](Objects),
|
||||
[Scripts](Scripts) and [Channels](Communications#Channels) 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#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) and
|
||||
[Tags](Tags) 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) that handles tagging. Use `tags.add()` , `tags.get()` etc.
|
||||
- `locks` - the [LockHandler](Locks) that manages access restrictions. Use `locks.add()`,
|
||||
`locks.get()` etc.
|
||||
- `attributes` - the [AttributeHandler](Attributes) 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) 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), [Scripts](Scripts), [Accounts](Accounts) and
|
||||
[Channels](Communications) for more info. It's also recommended that you explore the available
|
||||
entities using [Evennia's flat API](Evennia-API) 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) 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), [Tags](Tags) 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) and [Tags](Tags) 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.
|
||||
344
docs/source/Component/Webclient.md
Normal file
344
docs/source/Component/Webclient.md
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
# Webclient
|
||||
|
||||
# **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/webclient/`:
|
||||
|
||||
`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.
|
||||
|
||||
`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 location within one of the _overrides directories.
|
||||
These _override 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/web/static/` and then
|
||||
copies in all of your _overrides. 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 utilized plugin list, you need to override base.html by copying
|
||||
`evennia/web/webclient/templates/webclient/base.html` to
|
||||
`mygame/web/template_overrides/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.
|
||||
* `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.
|
||||
* `splithandler.js` Defines onText. Provides an older, less-flexible alternative to goldenlayout for
|
||||
multi-window UI to automatically separate out screen real-estate by type of message.
|
||||
|
||||
# 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. There are two plugins to help with this. The
|
||||
Goldenlayout plugin framework, and the older Splithandler framework.
|
||||
|
||||
## 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_overrides/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_overrides/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.
|
||||
|
||||
|
||||
## Older Splithandler
|
||||
The splithandler.js plugin provides a means to do this, but you don't want to have to force every
|
||||
player to set up their own layout every time they use the client.
|
||||
|
||||
Let's create a `mygame/web/static_overrides/webclient/js/plugins/layout.js` plugin!
|
||||
|
||||
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/layout.js" %} language="javascript"
|
||||
type="text/javascript"></script>
|
||||
```
|
||||
Remember, plugins are load-order dependent, so make sure the new `<script>` tag comes after the
|
||||
splithandler.js
|
||||
|
||||
And finally create the layout.js file and add the minimum skeleton of a plugin to it:
|
||||
|
||||
```
|
||||
// my new plugin
|
||||
var my_plugin = (function () {
|
||||
let init = function () {
|
||||
console.log("myplugin! Hello World!");
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
}
|
||||
})();
|
||||
plugin_handler.add("myplugin", my_plugin);
|
||||
```
|
||||
|
||||
Now, `evennia stop`, `evennia collectstatic`, and `evennia start` and then load the webclient up in
|
||||
your browser.
|
||||
Enable developer options and look in the console, and you should see the message 'myplugin! Hello
|
||||
World!'
|
||||
|
||||
Since our layout.js plugin is going to use the splithandler, let's enhance this by adding a check to
|
||||
make sure the splithandler.js plugin has been loaded:
|
||||
|
||||
change the above init function to:
|
||||
```
|
||||
let init = function () {
|
||||
let splithandler = plugins['splithandler'];
|
||||
if( splithandler ) {
|
||||
console.log("MyPlugin initialized");
|
||||
} else {
|
||||
alert('MyPlugin requires the splithandler.js plugin. Please contact the game maintainer to
|
||||
correct this');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And finally, the splithandler.js provides provides two functions to cut up the screen real-estate:
|
||||
`dynamic_split( pane_name_to_cut_apart, direction_of_split, new_pane_name1, new_pane_name2,
|
||||
text_flow_pane1, text_flow_pane2, array_of_split_percentages )`
|
||||
and
|
||||
`set_pane_types( pane_to_set, array_of_known_message_types_to_assign)`
|
||||
|
||||
In this case, we'll cut it into 3 panes, 1 bigger, two smaller, and assign 'help' messages to the
|
||||
top-right pane:
|
||||
```
|
||||
let init = function () {
|
||||
let splithandler = plugins['splithandler'];
|
||||
if( splithandler ) {
|
||||
splithandler.dynamic_split("main","horizontal","left","right","linefeed","linefeed",[50,50]);
|
||||
splithandler.dynamic_split("right","vertical","help","misc","replace","replace",[50,50]);
|
||||
splithandler.set_pane_types('help', ['help']);
|
||||
|
||||
console.log("MyPlugin initialized");
|
||||
} else {
|
||||
alert('MyPlugin requires the splithandler.js plugin. Please contact the game maintainer to
|
||||
correct this');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`evennia stop`, `evennia collectstatic`, and `evennia start` once more, and force-reload your
|
||||
browser page to clear any cached version. You should now have a nicely split layout.
|
||||
Loading…
Add table
Add a link
Reference in a new issue