Puppeteer/libs/UnitProxy.lua
2025-09-05 16:21:39 -07:00

460 lines
14 KiB
Lua

-- Proxy unit functions that resolve custom units to their GUIDs before doing what they do.
-- One might say you'd be insane to do this just to have custom units, and you'd be correct.
-- SuperWoW check, can't rely on PTUtil because it depends on this file
if SpellInfo == nil then
return
end
PTUnitProxy = {}
local _G = getfenv(0)
setmetatable(PTUnitProxy, {__index = getfenv(1)})
setfenv(1, PTUnitProxy)
local compost = AceLibrary("Compost-2.0")
-- Unit data
CustomUnitTypes = {} -- Array of custom unit types(ex: "focus")
CustomUnitTypesSet = {} -- Set of custom unit types
CustomUnitsTypeMap = {} -- Key: Custom unit(ex: "focus5") | Value: Unit type(ex: "focus")
CustomUnitsMap = {} -- Key: Type(ex: "focus") | Value: Array of all units
CustomUnitsSetMap = {} -- Key: Type(ex: "focus") | Value: Set of all units
AllCustomUnits = {} -- Array of all custom units
AllCustomUnitsSet = {} -- Set of all custom units
-- End of unit data
GUIDCustomUnitMap = {} -- Key: GUID | Value: Array of custom units
--UnitTypeGUIDUnitMap = {} -- Key: Unit type | Value: {Key: GUID | Value: Custom unit of type}
CustomUnitGUIDMap = {} -- Key: Custom unit | Value: GUID
DEFAULT_CUSTOM_UNITS = {["focus"] = 40}
UpdateListeners = {}
function RegisterCustomUnits(unitName, maxUnits)
table.insert(CustomUnitTypes, unitName)
CustomUnitTypesSet[unitName] = 1
local units = {}
local unitsSet = {}
for i = 1, maxUnits do
local unit = unitName..i
units[i] = unit
unitsSet[unit] = 1
CustomUnitsTypeMap[unit] = unitName
table.insert(AllCustomUnits, unit)
AllCustomUnitsSet[unit] = 1
end
CustomUnitsMap[unitName] = units
CustomUnitsSetMap[unitName] = unitsSet
for _, listener in ipairs(UpdateListeners) do
listener()
end
end
for unitName, maxUnits in pairs(DEFAULT_CUSTOM_UNITS) do
RegisterCustomUnits(unitName, maxUnits)
end
-- Listen for new custom unit registries
function RegisterUpdateListener(listener)
table.insert(UpdateListeners, listener)
end
function UnregisterUpdateListener(listener)
PTUtil.RemoveElement(UpdateListeners, listener)
end
ImportedEnvironments = {}
UnitFunctions = {}
function UpdateUnitTypeFrames(unitType)
local groups = compost:GetTable()
for _, unit in ipairs(CustomUnitsMap[unitType]) do
local guid = CustomUnitGUIDMap[unit]
for ui in Puppeteer.UnitFrames(unit) do
if guid then
ui:Show()
ui.guidUnit = guid
ui:UpdateAll()
ui:UpdateIncomingHealing()
else
ui:Hide()
ui.guidUnit = nil
end
groups[ui.owningGroup] = 1
end
end
for group, _ in pairs(groups) do
group:EvaluateShown()
end
compost:Reclaim(groups)
end
function UpdateUnitFrames(unit)
local groups = compost:GetTable()
local guid = CustomUnitGUIDMap[unit]
for ui in Puppeteer.UnitFrames(unit) do
if guid then
ui:Show()
ui.guidUnit = guid
ui:UpdateAll()
else
ui:Hide()
ui.guidUnit = nil
end
groups[ui.owningGroup] = 1
end
for group, _ in pairs(groups) do
group:EvaluateShown()
end
compost:Reclaim(groups)
end
function GetCurrentUnitOfType(guid, unitType)
local unitArray = GUIDCustomUnitMap[guid]
if not unitArray then
return
end
for _, unit in ipairs(unitArray) do
if CustomUnitsTypeMap[unit] == unitType then
return unit
end
end
end
-- Sets the GUID of the custom unit. If the GUID already has a unit of the type, this will do nothing.
-- If the GUID is nil, the unit will be freed. Returns true if state was changed.
function SetCustomUnitGuid(unit, guid, skipUpdate)
if guid then
local unitType = CustomUnitsTypeMap[unit]
local currentUnit = GetCurrentUnitOfType(guid, unitType)
if currentUnit then
return
end
CustomUnitGUIDMap[unit] = guid
local unitArray = GUIDCustomUnitMap[guid]
if not unitArray then
unitArray = compost:GetTable()
GUIDCustomUnitMap[guid] = unitArray
end
table.insert(unitArray, unit)
PTGuidRoster.SetUnitGuid(unit, guid)
PTUnit.UpdateGuidCaches()
if not skipUpdate then
UpdateUnitFrames(unit)
end
return true
else
local guid = CustomUnitGUIDMap[unit]
if guid then
CustomUnitGUIDMap[unit] = nil
local unitArray = GUIDCustomUnitMap[guid]
if table.getn(unitArray) == 1 then
compost:Reclaim(unitArray)
GUIDCustomUnitMap[guid] = nil
else
PTUtil.RemoveElement(unitArray, unit)
end
PTGuidRoster.SetUnitGuid(unit, nil)
PTUnit.UpdateGuidCaches()
if not skipUpdate then
UpdateUnitFrames(unit)
end
return true
end
end
end
function ClearGuidUnitType(guid, unitType)
for _, unit in ipairs(CustomUnitsMap[unitType]) do
if CustomUnitGUIDMap[unit] then
SetCustomUnitGuid(unit, nil)
return unit
end
end
end
-- Finds a free unit of the unit type to assign for the GUID. Returns the unit if successful, otherwise nil.
function SetGuidUnitType(guid, unitType, skipUpdate)
if GetCurrentUnitOfType(guid, unitType) then
return
end
local selectedUnit
for _, unit in ipairs(CustomUnitsMap[unitType]) do
if not CustomUnitGUIDMap[unit] then
selectedUnit = unit
break
end
end
if not selectedUnit then
return
end
SetCustomUnitGuid(selectedUnit, guid, skipUpdate)
return selectedUnit
end
-- Promotes a GUID from a lower unit number of the type to the first unit number. For example, if the GUID
-- is "focus5", it would be moved to "focus1" and all units in between would be shifted down and/or compressed.
-- TODO: Probably should just handle promotion in unit frame sorting rather than actually changing their IDs.
function PromoteGuidUnitType(guid, unitType)
local unit = GetCurrentUnitOfType(guid, unitType)
if not unit then
return
end
local units = CustomUnitsMap[unitType]
local index = PTUtil.IndexOf(units, unit)
SetCustomUnitGuid(unit, nil)
-- Remove all units before this unit
local reacquire = compost:GetTable()
for i = 1, index do
local moveUnit = units[i]
if CustomUnitGUIDMap[moveUnit] then
local moveGuid = CustomUnitGUIDMap[moveUnit]
SetCustomUnitGuid(moveUnit, nil, true)
table.insert(reacquire, moveGuid)
end
end
-- Set the promotion first so they get the first spot
SetGuidUnitType(guid, unitType, true)
-- Reacquire the removed units
for _, reacquireGuid in ipairs(reacquire) do
SetGuidUnitType(reacquireGuid, unitType, true)
end
compost:Reclaim(reacquire)
UpdateUnitTypeFrames(unitType)
end
function IsGuidCustomUnit(guid, unit)
local unitArray = GUIDCustomUnitMap[guid]
for _, customUnit in ipairs(unitArray) do
if customUnit == unit then
return true
end
end
return false
end
function IsUnitUnitType(unit, unitType)
local guid = PTGuidRoster.ResolveUnitGuid(unit)
return IsGuidUnitType(guid, unitType)
end
function IsGuidUnitType(guid, unitType)
local unitArray = GUIDCustomUnitMap[guid]
if not unitArray then
return false
end
for _, customUnit in ipairs(unitArray) do
if CustomUnitsTypeMap[customUnit] == unitType then
return true
end
end
return false
end
-- Returns a normal unit associated with the custom unit or GUID, or nil if there is none
function ResolveCustomUnit(customUnit)
local guid = CustomUnitGUIDMap[customUnit] or customUnit
local units = PTGuidRoster.GetUnits(guid)
if units and table.getn(units) > 1 then
for _, rosterUnit in ipairs(units) do
if not AllCustomUnitsSet[rosterUnit] then
return rosterUnit
end
end
end
end
function CycleUnitType(unitType, onlyAttackable)
local targetGuid = PTGuidRoster.GetUnitGuid("target")
local typeTarget = GetCurrentUnitOfType(targetGuid, unitType)
local typeUnits = CustomUnitsMap[unitType]
if not typeTarget then -- Not targeting this unit type
for _, typeUnit in ipairs(typeUnits) do
if UnitExists(typeUnit) then
TargetUnit(typeUnit)
return
end
end
return
end
local maxUnits = table.getn(typeUnits)
local targetIndex = PTUtil.IndexOf(typeUnits, typeTarget)
local i = targetIndex
for e = 1, maxUnits - 1 do
i = i + 1
if i > maxUnits then
i = i - maxUnits
end
local guid = CustomUnitGUIDMap[typeUnits[i]]
if guid and UnitExists(guid) and (not onlyAttackable or UnitCanAttack("player", guid)) then
TargetUnit(guid)
break
end
end
end
function ImportFunctions(env)
for name, func in pairs(UnitFunctions) do
env[name] = func
end
table.insert(ImportedEnvironments, env)
end
function UpdateImports()
for _, env in ipairs(ImportedEnvironments) do
for name, func in pairs(UnitFunctions) do
env[name] = func
end
end
end
function UnitProxy(name, func, defaultValue)
local proxy = function(unit)
if AllCustomUnitsSet[unit] then
unit = CustomUnitGUIDMap[unit]
if not unit then
return defaultValue
end
end
if not unit then
return
end
return func(unit)
end
PTUnitProxy[name] = proxy
UnitFunctions[name] = proxy
end
function DoubleUnitProxy(name, func, defaultValue)
local proxy = function(unit1, unit2)
if AllCustomUnitsSet[unit1] then
unit1 = CustomUnitGUIDMap[unit1]
if not unit1 then
return defaultValue
end
end
if AllCustomUnitsSet[unit2] then
unit2 = CustomUnitGUIDMap[unit2]
if not unit2 then
return defaultValue
end
end
return func(unit1, unit2)
end
PTUnitProxy[name] = proxy
UnitFunctions[name] = proxy
end
function AuraProxy(name, func, defaultValue)
local proxy = function(unit, index)
if AllCustomUnitsSet[unit] then
unit = CustomUnitGUIDMap[unit]
if not unit then
return defaultValue
end
end
return func(unit, index)
end
PTUnitProxy[name] = proxy
UnitFunctions[name] = proxy
end
function CustomProxy(name, constructor)
local proxy = constructor()
PTUnitProxy[name] = proxy
UnitFunctions[name] = proxy
end
function CustomFunction(name, func)
PTUnitProxy[name] = func
UnitFunctions[name] = func
end
function CreateUnitProxies()
AuraProxy("UnitBuff", _G.UnitBuff, nil)
AuraProxy("UnitDebuff", _G.UnitDebuff, nil)
UnitProxy("UnitExists", _G.UnitExists, false)
UnitProxy("UnitHealth", _G.UnitHealth, 0)
UnitProxy("UnitHealthMax", _G.UnitHealthMax, 0)
UnitProxy("UnitMana", _G.UnitMana, 0)
UnitProxy("UnitManaMax", _G.UnitManaMax, 0)
UnitProxy("UnitIsPlayer", _G.UnitIsPlayer, false)
UnitProxy("UnitIsConnected", _G.UnitIsConnected, false)
UnitProxy("UnitIsDead", _G.UnitIsDead, false)
UnitProxy("UnitIsGhost", _G.UnitIsGhost, false)
UnitProxy("UnitIsDeadOrGhost", _G.UnitIsDeadOrGhost, false)
UnitProxy("UnitIsCorpse", _G.UnitIsCorpse, false)
UnitProxy("UnitClass", _G.UnitClass, "")
UnitProxy("UnitName", _G.UnitName, "Unknown")
UnitProxy("UnitPowerType", _G.UnitPowerType, 0)
UnitProxy("UnitIsVisible", _G.UnitIsVisible, false)
UnitProxy("UnitIsCharmed", _G.UnitIsCharmed, false)
UnitProxy("TargetUnit", _G.TargetUnit, nil)
UnitProxy("FollowUnit", _G.FollowUnit, nil)
UnitProxy("AssistUnit", _G.AssistUnit, nil)
UnitProxy("UnitAffectingCombat", _G.UnitAffectingCombat, false)
UnitProxy("UnitIsPVP", _G.UnitIsPVP, false)
UnitProxy("UnitFactionGroup", _G.UnitFactionGroup, nil)
DoubleUnitProxy("UnitIsFriend", _G.UnitIsFriend, false)
DoubleUnitProxy("UnitIsEnemy", _G.UnitIsEnemy, false)
DoubleUnitProxy("UnitIsUnit", _G.UnitIsUnit, false)
DoubleUnitProxy("UnitCanAttack", _G.UnitCanAttack, false)
DoubleUnitProxy("CheckInteractDistance", _G.CheckInteractDistance, false)
CustomProxy("CastSpellByName", function()
local CastSpellByName = _G.CastSpellByName
return function(spell, unit)
if AllCustomUnitsSet[unit] then
unit = CustomUnitGUIDMap[unit]
if not unit then
return
end
end
return CastSpellByName(spell, unit)
end
end)
-- UnitXP SP3 compatibility
CustomProxy("UnitXP", function()
local UnitXP = _G.UnitXP
return function(a1, a2, a3, a4, a5)
if a1 then
a1 = AllCustomUnitsSet[a1] and CustomUnitGUIDMap[a1] or a1
if not a1 then return 0 end
end
if a2 then
a2 = AllCustomUnitsSet[a2] and CustomUnitGUIDMap[a2] or a2
if not a2 then return 0 end
end
if a3 then
a3 = AllCustomUnitsSet[a3] and CustomUnitGUIDMap[a3] or a3
if not a3 then return 0 end
end
if a4 then
a4 = AllCustomUnitsSet[a4] and CustomUnitGUIDMap[a4] or a4
if not a4 then return 0 end
end
if a5 then
a5 = AllCustomUnitsSet[a5] and CustomUnitGUIDMap[a5] or a5
if not a5 then return 0 end
end
return UnitXP(a1, a2, a3, a4, a5)
end
end)
UpdateImports()
end
CreateUnitProxies()