Address PR comments.

This commit is contained in:
Owllex 2021-12-13 11:34:19 -08:00
parent 189124c751
commit c796161e55
2 changed files with 149 additions and 98 deletions

View file

@ -15,18 +15,21 @@ state. They do not fire callbacks, so are not a good fit for use cases
where something needs to happen on a specific schedule (use delay or where something needs to happen on a specific schedule (use delay or
a TickerHandler for that instead). a TickerHandler for that instead).
See also the evennia documentation for command cooldowns
(https://github.com/evennia/evennia/wiki/Command-Cooldown) for more information
about the concept.
Installation: Installation:
To use, simply add the following property to the typeclass definition of To use, simply add the following property to the typeclass definition of any
any object type that you want to support cooldowns. It will expose a object type that you want to support cooldowns. It will expose a new `cooldowns`
new `cooldowns` property that persists data to the object's attribute property that persists data to the object's attribute storage. You can set this
storage. You can set this on your base `Object` typeclass to enable cooldown on your base `Object` typeclass to enable cooldown tracking on every kind of
tracking on every kind of object, or just put it on your `Character` object, or just put it on your `Character` typeclass.
typeclass.
By default the CooldownHandler will use the `cooldowns` property, but you By default the CooldownHandler will use the `cooldowns` property, but you can
can customize this if desired by passing a different value for the customize this if desired by passing a different value for the db_attribute
db_attribute parameter. parameter.
from evennia.contrib.cooldowns import Cooldownhandler from evennia.contrib.cooldowns import Cooldownhandler
from evennia.utils.utils import lazy_property from evennia.utils.utils import lazy_property
@ -37,14 +40,16 @@ db_attribute parameter.
Example: Example:
Assuming you've installed cooldowns on your Character typeclasses, you can Assuming you've installed cooldowns on your Character typeclasses, you can use a
use a cooldown to limit how often you can perform a command: cooldown to limit how often you can perform a command. The following code
snippet will limit the use of a Power Attack command to once every 10 seconds
per character.
class PowerAttack(Command): class PowerAttack(Command):
def func(self): def func(self):
if self.caller.cooldowns.ready("power attack"): if self.caller.cooldowns.ready("power attack"):
self.do_power_attack() self.do_power_attack()
self.caller.cooldowns.set("power attack", 10) self.caller.cooldowns.add("power attack", 10)
else: else:
self.caller.msg("That's not ready yet!") self.caller.msg("That's not ready yet!")
""" """
@ -55,42 +60,47 @@ import time
class CooldownHandler: class CooldownHandler:
""" """
Handler for cooldowns. This can be attached to any object that Handler for cooldowns. This can be attached to any object that supports DB
supports DB attributes (like a Character or Account). attributes (like a Character or Account).
A cooldown is a timer that is usually used to limit how often A cooldown is a timer that is usually used to limit how often some action
some action can be performed or some effect can trigger. When a can be performed or some effect can trigger. When a cooldown is first added,
cooldown is first set, it counts down from the amount of time it counts down from the amount of time provided back to zero, at which point
provided back to zero, at which point it is considered ready again. it is considered ready again.
Cooldowns are named with an arbitrary string, and that string is used Cooldowns are named with an arbitrary string, and that string is used to
to check on the progression of the cooldown. Each cooldown is tracked check on the progression of the cooldown. Each cooldown is tracked
separately and independently. separately and independently from other cooldowns on that same object. A
cooldown is unique per-object.
Cooldowns are saved persistently, so they survive reboots. This Cooldowns are saved persistently, so they survive reboots. This module does
module does not register or provide callback functionality for when not register or provide callback functionality for when a cooldown becomes
a cooldown becomes ready again. Users of cooldowns are expected to ready again. Users of cooldowns are expected to query the state of any
query the state of any cooldowns they are interested in. cooldowns they are interested in.
Methods: Methods:
- ready(name): Checks whether a given cooldown name is ready. - ready(name): Checks whether a given cooldown name is ready.
- time_left(name): Returns how much time is left on a cooldown. - time_left(name): Returns how much time is left on a cooldown.
- set(name, seconds): Sets a given cooldown to last for a certain - add(name, seconds): Sets a given cooldown to last for a certain
amount of time. Until then, ready() will return False for that amount of time. Until then, ready() will return False for that
cooldown name. cooldown name. set() is an alias.
- extend(name, seconds): Like set, but adds time to the given - extend(name, seconds): Like add(), but adds more time to the given
cooldown name. If it doesn't exist yet, calling this is equivalent cooldown if it already exists. If it doesn't exist yet, calling
to calling set. this is equivalent to calling add().
- reset(cooldown): Resets a given cooldown, causing ready() to return - reset(cooldown): Resets a given cooldown, causing ready() to return
True for that cooldown immediately. True for that cooldown immediately.
- clear(): Resets all cooldowns. - clear(): Resets all cooldowns.
""" """
__slots__ = ("data", "db_attribute", "obj")
def __init__(self, obj, db_attribute="cooldowns"): def __init__(self, obj, db_attribute="cooldowns"):
if not obj.attributes.has(db_attribute): if not obj.attributes.has(db_attribute):
obj.attributes.add(db_attribute, {}) obj.attributes.add(db_attribute, {})
self.data = obj.attributes.get(db_attribute) self.data = obj.attributes.get(db_attribute)
self.obj = obj
self.db_attribute = db_attribute
self.cleanup() self.cleanup()
@property @property
@ -102,63 +112,69 @@ class CooldownHandler:
def ready(self, *args): def ready(self, *args):
""" """
Checks whether all of the provided cooldowns are ready (expired). Checks whether all of the provided cooldowns are ready (expired). If a
If a requested cooldown does not exist, it is considered ready. requested cooldown does not exist, it is considered ready.
Args: Args:
any (str): One or more cooldown names to check. *args (str): One or more cooldown names to check.
Returns: Returns:
(bool): True if each cooldown has expired or does not exist. bool: True if each cooldown has expired or does not exist.
""" """
return self.time_left(*args) <= 0 return self.time_left(*args, use_int=True) <= 0
def time_left(self, *args): def time_left(self, *args, use_int=False):
""" """
Returns the maximum amount of time left on one or more given Returns the maximum amount of time left on one or more given cooldowns.
cooldowns. If a requested cooldown does not exist, it is If a requested cooldown does not exist, it is considered to have 0 time
considered to have 0 time left. left.
Args: Args:
any (str): One or more cooldown names to check. *args (str): One or more cooldown names to check.
use_int (bool): True to round the return value up to an int,
False (default) to return a more precise float.
Returns: Returns:
(int): Number of seconds until all provided cooldowns are float or int: Number of seconds until all provided cooldowns are
ready. Returns 0 if all cooldowns are ready (or don't ready. Returns 0 if all cooldowns are ready (or don't exist.)
exist.)
""" """
now = time.time() now = time.time()
cooldowns = [self.data[x] - now for x in args if x in self.data] cooldowns = [self.data[x] - now for x in args if x in self.data]
if not cooldowns: if not cooldowns:
return 0 return 0 if use_int else 0.0
return math.ceil(max(max(cooldowns), 0)) left = max(max(cooldowns), 0)
return math.ceil(left) if use_int else left
def set(self, cooldown, seconds): def add(self, cooldown, seconds):
""" """
Sets a given cooldown to last for a specific amount of time. Adds/sets a given cooldown to last for a specific amount of time.
If this cooldown is already set, this replaces it. If this cooldown already exits, this call replaces it.
Args: Args:
cooldown (str): The name of the cooldown. cooldown (str): The name of the cooldown.
seconds (int): The number of seconds before this cooldown is seconds (float or int): The number of seconds before this cooldown
ready again. is ready again.
""" """
now = time.time() now = time.time()
self.data[cooldown] = int(now) + (max(seconds, 0) if seconds else 0) self.data[cooldown] = now + (max(seconds, 0) if seconds else 0)
set = add
def extend(self, cooldown, seconds): def extend(self, cooldown, seconds):
""" """
Adds a specific amount of time to an existing cooldown. Adds a specific amount of time to an existing cooldown.
If this cooldown is already ready, this is equivalent to calling If this cooldown is already ready, this is equivalent to calling set. If
set. If the cooldown is not ready, it will be extended by the the cooldown is not ready, it will be extended by the provided duration.
provided duration.
Args: Args:
cooldown (str): The name of the cooldown. cooldown (str): The name of the cooldown.
seconds (int): The number of seconds to extend this cooldown. seconds (float or int): The number of seconds to extend this cooldown.
Returns:
float: The number of seconds until the cooldown will be ready again.
""" """
time_left = self.time_left(cooldown) time_left = self.time_left(cooldown) + (seconds if seconds else 0)
self.set(cooldown, time_left + (seconds if seconds else 0)) self.set(cooldown, time_left)
return max(time_left, 0)
def reset(self, cooldown): def reset(self, cooldown):
""" """
@ -174,8 +190,7 @@ class CooldownHandler:
""" """
Resets all cooldowns. Resets all cooldowns.
""" """
for cooldown in list(self.data.keys()): self.data.clear()
del self.data[cooldown]
def cleanup(self): def cleanup(self):
""" """
@ -183,6 +198,10 @@ class CooldownHandler:
requirements small. requirements small.
""" """
now = time.time() now = time.time()
keys = [x for x in self.data.keys() if self.data[x] - now < 0] cooldowns = dict(self.data)
keys = [x for x in cooldowns.keys() if cooldowns[x] - now < 0]
for key in keys: for key in keys:
del self.data[key] del cooldowns[key]
if keys:
self.obj.attributes.add(self.db_attribute, cooldowns)
self.data = self.obj.attributes.get(self.db_attribute)

View file

@ -3447,7 +3447,7 @@ class TestLegacyMuxComms(CommandTest):
from evennia.contrib import cooldowns from evennia.contrib import cooldowns
@patch("evennia.contrib.cooldowns.time.time", return_value=0) @patch("evennia.contrib.cooldowns.time.time", return_value=0.0)
class TestCooldowns(EvenniaTest): class TestCooldowns(EvenniaTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@ -3458,23 +3458,31 @@ class TestCooldowns(EvenniaTest):
self.assertTrue(self.handler.ready("a", "b", "c")) self.assertTrue(self.handler.ready("a", "b", "c"))
self.assertEqual(self.handler.time_left("a", "b", "c"), 0) self.assertEqual(self.handler.time_left("a", "b", "c"), 0)
def test_set(self, mock_time): def test_add(self, mock_time):
self.handler.set("a", 10) self.assertEqual(self.handler.add, self.handler.set)
self.handler.add("a", 10)
self.assertFalse(self.handler.ready("a")) self.assertFalse(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 10) self.assertEqual(self.handler.time_left("a"), 10)
mock_time.return_value = 9.0
mock_time.return_value = 9
self.assertFalse(self.handler.ready("a")) self.assertFalse(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 1) self.assertEqual(self.handler.time_left("a"), 1)
mock_time.return_value = 10.0
mock_time.return_value = 10
self.assertTrue(self.handler.ready("a")) self.assertTrue(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 0) self.assertEqual(self.handler.time_left("a"), 0)
def test_set_multi(self, mock_time): def test_add_float(self, mock_time):
self.handler.set("a", 10) self.assertEqual(self.handler.time_left("a"), 0)
self.handler.set("b", 5) self.assertEqual(self.handler.time_left("a", use_int=False), 0)
self.handler.set("c", 3) self.assertEqual(self.handler.time_left("a", use_int=True), 0)
self.handler.add("a", 5.5)
self.assertEqual(self.handler.time_left("a"), 5.5)
self.assertEqual(self.handler.time_left("a", use_int=False), 5.5)
self.assertEqual(self.handler.time_left("a", use_int=True), 6)
def test_add_multi(self, mock_time):
self.handler.add("a", 10)
self.handler.add("b", 5)
self.handler.add("c", 3)
self.assertFalse(self.handler.ready("a", "b", "c")) self.assertFalse(self.handler.ready("a", "b", "c"))
self.assertEqual(self.handler.time_left("a", "b", "c"), 10) self.assertEqual(self.handler.time_left("a", "b", "c"), 10)
self.assertEqual(self.handler.time_left("a", "b"), 10) self.assertEqual(self.handler.time_left("a", "b"), 10)
@ -3482,47 +3490,59 @@ class TestCooldowns(EvenniaTest):
self.assertEqual(self.handler.time_left("b", "c"), 5) self.assertEqual(self.handler.time_left("b", "c"), 5)
self.assertEqual(self.handler.time_left("c", "c"), 3) self.assertEqual(self.handler.time_left("c", "c"), 3)
def test_set_none(self, mock_time): def test_add_none(self, mock_time):
self.handler.set("a", None) self.handler.add("a", None)
self.assertTrue(self.handler.ready("a")) self.assertTrue(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 0) self.assertEqual(self.handler.time_left("a"), 0)
def test_set_negative(self, mock_time): def test_add_negative(self, mock_time):
self.handler.set("a", -5) self.handler.add("a", -5)
self.assertTrue(self.handler.ready("a")) self.assertTrue(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 0) self.assertEqual(self.handler.time_left("a"), 0)
def test_set_overwrite(self, mock_time): def test_add_overwrite(self, mock_time):
self.handler.set("a", 5) self.handler.add("a", 5)
self.handler.set("a", 10) self.handler.add("a", 10)
self.handler.set("a", 3) self.handler.add("a", 3)
self.assertFalse(self.handler.ready("a")) self.assertFalse(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 3) self.assertEqual(self.handler.time_left("a"), 3)
def test_extend(self, mock_time): def test_extend(self, mock_time):
self.handler.extend("a", 10) self.assertEqual(self.handler.extend("a", 10), 10)
self.assertFalse(self.handler.ready("a")) self.assertFalse(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 10) self.assertEqual(self.handler.time_left("a"), 10)
self.handler.extend("a", 10) self.assertEqual(self.handler.extend("a", 10), 20)
self.assertFalse(self.handler.ready("a")) self.assertFalse(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 20) self.assertEqual(self.handler.time_left("a"), 20)
def test_extend_none(self, mock_time): def test_extend_none(self, mock_time):
self.handler.extend("a", None) self.assertEqual(self.handler.extend("a", None), 0)
self.assertTrue(self.handler.ready("a")) self.assertTrue(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 0) self.assertEqual(self.handler.time_left("a"), 0)
self.handler.set("a", 10) self.handler.add("a", 10)
self.handler.extend("a", None) self.assertEqual(self.handler.extend("a", None), 10)
self.assertEqual(self.handler.time_left("a"), 10) self.assertEqual(self.handler.time_left("a"), 10)
def test_extend_negative(self, mock_time): def test_extend_negative(self, mock_time):
self.handler.extend("a", -5) self.assertEqual(self.handler.extend("a", -5), 0)
self.assertTrue(self.handler.ready("a")) self.assertTrue(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 0) self.assertEqual(self.handler.time_left("a"), 0)
self.handler.set("a", 10) self.handler.add("a", 10)
self.handler.extend("a", -5) self.assertEqual(self.handler.extend("a", -5), 5)
self.assertEqual(self.handler.time_left("a"), 5) self.assertEqual(self.handler.time_left("a"), 5)
def test_extend_float(self, mock_time):
self.assertEqual(self.handler.extend("a", -5.5), 0)
self.assertTrue(self.handler.ready("a"))
self.assertEqual(self.handler.time_left("a"), 0.0)
self.assertEqual(self.handler.time_left("a", use_int=False), 0.0)
self.assertEqual(self.handler.time_left("a", use_int=True), 0)
self.handler.add("a", 10.5)
self.assertEqual(self.handler.extend("a", -5.25), 5.25)
self.assertEqual(self.handler.time_left("a"), 5.25)
self.assertEqual(self.handler.time_left("a", use_int=False), 5.25)
self.assertEqual(self.handler.time_left("a", use_int=True), 6)
def test_reset_non_existent(self, mock_time): def test_reset_non_existent(self, mock_time):
self.handler.reset("a") self.handler.reset("a")
self.assertTrue(self.handler.ready("a")) self.assertTrue(self.handler.ready("a"))
@ -3535,20 +3555,32 @@ class TestCooldowns(EvenniaTest):
self.assertEqual(self.handler.time_left("a"), 0) self.assertEqual(self.handler.time_left("a"), 0)
def test_clear(self, mock_time): def test_clear(self, mock_time):
self.handler.set("a", 10) self.handler.add("a", 10)
self.handler.set("b", 10) self.handler.add("b", 10)
self.handler.set("c", 10) self.handler.add("c", 10)
self.handler.clear() self.handler.clear()
self.assertTrue(self.handler.ready("a", "b", "c")) self.assertTrue(self.handler.ready("a", "b", "c"))
self.assertEqual(self.handler.time_left("a", "b", "c"), 0) self.assertEqual(self.handler.time_left("a", "b", "c"), 0)
def test_cleanup(self, mock_time): def test_cleanup(self, mock_time):
self.handler.set("a", 10) self.handler.add("a", 10)
self.handler.set("b", 5) self.handler.add("b", 5)
self.handler.set("c", 5) self.handler.add("c", 5)
mock_time.return_value = 6 self.handler.add("d", 3.5)
mock_time.return_value = 6.0
self.handler.cleanup() self.handler.cleanup()
self.assertEqual(self.handler.time_left("b", "c"), 0) self.assertEqual(self.handler.time_left("b", "c", "d"), 0)
self.assertEqual(self.handler.time_left("a"), 4) self.assertEqual(self.handler.time_left("a"), 4)
self.assertNotIn("b", self.handler.data) self.assertEqual(list(self.handler.data.keys()), ["a"])
self.assertNotIn("c", self.handler.data)
def test_cleanup_doesnt_delete_anything(self, mock_time):
self.handler.add("a", 10)
self.handler.add("b", 5)
self.handler.add("c", 5)
self.handler.add("d", 3.5)
mock_time.return_value = 1.0
self.handler.cleanup()
self.assertEqual(self.handler.time_left("d"), 2.5)
self.assertEqual(self.handler.time_left("b", "c"), 4)
self.assertEqual(self.handler.time_left("a"), 9)
self.assertEqual(list(self.handler.data.keys()), ["a", "b", "c", "d"])