You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

300 lines
10 KiB

--========================================================--
-- Scorpio UnitFrame FrameWork --
-- --
-- Author : kurapica125@outlook.com --
-- Create Date : 2020/06/09 --
--========================================================--
--========================================================--
Scorpio "Scorpio.Secure.UnitReactive" "1.0.0"
--========================================================--
import "System.Reactive"
local getCurrentTarget = Scorpio.UI.Style.GetCurrentTarget
local isUIObject = UI.IsUIObject
local isObjectType = Class.IsObjectType
local FromEvent = Scorpio.Wow.FromEvent
--- The interface should be extended by all unit frame types(include secure and non-secure)
__Sealed__()
interface "IUnitFrame" (function(_ENV)
------------------------------------------------------
-- Event --
------------------------------------------------------
--- Fired when the unit frame need refreshing
__Abstract__()
event "OnUnitRefresh"
--- The current unit
__Abstract__()
property "Unit" { type = String, event = OnUnitRefresh }
end)
--- The unsecure unit frame that'd be used as nameplates
__Sealed__()
class "InSecureUnitFrame" { Frame, IUnitFrame }
__Sealed__()
class "UnitFrameSubject" (function(_ENV)
inherit "Subject"
local _UnitFrameMap = Toolset.newtable(true)
local function OnUnitRefresh(self, unit)
self = _UnitFrameMap[self]
self.Unit = unit
end
local _Recycle = Recycle()
local _UnitGuidMap = {}
local _GuidUnitMap = {}
local NAMEPLATE_SUBJECT = FromEvent("NAME_PLATE_UNIT_ADDED", "NAME_PLATE_UNIT_REMOVED")
local RAID_UNIT_SUBJECT = FromEvent("UNIT_NAME_UPDATE", "GROUP_ROSTER_UPDATE"):Map(function() return "any" end):Next() -- Force All
local function refreshUnitGuidMap(unit)
local guid = UnitGUID(unit)
local oguid = _UnitGuidMap[unit]
if guid == oguid then return end
_UnitGuidMap[unit] = guid
if guid then
local map = _GuidUnitMap[guid]
if not map then
map = _Recycle()
_GuidUnitMap[guid] = map
end
map[unit] = true
end
if oguid then
local map = _GuidUnitMap[oguid]
if map then
map[unit] = nil
if not next(map) then
-- Clear
_GuidUnitMap[oguid] = nil
_Recycle(map)
end
end
end
end
----------------------------------------------------
-- Extend Method To Scorpio
----------------------------------------------------
__Static__()
function Scorpio.GetUnitFromGUID(guid)
local map = _GuidUnitMap[guid]
return (map and next(map))
end
__Static__() __Iterator__()
function Scorpio.GetUnitsFromGUID(guid)
local map = _GuidUnitMap[guid]
if map then for unit in pairs(map) do yield(unit) end end
end
----------------------------------------------------
-- Method
----------------------------------------------------
function Subscribe(self, ...)
local observer = super.Subscribe(self, ...)
if self.Unit then observer:OnNext(self.Unit) end
end
-- Don't use __AsyncSingle__ here to reduce the memory garbage collect
__Async__()
function RefreshUnit(self, unit, oldunit)
self.TaskId = (self.TaskId or 0) + 1
local task = self.TaskId
-- May clear the old unit's cache
if oldunit then refreshUnitGuidMap(oldunit) end
if not unit then
-- need sepcial unit to be passed to clear the values
self:OnNext("clear", true)
elseif unit:match("%w+target") then
local frm = self.UnitFrame
while task == self.TaskId do
while frm:IsShown() and task == self.TaskId do
self:OnNext(unit, true)
Delay(self.Interval)
end
-- Wait the unit frame re-show
if task == self.TaskId then
Next(Observable.From(frm.OnShow))
end
end
elseif unit == "player" then
refreshUnitGuidMap(unit)
self:OnNext(unit)
elseif unit == "target" then
while task == self.TaskId do
refreshUnitGuidMap(unit)
self:OnNext(unit)
NextEvent("PLAYER_TARGET_CHANGED")
end
elseif unit == "mouseover" then
while task == self.TaskId do
refreshUnitGuidMap(unit)
self:OnNext(unit)
NextEvent("UPDATE_MOUSEOVER_UNIT")
end
elseif unit == "focus" then
while task == self.TaskId do
refreshUnitGuidMap(unit)
self:OnNext(unit)
NextEvent("PLAYER_FOCUS_CHANGED")
end
elseif unit:match("pet") then
local owner = unit:match("^(%w+)pet")
local index = owner and unit:match("%d+")
if not owner then
owner = "player"
elseif index then
owner = owner .. index
else
-- Not valid
return self:OnNext("none", true)
end
while task == self.TaskId do
refreshUnitGuidMap(unit)
self:OnNext(unit)
Next(FromEvent("UNIT_PET"):MatchUnit(owner))
end
elseif unit:match("nameplate") then
while task == self.TaskId do
refreshUnitGuidMap(unit)
self:OnNext(unit)
Next(NAMEPLATE_SUBJECT:MatchUnit(unit))
end
elseif unit:match("^party%d") or unit:match("^raid%d") then
while task == self.TaskId do
refreshUnitGuidMap(unit)
self:OnNext(unit)
if (UnitHealthMax(unit) or 0) > 0 then
Next(RAID_UNIT_SUBJECT:MatchUnit(unit))
else
-- Waiting the unit's info
Delay(0.1)
end
end
else
-- Other units: arenaN, bossN, vehicle, spectated<T><N>
while task == self.TaskId do
refreshUnitGuidMap(unit)
self:OnNext(unit)
Next(FromEvent("UNIT_NAME_UPDATE"):MatchUnit(unit))
end
end
end
----------------------------------------------------
-- Property
----------------------------------------------------
--- the unit frame
property "UnitFrame" { type = IUnitFrame }
--- The current unit
property "Unit" { type = String, handler = RefreshUnit }
--- The current unit
property "Interval" { type = PositiveNumber, default = function(self) return self.UnitFrame.Interval or 0.5 end }
----------------------------------------------------
-- Constructor
----------------------------------------------------
__Arguments__{ IUnitFrame }
function __ctor(self, unitfrm)
self.UnitFrame = unitfrm
_UnitFrameMap[unitfrm] = self
unitfrm.OnUnitRefresh = unitfrm.OnUnitRefresh + OnUnitRefresh
self.Unit = unitfrm.Unit
end
function __exist(_, unitfrm)
return _UnitFrameMap[unitfrm]
end
end)
local function getUnitFrameSubject()
local indicator = getCurrentTarget()
if indicator and isUIObject(indicator) then
local unitfrm = indicator
while unitfrm and not isObjectType(unitfrm, IUnitFrame) do
unitfrm = unitfrm:GetParent()
end
return unitfrm and UnitFrameSubject(unitfrm)
end
end
local unitFrameObservable = Toolset.newtable(true)
local function genUnitFrameObservable(unitEvent)
local observable = unitFrameObservable[unitEvent or 0]
if not observable then
observable = Observable(function(observer)
local unitSubject = getUnitFrameSubject()
if not unitSubject then return unitEvent and unitEvent:Subscribe(observer) end
if not unitEvent then return unitSubject:Subscribe(observer) end
-- Unit event observer
local obsEvent = Observer(function(...) return observer:OnNext(...) end)
-- Unit change observer
local obsUnit = Observer(function(unit, noevent)
obsEvent:Unsubscribe() -- Clear the previous observable
obsEvent:Resubscribe()
if not noevent then
unitEvent:MatchUnit(unit):Subscribe(obsEvent)
end
observer:OnNext(unit)
end)
local onUnsubscribe
onUnsubscribe = function()
observer.OnUnsubscribe = observer.OnUnsubscribe - onUnsubscribe
obsEvent:Unsubscribe()
obsUnit:Unsubscribe()
end
observer.OnUnsubscribe = observer.OnUnsubscribe + onUnsubscribe
-- Start the unit watching
unitSubject:Subscribe(obsUnit)
end)
unitFrameObservable[unitEvent or 0] = observable
end
return observable
end
------------------------------------------------------------
-- Wow API --
------------------------------------------------------------
--- The data sequences from the wow unit event binding to unit frames
__Arguments__{ (NEString + IObservable)/nil, NEString * 0 }
__Static__()
function Wow.FromUnitEvent(observable, ...)
if type(observable) == "string" then
return genUnitFrameObservable(FromEvent(observable, ...))
else
return genUnitFrameObservable(observable)
end
end