ID:2361551
 

Cooldown datum



Introduction
This tutorial will be explained in two parts. One explaining the functionality of the new beta version(v.512.1420) and one explaining the stable version(v.511.1385)

Example of the new 512 beta build list function
bread/proc/eat() world << "You eat some bread"
var/list/food = list("bread" = new/bread())
food["bread"].eat()



A cooldown acts as a datum which is stored for individual players. The way to store it is by adding it to a list, and save it as a string relative to the ability it functions for. In this example, we will use a fireball as an example. We will not store the cooldown to the ability itself, but to the user casting it. This is because we don't want every ability created to hold "unnecessary" information, as there would probably be hundreds of these objects created over time. An other reason is, the cooldown datum is easier to handle if it is a fixed data to each player, and not a cooldown that is created then deleted over and over again. We want to keep it simple.

This tutorial will also show you how Lummox's latest build improves the usage of datums inside lists, and how to properly apply the changes. However, the current stable build(v.511) will have a harder time doing what we want to achieve. Both versions will be explained.

Explanation of the code tree


/ability
parent_type = /obj
// obj holds all data for pixel movement and appearance, which is why we want to keep it an object.
// However, making it a child for /obj would make it less comprehensive as the project grows imo.
// example: /obj/ability = /parent/child
// And therefore i keep it simple by making it /ability

//This is also where you would want to add variables, such as damage, traveling speed and time etc.
fire/fb001 // short for fireball001
/cooldown
/mob/player
var/list/cooldown
//This holds all of our cooldowns.
It is important to know what is included in this tutorial. We are working with /mob, that is the player. We have /ability, a subclass of /obj. Then we have /cooldown, which is the cooldown itself.

We need to make sure /mob is able to cast /ability, and when cast it triggers /cooldown. Then unable to cast it while cooldown is triggered. That is the essence of the code.


/cooldown


 //For both v.512(Beta) and v.511(Stable)
cooldown
//First we need to set a few variables to our datum
var
name
// This acts as a name, in which it holds a name so we know which datum we are working with

time
// This holds the datums cooldown time, and to how long it will last.

triggered = FALSE
//Toggle explains the datum whether it is triggered or not. TRUE means it is ready to start a new cooldown

New(_name, _time)
src.name = _name
src.time = _time

proc/reset()
//Each cooldown needs a reset(), in where variables is reset
//time = originalTime
//This one is optional.

triggered = FALSE
/* the ability using this cooldown dstum calls for "triggered"
triggered needs to be set to FALSE, so the ability can be used.
*/


proc/trigger()
// This is how the cooldown works.
// We need to ensure the proc doesn't repeat itself. so lets make a condition
// (triggered is also used to inform a verb/ability() when it is allowed to cast a new object.
// If triggered is FALSE, it wont fulfill the statement of the proc, nor create a new object)
if(!triggered)
//Then flip the condition so it won't stack over itself
triggered = !triggered
//This is the sleep duration, before it can be cast again.
sleep(time)
//After it's off duration we need to set triggered to FALSE again
triggered = FALSE
proc/getToggle()
return triggered ? Triggered : 0


As cooldowns is just an example, this approach of code can be applied in so many different ways.

/ability


ability
parent_type = /obj
var
mob/owner
mob/target
damage
damageAmp
speed = 0
lag = 1
travel = 20 // Units to travel
New(mob/_owner, )
if(_owner)
loc = get_step(_owner,_owner.dir)
dir = _owner.dir
owner = _owner
else
throw EXCEPTION("[src]: /ability/New() : expecting mob as argument!")

Move()
travel-- // for every Move() iteration, we subtract from var/travel
if(!travel) del(src) //Once travel is depleted, we delete src(i.e the object)
return ..() // Else, return parent Move() (/obj/Move()) to continue the whole function of movement

Bump(atom/a)
if(ismob(a))
oview(10, src) << "[src.name] hit [a.name] for [damage]!"
del src // You can pass certain conditions to this statement, but for now lets keep it short.


The fireball object


ability/fire
fb002
icon = 'fireballs.dmi'
icon_state = "fb002"

name = "Huge fireball"

travel = 120
lag = 1
speed = 10

New()
..()


