1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Added spell channeling support

This commit is contained in:
SpoinkyNL 2017-09-11 01:48:46 +02:00
parent 7341613f21
commit 3b28170217
19 changed files with 3104 additions and 3028 deletions

View File

@ -17,8 +17,9 @@ namespace Artemis.Modules.Games.WoW.Models
public DateTime EndTime { get; set; } public DateTime EndTime { get; set; }
public bool NonInterruptible { get; set; } public bool NonInterruptible { get; set; }
public float Progress { get; set; } public float Progress { get; set; }
public bool IsChannel { get; set; }
public void ApplyJson(JToken spellJson) public void ApplyJson(JToken spellJson, bool isChannel)
{ {
var castMs = spellJson["e"].Value<int>() - spellJson["s"].Value<int>(); var castMs = spellJson["e"].Value<int>() - spellJson["s"].Value<int>();
var tickCount = Environment.TickCount; var tickCount = Environment.TickCount;
@ -29,6 +30,7 @@ namespace Artemis.Modules.Games.WoW.Models
StartTime = new DateTime(DateTime.Now.Ticks + difference); StartTime = new DateTime(DateTime.Now.Ticks + difference);
EndTime = StartTime.AddMilliseconds(castMs); EndTime = StartTime.AddMilliseconds(castMs);
NonInterruptible = spellJson["ni"].Value<bool>(); NonInterruptible = spellJson["ni"].Value<bool>();
IsChannel = isChannel;
} }
public void UpdateProgress() public void UpdateProgress()
@ -39,8 +41,12 @@ namespace Artemis.Modules.Games.WoW.Models
var elapsed = DateTime.Now - StartTime; var elapsed = DateTime.Now - StartTime;
var total = EndTime - StartTime; var total = EndTime - StartTime;
Progress = (float) (elapsed.TotalMilliseconds / total.TotalMilliseconds); if (IsChannel)
if (Progress > 1) Progress = 1 - (float) (elapsed.TotalMilliseconds / total.TotalMilliseconds);
else
Progress = (float) (elapsed.TotalMilliseconds / total.TotalMilliseconds);
if (Progress > 1 || Progress < 0)
Clear(); Clear();
} }
@ -52,6 +58,7 @@ namespace Artemis.Modules.Games.WoW.Models
EndTime = DateTime.MinValue; EndTime = DateTime.MinValue;
NonInterruptible = false; NonInterruptible = false;
Progress = 0; Progress = 0;
IsChannel = false;
} }
} }
} }

View File

@ -1,9 +1,9 @@
## Interface: 70300 ## Interface: 70300
## Title: Artemis ## Title: Artemis
## Notes: Transmits ingame data to Artemis ## Notes: Transmits ingame data to Artemis
## Author: SpoinkyNL ## Author: SpoinkyNL
## Version: 1.0.0 ## Version: 1.0.0
embeds.xml embeds.xml
Core.lua Core.lua

View File

@ -1,297 +1,358 @@
Artemis = LibStub("AceAddon-3.0"):NewAddon("Artemis", "AceConsole-3.0", "AceEvent-3.0", "AceTimer-3.0", "AceComm-3.0") Artemis = LibStub("AceAddon-3.0"):NewAddon("Artemis", "AceConsole-3.0", "AceEvent-3.0", "AceTimer-3.0", "AceComm-3.0")
json = LibStub("json") json = LibStub("json")
local debugging = false local debugging = false
local lastLine = {} local lastLine = {}
local unitUpdates = {} local channeling = {}
local lastTransmitMessage local unitUpdates = {}
local lastTransmitTime local lastTransmitMessage
local lastBuffs = ""; local lastTransmitTime
local lastDebuffs = ""; local lastBuffs = "";
local prefixCounts = {} local lastDebuffs = "";
local prefixCounts = {}
function Artemis:OnEnable()
Artemis:RegisterEvent("PLAYER_ENTERING_WORLD") channeling["player"] = false
Artemis:RegisterEvent("PLAYER_LEVEL_UP") channeling["target"] = false
Artemis:RegisterEvent("ACHIEVEMENT_EARNED")
Artemis:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED") function Artemis:OnEnable()
Artemis:RegisterEvent("UNIT_TARGET") Artemis:RegisterEvent("PLAYER_ENTERING_WORLD")
Artemis:RegisterEvent("UNIT_HEALTH") Artemis:RegisterEvent("PLAYER_LEVEL_UP")
Artemis:RegisterEvent("UNIT_POWER") Artemis:RegisterEvent("ACHIEVEMENT_EARNED")
Artemis:RegisterEvent("UNIT_AURA") Artemis:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
Artemis:RegisterEvent("UNIT_SPELLCAST_START") Artemis:RegisterEvent("UNIT_TARGET")
Artemis:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED") Artemis:RegisterEvent("UNIT_HEALTH")
Artemis:RegisterEvent("UNIT_SPELLCAST_FAILED") Artemis:RegisterEvent("UNIT_POWER")
Artemis:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED") Artemis:RegisterEvent("UNIT_AURA")
Artemis:RegisterEvent("ZONE_CHANGED") Artemis:RegisterEvent("UNIT_SPELLCAST_START")
Artemis:RegisterEvent("ZONE_CHANGED_NEW_AREA") Artemis:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
Artemis:RegisterEvent("UNIT_SPELLCAST_FAILED")
Artemis:RegisterChatCommand("artemis", "HandleChatCommand") Artemis:RegisterEvent("UNIT_SPELLCAST_DELAYED")
end Artemis:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
Artemis:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
function Artemis:HandleChatCommand(input) Artemis:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
if input == "debug" then Artemis:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
debugging = not (debugging) Artemis:RegisterEvent("ZONE_CHANGED")
if debugging then Artemis:RegisterEvent("ZONE_CHANGED_NEW_AREA")
Artemis:Print("Debugging enabled.")
else Artemis:RegisterChatCommand("artemis", "HandleChatCommand")
Artemis:Print("Debugging disabled.") end
end
end function Artemis:HandleChatCommand(input)
if input == "rc" then if input == "debug" then
prefixCounts = {} debugging = not (debugging)
Artemis:Print("Reset the send counters.") if debugging then
end Artemis:Print("Debugging enabled.")
if input == nill or input == "" or input == "help" then else
Artemis:Print("Available chat commands:") Artemis:Print("Debugging disabled.")
Artemis:Printf("|cffb7b7b7/artemis debug|r: Toggle debugging") end
Artemis:Printf("|cffb7b7b7/artemis rc|r: Reset the debug counters") end
end if input == "rc" then
end prefixCounts = {}
Artemis:Print("Reset the send counters.")
function Artemis:Transmit(prefix, data, prio) end
local msg = "artemis(".. prefix .. "|" .. json.encode(data) ..")" if input == nill or input == "" or input == "help" then
-- If the message is the same as the previous, make sure it wasn't sent less than 250ms ago Artemis:Print("Available chat commands:")
if msg == lastTransmitMessage then Artemis:Printf("|cffb7b7b7/artemis debug|r: Toggle debugging")
if not (lastTransmitTime == nil) then Artemis:Printf("|cffb7b7b7/artemis rc|r: Reset the debug counters")
local diff = GetTime() - lastTransmitTime; end
if (diff < 0.25) then end
return
end function Artemis:Transmit(prefix, data, prio)
end local msg = "artemis(".. prefix .. "|" .. json.encode(data) ..")"
end -- If the message is the same as the previous, make sure it wasn't sent less than 250ms ago
if msg == lastTransmitMessage then
lastTransmitTime = GetTime() if not (lastTransmitTime == nil) then
local diff = GetTime() - lastTransmitTime;
if debugging == true then if (diff < 0.25) then
if prefixCounts[prefix] == nill then return
prefixCounts[prefix] = 0 end
end end
prefixCounts[prefix] = prefixCounts[prefix] + 1 end
end
lastTransmitTime = GetTime()
if debugging == true then
Artemis:Printf("Transmitting with prefix |cfffdff71" .. prefix .. "|r (" .. prefixCounts[prefix] .. ").") if debugging == true then
Artemis:Print(msg) if prefixCounts[prefix] == nill then
end prefixCounts[prefix] = 0
Artemis:SendCommMessage("(artemis)", msg, "WHISPER", UnitName("player"), prio) end
end prefixCounts[prefix] = prefixCounts[prefix] + 1
end
function Artemis:TransmitUnitState(unit, ignoreThrottle)
if not ignoreThrottle then if debugging == true then
if not (unitUpdates[unit] == nil) then Artemis:Printf("Transmitting with prefix |cfffdff71" .. prefix .. "|r (" .. prefixCounts[prefix] .. ").")
local diff = GetTime() - unitUpdates[unit] Artemis:Print(msg)
if (diff < 0.5) then end
return Artemis:SendCommMessage("(artemis)", msg, "WHISPER", UnitName("player"), prio)
end end
end
end function Artemis:TransmitUnitState(unit, ignoreThrottle)
if not ignoreThrottle then
local table = { if not (unitUpdates[unit] == nil) then
h = UnitHealth(unit), local diff = GetTime() - unitUpdates[unit]
mh = UnitHealthMax(unit), if (diff < 0.5) then
p = UnitPower(unit), return
mp = UnitPowerMax(unit), end
t = UnitPowerType(unit) end
}; end
unitUpdates[unit] = GetTime() local table = {
Artemis:Transmit(unit .. "State", table) h = UnitHealth(unit),
end mh = UnitHealthMax(unit),
p = UnitPower(unit),
function Artemis:GetUnitDetails(unit) mp = UnitPowerMax(unit),
return { t = UnitPowerType(unit)
n = UnitName(unit), };
c = UnitClass(unit),
l = UnitLevel(unit), unitUpdates[unit] = GetTime()
r = UnitRace(unit), Artemis:Transmit(unit .. "State", table)
g = UnitSex(unit), end
f = UnitFactionGroup(unit)
}; function Artemis:GetUnitDetails(unit)
end return {
n = UnitName(unit),
function Artemis:GetPlayerDetails() c = UnitClass(unit),
local details = Artemis:GetUnitDetails("player") l = UnitLevel(unit),
local id, name, _, _, role = GetSpecializationInfo(GetSpecialization()) r = UnitRace(unit),
g = UnitSex(unit),
details.realm = GetRealmName() f = UnitFactionGroup(unit)
details.achievementPoints = GetTotalAchievementPoints(false) };
details.s = {id = id, n = name, r = role} end
return details function Artemis:GetPlayerDetails()
end local details = Artemis:GetUnitDetails("player")
local id, name, _, _, role = GetSpecializationInfo(GetSpecialization())
function Artemis:GetUnitAuras(unit, filter)
local auras = {}; details.realm = GetRealmName()
for index = 1, 40 do details.achievementPoints = GetTotalAchievementPoints(false)
local name, _, _, count, _, duration, expires, caster, _, _, spellID = UnitAura(unit, index, filter); details.s = {id = id, n = name, r = role}
if not (name == nil) then
local buffTable = {n = name, id = spellID} return details
-- Leave these values out if they are 0 to save some space end
if count > 0 then
buffTable["c"] = count function Artemis:GetUnitAuras(unit, filter)
end local auras = {};
if duration > 0 then for index = 1, 40 do
buffTable["d"] = duration local name, _, _, count, _, duration, expires, caster, _, _, spellID = UnitAura(unit, index, filter);
end if not (name == nil) then
if expires > 0 then local buffTable = {n = name, id = spellID}
buffTable["e"] = expires -- Leave these values out if they are 0 to save some space
end if count > 0 then
table.insert(auras, buffTable) buffTable["c"] = count
end end
end if duration > 0 then
return auras buffTable["d"] = duration
end end
if expires > 0 then
function Artemis:PLAYER_ENTERING_WORLD(...) buffTable["e"] = expires
Artemis:Transmit("player", Artemis:GetPlayerDetails()) end
Artemis:TransmitUnitState("player", true); table.insert(auras, buffTable)
end end
end
function Artemis:PLAYER_LEVEL_UP(...) return auras
Artemis:Transmit("player", Artemis:GetPlayerDetails()) end
end
function Artemis:PLAYER_ENTERING_WORLD(...)
function Artemis:ACHIEVEMENT_EARNED(...) Artemis:Transmit("player", Artemis:GetPlayerDetails())
Artemis:Transmit("player", Artemis:GetPlayerDetails()) Artemis:TransmitUnitState("player", true);
end end
function Artemis:ACTIVE_TALENT_GROUP_CHANGED(...) function Artemis:PLAYER_LEVEL_UP(...)
Artemis:Transmit("player", Artemis:GetPlayerDetails()) Artemis:Transmit("player", Artemis:GetPlayerDetails())
end end
function Artemis:UNIT_TARGET(...) function Artemis:ACHIEVEMENT_EARNED(...)
local _, source = ... Artemis:Transmit("player", Artemis:GetPlayerDetails())
if not (source == "player") then end
return
end function Artemis:ACTIVE_TALENT_GROUP_CHANGED(...)
Artemis:Transmit("player", Artemis:GetPlayerDetails())
local details = Artemis:GetUnitDetails("target") end
Artemis:Transmit("target", details) function Artemis:UNIT_TARGET(...)
Artemis:TransmitUnitState("target", true); local _, source = ...
end if not (source == "player") then
return
function Artemis:UNIT_HEALTH(...) end
local _, source = ...
if not (source == "player") and not (source == "target") then local details = Artemis:GetUnitDetails("target")
return channeling["target"] = false
end
Artemis:Transmit("target", details)
Artemis:TransmitUnitState(source, false); Artemis:TransmitUnitState("target", true);
end end
function Artemis:UNIT_POWER(...) function Artemis:UNIT_HEALTH(...)
local _, source = ... local _, source = ...
if not (source == "player") and not (source == "target") then if not (source == "player") and not (source == "target") then
return return
end end
Artemis:TransmitUnitState(source, false); Artemis:TransmitUnitState(source, false);
end end
function Artemis:UNIT_AURA(...) function Artemis:UNIT_POWER(...)
local _, source = ... local _, source = ...
if not (source == "player") then if not (source == "player") and not (source == "target") then
return return
end end
local buffs = Artemis:GetUnitAuras(source, "PLAYER|HELPFUL") Artemis:TransmitUnitState(source, false);
local debuffs = Artemis:GetUnitAuras(source, "PLAYER|HARMFUL") end
local newBuffs = json.encode(buffs) function Artemis:UNIT_AURA(...)
local newDebuffs = json.encode(debuffs) local _, source = ...
if not (source == "player") then
if not (lastBuffs == newBuffs) then return
Artemis:Transmit("buffs", buffs) end
end
if not (lastDebuffs == newDebuffs) then local buffs = Artemis:GetUnitAuras(source, "PLAYER|HELPFUL")
Artemis:Transmit("debuffs", debuffs) local debuffs = Artemis:GetUnitAuras(source, "PLAYER|HARMFUL")
end
local newBuffs = json.encode(buffs)
lastBuffs = newBuffs local newDebuffs = json.encode(debuffs)
lastDebuffs = newDebuffs
end if not (lastBuffs == newBuffs) then
Artemis:Transmit("buffs", buffs)
-- Detect non-instant spell casts end
function Artemis:UNIT_SPELLCAST_START(...) if not (lastDebuffs == newDebuffs) then
local _, unitID, spell, rank, lineID, spellID = ... Artemis:Transmit("debuffs", debuffs)
if not (unitID == "player") and not (unitID == "target") then end
return
end lastBuffs = newBuffs
lastDebuffs = newDebuffs
local name, _, _, _, startTime, endTime, _, _, notInterruptible = UnitCastingInfo(unitID) end
local table = {uid = unitID, n = name, sid = spellID, s = startTime, e = endTime, ni = notInterruptible}
lastLine[unitID] = lineID -- Detect non-instant spell casts
function Artemis:UNIT_SPELLCAST_START(...)
Artemis:Transmit("spellCast", table, "ALERT") local _, unitID, spell, rank, lineID, spellID = ...
end if not (unitID == "player") and not (unitID == "target") then
return
-- Detect instant spell casts end
function Artemis:UNIT_SPELLCAST_SUCCEEDED (...)
local _, unitID, spell, rank, lineID, spellID = ... local name, _, _, _, startTime, endTime, _, _, notInterruptible = UnitCastingInfo(unitID)
if not (unitID == "player") and not (unitID == "target") then local table = {uid = unitID, n = name, sid = spellID, s = startTime, e = endTime, ni = notInterruptible}
return lastLine[unitID] = lineID
end
-- Many spells are irrelevant system spells, don't transmit these Artemis:Transmit("spellCast", table, "ALERT")
if unitID == "player" and not (IsPlayerSpell(spellID)) then end
return
end -- Detect instant spell casts
function Artemis:UNIT_SPELLCAST_SUCCEEDED (...)
local name, subText, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible = UnitCastingInfo(unitID) local _, unitID, spell, rank, lineID, spellID = ...
-- Don't trigger on the success of a non instant cast if not (unitID == "player") and not (unitID == "target") then
if not (lastLine[unitID] == nil) and lastLine[unitID] == lineID then return
return end
end if channeling[unitID] == true then
return
-- Set back the last line to what is currently being cast (Fireblast during Fireball per example) end
if not (name == nil) then -- Many spells are irrelevant system spells, don't transmit these
lastLine[unitID] = castID if unitID == "player" and not (IsPlayerSpell(spellID)) then
else return
lastLine[unitID] = nil end
end
local name, subText, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible = UnitCastingInfo(unitID)
local table = {uid = unitID, n = spell, sid = spellID} -- Don't trigger on the success of a non instant cast
if not (lastLine[unitID] == nil) and lastLine[unitID] == lineID then
Artemis:Transmit("instantSpellCast", table, "ALERT") return
end end
-- Detect falure of non instant casts -- Set back the last line to what is currently being cast (Fireblast during Fireball per example)
function Artemis:UNIT_SPELLCAST_FAILED (...) if not (name == nil) then
local source, unitID, _, _, lineID = ... lastLine[unitID] = castID
if not (unitID == "player") and not (unitID == "target") then else
return lastLine[unitID] = nil
end end
if lastLine[unitID] == nil or not (lastLine[unitID] == lineID) then
return local table = {uid = unitID, n = spell, sid = spellID}
end
Artemis:Transmit("instantSpellCast", table, "ALERT")
lastLine[unitID] = nil end
Artemis:Transmit("spellCastFailed", unitID, "ALERT") -- Detect falure of non instant casts
end function Artemis:UNIT_SPELLCAST_FAILED (...)
local source, unitID, _, _, lineID = ...
-- Detect cancellation of non instant casts if not (unitID == "player") and not (unitID == "target") then
function Artemis:UNIT_SPELLCAST_INTERRUPTED (...) return
local source, unitID, _, _, lineID = ... end
if not (unitID == "player") and not (unitID == "target") then if lastLine[unitID] == nil or not (lastLine[unitID] == lineID) then
return return
end end
if lastLine[unitID] == nil or not (lastLine[unitID] == lineID) then
return lastLine[unitID] = nil
end
Artemis:Transmit("spellCastFailed", unitID, "ALERT")
lastLine[unitID] = nil end
Artemis:Transmit("spellCastInterrupted", unitID, "ALERT") -- Detect falure of non instant casts
end function Artemis:UNIT_SPELLCAST_DELAYED (...)
local _, unitID, spell, rank, lineID, spellID = ...
function Artemis:ZONE_CHANGED_NEW_AREA (...) if not (unitID == "player") and not (unitID == "target") then
local pvpType, isSubZonePVP, factionName = GetZonePVPInfo() return
end
Artemis:Transmit("zone", {z = GetRealZoneText(), s = GetSubZoneText(), t = pvpType, p = isSubZonePVP, f = factionName}) local name, _, _, _, startTime, endTime, _, _, notInterruptible = UnitCastingInfo(unitID)
end local table = {uid = unitID, n = name, sid = spellID, s = startTime, e = endTime, ni = notInterruptible}
function Artemis:ZONE_CHANGED (...)
local pvpType, isSubZonePVP, factionName = GetZonePVPInfo() Artemis:Transmit("spellCast", table, "ALERT")
end
Artemis:Transmit("zone", {z = GetRealZoneText(), s = GetSubZoneText(), t = pvpType, p = isSubZonePVP, f = factionName})
-- Detect cancellation of non instant casts
function Artemis:UNIT_SPELLCAST_INTERRUPTED (...)
local source, unitID, _, _, lineID = ...
if not (unitID == "player") and not (unitID == "target") then
return
end
if lastLine[unitID] == nil or not (lastLine[unitID] == lineID) then
return
end
lastLine[unitID] = nil
Artemis:Transmit("spellCastInterrupted", unitID, "ALERT")
end
-- Detect spell channels
function Artemis:UNIT_SPELLCAST_CHANNEL_START(...)
local _, unitID, spell, rank, lineID, spellID = ...
if not (unitID == "player") and not (unitID == "target") then
return
end
channeling[unitID] = true
local name, _, _, _, startTime, endTime, _, notInterruptible = UnitChannelInfo(unitID)
local table = {uid = unitID, n = name, sid = spellID, s = startTime, e = endTime, ni = notInterruptible}
Artemis:Transmit("spellChannel", table, "ALERT")
end
function Artemis:UNIT_SPELLCAST_CHANNEL_UPDATE (...)
local _, unitID, spell, rank, lineID, spellID = ...
if not (unitID == "player") and not (unitID == "target") then
return
end
local name, _, _, _, startTime, endTime, _, notInterruptible = UnitChannelInfo(unitID)
local table = {uid = unitID, n = name, sid = spellID, s = startTime, e = endTime, ni = notInterruptible}
Artemis:Transmit("spellChannel", table, "ALERT")
end
-- Detect cancellation of channels
function Artemis:UNIT_SPELLCAST_CHANNEL_STOP (...)
local source, unitID, _, _, lineID = ...
if not (unitID == "player") and not (unitID == "target") then
return
end
channeling[unitID] = false
Artemis:Transmit("spellChannelInterrupted", unitID, "ALERT")
end
function Artemis:ZONE_CHANGED_NEW_AREA (...)
local pvpType, isSubZonePVP, factionName = GetZonePVPInfo()
Artemis:Transmit("zone", {z = GetRealZoneText(), s = GetSubZoneText(), t = pvpType, p = isSubZonePVP, f = factionName})
end
function Artemis:ZONE_CHANGED (...)
local pvpType, isSubZonePVP, factionName = GetZonePVPInfo()
Artemis:Transmit("zone", {z = GetRealZoneText(), s = GetSubZoneText(), t = pvpType, p = isSubZonePVP, f = factionName})
end end

View File

@ -1,4 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ <Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd"> ..\FrameXML\UI.xsd">
<Script file="AceAddon-3.0.lua"/> <Script file="AceAddon-3.0.lua"/>
</Ui> </Ui>

View File

@ -1,301 +1,301 @@
--- **AceComm-3.0** allows you to send messages of unlimited length over the addon comm channels. --- **AceComm-3.0** allows you to send messages of unlimited length over the addon comm channels.
-- It'll automatically split the messages into multiple parts and rebuild them on the receiving end.\\ -- It'll automatically split the messages into multiple parts and rebuild them on the receiving end.\\
-- **ChatThrottleLib** is of course being used to avoid being disconnected by the server. -- **ChatThrottleLib** is of course being used to avoid being disconnected by the server.
-- --
-- **AceComm-3.0** can be embeded into your addon, either explicitly by calling AceComm:Embed(MyAddon) or by -- **AceComm-3.0** can be embeded into your addon, either explicitly by calling AceComm:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceComm itself.\\ -- and can be accessed directly, without having to explicitly call AceComm itself.\\
-- It is recommended to embed AceComm, otherwise you'll have to specify a custom `self` on all calls you -- It is recommended to embed AceComm, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceComm. -- make into AceComm.
-- @class file -- @class file
-- @name AceComm-3.0 -- @name AceComm-3.0
-- @release $Id: AceComm-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $ -- @release $Id: AceComm-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $
--[[ AceComm-3.0 --[[ AceComm-3.0
TODO: Time out old data rotting around from dead senders? Not a HUGE deal since the number of possible sender names is somewhat limited. TODO: Time out old data rotting around from dead senders? Not a HUGE deal since the number of possible sender names is somewhat limited.
]] ]]
local CallbackHandler = LibStub("CallbackHandler-1.0") local CallbackHandler = LibStub("CallbackHandler-1.0")
local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib") local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib")
local MAJOR, MINOR = "AceComm-3.0", 10 local MAJOR, MINOR = "AceComm-3.0", 10
local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR) local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceComm then return end if not AceComm then return end
-- Lua APIs -- Lua APIs
local type, next, pairs, tostring = type, next, pairs, tostring local type, next, pairs, tostring = type, next, pairs, tostring
local strsub, strfind = string.sub, string.find local strsub, strfind = string.sub, string.find
local match = string.match local match = string.match
local tinsert, tconcat = table.insert, table.concat local tinsert, tconcat = table.insert, table.concat
local error, assert = error, assert local error, assert = error, assert
-- WoW APIs -- WoW APIs
local Ambiguate = Ambiguate local Ambiguate = Ambiguate
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
-- List them here for Mikk's FindGlobals script -- List them here for Mikk's FindGlobals script
-- GLOBALS: LibStub, DEFAULT_CHAT_FRAME, geterrorhandler, RegisterAddonMessagePrefix -- GLOBALS: LibStub, DEFAULT_CHAT_FRAME, geterrorhandler, RegisterAddonMessagePrefix
AceComm.embeds = AceComm.embeds or {} AceComm.embeds = AceComm.embeds or {}
-- for my sanity and yours, let's give the message type bytes some names -- for my sanity and yours, let's give the message type bytes some names
local MSG_MULTI_FIRST = "\001" local MSG_MULTI_FIRST = "\001"
local MSG_MULTI_NEXT = "\002" local MSG_MULTI_NEXT = "\002"
local MSG_MULTI_LAST = "\003" local MSG_MULTI_LAST = "\003"
local MSG_ESCAPE = "\004" local MSG_ESCAPE = "\004"
-- remove old structures (pre WoW 4.0) -- remove old structures (pre WoW 4.0)
AceComm.multipart_origprefixes = nil AceComm.multipart_origprefixes = nil
AceComm.multipart_reassemblers = nil AceComm.multipart_reassemblers = nil
-- the multipart message spool: indexed by a combination of sender+distribution+ -- the multipart message spool: indexed by a combination of sender+distribution+
AceComm.multipart_spool = AceComm.multipart_spool or {} AceComm.multipart_spool = AceComm.multipart_spool or {}
--- Register for Addon Traffic on a specified prefix --- Register for Addon Traffic on a specified prefix
-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent), max 16 characters -- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent), max 16 characters
-- @param method Callback to call on message reception: Function reference, or method name (string) to call on self. Defaults to "OnCommReceived" -- @param method Callback to call on message reception: Function reference, or method name (string) to call on self. Defaults to "OnCommReceived"
function AceComm:RegisterComm(prefix, method) function AceComm:RegisterComm(prefix, method)
if method == nil then if method == nil then
method = "OnCommReceived" method = "OnCommReceived"
end end
if #prefix > 16 then -- TODO: 15? if #prefix > 16 then -- TODO: 15?
error("AceComm:RegisterComm(prefix,method): prefix length is limited to 16 characters") error("AceComm:RegisterComm(prefix,method): prefix length is limited to 16 characters")
end end
RegisterAddonMessagePrefix(prefix) RegisterAddonMessagePrefix(prefix)
return AceComm._RegisterComm(self, prefix, method) -- created by CallbackHandler return AceComm._RegisterComm(self, prefix, method) -- created by CallbackHandler
end end
local warnedPrefix=false local warnedPrefix=false
--- Send a message over the Addon Channel --- Send a message over the Addon Channel
-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent) -- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent)
-- @param text Data to send, nils (\000) not allowed. Any length. -- @param text Data to send, nils (\000) not allowed. Any length.
-- @param distribution Addon channel, e.g. "RAID", "GUILD", etc; see SendAddonMessage API -- @param distribution Addon channel, e.g. "RAID", "GUILD", etc; see SendAddonMessage API
-- @param target Destination for some distributions; see SendAddonMessage API -- @param target Destination for some distributions; see SendAddonMessage API
-- @param prio OPTIONAL: ChatThrottleLib priority, "BULK", "NORMAL" or "ALERT". Defaults to "NORMAL". -- @param prio OPTIONAL: ChatThrottleLib priority, "BULK", "NORMAL" or "ALERT". Defaults to "NORMAL".
-- @param callbackFn OPTIONAL: callback function to be called as each chunk is sent. receives 3 args: the user supplied arg (see next), the number of bytes sent so far, and the number of bytes total to send. -- @param callbackFn OPTIONAL: callback function to be called as each chunk is sent. receives 3 args: the user supplied arg (see next), the number of bytes sent so far, and the number of bytes total to send.
-- @param callbackArg: OPTIONAL: first arg to the callback function. nil will be passed if not specified. -- @param callbackArg: OPTIONAL: first arg to the callback function. nil will be passed if not specified.
function AceComm:SendCommMessage(prefix, text, distribution, target, prio, callbackFn, callbackArg) function AceComm:SendCommMessage(prefix, text, distribution, target, prio, callbackFn, callbackArg)
prio = prio or "NORMAL" -- pasta's reference implementation had different prio for singlepart and multipart, but that's a very bad idea since that can easily lead to out-of-sequence delivery! prio = prio or "NORMAL" -- pasta's reference implementation had different prio for singlepart and multipart, but that's a very bad idea since that can easily lead to out-of-sequence delivery!
if not( type(prefix)=="string" and if not( type(prefix)=="string" and
type(text)=="string" and type(text)=="string" and
type(distribution)=="string" and type(distribution)=="string" and
(target==nil or type(target)=="string") and (target==nil or type(target)=="string") and
(prio=="BULK" or prio=="NORMAL" or prio=="ALERT") (prio=="BULK" or prio=="NORMAL" or prio=="ALERT")
) then ) then
error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2) error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2)
end end
local textlen = #text local textlen = #text
local maxtextlen = 255 -- Yes, the max is 255 even if the dev post said 256. I tested. Char 256+ get silently truncated. /Mikk, 20110327 local maxtextlen = 255 -- Yes, the max is 255 even if the dev post said 256. I tested. Char 256+ get silently truncated. /Mikk, 20110327
local queueName = prefix..distribution..(target or "") local queueName = prefix..distribution..(target or "")
local ctlCallback = nil local ctlCallback = nil
if callbackFn then if callbackFn then
ctlCallback = function(sent) ctlCallback = function(sent)
return callbackFn(callbackArg, sent, textlen) return callbackFn(callbackArg, sent, textlen)
end end
end end
local forceMultipart local forceMultipart
if match(text, "^[\001-\009]") then -- 4.1+: see if the first character is a control character if match(text, "^[\001-\009]") then -- 4.1+: see if the first character is a control character
-- we need to escape the first character with a \004 -- we need to escape the first character with a \004
if textlen+1 > maxtextlen then -- would we go over the size limit? if textlen+1 > maxtextlen then -- would we go over the size limit?
forceMultipart = true -- just make it multipart, no escape problems then forceMultipart = true -- just make it multipart, no escape problems then
else else
text = "\004" .. text text = "\004" .. text
end end
end end
if not forceMultipart and textlen <= maxtextlen then if not forceMultipart and textlen <= maxtextlen then
-- fits all in one message -- fits all in one message
CTL:SendAddonMessage(prio, prefix, text, distribution, target, queueName, ctlCallback, textlen) CTL:SendAddonMessage(prio, prefix, text, distribution, target, queueName, ctlCallback, textlen)
else else
maxtextlen = maxtextlen - 1 -- 1 extra byte for part indicator in prefix(4.0)/start of message(4.1) maxtextlen = maxtextlen - 1 -- 1 extra byte for part indicator in prefix(4.0)/start of message(4.1)
-- first part -- first part
local chunk = strsub(text, 1, maxtextlen) local chunk = strsub(text, 1, maxtextlen)
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_FIRST..chunk, distribution, target, queueName, ctlCallback, maxtextlen) CTL:SendAddonMessage(prio, prefix, MSG_MULTI_FIRST..chunk, distribution, target, queueName, ctlCallback, maxtextlen)
-- continuation -- continuation
local pos = 1+maxtextlen local pos = 1+maxtextlen
while pos+maxtextlen <= textlen do while pos+maxtextlen <= textlen do
chunk = strsub(text, pos, pos+maxtextlen-1) chunk = strsub(text, pos, pos+maxtextlen-1)
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_NEXT..chunk, distribution, target, queueName, ctlCallback, pos+maxtextlen-1) CTL:SendAddonMessage(prio, prefix, MSG_MULTI_NEXT..chunk, distribution, target, queueName, ctlCallback, pos+maxtextlen-1)
pos = pos + maxtextlen pos = pos + maxtextlen
end end
-- final part -- final part
chunk = strsub(text, pos) chunk = strsub(text, pos)
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_LAST..chunk, distribution, target, queueName, ctlCallback, textlen) CTL:SendAddonMessage(prio, prefix, MSG_MULTI_LAST..chunk, distribution, target, queueName, ctlCallback, textlen)
end end
end end
---------------------------------------- ----------------------------------------
-- Message receiving -- Message receiving
---------------------------------------- ----------------------------------------
do do
local compost = setmetatable({}, {__mode = "k"}) local compost = setmetatable({}, {__mode = "k"})
local function new() local function new()
local t = next(compost) local t = next(compost)
if t then if t then
compost[t]=nil compost[t]=nil
for i=#t,3,-1 do -- faster than pairs loop. don't even nil out 1/2 since they'll be overwritten for i=#t,3,-1 do -- faster than pairs loop. don't even nil out 1/2 since they'll be overwritten
t[i]=nil t[i]=nil
end end
return t return t
end end
return {} return {}
end end
local function lostdatawarning(prefix,sender,where) local function lostdatawarning(prefix,sender,where)
DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: lost network data regarding '"..tostring(prefix).."' from '"..tostring(sender).."' (in "..where..")") DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: lost network data regarding '"..tostring(prefix).."' from '"..tostring(sender).."' (in "..where..")")
end end
function AceComm:OnReceiveMultipartFirst(prefix, message, distribution, sender) function AceComm:OnReceiveMultipartFirst(prefix, message, distribution, sender)
local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
local spool = AceComm.multipart_spool local spool = AceComm.multipart_spool
--[[ --[[
if spool[key] then if spool[key] then
lostdatawarning(prefix,sender,"First") lostdatawarning(prefix,sender,"First")
-- continue and overwrite -- continue and overwrite
end end
--]] --]]
spool[key] = message -- plain string for now spool[key] = message -- plain string for now
end end
function AceComm:OnReceiveMultipartNext(prefix, message, distribution, sender) function AceComm:OnReceiveMultipartNext(prefix, message, distribution, sender)
local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
local spool = AceComm.multipart_spool local spool = AceComm.multipart_spool
local olddata = spool[key] local olddata = spool[key]
if not olddata then if not olddata then
--lostdatawarning(prefix,sender,"Next") --lostdatawarning(prefix,sender,"Next")
return return
end end
if type(olddata)~="table" then if type(olddata)~="table" then
-- ... but what we have is not a table. So make it one. (Pull a composted one if available) -- ... but what we have is not a table. So make it one. (Pull a composted one if available)
local t = new() local t = new()
t[1] = olddata -- add old data as first string t[1] = olddata -- add old data as first string
t[2] = message -- and new message as second string t[2] = message -- and new message as second string
spool[key] = t -- and put the table in the spool instead of the old string spool[key] = t -- and put the table in the spool instead of the old string
else else
tinsert(olddata, message) tinsert(olddata, message)
end end
end end
function AceComm:OnReceiveMultipartLast(prefix, message, distribution, sender) function AceComm:OnReceiveMultipartLast(prefix, message, distribution, sender)
local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
local spool = AceComm.multipart_spool local spool = AceComm.multipart_spool
local olddata = spool[key] local olddata = spool[key]
if not olddata then if not olddata then
--lostdatawarning(prefix,sender,"End") --lostdatawarning(prefix,sender,"End")
return return
end end
spool[key] = nil spool[key] = nil
if type(olddata) == "table" then if type(olddata) == "table" then
-- if we've received a "next", the spooled data will be a table for rapid & garbage-free tconcat -- if we've received a "next", the spooled data will be a table for rapid & garbage-free tconcat
tinsert(olddata, message) tinsert(olddata, message)
AceComm.callbacks:Fire(prefix, tconcat(olddata, ""), distribution, sender) AceComm.callbacks:Fire(prefix, tconcat(olddata, ""), distribution, sender)
compost[olddata] = true compost[olddata] = true
else else
-- if we've only received a "first", the spooled data will still only be a string -- if we've only received a "first", the spooled data will still only be a string
AceComm.callbacks:Fire(prefix, olddata..message, distribution, sender) AceComm.callbacks:Fire(prefix, olddata..message, distribution, sender)
end end
end end
end end
---------------------------------------- ----------------------------------------
-- Embed CallbackHandler -- Embed CallbackHandler
---------------------------------------- ----------------------------------------
if not AceComm.callbacks then if not AceComm.callbacks then
AceComm.callbacks = CallbackHandler:New(AceComm, AceComm.callbacks = CallbackHandler:New(AceComm,
"_RegisterComm", "_RegisterComm",
"UnregisterComm", "UnregisterComm",
"UnregisterAllComm") "UnregisterAllComm")
end end
AceComm.callbacks.OnUsed = nil AceComm.callbacks.OnUsed = nil
AceComm.callbacks.OnUnused = nil AceComm.callbacks.OnUnused = nil
local function OnEvent(self, event, prefix, message, distribution, sender) local function OnEvent(self, event, prefix, message, distribution, sender)
if event == "CHAT_MSG_ADDON" then if event == "CHAT_MSG_ADDON" then
sender = Ambiguate(sender, "none") sender = Ambiguate(sender, "none")
local control, rest = match(message, "^([\001-\009])(.*)") local control, rest = match(message, "^([\001-\009])(.*)")
if control then if control then
if control==MSG_MULTI_FIRST then if control==MSG_MULTI_FIRST then
AceComm:OnReceiveMultipartFirst(prefix, rest, distribution, sender) AceComm:OnReceiveMultipartFirst(prefix, rest, distribution, sender)
elseif control==MSG_MULTI_NEXT then elseif control==MSG_MULTI_NEXT then
AceComm:OnReceiveMultipartNext(prefix, rest, distribution, sender) AceComm:OnReceiveMultipartNext(prefix, rest, distribution, sender)
elseif control==MSG_MULTI_LAST then elseif control==MSG_MULTI_LAST then
AceComm:OnReceiveMultipartLast(prefix, rest, distribution, sender) AceComm:OnReceiveMultipartLast(prefix, rest, distribution, sender)
elseif control==MSG_ESCAPE then elseif control==MSG_ESCAPE then
AceComm.callbacks:Fire(prefix, rest, distribution, sender) AceComm.callbacks:Fire(prefix, rest, distribution, sender)
else else
-- unknown control character, ignore SILENTLY (dont warn unnecessarily about future extensions!) -- unknown control character, ignore SILENTLY (dont warn unnecessarily about future extensions!)
end end
else else
-- single part: fire it off immediately and let CallbackHandler decide if it's registered or not -- single part: fire it off immediately and let CallbackHandler decide if it's registered or not
AceComm.callbacks:Fire(prefix, message, distribution, sender) AceComm.callbacks:Fire(prefix, message, distribution, sender)
end end
else else
assert(false, "Received "..tostring(event).." event?!") assert(false, "Received "..tostring(event).." event?!")
end end
end end
AceComm.frame = AceComm.frame or CreateFrame("Frame", "AceComm30Frame") AceComm.frame = AceComm.frame or CreateFrame("Frame", "AceComm30Frame")
AceComm.frame:SetScript("OnEvent", OnEvent) AceComm.frame:SetScript("OnEvent", OnEvent)
AceComm.frame:UnregisterAllEvents() AceComm.frame:UnregisterAllEvents()
AceComm.frame:RegisterEvent("CHAT_MSG_ADDON") AceComm.frame:RegisterEvent("CHAT_MSG_ADDON")
---------------------------------------- ----------------------------------------
-- Base library stuff -- Base library stuff
---------------------------------------- ----------------------------------------
local mixins = { local mixins = {
"RegisterComm", "RegisterComm",
"UnregisterComm", "UnregisterComm",
"UnregisterAllComm", "UnregisterAllComm",
"SendCommMessage", "SendCommMessage",
} }
-- Embeds AceComm-3.0 into the target object making the functions from the mixins list available on target:.. -- Embeds AceComm-3.0 into the target object making the functions from the mixins list available on target:..
-- @param target target object to embed AceComm-3.0 in -- @param target target object to embed AceComm-3.0 in
function AceComm:Embed(target) function AceComm:Embed(target)
for k, v in pairs(mixins) do for k, v in pairs(mixins) do
target[v] = self[v] target[v] = self[v]
end end
self.embeds[target] = true self.embeds[target] = true
return target return target
end end
function AceComm:OnEmbedDisable(target) function AceComm:OnEmbedDisable(target)
target:UnregisterAllComm() target:UnregisterAllComm()
end end
-- Update embeds -- Update embeds
for target, v in pairs(AceComm.embeds) do for target, v in pairs(AceComm.embeds) do
AceComm:Embed(target) AceComm:Embed(target)
end end

