mirror of
https://github.com/evennia/evennia.git
synced 2026-03-20 23:06:31 +01:00
Address PR comments.
This commit is contained in:
parent
5ce63ae52b
commit
f06b448500
2 changed files with 149 additions and 98 deletions
|
|
@ -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
|
||||
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:
|
||||
|
||||
To use, simply add the following property to the typeclass definition of
|
||||
any object type that you want to support cooldowns. It will expose a
|
||||
new `cooldowns` property that persists data to the object's attribute
|
||||
storage. You can set this on your base `Object` typeclass to enable cooldown
|
||||
tracking on every kind of object, or just put it on your `Character`
|
||||
typeclass.
|
||||
To use, simply add the following property to the typeclass definition of any
|
||||
object type that you want to support cooldowns. It will expose a new `cooldowns`
|
||||
property that persists data to the object's attribute storage. You can set this
|
||||
on your base `Object` typeclass to enable cooldown tracking on every kind of
|
||||
object, or just put it on your `Character` typeclass.
|
||||
|
||||
By default the CooldownHandler will use the `cooldowns` property, but you
|
||||
can customize this if desired by passing a different value for the
|
||||
db_attribute parameter.
|
||||
By default the CooldownHandler will use the `cooldowns` property, but you can
|
||||
customize this if desired by passing a different value for the db_attribute
|
||||
parameter.
|
||||
|
||||
from evennia.contrib.cooldowns import Cooldownhandler
|
||||
from evennia.utils.utils import lazy_property
|
||||
|
|
@ -37,14 +40,16 @@ db_attribute parameter.
|
|||
|
||||
Example:
|
||||
|
||||
Assuming you've installed cooldowns on your Character typeclasses, you can
|
||||
use a cooldown to limit how often you can perform a command:
|
||||
Assuming you've installed cooldowns on your Character typeclasses, you can use a
|
||||
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):
|
||||
def func(self):
|
||||
if self.caller.cooldowns.ready("power attack"):
|
||||
self.do_power_attack()
|
||||
self.caller.cooldowns.set("power attack", 10)
|
||||
self.caller.cooldowns.add("power attack", 10)
|
||||
else:
|
||||
self.caller.msg("That's not ready yet!")
|
||||
"""
|
||||
|
|
@ -55,42 +60,47 @@ import time
|
|||
|
||||
class CooldownHandler:
|
||||
"""
|
||||
Handler for cooldowns. This can be attached to any object that
|
||||
supports DB attributes (like a Character or Account).
|
||||
Handler for cooldowns. This can be attached to any object that supports DB
|
||||
attributes (like a Character or Account).
|
||||
|
||||
A cooldown is a timer that is usually used to limit how often
|
||||
some action can be performed or some effect can trigger. When a
|
||||
cooldown is first set, it counts down from the amount of time
|
||||
provided back to zero, at which point it is considered ready again.
|
||||
A cooldown is a timer that is usually used to limit how often some action
|
||||
can be performed or some effect can trigger. When a cooldown is first added,
|
||||
it counts down from the amount of time provided back to zero, at which point
|
||||
it is considered ready again.
|
||||
|
||||
Cooldowns are named with an arbitrary string, and that string is used
|
||||
to check on the progression of the cooldown. Each cooldown is tracked
|
||||
separately and independently.
|
||||
Cooldowns are named with an arbitrary string, and that string is used to
|
||||
check on the progression of the cooldown. Each cooldown is tracked
|
||||
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
|
||||
module does not register or provide callback functionality for when
|
||||
a cooldown becomes ready again. Users of cooldowns are expected to
|
||||
query the state of any cooldowns they are interested in.
|
||||
Cooldowns are saved persistently, so they survive reboots. This module does
|
||||
not register or provide callback functionality for when a cooldown becomes
|
||||
ready again. Users of cooldowns are expected to query the state of any
|
||||
cooldowns they are interested in.
|
||||
|
||||
Methods:
|
||||
- ready(name): Checks whether a given cooldown name is ready.
|
||||
- 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
|
||||
cooldown name.
|
||||
- extend(name, seconds): Like set, but adds time to the given
|
||||
cooldown name. If it doesn't exist yet, calling this is equivalent
|
||||
to calling set.
|
||||
cooldown name. set() is an alias.
|
||||
- extend(name, seconds): Like add(), but adds more time to the given
|
||||
cooldown if it already exists. If it doesn't exist yet, calling
|
||||
this is equivalent to calling add().
|
||||
- reset(cooldown): Resets a given cooldown, causing ready() to return
|
||||
True for that cooldown immediately.
|
||||
- clear(): Resets all cooldowns.
|
||||
"""
|
||||
|
||||
__slots__ = ("data", "db_attribute", "obj")
|
||||
|
||||
def __init__(self, obj, db_attribute="cooldowns"):
|
||||
if not obj.attributes.has(db_attribute):
|
||||
obj.attributes.add(db_attribute, {})
|
||||
|
||||
self.data = obj.attributes.get(db_attribute)
|
||||
self.obj = obj
|
||||
self.db_attribute = db_attribute
|
||||
self.cleanup()
|
||||
|
||||
@property
|
||||
|
|
@ -102,63 +112,69 @@ class CooldownHandler:
|
|||
|
||||
def ready(self, *args):
|
||||
"""
|
||||
Checks whether all of the provided cooldowns are ready (expired).
|
||||
If a requested cooldown does not exist, it is considered ready.
|
||||
Checks whether all of the provided cooldowns are ready (expired). If a
|
||||
requested cooldown does not exist, it is considered ready.
|
||||
|
||||
Args:
|
||||
any (str): One or more cooldown names to check.
|
||||
*args (str): One or more cooldown names to check.
|
||||
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
|
||||
cooldowns. If a requested cooldown does not exist, it is
|
||||
considered to have 0 time left.
|
||||
Returns the maximum amount of time left on one or more given cooldowns.
|
||||
If a requested cooldown does not exist, it is considered to have 0 time
|
||||
left.
|
||||
|
||||
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:
|
||||
(int): Number of seconds until all provided cooldowns are
|
||||
ready. Returns 0 if all cooldowns are ready (or don't
|
||||
exist.)
|
||||
float or int: Number of seconds until all provided cooldowns are
|
||||
ready. Returns 0 if all cooldowns are ready (or don't exist.)
|
||||
"""
|
||||
now = time.time()
|
||||
cooldowns = [self.data[x] - now for x in args if x in self.data]
|
||||
if not cooldowns:
|
||||
return 0
|
||||
return math.ceil(max(max(cooldowns), 0))
|
||||
return 0 if use_int else 0.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:
|
||||
cooldown (str): The name of the cooldown.
|
||||
seconds (int): The number of seconds before this cooldown is
|
||||
ready again.
|
||||
seconds (float or int): The number of seconds before this cooldown
|
||||
is ready again.
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Adds a specific amount of time to an existing cooldown.
|
||||
|
||||
If this cooldown is already ready, this is equivalent to calling
|
||||
set. If the cooldown is not ready, it will be extended by the
|
||||
provided duration.
|
||||
If this cooldown is already ready, this is equivalent to calling set. If
|
||||
the cooldown is not ready, it will be extended by the provided duration.
|
||||
|
||||
Args:
|
||||
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)
|
||||
self.set(cooldown, time_left + (seconds if seconds else 0))
|
||||
time_left = self.time_left(cooldown) + (seconds if seconds else 0)
|
||||
self.set(cooldown, time_left)
|
||||
return max(time_left, 0)
|
||||
|
||||
def reset(self, cooldown):
|
||||
"""
|
||||
|
|
@ -174,8 +190,7 @@ class CooldownHandler:
|
|||
"""
|
||||
Resets all cooldowns.
|
||||
"""
|
||||
for cooldown in list(self.data.keys()):
|
||||
del self.data[cooldown]
|
||||
self.data.clear()
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
|
|
@ -183,6 +198,10 @@ class CooldownHandler:
|
|||
requirements small.
|
||||
"""
|
||||
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:
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -3447,7 +3447,7 @@ class TestLegacyMuxComms(CommandTest):
|
|||
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):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -3458,23 +3458,31 @@ class TestCooldowns(EvenniaTest):
|
|||
self.assertTrue(self.handler.ready("a", "b", "c"))
|
||||
self.assertEqual(self.handler.time_left("a", "b", "c"), 0)
|
||||
|
||||
def test_set(self, mock_time):
|
||||
self.handler.set("a", 10)
|
||||
def test_add(self, mock_time):
|
||||
self.assertEqual(self.handler.add, self.handler.set)
|
||||
self.handler.add("a", 10)
|
||||
self.assertFalse(self.handler.ready("a"))
|
||||
self.assertEqual(self.handler.time_left("a"), 10)
|
||||
|
||||
mock_time.return_value = 9
|
||||
mock_time.return_value = 9.0
|
||||
self.assertFalse(self.handler.ready("a"))
|
||||
self.assertEqual(self.handler.time_left("a"), 1)
|
||||
|
||||
mock_time.return_value = 10
|
||||
mock_time.return_value = 10.0
|
||||
self.assertTrue(self.handler.ready("a"))
|
||||
self.assertEqual(self.handler.time_left("a"), 0)
|
||||
|
||||
def test_set_multi(self, mock_time):
|
||||
self.handler.set("a", 10)
|
||||
self.handler.set("b", 5)
|
||||
self.handler.set("c", 3)
|
||||
def test_add_float(self, mock_time):
|
||||
self.assertEqual(self.handler.time_left("a"), 0)
|
||||
self.assertEqual(self.handler.time_left("a", use_int=False), 0)
|
||||
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.assertEqual(self.handler.time_left("a", "b", "c"), 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("c", "c"), 3)
|
||||
|
||||
def test_set_none(self, mock_time):
|
||||
self.handler.set("a", None)
|
||||
def test_add_none(self, mock_time):
|
||||
self.handler.add("a", None)
|
||||
self.assertTrue(self.handler.ready("a"))
|
||||
self.assertEqual(self.handler.time_left("a"), 0)
|
||||
|
||||
def test_set_negative(self, mock_time):
|
||||
self.handler.set("a", -5)
|
||||
def test_add_negative(self, mock_time):
|
||||
self.handler.add("a", -5)
|
||||
self.assertTrue(self.handler.ready("a"))
|
||||
self.assertEqual(self.handler.time_left("a"), 0)
|
||||
|
||||
def test_set_overwrite(self, mock_time):
|
||||
self.handler.set("a", 5)
|
||||
self.handler.set("a", 10)
|
||||
self.handler.set("a", 3)
|
||||
def test_add_overwrite(self, mock_time):
|
||||
self.handler.add("a", 5)
|
||||
self.handler.add("a", 10)
|
||||
self.handler.add("a", 3)
|
||||
self.assertFalse(self.handler.ready("a"))
|
||||
self.assertEqual(self.handler.time_left("a"), 3)
|
||||
|
||||
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.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.assertEqual(self.handler.time_left("a"), 20)
|
||||
|
||||
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.assertEqual(self.handler.time_left("a"), 0)
|
||||
self.handler.set("a", 10)
|
||||
self.handler.extend("a", None)
|
||||
self.handler.add("a", 10)
|
||||
self.assertEqual(self.handler.extend("a", None), 10)
|
||||
self.assertEqual(self.handler.time_left("a"), 10)
|
||||
|
||||
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.assertEqual(self.handler.time_left("a"), 0)
|
||||
self.handler.set("a", 10)
|
||||
self.handler.extend("a", -5)
|
||||
self.handler.add("a", 10)
|
||||
self.assertEqual(self.handler.extend("a", -5), 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):
|
||||
self.handler.reset("a")
|
||||
self.assertTrue(self.handler.ready("a"))
|
||||
|
|
@ -3535,20 +3555,32 @@ class TestCooldowns(EvenniaTest):
|
|||
self.assertEqual(self.handler.time_left("a"), 0)
|
||||
|
||||
def test_clear(self, mock_time):
|
||||
self.handler.set("a", 10)
|
||||
self.handler.set("b", 10)
|
||||
self.handler.set("c", 10)
|
||||
self.handler.add("a", 10)
|
||||
self.handler.add("b", 10)
|
||||
self.handler.add("c", 10)
|
||||
self.handler.clear()
|
||||
self.assertTrue(self.handler.ready("a", "b", "c"))
|
||||
self.assertEqual(self.handler.time_left("a", "b", "c"), 0)
|
||||
|
||||
def test_cleanup(self, mock_time):
|
||||
self.handler.set("a", 10)
|
||||
self.handler.set("b", 5)
|
||||
self.handler.set("c", 5)
|
||||
mock_time.return_value = 6
|
||||
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 = 6.0
|
||||
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.assertNotIn("b", self.handler.data)
|
||||
self.assertNotIn("c", self.handler.data)
|
||||
self.assertEqual(list(self.handler.data.keys()), ["a"])
|
||||
|
||||
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"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue