diff --git a/docs/source/Components/Scripts.md b/docs/source/Components/Scripts.md index f8eb121e45..cdee64e849 100644 --- a/docs/source/Components/Scripts.md +++ b/docs/source/Components/Scripts.md @@ -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() \ No newline at end of file + logger.log_trace() diff --git a/evennia/utils/containers.py b/evennia/utils/containers.py index fa4faa1bfb..8c32d6ba1b 100644 --- a/evennia/utils/containers.py +++ b/evennia/utils/containers.py @@ -11,6 +11,7 @@ evennia.OPTION_CLASSES """ +from pickle import dumps from django.conf import settings from evennia.utils.utils import class_from_module, callables_from_module from evennia.utils import logger @@ -132,37 +133,42 @@ class GlobalScriptContainer(Container): self.load_data() typeclass = self.typeclass_storage[key] - found = typeclass.objects.filter(db_key=key).first() - interval = self.loaded_data[key].get("interval", None) - start_delay = self.loaded_data[key].get("start_delay", None) - repeats = self.loaded_data[key].get("repeats", 0) - desc = self.loaded_data[key].get("desc", "") + script = typeclass.objects.filter( + db_key=key, db_account__isnull=True, db_obj__isnull=True).first() - if not found: + kwargs = {**self.loaded_data[key]} + kwargs['key'] = key + kwargs['persistent'] = kwargs.get('persistent', True) + + compare_hash = str(dumps(kwargs, protocol=4)) + + if script: + script_hash = script.attributes.get("global_script_settings", category="settings_hash") + if script_hash is None: + # legacy - store the hash anew and assume no change + script.attributes.add("global_script_settings", compare_hash, + category="settings_hash") + elif script_hash != compare_hash: + # wipe the old version and create anew + logger.log_info(f"GLOBAL_SCRIPTS: Settings changed for {key} ({typeclass}).") + script.stop() + script.delete() + script = None + + if not script: logger.log_info(f"GLOBAL_SCRIPTS: (Re)creating {key} ({typeclass}).") - new_script, errors = typeclass.create( - key=key, - persistent=True, - interval=interval, - start_delay=start_delay, - repeats=repeats, - desc=desc, - ) + + script, errors = typeclass.create(**kwargs) if errors: logger.log_err("\n".join(errors)) return None - new_script.start() - return new_script + # store a hash representation of the setup + script.attributes.add("_global_script_settings", + compare_hash, category="settings_hash") + script.start() - if ((found.interval != interval) - or (found.start_delay != start_delay) - or (found.repeats != repeats)): - # the setup changed - found.start(interval=interval, start_delay=start_delay, repeats=repeats) - if found.desc != desc: - found.desc = desc - return found + return script def start(self): """