View File

@ -1,5 +1,5 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ <Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd"> ..\FrameXML\UI.xsd">
<Script file="ChatThrottleLib.lua"/> <Script file="ChatThrottleLib.lua"/>
<Script file="AceComm-3.0.lua"/> <Script file="AceComm-3.0.lua"/>
</Ui> </Ui>

View File

@ -1,250 +1,250 @@
--- **AceConsole-3.0** provides registration facilities for slash commands. --- **AceConsole-3.0** provides registration facilities for slash commands.
-- You can register slash commands to your custom functions and use the `GetArgs` function to parse them -- You can register slash commands to your custom functions and use the `GetArgs` function to parse them
-- to your addons individual needs. -- to your addons individual needs.
-- --
-- **AceConsole-3.0** can be embeded into your addon, either explicitly by calling AceConsole:Embed(MyAddon) or by -- **AceConsole-3.0** can be embeded into your addon, either explicitly by calling AceConsole:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceConsole itself.\\ -- and can be accessed directly, without having to explicitly call AceConsole itself.\\
-- It is recommended to embed AceConsole, otherwise you'll have to specify a custom `self` on all calls you -- It is recommended to embed AceConsole, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceConsole. -- make into AceConsole.
-- @class file -- @class file
-- @name AceConsole-3.0 -- @name AceConsole-3.0
-- @release $Id: AceConsole-3.0.lua 1143 2016-07-11 08:52:03Z nevcairiel $ -- @release $Id: AceConsole-3.0.lua 1143 2016-07-11 08:52:03Z nevcairiel $
local MAJOR,MINOR = "AceConsole-3.0", 7 local MAJOR,MINOR = "AceConsole-3.0", 7
local AceConsole, oldminor = LibStub:NewLibrary(MAJOR, MINOR) local AceConsole, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceConsole then return end -- No upgrade needed if not AceConsole then return end -- No upgrade needed
AceConsole.embeds = AceConsole.embeds or {} -- table containing objects AceConsole is embedded in. AceConsole.embeds = AceConsole.embeds or {} -- table containing objects AceConsole is embedded in.
AceConsole.commands = AceConsole.commands or {} -- table containing commands registered AceConsole.commands = AceConsole.commands or {} -- table containing commands registered
AceConsole.weakcommands = AceConsole.weakcommands or {} -- table containing self, command => func references for weak commands that don't persist through enable/disable AceConsole.weakcommands = AceConsole.weakcommands or {} -- table containing self, command => func references for weak commands that don't persist through enable/disable
-- Lua APIs -- Lua APIs
local tconcat, tostring, select = table.concat, tostring, select local tconcat, tostring, select = table.concat, tostring, select
local type, pairs, error = type, pairs, error local type, pairs, error = type, pairs, error
local format, strfind, strsub = string.format, string.find, string.sub local format, strfind, strsub = string.format, string.find, string.sub
local max = math.max local max = math.max
-- WoW APIs -- WoW APIs
local _G = _G local _G = _G
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
-- List them here for Mikk's FindGlobals script -- List them here for Mikk's FindGlobals script
-- GLOBALS: DEFAULT_CHAT_FRAME, SlashCmdList, hash_SlashCmdList -- GLOBALS: DEFAULT_CHAT_FRAME, SlashCmdList, hash_SlashCmdList
local tmp={} local tmp={}
local function Print(self,frame,...) local function Print(self,frame,...)
local n=0 local n=0
if self ~= AceConsole then if self ~= AceConsole then
n=n+1 n=n+1
tmp[n] = "|cff33ff99"..tostring( self ).."|r:" tmp[n] = "|cff33ff99"..tostring( self ).."|r:"
end end
for i=1, select("#", ...) do for i=1, select("#", ...) do
n=n+1 n=n+1
tmp[n] = tostring(select(i, ...)) tmp[n] = tostring(select(i, ...))
end end
frame:AddMessage( tconcat(tmp," ",1,n) ) frame:AddMessage( tconcat(tmp," ",1,n) )
end end
--- Print to DEFAULT_CHAT_FRAME or given ChatFrame (anything with an .AddMessage function) --- Print to DEFAULT_CHAT_FRAME or given ChatFrame (anything with an .AddMessage function)
-- @paramsig [chatframe ,] ... -- @paramsig [chatframe ,] ...
-- @param chatframe Custom ChatFrame to print to (or any frame with an .AddMessage function) -- @param chatframe Custom ChatFrame to print to (or any frame with an .AddMessage function)
-- @param ... List of any values to be printed -- @param ... List of any values to be printed
function AceConsole:Print(...) function AceConsole:Print(...)
local frame = ... local frame = ...
if type(frame) == "table" and frame.AddMessage then -- Is first argument something with an .AddMessage member? if type(frame) == "table" and frame.AddMessage then -- Is first argument something with an .AddMessage member?
return Print(self, frame, select(2,...)) return Print(self, frame, select(2,...))
else else
return Print(self, DEFAULT_CHAT_FRAME, ...) return Print(self, DEFAULT_CHAT_FRAME, ...)
end end
end end
--- Formatted (using format()) print to DEFAULT_CHAT_FRAME or given ChatFrame (anything with an .AddMessage function) --- Formatted (using format()) print to DEFAULT_CHAT_FRAME or given ChatFrame (anything with an .AddMessage function)
-- @paramsig [chatframe ,] "format"[, ...] -- @paramsig [chatframe ,] "format"[, ...]
-- @param chatframe Custom ChatFrame to print to (or any frame with an .AddMessage function) -- @param chatframe Custom ChatFrame to print to (or any frame with an .AddMessage function)
-- @param format Format string - same syntax as standard Lua format() -- @param format Format string - same syntax as standard Lua format()
-- @param ... Arguments to the format string -- @param ... Arguments to the format string
function AceConsole:Printf(...) function AceConsole:Printf(...)
local frame = ... local frame = ...
if type(frame) == "table" and frame.AddMessage then -- Is first argument something with an .AddMessage member? if type(frame) == "table" and frame.AddMessage then -- Is first argument something with an .AddMessage member?
return Print(self, frame, format(select(2,...))) return Print(self, frame, format(select(2,...)))
else else
return Print(self, DEFAULT_CHAT_FRAME, format(...)) return Print(self, DEFAULT_CHAT_FRAME, format(...))
end end
end end
--- Register a simple chat command --- Register a simple chat command
-- @param command Chat command to be registered WITHOUT leading "/" -- @param command Chat command to be registered WITHOUT leading "/"
-- @param func Function to call when the slash command is being used (funcref or methodname) -- @param func Function to call when the slash command is being used (funcref or methodname)
-- @param persist if false, the command will be soft disabled/enabled when aceconsole is used as a mixin (default: true) -- @param persist if false, the command will be soft disabled/enabled when aceconsole is used as a mixin (default: true)
function AceConsole:RegisterChatCommand( command, func, persist ) function AceConsole:RegisterChatCommand( command, func, persist )
if type(command)~="string" then error([[Usage: AceConsole:RegisterChatCommand( "command", func[, persist ]): 'command' - expected a string]], 2) end if type(command)~="string" then error([[Usage: AceConsole:RegisterChatCommand( "command", func[, persist ]): 'command' - expected a string]], 2) end
if persist==nil then persist=true end -- I'd rather have my addon's "/addon enable" around if the author screws up. Having some extra slash regged when it shouldnt be isn't as destructive. True is a better default. /Mikk if persist==nil then persist=true end -- I'd rather have my addon's "/addon enable" around if the author screws up. Having some extra slash regged when it shouldnt be isn't as destructive. True is a better default. /Mikk
local name = "ACECONSOLE_"..command:upper() local name = "ACECONSOLE_"..command:upper()
if type( func ) == "string" then if type( func ) == "string" then
SlashCmdList[name] = function(input, editBox) SlashCmdList[name] = function(input, editBox)
self[func](self, input, editBox) self[func](self, input, editBox)
end end
else else
SlashCmdList[name] = func SlashCmdList[name] = func
end end
_G["SLASH_"..name.."1"] = "/"..command:lower() _G["SLASH_"..name.."1"] = "/"..command:lower()
AceConsole.commands[command] = name AceConsole.commands[command] = name
-- non-persisting commands are registered for enabling disabling -- non-persisting commands are registered for enabling disabling
if not persist then if not persist then
if not AceConsole.weakcommands[self] then AceConsole.weakcommands[self] = {} end if not AceConsole.weakcommands[self] then AceConsole.weakcommands[self] = {} end
AceConsole.weakcommands[self][command] = func AceConsole.weakcommands[self][command] = func
end end
return true return true
end end
--- Unregister a chatcommand --- Unregister a chatcommand
-- @param command Chat command to be unregistered WITHOUT leading "/" -- @param command Chat command to be unregistered WITHOUT leading "/"
function AceConsole:UnregisterChatCommand( command ) function AceConsole:UnregisterChatCommand( command )
local name = AceConsole.commands[command] local name = AceConsole.commands[command]
if name then if name then
SlashCmdList[name] = nil SlashCmdList[name] = nil
_G["SLASH_" .. name .. "1"] = nil _G["SLASH_" .. name .. "1"] = nil
hash_SlashCmdList["/" .. command:upper()] = nil hash_SlashCmdList["/" .. command:upper()] = nil
AceConsole.commands[command] = nil AceConsole.commands[command] = nil
end end
end end
--- Get an iterator over all Chat Commands registered with AceConsole --- Get an iterator over all Chat Commands registered with AceConsole
-- @return Iterator (pairs) over all commands -- @return Iterator (pairs) over all commands
function AceConsole:IterateChatCommands() return pairs(AceConsole.commands) end function AceConsole:IterateChatCommands() return pairs(AceConsole.commands) end
local function nils(n, ...) local function nils(n, ...)
if n>1 then if n>1 then
return nil, nils(n-1, ...) return nil, nils(n-1, ...)
elseif n==1 then elseif n==1 then
return nil, ... return nil, ...
else else
return ... return ...
end end
end end
--- Retreive one or more space-separated arguments from a string. --- Retreive one or more space-separated arguments from a string.
-- Treats quoted strings and itemlinks as non-spaced. -- Treats quoted strings and itemlinks as non-spaced.
-- @param str The raw argument string -- @param str The raw argument string
-- @param numargs How many arguments to get (default 1) -- @param numargs How many arguments to get (default 1)
-- @param startpos Where in the string to start scanning (default 1) -- @param startpos Where in the string to start scanning (default 1)
-- @return Returns arg1, arg2, ..., nextposition\\ -- @return Returns arg1, arg2, ..., nextposition\\
-- Missing arguments will be returned as nils. 'nextposition' is returned as 1e9 at the end of the string. -- Missing arguments will be returned as nils. 'nextposition' is returned as 1e9 at the end of the string.
function AceConsole:GetArgs(str, numargs, startpos) function AceConsole:GetArgs(str, numargs, startpos)
numargs = numargs or 1 numargs = numargs or 1
startpos = max(startpos or 1, 1) startpos = max(startpos or 1, 1)
local pos=startpos local pos=startpos
-- find start of new arg -- find start of new arg
pos = strfind(str, "[^ ]", pos) pos = strfind(str, "[^ ]", pos)
if not pos then -- whoops, end of string if not pos then -- whoops, end of string
return nils(numargs, 1e9) return nils(numargs, 1e9)
end end
if numargs<1 then if numargs<1 then
return pos return pos
end end
-- quoted or space separated? find out which pattern to use -- quoted or space separated? find out which pattern to use
local delim_or_pipe local delim_or_pipe
local ch = strsub(str, pos, pos) local ch = strsub(str, pos, pos)
if ch=='"' then if ch=='"' then
pos = pos + 1 pos = pos + 1
delim_or_pipe='([|"])' delim_or_pipe='([|"])'
elseif ch=="'" then elseif ch=="'" then
pos = pos + 1 pos = pos + 1
delim_or_pipe="([|'])" delim_or_pipe="([|'])"
else else
delim_or_pipe="([| ])" delim_or_pipe="([| ])"
end end
startpos = pos startpos = pos
while true do while true do
-- find delimiter or hyperlink -- find delimiter or hyperlink
local ch,_ local ch,_
pos,_,ch = strfind(str, delim_or_pipe, pos) pos,_,ch = strfind(str, delim_or_pipe, pos)
if not pos then break end if not pos then break end
if ch=="|" then if ch=="|" then
-- some kind of escape -- some kind of escape
if strsub(str,pos,pos+1)=="|H" then if strsub(str,pos,pos+1)=="|H" then
-- It's a |H....|hhyper link!|h -- It's a |H....|hhyper link!|h
pos=strfind(str, "|h", pos+2) -- first |h pos=strfind(str, "|h", pos+2) -- first |h
if not pos then break end if not pos then break end
pos=strfind(str, "|h", pos+2) -- second |h pos=strfind(str, "|h", pos+2) -- second |h
if not pos then break end if not pos then break end
elseif strsub(str,pos, pos+1) == "|T" then elseif strsub(str,pos, pos+1) == "|T" then
-- It's a |T....|t texture -- It's a |T....|t texture
pos=strfind(str, "|t", pos+2) pos=strfind(str, "|t", pos+2)
if not pos then break end if not pos then break end
end end
pos=pos+2 -- skip past this escape (last |h if it was a hyperlink) pos=pos+2 -- skip past this escape (last |h if it was a hyperlink)
else else
-- found delimiter, done with this arg -- found delimiter, done with this arg
return strsub(str, startpos, pos-1), AceConsole:GetArgs(str, numargs-1, pos+1) return strsub(str, startpos, pos-1), AceConsole:GetArgs(str, numargs-1, pos+1)
end end
end end
-- search aborted, we hit end of string. return it all as one argument. (yes, even if it's an unterminated quote or hyperlink) -- search aborted, we hit end of string. return it all as one argument. (yes, even if it's an unterminated quote or hyperlink)
return strsub(str, startpos), nils(numargs-1, 1e9) return strsub(str, startpos), nils(numargs-1, 1e9)
end end
--- embedding and embed handling --- embedding and embed handling
local mixins = { local mixins = {
"Print", "Print",
"Printf", "Printf",
"RegisterChatCommand", "RegisterChatCommand",
"UnregisterChatCommand", "UnregisterChatCommand",
"GetArgs", "GetArgs",
} }
-- Embeds AceConsole into the target object making the functions from the mixins list available on target:.. -- Embeds AceConsole into the target object making the functions from the mixins list available on target:..
-- @param target target object to embed AceBucket in -- @param target target object to embed AceBucket in
function AceConsole:Embed( target ) function AceConsole:Embed( target )
for k, v in pairs( mixins ) do for k, v in pairs( mixins ) do
target[v] = self[v] target[v] = self[v]
end end
self.embeds[target] = true self.embeds[target] = true
return target return target
end end
function AceConsole:OnEmbedEnable( target ) function AceConsole:OnEmbedEnable( target )
if AceConsole.weakcommands[target] then if AceConsole.weakcommands[target] then
for command, func in pairs( AceConsole.weakcommands[target] ) do for command, func in pairs( AceConsole.weakcommands[target] ) do
target:RegisterChatCommand( command, func, false, true ) -- nonpersisting and silent registry target:RegisterChatCommand( command, func, false, true ) -- nonpersisting and silent registry
end end
end end
end end
function AceConsole:OnEmbedDisable( target ) function AceConsole:OnEmbedDisable( target )
if AceConsole.weakcommands[target] then if AceConsole.weakcommands[target] then
for command, func in pairs( AceConsole.weakcommands[target] ) do for command, func in pairs( AceConsole.weakcommands[target] ) do
target:UnregisterChatCommand( command ) -- TODO: this could potentially unregister a command from another application in case of command conflicts. Do we care? target:UnregisterChatCommand( command ) -- TODO: this could potentially unregister a command from another application in case of command conflicts. Do we care?
end end
end end
end end
for addon in pairs(AceConsole.embeds) do for addon in pairs(AceConsole.embeds) do
AceConsole:Embed(addon) AceConsole:Embed(addon)
end end

View File

@ -1,4 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ <Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd"> ..\FrameXML\UI.xsd">
<Script file="AceConsole-3.0.lua"/> <Script file="AceConsole-3.0.lua"/>
</Ui> </Ui>

View File

@ -1,126 +1,126 @@
--- AceEvent-3.0 provides event registration and secure dispatching. --- AceEvent-3.0 provides event registration and secure dispatching.
-- All dispatching is done using **CallbackHandler-1.0**. AceEvent is a simple wrapper around -- All dispatching is done using **CallbackHandler-1.0**. AceEvent is a simple wrapper around
-- CallbackHandler, and dispatches all game events or addon message to the registrees. -- CallbackHandler, and dispatches all game events or addon message to the registrees.
-- --
-- **AceEvent-3.0** can be embeded into your addon, either explicitly by calling AceEvent:Embed(MyAddon) or by -- **AceEvent-3.0** can be embeded into your addon, either explicitly by calling AceEvent:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceEvent itself.\\ -- and can be accessed directly, without having to explicitly call AceEvent itself.\\
-- It is recommended to embed AceEvent, otherwise you'll have to specify a custom `self` on all calls you -- It is recommended to embed AceEvent, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceEvent. -- make into AceEvent.
-- @class file -- @class file
-- @name AceEvent-3.0 -- @name AceEvent-3.0
-- @release $Id: AceEvent-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $ -- @release $Id: AceEvent-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $
local CallbackHandler = LibStub("CallbackHandler-1.0") local CallbackHandler = LibStub("CallbackHandler-1.0")
local MAJOR, MINOR = "AceEvent-3.0", 4 local MAJOR, MINOR = "AceEvent-3.0", 4
local AceEvent = LibStub:NewLibrary(MAJOR, MINOR) local AceEvent = LibStub:NewLibrary(MAJOR, MINOR)
if not AceEvent then return end if not AceEvent then return end
-- Lua APIs -- Lua APIs
local pairs = pairs local pairs = pairs
AceEvent.frame = AceEvent.frame or CreateFrame("Frame", "AceEvent30Frame") -- our event frame AceEvent.frame = AceEvent.frame or CreateFrame("Frame", "AceEvent30Frame") -- our event frame
AceEvent.embeds = AceEvent.embeds or {} -- what objects embed this lib AceEvent.embeds = AceEvent.embeds or {} -- what objects embed this lib
-- APIs and registry for blizzard events, using CallbackHandler lib -- APIs and registry for blizzard events, using CallbackHandler lib
if not AceEvent.events then if not AceEvent.events then
AceEvent.events = CallbackHandler:New(AceEvent, AceEvent.events = CallbackHandler:New(AceEvent,
"RegisterEvent", "UnregisterEvent", "UnregisterAllEvents") "RegisterEvent", "UnregisterEvent", "UnregisterAllEvents")
end end
function AceEvent.events:OnUsed(target, eventname) function AceEvent.events:OnUsed(target, eventname)
AceEvent.frame:RegisterEvent(eventname) AceEvent.frame:RegisterEvent(eventname)
end end
function AceEvent.events:OnUnused(target, eventname) function AceEvent.events:OnUnused(target, eventname)
AceEvent.frame:UnregisterEvent(eventname) AceEvent.frame:UnregisterEvent(eventname)
end end
-- APIs and registry for IPC messages, using CallbackHandler lib -- APIs and registry for IPC messages, using CallbackHandler lib
if not AceEvent.messages then if not AceEvent.messages then
AceEvent.messages = CallbackHandler:New(AceEvent, AceEvent.messages = CallbackHandler:New(AceEvent,
"RegisterMessage", "UnregisterMessage", "UnregisterAllMessages" "RegisterMessage", "UnregisterMessage", "UnregisterAllMessages"
) )
AceEvent.SendMessage = AceEvent.messages.Fire AceEvent.SendMessage = AceEvent.messages.Fire
end end
--- embedding and embed handling --- embedding and embed handling
local mixins = { local mixins = {
"RegisterEvent", "UnregisterEvent", "RegisterEvent", "UnregisterEvent",
"RegisterMessage", "UnregisterMessage", "RegisterMessage", "UnregisterMessage",
"SendMessage", "SendMessage",
"UnregisterAllEvents", "UnregisterAllMessages", "UnregisterAllEvents", "UnregisterAllMessages",
} }
--- Register for a Blizzard Event. --- Register for a Blizzard Event.
-- The callback will be called with the optional `arg` as the first argument (if supplied), and the event name as the second (or first, if no arg was supplied) -- The callback will be called with the optional `arg` as the first argument (if supplied), and the event name as the second (or first, if no arg was supplied)
-- Any arguments to the event will be passed on after that. -- Any arguments to the event will be passed on after that.
-- @name AceEvent:RegisterEvent -- @name AceEvent:RegisterEvent
-- @class function -- @class function
-- @paramsig event[, callback [, arg]] -- @paramsig event[, callback [, arg]]
-- @param event The event to register for -- @param event The event to register for
-- @param callback The callback function to call when the event is triggered (funcref or method, defaults to a method with the event name) -- @param callback The callback function to call when the event is triggered (funcref or method, defaults to a method with the event name)
-- @param arg An optional argument to pass to the callback function -- @param arg An optional argument to pass to the callback function
--- Unregister an event. --- Unregister an event.
-- @name AceEvent:UnregisterEvent -- @name AceEvent:UnregisterEvent
-- @class function -- @class function
-- @paramsig event -- @paramsig event
-- @param event The event to unregister -- @param event The event to unregister
--- Register for a custom AceEvent-internal message. --- Register for a custom AceEvent-internal message.
-- The callback will be called with the optional `arg` as the first argument (if supplied), and the event name as the second (or first, if no arg was supplied) -- The callback will be called with the optional `arg` as the first argument (if supplied), and the event name as the second (or first, if no arg was supplied)
-- Any arguments to the event will be passed on after that. -- Any arguments to the event will be passed on after that.
-- @name AceEvent:RegisterMessage -- @name AceEvent:RegisterMessage
-- @class function -- @class function
-- @paramsig message[, callback [, arg]] -- @paramsig message[, callback [, arg]]
-- @param message The message to register for -- @param message The message to register for
-- @param callback The callback function to call when the message is triggered (funcref or method, defaults to a method with the event name) -- @param callback The callback function to call when the message is triggered (funcref or method, defaults to a method with the event name)
-- @param arg An optional argument to pass to the callback function -- @param arg An optional argument to pass to the callback function
--- Unregister a message --- Unregister a message
-- @name AceEvent:UnregisterMessage -- @name AceEvent:UnregisterMessage
-- @class function -- @class function
-- @paramsig message -- @paramsig message
-- @param message The message to unregister -- @param message The message to unregister
--- Send a message over the AceEvent-3.0 internal message system to other addons registered for this message. --- Send a message over the AceEvent-3.0 internal message system to other addons registered for this message.
-- @name AceEvent:SendMessage -- @name AceEvent:SendMessage
-- @class function -- @class function
-- @paramsig message, ... -- @paramsig message, ...
-- @param message The message to send -- @param message The message to send
-- @param ... Any arguments to the message -- @param ... Any arguments to the message
-- Embeds AceEvent into the target object making the functions from the mixins list available on target:.. -- Embeds AceEvent into the target object making the functions from the mixins list available on target:..
-- @param target target object to embed AceEvent in -- @param target target object to embed AceEvent in
function AceEvent:Embed(target) function AceEvent:Embed(target)
for k, v in pairs(mixins) do for k, v in pairs(mixins) do
target[v] = self[v] target[v] = self[v]
end end
self.embeds[target] = true self.embeds[target] = true
return target return target
end end
-- AceEvent:OnEmbedDisable( target ) -- AceEvent:OnEmbedDisable( target )
-- target (object) - target object that is being disabled -- target (object) - target object that is being disabled
-- --
-- Unregister all events messages etc when the target disables. -- Unregister all events messages etc when the target disables.
-- this method should be called by the target manually or by an addon framework -- this method should be called by the target manually or by an addon framework
function AceEvent:OnEmbedDisable(target) function AceEvent:OnEmbedDisable(target)
target:UnregisterAllEvents() target:UnregisterAllEvents()
target:UnregisterAllMessages() target:UnregisterAllMessages()
end end
-- Script to fire blizzard events into the event listeners -- Script to fire blizzard events into the event listeners
local events = AceEvent.events local events = AceEvent.events
AceEvent.frame:SetScript("OnEvent", function(this, event, ...) AceEvent.frame:SetScript("OnEvent", function(this, event, ...)
events:Fire(event, ...) events:Fire(event, ...)
end) end)
--- Finally: upgrade our old embeds --- Finally: upgrade our old embeds
for target, v in pairs(AceEvent.embeds) do for target, v in pairs(AceEvent.embeds) do
AceEvent:Embed(target) AceEvent:Embed(target)
end end

View File

@ -1,4 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ <Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd"> ..\FrameXML\UI.xsd">
<Script file="AceEvent-3.0.lua"/> <Script file="AceEvent-3.0.lua"/>
</Ui> </Ui>

View File

@ -1,276 +1,276 @@
--- **AceTimer-3.0** provides a central facility for registering timers. --- **AceTimer-3.0** provides a central facility for registering timers.
-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient -- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered -- data structure that allows easy dispatching and fast rescheduling. Timers can be registered
-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\ -- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
-- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API -- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API
-- restricts us to. -- restricts us to.
-- --
-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you -- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
-- need to cancel the timer you just registered. -- need to cancel the timer you just registered.
-- --
-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by -- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceTimer itself.\\ -- and can be accessed directly, without having to explicitly call AceTimer itself.\\
-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you -- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceTimer. -- make into AceTimer.
-- @class file -- @class file
-- @name AceTimer-3.0 -- @name AceTimer-3.0
-- @release $Id: AceTimer-3.0.lua 1119 2014-10-14 17:23:29Z nevcairiel $ -- @release $Id: AceTimer-3.0.lua 1119 2014-10-14 17:23:29Z nevcairiel $
local MAJOR, MINOR = "AceTimer-3.0", 17 -- Bump minor on changes local MAJOR, MINOR = "AceTimer-3.0", 17 -- Bump minor on changes
local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR) local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceTimer then return end -- No upgrade needed if not AceTimer then return end -- No upgrade needed
AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list
local activeTimers = AceTimer.activeTimers -- Upvalue our private data local activeTimers = AceTimer.activeTimers -- Upvalue our private data
-- Lua APIs -- Lua APIs
local type, unpack, next, error, select = type, unpack, next, error, select local type, unpack, next, error, select = type, unpack, next, error, select
-- WoW APIs -- WoW APIs
local GetTime, C_TimerAfter = GetTime, C_Timer.After local GetTime, C_TimerAfter = GetTime, C_Timer.After
local function new(self, loop, func, delay, ...) local function new(self, loop, func, delay, ...)
if delay < 0.01 then if delay < 0.01 then
delay = 0.01 -- Restrict to the lowest time that the C_Timer API allows us delay = 0.01 -- Restrict to the lowest time that the C_Timer API allows us
end end
local timer = {...} local timer = {...}
timer.object = self timer.object = self
timer.func = func timer.func = func
timer.looping = loop timer.looping = loop
timer.argsCount = select("#", ...) timer.argsCount = select("#", ...)
timer.delay = delay timer.delay = delay
timer.ends = GetTime() + delay timer.ends = GetTime() + delay
activeTimers[timer] = timer activeTimers[timer] = timer
-- Create new timer closure to wrap the "timer" object -- Create new timer closure to wrap the "timer" object
timer.callback = function() timer.callback = function()
if not timer.cancelled then if not timer.cancelled then
if type(timer.func) == "string" then if type(timer.func) == "string" then
-- We manually set the unpack count to prevent issues with an arg set that contains nil and ends with nil -- We manually set the unpack count to prevent issues with an arg set that contains nil and ends with nil
-- e.g. local t = {1, 2, nil, 3, nil} print(#t) will result in 2, instead of 5. This fixes said issue. -- e.g. local t = {1, 2, nil, 3, nil} print(#t) will result in 2, instead of 5. This fixes said issue.
timer.object[timer.func](timer.object, unpack(timer, 1, timer.argsCount)) timer.object[timer.func](timer.object, unpack(timer, 1, timer.argsCount))
else else
timer.func(unpack(timer, 1, timer.argsCount)) timer.func(unpack(timer, 1, timer.argsCount))
end end
if timer.looping and not timer.cancelled then if timer.looping and not timer.cancelled then
-- Compensate delay to get a perfect average delay, even if individual times don't match up perfectly -- Compensate delay to get a perfect average delay, even if individual times don't match up perfectly
-- due to fps differences -- due to fps differences
local time = GetTime() local time = GetTime()
local delay = timer.delay - (time - timer.ends) local delay = timer.delay - (time - timer.ends)
-- Ensure the delay doesn't go below the threshold -- Ensure the delay doesn't go below the threshold
if delay < 0.01 then delay = 0.01 end if delay < 0.01 then delay = 0.01 end
C_TimerAfter(delay, timer.callback) C_TimerAfter(delay, timer.callback)
timer.ends = time + delay timer.ends = time + delay
else else
activeTimers[timer.handle or timer] = nil activeTimers[timer.handle or timer] = nil
end end
end end
end end
C_TimerAfter(delay, timer.callback) C_TimerAfter(delay, timer.callback)
return timer return timer
end end
--- Schedule a new one-shot timer. --- Schedule a new one-shot timer.
-- The timer will fire once in `delay` seconds, unless canceled before. -- The timer will fire once in `delay` seconds, unless canceled before.
-- @param callback Callback function for the timer pulse (funcref or method name). -- @param callback Callback function for the timer pulse (funcref or method name).
-- @param delay Delay for the timer, in seconds. -- @param delay Delay for the timer, in seconds.
-- @param ... An optional, unlimited amount of arguments to pass to the callback function. -- @param ... An optional, unlimited amount of arguments to pass to the callback function.
-- @usage -- @usage
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0") -- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
-- --
-- function MyAddOn:OnEnable() -- function MyAddOn:OnEnable()
-- self:ScheduleTimer("TimerFeedback", 5) -- self:ScheduleTimer("TimerFeedback", 5)
-- end -- end
-- --
-- function MyAddOn:TimerFeedback() -- function MyAddOn:TimerFeedback()
-- print("5 seconds passed") -- print("5 seconds passed")
-- end -- end
function AceTimer:ScheduleTimer(func, delay, ...) function AceTimer:ScheduleTimer(func, delay, ...)
if not func or not delay then if not func or not delay then
error(MAJOR..": ScheduleTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2) error(MAJOR..": ScheduleTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
end end
if type(func) == "string" then if type(func) == "string" then
if type(self) ~= "table" then if type(self) ~= "table" then
error(MAJOR..": ScheduleTimer(callback, delay, args...): 'self' - must be a table.", 2) error(MAJOR..": ScheduleTimer(callback, delay, args...): 'self' - must be a table.", 2)
elseif not self[func] then elseif not self[func] then
error(MAJOR..": ScheduleTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2) error(MAJOR..": ScheduleTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
end end
end end
return new(self, nil, func, delay, ...) return new(self, nil, func, delay, ...)
end end
--- Schedule a repeating timer. --- Schedule a repeating timer.
-- The timer will fire every `delay` seconds, until canceled. -- The timer will fire every `delay` seconds, until canceled.
-- @param callback Callback function for the timer pulse (funcref or method name). -- @param callback Callback function for the timer pulse (funcref or method name).
-- @param delay Delay for the timer, in seconds. -- @param delay Delay for the timer, in seconds.
-- @param ... An optional, unlimited amount of arguments to pass to the callback function. -- @param ... An optional, unlimited amount of arguments to pass to the callback function.
-- @usage -- @usage
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0") -- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
-- --
-- function MyAddOn:OnEnable() -- function MyAddOn:OnEnable()
-- self.timerCount = 0 -- self.timerCount = 0
-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5) -- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
-- end -- end
-- --
-- function MyAddOn:TimerFeedback() -- function MyAddOn:TimerFeedback()
-- self.timerCount = self.timerCount + 1 -- self.timerCount = self.timerCount + 1
-- print(("%d seconds passed"):format(5 * self.timerCount)) -- print(("%d seconds passed"):format(5 * self.timerCount))
-- -- run 30 seconds in total -- -- run 30 seconds in total
-- if self.timerCount == 6 then -- if self.timerCount == 6 then
-- self:CancelTimer(self.testTimer) -- self:CancelTimer(self.testTimer)
-- end -- end
-- end -- end
function AceTimer:ScheduleRepeatingTimer(func, delay, ...) function AceTimer:ScheduleRepeatingTimer(func, delay, ...)
if not func or not delay then if not func or not delay then
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2) error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
end end
if type(func) == "string" then if type(func) == "string" then
if type(self) ~= "table" then if type(self) ~= "table" then
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'self' - must be a table.", 2) error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'self' - must be a table.", 2)
elseif not self[func] then elseif not self[func] then
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2) error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
end end
end end
return new(self, true, func, delay, ...) return new(self, true, func, delay, ...)
end end
--- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer` --- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer`
-- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid -- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid
-- and the timer has not fired yet or was canceled before. -- and the timer has not fired yet or was canceled before.
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer` -- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
function AceTimer:CancelTimer(id) function AceTimer:CancelTimer(id)
local timer = activeTimers[id] local timer = activeTimers[id]
if not timer then if not timer then
return false return false
else else
timer.cancelled = true timer.cancelled = true
activeTimers[id] = nil activeTimers[id] = nil
return true return true
end end
end end
--- Cancels all timers registered to the current addon object ('self') --- Cancels all timers registered to the current addon object ('self')
function AceTimer:CancelAllTimers() function AceTimer:CancelAllTimers()
for k,v in pairs(activeTimers) do for k,v in pairs(activeTimers) do
if v.object == self then if v.object == self then
AceTimer.CancelTimer(self, k) AceTimer.CancelTimer(self, k)
end end
end end
end end
--- Returns the time left for a timer with the given id, registered by the current addon object ('self'). --- Returns the time left for a timer with the given id, registered by the current addon object ('self').
-- This function will return 0 when the id is invalid. -- This function will return 0 when the id is invalid.
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer` -- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
-- @return The time left on the timer. -- @return The time left on the timer.
function AceTimer:TimeLeft(id) function AceTimer:TimeLeft(id)
local timer = activeTimers[id] local timer = activeTimers[id]
if not timer then if not timer then
return 0 return 0
else else
return timer.ends - GetTime() return timer.ends - GetTime()
end end
end end
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
-- Upgrading -- Upgrading
-- Upgrade from old hash-bucket based timers to C_Timer.After timers. -- Upgrade from old hash-bucket based timers to C_Timer.After timers.
if oldminor and oldminor < 10 then if oldminor and oldminor < 10 then
-- disable old timer logic -- disable old timer logic
AceTimer.frame:SetScript("OnUpdate", nil) AceTimer.frame:SetScript("OnUpdate", nil)
AceTimer.frame:SetScript("OnEvent", nil) AceTimer.frame:SetScript("OnEvent", nil)
AceTimer.frame:UnregisterAllEvents() AceTimer.frame:UnregisterAllEvents()
-- convert timers -- convert timers
for object,timers in pairs(AceTimer.selfs) do for object,timers in pairs(AceTimer.selfs) do
for handle,timer in pairs(timers) do for handle,timer in pairs(timers) do
if type(timer) == "table" and timer.callback then if type(timer) == "table" and timer.callback then
local newTimer local newTimer
if timer.delay then if timer.delay then
newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.callback, timer.delay, timer.arg) newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.callback, timer.delay, timer.arg)
else else
newTimer = AceTimer.ScheduleTimer(timer.object, timer.callback, timer.when - GetTime(), timer.arg) newTimer = AceTimer.ScheduleTimer(timer.object, timer.callback, timer.when - GetTime(), timer.arg)
end end
-- Use the old handle for old timers -- Use the old handle for old timers
activeTimers[newTimer] = nil activeTimers[newTimer] = nil
activeTimers[handle] = newTimer activeTimers[handle] = newTimer
newTimer.handle = handle newTimer.handle = handle
end end
end end
end end
AceTimer.selfs = nil AceTimer.selfs = nil
AceTimer.hash = nil AceTimer.hash = nil
AceTimer.debug = nil AceTimer.debug = nil
elseif oldminor and oldminor < 17 then elseif oldminor and oldminor < 17 then
-- Upgrade from old animation based timers to C_Timer.After timers. -- Upgrade from old animation based timers to C_Timer.After timers.
AceTimer.inactiveTimers = nil AceTimer.inactiveTimers = nil
AceTimer.frame = nil AceTimer.frame = nil
local oldTimers = AceTimer.activeTimers local oldTimers = AceTimer.activeTimers
-- Clear old timer table and update upvalue -- Clear old timer table and update upvalue
AceTimer.activeTimers = {} AceTimer.activeTimers = {}
activeTimers = AceTimer.activeTimers activeTimers = AceTimer.activeTimers
for handle, timer in pairs(oldTimers) do for handle, timer in pairs(oldTimers) do
local newTimer local newTimer
-- Stop the old timer animation -- Stop the old timer animation
local duration, elapsed = timer:GetDuration(), timer:GetElapsed() local duration, elapsed = timer:GetDuration(), timer:GetElapsed()
timer:GetParent():Stop() timer:GetParent():Stop()
if timer.looping then if timer.looping then
newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.func, duration, unpack(timer.args, 1, timer.argsCount)) newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.func, duration, unpack(timer.args, 1, timer.argsCount))
else else
newTimer = AceTimer.ScheduleTimer(timer.object, timer.func, duration - elapsed, unpack(timer.args, 1, timer.argsCount)) newTimer = AceTimer.ScheduleTimer(timer.object, timer.func, duration - elapsed, unpack(timer.args, 1, timer.argsCount))
end end
-- Use the old handle for old timers -- Use the old handle for old timers
activeTimers[newTimer] = nil activeTimers[newTimer] = nil
activeTimers[handle] = newTimer activeTimers[handle] = newTimer
newTimer.handle = handle newTimer.handle = handle
end end
-- Migrate transitional handles -- Migrate transitional handles
if oldminor < 13 and AceTimer.hashCompatTable then if oldminor < 13 and AceTimer.hashCompatTable then
for handle, id in pairs(AceTimer.hashCompatTable) do for handle, id in pairs(AceTimer.hashCompatTable) do
local t = activeTimers[id] local t = activeTimers[id]
if t then if t then
activeTimers[id] = nil activeTimers[id] = nil
activeTimers[handle] = t activeTimers[handle] = t
t.handle = handle t.handle = handle
end end
end end
AceTimer.hashCompatTable = nil AceTimer.hashCompatTable = nil
end end
end end
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
-- Embed handling -- Embed handling
AceTimer.embeds = AceTimer.embeds or {} AceTimer.embeds = AceTimer.embeds or {}
local mixins = { local mixins = {
"ScheduleTimer", "ScheduleRepeatingTimer", "ScheduleTimer", "ScheduleRepeatingTimer",
"CancelTimer", "CancelAllTimers", "CancelTimer", "CancelAllTimers",
"TimeLeft" "TimeLeft"
} }
function AceTimer:Embed(target) function AceTimer:Embed(target)
AceTimer.embeds[target] = true AceTimer.embeds[target] = true
for _,v in pairs(mixins) do for _,v in pairs(mixins) do
target[v] = AceTimer[v] target[v] = AceTimer[v]
end end
return target return target
end end
-- AceTimer:OnEmbedDisable(target) -- AceTimer:OnEmbedDisable(target)
-- target (object) - target object that AceTimer is embedded in. -- target (object) - target object that AceTimer is embedded in.
-- --
-- cancel all timers registered for the object -- cancel all timers registered for the object
function AceTimer:OnEmbedDisable(target) function AceTimer:OnEmbedDisable(target)
target:CancelAllTimers() target:CancelAllTimers()
end end
for addon in pairs(AceTimer.embeds) do for addon in pairs(AceTimer.embeds) do
AceTimer:Embed(addon) AceTimer:Embed(addon)
end end

