mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Added WoW addon source
Moved to using the addon channel and splitting up >255 char messages
This commit is contained in:
parent
53dce2c4a5
commit
46a25fd597
@ -410,6 +410,7 @@
|
||||
<Compile Include="Modules\Games\WoW\Models\WoWSpecialization.cs" />
|
||||
<Compile Include="Modules\Games\WoW\Models\WoWSpell.cs" />
|
||||
<Compile Include="Modules\Games\WoW\Models\WoWUnit.cs" />
|
||||
<Compile Include="Modules\Games\WoW\WoWPacketScanner.cs" />
|
||||
<Compile Include="Modules\General\GeneralProfile\PerformanceInfo.cs" />
|
||||
<Compile Include="Modules\Games\EurotruckSimulator2\Data\Ets2TelemetryData.cs" />
|
||||
<Compile Include="Modules\Games\EurotruckSimulator2\Data\Ets2TelemetryDataReader.cs" />
|
||||
@ -811,6 +812,16 @@
|
||||
<Compile Include="Properties\AssemblyInfo.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<None Include="Modules\Games\WoW\Resources\Addon\Artemis.toc" />
|
||||
<None Include="Modules\Games\WoW\Resources\Addon\Core.lua" />
|
||||
<None Include="Modules\Games\WoW\Resources\Addon\Libs\AceAddon-3.0\AceAddon-3.0.lua" />
|
||||
<None Include="Modules\Games\WoW\Resources\Addon\Libs\AceComm-3.0\AceComm-3.0.lua" />
|
||||
<None Include="Modules\Games\WoW\Resources\Addon\Libs\AceComm-3.0\ChatThrottleLib.lua" />
|
||||
<None Include="Modules\Games\WoW\Resources\Addon\Libs\AceConsole-3.0\AceConsole-3.0.lua" />
|
||||
<None Include="Modules\Games\WoW\Resources\Addon\Libs\AceEvent-3.0\AceEvent-3.0.lua" />
|
||||
<None Include="Modules\Games\WoW\Resources\Addon\Libs\AceTimer-3.0\AceTimer-3.0.lua" />
|
||||
<None Include="Modules\Games\WoW\Resources\Addon\Libs\json.lua" />
|
||||
<None Include="Modules\Games\WoW\Resources\Addon\Libs\LibStub\LibStub.lua" />
|
||||
<None Include="NLog.xsd">
|
||||
<SubType>Designer</SubType>
|
||||
</None>
|
||||
@ -1085,6 +1096,12 @@
|
||||
<None Include="Modules\Games\EurotruckSimulator2\Resources\Win64\ets2-telemetry-server.dll" />
|
||||
<None Include="Resources\audio.png" />
|
||||
<None Include="Resources\ambilight.png" />
|
||||
<Resource Include="Modules\Games\WoW\Resources\Addon\embeds.xml" />
|
||||
<Resource Include="Modules\Games\WoW\Resources\Addon\Libs\AceAddon-3.0\AceAddon-3.0.xml" />
|
||||
<Resource Include="Modules\Games\WoW\Resources\Addon\Libs\AceComm-3.0\AceComm-3.0.xml" />
|
||||
<Resource Include="Modules\Games\WoW\Resources\Addon\Libs\AceConsole-3.0\AceConsole-3.0.xml" />
|
||||
<Resource Include="Modules\Games\WoW\Resources\Addon\Libs\AceEvent-3.0\AceEvent-3.0.xml" />
|
||||
<Resource Include="Modules\Games\WoW\Resources\Addon\Libs\AceTimer-3.0\AceTimer-3.0.xml" />
|
||||
<Content Include="Resources\CounterStrike\csgoGamestateConfiguration.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using Artemis.Managers;
|
||||
using Artemis.Models;
|
||||
using Artemis.Modules.Abstract;
|
||||
using Artemis.Modules.Games.WoW;
|
||||
using Artemis.Profiles.Layers.Abstract;
|
||||
using Artemis.Profiles.Layers.Interfaces;
|
||||
using Artemis.Profiles.Layers.Types.Audio.AudioCapturing;
|
||||
@ -65,7 +66,7 @@ namespace Artemis.InjectionModules
|
||||
|
||||
#endregion
|
||||
|
||||
#region Effects
|
||||
#region Modules
|
||||
|
||||
Kernel.Bind(x =>
|
||||
x.FromThisAssembly()
|
||||
@ -81,6 +82,7 @@ namespace Artemis.InjectionModules
|
||||
.BindAllBaseClasses()
|
||||
.Configure(b => b.InSingletonScope())
|
||||
);
|
||||
Bind<WowPacketScanner>().ToSelf();
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@ -9,17 +9,16 @@ namespace Artemis.Modules.Games.WoW.Models
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string Caster { get; set; }
|
||||
public int Stacks { get; set; }
|
||||
public DateTime StartTime { set; get; }
|
||||
public DateTime EndTime { get; set; }
|
||||
|
||||
public void ApplyJson(JToken buffJson)
|
||||
{
|
||||
Name = buffJson["name"].Value<string>();
|
||||
Id = buffJson["spellID"].Value<int>();
|
||||
Stacks = buffJson["count"].Value<int>();
|
||||
Caster = buffJson["caster"]?.Value<string>();
|
||||
Name = buffJson["n"].Value<string>();
|
||||
Id = buffJson["id"].Value<int>();
|
||||
if (buffJson["c"] != null)
|
||||
Stacks = buffJson["c"].Value<int>();
|
||||
|
||||
// TODO: Duration
|
||||
}
|
||||
|
||||
@ -20,15 +20,15 @@ namespace Artemis.Modules.Games.WoW.Models
|
||||
|
||||
public void ApplyJson(JToken spellJson)
|
||||
{
|
||||
var castMs = spellJson["endTime"].Value<int>() - spellJson["startTime"].Value<int>();
|
||||
var castMs = spellJson["e"].Value<int>() - spellJson["s"].Value<int>();
|
||||
var tickCount = Environment.TickCount;
|
||||
var difference = tickCount - spellJson["startTime"].Value<int>();
|
||||
var difference = tickCount - spellJson["s"].Value<int>();
|
||||
|
||||
Spell.Name = spellJson["name"].Value<string>();
|
||||
Spell.Id = spellJson["spellID"].Value<int>();
|
||||
Spell.Name = spellJson["n"].Value<string>();
|
||||
Spell.Id = spellJson["sid"].Value<int>();
|
||||
StartTime = new DateTime(DateTime.Now.Ticks + difference);
|
||||
EndTime = StartTime.AddMilliseconds(castMs);
|
||||
NonInterruptible = spellJson["notInterruptible"].Value<bool>();
|
||||
NonInterruptible = spellJson["ni"].Value<bool>();
|
||||
}
|
||||
|
||||
public void UpdateProgress()
|
||||
|
||||
@ -12,9 +12,9 @@ namespace Artemis.Modules.Games.WoW.Models
|
||||
|
||||
public void ApplyJson(JToken specJson)
|
||||
{
|
||||
Name = specJson["name"].Value<string>();
|
||||
Name = specJson["n"].Value<string>();
|
||||
Id = specJson["id"].Value<int>();
|
||||
Role = specJson["role"].Value<string>();
|
||||
Role = specJson["r"].Value<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,47 +39,47 @@ namespace Artemis.Modules.Games.WoW.Models
|
||||
public List<WoWAura> Debuffs { get; }
|
||||
public List<WoWSpell> RecentIntantCasts { get; private set; }
|
||||
|
||||
public void ApplyJson(JObject json)
|
||||
public void ApplyJson(JToken json)
|
||||
{
|
||||
if (json["name"] == null)
|
||||
if (json["n"] == null)
|
||||
return;
|
||||
|
||||
Name = json["name"].Value<string>();
|
||||
Level = json["level"].Value<int>();
|
||||
Class = json["class"].Value<string>();
|
||||
Gender = json["gender"].Value<int>() == 3 ? WoWGender.Female : WoWGender.Male;
|
||||
Name = json["n"].Value<string>();
|
||||
Level = json["l"].Value<int>();
|
||||
Class = json["c"].Value<string>();
|
||||
Gender = json["g"].Value<int>() == 3 ? WoWGender.Female : WoWGender.Male;
|
||||
|
||||
if (json["race"] != null)
|
||||
Race = GeneralHelpers.ParseEnum<WoWRace>(json["race"].Value<string>());
|
||||
if (json["specialization"] != null)
|
||||
Specialization.ApplyJson(json["specialization"]);
|
||||
if (json["r"] != null)
|
||||
Race = GeneralHelpers.ParseEnum<WoWRace>(json["r"].Value<string>());
|
||||
if (json["s"] != null)
|
||||
Specialization.ApplyJson(json["s"]);
|
||||
}
|
||||
|
||||
public void ApplyStateJson(JObject json)
|
||||
public void ApplyStateJson(JToken json)
|
||||
{
|
||||
Health = json["health"].Value<int>();
|
||||
MaxHealth = json["maxHealth"].Value<int>();
|
||||
PowerType = GeneralHelpers.ParseEnum<WoWPowerType>(json["powerType"].Value<int>().ToString());
|
||||
Power = json["power"].Value<int>();
|
||||
MaxPower = json["maxPower"].Value<int>();
|
||||
Health = json["h"].Value<int>();
|
||||
MaxHealth = json["mh"].Value<int>();
|
||||
PowerType = GeneralHelpers.ParseEnum<WoWPowerType>(json["t"].Value<int>().ToString());
|
||||
Power = json["p"].Value<int>();
|
||||
MaxPower = json["mp"].Value<int>();
|
||||
}
|
||||
|
||||
public void ApplyAuraJson(JObject json)
|
||||
public void ApplyAuraJson(JToken json, bool buffs)
|
||||
{
|
||||
Buffs.Clear();
|
||||
if (json["buffs"] != null)
|
||||
if (buffs)
|
||||
{
|
||||
foreach (var auraJson in json["buffs"].Children())
|
||||
Buffs.Clear();
|
||||
foreach (var auraJson in json.Children())
|
||||
{
|
||||
var aura = new WoWAura();
|
||||
aura.ApplyJson(auraJson);
|
||||
Buffs.Add(aura);
|
||||
}
|
||||
}
|
||||
Debuffs.Clear();
|
||||
if (json["debuffs"] != null)
|
||||
else
|
||||
{
|
||||
foreach (var auraJson in json["debuffs"].Children())
|
||||
Debuffs.Clear();
|
||||
foreach (var auraJson in json.Children())
|
||||
{
|
||||
var aura = new WoWAura();
|
||||
aura.ApplyJson(auraJson);
|
||||
@ -103,7 +103,7 @@ namespace Artemis.Modules.Games.WoW.Models
|
||||
// Remove all casts that weren't cast in the after the last frame
|
||||
RecentIntantCasts.Clear();
|
||||
RecentIntantCasts.AddRange(_currentFrameCasts);
|
||||
|
||||
|
||||
// Clear the that were after the last frame so that they are removed next frame when this method is called again
|
||||
_currentFrameCasts.Clear();
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
## Interface: 70300
|
||||
## Title: Artemis
|
||||
## Notes: Transmits ingame data to Artemis
|
||||
## Author: SpoinkyNL
|
||||
## Version: 1.0.0
|
||||
|
||||
embeds.xml
|
||||
|
||||
Core.lua
|
||||
297
Artemis/Artemis/Modules/Games/WoW/Resources/Addon/Core.lua
Normal file
297
Artemis/Artemis/Modules/Games/WoW/Resources/Addon/Core.lua
Normal file
@ -0,0 +1,297 @@
|
||||
Artemis = LibStub("AceAddon-3.0"):NewAddon("Artemis", "AceConsole-3.0", "AceEvent-3.0", "AceTimer-3.0", "AceComm-3.0")
|
||||
json = LibStub("json")
|
||||
local debugging = false
|
||||
local lastLine = {}
|
||||
local unitUpdates = {}
|
||||
local lastTransmitMessage
|
||||
local lastTransmitTime
|
||||
local lastBuffs = "";
|
||||
local lastDebuffs = "";
|
||||
local prefixCounts = {}
|
||||
|
||||
function Artemis:OnEnable()
|
||||
Artemis:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
Artemis:RegisterEvent("PLAYER_LEVEL_UP")
|
||||
Artemis:RegisterEvent("ACHIEVEMENT_EARNED")
|
||||
Artemis:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
|
||||
Artemis:RegisterEvent("UNIT_TARGET")
|
||||
Artemis:RegisterEvent("UNIT_HEALTH")
|
||||
Artemis:RegisterEvent("UNIT_POWER")
|
||||
Artemis:RegisterEvent("UNIT_AURA")
|
||||
Artemis:RegisterEvent("UNIT_SPELLCAST_START")
|
||||
Artemis:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
|
||||
Artemis:RegisterEvent("UNIT_SPELLCAST_FAILED")
|
||||
Artemis:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
|
||||
Artemis:RegisterEvent("ZONE_CHANGED")
|
||||
Artemis:RegisterEvent("ZONE_CHANGED_NEW_AREA")
|
||||
|
||||
Artemis:RegisterChatCommand("artemis", "HandleChatCommand")
|
||||
end
|
||||
|
||||
function Artemis:HandleChatCommand(input)
|
||||
if input == "debug" then
|
||||
debugging = not (debugging)
|
||||
if debugging then
|
||||
Artemis:Print("Debugging enabled.")
|
||||
else
|
||||
Artemis:Print("Debugging disabled.")
|
||||
end
|
||||
end
|
||||
if input == "rc" then
|
||||
prefixCounts = {}
|
||||
Artemis:Print("Reset the send counters.")
|
||||
end
|
||||
if input == nill or input == "" or input == "help" then
|
||||
Artemis:Print("Available chat commands:")
|
||||
Artemis:Printf("|cffb7b7b7/artemis debug|r: Toggle debugging")
|
||||
Artemis:Printf("|cffb7b7b7/artemis rc|r: Reset the debug counters")
|
||||
end
|
||||
end
|
||||
|
||||
function Artemis:Transmit(prefix, data, prio)
|
||||
local msg = "artemis(".. prefix .. "|" .. json.encode(data) ..")"
|
||||
-- If the message is the same as the previous, make sure it wasn't sent less than 250ms ago
|
||||
if msg == lastTransmitMessage then
|
||||
if not (lastTransmitTime == nil) then
|
||||
local diff = GetTime() - lastTransmitTime;
|
||||
if (diff < 0.25) then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
lastTransmitTime = GetTime()
|
||||
|
||||
if debugging == true then
|
||||
if prefixCounts[prefix] == nill then
|
||||
prefixCounts[prefix] = 0
|
||||
end
|
||||
prefixCounts[prefix] = prefixCounts[prefix] + 1
|
||||
end
|
||||
|
||||
if debugging == true then
|
||||
Artemis:Printf("Transmitting with prefix |cfffdff71" .. prefix .. "|r (" .. prefixCounts[prefix] .. ").")
|
||||
Artemis:Print(msg)
|
||||
end
|
||||
Artemis:SendCommMessage("(artemis)", msg, "WHISPER", UnitName("player"), prio)
|
||||
end
|
||||
|
||||
function Artemis:TransmitUnitState(unit, ignoreThrottle)
|
||||
if not ignoreThrottle then
|
||||
if not (unitUpdates[unit] == nil) then
|
||||
local diff = GetTime() - unitUpdates[unit]
|
||||
if (diff < 0.5) then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local table = {
|
||||
h = UnitHealth(unit),
|
||||
mh = UnitHealthMax(unit),
|
||||
p = UnitPower(unit),
|
||||
mp = UnitPowerMax(unit),
|
||||
t = UnitPowerType(unit)
|
||||
};
|
||||
|
||||
unitUpdates[unit] = GetTime()
|
||||
Artemis:Transmit(unit .. "State", table)
|
||||
end
|
||||
|
||||
function Artemis:GetUnitDetails(unit)
|
||||
return {
|
||||
n = UnitName(unit),
|
||||
c = UnitClass(unit),
|
||||
l = UnitLevel(unit),
|
||||
r = UnitRace(unit),
|
||||
g = UnitSex(unit),
|
||||
f = UnitFactionGroup(unit)
|
||||
};
|
||||
end
|
||||
|
||||
function Artemis:GetPlayerDetails()
|
||||
local details = Artemis:GetUnitDetails("player")
|
||||
local id, name, _, _, role = GetSpecializationInfo(GetSpecialization())
|
||||
|
||||
details.realm = GetRealmName()
|
||||
details.achievementPoints = GetTotalAchievementPoints(false)
|
||||
details.s = {id = id, n = name, r = role}
|
||||
|
||||
return details
|
||||
end
|
||||
|
||||
function Artemis:GetUnitAuras(unit, filter)
|
||||
local auras = {};
|
||||
for index = 1, 40 do
|
||||
local name, _, _, count, _, duration, expires, caster, _, _, spellID = UnitAura(unit, index, filter);
|
||||
if not (name == nil) then
|
||||
local buffTable = {n = name, id = spellID}
|
||||
-- Leave these values out if they are 0 to save some space
|
||||
if count > 0 then
|
||||
buffTable["c"] = count
|
||||
end
|
||||
if duration > 0 then
|
||||
buffTable["d"] = duration
|
||||
end
|
||||
if expires > 0 then
|
||||
buffTable["e"] = expires
|
||||
end
|
||||
table.insert(auras, buffTable)
|
||||
end
|
||||
end
|
||||
return auras
|
||||
end
|
||||
|
||||
function Artemis:PLAYER_ENTERING_WORLD(...)
|
||||
Artemis:Transmit("player", Artemis:GetPlayerDetails())
|
||||
Artemis:TransmitUnitState("player", true);
|
||||
end
|
||||
|
||||
function Artemis:PLAYER_LEVEL_UP(...)
|
||||
Artemis:Transmit("player", Artemis:GetPlayerDetails())
|
||||
end
|
||||
|
||||
function Artemis:ACHIEVEMENT_EARNED(...)
|
||||
Artemis:Transmit("player", Artemis:GetPlayerDetails())
|
||||
end
|
||||
|
||||
function Artemis:ACTIVE_TALENT_GROUP_CHANGED(...)
|
||||
Artemis:Transmit("player", Artemis:GetPlayerDetails())
|
||||
end
|
||||
|
||||
function Artemis:UNIT_TARGET(...)
|
||||
local _, source = ...
|
||||
if not (source == "player") then
|
||||
return
|
||||
end
|
||||
|
||||
local details = Artemis:GetUnitDetails("target")
|
||||
|
||||
Artemis:Transmit("target", details)
|
||||
Artemis:TransmitUnitState("target", true);
|
||||
end
|
||||
|
||||
function Artemis:UNIT_HEALTH(...)
|
||||
local _, source = ...
|
||||
if not (source == "player") and not (source == "target") then
|
||||
return
|
||||
end
|
||||
|
||||
Artemis:TransmitUnitState(source, false);
|
||||
end
|
||||
|
||||
function Artemis:UNIT_POWER(...)
|
||||
local _, source = ...
|
||||
if not (source == "player") and not (source == "target") then
|
||||
return
|
||||
end
|
||||
|
||||
Artemis:TransmitUnitState(source, false);
|
||||
end
|
||||
|
||||
function Artemis:UNIT_AURA(...)
|
||||
local _, source = ...
|
||||
if not (source == "player") then
|
||||
return
|
||||
end
|
||||
|
||||
local buffs = Artemis:GetUnitAuras(source, "PLAYER|HELPFUL")
|
||||
local debuffs = Artemis:GetUnitAuras(source, "PLAYER|HARMFUL")
|
||||
|
||||
local newBuffs = json.encode(buffs)
|
||||
local newDebuffs = json.encode(debuffs)
|
||||
|
||||
if not (lastBuffs == newBuffs) then
|
||||
Artemis:Transmit("buffs", buffs)
|
||||
end
|
||||
if not (lastDebuffs == newDebuffs) then
|
||||
Artemis:Transmit("debuffs", debuffs)
|
||||
end
|
||||
|
||||
lastBuffs = newBuffs
|
||||
lastDebuffs = newDebuffs
|
||||
end
|
||||
|
||||
-- Detect non-instant spell casts
|
||||
function Artemis:UNIT_SPELLCAST_START(...)
|
||||
local _, unitID, spell, rank, lineID, spellID = ...
|
||||
if not (unitID == "player") and not (unitID == "target") then
|
||||
return
|
||||
end
|
||||
|
||||
local name, _, _, _, startTime, endTime, _, _, notInterruptible = UnitCastingInfo(unitID)
|
||||
local table = {uid = unitID, n = name, sid = spellID, s = startTime, e = endTime, ni = notInterruptible}
|
||||
lastLine[unitID] = lineID
|
||||
|
||||
Artemis:Transmit("spellCast", table, "ALERT")
|
||||
end
|
||||
|
||||
-- Detect instant spell casts
|
||||
function Artemis:UNIT_SPELLCAST_SUCCEEDED (...)
|
||||
local _, unitID, spell, rank, lineID, spellID = ...
|
||||
if not (unitID == "player") and not (unitID == "target") then
|
||||
return
|
||||
end
|
||||
-- Many spells are irrelevant system spells, don't transmit these
|
||||
if unitID == "player" and not (IsPlayerSpell(spellID)) then
|
||||
return
|
||||
end
|
||||
|
||||
local name, subText, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible = UnitCastingInfo(unitID)
|
||||
-- Don't trigger on the success of a non instant cast
|
||||
if not (lastLine[unitID] == nil) and lastLine[unitID] == lineID then
|
||||
return
|
||||
end
|
||||
|
||||
-- Set back the last line to what is currently being cast (Fireblast during Fireball per example)
|
||||
if not (name == nil) then
|
||||
lastLine[unitID] = castID
|
||||
else
|
||||
lastLine[unitID] = nil
|
||||
end
|
||||
|
||||
local table = {uid = unitID, n = spell, sid = spellID}
|
||||
|
||||
Artemis:Transmit("instantSpellCast", table, "ALERT")
|
||||
end
|
||||
|
||||
-- Detect falure of non instant casts
|
||||
function Artemis:UNIT_SPELLCAST_FAILED (...)
|
||||
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("spellCastFailed", unitID, "ALERT")
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
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
|
||||
@ -0,0 +1,674 @@
|
||||
--- **AceAddon-3.0** provides a template for creating addon objects.
|
||||
-- It'll provide you with a set of callback functions that allow you to simplify the loading
|
||||
-- process of your addon.\\
|
||||
-- Callbacks provided are:\\
|
||||
-- * **OnInitialize**, which is called directly after the addon is fully loaded.
|
||||
-- * **OnEnable** which gets called during the PLAYER_LOGIN event, when most of the data provided by the game is already present.
|
||||
-- * **OnDisable**, which is only called when your addon is manually being disabled.
|
||||
-- @usage
|
||||
-- -- A small (but complete) addon, that doesn't do anything,
|
||||
-- -- but shows usage of the callbacks.
|
||||
-- local MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon")
|
||||
--
|
||||
-- function MyAddon:OnInitialize()
|
||||
-- -- do init tasks here, like loading the Saved Variables,
|
||||
-- -- or setting up slash commands.
|
||||
-- end
|
||||
--
|
||||
-- function MyAddon:OnEnable()
|
||||
-- -- Do more initialization here, that really enables the use of your addon.
|
||||
-- -- Register Events, Hook functions, Create Frames, Get information from
|
||||
-- -- the game that wasn't available in OnInitialize
|
||||
-- end
|
||||
--
|
||||
-- function MyAddon:OnDisable()
|
||||
-- -- Unhook, Unregister Events, Hide frames that you created.
|
||||
-- -- You would probably only use an OnDisable if you want to
|
||||
-- -- build a "standby" mode, or be able to toggle modules on/off.
|
||||
-- end
|
||||
-- @class file
|
||||
-- @name AceAddon-3.0.lua
|
||||
-- @release $Id: AceAddon-3.0.lua 1084 2013-04-27 20:14:11Z nevcairiel $
|
||||
|
||||
local MAJOR, MINOR = "AceAddon-3.0", 12
|
||||
local AceAddon, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceAddon then return end -- No Upgrade needed.
|
||||
|
||||
AceAddon.frame = AceAddon.frame or CreateFrame("Frame", "AceAddon30Frame") -- Our very own frame
|
||||
AceAddon.addons = AceAddon.addons or {} -- addons in general
|
||||
AceAddon.statuses = AceAddon.statuses or {} -- statuses of addon.
|
||||
AceAddon.initializequeue = AceAddon.initializequeue or {} -- addons that are new and not initialized
|
||||
AceAddon.enablequeue = AceAddon.enablequeue or {} -- addons that are initialized and waiting to be enabled
|
||||
AceAddon.embeds = AceAddon.embeds or setmetatable({}, {__index = function(tbl, key) tbl[key] = {} return tbl[key] end }) -- contains a list of libraries embedded in an addon
|
||||
|
||||
-- Lua APIs
|
||||
local tinsert, tconcat, tremove = table.insert, table.concat, table.remove
|
||||
local fmt, tostring = string.format, tostring
|
||||
local select, pairs, next, type, unpack = select, pairs, next, type, unpack
|
||||
local loadstring, assert, error = loadstring, assert, error
|
||||
local setmetatable, getmetatable, rawset, rawget = setmetatable, getmetatable, rawset, rawget
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: LibStub, IsLoggedIn, geterrorhandler
|
||||
|
||||
--[[
|
||||
xpcall safecall implementation
|
||||
]]
|
||||
local xpcall = xpcall
|
||||
|
||||
local function errorhandler(err)
|
||||
return geterrorhandler()(err)
|
||||
end
|
||||
|
||||
local function CreateDispatcher(argCount)
|
||||
local code = [[
|
||||
local xpcall, eh = ...
|
||||
local method, ARGS
|
||||
local function call() return method(ARGS) end
|
||||
|
||||
local function dispatch(func, ...)
|
||||
method = func
|
||||
if not method then return end
|
||||
ARGS = ...
|
||||
return xpcall(call, eh)
|
||||
end
|
||||
|
||||
return dispatch
|
||||
]]
|
||||
|
||||
local ARGS = {}
|
||||
for i = 1, argCount do ARGS[i] = "arg"..i end
|
||||
code = code:gsub("ARGS", tconcat(ARGS, ", "))
|
||||
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler)
|
||||
end
|
||||
|
||||
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
|
||||
local dispatcher = CreateDispatcher(argCount)
|
||||
rawset(self, argCount, dispatcher)
|
||||
return dispatcher
|
||||
end})
|
||||
Dispatchers[0] = function(func)
|
||||
return xpcall(func, errorhandler)
|
||||
end
|
||||
|
||||
local function safecall(func, ...)
|
||||
-- we check to see if the func is passed is actually a function here and don't error when it isn't
|
||||
-- this safecall is used for optional functions like OnInitialize OnEnable etc. When they are not
|
||||
-- present execution should continue without hinderance
|
||||
if type(func) == "function" then
|
||||
return Dispatchers[select('#', ...)](func, ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- local functions that will be implemented further down
|
||||
local Enable, Disable, EnableModule, DisableModule, Embed, NewModule, GetModule, GetName, SetDefaultModuleState, SetDefaultModuleLibraries, SetEnabledState, SetDefaultModulePrototype
|
||||
|
||||
-- used in the addon metatable
|
||||
local function addontostring( self ) return self.name end
|
||||
|
||||
-- Check if the addon is queued for initialization
|
||||
local function queuedForInitialization(addon)
|
||||
for i = 1, #AceAddon.initializequeue do
|
||||
if AceAddon.initializequeue[i] == addon then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Create a new AceAddon-3.0 addon.
|
||||
-- Any libraries you specified will be embeded, and the addon will be scheduled for
|
||||
-- its OnInitialize and OnEnable callbacks.
|
||||
-- The final addon object, with all libraries embeded, will be returned.
|
||||
-- @paramsig [object ,]name[, lib, ...]
|
||||
-- @param object Table to use as a base for the addon (optional)
|
||||
-- @param name Name of the addon object to create
|
||||
-- @param lib List of libraries to embed into the addon
|
||||
-- @usage
|
||||
-- -- Create a simple addon object
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon", "AceEvent-3.0")
|
||||
--
|
||||
-- -- Create a Addon object based on the table of a frame
|
||||
-- local MyFrame = CreateFrame("Frame")
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon(MyFrame, "MyAddon", "AceEvent-3.0")
|
||||
function AceAddon:NewAddon(objectorname, ...)
|
||||
local object,name
|
||||
local i=1
|
||||
if type(objectorname)=="table" then
|
||||
object=objectorname
|
||||
name=...
|
||||
i=2
|
||||
else
|
||||
name=objectorname
|
||||
end
|
||||
if type(name)~="string" then
|
||||
error(("Usage: NewAddon([object,] name, [lib, lib, lib, ...]): 'name' - string expected got '%s'."):format(type(name)), 2)
|
||||
end
|
||||
if self.addons[name] then
|
||||
error(("Usage: NewAddon([object,] name, [lib, lib, lib, ...]): 'name' - Addon '%s' already exists."):format(name), 2)
|
||||
end
|
||||
|
||||
object = object or {}
|
||||
object.name = name
|
||||
|
||||
local addonmeta = {}
|
||||
local oldmeta = getmetatable(object)
|
||||
if oldmeta then
|
||||
for k, v in pairs(oldmeta) do addonmeta[k] = v end
|
||||
end
|
||||
addonmeta.__tostring = addontostring
|
||||
|
||||
setmetatable( object, addonmeta )
|
||||
self.addons[name] = object
|
||||
object.modules = {}
|
||||
object.orderedModules = {}
|
||||
object.defaultModuleLibraries = {}
|
||||
Embed( object ) -- embed NewModule, GetModule methods
|
||||
self:EmbedLibraries(object, select(i,...))
|
||||
|
||||
-- add to queue of addons to be initialized upon ADDON_LOADED
|
||||
tinsert(self.initializequeue, object)
|
||||
return object
|
||||
end
|
||||
|
||||
|
||||
--- Get the addon object by its name from the internal AceAddon registry.
|
||||
-- Throws an error if the addon object cannot be found (except if silent is set).
|
||||
-- @param name unique name of the addon object
|
||||
-- @param silent if true, the addon is optional, silently return nil if its not found
|
||||
-- @usage
|
||||
-- -- Get the Addon
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
|
||||
function AceAddon:GetAddon(name, silent)
|
||||
if not silent and not self.addons[name] then
|
||||
error(("Usage: GetAddon(name): 'name' - Cannot find an AceAddon '%s'."):format(tostring(name)), 2)
|
||||
end
|
||||
return self.addons[name]
|
||||
end
|
||||
|
||||
-- - Embed a list of libraries into the specified addon.
|
||||
-- This function will try to embed all of the listed libraries into the addon
|
||||
-- and error if a single one fails.
|
||||
--
|
||||
-- **Note:** This function is for internal use by :NewAddon/:NewModule
|
||||
-- @paramsig addon, [lib, ...]
|
||||
-- @param addon addon object to embed the libs in
|
||||
-- @param lib List of libraries to embed into the addon
|
||||
function AceAddon:EmbedLibraries(addon, ...)
|
||||
for i=1,select("#", ... ) do
|
||||
local libname = select(i, ...)
|
||||
self:EmbedLibrary(addon, libname, false, 4)
|
||||
end
|
||||
end
|
||||
|
||||
-- - Embed a library into the addon object.
|
||||
-- This function will check if the specified library is registered with LibStub
|
||||
-- and if it has a :Embed function to call. It'll error if any of those conditions
|
||||
-- fails.
|
||||
--
|
||||
-- **Note:** This function is for internal use by :EmbedLibraries
|
||||
-- @paramsig addon, libname[, silent[, offset]]
|
||||
-- @param addon addon object to embed the library in
|
||||
-- @param libname name of the library to embed
|
||||
-- @param silent marks an embed to fail silently if the library doesn't exist (optional)
|
||||
-- @param offset will push the error messages back to said offset, defaults to 2 (optional)
|
||||
function AceAddon:EmbedLibrary(addon, libname, silent, offset)
|
||||
local lib = LibStub:GetLibrary(libname, true)
|
||||
if not lib and not silent then
|
||||
error(("Usage: EmbedLibrary(addon, libname, silent, offset): 'libname' - Cannot find a library instance of %q."):format(tostring(libname)), offset or 2)
|
||||
elseif lib and type(lib.Embed) == "function" then
|
||||
lib:Embed(addon)
|
||||
tinsert(self.embeds[addon], libname)
|
||||
return true
|
||||
elseif lib then
|
||||
error(("Usage: EmbedLibrary(addon, libname, silent, offset): 'libname' - Library '%s' is not Embed capable"):format(libname), offset or 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- Return the specified module from an addon object.
|
||||
-- Throws an error if the addon object cannot be found (except if silent is set)
|
||||
-- @name //addon//:GetModule
|
||||
-- @paramsig name[, silent]
|
||||
-- @param name unique name of the module
|
||||
-- @param silent if true, the module is optional, silently return nil if its not found (optional)
|
||||
-- @usage
|
||||
-- -- Get the Addon
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
|
||||
-- -- Get the Module
|
||||
-- MyModule = MyAddon:GetModule("MyModule")
|
||||
function GetModule(self, name, silent)
|
||||
if not self.modules[name] and not silent then
|
||||
error(("Usage: GetModule(name, silent): 'name' - Cannot find module '%s'."):format(tostring(name)), 2)
|
||||
end
|
||||
return self.modules[name]
|
||||
end
|
||||
|
||||
local function IsModuleTrue(self) return true end
|
||||
|
||||
--- Create a new module for the addon.
|
||||
-- The new module can have its own embeded libraries and/or use a module prototype to be mixed into the module.\\
|
||||
-- A module has the same functionality as a real addon, it can have modules of its own, and has the same API as
|
||||
-- an addon object.
|
||||
-- @name //addon//:NewModule
|
||||
-- @paramsig name[, prototype|lib[, lib, ...]]
|
||||
-- @param name unique name of the module
|
||||
-- @param prototype object to derive this module from, methods and values from this table will be mixed into the module (optional)
|
||||
-- @param lib List of libraries to embed into the addon
|
||||
-- @usage
|
||||
-- -- Create a module with some embeded libraries
|
||||
-- MyModule = MyAddon:NewModule("MyModule", "AceEvent-3.0", "AceHook-3.0")
|
||||
--
|
||||
-- -- Create a module with a prototype
|
||||
-- local prototype = { OnEnable = function(self) print("OnEnable called!") end }
|
||||
-- MyModule = MyAddon:NewModule("MyModule", prototype, "AceEvent-3.0", "AceHook-3.0")
|
||||
function NewModule(self, name, prototype, ...)
|
||||
if type(name) ~= "string" then error(("Usage: NewModule(name, [prototype, [lib, lib, lib, ...]): 'name' - string expected got '%s'."):format(type(name)), 2) end
|
||||
if type(prototype) ~= "string" and type(prototype) ~= "table" and type(prototype) ~= "nil" then error(("Usage: NewModule(name, [prototype, [lib, lib, lib, ...]): 'prototype' - table (prototype), string (lib) or nil expected got '%s'."):format(type(prototype)), 2) end
|
||||
|
||||
if self.modules[name] then error(("Usage: NewModule(name, [prototype, [lib, lib, lib, ...]): 'name' - Module '%s' already exists."):format(name), 2) end
|
||||
|
||||
-- modules are basically addons. We treat them as such. They will be added to the initializequeue properly as well.
|
||||
-- NewModule can only be called after the parent addon is present thus the modules will be initialized after their parent is.
|
||||
local module = AceAddon:NewAddon(fmt("%s_%s", self.name or tostring(self), name))
|
||||
|
||||
module.IsModule = IsModuleTrue
|
||||
module:SetEnabledState(self.defaultModuleState)
|
||||
module.moduleName = name
|
||||
|
||||
if type(prototype) == "string" then
|
||||
AceAddon:EmbedLibraries(module, prototype, ...)
|
||||
else
|
||||
AceAddon:EmbedLibraries(module, ...)
|
||||
end
|
||||
AceAddon:EmbedLibraries(module, unpack(self.defaultModuleLibraries))
|
||||
|
||||
if not prototype or type(prototype) == "string" then
|
||||
prototype = self.defaultModulePrototype or nil
|
||||
end
|
||||
|
||||
if type(prototype) == "table" then
|
||||
local mt = getmetatable(module)
|
||||
mt.__index = prototype
|
||||
setmetatable(module, mt) -- More of a Base class type feel.
|
||||
end
|
||||
|
||||
safecall(self.OnModuleCreated, self, module) -- Was in Ace2 and I think it could be a cool thing to have handy.
|
||||
self.modules[name] = module
|
||||
tinsert(self.orderedModules, module)
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
--- Returns the real name of the addon or module, without any prefix.
|
||||
-- @name //addon//:GetName
|
||||
-- @paramsig
|
||||
-- @usage
|
||||
-- print(MyAddon:GetName())
|
||||
-- -- prints "MyAddon"
|
||||
function GetName(self)
|
||||
return self.moduleName or self.name
|
||||
end
|
||||
|
||||
--- Enables the Addon, if possible, return true or false depending on success.
|
||||
-- This internally calls AceAddon:EnableAddon(), thus dispatching a OnEnable callback
|
||||
-- and enabling all modules of the addon (unless explicitly disabled).\\
|
||||
-- :Enable() also sets the internal `enableState` variable to true
|
||||
-- @name //addon//:Enable
|
||||
-- @paramsig
|
||||
-- @usage
|
||||
-- -- Enable MyModule
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
|
||||
-- MyModule = MyAddon:GetModule("MyModule")
|
||||
-- MyModule:Enable()
|
||||
function Enable(self)
|
||||
self:SetEnabledState(true)
|
||||
|
||||
-- nevcairiel 2013-04-27: don't enable an addon/module if its queued for init still
|
||||
-- it'll be enabled after the init process
|
||||
if not queuedForInitialization(self) then
|
||||
return AceAddon:EnableAddon(self)
|
||||
end
|
||||
end
|
||||
|
||||
--- Disables the Addon, if possible, return true or false depending on success.
|
||||
-- This internally calls AceAddon:DisableAddon(), thus dispatching a OnDisable callback
|
||||
-- and disabling all modules of the addon.\\
|
||||
-- :Disable() also sets the internal `enableState` variable to false
|
||||
-- @name //addon//:Disable
|
||||
-- @paramsig
|
||||
-- @usage
|
||||
-- -- Disable MyAddon
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
|
||||
-- MyAddon:Disable()
|
||||
function Disable(self)
|
||||
self:SetEnabledState(false)
|
||||
return AceAddon:DisableAddon(self)
|
||||
end
|
||||
|
||||
--- Enables the Module, if possible, return true or false depending on success.
|
||||
-- Short-hand function that retrieves the module via `:GetModule` and calls `:Enable` on the module object.
|
||||
-- @name //addon//:EnableModule
|
||||
-- @paramsig name
|
||||
-- @usage
|
||||
-- -- Enable MyModule using :GetModule
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
|
||||
-- MyModule = MyAddon:GetModule("MyModule")
|
||||
-- MyModule:Enable()
|
||||
--
|
||||
-- -- Enable MyModule using the short-hand
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
|
||||
-- MyAddon:EnableModule("MyModule")
|
||||
function EnableModule(self, name)
|
||||
local module = self:GetModule( name )
|
||||
return module:Enable()
|
||||
end
|
||||
|
||||
--- Disables the Module, if possible, return true or false depending on success.
|
||||
-- Short-hand function that retrieves the module via `:GetModule` and calls `:Disable` on the module object.
|
||||
-- @name //addon//:DisableModule
|
||||
-- @paramsig name
|
||||
-- @usage
|
||||
-- -- Disable MyModule using :GetModule
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
|
||||
-- MyModule = MyAddon:GetModule("MyModule")
|
||||
-- MyModule:Disable()
|
||||
--
|
||||
-- -- Disable MyModule using the short-hand
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
|
||||
-- MyAddon:DisableModule("MyModule")
|
||||
function DisableModule(self, name)
|
||||
local module = self:GetModule( name )
|
||||
return module:Disable()
|
||||
end
|
||||
|
||||
--- Set the default libraries to be mixed into all modules created by this object.
|
||||
-- Note that you can only change the default module libraries before any module is created.
|
||||
-- @name //addon//:SetDefaultModuleLibraries
|
||||
-- @paramsig lib[, lib, ...]
|
||||
-- @param lib List of libraries to embed into the addon
|
||||
-- @usage
|
||||
-- -- Create the addon object
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon")
|
||||
-- -- Configure default libraries for modules (all modules need AceEvent-3.0)
|
||||
-- MyAddon:SetDefaultModuleLibraries("AceEvent-3.0")
|
||||
-- -- Create a module
|
||||
-- MyModule = MyAddon:NewModule("MyModule")
|
||||
function SetDefaultModuleLibraries(self, ...)
|
||||
if next(self.modules) then
|
||||
error("Usage: SetDefaultModuleLibraries(...): cannot change the module defaults after a module has been registered.", 2)
|
||||
end
|
||||
self.defaultModuleLibraries = {...}
|
||||
end
|
||||
|
||||
--- Set the default state in which new modules are being created.
|
||||
-- Note that you can only change the default state before any module is created.
|
||||
-- @name //addon//:SetDefaultModuleState
|
||||
-- @paramsig state
|
||||
-- @param state Default state for new modules, true for enabled, false for disabled
|
||||
-- @usage
|
||||
-- -- Create the addon object
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon")
|
||||
-- -- Set the default state to "disabled"
|
||||
-- MyAddon:SetDefaultModuleState(false)
|
||||
-- -- Create a module and explicilty enable it
|
||||
-- MyModule = MyAddon:NewModule("MyModule")
|
||||
-- MyModule:Enable()
|
||||
function SetDefaultModuleState(self, state)
|
||||
if next(self.modules) then
|
||||
error("Usage: SetDefaultModuleState(state): cannot change the module defaults after a module has been registered.", 2)
|
||||
end
|
||||
self.defaultModuleState = state
|
||||
end
|
||||
|
||||
--- Set the default prototype to use for new modules on creation.
|
||||
-- Note that you can only change the default prototype before any module is created.
|
||||
-- @name //addon//:SetDefaultModulePrototype
|
||||
-- @paramsig prototype
|
||||
-- @param prototype Default prototype for the new modules (table)
|
||||
-- @usage
|
||||
-- -- Define a prototype
|
||||
-- local prototype = { OnEnable = function(self) print("OnEnable called!") end }
|
||||
-- -- Set the default prototype
|
||||
-- MyAddon:SetDefaultModulePrototype(prototype)
|
||||
-- -- Create a module and explicitly Enable it
|
||||
-- MyModule = MyAddon:NewModule("MyModule")
|
||||
-- MyModule:Enable()
|
||||
-- -- should print "OnEnable called!" now
|
||||
-- @see NewModule
|
||||
function SetDefaultModulePrototype(self, prototype)
|
||||
if next(self.modules) then
|
||||
error("Usage: SetDefaultModulePrototype(prototype): cannot change the module defaults after a module has been registered.", 2)
|
||||
end
|
||||
if type(prototype) ~= "table" then
|
||||
error(("Usage: SetDefaultModulePrototype(prototype): 'prototype' - table expected got '%s'."):format(type(prototype)), 2)
|
||||
end
|
||||
self.defaultModulePrototype = prototype
|
||||
end
|
||||
|
||||
--- Set the state of an addon or module
|
||||
-- This should only be called before any enabling actually happend, e.g. in/before OnInitialize.
|
||||
-- @name //addon//:SetEnabledState
|
||||
-- @paramsig state
|
||||
-- @param state the state of an addon or module (enabled=true, disabled=false)
|
||||
function SetEnabledState(self, state)
|
||||
self.enabledState = state
|
||||
end
|
||||
|
||||
|
||||
--- Return an iterator of all modules associated to the addon.
|
||||
-- @name //addon//:IterateModules
|
||||
-- @paramsig
|
||||
-- @usage
|
||||
-- -- Enable all modules
|
||||
-- for name, module in MyAddon:IterateModules() do
|
||||
-- module:Enable()
|
||||
-- end
|
||||
local function IterateModules(self) return pairs(self.modules) end
|
||||
|
||||
-- Returns an iterator of all embeds in the addon
|
||||
-- @name //addon//:IterateEmbeds
|
||||
-- @paramsig
|
||||
local function IterateEmbeds(self) return pairs(AceAddon.embeds[self]) end
|
||||
|
||||
--- Query the enabledState of an addon.
|
||||
-- @name //addon//:IsEnabled
|
||||
-- @paramsig
|
||||
-- @usage
|
||||
-- if MyAddon:IsEnabled() then
|
||||
-- MyAddon:Disable()
|
||||
-- end
|
||||
local function IsEnabled(self) return self.enabledState end
|
||||
local mixins = {
|
||||
NewModule = NewModule,
|
||||
GetModule = GetModule,
|
||||
Enable = Enable,
|
||||
Disable = Disable,
|
||||
EnableModule = EnableModule,
|
||||
DisableModule = DisableModule,
|
||||
IsEnabled = IsEnabled,
|
||||
SetDefaultModuleLibraries = SetDefaultModuleLibraries,
|
||||
SetDefaultModuleState = SetDefaultModuleState,
|
||||
SetDefaultModulePrototype = SetDefaultModulePrototype,
|
||||
SetEnabledState = SetEnabledState,
|
||||
IterateModules = IterateModules,
|
||||
IterateEmbeds = IterateEmbeds,
|
||||
GetName = GetName,
|
||||
}
|
||||
local function IsModule(self) return false end
|
||||
local pmixins = {
|
||||
defaultModuleState = true,
|
||||
enabledState = true,
|
||||
IsModule = IsModule,
|
||||
}
|
||||
-- Embed( target )
|
||||
-- target (object) - target object to embed aceaddon in
|
||||
--
|
||||
-- this is a local function specifically since it's meant to be only called internally
|
||||
function Embed(target, skipPMixins)
|
||||
for k, v in pairs(mixins) do
|
||||
target[k] = v
|
||||
end
|
||||
if not skipPMixins then
|
||||
for k, v in pairs(pmixins) do
|
||||
target[k] = target[k] or v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- - Initialize the addon after creation.
|
||||
-- This function is only used internally during the ADDON_LOADED event
|
||||
-- It will call the **OnInitialize** function on the addon object (if present),
|
||||
-- and the **OnEmbedInitialize** function on all embeded libraries.
|
||||
--
|
||||
-- **Note:** Do not call this function manually, unless you're absolutely sure that you know what you are doing.
|
||||
-- @param addon addon object to intialize
|
||||
function AceAddon:InitializeAddon(addon)
|
||||
safecall(addon.OnInitialize, addon)
|
||||
|
||||
local embeds = self.embeds[addon]
|
||||
for i = 1, #embeds do
|
||||
local lib = LibStub:GetLibrary(embeds[i], true)
|
||||
if lib then safecall(lib.OnEmbedInitialize, lib, addon) end
|
||||
end
|
||||
|
||||
-- we don't call InitializeAddon on modules specifically, this is handled
|
||||
-- from the event handler and only done _once_
|
||||
end
|
||||
|
||||
-- - Enable the addon after creation.
|
||||
-- Note: This function is only used internally during the PLAYER_LOGIN event, or during ADDON_LOADED,
|
||||
-- if IsLoggedIn() already returns true at that point, e.g. for LoD Addons.
|
||||
-- It will call the **OnEnable** function on the addon object (if present),
|
||||
-- and the **OnEmbedEnable** function on all embeded libraries.\\
|
||||
-- This function does not toggle the enable state of the addon itself, and will return early if the addon is disabled.
|
||||
--
|
||||
-- **Note:** Do not call this function manually, unless you're absolutely sure that you know what you are doing.
|
||||
-- Use :Enable on the addon itself instead.
|
||||
-- @param addon addon object to enable
|
||||
function AceAddon:EnableAddon(addon)
|
||||
if type(addon) == "string" then addon = AceAddon:GetAddon(addon) end
|
||||
if self.statuses[addon.name] or not addon.enabledState then return false end
|
||||
|
||||
-- set the statuses first, before calling the OnEnable. this allows for Disabling of the addon in OnEnable.
|
||||
self.statuses[addon.name] = true
|
||||
|
||||
safecall(addon.OnEnable, addon)
|
||||
|
||||
-- make sure we're still enabled before continueing
|
||||
if self.statuses[addon.name] then
|
||||
local embeds = self.embeds[addon]
|
||||
for i = 1, #embeds do
|
||||
local lib = LibStub:GetLibrary(embeds[i], true)
|
||||
if lib then safecall(lib.OnEmbedEnable, lib, addon) end
|
||||
end
|
||||
|
||||
-- enable possible modules.
|
||||
local modules = addon.orderedModules
|
||||
for i = 1, #modules do
|
||||
self:EnableAddon(modules[i])
|
||||
end
|
||||
end
|
||||
return self.statuses[addon.name] -- return true if we're disabled
|
||||
end
|
||||
|
||||
-- - Disable the addon
|
||||
-- Note: This function is only used internally.
|
||||
-- It will call the **OnDisable** function on the addon object (if present),
|
||||
-- and the **OnEmbedDisable** function on all embeded libraries.\\
|
||||
-- This function does not toggle the enable state of the addon itself, and will return early if the addon is still enabled.
|
||||
--
|
||||
-- **Note:** Do not call this function manually, unless you're absolutely sure that you know what you are doing.
|
||||
-- Use :Disable on the addon itself instead.
|
||||
-- @param addon addon object to enable
|
||||
function AceAddon:DisableAddon(addon)
|
||||
if type(addon) == "string" then addon = AceAddon:GetAddon(addon) end
|
||||
if not self.statuses[addon.name] then return false end
|
||||
|
||||
-- set statuses first before calling OnDisable, this allows for aborting the disable in OnDisable.
|
||||
self.statuses[addon.name] = false
|
||||
|
||||
safecall( addon.OnDisable, addon )
|
||||
|
||||
-- make sure we're still disabling...
|
||||
if not self.statuses[addon.name] then
|
||||
local embeds = self.embeds[addon]
|
||||
for i = 1, #embeds do
|
||||
local lib = LibStub:GetLibrary(embeds[i], true)
|
||||
if lib then safecall(lib.OnEmbedDisable, lib, addon) end
|
||||
end
|
||||
-- disable possible modules.
|
||||
local modules = addon.orderedModules
|
||||
for i = 1, #modules do
|
||||
self:DisableAddon(modules[i])
|
||||
end
|
||||
end
|
||||
|
||||
return not self.statuses[addon.name] -- return true if we're disabled
|
||||
end
|
||||
|
||||
--- Get an iterator over all registered addons.
|
||||
-- @usage
|
||||
-- -- Print a list of all installed AceAddon's
|
||||
-- for name, addon in AceAddon:IterateAddons() do
|
||||
-- print("Addon: " .. name)
|
||||
-- end
|
||||
function AceAddon:IterateAddons() return pairs(self.addons) end
|
||||
|
||||
--- Get an iterator over the internal status registry.
|
||||
-- @usage
|
||||
-- -- Print a list of all enabled addons
|
||||
-- for name, status in AceAddon:IterateAddonStatus() do
|
||||
-- if status then
|
||||
-- print("EnabledAddon: " .. name)
|
||||
-- end
|
||||
-- end
|
||||
function AceAddon:IterateAddonStatus() return pairs(self.statuses) end
|
||||
|
||||
-- Following Iterators are deprecated, and their addon specific versions should be used
|
||||
-- e.g. addon:IterateEmbeds() instead of :IterateEmbedsOnAddon(addon)
|
||||
function AceAddon:IterateEmbedsOnAddon(addon) return pairs(self.embeds[addon]) end
|
||||
function AceAddon:IterateModulesOfAddon(addon) return pairs(addon.modules) end
|
||||
|
||||
-- Event Handling
|
||||
local function onEvent(this, event, arg1)
|
||||
-- 2011-08-17 nevcairiel - ignore the load event of Blizzard_DebugTools, so a potential startup error isn't swallowed up
|
||||
if (event == "ADDON_LOADED" and arg1 ~= "Blizzard_DebugTools") or event == "PLAYER_LOGIN" then
|
||||
-- if a addon loads another addon, recursion could happen here, so we need to validate the table on every iteration
|
||||
while(#AceAddon.initializequeue > 0) do
|
||||
local addon = tremove(AceAddon.initializequeue, 1)
|
||||
-- this might be an issue with recursion - TODO: validate
|
||||
if event == "ADDON_LOADED" then addon.baseName = arg1 end
|
||||
AceAddon:InitializeAddon(addon)
|
||||
tinsert(AceAddon.enablequeue, addon)
|
||||
end
|
||||
|
||||
if IsLoggedIn() then
|
||||
while(#AceAddon.enablequeue > 0) do
|
||||
local addon = tremove(AceAddon.enablequeue, 1)
|
||||
AceAddon:EnableAddon(addon)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
AceAddon.frame:RegisterEvent("ADDON_LOADED")
|
||||
AceAddon.frame:RegisterEvent("PLAYER_LOGIN")
|
||||
AceAddon.frame:SetScript("OnEvent", onEvent)
|
||||
|
||||
-- upgrade embeded
|
||||
for name, addon in pairs(AceAddon.addons) do
|
||||
Embed(addon, true)
|
||||
end
|
||||
|
||||
-- 2010-10-27 nevcairiel - add new "orderedModules" table
|
||||
if oldminor and oldminor < 10 then
|
||||
for name, addon in pairs(AceAddon.addons) do
|
||||
addon.orderedModules = {}
|
||||
for module_name, module in pairs(addon.modules) do
|
||||
tinsert(addon.orderedModules, module)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +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/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceAddon-3.0.lua"/>
|
||||
</Ui>
|
||||
@ -0,0 +1,301 @@
|
||||
--- **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.\\
|
||||
-- **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
|
||||
-- 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.\\
|
||||
-- It is recommended to embed AceComm, otherwise you'll have to specify a custom `self` on all calls you
|
||||
-- make into AceComm.
|
||||
-- @class file
|
||||
-- @name AceComm-3.0
|
||||
-- @release $Id: AceComm-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $
|
||||
|
||||
--[[ 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.
|
||||
|
||||
]]
|
||||
|
||||
local CallbackHandler = LibStub("CallbackHandler-1.0")
|
||||
local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib")
|
||||
|
||||
local MAJOR, MINOR = "AceComm-3.0", 10
|
||||
local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceComm then return end
|
||||
|
||||
-- Lua APIs
|
||||
local type, next, pairs, tostring = type, next, pairs, tostring
|
||||
local strsub, strfind = string.sub, string.find
|
||||
local match = string.match
|
||||
local tinsert, tconcat = table.insert, table.concat
|
||||
local error, assert = error, assert
|
||||
|
||||
-- WoW APIs
|
||||
local Ambiguate = Ambiguate
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: LibStub, DEFAULT_CHAT_FRAME, geterrorhandler, RegisterAddonMessagePrefix
|
||||
|
||||
AceComm.embeds = AceComm.embeds or {}
|
||||
|
||||
-- for my sanity and yours, let's give the message type bytes some names
|
||||
local MSG_MULTI_FIRST = "\001"
|
||||
local MSG_MULTI_NEXT = "\002"
|
||||
local MSG_MULTI_LAST = "\003"
|
||||
local MSG_ESCAPE = "\004"
|
||||
|
||||
-- remove old structures (pre WoW 4.0)
|
||||
AceComm.multipart_origprefixes = nil
|
||||
AceComm.multipart_reassemblers = nil
|
||||
|
||||
-- the multipart message spool: indexed by a combination of sender+distribution+
|
||||
AceComm.multipart_spool = AceComm.multipart_spool or {}
|
||||
|
||||
--- 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 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)
|
||||
if method == nil then
|
||||
method = "OnCommReceived"
|
||||
end
|
||||
|
||||
if #prefix > 16 then -- TODO: 15?
|
||||
error("AceComm:RegisterComm(prefix,method): prefix length is limited to 16 characters")
|
||||
end
|
||||
RegisterAddonMessagePrefix(prefix)
|
||||
|
||||
return AceComm._RegisterComm(self, prefix, method) -- created by CallbackHandler
|
||||
end
|
||||
|
||||
local warnedPrefix=false
|
||||
|
||||
--- Send a message over the Addon Channel
|
||||
-- @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 distribution Addon channel, e.g. "RAID", "GUILD", etc; 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 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.
|
||||
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!
|
||||
if not( type(prefix)=="string" and
|
||||
type(text)=="string" and
|
||||
type(distribution)=="string" and
|
||||
(target==nil or type(target)=="string") and
|
||||
(prio=="BULK" or prio=="NORMAL" or prio=="ALERT")
|
||||
) then
|
||||
error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2)
|
||||
end
|
||||
|
||||
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 queueName = prefix..distribution..(target or "")
|
||||
|
||||
local ctlCallback = nil
|
||||
if callbackFn then
|
||||
ctlCallback = function(sent)
|
||||
return callbackFn(callbackArg, sent, textlen)
|
||||
end
|
||||
end
|
||||
|
||||
local forceMultipart
|
||||
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
|
||||
if textlen+1 > maxtextlen then -- would we go over the size limit?
|
||||
forceMultipart = true -- just make it multipart, no escape problems then
|
||||
else
|
||||
text = "\004" .. text
|
||||
end
|
||||
end
|
||||
|
||||
if not forceMultipart and textlen <= maxtextlen then
|
||||
-- fits all in one message
|
||||
CTL:SendAddonMessage(prio, prefix, text, distribution, target, queueName, ctlCallback, textlen)
|
||||
else
|
||||
maxtextlen = maxtextlen - 1 -- 1 extra byte for part indicator in prefix(4.0)/start of message(4.1)
|
||||
|
||||
-- first part
|
||||
local chunk = strsub(text, 1, maxtextlen)
|
||||
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_FIRST..chunk, distribution, target, queueName, ctlCallback, maxtextlen)
|
||||
|
||||
-- continuation
|
||||
local pos = 1+maxtextlen
|
||||
|
||||
while pos+maxtextlen <= textlen do
|
||||
chunk = strsub(text, pos, pos+maxtextlen-1)
|
||||
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_NEXT..chunk, distribution, target, queueName, ctlCallback, pos+maxtextlen-1)
|
||||
pos = pos + maxtextlen
|
||||
end
|
||||
|
||||
-- final part
|
||||
chunk = strsub(text, pos)
|
||||
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_LAST..chunk, distribution, target, queueName, ctlCallback, textlen)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
----------------------------------------
|
||||
-- Message receiving
|
||||
----------------------------------------
|
||||
|
||||
do
|
||||
local compost = setmetatable({}, {__mode = "k"})
|
||||
local function new()
|
||||
local t = next(compost)
|
||||
if t then
|
||||
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
|
||||
t[i]=nil
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
return {}
|
||||
end
|
||||
|
||||
local function lostdatawarning(prefix,sender,where)
|
||||
DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: lost network data regarding '"..tostring(prefix).."' from '"..tostring(sender).."' (in "..where..")")
|
||||
end
|
||||
|
||||
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 spool = AceComm.multipart_spool
|
||||
|
||||
--[[
|
||||
if spool[key] then
|
||||
lostdatawarning(prefix,sender,"First")
|
||||
-- continue and overwrite
|
||||
end
|
||||
--]]
|
||||
|
||||
spool[key] = message -- plain string for now
|
||||
end
|
||||
|
||||
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 spool = AceComm.multipart_spool
|
||||
local olddata = spool[key]
|
||||
|
||||
if not olddata then
|
||||
--lostdatawarning(prefix,sender,"Next")
|
||||
return
|
||||
end
|
||||
|
||||
if type(olddata)~="table" then
|
||||
-- ... but what we have is not a table. So make it one. (Pull a composted one if available)
|
||||
local t = new()
|
||||
t[1] = olddata -- add old data as first 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
|
||||
else
|
||||
tinsert(olddata, message)
|
||||
end
|
||||
end
|
||||
|
||||
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 spool = AceComm.multipart_spool
|
||||
local olddata = spool[key]
|
||||
|
||||
if not olddata then
|
||||
--lostdatawarning(prefix,sender,"End")
|
||||
return
|
||||
end
|
||||
|
||||
spool[key] = nil
|
||||
|
||||
if type(olddata) == "table" then
|
||||
-- if we've received a "next", the spooled data will be a table for rapid & garbage-free tconcat
|
||||
tinsert(olddata, message)
|
||||
AceComm.callbacks:Fire(prefix, tconcat(olddata, ""), distribution, sender)
|
||||
compost[olddata] = true
|
||||
else
|
||||
-- if we've only received a "first", the spooled data will still only be a string
|
||||
AceComm.callbacks:Fire(prefix, olddata..message, distribution, sender)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----------------------------------------
|
||||
-- Embed CallbackHandler
|
||||
----------------------------------------
|
||||
|
||||
if not AceComm.callbacks then
|
||||
AceComm.callbacks = CallbackHandler:New(AceComm,
|
||||
"_RegisterComm",
|
||||
"UnregisterComm",
|
||||
"UnregisterAllComm")
|
||||
end
|
||||
|
||||
AceComm.callbacks.OnUsed = nil
|
||||
AceComm.callbacks.OnUnused = nil
|
||||
|
||||
local function OnEvent(self, event, prefix, message, distribution, sender)
|
||||
if event == "CHAT_MSG_ADDON" then
|
||||
sender = Ambiguate(sender, "none")
|
||||
local control, rest = match(message, "^([\001-\009])(.*)")
|
||||
if control then
|
||||
if control==MSG_MULTI_FIRST then
|
||||
AceComm:OnReceiveMultipartFirst(prefix, rest, distribution, sender)
|
||||
elseif control==MSG_MULTI_NEXT then
|
||||
AceComm:OnReceiveMultipartNext(prefix, rest, distribution, sender)
|
||||
elseif control==MSG_MULTI_LAST then
|
||||
AceComm:OnReceiveMultipartLast(prefix, rest, distribution, sender)
|
||||
elseif control==MSG_ESCAPE then
|
||||
AceComm.callbacks:Fire(prefix, rest, distribution, sender)
|
||||
else
|
||||
-- unknown control character, ignore SILENTLY (dont warn unnecessarily about future extensions!)
|
||||
end
|
||||
else
|
||||
-- single part: fire it off immediately and let CallbackHandler decide if it's registered or not
|
||||
AceComm.callbacks:Fire(prefix, message, distribution, sender)
|
||||
end
|
||||
else
|
||||
assert(false, "Received "..tostring(event).." event?!")
|
||||
end
|
||||
end
|
||||
|
||||
AceComm.frame = AceComm.frame or CreateFrame("Frame", "AceComm30Frame")
|
||||
AceComm.frame:SetScript("OnEvent", OnEvent)
|
||||
AceComm.frame:UnregisterAllEvents()
|
||||
AceComm.frame:RegisterEvent("CHAT_MSG_ADDON")
|
||||
|
||||
|
||||
----------------------------------------
|
||||
-- Base library stuff
|
||||
----------------------------------------
|
||||
|
||||
local mixins = {
|
||||
"RegisterComm",
|
||||
"UnregisterComm",
|
||||
"UnregisterAllComm",
|
||||
"SendCommMessage",
|
||||
}
|
||||
|
||||
-- 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
|
||||
function AceComm:Embed(target)
|
||||
for k, v in pairs(mixins) do
|
||||
target[v] = self[v]
|
||||
end
|
||||
self.embeds[target] = true
|
||||
return target
|
||||
end
|
||||
|
||||
function AceComm:OnEmbedDisable(target)
|
||||
target:UnregisterAllComm()
|
||||
end
|
||||
|
||||
-- Update embeds
|
||||
for target, v in pairs(AceComm.embeds) do
|
||||
AceComm:Embed(target)
|
||||
end
|
||||
@ -0,0 +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/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="ChatThrottleLib.lua"/>
|
||||
<Script file="AceComm-3.0.lua"/>
|
||||
</Ui>
|
||||
@ -0,0 +1,524 @@
|
||||
--
|
||||
-- ChatThrottleLib by Mikk
|
||||
--
|
||||
-- Manages AddOn chat output to keep player from getting kicked off.
|
||||
--
|
||||
-- ChatThrottleLib:SendChatMessage/:SendAddonMessage functions that accept
|
||||
-- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage.
|
||||
--
|
||||
-- Priorities get an equal share of available bandwidth when fully loaded.
|
||||
-- Communication channels are separated on extension+chattype+destination and
|
||||
-- get round-robinned. (Destination only matters for whispers and channels,
|
||||
-- obviously)
|
||||
--
|
||||
-- Will install hooks for SendChatMessage and SendAddonMessage to measure
|
||||
-- bandwidth bypassing the library and use less bandwidth itself.
|
||||
--
|
||||
--
|
||||
-- Fully embeddable library. Just copy this file into your addon directory,
|
||||
-- add it to the .toc, and it's done.
|
||||
--
|
||||
-- Can run as a standalone addon also, but, really, just embed it! :-)
|
||||
--
|
||||
-- LICENSE: ChatThrottleLib is released into the Public Domain
|
||||
--
|
||||
|
||||
local CTL_VERSION = 23
|
||||
|
||||
local _G = _G
|
||||
|
||||
if _G.ChatThrottleLib then
|
||||
if _G.ChatThrottleLib.version >= CTL_VERSION then
|
||||
-- There's already a newer (or same) version loaded. Buh-bye.
|
||||
return
|
||||
elseif not _G.ChatThrottleLib.securelyHooked then
|
||||
print("ChatThrottleLib: Warning: There's an ANCIENT ChatThrottleLib.lua (pre-wow 2.0, <v16) in an addon somewhere. Get the addon updated or copy in a newer ChatThrottleLib.lua (>=v16) in it!")
|
||||
-- ATTEMPT to unhook; this'll behave badly if someone else has hooked...
|
||||
-- ... and if someone has securehooked, they can kiss that goodbye too... >.<
|
||||
_G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage
|
||||
if _G.ChatThrottleLib.ORIG_SendAddonMessage then
|
||||
_G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage
|
||||
end
|
||||
end
|
||||
_G.ChatThrottleLib.ORIG_SendChatMessage = nil
|
||||
_G.ChatThrottleLib.ORIG_SendAddonMessage = nil
|
||||
end
|
||||
|
||||
if not _G.ChatThrottleLib then
|
||||
_G.ChatThrottleLib = {}
|
||||
end
|
||||
|
||||
ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh)
|
||||
local ChatThrottleLib = _G.ChatThrottleLib
|
||||
|
||||
ChatThrottleLib.version = CTL_VERSION
|
||||
|
||||
|
||||
|
||||
------------------ TWEAKABLES -----------------
|
||||
|
||||
ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.
|
||||
ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff
|
||||
|
||||
ChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.
|
||||
|
||||
ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
|
||||
|
||||
|
||||
local setmetatable = setmetatable
|
||||
local table_remove = table.remove
|
||||
local tostring = tostring
|
||||
local GetTime = GetTime
|
||||
local math_min = math.min
|
||||
local math_max = math.max
|
||||
local next = next
|
||||
local strlen = string.len
|
||||
local GetFramerate = GetFramerate
|
||||
local strlower = string.lower
|
||||
local unpack,type,pairs,wipe = unpack,type,pairs,wipe
|
||||
local UnitInRaid,UnitInParty = UnitInRaid,UnitInParty
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Double-linked ring implementation
|
||||
|
||||
local Ring = {}
|
||||
local RingMeta = { __index = Ring }
|
||||
|
||||
function Ring:New()
|
||||
local ret = {}
|
||||
setmetatable(ret, RingMeta)
|
||||
return ret
|
||||
end
|
||||
|
||||
function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position)
|
||||
if self.pos then
|
||||
obj.prev = self.pos.prev
|
||||
obj.prev.next = obj
|
||||
obj.next = self.pos
|
||||
obj.next.prev = obj
|
||||
else
|
||||
obj.next = obj
|
||||
obj.prev = obj
|
||||
self.pos = obj
|
||||
end
|
||||
end
|
||||
|
||||
function Ring:Remove(obj)
|
||||
obj.next.prev = obj.prev
|
||||
obj.prev.next = obj.next
|
||||
if self.pos == obj then
|
||||
self.pos = obj.next
|
||||
if self.pos == obj then
|
||||
self.pos = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Recycling bin for pipes
|
||||
-- A pipe is a plain integer-indexed queue of messages
|
||||
-- Pipes normally live in Rings of pipes (3 rings total, one per priority)
|
||||
|
||||
ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different
|
||||
local PipeBin = setmetatable({}, {__mode="k"})
|
||||
|
||||
local function DelPipe(pipe)
|
||||
PipeBin[pipe] = true
|
||||
end
|
||||
|
||||
local function NewPipe()
|
||||
local pipe = next(PipeBin)
|
||||
if pipe then
|
||||
wipe(pipe)
|
||||
PipeBin[pipe] = nil
|
||||
return pipe
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Recycling bin for messages
|
||||
|
||||
ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different
|
||||
local MsgBin = setmetatable({}, {__mode="k"})
|
||||
|
||||
local function DelMsg(msg)
|
||||
msg[1] = nil
|
||||
-- there's more parameters, but they're very repetetive so the string pool doesn't suffer really, and it's faster to just not delete them.
|
||||
MsgBin[msg] = true
|
||||
end
|
||||
|
||||
local function NewMsg()
|
||||
local msg = next(MsgBin)
|
||||
if msg then
|
||||
MsgBin[msg] = nil
|
||||
return msg
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- ChatThrottleLib:Init
|
||||
-- Initialize queues, set up frame for OnUpdate, etc
|
||||
|
||||
|
||||
function ChatThrottleLib:Init()
|
||||
|
||||
-- Set up queues
|
||||
if not self.Prio then
|
||||
self.Prio = {}
|
||||
self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
|
||||
self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
|
||||
self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
|
||||
end
|
||||
|
||||
-- v4: total send counters per priority
|
||||
for _, Prio in pairs(self.Prio) do
|
||||
Prio.nTotalSent = Prio.nTotalSent or 0
|
||||
end
|
||||
|
||||
if not self.avail then
|
||||
self.avail = 0 -- v5
|
||||
end
|
||||
if not self.nTotalSent then
|
||||
self.nTotalSent = 0 -- v5
|
||||
end
|
||||
|
||||
|
||||
-- Set up a frame to get OnUpdate events
|
||||
if not self.Frame then
|
||||
self.Frame = CreateFrame("Frame")
|
||||
self.Frame:Hide()
|
||||
end
|
||||
self.Frame:SetScript("OnUpdate", self.OnUpdate)
|
||||
self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds
|
||||
self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
self.OnUpdateDelay = 0
|
||||
self.LastAvailUpdate = GetTime()
|
||||
self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup
|
||||
|
||||
-- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7)
|
||||
if not self.securelyHooked then
|
||||
-- Use secure hooks as of v16. Old regular hook support yanked out in v21.
|
||||
self.securelyHooked = true
|
||||
--SendChatMessage
|
||||
hooksecurefunc("SendChatMessage", function(...)
|
||||
return ChatThrottleLib.Hook_SendChatMessage(...)
|
||||
end)
|
||||
--SendAddonMessage
|
||||
hooksecurefunc("SendAddonMessage", function(...)
|
||||
return ChatThrottleLib.Hook_SendAddonMessage(...)
|
||||
end)
|
||||
end
|
||||
self.nBypass = 0
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
|
||||
|
||||
local bMyTraffic = false
|
||||
|
||||
function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...)
|
||||
if bMyTraffic then
|
||||
return
|
||||
end
|
||||
local self = ChatThrottleLib
|
||||
local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD
|
||||
self.avail = self.avail - size
|
||||
self.nBypass = self.nBypass + size -- just a statistic
|
||||
end
|
||||
function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
|
||||
if bMyTraffic then
|
||||
return
|
||||
end
|
||||
local self = ChatThrottleLib
|
||||
local size = tostring(text or ""):len() + tostring(prefix or ""):len();
|
||||
size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD
|
||||
self.avail = self.avail - size
|
||||
self.nBypass = self.nBypass + size -- just a statistic
|
||||
end
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- ChatThrottleLib:UpdateAvail
|
||||
-- Update self.avail with how much bandwidth is currently available
|
||||
|
||||
function ChatThrottleLib:UpdateAvail()
|
||||
local now = GetTime()
|
||||
local MAX_CPS = self.MAX_CPS;
|
||||
local newavail = MAX_CPS * (now - self.LastAvailUpdate)
|
||||
local avail = self.avail
|
||||
|
||||
if now - self.HardThrottlingBeginTime < 5 then
|
||||
-- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then
|
||||
avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5)
|
||||
self.bChoking = true
|
||||
elseif GetFramerate() < self.MIN_FPS then -- GetFrameRate call takes ~0.002 secs
|
||||
avail = math_min(MAX_CPS, avail + newavail*0.5)
|
||||
self.bChoking = true -- just a statistic
|
||||
else
|
||||
avail = math_min(self.BURST, avail + newavail)
|
||||
self.bChoking = false
|
||||
end
|
||||
|
||||
avail = math_max(avail, 0-(MAX_CPS*2)) -- Can go negative when someone is eating bandwidth past the lib. but we refuse to stay silent for more than 2 seconds; if they can do it, we can.
|
||||
|
||||
self.avail = avail
|
||||
self.LastAvailUpdate = now
|
||||
|
||||
return avail
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Despooling logic
|
||||
-- Reminder:
|
||||
-- - We have 3 Priorities, each containing a "Ring" construct ...
|
||||
-- - ... made up of N "Pipe"s (1 for each destination/pipename)
|
||||
-- - and each pipe contains messages
|
||||
|
||||
function ChatThrottleLib:Despool(Prio)
|
||||
local ring = Prio.Ring
|
||||
while ring.pos and Prio.avail > ring.pos[1].nSize do
|
||||
local msg = table_remove(ring.pos, 1)
|
||||
if not ring.pos[1] then -- did we remove last msg in this pipe?
|
||||
local pipe = Prio.Ring.pos
|
||||
Prio.Ring:Remove(pipe)
|
||||
Prio.ByName[pipe.name] = nil
|
||||
DelPipe(pipe)
|
||||
else
|
||||
Prio.Ring.pos = Prio.Ring.pos.next
|
||||
end
|
||||
local didSend=false
|
||||
local lowerDest = strlower(msg[3] or "")
|
||||
if lowerDest == "raid" and not UnitInRaid("player") then
|
||||
-- do nothing
|
||||
elseif lowerDest == "party" and not UnitInParty("player") then
|
||||
-- do nothing
|
||||
else
|
||||
Prio.avail = Prio.avail - msg.nSize
|
||||
bMyTraffic = true
|
||||
msg.f(unpack(msg, 1, msg.n))
|
||||
bMyTraffic = false
|
||||
Prio.nTotalSent = Prio.nTotalSent + msg.nSize
|
||||
DelMsg(msg)
|
||||
didSend = true
|
||||
end
|
||||
-- notify caller of delivery (even if we didn't send it)
|
||||
if msg.callbackFn then
|
||||
msg.callbackFn (msg.callbackArg, didSend)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function ChatThrottleLib.OnEvent(this,event)
|
||||
-- v11: We know that the rate limiter is touchy after login. Assume that it's touchy after zoning, too.
|
||||
local self = ChatThrottleLib
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning
|
||||
self.avail = 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function ChatThrottleLib.OnUpdate(this,delay)
|
||||
local self = ChatThrottleLib
|
||||
|
||||
self.OnUpdateDelay = self.OnUpdateDelay + delay
|
||||
if self.OnUpdateDelay < 0.08 then
|
||||
return
|
||||
end
|
||||
self.OnUpdateDelay = 0
|
||||
|
||||
self:UpdateAvail()
|
||||
|
||||
if self.avail < 0 then
|
||||
return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu.
|
||||
end
|
||||
|
||||
-- See how many of our priorities have queued messages (we only have 3, don't worry about the loop)
|
||||
local n = 0
|
||||
for prioname,Prio in pairs(self.Prio) do
|
||||
if Prio.Ring.pos or Prio.avail < 0 then
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Anything queued still?
|
||||
if n<1 then
|
||||
-- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing
|
||||
for prioname, Prio in pairs(self.Prio) do
|
||||
self.avail = self.avail + Prio.avail
|
||||
Prio.avail = 0
|
||||
end
|
||||
self.bQueueing = false
|
||||
self.Frame:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
-- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
|
||||
local avail = self.avail/n
|
||||
self.avail = 0
|
||||
|
||||
for prioname, Prio in pairs(self.Prio) do
|
||||
if Prio.Ring.pos or Prio.avail < 0 then
|
||||
Prio.avail = Prio.avail + avail
|
||||
if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then
|
||||
self:Despool(Prio)
|
||||
-- Note: We might not get here if the user-supplied callback function errors out! Take care!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Spooling logic
|
||||
|
||||
function ChatThrottleLib:Enqueue(prioname, pipename, msg)
|
||||
local Prio = self.Prio[prioname]
|
||||
local pipe = Prio.ByName[pipename]
|
||||
if not pipe then
|
||||
self.Frame:Show()
|
||||
pipe = NewPipe()
|
||||
pipe.name = pipename
|
||||
Prio.ByName[pipename] = pipe
|
||||
Prio.Ring:Add(pipe)
|
||||
end
|
||||
|
||||
pipe[#pipe + 1] = msg
|
||||
|
||||
self.bQueueing = true
|
||||
end
|
||||
|
||||
function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg)
|
||||
if not self or not prio or not prefix or not text or not self.Prio[prio] then
|
||||
error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2)
|
||||
end
|
||||
if callbackFn and type(callbackFn)~="function" then
|
||||
error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
|
||||
end
|
||||
|
||||
local nSize = text:len()
|
||||
|
||||
if nSize>255 then
|
||||
error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2)
|
||||
end
|
||||
|
||||
nSize = nSize + self.MSG_OVERHEAD
|
||||
|
||||
-- Check if there's room in the global available bandwidth gauge to send directly
|
||||
if not self.bQueueing and nSize < self:UpdateAvail() then
|
||||
self.avail = self.avail - nSize
|
||||
bMyTraffic = true
|
||||
_G.SendChatMessage(text, chattype, language, destination)
|
||||
bMyTraffic = false
|
||||
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
|
||||
if callbackFn then
|
||||
callbackFn (callbackArg, true)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
return
|
||||
end
|
||||
|
||||
-- Message needs to be queued
|
||||
local msg = NewMsg()
|
||||
msg.f = _G.SendChatMessage
|
||||
msg[1] = text
|
||||
msg[2] = chattype or "SAY"
|
||||
msg[3] = language
|
||||
msg[4] = destination
|
||||
msg.n = 4
|
||||
msg.nSize = nSize
|
||||
msg.callbackFn = callbackFn
|
||||
msg.callbackArg = callbackArg
|
||||
|
||||
self:Enqueue(prio, queueName or (prefix..(chattype or "SAY")..(destination or "")), msg)
|
||||
end
|
||||
|
||||
|
||||
function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
|
||||
if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then
|
||||
error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2)
|
||||
end
|
||||
if callbackFn and type(callbackFn)~="function" then
|
||||
error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
|
||||
end
|
||||
|
||||
local nSize = text:len();
|
||||
|
||||
if RegisterAddonMessagePrefix then
|
||||
if nSize>255 then
|
||||
error("ChatThrottleLib:SendAddonMessage(): message length cannot exceed 255 bytes", 2)
|
||||
end
|
||||
else
|
||||
nSize = nSize + prefix:len() + 1
|
||||
if nSize>255 then
|
||||
error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 bytes", 2)
|
||||
end
|
||||
end
|
||||
|
||||
nSize = nSize + self.MSG_OVERHEAD;
|
||||
|
||||
-- Check if there's room in the global available bandwidth gauge to send directly
|
||||
if not self.bQueueing and nSize < self:UpdateAvail() then
|
||||
self.avail = self.avail - nSize
|
||||
bMyTraffic = true
|
||||
_G.SendAddonMessage(prefix, text, chattype, target)
|
||||
bMyTraffic = false
|
||||
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
|
||||
if callbackFn then
|
||||
callbackFn (callbackArg, true)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
return
|
||||
end
|
||||
|
||||
-- Message needs to be queued
|
||||
local msg = NewMsg()
|
||||
msg.f = _G.SendAddonMessage
|
||||
msg[1] = prefix
|
||||
msg[2] = text
|
||||
msg[3] = chattype
|
||||
msg[4] = target
|
||||
msg.n = (target~=nil) and 4 or 3;
|
||||
msg.nSize = nSize
|
||||
msg.callbackFn = callbackFn
|
||||
msg.callbackArg = callbackArg
|
||||
|
||||
self:Enqueue(prio, queueName or (prefix..chattype..(target or "")), msg)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Get the ball rolling!
|
||||
|
||||
ChatThrottleLib:Init()
|
||||
|
||||
--[[ WoWBench debugging snippet
|
||||
if(WOWB_VER) then
|
||||
local function SayTimer()
|
||||
print("SAY: "..GetTime().." "..arg1)
|
||||
end
|
||||
ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer)
|
||||
ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY")
|
||||
end
|
||||
]]
|
||||
|
||||
|
||||
@ -0,0 +1,250 @@
|
||||
--- **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
|
||||
-- to your addons individual needs.
|
||||
--
|
||||
-- **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
|
||||
-- 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
|
||||
-- make into AceConsole.
|
||||
-- @class file
|
||||
-- @name AceConsole-3.0
|
||||
-- @release $Id: AceConsole-3.0.lua 1143 2016-07-11 08:52:03Z nevcairiel $
|
||||
local MAJOR,MINOR = "AceConsole-3.0", 7
|
||||
|
||||
local AceConsole, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConsole then return end -- No upgrade needed
|
||||
|
||||
AceConsole.embeds = AceConsole.embeds or {} -- table containing objects AceConsole is embedded in.
|
||||
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
|
||||
|
||||
-- Lua APIs
|
||||
local tconcat, tostring, select = table.concat, tostring, select
|
||||
local type, pairs, error = type, pairs, error
|
||||
local format, strfind, strsub = string.format, string.find, string.sub
|
||||
local max = math.max
|
||||
|
||||
-- WoW APIs
|
||||
local _G = _G
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: DEFAULT_CHAT_FRAME, SlashCmdList, hash_SlashCmdList
|
||||
|
||||
local tmp={}
|
||||
local function Print(self,frame,...)
|
||||
local n=0
|
||||
if self ~= AceConsole then
|
||||
n=n+1
|
||||
tmp[n] = "|cff33ff99"..tostring( self ).."|r:"
|
||||
end
|
||||
for i=1, select("#", ...) do
|
||||
n=n+1
|
||||
tmp[n] = tostring(select(i, ...))
|
||||
end
|
||||
frame:AddMessage( tconcat(tmp," ",1,n) )
|
||||
end
|
||||
|
||||
--- Print to DEFAULT_CHAT_FRAME or given ChatFrame (anything with an .AddMessage function)
|
||||
-- @paramsig [chatframe ,] ...
|
||||
-- @param chatframe Custom ChatFrame to print to (or any frame with an .AddMessage function)
|
||||
-- @param ... List of any values to be printed
|
||||
function AceConsole:Print(...)
|
||||
local frame = ...
|
||||
if type(frame) == "table" and frame.AddMessage then -- Is first argument something with an .AddMessage member?
|
||||
return Print(self, frame, select(2,...))
|
||||
else
|
||||
return Print(self, DEFAULT_CHAT_FRAME, ...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Formatted (using format()) print to DEFAULT_CHAT_FRAME or given ChatFrame (anything with an .AddMessage function)
|
||||
-- @paramsig [chatframe ,] "format"[, ...]
|
||||
-- @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 ... Arguments to the format string
|
||||
function AceConsole:Printf(...)
|
||||
local frame = ...
|
||||
if type(frame) == "table" and frame.AddMessage then -- Is first argument something with an .AddMessage member?
|
||||
return Print(self, frame, format(select(2,...)))
|
||||
else
|
||||
return Print(self, DEFAULT_CHAT_FRAME, format(...))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
--- Register a simple chat command
|
||||
-- @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 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 )
|
||||
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
|
||||
|
||||
local name = "ACECONSOLE_"..command:upper()
|
||||
|
||||
if type( func ) == "string" then
|
||||
SlashCmdList[name] = function(input, editBox)
|
||||
self[func](self, input, editBox)
|
||||
end
|
||||
else
|
||||
SlashCmdList[name] = func
|
||||
end
|
||||
_G["SLASH_"..name.."1"] = "/"..command:lower()
|
||||
AceConsole.commands[command] = name
|
||||
-- non-persisting commands are registered for enabling disabling
|
||||
if not persist then
|
||||
if not AceConsole.weakcommands[self] then AceConsole.weakcommands[self] = {} end
|
||||
AceConsole.weakcommands[self][command] = func
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Unregister a chatcommand
|
||||
-- @param command Chat command to be unregistered WITHOUT leading "/"
|
||||
function AceConsole:UnregisterChatCommand( command )
|
||||
local name = AceConsole.commands[command]
|
||||
if name then
|
||||
SlashCmdList[name] = nil
|
||||
_G["SLASH_" .. name .. "1"] = nil
|
||||
hash_SlashCmdList["/" .. command:upper()] = nil
|
||||
AceConsole.commands[command] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Get an iterator over all Chat Commands registered with AceConsole
|
||||
-- @return Iterator (pairs) over all commands
|
||||
function AceConsole:IterateChatCommands() return pairs(AceConsole.commands) end
|
||||
|
||||
|
||||
local function nils(n, ...)
|
||||
if n>1 then
|
||||
return nil, nils(n-1, ...)
|
||||
elseif n==1 then
|
||||
return nil, ...
|
||||
else
|
||||
return ...
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Retreive one or more space-separated arguments from a string.
|
||||
-- Treats quoted strings and itemlinks as non-spaced.
|
||||
-- @param str The raw argument string
|
||||
-- @param numargs How many arguments to get (default 1)
|
||||
-- @param startpos Where in the string to start scanning (default 1)
|
||||
-- @return Returns arg1, arg2, ..., nextposition\\
|
||||
-- Missing arguments will be returned as nils. 'nextposition' is returned as 1e9 at the end of the string.
|
||||
function AceConsole:GetArgs(str, numargs, startpos)
|
||||
numargs = numargs or 1
|
||||
startpos = max(startpos or 1, 1)
|
||||
|
||||
local pos=startpos
|
||||
|
||||
-- find start of new arg
|
||||
pos = strfind(str, "[^ ]", pos)
|
||||
if not pos then -- whoops, end of string
|
||||
return nils(numargs, 1e9)
|
||||
end
|
||||
|
||||
if numargs<1 then
|
||||
return pos
|
||||
end
|
||||
|
||||
-- quoted or space separated? find out which pattern to use
|
||||
local delim_or_pipe
|
||||
local ch = strsub(str, pos, pos)
|
||||
if ch=='"' then
|
||||
pos = pos + 1
|
||||
delim_or_pipe='([|"])'
|
||||
elseif ch=="'" then
|
||||
pos = pos + 1
|
||||
delim_or_pipe="([|'])"
|
||||
else
|
||||
delim_or_pipe="([| ])"
|
||||
end
|
||||
|
||||
startpos = pos
|
||||
|
||||
while true do
|
||||
-- find delimiter or hyperlink
|
||||
local ch,_
|
||||
pos,_,ch = strfind(str, delim_or_pipe, pos)
|
||||
|
||||
if not pos then break end
|
||||
|
||||
if ch=="|" then
|
||||
-- some kind of escape
|
||||
|
||||
if strsub(str,pos,pos+1)=="|H" then
|
||||
-- It's a |H....|hhyper link!|h
|
||||
pos=strfind(str, "|h", pos+2) -- first |h
|
||||
if not pos then break end
|
||||
|
||||
pos=strfind(str, "|h", pos+2) -- second |h
|
||||
if not pos then break end
|
||||
elseif strsub(str,pos, pos+1) == "|T" then
|
||||
-- It's a |T....|t texture
|
||||
pos=strfind(str, "|t", pos+2)
|
||||
if not pos then break end
|
||||
end
|
||||
|
||||
pos=pos+2 -- skip past this escape (last |h if it was a hyperlink)
|
||||
|
||||
else
|
||||
-- found delimiter, done with this arg
|
||||
return strsub(str, startpos, pos-1), AceConsole:GetArgs(str, numargs-1, pos+1)
|
||||
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)
|
||||
return strsub(str, startpos), nils(numargs-1, 1e9)
|
||||
end
|
||||
|
||||
|
||||
--- embedding and embed handling
|
||||
|
||||
local mixins = {
|
||||
"Print",
|
||||
"Printf",
|
||||
"RegisterChatCommand",
|
||||
"UnregisterChatCommand",
|
||||
"GetArgs",
|
||||
}
|
||||
|
||||
-- Embeds AceConsole into the target object making the functions from the mixins list available on target:..
|
||||
-- @param target target object to embed AceBucket in
|
||||
function AceConsole:Embed( target )
|
||||
for k, v in pairs( mixins ) do
|
||||
target[v] = self[v]
|
||||
end
|
||||
self.embeds[target] = true
|
||||
return target
|
||||
end
|
||||
|
||||
function AceConsole:OnEmbedEnable( target )
|
||||
if AceConsole.weakcommands[target] then
|
||||
for command, func in pairs( AceConsole.weakcommands[target] ) do
|
||||
target:RegisterChatCommand( command, func, false, true ) -- nonpersisting and silent registry
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AceConsole:OnEmbedDisable( target )
|
||||
if AceConsole.weakcommands[target] then
|
||||
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?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for addon in pairs(AceConsole.embeds) do
|
||||
AceConsole:Embed(addon)
|
||||
end
|
||||
@ -0,0 +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/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceConsole-3.0.lua"/>
|
||||
</Ui>
|
||||
@ -0,0 +1,126 @@
|
||||
--- AceEvent-3.0 provides event registration and secure dispatching.
|
||||
-- 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.
|
||||
--
|
||||
-- **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
|
||||
-- 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
|
||||
-- make into AceEvent.
|
||||
-- @class file
|
||||
-- @name AceEvent-3.0
|
||||
-- @release $Id: AceEvent-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $
|
||||
local CallbackHandler = LibStub("CallbackHandler-1.0")
|
||||
|
||||
local MAJOR, MINOR = "AceEvent-3.0", 4
|
||||
local AceEvent = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceEvent then return end
|
||||
|
||||
-- Lua APIs
|
||||
local pairs = pairs
|
||||
|
||||
AceEvent.frame = AceEvent.frame or CreateFrame("Frame", "AceEvent30Frame") -- our event frame
|
||||
AceEvent.embeds = AceEvent.embeds or {} -- what objects embed this lib
|
||||
|
||||
-- APIs and registry for blizzard events, using CallbackHandler lib
|
||||
if not AceEvent.events then
|
||||
AceEvent.events = CallbackHandler:New(AceEvent,
|
||||
"RegisterEvent", "UnregisterEvent", "UnregisterAllEvents")
|
||||
end
|
||||
|
||||
function AceEvent.events:OnUsed(target, eventname)
|
||||
AceEvent.frame:RegisterEvent(eventname)
|
||||
end
|
||||
|
||||
function AceEvent.events:OnUnused(target, eventname)
|
||||
AceEvent.frame:UnregisterEvent(eventname)
|
||||
end
|
||||
|
||||
|
||||
-- APIs and registry for IPC messages, using CallbackHandler lib
|
||||
if not AceEvent.messages then
|
||||
AceEvent.messages = CallbackHandler:New(AceEvent,
|
||||
"RegisterMessage", "UnregisterMessage", "UnregisterAllMessages"
|
||||
)
|
||||
AceEvent.SendMessage = AceEvent.messages.Fire
|
||||
end
|
||||
|
||||
--- embedding and embed handling
|
||||
local mixins = {
|
||||
"RegisterEvent", "UnregisterEvent",
|
||||
"RegisterMessage", "UnregisterMessage",
|
||||
"SendMessage",
|
||||
"UnregisterAllEvents", "UnregisterAllMessages",
|
||||
}
|
||||
|
||||
--- 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)
|
||||
-- Any arguments to the event will be passed on after that.
|
||||
-- @name AceEvent:RegisterEvent
|
||||
-- @class function
|
||||
-- @paramsig event[, callback [, arg]]
|
||||
-- @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 arg An optional argument to pass to the callback function
|
||||
|
||||
--- Unregister an event.
|
||||
-- @name AceEvent:UnregisterEvent
|
||||
-- @class function
|
||||
-- @paramsig event
|
||||
-- @param event The event to unregister
|
||||
|
||||
--- 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)
|
||||
-- Any arguments to the event will be passed on after that.
|
||||
-- @name AceEvent:RegisterMessage
|
||||
-- @class function
|
||||
-- @paramsig message[, callback [, arg]]
|
||||
-- @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 arg An optional argument to pass to the callback function
|
||||
|
||||
--- Unregister a message
|
||||
-- @name AceEvent:UnregisterMessage
|
||||
-- @class function
|
||||
-- @paramsig message
|
||||
-- @param message The message to unregister
|
||||
|
||||
--- Send a message over the AceEvent-3.0 internal message system to other addons registered for this message.
|
||||
-- @name AceEvent:SendMessage
|
||||
-- @class function
|
||||
-- @paramsig message, ...
|
||||
-- @param message The message to send
|
||||
-- @param ... Any arguments to the message
|
||||
|
||||
|
||||
-- Embeds AceEvent into the target object making the functions from the mixins list available on target:..
|
||||
-- @param target target object to embed AceEvent in
|
||||
function AceEvent:Embed(target)
|
||||
for k, v in pairs(mixins) do
|
||||
target[v] = self[v]
|
||||
end
|
||||
self.embeds[target] = true
|
||||
return target
|
||||
end
|
||||
|
||||
-- AceEvent:OnEmbedDisable( target )
|
||||
-- target (object) - target object that is being disabled
|
||||
--
|
||||
-- Unregister all events messages etc when the target disables.
|
||||
-- this method should be called by the target manually or by an addon framework
|
||||
function AceEvent:OnEmbedDisable(target)
|
||||
target:UnregisterAllEvents()
|
||||
target:UnregisterAllMessages()
|
||||
end
|
||||
|
||||
-- Script to fire blizzard events into the event listeners
|
||||
local events = AceEvent.events
|
||||
AceEvent.frame:SetScript("OnEvent", function(this, event, ...)
|
||||
events:Fire(event, ...)
|
||||
end)
|
||||
|
||||
--- Finally: upgrade our old embeds
|
||||
for target, v in pairs(AceEvent.embeds) do
|
||||
AceEvent:Embed(target)
|
||||
end
|
||||
@ -0,0 +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/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceEvent-3.0.lua"/>
|
||||
</Ui>
|
||||
@ -0,0 +1,276 @@
|
||||
--- **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
|
||||
-- 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.\\
|
||||
-- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API
|
||||
-- restricts us to.
|
||||
--
|
||||
-- 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.
|
||||
--
|
||||
-- **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
|
||||
-- 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
|
||||
-- make into AceTimer.
|
||||
-- @class file
|
||||
-- @name AceTimer-3.0
|
||||
-- @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 AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceTimer then return end -- No upgrade needed
|
||||
AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list
|
||||
local activeTimers = AceTimer.activeTimers -- Upvalue our private data
|
||||
|
||||
-- Lua APIs
|
||||
local type, unpack, next, error, select = type, unpack, next, error, select
|
||||
-- WoW APIs
|
||||
local GetTime, C_TimerAfter = GetTime, C_Timer.After
|
||||
|
||||
local function new(self, loop, func, delay, ...)
|
||||
if delay < 0.01 then
|
||||
delay = 0.01 -- Restrict to the lowest time that the C_Timer API allows us
|
||||
end
|
||||
|
||||
local timer = {...}
|
||||
timer.object = self
|
||||
timer.func = func
|
||||
timer.looping = loop
|
||||
timer.argsCount = select("#", ...)
|
||||
timer.delay = delay
|
||||
timer.ends = GetTime() + delay
|
||||
|
||||
activeTimers[timer] = timer
|
||||
|
||||
-- Create new timer closure to wrap the "timer" object
|
||||
timer.callback = function()
|
||||
if not timer.cancelled 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
|
||||
-- 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))
|
||||
else
|
||||
timer.func(unpack(timer, 1, timer.argsCount))
|
||||
end
|
||||
|
||||
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
|
||||
-- due to fps differences
|
||||
local time = GetTime()
|
||||
local delay = timer.delay - (time - timer.ends)
|
||||
-- Ensure the delay doesn't go below the threshold
|
||||
if delay < 0.01 then delay = 0.01 end
|
||||
C_TimerAfter(delay, timer.callback)
|
||||
timer.ends = time + delay
|
||||
else
|
||||
activeTimers[timer.handle or timer] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
C_TimerAfter(delay, timer.callback)
|
||||
return timer
|
||||
end
|
||||
|
||||
--- Schedule a new one-shot timer.
|
||||
-- The timer will fire once in `delay` seconds, unless canceled before.
|
||||
-- @param callback Callback function for the timer pulse (funcref or method name).
|
||||
-- @param delay Delay for the timer, in seconds.
|
||||
-- @param ... An optional, unlimited amount of arguments to pass to the callback function.
|
||||
-- @usage
|
||||
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
|
||||
--
|
||||
-- function MyAddOn:OnEnable()
|
||||
-- self:ScheduleTimer("TimerFeedback", 5)
|
||||
-- end
|
||||
--
|
||||
-- function MyAddOn:TimerFeedback()
|
||||
-- print("5 seconds passed")
|
||||
-- end
|
||||
function AceTimer:ScheduleTimer(func, delay, ...)
|
||||
if not func or not delay then
|
||||
error(MAJOR..": ScheduleTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
|
||||
end
|
||||
if type(func) == "string" then
|
||||
if type(self) ~= "table" then
|
||||
error(MAJOR..": ScheduleTimer(callback, delay, args...): 'self' - must be a table.", 2)
|
||||
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)
|
||||
end
|
||||
end
|
||||
return new(self, nil, func, delay, ...)
|
||||
end
|
||||
|
||||
--- Schedule a repeating timer.
|
||||
-- The timer will fire every `delay` seconds, until canceled.
|
||||
-- @param callback Callback function for the timer pulse (funcref or method name).
|
||||
-- @param delay Delay for the timer, in seconds.
|
||||
-- @param ... An optional, unlimited amount of arguments to pass to the callback function.
|
||||
-- @usage
|
||||
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
|
||||
--
|
||||
-- function MyAddOn:OnEnable()
|
||||
-- self.timerCount = 0
|
||||
-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
|
||||
-- end
|
||||
--
|
||||
-- function MyAddOn:TimerFeedback()
|
||||
-- self.timerCount = self.timerCount + 1
|
||||
-- print(("%d seconds passed"):format(5 * self.timerCount))
|
||||
-- -- run 30 seconds in total
|
||||
-- if self.timerCount == 6 then
|
||||
-- self:CancelTimer(self.testTimer)
|
||||
-- end
|
||||
-- end
|
||||
function AceTimer:ScheduleRepeatingTimer(func, delay, ...)
|
||||
if not func or not delay then
|
||||
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
|
||||
end
|
||||
if type(func) == "string" then
|
||||
if type(self) ~= "table" then
|
||||
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'self' - must be a table.", 2)
|
||||
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)
|
||||
end
|
||||
end
|
||||
return new(self, true, func, delay, ...)
|
||||
end
|
||||
|
||||
--- 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
|
||||
-- and the timer has not fired yet or was canceled before.
|
||||
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
|
||||
function AceTimer:CancelTimer(id)
|
||||
local timer = activeTimers[id]
|
||||
|
||||
if not timer then
|
||||
return false
|
||||
else
|
||||
timer.cancelled = true
|
||||
activeTimers[id] = nil
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
--- Cancels all timers registered to the current addon object ('self')
|
||||
function AceTimer:CancelAllTimers()
|
||||
for k,v in pairs(activeTimers) do
|
||||
if v.object == self then
|
||||
AceTimer.CancelTimer(self, k)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- 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.
|
||||
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
|
||||
-- @return The time left on the timer.
|
||||
function AceTimer:TimeLeft(id)
|
||||
local timer = activeTimers[id]
|
||||
if not timer then
|
||||
return 0
|
||||
else
|
||||
return timer.ends - GetTime()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Upgrading
|
||||
|
||||
-- Upgrade from old hash-bucket based timers to C_Timer.After timers.
|
||||
if oldminor and oldminor < 10 then
|
||||
-- disable old timer logic
|
||||
AceTimer.frame:SetScript("OnUpdate", nil)
|
||||
AceTimer.frame:SetScript("OnEvent", nil)
|
||||
AceTimer.frame:UnregisterAllEvents()
|
||||
-- convert timers
|
||||
for object,timers in pairs(AceTimer.selfs) do
|
||||
for handle,timer in pairs(timers) do
|
||||
if type(timer) == "table" and timer.callback then
|
||||
local newTimer
|
||||
if timer.delay then
|
||||
newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.callback, timer.delay, timer.arg)
|
||||
else
|
||||
newTimer = AceTimer.ScheduleTimer(timer.object, timer.callback, timer.when - GetTime(), timer.arg)
|
||||
end
|
||||
-- Use the old handle for old timers
|
||||
activeTimers[newTimer] = nil
|
||||
activeTimers[handle] = newTimer
|
||||
newTimer.handle = handle
|
||||
end
|
||||
end
|
||||
end
|
||||
AceTimer.selfs = nil
|
||||
AceTimer.hash = nil
|
||||
AceTimer.debug = nil
|
||||
elseif oldminor and oldminor < 17 then
|
||||
-- Upgrade from old animation based timers to C_Timer.After timers.
|
||||
AceTimer.inactiveTimers = nil
|
||||
AceTimer.frame = nil
|
||||
local oldTimers = AceTimer.activeTimers
|
||||
-- Clear old timer table and update upvalue
|
||||
AceTimer.activeTimers = {}
|
||||
activeTimers = AceTimer.activeTimers
|
||||
for handle, timer in pairs(oldTimers) do
|
||||
local newTimer
|
||||
-- Stop the old timer animation
|
||||
local duration, elapsed = timer:GetDuration(), timer:GetElapsed()
|
||||
timer:GetParent():Stop()
|
||||
if timer.looping then
|
||||
newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.func, duration, unpack(timer.args, 1, timer.argsCount))
|
||||
else
|
||||
newTimer = AceTimer.ScheduleTimer(timer.object, timer.func, duration - elapsed, unpack(timer.args, 1, timer.argsCount))
|
||||
end
|
||||
-- Use the old handle for old timers
|
||||
activeTimers[newTimer] = nil
|
||||
activeTimers[handle] = newTimer
|
||||
newTimer.handle = handle
|
||||
end
|
||||
|
||||
-- Migrate transitional handles
|
||||
if oldminor < 13 and AceTimer.hashCompatTable then
|
||||
for handle, id in pairs(AceTimer.hashCompatTable) do
|
||||
local t = activeTimers[id]
|
||||
if t then
|
||||
activeTimers[id] = nil
|
||||
activeTimers[handle] = t
|
||||
t.handle = handle
|
||||
end
|
||||
end
|
||||
AceTimer.hashCompatTable = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Embed handling
|
||||
|
||||
AceTimer.embeds = AceTimer.embeds or {}
|
||||
|
||||
local mixins = {
|
||||
"ScheduleTimer", "ScheduleRepeatingTimer",
|
||||
"CancelTimer", "CancelAllTimers",
|
||||
"TimeLeft"
|
||||
}
|
||||
|
||||
function AceTimer:Embed(target)
|
||||
AceTimer.embeds[target] = true
|
||||
for _,v in pairs(mixins) do
|
||||
target[v] = AceTimer[v]
|
||||
end
|
||||
return target
|
||||
end
|
||||
|
||||
-- AceTimer:OnEmbedDisable(target)
|
||||
-- target (object) - target object that AceTimer is embedded in.
|
||||
--
|
||||
-- cancel all timers registered for the object
|
||||
function AceTimer:OnEmbedDisable(target)
|
||||
target:CancelAllTimers()
|
||||
end
|
||||
|
||||
for addon in pairs(AceTimer.embeds) do
|
||||
AceTimer:Embed(addon)
|
||||
end
|
||||
@ -0,0 +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/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceTimer-3.0.lua"/>
|
||||
</Ui>
|
||||
@ -0,0 +1,30 @@
|
||||
-- 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
|
||||
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]
|
||||
|
||||
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
|
||||
LibStub = LibStub or {libs = {}, minors = {} }
|
||||
_G[LIBSTUB_MAJOR] = LibStub
|
||||
LibStub.minor = LIBSTUB_MINOR
|
||||
|
||||
function LibStub:NewLibrary(major, minor)
|
||||
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.")
|
||||
|
||||
local oldminor = self.minors[major]
|
||||
if oldminor and oldminor >= minor then return nil end
|
||||
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
|
||||
return self.libs[major], oldminor
|
||||
end
|
||||
|
||||
function LibStub:GetLibrary(major, silent)
|
||||
if not self.libs[major] and not silent then
|
||||
error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
|
||||
end
|
||||
return self.libs[major], self.minors[major]
|
||||
end
|
||||
|
||||
function LibStub:IterateLibraries() return pairs(self.libs) end
|
||||
setmetatable(LibStub, { __call = LibStub.GetLibrary })
|
||||
end
|
||||
382
Artemis/Artemis/Modules/Games/WoW/Resources/Addon/Libs/json.lua
Normal file
382
Artemis/Artemis/Modules/Games/WoW/Resources/Addon/Libs/json.lua
Normal file
@ -0,0 +1,382 @@
|
||||
local json = LibStub:NewLibrary("json", 1)
|
||||
if not json then return end
|
||||
|
||||
--
|
||||
-- json.lua
|
||||
--
|
||||
-- Copyright (c) 2015 rxi
|
||||
--
|
||||
-- This library is free software; you can redistribute it and/or modify it
|
||||
-- under the terms of the MIT license. See LICENSE for details.
|
||||
--
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Encode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local encode
|
||||
|
||||
local escape_char_map = {
|
||||
[ "\\" ] = "\\\\",
|
||||
[ "\"" ] = "\\\"",
|
||||
[ "\b" ] = "\\b",
|
||||
[ "\f" ] = "\\f",
|
||||
[ "\n" ] = "\\n",
|
||||
[ "\r" ] = "\\r",
|
||||
[ "\t" ] = "\\t",
|
||||
}
|
||||
|
||||
local escape_char_map_inv = { [ "\\/" ] = "/" }
|
||||
for k, v in pairs(escape_char_map) do
|
||||
escape_char_map_inv[v] = k
|
||||
end
|
||||
|
||||
|
||||
local function escape_char(c)
|
||||
return escape_char_map[c] or string.format("\\u%04x", c:byte())
|
||||
end
|
||||
|
||||
|
||||
local function encode_nil(val)
|
||||
return "null"
|
||||
end
|
||||
|
||||
|
||||
local function encode_table(val, stack)
|
||||
local res = {}
|
||||
stack = stack or {}
|
||||
|
||||
-- Circular reference?
|
||||
if stack[val] then error("circular reference") end
|
||||
|
||||
stack[val] = true
|
||||
|
||||
if val[1] ~= nil or next(val) == nil then
|
||||
-- Treat as array -- check keys are valid and it is not sparse
|
||||
local n = 0
|
||||
for k in pairs(val) do
|
||||
if type(k) ~= "number" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
n = n + 1
|
||||
end
|
||||
if n ~= #val then
|
||||
error("invalid table: sparse array")
|
||||
end
|
||||
-- Encode
|
||||
for i, v in ipairs(val) do
|
||||
table.insert(res, encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "[" .. table.concat(res, ",") .. "]"
|
||||
|
||||
else
|
||||
-- Treat as an object
|
||||
for k, v in pairs(val) do
|
||||
if type(k) ~= "string" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "{" .. table.concat(res, ",") .. "}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function encode_string(val)
|
||||
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||
end
|
||||
|
||||
|
||||
local function encode_number(val)
|
||||
-- Check for NaN, -inf and inf
|
||||
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||
error("unexpected number value '" .. tostring(val) .. "'")
|
||||
end
|
||||
return string.format("%.14g", val)
|
||||
end
|
||||
|
||||
|
||||
local type_func_map = {
|
||||
[ "nil" ] = encode_nil,
|
||||
[ "table" ] = encode_table,
|
||||
[ "string" ] = encode_string,
|
||||
[ "number" ] = encode_number,
|
||||
[ "boolean" ] = tostring,
|
||||
}
|
||||
|
||||
|
||||
encode = function(val, stack)
|
||||
local t = type(val)
|
||||
local f = type_func_map[t]
|
||||
if f then
|
||||
return f(val, stack)
|
||||
end
|
||||
error("unexpected type '" .. t .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.encode(val)
|
||||
return ( encode(val) )
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Decode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local parse
|
||||
|
||||
local function create_set(...)
|
||||
local res = {}
|
||||
for i = 1, select("#", ...) do
|
||||
res[ select(i, ...) ] = true
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local space_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 literals = create_set("true", "false", "null")
|
||||
|
||||
local literal_map = {
|
||||
[ "true" ] = true,
|
||||
[ "false" ] = false,
|
||||
[ "null" ] = nil,
|
||||
}
|
||||
|
||||
|
||||
local function next_char(str, idx, set, negate)
|
||||
for i = idx, #str do
|
||||
if set[str:sub(i, i)] ~= negate then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #str + 1
|
||||
end
|
||||
|
||||
|
||||
local function decode_error(str, idx, msg)
|
||||
local line_count = 1
|
||||
local col_count = 1
|
||||
for i = 1, idx - 1 do
|
||||
col_count = col_count + 1
|
||||
if str:sub(i, i) == "\n" then
|
||||
line_count = line_count + 1
|
||||
col_count = 1
|
||||
end
|
||||
end
|
||||
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||
end
|
||||
|
||||
|
||||
local function codepoint_to_utf8(n)
|
||||
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||
local f = math.floor
|
||||
if n <= 0x7f then
|
||||
return string.char(n)
|
||||
elseif n <= 0x7ff then
|
||||
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||
elseif n <= 0xffff then
|
||||
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
elseif n <= 0x10ffff then
|
||||
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
end
|
||||
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||
end
|
||||
|
||||
|
||||
local function parse_unicode_escape(s)
|
||||
local n1 = tonumber( s:sub(3, 6), 16 )
|
||||
local n2 = tonumber( s:sub(9, 12), 16 )
|
||||
-- Surrogate pair?
|
||||
if n2 then
|
||||
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||
else
|
||||
return codepoint_to_utf8(n1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function parse_string(str, i)
|
||||
local has_unicode_escape = false
|
||||
local has_surrogate_escape = false
|
||||
local has_escape = false
|
||||
local last
|
||||
for j = i + 1, #str do
|
||||
local x = str:byte(j)
|
||||
|
||||
if x < 32 then
|
||||
decode_error(str, j, "control character in string")
|
||||
end
|
||||
|
||||
if last == 92 then -- "\\" (escape char)
|
||||
if x == 117 then -- "u" (unicode escape sequence)
|
||||
local hex = str:sub(j + 1, j + 5)
|
||||
if not hex:find("%x%x%x%x") then
|
||||
decode_error(str, j, "invalid unicode escape in string")
|
||||
end
|
||||
if hex:find("^[dD][89aAbB]") then
|
||||
has_surrogate_escape = true
|
||||
else
|
||||
has_unicode_escape = true
|
||||
end
|
||||
else
|
||||
local c = string.char(x)
|
||||
if not escape_chars[c] then
|
||||
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
|
||||
end
|
||||
has_escape = true
|
||||
end
|
||||
last = nil
|
||||
|
||||
elseif x == 34 then -- '"' (end of string)
|
||||
local s = str:sub(i + 1, j - 1)
|
||||
if has_surrogate_escape then
|
||||
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_unicode_escape then
|
||||
s = s:gsub("\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_escape then
|
||||
s = s:gsub("\\.", escape_char_map_inv)
|
||||
end
|
||||
return s, j + 1
|
||||
|
||||
else
|
||||
last = x
|
||||
end
|
||||
end
|
||||
decode_error(str, i, "expected closing quote for string")
|
||||
end
|
||||
|
||||
|
||||
local function parse_number(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local s = str:sub(i, x - 1)
|
||||
local n = tonumber(s)
|
||||
if not n then
|
||||
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||
end
|
||||
return n, x
|
||||
end
|
||||
|
||||
|
||||
local function parse_literal(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local word = str:sub(i, x - 1)
|
||||
if not literals[word] then
|
||||
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||
end
|
||||
return literal_map[word], x
|
||||
end
|
||||
|
||||
|
||||
local function parse_array(str, i)
|
||||
local res = {}
|
||||
local n = 1
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local x
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of array?
|
||||
if str:sub(i, i) == "]" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read token
|
||||
x, i = parse(str, i)
|
||||
res[n] = x
|
||||
n = n + 1
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "]" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local function parse_object(str, i)
|
||||
local res = {}
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local key, val
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of object?
|
||||
if str:sub(i, i) == "}" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read key
|
||||
if str:sub(i, i) ~= '"' then
|
||||
decode_error(str, i, "expected string for key")
|
||||
end
|
||||
key, i = parse(str, i)
|
||||
-- Read ':' delimiter
|
||||
i = next_char(str, i, space_chars, true)
|
||||
if str:sub(i, i) ~= ":" then
|
||||
decode_error(str, i, "expected ':' after key")
|
||||
end
|
||||
i = next_char(str, i + 1, space_chars, true)
|
||||
-- Read value
|
||||
val, i = parse(str, i)
|
||||
-- Set
|
||||
res[key] = val
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "}" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local char_func_map = {
|
||||
[ '"' ] = parse_string,
|
||||
[ "0" ] = parse_number,
|
||||
[ "1" ] = parse_number,
|
||||
[ "2" ] = parse_number,
|
||||
[ "3" ] = parse_number,
|
||||
[ "4" ] = parse_number,
|
||||
[ "5" ] = parse_number,
|
||||
[ "6" ] = parse_number,
|
||||
[ "7" ] = parse_number,
|
||||
[ "8" ] = parse_number,
|
||||
[ "9" ] = parse_number,
|
||||
[ "-" ] = parse_number,
|
||||
[ "t" ] = parse_literal,
|
||||
[ "f" ] = parse_literal,
|
||||
[ "n" ] = parse_literal,
|
||||
[ "[" ] = parse_array,
|
||||
[ "{" ] = parse_object,
|
||||
}
|
||||
|
||||
|
||||
parse = function(str, idx)
|
||||
local chr = str:sub(idx, idx)
|
||||
local f = char_func_map[chr]
|
||||
if f then
|
||||
return f(str, idx)
|
||||
end
|
||||
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.decode(str)
|
||||
if type(str) ~= "string" then
|
||||
error("expected argument of type string, got " .. type(str))
|
||||
end
|
||||
return ( parse(str, next_char(str, 1, space_chars, true)) )
|
||||
end
|
||||
|
||||
|
||||
return json
|
||||
@ -0,0 +1,9 @@
|
||||
<Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||
<Script file="Libs\LibStub\LibStub.lua"/>
|
||||
<Include file="Libs\AceAddon-3.0\AceAddon-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\AceTimer-3.0\AceTimer-3.0.xml"/>
|
||||
<Include file="Libs\AceComm-3.0\AceComm-3.0.xml"/>
|
||||
<Include file="Libs\json.lua"/>
|
||||
</Ui>
|
||||
@ -1,106 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.DAL;
|
||||
using Artemis.DAL;
|
||||
using Artemis.Managers;
|
||||
using Artemis.Modules.Abstract;
|
||||
using Artemis.Modules.Games.WoW.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PcapDotNet.Core;
|
||||
using PcapDotNet.Packets;
|
||||
using static Artemis.Modules.Games.WoW.WowPacketScanner;
|
||||
|
||||
namespace Artemis.Modules.Games.WoW
|
||||
{
|
||||
public class WoWModel : ModuleModel
|
||||
{
|
||||
private readonly Regex _rgx;
|
||||
private PacketCommunicator _communicator;
|
||||
private readonly WowPacketScanner _packetScanner;
|
||||
|
||||
public WoWModel(DeviceManager deviceManager, LuaManager luaManager) : base(deviceManager, luaManager)
|
||||
public WoWModel(DeviceManager deviceManager, LuaManager luaManager, WowPacketScanner packetScanner) : base(deviceManager, luaManager)
|
||||
{
|
||||
Settings = SettingsProvider.Load<WoWSettings>();
|
||||
DataModel = new WoWDataModel();
|
||||
ProcessNames.Add("Wow-64");
|
||||
|
||||
_rgx = new Regex("(artemis)\\((.*?)\\)", RegexOptions.Compiled);
|
||||
_packetScanner = packetScanner;
|
||||
_packetScanner.RaiseDataReceived += (sender, args) => HandleGameData(args.Command, args.Data);
|
||||
}
|
||||
|
||||
public override string Name => "WoW";
|
||||
public override bool IsOverlay => false;
|
||||
public override bool IsBoundToProcess => true;
|
||||
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
// Start scanning WoW packets
|
||||
// Retrieve the device list from the local machine
|
||||
IList<LivePacketDevice> allDevices = LivePacketDevice.AllLocalMachine;
|
||||
|
||||
if (allDevices.Count == 0)
|
||||
{
|
||||
Logger.Warn("No interfaces found! Can't scan WoW packets.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Take the selected adapter
|
||||
PacketDevice selectedDevice = allDevices.First();
|
||||
|
||||
// Open the device
|
||||
_communicator = selectedDevice.Open(65536, PacketDeviceOpenAttributes.Promiscuous, 40);
|
||||
Logger.Debug("Listening on " + selectedDevice.Description + " for WoW packets");
|
||||
|
||||
// Compile the filter
|
||||
using (var filter = _communicator.CreateFilter("tcp"))
|
||||
{
|
||||
// Set the filter
|
||||
_communicator.SetFilter(filter);
|
||||
}
|
||||
|
||||
Task.Run(() => ReceivePackets());
|
||||
_packetScanner.Start();
|
||||
base.Enable();
|
||||
}
|
||||
|
||||
private void ReceivePackets()
|
||||
public override void Dispose()
|
||||
{
|
||||
// start the capture
|
||||
try
|
||||
{
|
||||
_communicator.ReceivePackets(0, PacketHandler);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// ignored, happens on shutdown
|
||||
}
|
||||
_packetScanner.Stop();
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
private void PacketHandler(Packet packet)
|
||||
public override void Update()
|
||||
{
|
||||
// Ignore duplicates
|
||||
if (packet.Ethernet.IpV4.Udp.SourcePort == 3724)
|
||||
return;
|
||||
var dataModel = (WoWDataModel) DataModel;
|
||||
|
||||
var str = Encoding.Default.GetString(packet.Buffer);
|
||||
if (str.ToLower().Contains("artemis"))
|
||||
{
|
||||
var match = _rgx.Match(str);
|
||||
if (match.Groups.Count != 3)
|
||||
return;
|
||||
|
||||
Logger.Trace("[{0}] {1}", packet.Ethernet.IpV4.Udp.SourcePort, match.Groups[2].Value);
|
||||
// Get the command and argument
|
||||
var parts = match.Groups[2].Value.Split('|');
|
||||
HandleGameData(parts[0], parts[1]);
|
||||
}
|
||||
dataModel.Player.Update();
|
||||
dataModel.Target.Update();
|
||||
}
|
||||
|
||||
private void HandleGameData(string command, string data)
|
||||
{
|
||||
JObject json = null;
|
||||
JToken json = null;
|
||||
if (!data.StartsWith("\"") && !data.EndsWith("\""))
|
||||
json = JObject.Parse(data);
|
||||
json = JToken.Parse(data);
|
||||
|
||||
lock (DataModel)
|
||||
{
|
||||
@ -119,8 +68,11 @@ namespace Artemis.Modules.Games.WoW
|
||||
case "targetState":
|
||||
ParseTargetState(json, dataModel);
|
||||
break;
|
||||
case "auras":
|
||||
ParseAuras(json, dataModel);
|
||||
case "buffs":
|
||||
ParseAuras(json, dataModel, true);
|
||||
break;
|
||||
case "debuffs":
|
||||
ParseAuras(json, dataModel, false);
|
||||
break;
|
||||
case "spellCast":
|
||||
ParseSpellCast(json, dataModel);
|
||||
@ -141,50 +93,50 @@ namespace Artemis.Modules.Games.WoW
|
||||
}
|
||||
}
|
||||
|
||||
private void ParsePlayer(JObject json, WoWDataModel dataModel)
|
||||
private void ParsePlayer(JToken json, WoWDataModel dataModel)
|
||||
{
|
||||
dataModel.Player.ApplyJson(json);
|
||||
}
|
||||
|
||||
private void ParseTarget(JObject json, WoWDataModel dataModel)
|
||||
private void ParseTarget(JToken json, WoWDataModel dataModel)
|
||||
{
|
||||
dataModel.Target.ApplyJson(json);
|
||||
}
|
||||
|
||||
private void ParsePlayerState(JObject json, WoWDataModel dataModel)
|
||||
private void ParsePlayerState(JToken json, WoWDataModel dataModel)
|
||||
{
|
||||
dataModel.Player.ApplyStateJson(json);
|
||||
}
|
||||
|
||||
private void ParseTargetState(JObject json, WoWDataModel dataModel)
|
||||
private void ParseTargetState(JToken json, WoWDataModel dataModel)
|
||||
{
|
||||
dataModel.Target.ApplyStateJson(json);
|
||||
}
|
||||
|
||||
private void ParseAuras(JObject json, WoWDataModel dataModel)
|
||||
private void ParseAuras(JToken json, WoWDataModel dataModel, bool buffs)
|
||||
{
|
||||
dataModel.Player.ApplyAuraJson(json);
|
||||
dataModel.Player.ApplyAuraJson(json, buffs);
|
||||
}
|
||||
|
||||
private void ParseSpellCast(JObject json, WoWDataModel dataModel)
|
||||
private void ParseSpellCast(JToken json, WoWDataModel dataModel)
|
||||
{
|
||||
if (json["unitID"].Value<string>() == "player")
|
||||
if (json["uid"].Value<string>() == "player")
|
||||
dataModel.Player.CastBar.ApplyJson(json);
|
||||
else if (json["unitID"].Value<string>() == "target")
|
||||
else if (json["uid"].Value<string>() == "target")
|
||||
dataModel.Target.CastBar.ApplyJson(json);
|
||||
}
|
||||
|
||||
private void ParseInstantSpellCast(JObject json, WoWDataModel dataModel)
|
||||
private void ParseInstantSpellCast(JToken json, WoWDataModel dataModel)
|
||||
{
|
||||
var spell = new WoWSpell
|
||||
{
|
||||
Name = json["name"].Value<string>(),
|
||||
Id = json["spellID"].Value<int>()
|
||||
Name = json["n"].Value<string>(),
|
||||
Id = json["sid"].Value<int>()
|
||||
};
|
||||
|
||||
if (json["unitID"].Value<string>() == "player")
|
||||
if (json["uid"].Value<string>() == "player")
|
||||
dataModel.Player.AddInstantCast(spell);
|
||||
else if (json["unitID"].Value<string>() == "target")
|
||||
else if (json["uid"].Value<string>() == "target")
|
||||
dataModel.Target.AddInstantCast(spell);
|
||||
}
|
||||
|
||||
@ -203,20 +155,5 @@ namespace Artemis.Modules.Games.WoW
|
||||
else if (data == "\"target\"")
|
||||
dataModel.Target.CastBar.Clear();
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_communicator.Break();
|
||||
_communicator.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
var dataModel = (WoWDataModel) DataModel;
|
||||
|
||||
dataModel.Player.Update();
|
||||
dataModel.Target.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
125
Artemis/Artemis/Modules/Games/WoW/WoWPacketScanner.cs
Normal file
125
Artemis/Artemis/Modules/Games/WoW/WoWPacketScanner.cs
Normal file
@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Ninject.Extensions.Logging;
|
||||
using PcapDotNet.Core;
|
||||
using PcapDotNet.Packets;
|
||||
|
||||
namespace Artemis.Modules.Games.WoW
|
||||
{
|
||||
public class WowPacketScanner
|
||||
{
|
||||
private const string MsgStart = "\u0001";
|
||||
private const string MsgNext = "\u0002";
|
||||
private const string MsgLast = "\u0003";
|
||||
private PacketCommunicator _communicator;
|
||||
private string _dataParts;
|
||||
|
||||
public WowPacketScanner(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; }
|
||||
|
||||
public event EventHandler<WowDataReceivedEventArgs> RaiseDataReceived;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// Start scanning WoW packets
|
||||
// Retrieve the device list from the local machine
|
||||
IList<LivePacketDevice> allDevices = LivePacketDevice.AllLocalMachine;
|
||||
|
||||
if (allDevices.Count == 0)
|
||||
{
|
||||
Logger.Warn("No interfaces found! Can't scan WoW packets.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Take the selected adapter
|
||||
PacketDevice selectedDevice = allDevices.First();
|
||||
|
||||
// Open the device
|
||||
_communicator = selectedDevice.Open(65536, PacketDeviceOpenAttributes.Promiscuous, 40);
|
||||
Logger.Debug("Listening on " + selectedDevice.Description + " for WoW packets");
|
||||
|
||||
// Compile the filter
|
||||
using (var filter = _communicator.CreateFilter("tcp"))
|
||||
{
|
||||
// Set the filter
|
||||
_communicator.SetFilter(filter);
|
||||
}
|
||||
|
||||
Task.Run(() => ReceivePackets());
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_communicator?.Break();
|
||||
_communicator?.Dispose();
|
||||
_communicator = null;
|
||||
}
|
||||
|
||||
private void ReceivePackets()
|
||||
{
|
||||
// start the capture
|
||||
try
|
||||
{
|
||||
_communicator.ReceivePackets(0, PacketHandler);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// ignored, happens on shutdown
|
||||
}
|
||||
}
|
||||
|
||||
// Callback function invoked by Pcap.Net for every incoming packet
|
||||
private void PacketHandler(Packet packet)
|
||||
{
|
||||
var str = Encoding.Default.GetString(packet.Buffer);
|
||||
if (!str.ToLower().Contains("artemis"))
|
||||
return;
|
||||
|
||||
// Split the string at the prefix
|
||||
var parts = str.Split(new[] {"(artemis)"}, StringSplitOptions.None);
|
||||
var msg = parts[1];
|
||||
// Start escape char
|
||||
if (msg.StartsWith(MsgStart))
|
||||
_dataParts = msg.Substring(1);
|
||||
else if (msg.StartsWith(MsgNext))
|
||||
_dataParts = _dataParts + msg.Substring(1);
|
||||
else if (msg.StartsWith(MsgLast))
|
||||
{
|
||||
_dataParts = _dataParts + msg.Substring(1);
|
||||
var dataParts = _dataParts.Split('|');
|
||||
// Data is wrapped in artemis(), take this off
|
||||
OnRaiseDataReceived(dataParts[0].Substring(8), dataParts[1].Substring(0, dataParts[1].Length - 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
public class WowDataReceivedEventArgs : EventArgs
|
||||
{
|
||||
public WowDataReceivedEventArgs(string command, string data)
|
||||
{
|
||||
Command = command;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public string Command { get; }
|
||||
public string Data { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ using Artemis.Managers;
|
||||
using Artemis.Modules.Abstract;
|
||||
using Artemis.Utilities;
|
||||
using Artemis.Utilities.ActiveWindowDetection;
|
||||
using Betwixt;
|
||||
using CSCore.CoreAudioAPI;
|
||||
using Newtonsoft.Json;
|
||||
using SpotifyAPI.Local;
|
||||
@ -28,7 +29,7 @@ namespace Artemis.Modules.General.GeneralProfile
|
||||
AudioCaptureManager audioCaptureManager) : base(deviceManager, luaManager)
|
||||
{
|
||||
_lastMusicUpdate = DateTime.Now;
|
||||
|
||||
|
||||
Settings = SettingsProvider.Load<GeneralProfileSettings>();
|
||||
DataModel = new GeneralProfileDataModel();
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ using Artemis.Profiles.Layers.Interfaces;
|
||||
using Artemis.Profiles.Layers.Types.Keyboard;
|
||||
using Artemis.Utilities;
|
||||
using Artemis.Utilities.ParentChild;
|
||||
using Betwixt;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Artemis.Profiles.Layers.Models
|
||||
@ -26,8 +27,14 @@ namespace Artemis.Profiles.Layers.Models
|
||||
var model = Properties as KeyboardPropertiesModel;
|
||||
if (model != null)
|
||||
GifImage = new GifImage(model.GifFile, Properties.AnimationSpeed);
|
||||
|
||||
LayerConditionsMet += OnLayerConditionsMet;
|
||||
LayerConditionsUnmet += OnLayerConditionsUnmet;
|
||||
}
|
||||
|
||||
public event EventHandler<EventArgs> LayerConditionsMet;
|
||||
public event EventHandler<EventArgs> LayerConditionsUnmet;
|
||||
|
||||
[JsonIgnore]
|
||||
public ImageSource LayerImage => LayerType.DrawThumbnail(this);
|
||||
|
||||
@ -43,7 +50,18 @@ namespace Artemis.Profiles.Layers.Models
|
||||
public bool AreConditionsMet(ModuleDataModel dataModel)
|
||||
{
|
||||
// Conditions are not even checked if the layer isn't enabled
|
||||
return Enabled && LayerCondition.ConditionsMet(this, dataModel);
|
||||
if (!Enabled)
|
||||
return false;
|
||||
|
||||
_fadeTweener?.Update(40);
|
||||
var conditionsMet = LayerCondition.ConditionsMet(this, dataModel);
|
||||
if (conditionsMet && !_conditionsMetLastFrame)
|
||||
OnLayerConditionsMet();
|
||||
if (!conditionsMet && _conditionsMetLastFrame)
|
||||
OnLayerConditionsUnmet();
|
||||
|
||||
_conditionsMetLastFrame = conditionsMet;
|
||||
return _fadeTweener != null && _fadeTweener.Running || conditionsMet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -56,7 +74,7 @@ namespace Artemis.Profiles.Layers.Models
|
||||
{
|
||||
if (LayerType == null)
|
||||
return;
|
||||
|
||||
|
||||
LayerType.Update(this, dataModel, preview);
|
||||
LayerAnimation?.Update(this, updateAnimations);
|
||||
|
||||
@ -96,7 +114,12 @@ namespace Artemis.Profiles.Layers.Models
|
||||
if (Brush == null || !preview && !RenderAllowed)
|
||||
return;
|
||||
|
||||
// If fading in/out, push transparency on the entire context
|
||||
if (_fadeTweener != null && _fadeTweener.Running)
|
||||
c.PushOpacity(_fadeTweener.Value);
|
||||
LayerType.Draw(this, c);
|
||||
if (_fadeTweener != null && _fadeTweener.Running)
|
||||
c.Pop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -105,7 +128,7 @@ namespace Artemis.Profiles.Layers.Models
|
||||
public void SetupProperties()
|
||||
{
|
||||
LayerType.SetupProperties(this);
|
||||
|
||||
|
||||
// If the type is an event, set it up
|
||||
if (IsEvent && EventProperties == null)
|
||||
EventProperties = new KeyboardEventPropertiesModel
|
||||
@ -303,6 +326,29 @@ namespace Artemis.Profiles.Layers.Models
|
||||
keybindModel.Unregister();
|
||||
}
|
||||
|
||||
private void OnLayerConditionsMet(object sender, EventArgs eventArgs)
|
||||
{
|
||||
if (FadeInTime <= 0)
|
||||
return;
|
||||
if (_fadeTweener != null && _fadeTweener.Running)
|
||||
_fadeTweener = new Tweener<float>(_fadeTweener.Value, 1, FadeInTime, Ease.Quint.Out, TweenModel.LerpFuncFloat);
|
||||
else
|
||||
_fadeTweener = new Tweener<float>(0, 1, FadeInTime, Ease.Quint.Out, TweenModel.LerpFuncFloat);
|
||||
_fadeTweener.Start();
|
||||
}
|
||||
|
||||
private void OnLayerConditionsUnmet(object sender, EventArgs eventArgs)
|
||||
{
|
||||
if (FadeOutTime <= 0)
|
||||
return;
|
||||
|
||||
if (_fadeTweener != null && _fadeTweener.Running)
|
||||
_fadeTweener = new Tweener<float>(_fadeTweener.Value, 0, FadeOutTime, Ease.Quint.In, TweenModel.LerpFuncFloat);
|
||||
else
|
||||
_fadeTweener = new Tweener<float>(1, 0, FadeOutTime, Ease.Quint.In, TweenModel.LerpFuncFloat);
|
||||
_fadeTweener.Start();
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
#region Layer type properties
|
||||
@ -321,6 +367,8 @@ namespace Artemis.Profiles.Layers.Models
|
||||
public bool RenderAllowed { get; set; }
|
||||
public bool Expanded { get; set; }
|
||||
public bool IsEvent { get; set; }
|
||||
public double FadeInTime { get; set; }
|
||||
public double FadeOutTime { get; set; }
|
||||
public LayerPropertiesModel Properties { get; set; }
|
||||
public EventPropertiesModel EventProperties { get; set; }
|
||||
|
||||
@ -341,6 +389,8 @@ namespace Artemis.Profiles.Layers.Models
|
||||
#region Render properties
|
||||
|
||||
[JsonIgnore] private Brush _brush;
|
||||
[JsonIgnore] private bool _conditionsMetLastFrame;
|
||||
[JsonIgnore] private Tweener<float> _fadeTweener;
|
||||
|
||||
[JsonIgnore]
|
||||
public double X { get; set; }
|
||||
@ -390,7 +440,7 @@ namespace Artemis.Profiles.Layers.Models
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime LastRender { get; set; }
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
@ -416,5 +466,15 @@ namespace Artemis.Profiles.Layers.Models
|
||||
{
|
||||
return $"{nameof(Name)}: {Name}, {nameof(Order)}: {Order}, {nameof(X)}: {X}, {nameof(Y)}: {Y}, {nameof(Width)}: {Width}, {nameof(Height)}: {Height}";
|
||||
}
|
||||
|
||||
protected virtual void OnLayerConditionsMet()
|
||||
{
|
||||
LayerConditionsMet?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected virtual void OnLayerConditionsUnmet()
|
||||
{
|
||||
LayerConditionsUnmet?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +136,7 @@ namespace Artemis.Profiles.Layers.Models
|
||||
}
|
||||
}
|
||||
|
||||
private static float LerpFuncFloat(float start, float end, float percent)
|
||||
public static float LerpFuncFloat(float start, float end, float percent)
|
||||
{
|
||||
return start + (end - start) * percent;
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Layer name -->
|
||||
@ -66,6 +67,14 @@
|
||||
<Label Grid.Row="0" Grid.Column="4" Content="Is event: " VerticalAlignment="Center" />
|
||||
<controls:ToggleSwitchButton Grid.Row="0" Grid.Column="5" VerticalAlignment="Center" IsChecked="{Binding Path=ProposedLayer.IsEvent, Mode=TwoWay}" cal:Message.Attach="[Event Click] = [Action ToggleIsEvent]"
|
||||
Style="{StaticResource MahApps.Metro.Styles.ToggleSwitchButton.Win10}" HorizontalAlignment="Left" Margin="10" />
|
||||
|
||||
<!-- Fade in -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="Fade in:" VerticalAlignment="Center" />
|
||||
<controls:NumericUpDown Grid.Row="1" Grid.Column="1" Margin="10" Height="22" Value="{Binding Path=ProposedLayer.FadeInTime}" VerticalAlignment="Center" />
|
||||
|
||||
<!-- Fade out -->
|
||||
<Label Grid.Row="1" Grid.Column="2" Content="Fade out:" VerticalAlignment="Center" />
|
||||
<controls:NumericUpDown Grid.Row="1" Grid.Column="3" Margin="10" Height="22" Value="{Binding Path=ProposedLayer.FadeOutTime}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!-- Advanced -->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user