vanilla-wow-addons – Rev 1

Subversion Repositories:
Rev:
CandyDice = AceLibrary("AceAddon-2.0"):new(
    "AceEvent-2.0","AceHook-2.0","AceDebug-2.0",
    "AceDB-2.0","AceConsole-2.0","CandyBar-2.0"
)

local cd = CandyDice
cd.revision = tonumber(string.sub("$Revision: 129 $", 12, -3))

cd.textures = {} -- they'll get registered in OnInitialize
cd.texturenames = {} -- instead of storing this I should just update the options table in RegisterTexture()

local compost = AceLibrary:HasInstance("Compost-2.0") and AceLibrary("Compost-2.0")
local function GetTable() return compost and compost:Acquire() or {} end
local function RecycleTable(t) if compost then compost:Reclaim(t) end end

cd:RegisterDB("CandyDiceDBPerChar")
cd.cmdtable = {type="group",handler=CandyDice,args = {
    timers = {
        type="group",
        name="Timers",
        desc="Commands for the ability timers",
        args = {
            show = {
                type="toggle",
                name="Show Anchor",
                desc="Show the timer anchor",
                get=function() return CandyDiceAnchorFrame:IsShown() end,
                set=function(show) if show then CandyDiceAnchorFrame:Show() else CandyDiceAnchorFrame:Hide() end end
            },
            center = {
                type="toggle",
                name="Center Timers",
                desc="Center the timer bars on the screen rather than anchoring them",
                get=function() return CandyDice.db.profile.options["center"] end,
                set="SetCentered"
            },
            enable = {
                type="toggle",
                name="Enabled",
                desc="Enable showing of ability timer bars",
                get=function() return CandyDice.db.profile.options["buffs"] end,
                set=function(v) if v then CandyDice:EnableBuffs() else CandyDice:DisableBuffs() end end
            },
            grow = {
                type="toggle",
                name="Grow Upward",
                desc="When enabled, the bars will grow upward from the anchor. Otherwise, they will grow down",
                get=function() return CandyDice.db.profile.options.grow end,
                set=function(v)
                        CandyDice:Debug(v)
                        CandyDice.db.profile.options.grow = v
                        CandyDice:UpdateGrowth()
                end
            },
            scale = {
                type="range",
                name="Scale",
                min=.1,
                max=10,
                desc="Scale factor for the timer bars",
                get=function() return CandyDice.db.profile.options.scale end,
                set=function(v)
                    CandyDice.db.profile.options.scale = v
                    CandyDice:Setup()
                end
            },
            icon = {
                type="toggle",
                name="Show Icon",
                desc="Toggle the showing of icons on timer bars",
                get=function()
                    return CandyDice.db.profile.options.icon
                end,
                set=function(v)
                    if v then 
                        CandyDice.db.profile.options.icon = true 
                    else 
                        CandyDice.db.profile.options.icon = false 
                    end
                    CandyDice:Setup()
                end,
            },
        },
    },
    cooldowns = {
        type="group",
        name="Cooldowns",
        desc="Commands for the cooldown timers",
        args = {
            show = {
                type="toggle",
                name="Show Anchor",
                desc="Show the cooldown anchor",
                get=function() return CandyDiceCooldownAnchorFrame:IsShown() end,
                set=function(show) if show then CandyDiceCooldownAnchorFrame:Show() else CandyDiceCooldownAnchorFrame:Hide() end end
            },
            enable = {
                type="toggle",
                name="Enabled",
                desc="Enable or disable cooldown scanning",
                get=function() return CandyDice.db.profile.options.cooldowns end,
                set=function(v) if v then CandyDice:EnableCooldowns() else CandyDice:DisableCooldowns() end end
            },
            grow = {
                type="toggle",
                name="Grow Upward",
                desc="When enabled, the bars will grow upward from the anchor. Otherwise, they will grow down",
                get=function() return CandyDice.db.profile.options.growcd end,
                set=function(v)
                        CandyDice:Debug(v)
                        CandyDice.db.profile.options.growcd = v;
                        CandyDice:UpdateGrowth();
                end
            },
            scale = {
                type="range",
                name="Scale",
                min=.1,
                max=10,
                desc="Scale factor for the cooldown bars",
                get=function() return CandyDice.db.profile.options.scalecd end,
                set=function(v)
                    CandyDice.db.profile.options.scalecd = v
                    CandyDice:Setup()
                end
            },
            reversed = {
                type="toggle",
                name="Reversed",
                desc="Controls whether the cooldown bars move from empty to filled or filled to empty",
                get=function()
                    return CandyDice.db.profile.options.reversecd
                end,
                set=function(v)
                    if (v) then 
                        CandyDice.db.profile.options.reversecd = true
                    else
                        CandyDice.db.profile.options.reversecd = false
                    end
                    CandyDice:Setup()
                end
            },
            icon = {
                type="toggle",
                name="Show Icon",
                desc="Toggle the showing of icons on cooldown bars",
                get=function()
                    return CandyDice.db.profile.options.iconcd
                end,
                set=function(v)
                    if v then 
                        CandyDice.db.profile.options.iconcd = true 
                    else 
                        CandyDice.db.profile.options.iconcd = false 
                    end
                    CandyDice:Setup()
                end,
            }
        },
    },
    reset = {
        type="execute",
        name="Reset Settings",
        desc="Resets all settings to the default",
        func=function() CandyDice:ResetDB("profile");CandyDice:Setup() end,
        order=5000
    },
    version = {
        type="execute",
        name="Report Version",
        desc="Prints the CandyDice version",
        func=function() CandyDice:Print(CandyDice.revision) end
    },
    texture = {
        type="text",
        name="Bar Texture",
        desc="The texture used for the Candy Bars",
        validate=CandyDice.texturenames,
        set=function(v)
            CandyDice.db.profile.options.texture = v
            CandyDice:Setup()
        end,
        get=function()
            return CandyDice.db.profile.options.texture
        end,
    }
}
}

cd.defaults = {}
cd:RegisterDefaults(
    'profile',
    {
        tracked = nil,
        options = {
            center = true, -- center the timer bars
            cooldowns = true, -- scan for cooldowns
            buffs = true, -- scan for buffs
            grow = true, -- timer bars grow vertically
            growcd = true, -- cd bars grow vertically
            scale=1,    -- timer bar scale
            scalecd=1,  -- cd bar scale
            reversecd = true, -- cooldowns count up instead of down
            iconcd = true, -- show icons in cooldowns
            icon = true, -- show icons on timers
            texture= 'Default'
        }
    }
)


local gratuity = AceLibrary("Gratuity-2.0")
local cb = CandyDice
local pc = AceLibrary("PaintChips-2.0")

cd:RegisterChatCommand({"/cd", "/cdice", "/candydice"}, CandyDice.cmdtable)

function CandyDice:OnInitialize()
    --- erase all the class/race specific defaults
    --- except the ones in the players race/class
    local _,class = UnitClass("player")
    local _,race = UnitRace("Player")
    for category, buffs in self.defaults do
        if category ~= race and category ~= class then
            self.defaults[category] = nil
        end
    end
    self:RegisterTexture('Default','Interface\\Addons\\CandyDice\\Textures\\bar.tga')
--~     self:SetDebugging(true)
    self.debugFrame = ChatFrame3
end

function CandyDice:OnEnable()
    -- CandyBar setup
    self:RegisterCandyBarGroup("CandyDice")
    self:RegisterCandyBarGroup("CandyDiceCooldowns")
    
    -- Load the class specific defaults into the profile
    local _,class = UnitClass("player")
    local _,race = UnitRace("Player")
    if not self.db.profile.tracked then
        self.db.profile.tracked = {}
        self:Print("No tracking data found, loading defaults for %s/%s", race, class)
        local classbuffs = self.defaults[class]
        if classbuffs then
            for buff, info in pairs(classbuffs) do
                self.db.profile.tracked[buff] = info
            end
        end
        local racebuffs = self.defaults[race]
        if racebuffs then
            for buff, info in pairs(self.defaults[race]) do
                self.db.profile.tracked[buff] = info
            end
        end
    end

    self:UpdateGrowth()
    
    -- Make PaintChip not suck
    self:RegisterColors()
    
    -- Scan the spellbook and create a index/icon cache
    self:RegisterEvent("SPELLS_CHANGED", "ScanSpellbook")
    self:RegisterEvent("PLAYER_PET_CHANGED", "ScanSpellbook")
    self:ScanSpellbook()
   
    -- Push the tracked buffs into the options table so they can be manipulated from the command line  
    self:PushTrackedToOptions()
    
    if self.db.profile.options.cooldowns then
        self:EnableCooldowns() -- turn on the cooldown scanning
    end
    if self.db.profile.options.buffs then
        self:EnableBuffs() -- turn on the buff scanning
    end