View File

@ -1,4 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ <Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd"> ..\FrameXML\UI.xsd">
<Script file="AceTimer-3.0.lua"/> <Script file="AceTimer-3.0.lua"/>
</Ui> </Ui>

View File

@ -1,30 +1,30 @@
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info -- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke -- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS! local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local LibStub = _G[LIBSTUB_MAJOR] local LibStub = _G[LIBSTUB_MAJOR]
if not LibStub or LibStub.minor < LIBSTUB_MINOR then if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or {libs = {}, minors = {} } LibStub = LibStub or {libs = {}, minors = {} }
_G[LIBSTUB_MAJOR] = LibStub _G[LIBSTUB_MAJOR] = LibStub
LibStub.minor = LIBSTUB_MINOR LibStub.minor = LIBSTUB_MINOR
function LibStub:NewLibrary(major, minor) function LibStub:NewLibrary(major, minor)
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)") assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.") minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
local oldminor = self.minors[major] local oldminor = self.minors[major]
if oldminor and oldminor >= minor then return nil end if oldminor and oldminor >= minor then return nil end
self.minors[major], self.libs[major] = minor, self.libs[major] or {} self.minors[major], self.libs[major] = minor, self.libs[major] or {}
return self.libs[major], oldminor return self.libs[major], oldminor
end end
function LibStub:GetLibrary(major, silent) function LibStub:GetLibrary(major, silent)
if not self.libs[major] and not silent then if not self.libs[major] and not silent then
error(("Cannot find a library instance of %q."):format(tostring(major)), 2) error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
end end
return self.libs[major], self.minors[major] return self.libs[major], self.minors[major]
end end
function LibStub:IterateLibraries() return pairs(self.libs) end function LibStub:IterateLibraries() return pairs(self.libs) end
setmetatable(LibStub, { __call = LibStub.GetLibrary }) setmetatable(LibStub, { __call = LibStub.GetLibrary })
end end

