mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Working on Components docs
This commit is contained in:
parent
ff6e01475d
commit
b21cd5952c
5 changed files with 196 additions and 281 deletions
|
|
@ -1,41 +1,39 @@
|
|||
# Accounts
|
||||
|
||||
```
|
||||
┌──────┐ │ ┌───────┐ ┌───────┐ ┌──────┐
|
||||
│Client├─┼──►│Session├───►│Account├──►│Object│
|
||||
└──────┘ │ └───────┘ └───────┘ └──────┘
|
||||
^
|
||||
```
|
||||
|
||||
All *users* (real people) that starts a game [Session](./Sessions.md) on Evennia are doing so through an
|
||||
object called *Account*. The Account object has no in-game representation, it represents a unique
|
||||
game account. In order to actually get on the game the Account must *puppet* an [Object](./Objects.md)
|
||||
(normally a [Character](./Objects.md#characters)).
|
||||
An _Account_ represents a unique game account - one player playing the game. Whereas a player can potentially connect to the game from several Clients/Sessions, they will normally have only one Account.
|
||||
|
||||
The Account object has no in-game representation. In order to actually get on the game the Account must *puppet* an [Object](./Objects.md) (normally a [Character](./Objects.md#characters)).
|
||||
|
||||
Exactly how many Sessions can interact with an Account and its Puppets at once is determined by
|
||||
Evennia's [MULTISESSION_MODE](./Sessions.md#multisession-mode) setting.
|
||||
|
||||
Apart from storing login information and other account-specific data, the Account object is what is
|
||||
chatting on [Channels](./Channels.md). It is also a good place to store [Permissions](./Locks.md) to be
|
||||
consistent between different in-game characters as well as configuration options. The Account
|
||||
object also has its own [CmdSet](./Command-Sets.md), the `AccountCmdSet`.
|
||||
Apart from storing login information and other account-specific data, the Account object is what is chatting on Evennia's default [Channels](./Channels.md). It is also a good place to store [Permissions](./Locks.md) to be consistent between different in-game characters. It can also hold player-level configuration options.
|
||||
|
||||
Logged into default evennia, you can use the `ooc` command to leave your current
|
||||
[character](./Objects.md) and go into OOC mode. You are quite limited in this mode, basically it works
|
||||
like a simple chat program. It acts as a staging area for switching between Characters (if your
|
||||
game supports that) or as a safety mode if your Character gets deleted. Use `ic` to attempt to
|
||||
(re)puppet a Character.
|
||||
The Account object has its own default [CmdSet](./Command-Sets.md), the `AccountCmdSet`. The commands in this set are available to the player no matter which character they are puppeting. Most notably the default game's `exit`, `who` and chat-channel commands are in the Account cmdset.
|
||||
|
||||
Note that the Account object can have, and often does have, a different set of
|
||||
[Permissions](./Permissions.md) from the Character they control. Normally you should put your
|
||||
permissions on the Account level - this will overrule permissions set on the Character level. For
|
||||
the permissions of the Character to come into play the default `quell` command can be used. This
|
||||
allows for exploring the game using a different permission set (but you can't escalate your
|
||||
permissions this way - for hierarchical permissions like `Builder`, `Admin` etc, the *lower* of the
|
||||
permissions on the Character/Account will always be used).
|
||||
> ooc
|
||||
|
||||
## How to create your own Account types
|
||||
The default `ooc` command causes you to leave your current puppet and go into OOC mode. In this mode you have no location and have only the Account-cmdset available. It acts a staging area for switching characters (if your game supports that) as well as a safety fallback if your character gets accidentally deleted.
|
||||
|
||||
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).
|
||||
> ic
|
||||
|
||||
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`).
|
||||
This re-puppets the latest character.
|
||||
|
||||
Note that the Account object can have, and often does have, a different set of [Permissions](./Permissions.md) from the Character they control. Normally you should put your permissions on the Account level - this will overrule permissions set on the Character level. For the permissions of the Character to come into play the default `quell` command can be used. This allows for exploring the game using a different permission set (but you can't escalate your permissions this way - for hierarchical permissions like `Builder`, `Admin` etc, the *lower* of the permissions on the Character/Account will always be used).
|
||||
|
||||
|
||||
## How to customize your own Account types
|
||||
|
||||
You will usually not want more than one Account typeclass for all new accounts.
|
||||
|
||||
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:
|
||||
|
||||
|
|
@ -44,64 +42,49 @@ Here's an example of modifying the default Account class in code:
|
|||
|
||||
from evennia import DefaultAccount
|
||||
|
||||
class Account(DefaultAccount): # [...]
|
||||
class Account(DefaultAccount):
|
||||
# [...]
|
||||
def 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
|
||||
```
|
||||
|
||||
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`:
|
||||
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.
|
||||
> If you wanted Evennia to default to a completely *different* Account class located elsewhere, you > must point Evennia to it. Add `BASE_ACCOUNT_TYPECLASS` to your settings file, and give the python path to your custom class as its value. By default this points to `typeclasses.accounts.Account`, the empty template we used above.
|
||||
|
||||
|
||||
## Properties on Accounts
|
||||
|
||||
Beyond those properties assigned to all typeclassed objects (see [Typeclasses](./Typeclasses.md)), the
|
||||
Account also has the following custom properties:
|
||||
Beyond those properties assigned to all typeclassed objects (see [Typeclasses](./Typeclasses.md)), the Account also has the following custom properties:
|
||||
|
||||
- `user` - a unique link to a `User` Django object, representing the logged-in user.
|
||||
- `obj` - an alias for `character`.
|
||||
- `name` - an alias for `user.username`
|
||||
- `sessions` - an instance of
|
||||
[ObjectSessionHandler](github:evennia.objects.objects#objectsessionhandler)
|
||||
managing all connected Sessions (physical connections) this object listens to (Note: In older
|
||||
versions of Evennia, this was a list). The so-called `session-id` (used in many places) is found
|
||||
as
|
||||
a property `sessid` on each Session instance.
|
||||
- `sessions` - an instance of [ObjectSessionHandler](github:evennia.objects.objects#objectsessionhandler) managing all connected Sessions (physical connections) this object listens to (Note: In older versions of Evennia, this was a list). The so-called `session-id` (used in many places) is found as a property `sessid` on each Session instance.
|
||||
- `is_superuser` (bool: True/False) - if this account is a superuser.
|
||||
|
||||
Special handlers:
|
||||
- `cmdset` - This holds all the current [Commands](./Commands.md) of this Account. By default these are
|
||||
the commands found in the cmdset defined by `settings.CMDSET_ACCOUNT`.
|
||||
- `nicks` - This stores and handles [Nicks](./Nicks.md), in the same way as nicks it works on Objects.
|
||||
For Accounts, nicks are primarily used to store custom aliases for
|
||||
[Channels](./Channels.md).
|
||||
- `nicks` - This stores and handles [Nicks](./Nicks.md), in the same way as nicks it works on Objects. For Accounts, nicks are primarily used to store custom aliases for [Channels](./Channels.md).
|
||||
|
||||
Selection of special methods (see `evennia.DefaultAccount` for details):
|
||||
- `get_puppet` - get a currently puppeted object connected to the Account and a given session id, if
|
||||
any.
|
||||
- `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.
|
||||
- `search` - search for Accounts.
|
||||
|
|
@ -1,23 +1,53 @@
|
|||
# Objects
|
||||
|
||||
```
|
||||
┌──────┐ │ ┌───────┐ ┌───────┐ ┌──────┐
|
||||
│Client├─┼──►│Session├───►│Account├──►│Object│
|
||||
└──────┘ │ └───────┘ └───────┘ └──────┘
|
||||
^
|
||||
```
|
||||
|
||||
All in-game objects in Evennia, be it characters, chairs, monsters, rooms or hand grenades are
|
||||
represented by an Evennia *Object*. Objects form the core of Evennia and is probably what you'll
|
||||
spend most time working with. Objects are [Typeclassed](./Typeclasses.md) entities.
|
||||
|
||||
An Evennia Object is, by definition, a Python class that includes
|
||||
[evennia.objects.objects.DefaultObject](evennia.objects.objects.DefaultObject) among its
|
||||
parents. Evennia defines several subclasses of `DefaultObject`:
|
||||
An Evennia Object is, by definition, a Python class that includes [evennia.objects.objects.DefaultObject](evennia.objects.objects.DefaultObject) among its parents. Evennia defines several subclasses of `DefaultObject` in the following inheritance tree:
|
||||
|
||||
- [evennia.objects.objects.DefaultCharacter](evennia.objects.objects.DefaultCharacter) -
|
||||
the normal in-game Character, controlled by a player.
|
||||
```
|
||||
┌────────────┐
|
||||
Evennia│ │ObjectParent│
|
||||
library│ └──────▲─────┘
|
||||
│ │
|
||||
┌─────────────┐ │ ┌──────┐ │
|
||||
│DefaultObject◄────────────────────┼────┤Object├──────┤
|
||||
└──────▲──────┘ │ └──────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────┐ │ ┌─────────┐ │
|
||||
├────────┤DefaultCharacter◄─┼────┤Character├───┤
|
||||
│ └────────────────┘ │ └─────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────┐ │ ┌────┐ │
|
||||
├────────┤DefaultRoom ◄─┼────┤Room├────────┤
|
||||
│ └────────────────┘ │ └────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────┐ │ ┌────┐ │
|
||||
└────────┤DefaultExit ◄─┼────┤Exit├────────┘
|
||||
└────────────────┘ │ └────┘
|
||||
│
|
||||
│Game-dir
|
||||
```
|
||||
|
||||
Here, arrows indicate inheritance and point from-child-to-parent.
|
||||
|
||||
So, for example `DefaultObjet` is a child of `DefaultCharacter` (in the Evennia library), which is a parent of `Character` (in the game dir). The class in the game-dir is the one you should modify for your game.
|
||||
|
||||
> Note the `ObjectParent` class. This is an empty mix-in that all classes in the game-dir inherits from. It's where you put things you want _all_ these classes to have.
|
||||
|
||||
- [evennia.objects.objects.DefaultCharacter](evennia.objects.objects.DefaultCharacter) - the normal in-game Character, controlled by a player.
|
||||
- [evennia.objects.objects.DefaultRoom](evennia.objects.objects.DefaultRoom) - a location in the game world.
|
||||
- [evennia.objects.objects.DefaultExit](evennia.objects.objects.DefaultExit) - an entity that (usually) sits
|
||||
in a room and represents a one-way connection to another location.
|
||||
- [evennia.objects.objects.DefaultExit](evennia.objects.objects.DefaultExit) - an entity that in a location (usually a Room). It represents a one-way connection to another location.
|
||||
|
||||
You will usually not use the `Default*` parents themselves. In `mygame/typeclasses/` there are
|
||||
convenient subclasses to use. They are empty, and thus identical to
|
||||
the defaults. Tweaking them is one of the main ways to customize you game!
|
||||
Here are the import paths for the relevant child classes in the game dir
|
||||
|
||||
- `mygame.typeclasses.objects.Object` (inherits from `DefaultObject`)
|
||||
- `mygame.typeclasses.characters.Character` (inherits from `DefaultCharacter`)
|
||||
|
|
@ -26,12 +56,9 @@ the defaults. Tweaking them is one of the main ways to customize you game!
|
|||
|
||||
## How to create your own object types
|
||||
|
||||
You can easily add your own in-game behavior by either modifying one of the typeclasses in
|
||||
your game dir or by inheriting from them.
|
||||
You can easily add your own in-game behavior by either modifying one of the typeclasses in your game dir or by inheriting from them.
|
||||
|
||||
You can put your new typeclass directly in the relevant parent
|
||||
module, or you could organize your code in some other way. Here we assume we make a new module
|
||||
`mygame/typeclasses/flowers.py`:
|
||||
You can put your new typeclass directly in the relevant 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
|
||||
|
|
@ -52,7 +79,7 @@ module, or you could organize your code in some other way. Here we assume we mak
|
|||
Now you just need to point to the class *Rose* with the `create` command
|
||||
to make a new rose:
|
||||
|
||||
@create/drop MyRose:flowers.Rose
|
||||
create/drop MyRose:flowers.Rose
|
||||
|
||||
What the `create` command actually *does* is to use the [evennia.create_object](evennia.utils.create.create_object)
|
||||
function. You can do the same thing yourself in code:
|
||||
|
|
@ -75,11 +102,9 @@ will usually want to change this at build time (using the `desc` command or usin
|
|||
## Adding common functionality
|
||||
|
||||
`Object`, `Character`, `Room` and `Exit` also inherit from `mygame.typeclasses.objects.ObjectParent`.
|
||||
This is an empty 'mixin' class. Optionally, you can modify this class if you want to easily add some _common_ functionality to all
|
||||
your Objects, Characters, Rooms and Exits at once. You can still customize each subclass separately (see the Python
|
||||
docs on [multiple inheritance](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance) for details).
|
||||
This is an empty 'mixin' class. Optionally, you can modify this class if you want to easily add some _common_ functionality to all your Objects, Characters, Rooms and Exits at once. You can still customize each subclass separately (see the Python docs on [multiple inheritance](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance) for details).
|
||||
|
||||
For example:
|
||||
Here is an example:
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/objects.py
|
||||
|
|
@ -91,139 +116,68 @@ class ObjectParent:
|
|||
def at_pre_get(self, getter, **kwargs):
|
||||
# make all entities by default un-pickable
|
||||
return False
|
||||
|
||||
class Object(ObjectParent, DefaultObject):
|
||||
# replaces at_pre_get with its own
|
||||
def at_pre_get(self, getter, **kwargs):
|
||||
return True
|
||||
|
||||
# each in their respective modules ...
|
||||
|
||||
class Character(ObjectParent, DefaultCharacter):
|
||||
# will inherit at_pre_get from ObjectParent
|
||||
pass
|
||||
|
||||
class Exit(ObjectParent, DefaultExit):
|
||||
# Overrides and uses the DefaultExit version of at_pre_get instead
|
||||
def at_pre_get(self, getter, **kwargs):
|
||||
return DefaultExit.at_pre_get(self, getter, **kwargs)
|
||||
|
||||
```
|
||||
|
||||
Now all of `Object`, `Exit`. `Room` and `Character` default to not being able to be picked up using the `get` command.
|
||||
|
||||
## Properties and functions on Objects
|
||||
|
||||
Beyond the properties assigned to all [typeclassed](./Typeclasses.md) objects (see that page for a list
|
||||
of those), the Object also has the following custom properties:
|
||||
|
||||
- `aliases` - a handler that allows you to add and remove aliases from this object. Use
|
||||
`aliases.add()` to add a new alias and `aliases.remove()` to remove one.
|
||||
- `aliases` - a handler that allows you to add and remove aliases from this object. Use `aliases.add()` to add a new alias and `aliases.remove()` to remove one.
|
||||
- `location` - a reference to the object currently containing this object.
|
||||
- `home` is a backup location. The main motivation is to have a safe place to move the object to if
|
||||
its `location` is destroyed. All objects should usually have a home location for safety.
|
||||
- `destination` - this holds a reference to another object this object links to in some way. Its
|
||||
main use is for [Exits](./Objects.md#exits), it's otherwise usually unset.
|
||||
- `nicks` - as opposed to aliases, a [Nick](./Nicks.md) holds a convenient nickname replacement for a
|
||||
real name, word or sequence, only valid for this object. This mainly makes sense if the Object is
|
||||
used as a game character - it can then store briefer shorts, example so as to quickly reference game
|
||||
commands or other characters. Use nicks.add(alias, realname) to add a new one.
|
||||
- `account` - this holds a reference to a connected [Account](./Accounts.md) controlling this object (if
|
||||
any). Note that this is set also if the controlling account is *not* currently online - to test if
|
||||
an account is online, use the `has_account` property instead.
|
||||
- `sessions` - if `account` field is set *and the account is online*, this is a list of all active
|
||||
sessions (server connections) to contact them through (it may be more than one if multiple
|
||||
connections are allowed in settings).
|
||||
- `has_account` - a shorthand for checking if an *online* account is currently connected to this
|
||||
object.
|
||||
- `contents` - this returns a list referencing all objects 'inside' this object (i,e. which has this
|
||||
object set as their `location`).
|
||||
- `exits` - this returns all objects inside this object that are *Exits*, that is, has the
|
||||
`destination` property set.
|
||||
- `home` is a backup location. The main motivation is to have a safe place to move the object to if its `location` is destroyed. All objects should usually have a home location for safety.
|
||||
- `destination` - this holds a reference to another object this object links to in some way. Its main use is for [Exits](./Objects.md#exits), it's otherwise usually unset.
|
||||
- `nicks` - as opposed to aliases, a [Nick](./Nicks.md) holds a convenient nickname replacement for a real name, word or sequence, only valid for this object. This mainly makes sense if the Object is used as a game character - it can then store briefer shorts, example so as to quickly reference game commands or other characters. Use nicks.add(alias, realname) to add a new one.
|
||||
- `account` - this holds a reference to a connected [Account](./Accounts.md) controlling this object (if any). Note that this is set also if the controlling account is *not* currently online - to test if an account is online, use the `has_account` property instead.
|
||||
- `sessions` - if `account` field is set *and the account is online*, this is a list of all active sessions (server connections) to contact them through (it may be more than one if multiple connections are allowed in settings).
|
||||
- `has_account` - a shorthand for checking if an *online* account is currently connected to this object.
|
||||
- `contents` - this returns a list referencing all objects 'inside' this object (i,e. which has this object set as their `location`).
|
||||
- `exits` - this returns all objects inside this object that are *Exits*, that is, has the `destination` property set.
|
||||
|
||||
The last two properties are special:
|
||||
|
||||
- `cmdset` - this is a handler that stores all [command sets](./Command-Sets.md) defined on the
|
||||
object (if any).
|
||||
- `cmdset` - this is a handler that stores all [command sets](./Command-Sets.md) defined on the object (if any).
|
||||
- `scripts` - this is a handler that manages [Scripts](./Scripts.md) attached to the object (if any).
|
||||
|
||||
The Object also has a host of useful utility functions. See the function headers in
|
||||
`src/objects/objects.py` for their arguments and more details.
|
||||
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()` - 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).
|
||||
- `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.
|
||||
- `move_to` - perform a full move of this object to a new location. This is the main move method and will call all relevant hooks, do all checks etc.
|
||||
- `clear_exits()` - will delete all [Exits](./Objects.md#exits) to *and* from this object.
|
||||
- `clear_contents()` - this will not delete anything, but rather move all contents (except Exits) to
|
||||
their designated `Home` locations.
|
||||
- `delete()` - deletes this object, first calling `clear_exits()` and
|
||||
`clear_contents()`.
|
||||
- `clear_contents()` - this will not delete anything, but rather move all contents (except Exits) to their designated `Home` locations.
|
||||
- `delete()` - deletes this object, first calling `clear_exits()` and `clear_contents()`.
|
||||
|
||||
The Object Typeclass defines many more *hook methods* beyond `at_object_creation`. Evennia calls
|
||||
these hooks at various points. When implementing your custom objects, you will inherit from the
|
||||
base parent and overload these hooks with your own custom code. See `evennia.objects.objects` for an
|
||||
updated list of all the available hooks or the [API for DefaultObject here](evennia.objects.objects.DefaultObject).
|
||||
The Object Typeclass defines many more *hook methods* beyond `at_object_creation`. Evennia calls these hooks at various points. When implementing your custom objects, you will inherit from the base parent and overload these hooks with your own custom code. See `evennia.objects.objects` for an updated list of all the available hooks or the [API for DefaultObject here](evennia.objects.objects.DefaultObject).
|
||||
|
||||
## Subclasses of `Object`
|
||||
|
||||
There are three special subclasses of *Object* in default Evennia - *Characters*, *Rooms* and
|
||||
*Exits*. The reason they are separated is because these particular object types are fundamental,
|
||||
something you will always need and in some cases requires some extra attention in order to be
|
||||
recognized by the game engine (there is nothing stopping you from redefining them though). In
|
||||
practice they are all pretty similar to the base Object.
|
||||
There are three special subclasses of *Object* in default Evennia - *Characters*, *Rooms* and *Exits*. The reason they are separated is because these particular object types are fundamental, something you will always need and in some cases requires some extra attention in order to be recognized by the game engine (there is nothing stopping you from redefining them though). In practice they are all pretty similar to the base Object.
|
||||
|
||||
### Characters
|
||||
|
||||
Characters are objects controlled by [Accounts](./Accounts.md). When a new Account
|
||||
logs in to Evennia for the first time, a new `Character` object is created and
|
||||
the Account object is assigned to the `account` attribute. A `Character` object
|
||||
must have a [Default Commandset](./Command-Sets.md) set on itself at
|
||||
creation, or the account will not be able to issue any commands! If you just
|
||||
inherit your own class from `evennia.DefaultCharacter` and make sure to use
|
||||
`super()` to call the parent methods you should be fine. In
|
||||
`mygame/typeclasses/characters.py` is an empty `Character` class ready for you
|
||||
to modify.
|
||||
Characters are objects controlled by [Accounts](./Accounts.md). When a new Account logs in to Evennia for the first time, a new `Character` object is created and the Account object is assigned to the `account` attribute. A `Character` object must have a [Default Commandset](./Command-Sets.md) set on itself at creation, or the account will not be able to issue any commands! If you just inherit your own class from `evennia.DefaultCharacter` and make sure to use `super()` to call the parent methods you should be fine. In `mygame/typeclasses/characters.py` is an empty `Character` class ready for you to modify.
|
||||
|
||||
### Rooms
|
||||
|
||||
*Rooms* are the root containers of all other objects. The only thing really separating a room from
|
||||
any other object is that they have no `location` of their own and that default commands like `@dig`
|
||||
creates objects of this class - so if you want to expand your rooms with more functionality, just
|
||||
inherit from `ev.DefaultRoom`. In `mygame/typeclasses/rooms.py` is an empty `Room` class ready for
|
||||
you to modify.
|
||||
*Rooms* are the root containers of all other objects. The only thing really separating a room from any other object is that they have no `location` of their own and that default commands like `@dig` creates objects of this class - so if you want to expand your rooms with more functionality, just inherit from `ev.DefaultRoom`. In `mygame/typeclasses/rooms.py` is an empty `Room` class ready for you to modify.
|
||||
|
||||
### Exits
|
||||
|
||||
*Exits* are objects connecting other objects (usually *Rooms*) together. An object named *North* or
|
||||
*in* might be an exit, as well as *door*, *portal* or *jump out the window*. An exit has two things
|
||||
that separate them from other objects. Firstly, their *destination* property is set and points to a
|
||||
valid object. This fact makes it easy and fast to locate exits in the database. Secondly, exits
|
||||
define a special [Transit Command](./Commands.md) on themselves when they are created. This command is
|
||||
named the same as the exit object and will, when called, handle the practicalities of moving the
|
||||
character to the Exits's *destination* - this allows you to just enter the name of the exit on its
|
||||
own to move around, just as you would expect.
|
||||
*Exits* are objects connecting other objects (usually *Rooms*) together. An object named *North* or *in* might be an exit, as well as *door*, *portal* or *jump out the window*. An exit has two things that separate them from other objects. Firstly, their *destination* property is set and points to a valid object. This fact makes it easy and fast to locate exits in the database. Secondly, exits define a special [Transit Command](./Commands.md) on themselves when they are created. This command is named the same as the exit object and will, when called, handle the practicalities of moving the character to the Exits's *destination* - this allows you to just enter the name of the exit on its own to move around, just as you would expect.
|
||||
|
||||
The exit functionality is all defined on the Exit typeclass, so you could in principle completely
|
||||
change how exits work in your game (it's not recommended though, unless you really know what you are
|
||||
doing). Exits are [locked](./Locks.md) using an access_type called *traverse* and also make use of a few
|
||||
hook methods for giving feedback if the traversal fails. See `evennia.DefaultExit` for more info.
|
||||
In `mygame/typeclasses/exits.py` there is an empty `Exit` class for you to modify.
|
||||
The exit functionality is all defined on the Exit typeclass, so you could in principle completely change how exits work in your game (it's not recommended though, unless you really know what you are doing). Exits are [locked](./Locks.md) using an access_type called *traverse* and also make use of a few hook methods for giving feedback if the traversal fails. See `evennia.DefaultExit` for more info. In `mygame/typeclasses/exits.py` there is an empty `Exit` class for you to modify.
|
||||
|
||||
The process of traversing an exit is as follows:
|
||||
|
||||
1. The traversing `obj` sends a command that matches the Exit-command name on the Exit object. The
|
||||
[cmdhandler](./Commands.md) detects this and triggers the command defined on the Exit. Traversal always
|
||||
involves the "source" (the current location) and the `destination` (this is stored on the Exit
|
||||
object).
|
||||
1. The traversing `obj` sends a command that matches the Exit-command name on the Exit object. The [cmdhandler](./Commands.md) detects this and triggers the command defined on the Exit. Traversal always involves the "source" (the current location) and the `destination` (this is stored on the Exit object).
|
||||
1. The Exit command checks the `traverse` lock on the Exit object
|
||||
1. The Exit command triggers `at_traverse(obj, destination)` on the Exit object.
|
||||
1. In `at_traverse`, `object.move_to(destination)` is triggered. This triggers the following hooks,
|
||||
in order:
|
||||
1. In `at_traverse`, `object.move_to(destination)` is triggered. This triggers the following hooks, in order:
|
||||
1. `obj.at_pre_move(destination)` - if this returns False, move is aborted.
|
||||
1. `origin.at_pre_leave(obj, destination)`
|
||||
1. `obj.announce_move_from(destination)`
|
||||
|
|
@ -233,6 +187,4 @@ in order:
|
|||
1. `obj.at_post_move(source)`
|
||||
1. On the Exit object, `at_post_traverse(obj, source)` is triggered.
|
||||
|
||||
If the move fails for whatever reason, the Exit will look for an Attribute `err_traverse` on itself
|
||||
and display this as an error message. If this is not found, the Exit will instead call
|
||||
`at_failed_traverse(obj)` on itself.
|
||||
If the move fails for whatever reason, the Exit will look for an Attribute `err_traverse` on itself and display this as an error message. If this is not found, the Exit will instead call `at_failed_traverse(obj)` on itself.
|
||||
|
|
@ -1,23 +1,19 @@
|
|||
# Sessions
|
||||
|
||||
```
|
||||
┌──────┐ │ ┌───────┐ ┌───────┐ ┌──────┐
|
||||
│Client├─┼──►│Session├───►│Account├──►│Object│
|
||||
└──────┘ │ └───────┘ └───────┘ └──────┘
|
||||
^
|
||||
```
|
||||
|
||||
An Evennia *Session* represents one single established connection to the server. Depending on the
|
||||
Evennia session, it is possible for a person to connect multiple times, for example using different
|
||||
clients in multiple windows. Each such connection is represented by a session object.
|
||||
|
||||
A session object has its own [cmdset](./Command-Sets.md), usually the "unloggedin" cmdset. This is what
|
||||
is used to show the login screen and to handle commands to create a new account (or
|
||||
[Account](./Accounts.md) in evennia lingo) read initial help and to log into the game with an existing
|
||||
account. A session object can either be "logged in" or not. Logged in means that the user has
|
||||
authenticated. When this happens the session is associated with an Account object (which is what
|
||||
holds account-centric stuff). The account can then in turn puppet any number of objects/characters.
|
||||
A session object has its own [cmdset](./Command-Sets.md), usually the "unloggedin" cmdset. This is what is used to show the login screen and to handle commands to create a new account (or [Account](./Accounts.md) in evennia lingo) read initial help and to log into the game with an existing account. A session object can either be "logged in" or not. Logged in means that the user has authenticated. When this happens the session is associated with an Account object (which is what holds account-centric stuff). The account can then in turn puppet any number of objects/characters.
|
||||
|
||||
> Warning: A Session is not *persistent* - it is not a [Typeclass](./Typeclasses.md) and has no
|
||||
connection to the database. The Session will go away when a user disconnects and you will lose any
|
||||
custom data on it if the server reloads. The `.db` handler on Sessions is there to present a uniform
|
||||
API (so you can assume `.db` exists even if you don't know if you receive an Object or a Session),
|
||||
but this is just an alias to `.ndb`. So don't store any data on Sessions that you can't afford to
|
||||
lose in a reload. You have been warned.
|
||||
A Session is not *persistent* - it is not a [Typeclass](./Typeclasses.md) and has no connection to the database. The Session will go away when a user disconnects and you will lose any custom data on it if the server reloads. The `.db` handler on Sessions is there to present a uniform API (so you can assume `.db` exists even if you don't know if you receive an Object or a Session), but this is just an alias to `.ndb`. So don't store any data on Sessions that you can't afford to lose in a reload.
|
||||
|
||||
## Properties on Sessions
|
||||
|
||||
|
|
@ -42,41 +38,59 @@ Session statistics are mainly used internally by Evennia.
|
|||
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:
|
||||
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.
|
||||
* `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.
|
||||
```
|
||||
┌──────┐ │ ┌───────┐ ┌───────┐ ┌─────────┐
|
||||
│Client├─┼──►│Session├───►│Account├──►│Character│
|
||||
└──────┘ │ └───────┘ └───────┘ └─────────┘
|
||||
```
|
||||
* `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.
|
||||
```
|
||||
│
|
||||
┌──────┐ │ ┌───────┐
|
||||
│Client├─┼──►│Session├──┐
|
||||
└──────┘ │ └───────┘ └──►┌───────┐ ┌─────────┐
|
||||
│ │Account├──►│Character│
|
||||
┌──────┐ │ ┌───────┐ ┌──►└───────┘ └─────────┘
|
||||
│Client├─┼──►│Session├──┘
|
||||
└──────┘ │ └───────┘
|
||||
│
|
||||
```
|
||||
|
||||
> Note that even if multiple Sessions puppet one Character, there is only ever one instance of that
|
||||
Character.
|
||||
* `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.
|
||||
```
|
||||
│ ┌───────┐
|
||||
┌──────┐ │ ┌───────┐ │Account│ ┌─────────┐
|
||||
│Client├─┼──►│Session├──┐ │ │ ┌►│Character│
|
||||
└──────┘ │ └───────┘ └──┼───────┼──┘ └─────────┘
|
||||
│ │ │
|
||||
┌──────┐ │ ┌───────┐ ┌──┼───────┼──┐ ┌─────────┐
|
||||
│Client├─┼──►│Session├──┘ │ │ └►│Character│
|
||||
└──────┘ │ └───────┘ │ │ └─────────┘
|
||||
│ └───────┘
|
||||
```
|
||||
* `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.
|
||||
```
|
||||
│ ┌───────┐
|
||||
┌──────┐ │ ┌───────┐ │Account│ ┌─────────┐
|
||||
│Client├─┼──►│Session├──┐ │ │ ┌►│Character│
|
||||
└──────┘ │ └───────┘ └──┼───────┼──┘ └─────────┘
|
||||
│ │ │
|
||||
┌──────┐ │ ┌───────┐ ┌──┼───────┼──┐
|
||||
│Client├─┼──►│Session├──┘ │ │ └►┌─────────┐
|
||||
└──────┘ │ └───────┘ │ │ │Character│
|
||||
│ │ │ ┌►└─────────┘
|
||||
┌──────┐ │ ┌───────┐ ┌──┼───────┼──┘ ▼
|
||||
│Client├─┼──►│Session├──┘ │ │
|
||||
└──────┘ │ └───────┘ └───────┘
|
||||
│
|
||||
```
|
||||
|
||||
> Note that even if multiple Sessions puppet one Character, there is only ever one instance of that Character.
|
||||
|
||||
## Returning data to the session
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@
|
|||
different game entities as Python classes, without having to modify the database schema for every
|
||||
new type.
|
||||
|
||||
In Evennia the most important game entities, [Accounts](./Accounts.md), [Objects](./Objects.md),
|
||||
[Scripts](./Scripts.md) and [Channels](./Channels.md) are all Python classes inheriting, at
|
||||
varying distance, from `evennia.typeclasses.models.TypedObject`. In the documentation we refer to
|
||||
these objects as being "typeclassed" or even "being a typeclass".
|
||||
In Evennia the most important game entities, [Accounts](./Accounts.md), [Objects](./Objects.md), [Scripts](./Scripts.md) and [Channels](./Channels.md) are all Python classes inheriting, at varying distance, from `evennia.typeclasses.models.TypedObject`. In the documentation we refer to these objects as being "typeclassed" or even "being a typeclass".
|
||||
|
||||
This is how the inheritance looks for the typeclasses in Evennia:
|
||||
|
||||
|
|
@ -66,12 +63,7 @@ default library):
|
|||
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::
|
||||
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):
|
||||
|
|
@ -86,8 +78,7 @@ 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:
|
||||
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
|
||||
|
|
@ -247,8 +238,7 @@ 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.
|
||||
due to how Python inheritance works your changes will automatically be applied to all children once you have reloaded the server.
|
||||
|
||||
However, database-saved data, like `db_*` fields, [Attributes](./Attributes.md), [Tags](./Tags.md) etc, are
|
||||
not themselves embedded into the class and will *not* be updated automatically. This you need to
|
||||
|
|
@ -281,7 +271,7 @@ comprehensions](http://www.secnetix.de/olli/Python/list_comprehensions.hawk), li
|
|||
line break, that's only for readability in the wiki):
|
||||
|
||||
```
|
||||
@py from typeclasses.furniture import Furniture;
|
||||
py from typeclasses.furniture import Furniture;
|
||||
[obj.at_object_creation() for obj in Furniture.objects.all() if not obj.db.worth]
|
||||
```
|
||||
|
||||
|
|
@ -290,20 +280,15 @@ 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:
|
||||
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
|
||||
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.
|
||||
- `/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:
|
||||
|
||||
|
|
@ -312,44 +297,25 @@ 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).
|
||||
The arguments to this method are described [in the API docs here](github:evennia.typeclasses.models#typedobjectswap_typeclass).
|
||||
|
||||
|
||||
## How typeclasses actually work
|
||||
|
||||
*This is considered an advanced section.*
|
||||
|
||||
Technically, typeclasses are [Django proxy
|
||||
models](https://docs.djangoproject.com/en/1.7/topics/db/models/#proxy-models). The only database
|
||||
models that are "real" in the typeclass system (that is, are represented by actual tables in the
|
||||
database) are `AccountDB`, `ObjectDB`, `ScriptDB` and `ChannelDB` (there are also
|
||||
[Attributes](./Attributes.md) and [Tags](./Tags.md) but they are not typeclasses themselves). All the
|
||||
subclasses of them are "proxies", extending them with Python code without actually modifying the
|
||||
database layout.
|
||||
Technically, typeclasses are [Django proxy models](https://docs.djangoproject.com/en/1.7/topics/db/models/#proxy-models). The only database
|
||||
models that are "real" in the typeclass system (that is, are represented by actual tables in the database) are `AccountDB`, `ObjectDB`, `ScriptDB` and `ChannelDB` (there are also [Attributes](./Attributes.md) and [Tags](./Tags.md) but they are not typeclasses themselves). All the subclasses of them are "proxies", extending them with Python code without actually modifying the database layout.
|
||||
|
||||
Evennia modifies Django's proxy model in various ways to allow them to work without any boiler plate
|
||||
(for example you don't need to set the Django "proxy" property in the model `Meta` subclass, Evennia
|
||||
handles this for you using metaclasses). Evennia also makes sure you can query subclasses as well as
|
||||
patches django to allow multiple inheritance from the same base class.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
## Will I run out of dbrefs?
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue