Here is my first library: LibNameplateRegistry-1.0. It's a complete port of Healers Have To Die's NameplateRegistry.lua sub-module which is compatible with several known major nameplate add-ons.
The main difference compared with libNamePlate-1.0 (which has become too difficult to maintain) is that LibNameplateRegistry-1.0 contains absolutely no code to be compatible with such or such add-on and never will.
For now it only includes what I need for HHTD to work (latest HHTD alpha releases are using LibNameplateRegistry-1.0).
I hope it'll be useful and bring a robust and reliable way to deal with nameplates which until now (let's be optimistic :) ) were one of the remaining grey areas in WoW add-on programming.
Remarks and comments are welcomed!
LibNameplateRegistry-1.0 is an embeddable library providing an abstraction layer for tracking and querying Blizzard's Nameplate frames with ease and efficiency. Features:
Provides callbacks to track nameplate appearance and disappearance
Caches and maintain nameplates' related data
Provides a simple API to extract information from nameplate
Links GUID to nameplates
Provides auto-diagnostic features to detect incompatibilities
Please note that LibNameplateRegistry-1.0 is still in development and as
a result the provided API is still scarce at the moment.
Do not hesitate to request features via the ticket system.
This library focuses on optimization and simplicity, it will never try to
be compatible with other nameplate add-ons. It will be naturally compatible
with such other add-ons as long as they are coded properly. It includes means
(callbacks) of identifying and reporting incompatibility issues
so incompatible add-ons' users can act accordingly.
To implement LibNameplateRegistry-1.0 in your add-on:
[FONT=helvetica](WIP current alpha only) Gets a platename's frame specific region using a normalized name.[/FONT]
[FONT=helvetica]Use this API to get an easy and direct access to a specific sub-frame of any nameplate. This is useful if you want to access data for which LibNameplateRegistry provides no API (yet).[/FONT]
[FONT=helvetica]The result is cached for each frame making subsequent identical calls very fast.[/FONT]
[FONT=helvetica]The following regions are supported: 'name', 'statusBar', 'highlight', 'level', 'raidIcon', 'eliteIcon'.
If you need to access a specific region which is not supported, please make a feature request using the ticket system.[/FONT]
Parameters
plateFramethe platename's root frameinternalRegionNormalizedNamea normalized name referring to a specific region
Return value
[FONT=helvetica]region or throws an error if asked an unsupported region's name.[/FONT]
Ok, so, the API does what LibNamePlate-1.0 did, I just had to replace some names there and there. It seems to work a first glance, I have to test it further.
One question though about the internals: why bother with all that private stuff ? It just seems to make things more complicated than they should.
By the way, isn't there any way to get rid of the dependency to AceTimer-3.0 ? Most of the timers could be handled by a simple OnUpdate handler (eventually of a animation to control the execution rate) and one of them could just be done using an event handler.
By the way, isn't there any way to get rid of the dependency to AceTimer-3.0 ? Most of the timers could be handled by a simple OnUpdate handler (eventually of a animation to control the execution rate) and one of them could just be done using an event handler.
This is definitely something you should do; rarely is it a good thing to have one lib rely on another.
Ok, so, the API does what LibNamePlate-1.0 did, I just had to replace some names there and there. It seems to work a first glance, I have to test it further.
By the way, isn't there any way to get rid of the dependency to AceTimer-3.0 ? Most of the timers could be handled by a simple OnUpdate handler (eventually of a animation to control the execution rate) and one of them could just be done using an event handler.
This is definitely something you should do; rarely is it a good thing to have one lib rely on another.
I know, I hesitated seeing that no other library was doing this but I was too lazy to reinvent the wheel once more and make the library more complex and bug prone.
AceTimer being a very common and simple library, it shouldn't be a problem anyway.
One question though about the internals: why bother with all that private stuff ? It just seems to make things more complicated than they should.
I think it's cleaner that way and it also limits the amount of strange bugs that could be caused by a bugged add-on that would find its way into the library and damage it. (I've seen this happen a few times and lost hours to find the cause)
It's not that complicated once you get used to it, it makes the design clearer. At least for me.
The first is DiminishingReturns, that tracks PvP diminishing returns and displays feedback as icons on unit frames and nameplates.
The second is a "private" (i.e. not published on Curse), experimental one: AdiBuffPlate, that displays player's debuffs on enemy nameplates.
One I found quite annoying though is the difference between the second argument of the LNR_ON_NEW_PLATE and LNR_ON_GUID_FOUND callbacks: the former passes nameplate data, the latter only the GUID. I was doing something like this :
function addon:OnEnable()
--[[ ... ]]
self:LNR_RegisterCallback('LNR_ON_NEW_PLATE', 'NewPlate')
self:LNR_RegisterCallback('LNR_ON_GUID_FOUND', 'NewPlate')
--[[ ... ]]
end
--[[ ... ]]
function addon:NewPlate(event, nameplate, data)
local unitFrame = data.GUID and unitFrames[data.GUID]
if unitFrame then
unitFrame:AttachToNameplate(nameplate)
end
end
I know, I hesitated seeing that no other library was doing this but I was too lazy to reinvent the wheel once more and make the library more complex and bug prone.
AceTimer being a very common and simple library, it shouldn't be a problem anyway.
Yep, but it requires the developer to include it in the addon. For example, DiminishingReturns doesn't use AceTimer-3.0. So instead of shipping LibNameplateRegistry-1.0 with it, it is optional and I tell the user had to install the standalone version if he wanted the nameplate icons.
What you do with LNR isn't that complex. It doesn't seems there is more than one timer for each task so you could just have a big OnUpdate handler with a front rate limiter and several tasks based on boolean tests. AceTimer-3.0 spawns (or recycles) an animation each time you call :Schedule*Timer.
local timer = 0
local sanityDivisor = 0
LNR_Private.EventFrame:SetScript('OnUpdate', function(_, elapsed)
-- Main throttle
timer = timer - elapsed
if timer > 0 then return end
timer = 0.1
-- Check sanity every 100th tick
sanityDivisor = sanityDivisor + 1
if sanityDivisor == 100 then
sanityDivisor = 0
LNR_Private:CheckHookSanity()
end
-- Look for new plates
LNR_Private:LookForNewPlates()
-- Look for target plate
if HasTarget then
LNR_Private:CheckPlatesForTarget()
end
--[===[@debug@
self:DebugTests()
--@end-debug@]===]
end)
Hide EventFrame at creation, show it in :Enable and hide it in :Disable, et voilĂ !
And for the out-of-combat enabling, just register PLAYER_REGEN_ENABLED and call Enable when it fires.
I think it's cleaner that way and it also limits the amount of strange bugs that could be caused by a bugged add-on that would find its way into the library and damage it. (I've seen this happen a few times and lost hours to find the cause)
It's not that complicated once you get used to it, it makes the design clearer. At least for me.
A bunch of upvalues would have worked too and the library tables were designed intended to pass important data from one instance of the library to another one.
One I found quite annoying though is the difference between the second argument of the LNR_ON_NEW_PLATE and LNR_ON_GUID_FOUND callbacks: the former passes nameplate data, the latter only the GUID. I was doing something like this :
function addon:OnEnable()
--[[ ... ]]
self:LNR_RegisterCallback('LNR_ON_NEW_PLATE', 'NewPlate')
self:LNR_RegisterCallback('LNR_ON_GUID_FOUND', 'NewPlate')
--[[ ... ]]
end
--[[ ... ]]
function addon:NewPlate(event, nameplate, data)
local unitFrame = data.GUID and unitFrames[data.GUID]
if unitFrame then
unitFrame:AttachToNameplate(nameplate)
end
end
So I had to add another handler.
I made it this way mostly because the other plate associated data are not guaranteed to be accurate (except for data.name and data.GUID) when LNR_ON_GUID_FOUND fires (a plate's reaction could have changed for example since LNR_ON_NEW_PLATE fired).
It's true that I could update data.reaction before firing but it seems like an overhead since this callback only fires when a GUID is found, it's not meant to track nameplate data changes.
I'd also like to point out that using the same handler for both is not recommended as those two callbacks behave very differently:
One fires each time a nameplate's frame appears on screen and the other one only fires when new information (the GUID) is available about a nameplate which is already on screen and thus for which LNR_ON_NEW_PLATE fired.
What you do with LNR isn't that complex. It doesn't seems there is more than one timer for each task so you could just have a big OnUpdate handler with a front rate limiter and several tasks based on boolean tests. AceTimer-3.0 spawns (or recycles) an animation each time you call :Schedule*Timer.
local timer = 0
local sanityDivisor = 0
LNR_Private.EventFrame:SetScript('OnUpdate', function(_, elapsed)
-- Main throttle
timer = timer - elapsed
if timer > 0 then return end
timer = 0.1
-- Check sanity every 100th tick
sanityDivisor = sanityDivisor + 1
if sanityDivisor == 100 then
sanityDivisor = 0
LNR_Private:CheckHookSanity()
end
-- Look for new plates
LNR_Private:LookForNewPlates()
-- Look for target plate
if HasTarget then
LNR_Private:CheckPlatesForTarget()
end
--[===[@debug@
self:DebugTests()
--@end-debug@]===]
end)
Hide EventFrame at creation, show it in :Enable and hide it in :Disable, et voilĂ !
And for the out-of-combat enabling, just register PLAYER_REGEN_ENABLED and call Enable when it fires.
You're right it's not that complex, thanks for the code snippet above (that removes me the laziness excuse^^). Anyway I'll regret the animation timer neatness which only executes code when they tick at the scheduled rate. Knowing that "timer = timer - elapsed; if timer > 0 then return end;" will be done 120 times (more or less) per second hurts my perfectionism side :(
A bunch of upvalues would have worked too and the library tables were designed intended to pass important data from one instance of the library to another one.
That's what I tried to do at first but it became messy pretty quickly so I decided to do it using a known paradigm, it makes it easier to understand. (I'm very much into OO programming nowadays)
I also wanted to use a shutdown mechanism instead of passing everything to a newer version which would have to know and handle every other previous version particularities to upgrade cleanly. This private/public design simplify things in my opinion, each library version only has to know about itself...
I'd also like to point out that using the same handler for both is not recommended as those two callbacks behave very differently:
One fires each time a nameplate's frame appears on screen and the other one only fires when new information (the GUID) is available about a nameplate which is already on screen and thus for which LNR_ON_NEW_PLATE fired.
The addon doesn't care about nameplates without GUID. Moreover, if the "unitFrame" is already attached to the same nameplate, :AttachToNameplate does nothing.
You're right it's not that complex, thanks for the code snippet above (that removes me the laziness excuse^^). Anyway I'll regret the animation timer neatness which only executes code when they tick at the scheduled rate. Knowing that "timer = timer - elapsed; if timer > 0 then return end;" will be done 120 times (more or less) per second hurts my perfectionism side :(
You can do that too :
if not LNR_Private.Anim then
LNR_Private.Anim = LNR_Private.EventFrame:CreateAnimationGroup()
end
if not LNR_Private.Timer then
LNR_Private.Timer = LNR_Private.Anim:CreateAnimation()
end
LNR_Private.Anim:SetLooping("REPEAT")
LNR_Private.Timer:SetDuration(0.1)
LNR_Private.Timer:SetScript('OnFinished', function()
-- Copy previous handler there, without the throttle code
end)
Then call LNR_Private.Anim:Play() in :Enable and LNR_Private.Anim:Stop() in :Disable.
That's what I tried to do at first but it became messy pretty quickly so I decided to do it using a known paradigm, it makes it easier to understand. (I'm very much into OO programming nowadays)
I also wanted to use a shutdown mechanism instead of passing everything to a newer version which would have to know and handle every other previous version particularities to upgrade cleanly. This private/public design simplify things in my opinion, each library version only has to know about itself...
Actually, this is the upgrading process of the defunct AceLibrary. It has been abandoned because it was considered too complex and heavy.
I've removed AceTime-3.0. I found an unrelated race condition bug while testing the new timer implementation. So I decided to release a new version (0.6) before changing the hooking system.
Actually, this is the upgrading process of the defunct AceLibrary. It has been abandoned because it was considered too complex and heavy.
I didn't know :/ Maybe I'll reach the same conclusions one day but for now I'm not convinced.
I just updated to the latest alpha. I'll do some tests.
My "testing" addon (AdiBuffPlate) creates frame for enemy to show useful (de)buffs. These frames are identified by the unit GUID. They are attached (e.g. reparented and reanchored) to a nameplate as soon as the nameplate GUID is found. And they should be detached and hidden as soon as the nameplate is recycled.
However, for some reason, that was not happening properly with only the LNB callbacks. I had to double-check if the nameplate GUID still matches my frame GUID (see there) and an OnHide handler (since my frames are parented to the nameplate, their OnHide handler is called when the nameplate is hidden). Please note this was happening with the old LibNameplate-1.0 too, so maybe I'm doing something wrong.
This would mean that you miss some LNR_ON_RECYCLE_PLATE events. If LNR was not firing them, it would trigger its 'hook' sanity check and the library would shut down.
So in the end, since the library is not shutting down, this would mean that libCallBackHandler is not calling some of your callbacks which is quite unlikely.
I think that the problem comes from the way your unitProto.DoLayout() is called. As it's called from an onupdate handler it might get called right after a nameplate is hidden but before it's recycled. I'm not sure of the order in which handlers are called but this might happen.
How did you notice this issue? Have you found an easy way to reproduce it?
I've tried adding calls to error() into your add-on but through my tests I couldn't produce a non matching GUID nor a plate hidden before it is recycled (not counting your internal self:Hide() calls).
Well, I have noticed some frames got hung in the middle of the screen, which was pretty strange. It seems to only happen in the middle of a raid encounter with a good number of adds. I cannot reproduce it. In the latest changes, the unit frames themselves register with LNR so they always check if everything is ok, whatever my common unitFrames table contains.
I've updated LibNamePlateRegistry for WoW 7. While its main functionality (tracking nameplates and firing proper events on their creation and removal) is no longer required its API is still useful (especially :GetPlateByGUID(GUID) (which is now completely reliable) and :EachPlateByName() as well as its LNR_ON_TARGET_PLATE_ON_SCREEN callback).
The library is much simpler now (about 700 lines of code got removed). Several callbacks dealing with incompatibility detection with baddons have been removed since no longer required.
A new .unitToken member has been added to the plateData table provided by callbacks. This unitToken comes from the new Blizzard's NAME_PLATE_* events and as you know can be used as a standard unitID with WoW's API.
The main difference compared with libNamePlate-1.0 (which has become too difficult to maintain) is that LibNameplateRegistry-1.0 contains absolutely no code to be compatible with such or such add-on and never will.
For now it only includes what I need for HHTD to work (latest HHTD alpha releases are using LibNameplateRegistry-1.0).
I hope it'll be useful and bring a robust and reliable way to deal with nameplates which until now (let's be optimistic :) ) were one of the remaining grey areas in WoW add-on programming.
Remarks and comments are welcomed!
One question though about the internals: why bother with all that private stuff ? It just seems to make things more complicated than they should.
By the way, isn't there any way to get rid of the dependency to AceTimer-3.0 ? Most of the timers could be handled by a simple OnUpdate handler (eventually of a animation to control the execution rate) and one of them could just be done using an event handler.
This is definitely something you should do; rarely is it a good thing to have one lib rely on another.
nice :) what is your add-on by the way?
I know, I hesitated seeing that no other library was doing this but I was too lazy to reinvent the wheel once more and make the library more complex and bug prone.
AceTimer being a very common and simple library, it shouldn't be a problem anyway.
I think it's cleaner that way and it also limits the amount of strange bugs that could be caused by a bugged add-on that would find its way into the library and damage it. (I've seen this happen a few times and lost hours to find the cause)
It's not that complicated once you get used to it, it makes the design clearer. At least for me.
The first is DiminishingReturns, that tracks PvP diminishing returns and displays feedback as icons on unit frames and nameplates.
The second is a "private" (i.e. not published on Curse), experimental one: AdiBuffPlate, that displays player's debuffs on enemy nameplates.
One I found quite annoying though is the difference between the second argument of the LNR_ON_NEW_PLATE and LNR_ON_GUID_FOUND callbacks: the former passes nameplate data, the latter only the GUID. I was doing something like this :
So I had to add another handler.
Yep, but it requires the developer to include it in the addon. For example, DiminishingReturns doesn't use AceTimer-3.0. So instead of shipping LibNameplateRegistry-1.0 with it, it is optional and I tell the user had to install the standalone version if he wanted the nameplate icons.
What you do with LNR isn't that complex. It doesn't seems there is more than one timer for each task so you could just have a big OnUpdate handler with a front rate limiter and several tasks based on boolean tests. AceTimer-3.0 spawns (or recycles) an animation each time you call :Schedule*Timer.
Hide EventFrame at creation, show it in :Enable and hide it in :Disable, et voilĂ !
And for the out-of-combat enabling, just register PLAYER_REGEN_ENABLED and call Enable when it fires.
A bunch of upvalues would have worked too and the library tables were designed intended to pass important data from one instance of the library to another one.
I made it this way mostly because the other plate associated data are not guaranteed to be accurate (except for data.name and data.GUID) when LNR_ON_GUID_FOUND fires (a plate's reaction could have changed for example since LNR_ON_NEW_PLATE fired).
It's true that I could update data.reaction before firing but it seems like an overhead since this callback only fires when a GUID is found, it's not meant to track nameplate data changes.
I'd also like to point out that using the same handler for both is not recommended as those two callbacks behave very differently:
One fires each time a nameplate's frame appears on screen and the other one only fires when new information (the GUID) is available about a nameplate which is already on screen and thus for which LNR_ON_NEW_PLATE fired.
You're right it's not that complex, thanks for the code snippet above (that removes me the laziness excuse^^). Anyway I'll regret the animation timer neatness which only executes code when they tick at the scheduled rate. Knowing that "timer = timer - elapsed; if timer > 0 then return end;" will be done 120 times (more or less) per second hurts my perfectionism side :(
I'll make the switch when I have some time.
That's what I tried to do at first but it became messy pretty quickly so I decided to do it using a known paradigm, it makes it easier to understand. (I'm very much into OO programming nowadays)
I also wanted to use a shutdown mechanism instead of passing everything to a newer version which would have to know and handle every other previous version particularities to upgrade cleanly. This private/public design simplify things in my opinion, each library version only has to know about itself...
The addon doesn't care about nameplates without GUID. Moreover, if the "unitFrame" is already attached to the same nameplate, :AttachToNameplate does nothing.
You can do that too :
Then call LNR_Private.Anim:Play() in :Enable and LNR_Private.Anim:Stop() in :Disable.
Actually, this is the upgrading process of the defunct AceLibrary. It has been abandoned because it was considered too complex and heavy.
I didn't know :/ Maybe I'll reach the same conclusions one day but for now I'm not convinced.
My "testing" addon (AdiBuffPlate) creates frame for enemy to show useful (de)buffs. These frames are identified by the unit GUID. They are attached (e.g. reparented and reanchored) to a nameplate as soon as the nameplate GUID is found. And they should be detached and hidden as soon as the nameplate is recycled.
However, for some reason, that was not happening properly with only the LNB callbacks. I had to double-check if the nameplate GUID still matches my frame GUID (see there) and an OnHide handler (since my frames are parented to the nameplate, their OnHide handler is called when the nameplate is hidden). Please note this was happening with the old LibNameplate-1.0 too, so maybe I'm doing something wrong.
So in the end, since the library is not shutting down, this would mean that libCallBackHandler is not calling some of your callbacks which is quite unlikely.
I think that the problem comes from the way your unitProto.DoLayout() is called. As it's called from an onupdate handler it might get called right after a nameplate is hidden but before it's recycled. I'm not sure of the order in which handlers are called but this might happen.
How did you notice this issue? Have you found an easy way to reproduce it?
I've tried adding calls to error() into your add-on but through my tests I couldn't produce a non matching GUID nor a plate hidden before it is recycled (not counting your internal self:Hide() calls).
The library is much simpler now (about 700 lines of code got removed). Several callbacks dealing with incompatibility detection with baddons have been removed since no longer required.
A new .unitToken member has been added to the plateData table provided by callbacks. This unitToken comes from the new Blizzard's NAME_PLATE_* events and as you know can be used as a standard unitID with WoW's API.