Bump(atom/a)
if(ismob(a))
var/mob/m = a //Typecast to mob
target = m

if(owner)
target.takeDamage(damage)
..(a)


/mob


mob
//This is how we apply our cooldowns.

//At first we need to declare a list, in which we want to contain all our cooldowns.
//By default, lets make it empty
var/list/cooldown = list()

//Then we could make a proc that adds property to our empty list.
//It would make more sense to add data to our list, rather wanting all mobs to hold unnecessary data.
proc
giveAbilityCooldown()
cooldown = list(\
"fb002" = new/cooldown("fb002", 100))
//We name the content relative to our ability, then create a new /cooldown
//You can look at the arguments in /cooldown/New() further up.

get_cooldown_trigger(argument)
var/cooldown/cd = cooldown && cooldown[argument]
return cd ? cd.triggered : 0


/mob/player


mob/player
verb
fb002()
// version 511
if(!get_cooldown_trigger("fb002"))
var/ability/fire/fb002/f = new(src)
walk(f, f.dir, world.tick_lag, f.speed)

// version 512
if(!cooldown["fb002"].getTrigger())
var/ability/fire/fb002/f = new(src)
walk(f, f.dir, world.tick_lag, f.speed)

cooldown["fb002"].trigger()



// All of this is required in v.511
// By lummox's latest work, you can bypass all sorts of overhead procs, by calling straight from the list containing the datum.




The few issues with this is when a player is saved. then it happens that cooldown/toggle is saved as TRUE, and the cooldowns will be locked. Therefore we have cooldown/reset() to turn toggle = FALSE
Our first step is create a datum that holds our cooldown property. The reason for this is because it is easier for us to structure a cooldown variable as an individual datum, rather then a bunch of random variables for mob. It is flexible and this datum can act as a cooldown for anything! Not just abilities, but weapon usages, items, potions and many other.

So we need to set our cooldown variables uppon creation of the datum. We do this through /cooldown/New()
cooldown
New(name, time)
All the variables related to how long it lasts is all located inside the /cooldown datum.

And to save a datum to a mob, we store it in a list and give it a list prefix, for example: Lets imagine we have a fireball we, as in the example, and our implimentation of this fireball we call it fb002. By giving the fireball a prefix, we can store the datum to the same prefix as a list property.

mob
var/list/cooldown = list(\
"fb002" = new/cooldown(name, time))


At this point, all you need to do is make sure the cooldown/toggle() is a demanded check for the fb002 verb, as shown in the example above.

At this time, what we need to do to access the datums functionality is access it straight from our cooldown[] list, inside a mob's scope
cooldown["fb002"].proc()
cooldown["fb002"].var
Sleeping to handle a cooldown is really not ideal. Check out cooldownlib for a less intrusive approach.


http://www.byond.com/developer/Ter13/CooldownLib

I like the effort tho.
Taking in concideration what Ter said, using worldtime to count durration is far more efficient and prominent. So keep that in mind.

Using sleep() is sub-optimal, but an easy way of getting away from a technical bit of programming.
Issue #1: Scheduler hell

Okay, so this one's more of a usability problem than a legitimate error on your part.

It's a common trope in an RPG to set two cooldowns with an attack. So let's say I want to have a global cooldown plus an ability specific cooldown tied to one attack.

When I used the attack, I'd do something like:

user.cooldown["global"].trigger()
user.cooldown["slash"].trigger()


The only problem is, that trigger() has a sleep() in it.

That means that slash's cooldown won't be triggered until the global cooldown is no longer on cooldown, because the calling proc needs to wait for any called procs that are told to sleep().

To fix this, we could do one of two things:

1) Change trigger()

    proc/trigger()
set waitfor = 0
if(!triggered)
triggered = !triggered
sleep(time)
triggered = FALSE


or:

    proc/trigger()
if(!triggered)
triggered = !triggered
spawn(time)
triggered = FALSE


2) Change how we call our cooldowns:

spawn()
user.cooldown["global"].trigger()
spawn()
user.cooldown["slash"].trigger()


In either case, though, this isn't good for efficiency reasons. Copying stack context and args lists takes time. All of these spawns hit the scheduler, so the more complex your game, the more significant your slowdowns will be. You've said that you don't really care about the efficiency of the approach, so I'll leave that issue be. This is a major usability problem more than an efficiency one.



Issue #2: Bad logic

Also, I reread this again, and wanted to point a few things out:

You've got a major logic mistake in here because of that sleep()

Let's imagine a theoretical cooldown:
cooldown
name = "herpyderp"
time = 100


Now, let's imagine this sequence of events:

@0s: herpyderp.trigger()
@5s: herpyderp.reset()
@6s: herpyderp.trigger()


1) The cooldown was triggered at 0 seconds. That means trigger() has been slept starting at 0 seconds, and ending at 10 seconds. Let's notate this as trigger<0>().

2) The cooldown was reset at 5 seconds.

3) The cooldown was triggered at 5 seconds. That means trigger() has been slept starting at 6 seconds and ending at 16 seconds. Let's notate this sleeping proc as trigger<6>().

So here's what's going to happen:

0s: Triggered is 0, trigger<0>() is called.

    proc/trigger()
<----ENTRY POINT---->
if(!triggered)
triggered = !triggered
sleep(time)
<----BREAK POINT---->
triggered = FALSE


0s: Triggered is now 1.

5s: reset() is called.

    proc/reset()
<----ENTRY POINT---->
triggered = FALSE
<----END POINT---->


5s: Triggered is now 0.

6s: Triggered is 0, trigger<6>() is called.

    proc/trigger()
<----ENTRY POINT---->
if(!triggered)
triggered = !triggered
sleep(time)
<----BREAK POINT---->
triggered = FALSE


10s: Triggered is 1, trigger<0>() resumes.

    proc/trigger()
if(!triggered)
triggered = !triggered
sleep(time)
<----RESUME POINT---->
triggered = FALSE
<----END POINT---->


10s: Triggered is now 0

16s: Triggered is 0, trigger<6>() resumes.

    proc/trigger()
if(!triggered)
triggered = !triggered
sleep(time)
<----RESUME POINT---->
triggered = FALSE
<----END POINT---->


16s: Triggered is still 0

Expected results:            | Actual results:
0s:  Trigger                 | 0s:  Trigger
0s:  On cooldown             | 0s:  On cooldown
5s:  Reset                   | 5s:  Reset
5s:  Not on cooldown         | 5s:  Not on cooldown
6s:  Trigger                 | 6s:  Trigger
6s:  On Cooldown             | 6s:  On cooldown
10s: On Cooldown             | 10s: Not on cooldown
16s: Not on cooldown         | 16s: Not on cooldown


Issue #3: Runtime error / redundant logic

 proc/getToggle()
return triggered ? Triggered : 0


The ternary shouldn't be there:

 proc/getToggle()
return triggered


If you are going to argue that getToggle() should only return a boolean value, your ternary is wrong anyway:

 proc/getToggle()
return triggered ? 1 : 0


Triggered -> triggered. Runtime error.
Alright, so in my last post, I mentioned issue #2. Fixing that issue is a whole can of worms, so let's open that can.

Fixing this problem is hard. You need to be able to identify which iteration of trigger() is the one that's supposed to unset the cooldown properly.

There are a couple ways to go about this. You can keep track of the time that trigger() started, and store that time as a current variable:

1) We can use time to identify the running function.

cooldown
var
name
time
triggered = FALSE
current = -1#INF

New(_name, _time)
src.name = _name
src.time = _time

proc/reset()
triggered = FALSE
current = -1#INF

proc/trigger()
if(!triggered)
var/this = current = world.time
triggered = !triggered
sleep(time)
if(current==this)
triggered = FALSE

proc/getToggle()
return triggered


The downside to this approach is if you trigger(), reset(), trigger() in the same tick, you'll reset triggered twice. This will almost completely eliminate the bug we're trying to fix, but it still won't do it completely. There's still a very small window where you can wind up accidentally wiping out a cooldown.

cooldown
var
name
time
triggered = FALSE
sequence = 0

New(_name, _time)
src.name = _name
src.time = _time

proc/reset()
triggered = FALSE
current = -1#INF

proc/trigger()
if(!triggered)
var/this = current = world.time
triggered = !triggered
spawn(time)
if(current==this)
triggered = FALSE

proc/getToggle()
return triggered


2) We can track iterations.

cooldown
var
name
time
triggered = 0
sequence = 0

New(_name, _time)
src.name = _name
src.time = _time

proc/reset()
triggered = FALSE
if(++sequence>16000000)
sequence = 0

proc/trigger()
if(!triggered)
if(++sequence>16000000)
sequence = 0
var/this = sequence
triggered = !triggered
spawn(time)
if(sequence==this)
triggered = FALSE

proc/getToggle()
return triggered


The problem with this approach is that keeping the sequence between 0 and 16M to prevent precision problems is ugly and confusing to most people, but at least the possibility for collision can only occur if the effect is triggered and reset 16 million times in the same frame.

3) We can track args lists.

cooldown
var
name
time
triggered = 0
current

New(_name, _time)
src.name = _name
src.time = _time

proc/reset()
triggered = FALSE
current = null

proc/trigger()
if(!triggered)
var/this = current = args
triggered = !triggered
spawn(time)
if(current==this)
triggered = FALSE

proc/getToggle()
return triggered


The downside to this approach is that it's a pretty ugly DM-specific hack. It'll work, but it is something that should never really be done.

4) OR: We can get rid of the sleep/spawn:

cooldown
var
name
time
end

New(_name, _time)
src.name = _name
src.time = _time

proc/reset()
end = world.time

proc/trigger()
end = world.time + time

proc/getToggle()
return end>world.time


There are no real downsides to this approach. It's the obvious winner.
Also, I wanted to point out two more major problems with this approach. One's a huge dealbreaker.

Problem #4: Persistence

Because you are depending on sleep(), you need to account for what happens when you try to save a cooldown.

When a player logs out, those slept procs don't save. They are stopped, and their cooldowns will save as they currently are, which in your system's case means they will never be set to not triggered again. This is why boolean-based cooldowns and sleeps should never be used for timing at this level in a game that has any kind of persistence.

This cannot be fixed with a boolean-based approach, so if you are going to add any kind of saving or persistence to these, you need to track the time that the cooldown will end. Which means you don't need the boolean anymore because we have time, and as I showed above, time can be used to not only fix bugs already in your current approach, but can completely replace the boolean because time is a quantity that can be derived down to a boolean value when paired with a second value on demand. There is an implicit second value with time, and that's now, or world.time.

If your cooldowns aren't meant to be used in a game with any kind of saving, you need to mark your variables temporary:

mob/player
var/tmp/list/cooldown


Or, if it is meant to be used in games with persistence, you need to actually handle this by storing the relative remaining time at the time the cooldown is saved. I'll demonstrate this on the end result of what I did in my last post to fix the problems with your approach:

cooldown
var
name
time
tmp/end = -1#INF

New(_name, _time)
src.name = _name
src.time = _time

proc/reset()
end = world.time

proc/trigger()
end = world.time + time

proc/getToggle()
return end>world.time

Write(savefile/F)
..()
var/time = world.time
if(end>time)
F["remaining"] << end-time

Read(savefile/F)
..()
var/remaining
F["remaining"] >> remaining
if(remaining)
end = world.time+remaining


And now the whole persistence issue is completely fixed.



Issue #5: The unsafe, ugly chain of operators

Usability again. The really annoying part about using this approach, is that embedding these behaviors into a datum of its own is cool and all, but really annoying from a usability standpoint for a number of reasons. For starters, you lose code clarity and compiler safety when you call on a list like you are doing. Two, label resolution is going to slow down the execution, and three, it requires the developer to understand not only the structure of the mob itself, but the structure of cooldown objects. And those cooldown objects? They are inconvenient wrappers around basically a number now that I've had my way with them and fixed all the problems with them. The only benefit to having these be component-level objects is if you want to reuse the data structure in dozens of places in your code and don't want people using your code to have to reinvent it at every place it needs to be used. Let's take a look at how a user is going to interact with them in certain cases:

resetting a cooldown:
user.cooldown["herpderp"].reset() //suppressed compiler errors.


triggering a cooldown:
user.cooldown["herpderp"].trigger() //supressed compiler errors.


