Add support for all creation kwargs to GLOBAL_SCRIPT setting. Resolve #2373

This commit is contained in:
Griatch 2021-08-06 20:31:01 +02:00
parent 2c3fd143cc
commit 3f436a5bb2
2 changed files with 110 additions and 101 deletions

View file

@ -5,37 +5,37 @@
*Scripts* are the out-of-character siblings to the in-character
[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`.
(depending on what you'd use them for) would be `OOBObjects`, `StorageContainers` or `TimerObjects`.
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.
- 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
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
- 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
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
- 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
In previus Evennia versions, stopping the Script's timer also meant deleting the Script object.
Starting with this version, the timer can be start/stopped separately and `.delete()` must be called
on the Script explicitly to delete it.
```
```
### In-game command examples
There are two main commands controlling scripts in the default cmdset:
@ -43,7 +43,7 @@ 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
@ -70,7 +70,7 @@ Create script with timer component:
```python
# (note that this will call `timed_script.at_repeat` which is empty by default)
timed_script = evennia.create_script(key="Timed script",
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() )
@ -78,7 +78,7 @@ timed_script = evennia.create_script(key="Timed script",
# manipulate the script's timer
timed_script.stop()
timed_script.start()
timed_script.pause()
timed_script.pause()
timed_script.unpause()
```
@ -96,7 +96,7 @@ Search/find scripts in various ways:
# 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
# search through Evennia's GLOBAL_SCRIPTS container (based on
# script's key only)
from evennia import GLOBAL_SCRIPTS
@ -119,7 +119,7 @@ A Script is defined as a class and is created in the same way as other
### Simple storage script
In `mygame/typeclasses/scripts.py` is an empty `Script` class already set up. You
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
@ -143,7 +143,7 @@ Once created, this simple Script could act as a global storage:
```python
evennia.create_script('typeclasses.scripts.MyScript')
# from somewhere else
# from somewhere else
myscript = evennia.search_script("myscript")
bar = myscript.db.foo
@ -158,11 +158,11 @@ you set in your `at_script_creation`:
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
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.
@ -182,44 +182,44 @@ class TimerScript(Script):
self.interval = 60 # 1 min repeat
def at_repeat(self):
# do stuff every minute
# do stuff every minute
```
This example will call `at_repeat` every minute. The `create_script` function has an `autostart=True` keyword
set by default - this means the script's timer component will be started automatically. Otherwise
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
- `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
- `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
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
- `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.
- `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:
delete the Script object itself (use `.delete()` for this).
- `.at_repeat()` - this method is called every `interval` seconds while the timer is
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.
- `.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
- `.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.
@ -231,22 +231,22 @@ The timer component is controlled with methods on the Script class:
#### 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
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
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
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
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
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
@ -256,16 +256,16 @@ If so, the 'parent object' will be available to the script as either `.obj` or `
```python
# mygame/typeclasses/scripts.py
# mygame/typeclasses/scripts.py
# Script class is defined at the top of this module
import random
class Weather(Script):
class Weather(Script):
"""
A timer script that displays weather info. Meant to
be attached to a room.
A timer script that displays weather info. Meant to
be attached to a room.
"""
def at_script_creation(self):
self.key = "weather_script"
@ -273,12 +273,12 @@ If so, the 'parent object' will be available to the script as either `.obj` or `
self.interval = 60 * 5 # every 5 minutes
def at_repeat(self):
"called every self.interval seconds."
"called every self.interval seconds."
rand = random.random()
if rand < 0.5:
weather = "A faint breeze is felt."
elif rand < 0.7:
weather = "Clouds sweep across the sky."
weather = "Clouds sweep across the sky."
else:
weather = "There is a light drizzle of rain."
# send this message to everyone inside the object this
@ -286,24 +286,24 @@ If so, the 'parent object' will be available to the script as either `.obj` or `
self.obj.msg_contents(weather)
```
If attached to a room, this Script 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.
```python
myroom.scripts.add(scripts.Weather)
```
> Note that `typeclasses` in your game dir is added to the setting `TYPECLASS_PATHS`.
> Note that `typeclasses` in your game dir is added to the setting `TYPECLASS_PATHS`.
> Therefore we don't need to give the full path (`typeclasses.scripts.Weather`
> but only `scripts.Weather` above.
You can also attach the script as part of creating it:
You can also attach the script as part of creating it:
```python
create_script('typeclasses.weather.Weather', obj=myroom)
```
## Other Script methods
## 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
@ -354,9 +354,9 @@ GLOBAL_SCRIPTS.weather.db.current_weather = "Cloudy"
```
```warning::
Note that global scripts appear as properties on `GLOBAL_SCRIPTS` based on their `key`.
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 `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.
```
@ -364,9 +364,11 @@ GLOBAL_SCRIPTS.weather.db.current_weather = "Cloudy"
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
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.
This is very useful for scripts that must always exist and/or should be auto-created
when your server restarts. If you use this method, you must make sure all
script keys are globally unique.
Here's how to tell Evennia to manage the script in settings:
@ -374,7 +376,7 @@ Here's how to tell Evennia to manage the script in settings:
# in mygame/server/conf/settings.py
GLOBAL_SCRIPTS = {
"my_script": {
"my_script": {
"typeclass": "scripts.Weather",
"repeats": -1,
"interval": 50,
@ -383,15 +385,16 @@ GLOBAL_SCRIPTS = {
"storagescript": {}
}
```
Above we add two scripts with keys `myscript` and `storagescript`respectively. The following dict
can be empty - the `settings.BASE_SCRIPT_TYPECLASS` will then be used. Under the hood, the provided
dict (along with the `key`) will be passed into `create_script` automatically, so
all the [same keyword arguments as for create_script](api:evennia.utils.create.create_script) are
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.
```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
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.
```
@ -402,12 +405,12 @@ from evennia import GLOBAL_SCRIPTS
# Delete the script
GLOBAL_SCRIPTS.storagescript.delete()
# running the `scripts` command now will show no storagescript
# but below it's automatically recreated again!
# but below it's automatically recreated again!
storage = GLOBAL_SCRIPTS.storagescript
```
That is, if the script is deleted, next time you get it from `GLOBAL_SCRIPTS`, Evennia will use the
information in settings to recreate it for you on the fly.
information in settings to recreate it for you on the fly.
## Hints: Dealing with Script Errors
@ -422,13 +425,13 @@ traceback occurred in your script.
from evennia.utils import logger
class Weather(Script):
class Weather(Script):
# [...]
def at_repeat(self):
try:
try:
# [...]
except Exception:
logger.log_trace()
logger.log_trace()