From e1762c8b2fbc7250166ce841f475f7213c4c5dd5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 7 Mar 2021 17:41:48 +0100 Subject: [PATCH] Update scripts command, Scripts documentation --- docs/source/Components/Scripts.md | 516 ++++++++++-------- ...ib.tutorial_examples.cmdset_red_button.rst | 7 - ...b.tutorial_examples.red_button_scripts.rst | 7 - .../api/evennia.contrib.tutorial_examples.rst | 2 - docs/source/toc.md | 2 +- evennia/commands/default/building.py | 6 +- evennia/commands/default/system.py | 144 +++-- evennia/scripts/scripts.py | 15 +- evennia/utils/utils.py | 5 +- 9 files changed, 386 insertions(+), 318 deletions(-) delete mode 100644 docs/source/api/evennia.contrib.tutorial_examples.cmdset_red_button.rst delete mode 100644 docs/source/api/evennia.contrib.tutorial_examples.red_button_scripts.rst diff --git a/docs/source/Components/Scripts.md b/docs/source/Components/Scripts.md index 2526bf017c..f8eb121e45 100644 --- a/docs/source/Components/Scripts.md +++ b/docs/source/Components/Scripts.md @@ -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). \ No newline at end of file + logger.log_trace() \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.tutorial_examples.cmdset_red_button.rst b/docs/source/api/evennia.contrib.tutorial_examples.cmdset_red_button.rst deleted file mode 100644 index 4686f928d2..0000000000 --- a/docs/source/api/evennia.contrib.tutorial_examples.cmdset_red_button.rst +++ /dev/null @@ -1,7 +0,0 @@ -evennia.contrib.tutorial\_examples.cmdset\_red\_button -============================================================= - -.. automodule:: evennia.contrib.tutorial_examples.cmdset_red_button - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/evennia.contrib.tutorial_examples.red_button_scripts.rst b/docs/source/api/evennia.contrib.tutorial_examples.red_button_scripts.rst deleted file mode 100644 index 157747b84e..0000000000 --- a/docs/source/api/evennia.contrib.tutorial_examples.red_button_scripts.rst +++ /dev/null @@ -1,7 +0,0 @@ -evennia.contrib.tutorial\_examples.red\_button\_scripts -============================================================== - -.. automodule:: evennia.contrib.tutorial_examples.red_button_scripts - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/evennia.contrib.tutorial_examples.rst b/docs/source/api/evennia.contrib.tutorial_examples.rst index 96abd21912..b6935063e2 100644 --- a/docs/source/api/evennia.contrib.tutorial_examples.rst +++ b/docs/source/api/evennia.contrib.tutorial_examples.rst @@ -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 diff --git a/docs/source/toc.md b/docs/source/toc.md index 5ad330aff6..b2e6dbbe51 100644 --- a/docs/source/toc.md +++ b/docs/source/toc.md @@ -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) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 38bb5231a3..50b57e41ff 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -3059,7 +3059,7 @@ class CmdScript(COMMAND_DEFAULT_CLASS): attach a script to an object Usage: - script[/switch] [= script_path or ] + addscript[/switch] [= script_path or ] 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" diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index 100a6bb085..69b688e7b2 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -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 "", @@ -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 ] + script[/switches] [#dbref, key, script.path or ] 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 ") 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) diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index 1c457647c8..f10edebcb6 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -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): """ diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 3d6e185da9..2356ceef61 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -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.