end

function CandyDice:OnDisable()
    -- I'm sure theres something I should do here.
end

function CandyDice:GetFuncsForBuff(buff)
    --- Creates closures that we can feed to the option table to get/set the various buff options
    local ftable = {}
    if not self.db.profile.tracked[buff] then return end
    ftable["GetFGColor"] = function()
        local c = unpack(CandyDice.db.profile.tracked[buff].colors)
        local r,g,b=unpack(pc:GetRGB(c))
        return r/255,g/255,b/255
    end
    ftable["SetFGColor"]=function(r,g,b)
        local hex = string.format("%02x%02x%02x", r*255, g*255, b*255)
        -- Stupid paintchips
        pc:RegisterHex(hex)
        CandyDice.db.profile.tracked[buff].colors[1] = hex
        self:SetCandyBarColor(buff, hex)
        self:SetCandyBarColor(buff.."CD", hex)
        
    end
    ftable["GetBGColor"]=function()
        local fg,bg= unpack(CandyDice.db.profile.tracked[buff].colors)
        local c = bg or fg
        local r,g,b=unpack(pc:GetRGB(c))
        return r/255,g/255,b/255
    end
    ftable["SetBGColor"]=function(r,g,b)
        CandyDice:Debug("R:"..tostring(r).."G:"..tostring(g).."B:"..tostring(b))
        local hex = string.format("%02x%02x%02x", r*255, g*255, b*255)
        CandyDice:Debug(hex)
        -- Stupid paintchips
        pc:RegisterHex(hex)
        CandyDice.db.profile.tracked[buff].colors[2] = hex
        cb:SetCandyBarBackgroundColor(buff, hex)
        cb:SetCandyBarBackgroundColor(buff.."CD", hex)
    end
    ftable["GetScanBuff"]= function() return CandyDice.db.profile.tracked[buff]["buff"] end
    ftable["SetScanBuff"]= function(v) 
        if (v) then CandyDice.db.profile.tracked[buff]["buff"] = true else CandyDice.db.profile.tracked[buff]["buff"]=false end
        CandyDice:StopCandyBar(buff)
    end
    ftable["GetScanCD"]= function() return CandyDice.db.profile.tracked[buff]["cooldown"] end
    ftable["SetScanCD"]= function(v) 
        if (v) then 
            CandyDice.db.profile.tracked[buff]["cooldown"] = true 
        else 
            CandyDice.db.profile.tracked[buff]["cooldown"]=false 
        end 
        CandyDice:StopCandyBar(buff.."CD")
    end
    ftable["delete"] = function()
        CandyDice.db.profile.tracked[buff] = nil
        CandyDice:UnregisterCandyBar(buff)
        CandyDice:UnregisterCandyBar(buff.."CD")
        CandyDice:PushTrackedToOptions()
    end
    return ftable
end

function CandyDice:PushTrackedToOptions()
    local header = {
        name="Abilities",
        desc="Abilities and buffs scanned",
        type="group",
        args = {
            addnew={
                type="text",
                name="Add New",
                usage="/cdice abilities addnew <ability name>",
                order=1,
                desc="Add a new ability to be tracked",
                get=false,
                set=function(v)
                    CandyDice:AddNewAbility(v)
                end,
                message="[%s]: %s has been added to tracked abilities."
            },
            spacer={
                type="header",
                order=2,
            },
        }
    }
    for buff in self.db.profile.tracked do
        if self.db.profile.tracked[buff] then
            local ftable = self:GetFuncsForBuff(buff)
            local btable = {
                name=buff,
                type="group",
                desc=buff,
                args = {
                    fgcolor= {
                        type="color",
                        name="Foreground color",
                        desc="Foreground color",
                        get=ftable["GetFGColor"],
                        set=ftable["SetFGColor"]
                    },
                    bgcolor= {
                        type="color",
                        name="Background color",
                        desc="Background color",
                        get=ftable["GetBGColor"],
                        set=ftable["SetBGColor"]  
                    },
                    scanbuff={
                        type="toggle",
                        name="Scan Buffs",
                        desc="Scan your buffs for this ability",
                        get=ftable.GetScanBuff,
                        set=ftable.SetScanBuff,
                    },
                    scancd={
                        type="toggle",
                        name="Scan Cooldowns",
                        desc="Scan for an ability cooldown",
                        get=ftable.GetScanCD,
                        set=ftable.SetScanCD,
                    },
                    delete={
                        type="execute",
                        name="Delete",
                        desc="Remove the ability from the list of tracked abilities",
                        func=ftable.delete
                    }
                }
            }
            local key = string.lower(buff)
            key = string.gsub(key, ' ', '_')
            header.args[key] = btable
        end --if self.db.profile.tracked[buff] then
    end --for buff in self.db.profile.tracked
    self.cmdtable.args["abilities"] = header
end

function CandyDice:RegisterColors()
    for buff, info in pairs(self.db.profile.tracked) do
        if info then
            local fgcolor, bgcolor = unpack(info.colors)
            if fgcolor then pc:RegisterHex(fgcolor) end
            if bgcolor then pc:RegisterHex(bgcolor) end
        end
    end
end

function CandyDice:GetTrackedIcon(name)
    --- Returns the icon to use for a tracked buff/cooldown
    local t = self.db.profile.tracked
    if t[name] and t.texture then return t.texture end
    if self.spellcache[name] then return self.spellcache[name].texture end
    return nil
end



function CandyDice:ScanSpellbook()
    -- returns a table formated with
    -- spellname - {spellIndex, texture}
    -- Only need to scan the cooldowns
    local cache = {}
    self.shoot = nil
    local p = self.db.profile
    for ii =1,MAX_SPELLS do
        local name = GetSpellName(ii, BOOKTYPE_SPELL)
        if name == BabbleSpell["Shoot"] then
            self.shoot = ii
        end
        if name and p.tracked[name] then
            local texture = GetSpellTexture(ii, BOOKTYPE_SPELL)
            cache[name] = {["id"]=ii, ["texture"]=texture, ["type"] = BOOKTYPE_SPELL}
        end
    end
    local pspellcount = HasPetSpells()
    if pspellcount then
        for ii = 1, pspellcount do
            local name = GetSpellName(ii, BOOKTYPE_PET)
            if name and p.tracked[name] then
                local texture = GetSpellTexture(ii, BOOKTYPE_PET)
                cache[name] = {["id"]=ii, ["texture"]=texture, ["type"] = BOOKTYPE_PET}
            end
        end
    end
    self.spellcache = cache
end


--~ memtrack = {
--~     calls = 0,
--~     memory = 0,
--~     time = 0,
--~ }

function CandyDice:ScanBuffs()
--~     local time = GetTime()
--~     local mem = gcinfo()
    local tracked = self.db.profile.tracked
    local cbuffs = GetTable()
    
    if self.db.profile.options.buffs then
        for ii = 0,15 do
            local bidx = GetPlayerBuff(ii, "HELPFUL")
            gratuity:SetPlayerBuff(bidx)
            local buff = gratuity:GetLine(1)
            if buff and tracked[buff] and tracked[buff].buff then
                cbuffs[buff] = buff
                self:UpdateBuff(buff, bidx, GetPlayerBuffTimeLeft(bidx))
            end
        end
    end
    for buff, info in pairs(tracked) do
        if not cbuffs[buff] then
            local registered,bartime,elapsed,running = cb:CandyBarStatus(buff)
            if running then
                cd:StopCandyBar(buff)
            end
        end
    end
    RecycleTable(cbuffs)
    
--~     memtrack.calls = memtrack.calls + 1
--~     memtrack.time = memtrack.time + (GetTime()-time)
--~     local mem2 = gcinfo()
--~     memtrack.memory = memtrack.memory + (mem2 - mem)
--~     if math.mod(memtrack.calls, 10) == 0 then
--~         self:PrintComma(memtrack.calls, memtrack.time, memtrack.memory)
--~     end
end

function CandyDice:ScanCooldowns()
    local tracked = self.db.profile.tracked
    self:Debug("Scanning Cooldowns")
    if self.db.profile.options.cooldowns then
        for buff, info in pairs(tracked) do
            if info.cooldown then
                self:UpdateCooldown(buff, info)
            end
        end
    end
end

function CandyDice:UpdateBuff(buff, bidx, bufftime)
    -- Updates a buff bar to match the state of the buff. Be prepared for bidx/bufftime to be null
    --self:Debug("Updating buff"..tostring(buff))
    local info  = self.db.profile.tracked[buff]
    if not info and info.buff then return end
    local registered,bartime,elapsed,running = cb:CandyBarStatus(buff)
    if running then
        if (not (bufftime or bidx)) or bufftime == 0 then
            cd:StopCandyBar(buff)
        else
            cd:SetCandyBarTimeLeft(buff, bufftime)
        end
    elseif bufftime and bufftime > 0 then-- bar not running, but with bufftime,so start the bar
        if not registered then
            self:Debug("Registering bar for "..buff)
            local fgcolor, bgcolor = unpack(info.colors)
            local icon = CandyDice.db.profile.options.icon and (GetPlayerBuffTexture(bidx) or GetPlayerBuffTexture(ii)) or ''
            self:RegisterTimerBar(buff, fgcolor, bgcolor, icon)
            
        end
        cb:SetCandyBarTime(buff, bufftime)
        cb:StartCandyBar(buff)
    else
        -- This would be a buff with no time, and a bar that isn't running
    end
end

function CandyDice:UpdateCooldown(ability, info, doublecheck)
    local cache = self.spellcache[ability]
    if not (cache and cache.id and cache.type) then return end --- not in the spellcache
    if not info and info.cooldown then return end -- not set for cooldown scanning, probably redundant
    local cbn = ability..'CD'
    local now = GetTime()
    local start, duration = GetSpellCooldown(cache.id, cache.type)
    local registered,bartime,elapsed,running = cb:CandyBarStatus(cbn)
    if now == start then
        --- Stealth, natures swiftness, or something else that doesn't actually start its CD until it's buff is gone.
        --- Remotely possible this could misdetect a regular cooldown.
        --- To catch any regular CDs we miss, we schedule one (just one) check 1/10th of a second from now
--~         self:Print("Skipping CD check for "..ability)
        if not doublecheck then
--~             self:Print("Doublecheck scheduled")
            self:ScheduleEvent(self.UpdateCooldown, 0.1, self, ability, info, true)
        end
        return
    end
    if not running then
        local skipdur = 1.5
        if self.shoot then
            local speed, lowDmg, hiDmg, posBuff, negBuff, percent = UnitRangedDamage("player")
            if speed > skipdur then skipdur = speed end
        end
        if duration <= skipdur then return end -- ignore global CD
        if not registered then
            self:Debug("Registering bar for "..cbn)
            local fgcolor, bgcolor = unpack(info.colors)
            self:RegisterCooldownBar(ability, fgcolor, bgcolor, cache.texture)
        end
        local left = duration - (now - start)
        self:SetCandyBarTime(cbn, duration)
        self:StartCandyBar(cbn)
        cb:SetCandyBarTimeLeft(cbn, left)
    else -- currently running, stop if CD is gone or update time if not
        if start == 0 or duration == 0 then
            self:StopCandyBar(cbn)
            return
        end
        local left = duration - (now - start)
        cb:SetCandyBarTimeLeft(cbn, left)
    end
end

-- command handlers
function CandyDice:SetCentered(v) 
    self.db.profile.options["center"] = v
    self:UpdateGrowth()
end

function CandyDice:EnableCooldowns()
    self.db.profile.options.cooldowns = true
    self:RegisterEvent("SPELL_UPDATE_COOLDOWN", "ScanCooldowns")
    self:ScanCooldowns() -- pick up any current ones
end

function CandyDice:DisableCooldowns()
    self.db.profile.options.cooldowns = false
    for cd, info in pairs(self.db.profile.tracked) do
        -- Stop all our bars.
        if info.cooldown then
            cb:UnregisterCandyBar(cd.."CD")
        end
    end
    self:UnregisterEvent("SPELL_UPDATE_COOLDOWN")
end

function CandyDice:EnableBuffs()
    self.db.profile.options.buffs = true
    --  PLAYER_ARUAS_CHANGED only fires when you actually gain or lose a debuff,
    --  not when you re-apply one
    self:RegisterEvent("PLAYER_AURAS_CHANGED", "ScanBuffs")
    
    -- To keep the durations accurate if you re-apply a buff
    self:ScheduleRepeatingEvent("CandyDice", self.ScanBuffs, 0.3, self)
end

function CandyDice:DisableBuffs()
    self.db.profile.options.buffs = false
    for cd, info in pairs(self.db.profile.tracked) do
        -- Stop all our bars.
        if info.buff then
            cb:UnregisterCandyBar(cd)
        end
    end
    self:UnregisterEvent("PLAYER_AURAS_CHANGED")
    self:CancelScheduledEvent("CandyDice")
end

function CandyDice:UpdateGrowth()
    local o = self.db.profile.options
    if o.center then
        cb:SetCandyBarGroupPoint("CandyDice", "CENTER", "UIParent", "CENTER", 0, 0)
    else
        if (o.grow) then
            cb:SetCandyBarGroupPoint("CandyDice", "BOTTOM", "CandyDiceAnchorFrame", "TOP", 0, 0)
            cb:SetCandyBarGroupGrowth("CandyDice", true)
        else
            cb:SetCandyBarGroupPoint("CandyDice", "TOP", "CandyDiceAnchorFrame", "BOTTOM", 0, 0)
            cb:SetCandyBarGroupGrowth("CandyDice", false)
        end
    end
    if (o.growcd) then
        cb:SetCandyBarGroupPoint("CandyDiceCooldowns", "BOTTOM", "CandyDiceCooldownAnchorFrame", "TOP", 0, 0)
        cb:SetCandyBarGroupGrowth("CandyDiceCooldowns", true)
    else
        cb:SetCandyBarGroupPoint("CandyDiceCooldowns", "TOP", "CandyDiceCooldownAnchorFrame", "BOTTOM", 0, 0)
        cb:SetCandyBarGroupGrowth("CandyDiceCooldowns", false)
    end
end


function CandyDice:AddNewAbility(name)
    local info = {
        colors = {"FFFFFF"},
        buff = true,
        cooldown = true
    }
    local p = self.db.profile
    p.tracked[name] = info
    self:ScanSpellbook()
    self:UpdateCooldown(name, info)
    self:PushTrackedToOptions()
end


function CandyDice:Setup()
    -- Cheat for right now & just enable/disable
    if CandyDice:IsActive() then
        self:ToggleActive()
        self:ToggleActive()
    end
end


function CandyDice:RegisterTexture(name, file)
    self.textures[name] = file
    table.insert(self.texturenames, name)
end

function CandyDice:RegisterCooldownBar(ability, fgcolor, bgcolor, icon)
    --- register a CandyBar respecting all the CandyDice settings like show icon, texture, scaling, etc
    --- Returns the name of the bar
    local cbn = ability.."CD"
    local icon = CandyDice.db.profile.options.iconcd and icon or ''
    local o = self.db.profile.options
    local texture = self.textures[o.texture]
    -- register bar
    cb:RegisterCandyBar(cbn, 1, ability, icon)
    if fgcolor then
        cb:SetCandyBarColor(cbn, fgcolor, 0.5)
        cb:SetCandyBarBackgroundColor(cbn, bgcolor or fgcolor, 0.2)
    end
    cb:RegisterCandyBarWithGroup(cbn,"CandyDiceCooldowns")
    cb:SetCandyBarScale(cbn, o.scalecd)
    if texture then cb:SetCandyBarTexture(cbn, texture) end
    if CandyDice.db.profile.options.reversecd then
        cb:SetCandyBarReversed(cbn, true)
    end
    return cbn
end

function CandyDice:RegisterTimerBar(buff, fgcolor, bgcolor, icon)
    -- register a CandyBar respecting all the CandyDice settings for timers
    -- Returns the name of the bar
    local o = self.db.profile.options
    local icon = o.icon and icon or ''
    local texture = self.textures[o.texture]
    -- register bar
    cb:RegisterCandyBar(buff, 1, buff, icon)
    if fgcolor then 
        cb:SetCandyBarColor(buff, fgcolor, 0.5)
        cb:SetCandyBarBackgroundColor(buff, bgcolor or fgcolor, 0.2)
    end
    cb:RegisterCandyBarWithGroup(buff,"CandyDice")
    cb:SetCandyBarScale(buff, o.scale)
    if texture then cb:SetCandyBarTexture(buff, texture) end
    return buff
end