View File

@ -1,382 +1,382 @@
local json = LibStub:NewLibrary("json", 1) local json = LibStub:NewLibrary("json", 1)
if not json then return end if not json then return end
-- --
-- json.lua -- json.lua
-- --
-- Copyright (c) 2015 rxi -- Copyright (c) 2015 rxi
-- --
-- This library is free software; you can redistribute it and/or modify it -- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details. -- under the terms of the MIT license. See LICENSE for details.
-- --
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- Encode -- Encode
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
local encode local encode
local escape_char_map = { local escape_char_map = {
[ "\\" ] = "\\\\", [ "\\" ] = "\\\\",
[ "\"" ] = "\\\"", [ "\"" ] = "\\\"",
[ "\b" ] = "\\b", [ "\b" ] = "\\b",
[ "\f" ] = "\\f", [ "\f" ] = "\\f",
[ "\n" ] = "\\n", [ "\n" ] = "\\n",
[ "\r" ] = "\\r", [ "\r" ] = "\\r",
[ "\t" ] = "\\t", [ "\t" ] = "\\t",
} }
local escape_char_map_inv = { [ "\\/" ] = "/" } local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k escape_char_map_inv[v] = k
end end
local function escape_char(c) local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte()) return escape_char_map[c] or string.format("\\u%04x", c:byte())
end end
local function encode_nil(val) local function encode_nil(val)
return "null" return "null"
end end
local function encode_table(val, stack) local function encode_table(val, stack)
local res = {} local res = {}
stack = stack or {} stack = stack or {}
-- Circular reference? -- Circular reference?
if stack[val] then error("circular reference") end if stack[val] then error("circular reference") end
stack[val] = true stack[val] = true
if val[1] ~= nil or next(val) == nil then if val[1] ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse -- Treat as array -- check keys are valid and it is not sparse
local n = 0 local n = 0
for k in pairs(val) do for k in pairs(val) do
if type(k) ~= "number" then if type(k) ~= "number" then
error("invalid table: mixed or invalid key types") error("invalid table: mixed or invalid key types")
end end
n = n + 1 n = n + 1
end end
if n ~= #val then if n ~= #val then
error("invalid table: sparse array") error("invalid table: sparse array")
end end
-- Encode -- Encode
for i, v in ipairs(val) do for i, v in ipairs(val) do
table.insert(res, encode(v, stack)) table.insert(res, encode(v, stack))
end end
stack[val] = nil stack[val] = nil
return "[" .. table.concat(res, ",") .. "]" return "[" .. table.concat(res, ",") .. "]"
else else
-- Treat as an object -- Treat as an object
for k, v in pairs(val) do for k, v in pairs(val) do
if type(k) ~= "string" then if type(k) ~= "string" then
error("invalid table: mixed or invalid key types") error("invalid table: mixed or invalid key types")
end end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end end
stack[val] = nil stack[val] = nil
return "{" .. table.concat(res, ",") .. "}" return "{" .. table.concat(res, ",") .. "}"
end end
end end
local function encode_string(val) local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end end
local function encode_number(val) local function encode_number(val)
-- Check for NaN, -inf and inf -- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'") error("unexpected number value '" .. tostring(val) .. "'")
end end
return string.format("%.14g", val) return string.format("%.14g", val)
end end
local type_func_map = { local type_func_map = {
[ "nil" ] = encode_nil, [ "nil" ] = encode_nil,
[ "table" ] = encode_table, [ "table" ] = encode_table,
[ "string" ] = encode_string, [ "string" ] = encode_string,
[ "number" ] = encode_number, [ "number" ] = encode_number,
[ "boolean" ] = tostring, [ "boolean" ] = tostring,
} }
encode = function(val, stack) encode = function(val, stack)
local t = type(val) local t = type(val)
local f = type_func_map[t] local f = type_func_map[t]
if f then if f then
return f(val, stack) return f(val, stack)
end end
error("unexpected type '" .. t .. "'") error("unexpected type '" .. t .. "'")
end end
function json.encode(val) function json.encode(val)
return ( encode(val) ) return ( encode(val) )
end end
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- Decode -- Decode
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
local parse local parse
local function create_set(...) local function create_set(...)
local res = {} local res = {}
for i = 1, select("#", ...) do for i = 1, select("#", ...) do
res[ select(i, ...) ] = true res[ select(i, ...) ] = true
end end
return res return res
end end
local space_chars = create_set(" ", "\t", "\r", "\n") local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null") local literals = create_set("true", "false", "null")
local literal_map = { local literal_map = {
[ "true" ] = true, [ "true" ] = true,
[ "false" ] = false, [ "false" ] = false,
[ "null" ] = nil, [ "null" ] = nil,
} }
local function next_char(str, idx, set, negate) local function next_char(str, idx, set, negate)
for i = idx, #str do for i = idx, #str do
if set[str:sub(i, i)] ~= negate then if set[str:sub(i, i)] ~= negate then
return i return i
end end
end end
return #str + 1 return #str + 1
end end
local function decode_error(str, idx, msg) local function decode_error(str, idx, msg)
local line_count = 1 local line_count = 1
local col_count = 1 local col_count = 1
for i = 1, idx - 1 do for i = 1, idx - 1 do
col_count = col_count + 1 col_count = col_count + 1
if str:sub(i, i) == "\n" then if str:sub(i, i) == "\n" then
line_count = line_count + 1 line_count = line_count + 1
col_count = 1 col_count = 1
end end
end end
error( string.format("%s at line %d col %d", msg, line_count, col_count) ) error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end end
local function codepoint_to_utf8(n) local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor local f = math.floor
if n <= 0x7f then if n <= 0x7f then
return string.char(n) return string.char(n)
elseif n <= 0x7ff then elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128) return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128) f(n % 4096 / 64) + 128, n % 64 + 128)
end end
error( string.format("invalid unicode codepoint '%x'", n) ) error( string.format("invalid unicode codepoint '%x'", n) )
end end
local function parse_unicode_escape(s) local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 ) local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 ) local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair? -- Surrogate pair?
if n2 then if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else else
return codepoint_to_utf8(n1) return codepoint_to_utf8(n1)
end end
end end
local function parse_string(str, i) local function parse_string(str, i)
local has_unicode_escape = false local has_unicode_escape = false
local has_surrogate_escape = false local has_surrogate_escape = false
local has_escape = false local has_escape = false
local last local last
for j = i + 1, #str do for j = i + 1, #str do
local x = str:byte(j) local x = str:byte(j)
if x < 32 then if x < 32 then
decode_error(str, j, "control character in string") decode_error(str, j, "control character in string")
end end
if last == 92 then -- "\\" (escape char) if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence) if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5) local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string") decode_error(str, j, "invalid unicode escape in string")
end end
if hex:find("^[dD][89aAbB]") then if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true has_surrogate_escape = true
else else
has_unicode_escape = true has_unicode_escape = true
end end
else else
local c = string.char(x) local c = string.char(x)
if not escape_chars[c] then if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string") decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end end
has_escape = true has_escape = true
end end
last = nil last = nil
elseif x == 34 then -- '"' (end of string) elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1) local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end end
if has_unicode_escape then if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape) s = s:gsub("\\u....", parse_unicode_escape)
end end
if has_escape then if has_escape then
s = s:gsub("\\.", escape_char_map_inv) s = s:gsub("\\.", escape_char_map_inv)
end end
return s, j + 1 return s, j + 1
else else
last = x last = x
end end
end end
decode_error(str, i, "expected closing quote for string") decode_error(str, i, "expected closing quote for string")
end end
local function parse_number(str, i) local function parse_number(str, i)
local x = next_char(str, i, delim_chars) local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1) local s = str:sub(i, x - 1)
local n = tonumber(s) local n = tonumber(s)
if not n then if not n then
decode_error(str, i, "invalid number '" .. s .. "'") decode_error(str, i, "invalid number '" .. s .. "'")
end end
return n, x return n, x
end end
local function parse_literal(str, i) local function parse_literal(str, i)
local x = next_char(str, i, delim_chars) local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1) local word = str:sub(i, x - 1)
if not literals[word] then if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'") decode_error(str, i, "invalid literal '" .. word .. "'")
end end
return literal_map[word], x return literal_map[word], x
end end
local function parse_array(str, i) local function parse_array(str, i)
local res = {} local res = {}
local n = 1 local n = 1
i = i + 1 i = i + 1
while 1 do while 1 do
local x local x
i = next_char(str, i, space_chars, true) i = next_char(str, i, space_chars, true)
-- Empty / end of array? -- Empty / end of array?
if str:sub(i, i) == "]" then if str:sub(i, i) == "]" then
i = i + 1 i = i + 1
break break
end end
-- Read token -- Read token
x, i = parse(str, i) x, i = parse(str, i)
res[n] = x res[n] = x
n = n + 1 n = n + 1
-- Next token -- Next token
i = next_char(str, i, space_chars, true) i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i) local chr = str:sub(i, i)
i = i + 1 i = i + 1
if chr == "]" then break end if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end end
return res, i return res, i
end end
local function parse_object(str, i) local function parse_object(str, i)
local res = {} local res = {}
i = i + 1 i = i + 1
while 1 do while 1 do
local key, val local key, val
i = next_char(str, i, space_chars, true) i = next_char(str, i, space_chars, true)
-- Empty / end of object? -- Empty / end of object?
if str:sub(i, i) == "}" then if str:sub(i, i) == "}" then
i = i + 1 i = i + 1
break break
end end
-- Read key -- Read key
if str:sub(i, i) ~= '"' then if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key") decode_error(str, i, "expected string for key")
end end
key, i = parse(str, i) key, i = parse(str, i)
-- Read ':' delimiter -- Read ':' delimiter
i = next_char(str, i, space_chars, true) i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key") decode_error(str, i, "expected ':' after key")
end end
i = next_char(str, i + 1, space_chars, true) i = next_char(str, i + 1, space_chars, true)
-- Read value -- Read value
val, i = parse(str, i) val, i = parse(str, i)
-- Set -- Set
res[key] = val res[key] = val
-- Next token -- Next token
i = next_char(str, i, space_chars, true) i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i) local chr = str:sub(i, i)
i = i + 1 i = i + 1
if chr == "}" then break end if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end end
return res, i return res, i
end end
local char_func_map = { local char_func_map = {
[ '"' ] = parse_string, [ '"' ] = parse_string,
[ "0" ] = parse_number, [ "0" ] = parse_number,
[ "1" ] = parse_number, [ "1" ] = parse_number,
[ "2" ] = parse_number, [ "2" ] = parse_number,
[ "3" ] = parse_number, [ "3" ] = parse_number,
[ "4" ] = parse_number, [ "4" ] = parse_number,
[ "5" ] = parse_number, [ "5" ] = parse_number,
[ "6" ] = parse_number, [ "6" ] = parse_number,
[ "7" ] = parse_number, [ "7" ] = parse_number,
[ "8" ] = parse_number, [ "8" ] = parse_number,
[ "9" ] = parse_number, [ "9" ] = parse_number,
[ "-" ] = parse_number, [ "-" ] = parse_number,
[ "t" ] = parse_literal, [ "t" ] = parse_literal,
[ "f" ] = parse_literal, [ "f" ] = parse_literal,
[ "n" ] = parse_literal, [ "n" ] = parse_literal,
[ "[" ] = parse_array, [ "[" ] = parse_array,
[ "{" ] = parse_object, [ "{" ] = parse_object,
} }
parse = function(str, idx) parse = function(str, idx)
local chr = str:sub(idx, idx) local chr = str:sub(idx, idx)
local f = char_func_map[chr] local f = char_func_map[chr]
if f then if f then
return f(str, idx) return f(str, idx)
end end
decode_error(str, idx, "unexpected character '" .. chr .. "'") decode_error(str, idx, "unexpected character '" .. chr .. "'")
end end
function json.decode(str) function json.decode(str)
if type(str) ~= "string" then if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str)) error("expected argument of type string, got " .. type(str))
end end
return ( parse(str, next_char(str, 1, space_chars, true)) ) return ( parse(str, next_char(str, 1, space_chars, true)) )
end end
return json return json

