Frame Recycler

This is a simple system to recycle frames so that you can avoid calling BlzDestroyFrame (which can cause desyncs). It also allows you to "create" and "destroy" frames safely in an async context.

First, you have to register a frametype with a name and a constructor function. The constructor is called when a new frame is created or allocated. Example:
Lua:
FrameRecycler.Define("unitIcon", true, function()
    local newFrame = BlzCreateFrameByType("BACKDROP", "", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "", 0)
    BlzFrameSetEnable(newFrame, false)
    return newFrame
end)
If the frame is displayed asynchronously, you have to allocate a number of frames that is greater than the amount that could reasonably be displayed on the screen at the same time:
Lua:
FrameRecycler.Allocate("unitIcon", 100)
Now you can use the Get and Return functions to create and destroy frames asynchronously and without any memory leaks.

Lua:
if Debug then Debug.beginFile "FrameRecycler" end
do
    --[[
    =============================================================================================================================================================
                                                                      Frame Recycler
                                                                        by Antares

                          Recycle frames to avoid using BlzDestroyFrame (which can desync) and use frames in a dynamic, async context.

								Requires:
								TotalInitialization			    https://www.hiveworkshop.com/threads/total-initialization.317099/

    =============================================================================================================================================================
                                                                          A P I
    =============================================================================================================================================================

    FrameRecycler.Define(name, isAsync, constructor)            Define a frametype under the given name. The constructor function will be called whenever a frame
                                                                is requested. It must return a framehandle. If you set the frametype to async, the Get function
                                                                will throw an error when it has run out of preallocated frames.
    FrameRecycler.Allocate(name, amount)                        Preallocate the specified amount for the given frametype. Use this for async frames.
    FrameRecycler.Get(name)                                     Returns a frame of the specified frametype and makes it visible.
    FrameRecycler.Return(whichFrame)                            Hides the specified framehandle and returns it to the recycling system.

    =============================================================================================================================================================
    ]]

    local unusedFrames = {}                 ---@type framehandle[][]
    local frameTypeIsAsync = {}             ---@type table<string,boolean>
    local frameTypeOf = {}                  ---@type table<framehandle,string>
    local constructorOfFrameType = {}       ---@type table<string,function>

    FrameRecycler = {
        ---Define a frametype under the given name. The constructor function will be called whenever a frame is requested. It must return a framehandle.
        ---@param name string
        ---@param isAsync boolean
        ---@param constructor function
        Define = function(name, isAsync, constructor)
            unusedFrames[name] = {}
            frameTypeIsAsync[name] = isAsync
            constructorOfFrameType[name] = constructor
        end,

        ---Preallocate the specified amount for the given frametype.
        ---@param name string
        ---@param amount integer
        Allocate = function(name, amount)
            local list = unusedFrames[name]
            local constructor = constructorOfFrameType[name]
            for i = 1, amount do
                list[i] = constructor()
                BlzFrameSetVisible(list[i], false)
                frameTypeOf[list[i]] = name
            end
        end,

        ---Returns a frame of the specified frametype and makes it visible.
        ---@param name string
        ---@return framehandle | nil
        Get = function(name)
            local list = unusedFrames[name]
            if #list == 0 then
                if frameTypeIsAsync[name] then
                    print("|cffff0000Warning:|r Not enough frames allocated for type " .. name .. ".")
                    return nil
                end
                local newFrame = constructorOfFrameType[name]()
                frameTypeOf[newFrame] = name
                return newFrame
            end

            local frame = list[#list]
            list[#list] = nil
            BlzFrameSetVisible(frame, true)
            return frame
        end,

        ---Hides the specified framehandle and returns it to the recycling system.
        ---@param whichFrame framehandle
        Return = function(whichFrame)
            local name = frameTypeOf[whichFrame]
            local list = unusedFrames[name]
            list[#list + 1] = whichFrame
            BlzFrameSetVisible(whichFrame, false)
        end
    }
Contents

Frame Recycler (Binary)

Reviews
Wrda
Short, sweet and simple. Lovely picture, and very useful for users who want to use frames dynamically such as making healthbars. Remember the 3 R's guys, Reuse, Recycle and Reduce! Approved
I love the pic! That is some gold-tier flavor for this resource.

I would, however, provide feedback that the amount of boilerplate required for users seems too much. I've not worked with frames before, aside from treating them simply as any other handle in systems like Table.

Correct me if I'm wrong with this, but I'm thinking that the immutable properties of a frame are defined simply via the BlzCreateFrame... calls?

If that's the case, you have the ability to do some automation with how the recycling works, by hooking the BlzCreateFrame... functions, storing the arguments to a table, also hooking the BlzDestroyFrame function to flag when it's to be released. Once the user calls one of the BlzCreateFrame... functions, as long as the args are exactly the same, and a recycled frame coincides with that same argument list (checking if all args are equal) then go ahead and provide that frame to the user.

Given your example of how to use this, I would think that BlzGetOriginFrame is always returning the exact same frame, correct? If not, then that's another reason to discard this feedback.
 
I love the pic! That is some gold-tier flavor for this resource.
Haha, thanks! :plol:

Correct me if I'm wrong with this, but I'm thinking that the immutable properties of a frame are defined simply via the BlzCreateFrame... calls?
Yes, that is correct. With the exception that you cannot change the tooltip of a frame after setting it once because it crashes the game.

If that's the case, you have the ability to do some automation with how the recycling works, by hooking the BlzCreateFrame... functions, storing the arguments to a table, also hooking the BlzDestroyFrame function to flag when it's to be released. Once the user calls one of the BlzCreateFrame... functions, as long as the args are exactly the same, and a recycled frame coincides with that same argument list (checking if all args are equal) then go ahead and provide that frame to the user.
Using a hook to replace BlzDestroyFrame is interesting, but I think what you're suggesting about the create functions may lead to unexpected behavior. Frames have default properties which are overwritten if you call a native. For example, a frame is enabled by default, but you can disable it with BlzFrameSetEnable. This is not captured in a BlzCreateFrame function call and so the system could return a frame that was created for a different purpose and has been disabled after creation if the arguments in the BlzCreateFrame function are the same. Suddenly, a frame that's supposed to be enabled is not. The solution would be to return a frame to all default values when released, but that's a lot of extra operations and I don't think I know all of them.

In addition, while your idea of registering new frametypes on the fly would work well for synchronous frames, when you have an async frametype, you need to allocate them before using it, so there's no way around registering them with the system in some way.

Having the user define a constructor function is, I think, the best way to go about this because it allows the user to define any default properties of a frame, such as texture and size, and then share them across all instances of the frame being recycled without having to set them again. For example:

Lua:
FrameRecycler.Define("utherFace", true, function()
    local newFrame = BlzCreateFrameByType("BACKDROP", "", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "", 0)
    BlzFrameSetEnable(newFrame, false)
    BlzFrameSetTexture(newFrame, "ReplaceableTextures\\CommandButtons\\BTNHeroPaladin.blp", 0, true) --My frames will always have this icon, so I set it in the constructor.
    return newFrame
end)

Lua:
local newFrame = FrameRecycler.Get("unitIcon")
BlzFrameSetTexture(newFrame, whichTexture, 0, true) --My frames will have a different texture each time, so I set it externally in whatever is the calling function.

Given your example of how to use this, I would think that BlzGetOriginFrame is always returning the exact same frame, correct? If not, then that's another reason to discard this feedback.
Yes, it always returns the same frame.
 
I think you should drop the isAsync feature and return nil when nothing/not enough was Allocated. Frames need to exist and enter map script for all users anyway. After that almost anything can be done to them in GetlocalPlayer context.

You should add a function to handle warcraft 3 Save&Load system, I guess it would loop unusedFrames and recreate each table or empty them.
After Save&Loading the game crashs when any reference to a frame from before the load is used in a frame native.
 
Lua:
    OnInit.global(function()
        local trig = CreateTrigger()
        TriggerRegisterGameEvent(trig, EVENT_GAME_LOADED)
        TriggerAddAction(function()
            TimerStart(CreateTimer(), 0.0, false, function()
                local tempKeys = {}
                for key, __ in pairs(unusedFrames) do
                    tempKeys[#tempKeys + 1] = key
                end
                table.sort(tempKeys)

                for __, frametype in ipairs(tempKeys) do
                    for i = 1, #unusedFrames[frametype] do
                        unusedFrames[frametype][i] = constructorOfFrameType[frametype]()
                    end
                end

                DestroyTimer(GetExpiredTimer())
            end)
        end)
    end)
Does this look correct?
 
Top