diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f4d92d266..41bde9c50b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Evennia Main branch + +- Feature: Add `ON_DEMAND_HANDLER.set_dt(key, category, dt)` and + `.set_stage(key, category, stage)` to allow manual tweaking of task timings, + for example for a spell speeding a plant's growth. (Griatch) +- Fix: Resolve a bug when loading on-demand-handler data from database. + ## Evennia 3.2.0 Feb 25, 2024 diff --git a/evennia/scripts/ondemandhandler.py b/evennia/scripts/ondemandhandler.py index e9282edd17..6aa7b8fd05 100644 --- a/evennia/scripts/ondemandhandler.py +++ b/evennia/scripts/ondemandhandler.py @@ -199,11 +199,17 @@ class OnDemandTask: self.iterations = 0 # only used with looping staging functions self.stages = None + self.stages_by_name = None if isinstance(stages, dict): # sort the stages by ending time, inserting each state as {dt: (statename, callable)} _stages = {} for dt, tup in stages.items(): + # validate the input + if not isinstance(dt, (int, float)): + raise ValueError( + "Each stage must given as a time-delta in seconds (int or float)." + ) if is_iter(tup): if len(tup) != 2: raise ValueError( @@ -218,6 +224,7 @@ class OnDemandTask: _stages[dt] = tup self.stages = {dt: tup for dt, tup in sorted(_stages.items(), reverse=True)} + self.stages_by_name = {tup[0]: dt for dt, tup in self.stages.items()} self.check(autostart=autostart) @@ -306,6 +313,23 @@ class OnDemandTask: return self.check()[0] + def set_dt(self, dt): + """ + Set the time-delta since the task started manually. This allows you to 'cheat' the system + and set the time manually. This is useful for testing or when a system manipulates the state + somehow (like using a potion that speeds up the growth of a plant). + + Args: + dt (int): The time-delta to set. This is an absolute value in seconds, same as returned + by `get_dt`. + + Notes: + Setting this will not on its own trigger any stage functions - this will only happen + as normal, next time the state is checked and the stage is found to have changed. + + """ + self.start_time = OnDemandTask.runtime() - dt + def get_stage(self): """ Get the current stage of the task. If no stage was given, this will return `None` but @@ -317,6 +341,30 @@ class OnDemandTask: """ return self.check()[1] + def set_stage(self, stage=None): + """ + Set the stage of the task manually. This allows you to 'cheat' the system and set the stage + manually. This is useful for testing or when a system manipulates the state somehow (like + using a potion that speeds up the growth of a plant). The given stage must be previously + created for the given task. If task has no stages, this will do nothing. + + Args: + stage (str, optional): The stage to set. If `None`, the task will be reset to its + initial (first) state. + + Notes: + Setting this will not on its own trigger any stage functions - this will only happen + as normal, next time the state is checked and the stage is found to have changed. + + """ + if not self.stages: + return + + if stage is None: + self.start_time = OnDemandTask.runtime() - min(self.stages.keys()) + elif stage in self.stages_by_name: + self.start_time = OnDemandTask.runtime() - self.stages_by_name[stage] + class OnDemandHandler: """ @@ -338,7 +386,7 @@ class OnDemandHandler: This should be automatically called when Evennia starts. """ - self.tasks = ServerConfig.objects.conf("on_demand_timers", default=dict) + self.tasks = dict(ServerConfig.objects.conf("on_demand_timers", default=dict)) def save(self): """ @@ -520,6 +568,30 @@ class OnDemandHandler: task = self.get(key, category) return task.get_dt() if task else None + def set_dt(self, key, category, dt): + """ + Set the time-delta since the task started manually. This allows you to 'cheat' the system + and set the time manually. This is useful for testing or when a system manipulates the state + somehow (like using a potion that speeds up the growth of a plant). + + Args: + key (str, callable, OnDemandTask or Object): The unique identifier for the task. If a + callable, will be called without arguments. If an Object, will be converted to a string. + If an `OnDemandTask`, then all other arguments are ignored and the task will be used + to identify the task to set the time-delta for. + category (str, optional): The category of the task. + dt (int): The time-delta to set. This is an absolute value in seconds, same as returned + by `get_dt`. + + Notes: + Setting this will not on its own trigger any stage functions - this will only happen + as normal, next time the state is checked and the stage is found to have changed. + + """ + task = self.get(key, category) + if task: + task.set_dt(dt) + def get_stage(self, key, category=None): """ Get the current stage of an on-demand task. @@ -537,6 +609,31 @@ class OnDemandHandler: task = self.get(key, category) return task.get_stage() if task else None + def set_stage(self, key, category=None, stage=None): + """ + Set the stage of an on-demand task manually. This allows you to 'cheat' the system and set + the stage manually. This is useful for testing or when a system manipulates the state + somehow (like using a potion that speeds up the growth of a plant). The given stage must + be previously created for the given task. If task has no stages, this will do nothing. + + Args: + key (str, callable, OnDemandTask or Object): The unique identifier for the task. If a + callable, will be called without arguments. If an Object, will be converted to a + string. If an `OnDemandTask`, then all other arguments are ignored and the task + will be used to identify the task to set the stage for. + category (str, optional): The category of the task. + stage (str, optional): The stage to set. If `None`, the task will be reset to its + initial (first) state. + + Notes: + Setting this will not on its own trigger any stage functions - this will only happen + as normal, next time the state is checked and the stage is found to have changed. + + """ + task = self.get(key, category) + if task: + task.set_stage(stage) + # Create singleton ON_DEMAND_HANDLER = OnDemandHandler() diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 631a4c66e9..33bb48ccfa 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -6,8 +6,6 @@ Unit tests for the scripts package from collections import defaultdict from unittest import TestCase, mock -from parameterized import parameterized - from evennia import DefaultScript from evennia.objects.objects import DefaultObject from evennia.scripts.manager import ScriptDBManager @@ -19,6 +17,7 @@ from evennia.scripts.tickerhandler import TickerHandler from evennia.utils.create import create_script from evennia.utils.dbserialize import dbserialize from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTest +from parameterized import parameterized class TestScript(BaseEvenniaTest): @@ -628,3 +627,51 @@ class TestOnDemandHandler(EvenniaTest): self.assertEqual(self.handler.get_dt("daffodil", "flower"), 10000) self.assertEqual(self.handler.get_stage("rose", "flower"), "dead") self.assertEqual(self.handler.get_stage("daffodil", "flower"), "dead") + + @mock.patch("evennia.scripts.ondemandhandler.OnDemandTask.runtime") + def test_set_dt(self, mock_runtime): + START_TIME = 0 + + mock_runtime.return_value = START_TIME + self.handler.batch_add(self.task1, self.task2) + + for task in self.handler.tasks.values(): + task.start_time = START_TIME + + self.assertEqual(self.handler.get_stage("rose", "flower"), "seedling") + self.assertEqual(self.handler.get_stage("daffodil", "flower"), "seedling") + + self.handler.set_dt("rose", "flower", 100) + self.handler.set_dt("daffodil", "flower", 150) + self.assertEquals( + [task.start_time for task in self.handler.tasks.values()], + [START_TIME - 100, START_TIME - 150], + ) + self.assertEqual(self.handler.get_dt("rose", "flower"), 100) + self.assertEqual(self.handler.get_dt("daffodil", "flower"), 150) + self.assertEqual(self.handler.get_stage("rose", "flower"), "bud") + self.assertEqual(self.handler.get_stage("daffodil", "flower"), "wilted") + + @mock.patch("evennia.scripts.ondemandhandler.OnDemandTask.runtime") + def test_set_stage(self, mock_runtime): + START_TIME = 0 + + mock_runtime.return_value = START_TIME + self.handler.batch_add(self.task1, self.task2) + + for task in self.handler.tasks.values(): + task.start_time = START_TIME + + self.assertEqual(self.handler.get_stage("rose", "flower"), "seedling") + self.assertEqual(self.handler.get_stage("daffodil", "flower"), "seedling") + + self.handler.set_stage("rose", "flower", "bud") + self.handler.set_stage("daffodil", "flower", "wilted") + self.assertEquals( + [task.start_time for task in self.handler.tasks.values()], + [START_TIME - 100, START_TIME - 150], + ) + self.assertEqual(self.handler.get_dt("rose", "flower"), 100) + self.assertEqual(self.handler.get_dt("daffodil", "flower"), 150) + self.assertEqual(self.handler.get_stage("rose", "flower"), "bud") + self.assertEqual(self.handler.get_stage("daffodil", "flower"), "wilted")