View File

@ -1,9 +1,9 @@
<Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd"> <Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="Libs\LibStub\LibStub.lua"/> <Script file="Libs\LibStub\LibStub.lua"/>
<Include file="Libs\AceAddon-3.0\AceAddon-3.0.xml"/> <Include file="Libs\AceAddon-3.0\AceAddon-3.0.xml"/>
<Include file="Libs\AceConsole-3.0\AceConsole-3.0.xml"/> <Include file="Libs\AceConsole-3.0\AceConsole-3.0.xml"/>
<Include file="Libs\AceEvent-3.0\AceEvent-3.0.xml"/> <Include file="Libs\AceEvent-3.0\AceEvent-3.0.xml"/>
<Include file="Libs\AceTimer-3.0\AceTimer-3.0.xml"/> <Include file="Libs\AceTimer-3.0\AceTimer-3.0.xml"/>
<Include file="Libs\AceComm-3.0\AceComm-3.0.xml"/> <Include file="Libs\AceComm-3.0\AceComm-3.0.xml"/>
<Include file="Libs\json.lua"/> <Include file="Libs\json.lua"/>
</Ui> </Ui>

View File

@ -75,7 +75,7 @@ namespace Artemis.Modules.Games.WoW
ParseAuras(json, dataModel, false); ParseAuras(json, dataModel, false);
break; break;
case "spellCast": case "spellCast":
ParseSpellCast(json, dataModel); ParseSpellCast(json, dataModel, false);
break; break;
case "instantSpellCast": case "instantSpellCast":
ParseInstantSpellCast(json, dataModel); ParseInstantSpellCast(json, dataModel);
@ -86,6 +86,12 @@ namespace Artemis.Modules.Games.WoW
case "spellCastInterrupted": case "spellCastInterrupted":
ParseSpellCastInterrupted(data, dataModel); ParseSpellCastInterrupted(data, dataModel);
break; break;
case "spellChannel":
ParseSpellCast(json, dataModel, true);
break;
case "spellChannelInterrupted":
ParseSpellCastInterrupted(data, dataModel);
break;
default: default:
Logger.Warn("The WoW addon sent an unknown command: {0}", command); Logger.Warn("The WoW addon sent an unknown command: {0}", command);
break; break;
@ -118,12 +124,12 @@ namespace Artemis.Modules.Games.WoW
dataModel.Player.ApplyAuraJson(json, buffs); dataModel.Player.ApplyAuraJson(json, buffs);
} }
private void ParseSpellCast(JToken json, WoWDataModel dataModel) private void ParseSpellCast(JToken json, WoWDataModel dataModel, bool isChannel)
{ {
if (json["uid"].Value<string>() == "player") if (json["uid"].Value<string>() == "player")
dataModel.Player.CastBar.ApplyJson(json); dataModel.Player.CastBar.ApplyJson(json, isChannel);
else if (json["uid"].Value<string>() == "target") else if (json["uid"].Value<string>() == "target")
dataModel.Target.CastBar.ApplyJson(json); dataModel.Target.CastBar.ApplyJson(json, isChannel);
} }
private void ParseInstantSpellCast(JToken json, WoWDataModel dataModel) private void ParseInstantSpellCast(JToken json, WoWDataModel dataModel)

View File

@ -1,125 +1,127 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ninject.Extensions.Logging; using Ninject.Extensions.Logging;
using PcapDotNet.Core; using PcapDotNet.Core;
using PcapDotNet.Packets; using PcapDotNet.Packets;
namespace Artemis.Modules.Games.WoW namespace Artemis.Modules.Games.WoW
{ {
public class WowPacketScanner public class WowPacketScanner
{ {
private const string MsgStart = "\u0001"; private const string MsgStart = "\u0001";
private const string MsgNext = "\u0002"; private const string MsgNext = "\u0002";
private const string MsgLast = "\u0003"; private const string MsgLast = "\u0003";
private PacketCommunicator _communicator; private PacketCommunicator _communicator;
private string _dataParts; private string _dataParts;
public WowPacketScanner(ILogger logger) public WowPacketScanner(ILogger logger)
{ {
Logger = logger; Logger = logger;
} }
public ILogger Logger { get; } public ILogger Logger { get; }
public event EventHandler<WowDataReceivedEventArgs> RaiseDataReceived; public event EventHandler<WowDataReceivedEventArgs> RaiseDataReceived;
public void Start() public void Start()
{ {
// Start scanning WoW packets // Start scanning WoW packets
// Retrieve the device list from the local machine // Retrieve the device list from the local machine
IList<LivePacketDevice> allDevices = LivePacketDevice.AllLocalMachine; IList<LivePacketDevice> allDevices = LivePacketDevice.AllLocalMachine;
if (allDevices.Count == 0) if (allDevices.Count == 0)
{ {
Logger.Warn("No interfaces found! Can't scan WoW packets."); Logger.Warn("No interfaces found! Can't scan WoW packets.");
return; return;
} }
// Take the selected adapter // Take the selected adapter
PacketDevice selectedDevice = allDevices.First(); PacketDevice selectedDevice = allDevices.First();
// Open the device // Open the device
_communicator = selectedDevice.Open(65536, PacketDeviceOpenAttributes.Promiscuous, 40); _communicator = selectedDevice.Open(65536, PacketDeviceOpenAttributes.Promiscuous, 40);
Logger.Debug("Listening on " + selectedDevice.Description + " for WoW packets"); Logger.Debug("Listening on " + selectedDevice.Description + " for WoW packets");
// Compile the filter // Compile the filter
using (var filter = _communicator.CreateFilter("tcp")) using (var filter = _communicator.CreateFilter("tcp"))
{ {
// Set the filter // Set the filter
_communicator.SetFilter(filter); _communicator.SetFilter(filter);
} }
Task.Run(() => ReceivePackets()); Task.Run(() => ReceivePackets());
} }
public void Stop() public void Stop()
{ {
_communicator?.Break(); _communicator?.Break();
_communicator?.Dispose(); _communicator?.Dispose();
_communicator = null; _communicator = null;
} }
private void ReceivePackets() private void ReceivePackets()
{ {
// start the capture // start the capture
try try
{ {
_communicator.ReceivePackets(0, PacketHandler); _communicator.ReceivePackets(0, PacketHandler);
} }
catch (InvalidOperationException) catch (InvalidOperationException)
{ {
// ignored, happens on shutdown // ignored, happens on shutdown
} }
} }
// Callback function invoked by Pcap.Net for every incoming packet // Callback function invoked by Pcap.Net for every incoming packet
private void PacketHandler(Packet packet) private void PacketHandler(Packet packet)
{ {
var str = Encoding.Default.GetString(packet.Buffer); var str = Encoding.Default.GetString(packet.Buffer);
if (!str.ToLower().Contains("artemis")) if (!str.ToLower().Contains("artemis"))
return; return;
// Split the string at the prefix // Split the string at the prefix
var parts = str.Split(new[] {"(artemis)"}, StringSplitOptions.None); var parts = str.Split(new[] {"(artemis)"}, StringSplitOptions.None);
var msg = parts[1]; if (parts.Length < 2)
// Start escape char return;
if (msg.StartsWith(MsgStart)) var msg = parts[1];
_dataParts = msg.Substring(1); // Start escape char
else if (msg.StartsWith(MsgNext)) if (msg.StartsWith(MsgStart))
_dataParts = _dataParts + msg.Substring(1); _dataParts = msg.Substring(1);
else if (msg.StartsWith(MsgLast)) else if (msg.StartsWith(MsgNext))
{ _dataParts = _dataParts + msg.Substring(1);
_dataParts = _dataParts + msg.Substring(1); else if (msg.StartsWith(MsgLast))
var dataParts = _dataParts.Split('|'); {
// Data is wrapped in artemis(), take this off _dataParts = _dataParts + msg.Substring(1);
OnRaiseDataReceived(dataParts[0].Substring(8), dataParts[1].Substring(0, dataParts[1].Length - 1)); var dataParts = _dataParts.Split('|');
} // Data is wrapped in artemis(), take this off
else OnRaiseDataReceived(dataParts[0].Substring(8), dataParts[1].Substring(0, dataParts[1].Length - 1));
{ }
var dataParts = msg.Split('|'); else
// Data is wrapped in artemis(), take this off {
OnRaiseDataReceived(dataParts[0].Substring(8), dataParts[1].Substring(0, dataParts[1].Length - 1)); var dataParts = msg.Split('|');
} // Data is wrapped in artemis(), take this off
} OnRaiseDataReceived(dataParts[0].Substring(8), dataParts[1].Substring(0, dataParts[1].Length - 1));
}
private void OnRaiseDataReceived(string command, string data) }
{
RaiseDataReceived?.Invoke(this, new WowDataReceivedEventArgs(command, data)); private void OnRaiseDataReceived(string command, string data)
} {
RaiseDataReceived?.Invoke(this, new WowDataReceivedEventArgs(command, data));
public class WowDataReceivedEventArgs : EventArgs }
{
public WowDataReceivedEventArgs(string command, string data) public class WowDataReceivedEventArgs : EventArgs
{ {
Command = command; public WowDataReceivedEventArgs(string command, string data)
Data = data; {
} Command = command;
Data = data;
public string Command { get; } }
public string Data { get; set; }
} public string Command { get; }
} public string Data { get; set; }
} }
}
}