mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Update scripts command, Scripts documentation
This commit is contained in:
parent
b5195a6e96
commit
e1762c8b2f
9 changed files with 386 additions and 318 deletions
|
|
@ -1,75 +1,262 @@
|
|||
# Scripts
|
||||
|
||||
[Script API reference](api:evennia.scripts.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`.
|
||||
[Objects](./Objects). Scripts are so flexible that the name "Script" is a bit limiting
|
||||
in itself - but we had to pick _something_ to name them. Other possible names
|
||||
(depending on what you'd use them for) would be `OOBObjects`, `StorageContainers` or `TimerObjects`.
|
||||
|
||||
Scripts can be used for many different things in Evennia:
|
||||
If you ever consider creating an [Object](./Objects) with a `None`-location just to store some game data,
|
||||
you should really be using a Script instead.
|
||||
|
||||
- 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 full [Typeclassed](./Typeclasses) entities - they have [Attributes](./Attributes) and
|
||||
can be modified in the same way. But they have _no in-game existence_, so no
|
||||
location or command-execution like [Objects](./Objects) and no connection to a particular
|
||||
player/session like [Accounts](./Accounts). This means they are perfectly suitable for acting
|
||||
as database-storage backends for game _systems_: Storing the current state of the economy,
|
||||
who is involved in the current fight, tracking an ongoing barter and so on. They are great as
|
||||
persistent system handlers.
|
||||
- Scripts have an optional _timer component_. This means that you can set up the script
|
||||
to tick the `at_repeat` hook on the Script at a certain interval. The timer can be controlled
|
||||
independently of the rest of the script as needed. This component is optional
|
||||
and complementary to other timing functions in Evennia, like
|
||||
[evennia.utils.delay](api:evennia.utils.utils#evennia.utils.utils.delay) and
|
||||
[evennia.utils.repeat](api:evennia.utils.utils#evennia.utils.utils.repeat).
|
||||
- Scripts can _attach_ to Objects and Accounts via e.g. `obj.scripts.add/remove`. In the
|
||||
script you can then access the object/account as `self.obj` or `self.account`. This can be used to
|
||||
dynamically extend other typeclasses but also to use the timer component to affect the parent object
|
||||
in various ways. For historical reasons, a Script _not_ attached to an object is referred to as a
|
||||
_Global_ Script.
|
||||
|
||||
```versionchanged:: 1.0
|
||||
In previus Evennia versions, stopping the Script's timer also meant deleting the Script object.
|
||||
Starting with this version, the timer can be start/stopped separately and `.delete()` must be called
|
||||
on the Script explicitly to delete it.
|
||||
|
||||
Scripts are [Typeclassed](./Typeclasses) entities and are manipulated in a similar
|
||||
way to how it works for other such Evennia entities:
|
||||
```
|
||||
|
||||
### In-game command examples
|
||||
|
||||
There are two main commands controlling scripts in the default cmdset:
|
||||
|
||||
The `addscript` command is used for attaching scripts to existing objects:
|
||||
|
||||
> addscript obj = bodyfunctions.BodyFunctions
|
||||
|
||||
The `scripts` command is used to view all scripts and perform operations on them:
|
||||
|
||||
> scripts
|
||||
> scripts/stop bodyfunctions.BodyFunctions
|
||||
> scripts/start #244
|
||||
> scripts/pause #11
|
||||
> scripts/delete #566
|
||||
|
||||
```versionchanged:: 1.0
|
||||
The `addscript` command used to be only `script` which was easy to confuse with `scripts`.
|
||||
```
|
||||
|
||||
### Code examples
|
||||
|
||||
Here are some examples of working with Scripts in-code (more details to follow in later
|
||||
sections).
|
||||
|
||||
Create a new script:
|
||||
```python
|
||||
new_script = evennia.create_script(key="myscript", typeclass=...)
|
||||
```
|
||||
|
||||
Create script with timer component:
|
||||
|
||||
```python
|
||||
# create a new script
|
||||
new_script = evennia.create_script(key="myscript", typeclass=...)
|
||||
# (note that this will call `timed_script.at_repeat` which is empty by default)
|
||||
timed_script = evennia.create_script(key="Timed script",
|
||||
interval=34, # seconds <=0 means off
|
||||
start_delay=True, # wait interval before first call
|
||||
autostart=True) # start timer (else needing .start() )
|
||||
|
||||
# search (this is always a list, also if there is only one match)
|
||||
list_of_myscript = evennia.search_script("myscript")
|
||||
# manipulate the script's timer
|
||||
timed_script.stop()
|
||||
timed_script.start()
|
||||
timed_script.pause()
|
||||
timed_script.unpause()
|
||||
```
|
||||
|
||||
Attach script to another object:
|
||||
|
||||
```python
|
||||
myobj.scripts.add(new_script)
|
||||
myobj.scripts.add(evennia.DefaultScript)
|
||||
all_scripts_on_obj = myobj.scripts.all()
|
||||
```
|
||||
|
||||
Search/find scripts in various ways:
|
||||
|
||||
```python
|
||||
# regular search (this is always a list, also if there is only one match)
|
||||
list_of_myscripts = evennia.search_script("myscript")
|
||||
|
||||
# search through Evennia's GLOBAL_SCRIPTS container (based on
|
||||
# script's key only)
|
||||
from evennia import GLOBAL_SCRIPTS
|
||||
|
||||
myscript = GLOBAL_SCRIPTS.myscript
|
||||
GLOBAL_SCRIPTS.get("Timed script").db.foo = "bar"
|
||||
```
|
||||
|
||||
Delete the Script (this will also stop its timer):
|
||||
|
||||
```python
|
||||
new_script.delete()
|
||||
timed_script.delete()
|
||||
```
|
||||
|
||||
## Defining new Scripts
|
||||
|
||||
A Script is defined as a class and is created in the same way as other
|
||||
[typeclassed](./Typeclasses) 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).
|
||||
[typeclassed](./Typeclasses) entities. The parent class is `evennia.DefaultScript`.
|
||||
|
||||
This you can do for example in the module
|
||||
`evennia/typeclasses/scripts.py`. Below is an example Script
|
||||
Typeclass.
|
||||
|
||||
### Simple storage script
|
||||
|
||||
In `mygame/typeclasses/scripts.py` is an empty `Script` class already set up. You
|
||||
can use this as a base for your own scripts.
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/scripts.py
|
||||
|
||||
from evennia import DefaultScript
|
||||
|
||||
class MyScript(DefaultScript):
|
||||
class Script(DefaultScript):
|
||||
# stuff common for all your scripts goes here
|
||||
|
||||
class MyScript(Script):
|
||||
def at_script_creation(selfself):
|
||||
"""Called once, when script is first created"""
|
||||
self.key = "myscript"
|
||||
self.db.foo = "bar"
|
||||
|
||||
```
|
||||
|
||||
Once created, this simple Script could act as a global storage:
|
||||
|
||||
```python
|
||||
evennia.create_script('typeclasses.scripts.MyScript')
|
||||
|
||||
# from somewhere else
|
||||
|
||||
myscript = evennia.search_script("myscript")
|
||||
bar = myscript.db.foo
|
||||
myscript.db.something_else = 1000
|
||||
|
||||
```
|
||||
|
||||
Note that if you give keyword arguments to `create_script` you can override the values
|
||||
you set in your `at_script_creation`:
|
||||
|
||||
```python
|
||||
|
||||
evennia.create_script('typeclasses.scripts.MyScript', key="another name",
|
||||
attributes=[("foo", "bar-alternative")])
|
||||
|
||||
|
||||
```
|
||||
|
||||
See the [create_script](api:evennia.utils.create#evennia.utils.create.create_script) and
|
||||
[search_script](api:evennia.utils.search#evennia.utils.search.search_script) API documentation for more options
|
||||
on creating and finding Scripts.
|
||||
|
||||
|
||||
### Timed Scripts
|
||||
|
||||
There are several properties one can set on the Script to control its timer component.
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/scripts.py
|
||||
|
||||
class TimerScript(Script):
|
||||
|
||||
def at_script_creation(self):
|
||||
self.key = "myscript"
|
||||
self.desc = "An example script"
|
||||
self.interval = 60 # 1 min repeat
|
||||
|
||||
def at_repeat(self):
|
||||
# do stuff every minute
|
||||
|
||||
```
|
||||
|
||||
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:
|
||||
This example will call `at_repeat` every minute. The `create_script` function has an `autostart=True` keyword
|
||||
set by default - this means the script's timer component will be started automatically. Otherwise
|
||||
`.start()` must be called separately.
|
||||
|
||||
Supported properties are:
|
||||
|
||||
- `key` (str): The name of the script. This makes it easier to search for it later. If it's a script
|
||||
attached to another object one can also get all scripts off that object and get the script that way.
|
||||
- `desc` (str): Note - not `.db.desc`! This is a database field on the Script shown in script listings
|
||||
to help identifying what does what.
|
||||
- `interval` (int): The amount of time (in seconds) between every 'tick' of the timer. Note that
|
||||
it's generally bad practice to use sub-second timers for anything in a text-game - the player will
|
||||
not be able to appreciate the precision (and if you print it, it will just spam the screen). For
|
||||
calculations you can pretty much always do them on-demand, or at a much slower interval without the
|
||||
player being the wiser.
|
||||
- `start_delay` (bool): If timer should start right away or wait `interval` seconds first.
|
||||
- `repeats` (int): If >0, the timer will only run this many times before stopping. Otherwise the
|
||||
number of repeats are infinite. If set to 1, the Script mimics a `delay` action.
|
||||
- `persistent` (bool): This defaults to `True` and means the timer will survive a server reload/reboot.
|
||||
If not, a reload will have the timer come back in a stopped state. Setting this to `False` will _not_
|
||||
delete the Script object itself (use `.delete()` for this).
|
||||
|
||||
The timer component is controlled with methods on the Script class:
|
||||
|
||||
- `.at_repeat()` - this method is called every `interval` seconds while the timer is
|
||||
active.
|
||||
- `.is_valid()` - this method is called by the timer just before `at_repeat()`. If it returns `False`
|
||||
the timer is immediately stopped.
|
||||
- `.start()` - start/update the timer. If keyword arguments are given, they can be used to
|
||||
change `interval`, `start_delay` etc on the fly. This calls the `.at_start()` hook.
|
||||
This is also called after a server reload assuming the timer was not previously stopped.
|
||||
- `.update()` - legacy alias for `.start`.
|
||||
- `.stop()` - stops and resets the timer. This calls the `.at_stop()` hook.
|
||||
- `.pause()` - pauses the timer where it is, storing its current position. This calls
|
||||
the `.at_pause(manual_pause=True)` hook. This is also called on a server reload/reboot,
|
||||
at which time the `manual_pause` will be `False`.
|
||||
- `.unpause()` - unpause a previously paused script. This will call the `at_start` hook.
|
||||
- `.time_until_next_repeat()` - get the time until next time the timer fires.
|
||||
- `.remaining_repeats()` - get the number of repeats remaining, or `None` if repeats are infinite.
|
||||
- `.reset_callcount()` - this resets the repeat counter to start over from 0. Only useful if `repeats>0`.
|
||||
- `.force_repeat()` - this prematurely forces `at_repeat` to be called right away. Doing so will reset the
|
||||
countdown so that next call will again happen after `interval` seconds.
|
||||
|
||||
#### Script timers vs delay/repeat
|
||||
|
||||
If the _only_ goal is to get a repeat/delay effect, the
|
||||
[evennia.utils.delay](api:evennia.utils.utils#evennia.utils.utils.delay) and
|
||||
[evennia.utils.repeat](api:evennia.utils.utils#evennia.utils.utils.repeat) functions
|
||||
should generally be considered first. A Script is a lot 'heavier' to create/delete on the fly.
|
||||
In fact, for making a single delayed call (`script.repeats==1`), the `utils.delay` call is
|
||||
probably always the better choice.
|
||||
|
||||
For repeating tasks, the `utils.repeat` is optimized for quick repeating of a large number of objects. It
|
||||
uses the TickerHandler under the hood. Its subscription-based model makes it very efficient to
|
||||
start/stop the repeating action for an object. The side effect is however that all objects set to tick
|
||||
at a given interval will _all do so at the same time_. This may or may not look strange in-game depending
|
||||
on the situation. By contrast the Script uses its own ticker that will operate independently from the
|
||||
tickers of all other Scripts.
|
||||
|
||||
It's also worth noting that once the script object has _already been created_,
|
||||
starting/stopping/pausing/unpausing the timer has very little overhead. The pause/unpause and update
|
||||
methods of the script also offers a bit more fine-control than using `utils.delays/repeat`.
|
||||
|
||||
### Script attached to another object
|
||||
|
||||
Scripts can be attached to an [Account](./Accounts) or (more commonly) an [Object](./Objects).
|
||||
If so, the 'parent object' will be available to the script as either `.obj` or `.account`.
|
||||
|
||||
|
||||
```python
|
||||
# for example in mygame/typeclasses/scripts.py
|
||||
# mygame/typeclasses/scripts.py
|
||||
# Script class is defined at the top of this module
|
||||
|
||||
import random
|
||||
|
|
@ -84,7 +271,6 @@ Here's an example:
|
|||
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."
|
||||
|
|
@ -100,14 +286,10 @@ Here's an example:
|
|||
self.obj.msg_contents(weather)
|
||||
```
|
||||
|
||||
If we put this script on a room, it will randomly report some weather
|
||||
If attached to a room, this Script 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`:
|
||||
|
||||
```
|
||||
```python
|
||||
myroom.scripts.add(scripts.Weather)
|
||||
```
|
||||
|
||||
|
|
@ -115,204 +297,122 @@ put it on a room called `myroom`:
|
|||
> 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:
|
||||
You can also attach the script as part of creating it:
|
||||
|
||||
```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
|
||||
## Other Script methods
|
||||
|
||||
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_script_creation()` - this is only called once - when the script is first created.
|
||||
- `at_server_reload()` - this is called whenever the server is warm-rebooted (e.g. with the
|
||||
`@reload` command). It's a good place to save non-persistent data you might want to survive a
|
||||
`reload` command). It's a good place to save non-persistent data you might want to survive a
|
||||
reload.
|
||||
- `at_server_shutdown()` - this is called when a system reset or systems shutdown is invoked.
|
||||
- `at_server_start()` - this is called when the server comes back (from reload/shutdown/reboot). It
|
||||
can be usuful for initializations and caching of non-persistent data when starting up a script's
|
||||
functionality.
|
||||
- `at_repeat()`
|
||||
- `at_start()`
|
||||
- `at_pause()`
|
||||
- `at_stop()`
|
||||
- `delete()` - same as for other typeclassed entities, this will delete the Script. Of note is that
|
||||
it will also stop the timer (if it runs), leading to the `at_stop` hook being called.
|
||||
|
||||
Running methods (usually called automatically by the engine, but possible to also invoke manually)
|
||||
In addition, Scripts support [Attributes](./Attributes), [Tags](./Tags) and [Locks](./Locks) etc like other
|
||||
Typeclassed entities.
|
||||
|
||||
- `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.
|
||||
See also the methods involved in controlling a [Timed Script](#Timed_Scripts) above.
|
||||
|
||||
## The GLOBAL_SCRIPTS container
|
||||
|
||||
## Global Scripts
|
||||
A Script not attached to another entity is commonly referred to as a _Global_ script since it't available
|
||||
to access from anywhere. This means they need to be searched for in order to be used.
|
||||
|
||||
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:
|
||||
Evennia supplies a convenient "container" `evennia.GLOBAL_SCRIPTS` to help organize your global
|
||||
scripts. All you need is the Script's `key`.
|
||||
|
||||
```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
|
||||
|
||||
# access as a property on the container, named the same as the key
|
||||
my_script = GLOBAL_SCRIPTS.my_script
|
||||
# needed if there are spaces in name or name determined on the fly
|
||||
another_script = GLOBAL_SCRIPTS.get("another script")
|
||||
# get all global scripts (this returns a Queryset)
|
||||
# get all global scripts (this returns a Django Queryset)
|
||||
all_scripts = GLOBAL_SCRIPTS.all()
|
||||
# you can operate directly on the script
|
||||
GLOBAL_SCRIPTS.weather.db.current_weather = "Cloudy"
|
||||
|
||||
```
|
||||
|
||||
> 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.
|
||||
```warning::
|
||||
Note that global scripts appear as properties on `GLOBAL_SCRIPTS` based on their `key`.
|
||||
If you were to create two global scripts with the same `key` (even with different typeclasses),
|
||||
the `GLOBAL_SCRIPTS` container will only return one of them (which one depends on order in
|
||||
the database). Best is to organize your scripts so that this does not happen. Otherwise, use
|
||||
`evennia.search_scripts` to get exactly the script you want.
|
||||
```
|
||||
|
||||
There are two ways to make a script appear as a property on `GLOBAL_SCRIPTS`. 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`:
|
||||
There are two ways to make a script appear as a property on `GLOBAL_SCRIPTS`:
|
||||
|
||||
1. Manually create a new global script with a `key` using `create_script`.
|
||||
2. Define the script's properties in the `GLOBAL_SCRIPTS` settings variable. This tells Evennia
|
||||
that it should check if a script with that `key` exists and if not, create it for you.
|
||||
This is very useful for scripts that must always exist and/or should be auto-created with your server.
|
||||
|
||||
Here's how to tell Evennia to manage the script in settings:
|
||||
|
||||
```python
|
||||
# in mygame/server/conf/settings.py
|
||||
|
||||
GLOBAL_SCRIPTS = {
|
||||
"my_script": {
|
||||
"typeclass": "scripts.Weather",
|
||||
"repeats": -1,
|
||||
"interval": 50,
|
||||
"desc": "Weather script"
|
||||
"persistent": True
|
||||
},
|
||||
"storagescript": {
|
||||
"typeclass": "scripts.Storage",
|
||||
"persistent": True
|
||||
}
|
||||
"storagescript": {}
|
||||
}
|
||||
```
|
||||
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.
|
||||
Above we add two scripts with keys `myscript` and `storagescript`respectively. The following dict
|
||||
can be empty - the `settings.BASE_SCRIPT_TYPECLASS` will then be used. Under the hood, the provided
|
||||
dict (along with the `key`) will be passed into `create_script` automatically, so
|
||||
all the [same keyword arguments as for create_script](api:evennia.utils.create.create_script) are
|
||||
supported here.
|
||||
|
||||
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.
|
||||
```warning::
|
||||
Before setting up Evennia to manage your script like this, make sure that your Script typeclass
|
||||
does not have any critical errors (test it separately). If there are, you'll see errors in your log
|
||||
and your Script will temporarily fall back to being a `DefaultScript` type.
|
||||
```
|
||||
|
||||
Moreover, a script defined this way is *guaranteed* to exist when you try to access it:
|
||||
|
||||
```python
|
||||
from evennia import GLOBAL_SCRIPTS
|
||||
# first stop the script
|
||||
GLOBAL_SCRIPTS.storagescript.stop()
|
||||
# Delete the script
|
||||
GLOBAL_SCRIPTS.storagescript.delete()
|
||||
# running the `scripts` command now will show no storagescript
|
||||
# but below now it's recreated again!
|
||||
# but below it's automatically recreated again!
|
||||
storage = GLOBAL_SCRIPTS.storagescript
|
||||
```
|
||||
That is, if the script is deleted, next time you get it from `GLOBAL_SCRIPTS`, 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).
|
||||
That is, if the script is deleted, next time you get it from `GLOBAL_SCRIPTS`, Evennia will use the
|
||||
information in settings to recreate it for you on the fly.
|
||||
|
||||
## Dealing with Errors
|
||||
|
||||
Errors inside an timed, executing script can sometimes be rather terse or point to
|
||||
## Hints: Dealing with Script Errors
|
||||
|
||||
Errors inside a timed, executing script can sometimes be rather terse or point to
|
||||
parts of the execution mechanism that is hard to interpret. One way to make it
|
||||
easier to debug scripts is to import Evennia's native logger and wrap your
|
||||
functions in a try/catch block. Evennia's logger can show you where the
|
||||
|
|
@ -322,45 +422,13 @@ traceback occurred in your script.
|
|||
|
||||
from evennia.utils import logger
|
||||
|
||||
class Weather(DefaultScript):
|
||||
class Weather(Script):
|
||||
|
||||
# [...]
|
||||
|
||||
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).
|
||||
logger.log_trace()
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
evennia.contrib.tutorial\_examples.cmdset\_red\_button
|
||||
=============================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorial_examples.cmdset_red_button
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
evennia.contrib.tutorial\_examples.red\_button\_scripts
|
||||
==============================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorial_examples.red_button_scripts
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
@ -12,9 +12,7 @@ evennia.contrib.tutorial\_examples
|
|||
:maxdepth: 6
|
||||
|
||||
evennia.contrib.tutorial_examples.bodyfunctions
|
||||
evennia.contrib.tutorial_examples.cmdset_red_button
|
||||
evennia.contrib.tutorial_examples.example_batch_code
|
||||
evennia.contrib.tutorial_examples.mirror
|
||||
evennia.contrib.tutorial_examples.red_button
|
||||
evennia.contrib.tutorial_examples.red_button_scripts
|
||||
evennia.contrib.tutorial_examples.tests
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Toc
|
||||
- [API root](api/evennia-api.rst)
|
||||
|
||||
- [Coding/Coding Introduction](Coding/Coding-Introduction)
|
||||
- [Coding/Coding Overview](Coding/Coding-Overview)
|
||||
- [Coding/Continuous Integration](Coding/Continuous-Integration)
|
||||
|
|
|
|||
|
|
@ -3059,7 +3059,7 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
|
|||
attach a script to an object
|
||||
|
||||
Usage:
|
||||
script[/switch] <obj> [= script_path or <scriptkey>]
|
||||
addscript[/switch] <obj> [= script_path or <scriptkey>]
|
||||
|
||||
Switches:
|
||||
start - start all non-running scripts on object, or a given script only
|
||||
|
|
@ -3074,8 +3074,8 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
|
|||
the object.
|
||||
"""
|
||||
|
||||
key = "script"
|
||||
aliases = "addscript"
|
||||
key = "addscript"
|
||||
aliases = ["attachscript"]
|
||||
switch_options = ("start", "stop")
|
||||
locks = "cmd:perm(script) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
|
|
|||
|
|
@ -460,7 +460,7 @@ class ScriptEvMore(EvMore):
|
|||
rept = "-/-"
|
||||
|
||||
table.add_row(
|
||||
script.id,
|
||||
f"#{script.id}",
|
||||
f"{script.obj.key}({script.obj.dbref})"
|
||||
if (hasattr(script, "obj") and script.obj)
|
||||
else "<Global>",
|
||||
|
|
@ -477,15 +477,19 @@ class ScriptEvMore(EvMore):
|
|||
|
||||
class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
list and manage all running scripts
|
||||
List and manage all running scripts. Allows for creating new global
|
||||
scripts.
|
||||
|
||||
Usage:
|
||||
scripts[/switches] [#dbref, key, script.path or <obj>]
|
||||
script[/switches] [#dbref, key, script.path or <obj>]
|
||||
|
||||
Switches:
|
||||
start - start a script (must supply a script path)
|
||||
stop - stops an existing script
|
||||
kill - kills a script - without running its cleanup hooks
|
||||
create - create a new global script of given typeclass path. This will
|
||||
auto-start the script's timer if it has one.
|
||||
start - start/unpause an existing script's timer.
|
||||
stop - stops an existing script's timer
|
||||
pause - pause a script's timer
|
||||
delete - deletes script. This will also stop the timer as needed
|
||||
|
||||
If no switches are given, this command just views all active
|
||||
scripts. The argument can be either an object, at which point it
|
||||
|
|
@ -493,76 +497,100 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
or #dbref. For using the /stop switch, a unique script #dbref is
|
||||
required since whole classes of scripts often have the same name.
|
||||
|
||||
Use script for managing commands on objects.
|
||||
Use the `script` build-level command for managing scripts attached to
|
||||
objects.
|
||||
|
||||
"""
|
||||
|
||||
key = "scripts"
|
||||
aliases = ["globalscript", "listscripts"]
|
||||
switch_options = ("start", "stop", "kill")
|
||||
aliases = ["scripts"]
|
||||
switch_options = ("create", "start", "stop", "pause", "delete")
|
||||
locks = "cmd:perm(listscripts) or perm(Admin)"
|
||||
help_category = "System"
|
||||
|
||||
excluded_typeclass_paths = ["evennia.prototypes.prototypes.DbPrototype"]
|
||||
|
||||
switch_mapping = {
|
||||
"create": "|gCreated|n",
|
||||
"start": "|gStarted|n",
|
||||
"stop": "|RStopped|n",
|
||||
"pause": "|Paused|n",
|
||||
"delete": "|rDeleted|n"
|
||||
}
|
||||
|
||||
def _search_script(self, args):
|
||||
# test first if this is a script match
|
||||
scripts = ScriptDB.objects.get_all_scripts(key=args)
|
||||
if scripts:
|
||||
return scripts
|
||||
# try typeclass path
|
||||
scripts = ScriptDB.objects.filter(db_typeclass_path__iendswith=args)
|
||||
if scripts:
|
||||
return scripts
|
||||
# try to find an object instead.
|
||||
objects = ObjectDB.objects.object_search(args)
|
||||
if objects:
|
||||
scripts = ScriptDB.objects.filter(db_obj__in=objects)
|
||||
return scripts
|
||||
|
||||
def func(self):
|
||||
"""implement method"""
|
||||
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
|
||||
if args:
|
||||
if "start" in self.switches:
|
||||
# global script-start mode
|
||||
new_script = create.create_script(args)
|
||||
if new_script:
|
||||
caller.msg("Global script %s was started successfully." % args)
|
||||
else:
|
||||
caller.msg("Global script %s could not start correctly. See logs." % args)
|
||||
if "create" in self.switches:
|
||||
# global script-start mode
|
||||
verb = self.switch_mapping['create']
|
||||
if not args:
|
||||
caller.msg("Usage script/create <key or typeclass>")
|
||||
return
|
||||
|
||||
# test first if this is a script match
|
||||
scripts = ScriptDB.objects.get_all_scripts(key=args)
|
||||
if not scripts:
|
||||
# try to find an object instead.
|
||||
objects = ObjectDB.objects.object_search(args)
|
||||
if objects:
|
||||
scripts = []
|
||||
for obj in objects:
|
||||
# get all scripts on the object(s)
|
||||
scripts.extend(ScriptDB.objects.get_all_scripts_on_obj(obj))
|
||||
else:
|
||||
# we want all scripts.
|
||||
scripts = ScriptDB.objects.get_all_scripts()
|
||||
if not scripts:
|
||||
caller.msg("No scripts are running.")
|
||||
return
|
||||
# filter any found scripts by tag category.
|
||||
scripts = scripts.exclude(db_typeclass_path__in=self.excluded_typeclass_paths)
|
||||
|
||||
if not scripts:
|
||||
string = "No scripts found with a key '%s', or on an object named '%s'." % (args, args)
|
||||
caller.msg(string)
|
||||
new_script = create.create_script(args)
|
||||
if new_script:
|
||||
caller.msg(f"Global Script {verb} - {new_script.key} ({new_script.typeclass_path})")
|
||||
ScriptEvMore(caller, [new_script], session=self.session)
|
||||
else:
|
||||
caller.msg(f"Global Script |rNOT|n {verb} |r(see log)|n - arguments: {args}")
|
||||
return
|
||||
|
||||
if self.switches and self.switches[0] in ("stop", "del", "delete", "kill"):
|
||||
# we want to delete something
|
||||
if len(scripts) == 1:
|
||||
# we have a unique match!
|
||||
if "kill" in self.switches:
|
||||
string = "Killing script '%s'" % scripts[0].key
|
||||
scripts[0].stop(kill=True)
|
||||
else:
|
||||
string = "Stopping script '%s'." % scripts[0].key
|
||||
scripts[0].stop()
|
||||
# import pdb # DEBUG
|
||||
# pdb.set_trace() # DEBUG
|
||||
caller.msg(string)
|
||||
else:
|
||||
# multiple matches.
|
||||
ScriptEvMore(caller, scripts, session=self.session)
|
||||
caller.msg("Multiple script matches. Please refine your search")
|
||||
# all other switches require existing scripts
|
||||
if args:
|
||||
scripts = self._search_script(args)
|
||||
if not scripts:
|
||||
caller.msg(f"No scripts found matching '{args}'.")
|
||||
return
|
||||
else:
|
||||
# No stopping or validation. We just want to view things.
|
||||
scripts = ScriptDB.objects.all()
|
||||
if not scripts:
|
||||
caller.msg("No scripts found.")
|
||||
return
|
||||
|
||||
if args and self.switches:
|
||||
# global script-modifying mode
|
||||
if scripts.count() > 1:
|
||||
caller.msg("Multiple script matches. Please refine your search.")
|
||||
return
|
||||
script = scripts[0]
|
||||
script_key = script.key
|
||||
script_typeclass_path = script.typeclass_path
|
||||
for switch in self.switches:
|
||||
verb = self.switch_mapping[switch]
|
||||
msgs = []
|
||||
try:
|
||||
getattr(script, switch)()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
msgs.append(f"Global Script |rNOT|n {verb} |r(see log)|n - "
|
||||
f"{script_key} ({script_typeclass_path})|n")
|
||||
else:
|
||||
msgs.append(f"Global Script {verb} - "
|
||||
f"{script_key} ({script_typeclass_path})")
|
||||
caller.msg("\n".join(msgs))
|
||||
if "delete" not in self.switches:
|
||||
ScriptEvMore(caller, [script], session=self.session)
|
||||
return
|
||||
else:
|
||||
# simply show the found scripts
|
||||
ScriptEvMore(caller, scripts.order_by("id"), session=self.session)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -515,19 +515,8 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
"""
|
||||
self._start_task(interval=interval, start_delay=start_delay, repeats=repeats, **kwargs)
|
||||
|
||||
def update(self, interval=None, start_delay=None, repeats=None, **kwargs):
|
||||
"""
|
||||
Update the Script's timer component with new settings.
|
||||
|
||||
Keyword Args:
|
||||
interval (int): How often to fire `at_repeat` in seconds.
|
||||
start_delay (int): If the start of ticking should be delayed.
|
||||
repeats (int): How many repeats. 0 for infinite repeats.
|
||||
**kwargs: Optional (default unused) kwargs passed on into the `at_start` hook.
|
||||
|
||||
"""
|
||||
self._start_task(interval=interval, start_delay=start_delay,
|
||||
repeats=repeats, force_restart=True, **kwargs)
|
||||
# legacy alias
|
||||
update = start
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1083,9 +1083,8 @@ def repeat(interval, callback, persistent=True, idstring="", stop=False,
|
|||
Returns:
|
||||
tuple or None: This is the `store_key` - the identifier for the created ticker.
|
||||
Store this and pass into unrepat() in order to to stop this ticker
|
||||
later. It this lost you need to stop the ticker via TICKER_HANDLER.remove
|
||||
by supplying all the same arguments
|
||||
directly. No return if `stop=True`
|
||||
later. It this lost you need to stop the ticker via `TICKER_HANDLER.remove`
|
||||
by supplying all the same arguments directly. No return if `stop=True`
|
||||
|
||||
Raises:
|
||||
KeyError: If trying to stop a ticker that was not found.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue