diff --git a/!Compatibility/libs/Classy-1.0.lua b/!Compatibility/libs/Classy-1.0.lua new file mode 100644 index 0000000..237273e --- /dev/null +++ b/!Compatibility/libs/Classy-1.0.lua @@ -0,0 +1,26 @@ +--[[ + Classy.lua + A wrapper for defining classes that inherit from widgets +--]] + +local Classy = LibStub:NewLibrary('Classy-1.0', 0) +if not Classy then return end + +function Classy:New(frameType, parentClass) + local class = CreateFrame(frameType) + class.mt = {__index = class} + + if parentClass then + class = setmetatable(class, {__index = parentClass}) + + class.super = function(self, method, ...) + parentClass[method](self, unpack(arg)) + end + end + + class.Bind = function(self, obj) + return setmetatable(obj, self.mt) + end + + return class +end \ No newline at end of file diff --git a/!Compatibility/libs/Parser.lua b/!Compatibility/libs/Parser.lua new file mode 100644 index 0000000..34ffb15 --- /dev/null +++ b/!Compatibility/libs/Parser.lua @@ -0,0 +1,268 @@ +if not LibStub then return end + +local SD = LibStub:GetLibrary('StateDriver-1.0') +if not SD then return end + +local Parser = SD:New('Parser') + +local IsMounted = IsMounted +local IsStealthed = IsStealthed +local GetShapeshiftForm = GetShapeshiftForm + +local _G = getfenv(0) +local function IsInShapeshiftForm(index) + return GetShapeshiftForm() == tonumber(index) +end + + +function Parser:Initialize() + self.casting = nil + self.channel_spell = nil + self.channeling = nil + + self:RegisterEvent('PLAYER_ENTERING_WORLD') + self:RegisterEvent('SPELLCAST_START') + self:RegisterEvent('SPELLCAST_CHANNEL_START') + self:RegisterEvent('SPELLCAST_STOP') + self:RegisterEvent('SPELLCAST_CHANNEL_STOP') + -- self:SetScript('OnEvent', function() self[event](self) end) +end + +local _CastSpellByName +local Hooked_CastSpellByName = function(spell, unit) + if spell then + self.channel_spell = spell + _CastSpellByName(spell, unit) + end +end + +local HOOKED = false +function Parser:PLAYER_ENTERING_WORLD() + if not HOOKED then + _CastSpellByName = CastSpellByName + CastSpellByName = Hooked_CastSpellByName + HOOKED = true + end +end + +function Parser:SPELLCAST_START() + self.casting = arg1 +end + +function Parser:SPELLCAST_CHANNEL_START() + self.channeling = true +end + +function Parser:SPELLCAST_STOP() + self.casting = false +end + +function Parser:SPELLCAST_CHANNEL_STOP() + self.channel_spell = nil + self.channeling = false +end + +local conditions_map, casting, existence, hostility +do + local function IsChanneling(dependency) + return dependency and dependency == Parser.channeling or Parser.channeling + end + + local function IsCasting(dependency) + return dependency and dependency == Parser.casting or Parser.casting + end + + local function IsMouseOverUnit() + local is_mouseover = false + + is_mouseover = UnitName('mouseover') and 'mouseover' + + local frame = GetMouseFocus() + if frame and frame.GetAttribute then + is_mouseover = is_mouseover or frame:GetAttribute('unit') + end + is_mouseover = is_mouseover or frame and frame.unit + + return is_mouseover + end + + local function modifierCondition(dependency) + dependency = dependency == 'ctrl' and 'control' or dependency + local modifier = string.gsub(dependency, "^%l", string.upper) + return _G['Is' .. modifier .. 'KeyDown']() + end + + -- Rename dependent? + casting = { + ['channeling'] = IsChanneling, + ['nochanneling'] = function(dependency) + return not IsChanneling(dependency) + end, + ['casting'] = IsCasting, + ['nocasting'] = function(dependency) + return not IsCasting(dependency) + end, + ['group'] = function(dependency) + local in_raid = UnitInRaid('player') + -- Player is apparently always in a party... Party Rock Anthem, GO! + local in_party = UnitInParty('party1') + + if dependency then + if dependency == 'raid' then + return in_raid + else + return in_party + end + else + return in_raid or in_party + end + end, + ['nogroup'] = function(dependency) + local in_raid = UnitInRaid('player') + local in_party = UnitInParty('party1') + + if dependency then + if dependency == 'raid' then + return not in_raid + else + return not in_party + end + else + return (not in_raid) and (not in_party) + end + end, + ['mod'] = modifierCondition, + ['modifier'] = modifierCondition, + ['form'] = IsInShapeshiftForm, + ['stance'] = IsInShapeshiftForm + } + + existence = { + ['exists'] = UnitExists, + ['noexists'] = function(unit) + return not UnitExists(unit) + end, + ['dead'] = function(unit) + return UnitIsDead(unit) and not UnitAura(unit, 'Feign Death') + end, + ['nodead'] = function(unit) + return not (UnitIsDead(unit) and not UnitAura(unit, 'Feign Death')) + end, + } + + + hostility = { + ['harm'] = function(unit_one, unit_two) + return UnitExists(unit_two) and (not UnitIsFriend(unit_one, unit_two)) + end, + ['nohelp'] = function(unit_one, unit_two) + return UnitExists(unit_two) and (not UnitIsFriend(unit_one, unit_two)) + end, + ['help'] = UnitIsFriend, + ['noharm'] = UnitIsFriend + } + + + conditions_map = { + -- Targets + ['mouseover'] = IsMouseOverUnit, + + ['pet'] = HasPetUI, + ['nopet'] = function() return not HasPetUI() end, + + ['party'] = UnitInParty, + ['raid'] = UnitInRaid, + + ['combat'] = UnitAffectingCombat, + + ['mounted'] = IsMounted, + -- ['indoors'] = IsIndoors, + -- ['outdoors'] = IsOutdoors, + ['stealth'] = IsStealthed, + -- ['swimming'] = IsSwimming, + + ['bonusbar'] = function(bar_id) + return false + end, + } + + for k, v in next, casting do conditions_map[k] = v end + for k, v in next, existence do conditions_map[k] = v end + for k, v in next, hostility do conditions_map[k] = v end +end + +function CmdOptionParse(command) + -- /action [conditions] + local action, conditions + + -- Accumulating condition + local acc_condition + + -- Condition-mapped function and dependency argument + local func, dependency + + -- [@target] + local target, is_cond_target + + if not string.find(command, ';') then + command = command .. ' ;' + end + + -- Iterate condition cases + for _, cases in { string.split(';', command) } do + conditions, action = string.match(cases, '%[([^%]]+)%]%s*(.+)') + + -- Does the action have any conditions? + if conditions then + conditions = { string.split(',', conditions) } + acc_condition = true + target = nil + for _, cond in conditions do + cond = string.trim(cond) + cond, dependency = string.split(':', cond) + + -- Is the current condition a target command? Usually the first + -- condition is + is_cond_target = + string.match(cond, '@(.*)') or + string.match(cond, 'target=(.*)') + + -- Mouseover has a special condition, since unitframes don't + -- utilise the mouseover unit, in which case it's mapped to + -- player, target, partyN etc. + func = conditions_map[is_cond_target or cond] + + if is_cond_target then + target = func and func() or is_cond_target + else + local res + + if hostility[cond] then + res = func and func('player', target or 'target') + elseif existence[cond] then + res = func and func(target or 'target') + elseif cond == 'combat' then + res = func and func('player') + elseif casting[cond] then + res = func and func(dependency) + else + res = func and func() + end + + acc_condition = acc_condition and res + end + end + + -- Are all conditions met? Then perform that action + if acc_condition then + return string.trim(action), target + end + else + local cases = string.trim(cases) + return cases + end + end +end +SecureCmdOptionParse = CmdOptionParse + +Parser:Initialize() \ No newline at end of file diff --git a/!Compatibility/libs/StateDriver-1.0.lua b/!Compatibility/libs/StateDriver-1.0.lua new file mode 100644 index 0000000..cd759bf --- /dev/null +++ b/!Compatibility/libs/StateDriver-1.0.lua @@ -0,0 +1,513 @@ +--[[ +Name: StateDriver-1.0 +Revision: $Rev: 107 $ +Maintainers: martinjlowm +Website: https://github.com/martinjlowm/StateDriver/ +Dependencies: Classy-1.0 +License: None +]] + +if not LibStub then return end + +local SD = LibStub:NewLibrary('StateDriver-1.0', 0) +if not SD then return end + +local Classy = LibStub('Classy-1.0') + +local _G = getfenv(0) + +function SD:New(name, parent) + self[name] = Classy:New('Frame', parent) + + return self[name] +end + +local function GetAttribute(self, prefix, name, suffix) + local attributes = self.__state_driver.attributes + local value + + if not name and not suffix then + name = prefix + else + value = attributes[prefix .. name .. suffix] + value = value or attributes['*' .. name .. suffix] + value = value or attributes[prefix .. name .. '*'] + value = value or attributes['*' .. name .. '*'] + end + + return value or attributes[name] +end + +local function SetAttribute(self, attr, value) + local old_value = self.__state_driver.attributes[attr] + self.__state_driver.attributes[attr] = value + + local func = self.__state_driver.handlers['OnAttributeChanged'] + if func and type(func) == 'function' and value ~= old_value then + func(self, attr, value) + end +end + + +-- Figure out where to place this +local function initPlayerDrop() + UnitPopup_ShowMenu(PlayerFrameDropDown, "SELF", "player") + if not (UnitInRaid("player") or GetNumPartyMembers() > 0) or UnitIsPartyLeader("player") and PlayerFrameDropDown.init and not CanShowResetInstances() then + UIDropDownMenu_AddButton({text = RESET_INSTANCES, func = ResetInstances, notCheckable = 1}, 1) + PlayerFrameDropDown.init = nil + end +end + + +local ACTIONS = {} + +ACTIONS.target = function(self, unit, button) + if unit then + if unit == 'none' then + ClearTarget(); + elseif ( SpellIsTargeting() ) then + SpellTargetUnit(unit); + elseif ( CursorHasItem() ) then + DropItemOnUnit(unit); + else + TargetUnit(unit); + end + end +end + +ACTIONS.togglemenu = function(self, unit, button) + if UnitIsUnit(unit, 'player') then + UIDropDownMenu_Initialize(PlayerFrameDropDown, initPlayerDrop, 'MENU') + PlayerFrameDropDown.init = true + ToggleDropDownMenu(1, nil, PlayerFrameDropDown, 'cursor') + elseif unit == 'pet' then + ToggleDropDownMenu(1, nil, PetFrameDropDown, 'cursor') + elseif unit == 'target' then + ToggleDropDownMenu(1, nil, TargetFrameDropDown, 'cursor') + elseif string.sub(unit, 1, 5) == 'party' then + ToggleDropDownMenu(1, nil, _G['PartyMemberFrame' .. string.sub(unit,6) .. 'DropDown'], 'cursor') + elseif string.sub(unit, 1, 4) == 'raid' then + HideDropDownMenu(1) + + local menuFrame = FriendsDropDown + menuFrame.displayMode = 'MENU' + menuFrame.id = string.sub(this.unit,5) + menuFrame.unit = unit + menuFrame.name = UnitName(this.unit) + menuFrame.initialize = function() + UnitPopup_ShowMenu(getglobal(UIDROPDOWNMENU_OPEN_MENU), "PARTY", self.unit, self.name, self.id) + end + + ToggleDropDownMenu(1, nil, FriendsDropDown, 'cursor') + end +end + +ACTIONS.macro = function(self, unit, button) + local macro_text = self:GetAttribute('macrotext') + local spell, unit = SecureCmdOptionParse(macro_text) + CastSpellByName(spell, unit) +end + +ACTIONS.spell = function(self, unit, button) + local spell = self:GetAttribute('spell') + CastSpellByName(spell) +end + +ACTIONS.pet = function(self, unit, button) + local index = self:GetAttribute('pet') + CastPetAction(index, unit) +end + +ACTIONS.func = function(self, unit, button) + local func = SlashCmdList[self:GetAttribute('func')] + + if not func then + error(string.format('Slash command `%s` does not exist in SlashCmdList')) + return + end + + func() +end + +local link_pattern = '%[([^%]]+)%]' +local function CmdItemParse(item) + local slot = tonumber(item) + if slot then + return nil, nil, slot + end + + local link, name + for bag = 0, 4 do + for slot = 1, GetContainerNumSlots(bag) do + link = GetContainerItemLink(bag, slot) + + if link then + name = string.match(link, link_pattern) + if name and name == item then + return name, bag, slot + end + end + end + end +end + +ACTIONS.item = function(self, unit, button) + local item = self:GetAttribute('item') + + if item then + local name, bag, slot = CmdItemParse(item) + UseItem(name, bag, slot) + end +end + +function UseItem(name, bag, slot) + if bag then + UseContainerItem(bag, slot) + elseif slot then + UseInventoryItem(slot) + end +end + +ACTIONS.assign = function(self, unit, button) + local macro_text = self:GetAttribute('assign') + local symbol, unit = SecureCmdOptionParse(macro_text) + SetRaidTargetIcon(unit, symbol) +end + +function Button_GetModifierPrefix(frame) + local prefix = '' + prefix = IsShiftKeyDown() and 'shift-' .. prefix or prefix + prefix = IsControlKeyDown() and 'ctrl-' .. prefix or prefix + return IsAltKeyDown() and 'alt-' .. prefix or prefix +end + +function Button_GetButtonSuffix(button) + if button == 'LeftButton' then + return '1' + elseif button == 'RightButton' then + return '2' + elseif button == 'MiddleButton' then + return '3' + end + + return ''; +end + +function Button_GetModifiedAttribute(frame, name, button, prefix, suffix) + if not prefix then + prefix = Button_GetModifierPrefix(frame) + end + if not suffix then + suffix = Button_GetButtonSuffix(button) + end + + return frame:GetAttribute(prefix, name, suffix) +end + +local function OnClick(self, button) + if not self.GetAttribute then return end + + local unit = self:GetAttribute('unit') + + local action_type = Button_GetModifiedAttribute(self, 'type', button) + + if action_type then + local handler = ACTIONS[action_type] + if handler and type(handler) == 'function' then + handler(self, unit, button) + end + end + +end + +local function SetScript(self, handler, func) + if self.__state_driver.handlers[handler] then + if self:HasScript(handler) and handler == 'OnClick' then + self.__state_driver._SetScript(self, handler, function(...) + OnClick(this, arg1) + func() + end) + else + self.__state_driver.handlers[handler] = func + end + else + self.__state_driver._SetScript(self, handler, func) + end +end + +local _CreateFrame = CreateFrame +function CreateFrame(...) + local frame = _CreateFrame(unpack(arg)) + + -- State Driver environment + frame.__state_driver = {} + + frame.__state_driver._SetScript = frame.SetScript + frame.SetScript = SetScript + + frame.__state_driver.attributes = {} + frame.__state_driver.handlers = { + ['OnAttributeChanged'] = true, + ['OnClick'] = true + } + + frame.GetAttribute = GetAttribute + frame.SetAttribute = SetAttribute + + if frame:HasScript('OnClick') and not frame:GetScript('OnClick') then + frame:SetScript('OnClick', NOOP) + end + + return frame +end + +local function Execute(frame, func) + +end + +local function WrapScript(frame, func) + +end + +local function UnwrapScript(frame, func) + +end + +local function ChildUpdate(frame, attr_snippet, value) + local children = { frame:GetChildren() } + local childUpdate + + for _, child in next, children do + if child.GetAttribute then + childUpdate = child:GetAttribute('_childupdate-' .. attr_snippet) + if childUpdate then + childUpdate(child, value) + else + childUpdate = child:GetAttribute('_childupdate') + if childUpdate then + childUpdate(child, value) + end + end + ChildUpdate(child, attr_snippet, value) + end + end +end + +-- +-- SecureStateDriverManager +-- Automatically sets states based on macro options for state driver frames +-- Also handled showing/hiding frames based on unit existence (code originally by Tem) +-- + +-- Register a frame attribute to be set automatically with changes in game state +function RegisterAttributeDriver(frame, attribute, values) + frame.Execute = Execute + frame.WrapScript = WrapScript + frame.UnwrapScript = UnwrapScript + frame.ChildUpdate = ChildUpdate + if attribute and values and string.sub(attribute, 1, 1) ~= '_' then + Manager:SetAttribute('setframe', frame) + Manager:SetAttribute('setstate', attribute .. ' ' .. values) + end +end + +-- Unregister a frame from the state driver manager. +function UnregisterAttributeDriver(frame, attribute) + if attribute then + Manager:SetAttribute('setframe', frame) + Manager:SetAttribute('setstate', attribute) + else + Manager:SetAttribute('delframe', frame) + end +end + +-- Bridge functions for compatibility +function RegisterStateDriver(frame, state, values) + return RegisterAttributeDriver(frame, 'state-' .. state, values) +end + +function UnregisterStateDriver(frame, state) + return UnregisterAttributeDriver(frame, 'state-' .. state) +end + +-- Register a frame to be notified when a unit's existence changes, the +-- unit is obtained from the frame's attributes. If asState is true then +-- notification is via the 'state-unitexists' attribute with values +-- true and false. Otherwise it's via :Show() and :Hide() +function RegisterUnitWatch(frame, asState) + if asState then + Manager:SetAttribute('addwatchstate', frame) + else + Manager:SetAttribute('addwatch', frame) + end +end + +-- Unregister a frame from the unit existence monitor. +function UnregisterUnitWatch(frame) + SecureStateDriverManager:SetAttribute('removewatch', frame) +end + +-- +-- Private implementation +-- +local secureAttributeDrivers = {} +local unitExistsWatchers = {} +local unitExistsCache = setmetatable( + {}, + { __index = function(t,k) + local v = UnitExists(k) or false + t[k] = v + return v + end +}) +local STATE_DRIVER_UPDATE_THROTTLE = 0.1 +local timer = 0 + +local wipe = table.wipe + +-- Check to see if a frame is registered +function UnitWatchRegistered(frame) + return not (unitExistsWatchers[frame] == nil) +end + +local function SecureStateDriverManager_UpdateUnitWatch(frame, doState) + -- Not really so secure, eh? + local unit = frame:GetAttribute('unit') + local exists = (unit and unitExistsCache[unit]) + if doState then + local attr = exists or false + if frame:GetAttribute('state-unitexists') ~= attr then + frame:SetAttribute('state-unitexists', attr) + end + else + if exists then + frame:Show() + frame:SetAttribute('statehidden', nil) + else + frame:Hide() + frame:SetAttribute('statehidden', true) + end + end +end + +local pairs = pairs + +-- consolidate duplicated code for footprint and maintainability +local function resolveDriver(frame, attribute, values) + local newValue = SecureCmdOptionParse(values) + + if attribute == 'state-visibility' then + if newValue == 'show' then + frame:Show() + frame:SetAttribute('statehidden', nil) + elseif newValue == 'hide' then + frame:Hide() + frame:SetAttribute('statehidden', true) + end + elseif newValue then + if newValue == 'nil' then + newValue = nil + else + newValue = tonumber(newValue) or newValue + end + local oldValue = frame:GetAttribute(attribute) + if newValue ~= oldValue then + frame:SetAttribute(attribute, newValue) + local onState = frame:GetAttribute('_on' .. attribute) + if onState then + onState(frame, attribute, newValue) + end + end + end +end + +local function OnUpdate() + local self, elapsed = this, arg1 + + timer = timer - elapsed + if timer <= 0 then + timer = STATE_DRIVER_UPDATE_THROTTLE + + -- Handle state driver updates + for frame, drivers in next, secureAttributeDrivers do + for attribute, values in next, drivers do + resolveDriver(frame, attribute, values) + end + end + + -- Handle unit existence changes + wipe(unitExistsCache) + for k in next, unitExistsCache do + unitExistsCache[k] = nil + end + for frame, doState in next, unitExistsWatchers do + SecureStateDriverManager_UpdateUnitWatch(frame, doState) + end + end +end + +local function OnEvent() + local self, event = this, arg1 + + timer = 0 +end + +local function OnAttributeChanged(self, name, value) + if not value then + return + end + + if name == 'setframe' then + if not secureAttributeDrivers[value] then + secureAttributeDrivers[value] = {} + end + SecureStateDriverManager:Show() + elseif name == 'delframe' then + secureAttributeDrivers[value] = nil + elseif name == 'setstate' then + local frame = self:GetAttribute('setframe') + local attribute, values = string.match(value, '^(%S+)%s*(.*)$') + + if values == '' then + secureAttributeDrivers[frame][attribute] = nil + else + secureAttributeDrivers[frame][attribute] = values + resolveDriver(frame, attribute, values) + end + -- Two frames registering with identical setstate fails unless the value + -- is reset afterwards + self:SetAttribute('setstate', nil) + elseif name == 'addwatch' or name == 'addwatchstate' then + local doState = (name == 'addwatchstate') + unitExistsWatchers[value] = doState + SecureStateDriverManager:Show() + SecureStateDriverManager_UpdateUnitWatch(value, doState) + elseif name == 'removewatch' then + unitExistsWatchers[value] = nil + elseif name == 'updatetime' then + STATE_DRIVER_UPDATE_THROTTLE = value + end +end + + +Manager = SD:New('Manager') +Manager:SetScript('OnUpdate', OnUpdate) +Manager:SetScript('OnEvent', OnEvent) +Manager:SetScript('OnAttributeChanged', OnAttributeChanged) + +-- Events that trigger early rescans +Manager:RegisterEvent('MODIFIER_STATE_CHANGED') +Manager:RegisterEvent('ACTIONBAR_PAGE_CHANGED') +Manager:RegisterEvent('UPDATE_BONUS_ACTIONBAR') +Manager:RegisterEvent('PLAYER_ENTERING_WORLD') +Manager:RegisterEvent('UPDATE_SHAPESHIFT_FORM') +Manager:RegisterEvent('UPDATE_STEALTH') +Manager:RegisterEvent('PLAYER_TARGET_CHANGED') +Manager:RegisterEvent('PLAYER_FOCUS_CHANGED') +Manager:RegisterEvent('PLAYER_REGEN_DISABLED') +Manager:RegisterEvent('PLAYER_REGEN_ENABLED') +Manager:RegisterEvent('UNIT_PET') +Manager:RegisterEvent('GROUP_ROSTER_UPDATE') +-- Deliberately ignoring mouseover and others' target changes because they change so much + +_G['SecureStateDriverManager'] = Manager \ No newline at end of file diff --git a/!Compatibility/libs/libs.xml b/!Compatibility/libs/libs.xml index b37fb2c..6c654fe 100644 --- a/!Compatibility/libs/libs.xml +++ b/!Compatibility/libs/libs.xml @@ -3,4 +3,7 @@