checking a cooldown:
user.cooldown["herpderp"].getToggle()


This is really ugly code from a usability perspective. It can be cleaned quite a bit if we just get rid of the datum completely and build cooldown management into the player object itself:

mob/player
var
tmp/list/cooldown

proc
setCooldown(name,duration)
if(!cooldown)
cooldown = list()
cooldown[name] = world.time + duration

resetCooldown(name)
if(!cooldown)
return
cooldown[name] = world.time

getCooldown(name)
return cooldown ? cooldown[name] : -1#INF

onCooldown(name)
return cooldown ? cooldown[name]>world.time : 0

Write(savefile/F)
..()
var/list/l = list()
var/time = world.time
for(var/v in cooldown)
remaining = cooldown[v] - time
if(remaining>0)
l[v] = remaining
F["cooldown"] << l

Read(savefile/F)
..()
var/list/l
F["cooldown"] >> l
if(l && l.len)
for(var/v in l)
l[v] += time
cooldown = l


Now let's take a look at usability:

resetting a cooldown:
user.cooldown["herpderp"].reset() //old
//vs
user.resetCooldown("herpderp") //new


triggering a cooldown:
user.cooldown["herpderp"].trigger() //old
//vs
user.setCooldown("herpderp",100) //new


checking a cooldown:
user.cooldown["herpderp"].getToggle()
//vs
user.onCooldown("herpderp")


On top of all of the speed improvements from the last five issues I've pointed out with the approach you were saying was easier than mine, ease of use and understanding improvements, we also get quality of life fixes such as flexibility of cooldown durations, the ability to lengthen or shorten cooldowns on the fly, and more.

I'm not trying to discourage you. I'm just pointing out that calling this approach easier is dead wrong. It's overcomplicated, it's buggy, it's slower, and it's just plain a really bad use of sleep() where it's not only not necessary, but actively detrimental for a multitude of reasons that go well beyond performance or stylistic preference.

Like I said, I like that you wrote the tutorial, and it was well formatted. It's just that the system you designed not only is a bad use of the 512 features you are trying to show people (which is applaudable, by the way, and appreciated), but it teaches people to think of a complicated, bad solution to a problem that's incredibly simple.

In summary, not only is using world.time easier, it's less buggy, it's faster, and it requires less understanding of the moving parts of BYOND's engine to do properly.
oh wow!

Ter... You sure surprised me with this long, long reply! And I thank you for the honest criticism. I can tell you put quite some effort in this, and it sure clears a lot of stuff up. Not only does it help me, but to any person in the byond community reading this for educational purposes.

I sort of already knew that the foundation of the code in this "tutorial" was incomplete, rather... slow, and unnecessarily overcomplicated. I just thought i wanted to feel like i contributed to the community somewhat, to the best of my ability. Sure, with errors. But! then you show up and holy sweet jesus... This reply was something else. Thank you very much!

Again, none offence taken.
In response to Tafe
Tafe wrote:
oh wow!

Ter... You sure surprised me with this long, long reply! And I thank you for the honest criticism. I can tell you put quite some effort in this, and it sure clears a lot of stuff up. Not only does it help me, but to any person in the byond community reading this for educational purposes.

I sort of already knew that the foundation of the code in this "tutorial" was incomplete, rather... slow, and unnecessarily overcomplicated. I just thought i wanted to feel like i contributed to the community somewhat, to the best of my ability. Sure, with errors. But! then you show up and holy sweet jesus... This reply was something else. Thank you very much!

Again, none offence taken.

None intended. My cooldown library started out almost exactly where yours is. I just kept going back and refining it over the years with things I'd learned. I just wanted to share that journey with you. That said, I know your intention was to show off the new features moreso than actually create a cooldown tutorial. Which is cool.

This kind of criticism is something that takes years of trial and error to be able to make. Sorry the responses are so long, but it's just a lot of downrange consequences come out of code design.

Just don't think that every time I lash out at a piece of code, I'm doing it because everything has to be perfect. I don't want you to think that your code needs to be 11/10 to post it for the community. I just think it's worth pointing out compromises and flaws for the sake of learning. It's better to have a working piece of code than nothing, so don't let fear of criticism stop you from doing what you do. <3