Compare commits
141 Commits
Author | SHA1 | Date | |
---|---|---|---|
a01dd5dace | |||
b6f0fa9776 | |||
cb03f8e23c | |||
fbff094227 | |||
e08cf455ee | |||
de57b4c8a8 | |||
9fef1da261 | |||
1f6a1a49a6 | |||
fddd294a03 | |||
00ffe6973e | |||
f73096e439 | |||
e26ab02862 | |||
c2b9c1267b | |||
a400a85dc9 | |||
f5a4b49935 | |||
2dabe7959f | |||
dcfc406481 | |||
f680d36d2b | |||
d44aa04e44 | |||
4594dff4a6 | |||
be1093d51f | |||
546aa27bb1 | |||
dfb2f687d0 | |||
9e344aed64 | |||
ba77555cab | |||
e8e15444c9 | |||
cdd859ba50 | |||
ab85b3712a | |||
fc3732fd0c | |||
4652c60e64 | |||
e425fa9209 | |||
f1e2f60836 | |||
db7edaf802 | |||
58a7ecd723 | |||
bff1a4acf9 | |||
b287fb41c4 | |||
5e0f81ce53 | |||
bd8b3fa00f | |||
899fb888c3 | |||
2d3820952f | |||
f6b043fa39 | |||
8bce840497 | |||
aff3e83bab | |||
85a28bc7ca | |||
452aa2e3a0 | |||
784cee6f04 | |||
ca30998a5a | |||
476adcdc2e | |||
2c6142e6c4 | |||
cae1eef659 | |||
2dfb60e63d | |||
8524a1116a | |||
60ccbc72bb | |||
8d3813f3ee | |||
aa46000abf | |||
6059c16a6e | |||
2e805abef7 | |||
06f143915c | |||
be20aa77b9 | |||
7b2c67d130 | |||
9480c42181 | |||
17c163c71c | |||
3ee90fb767 | |||
7f9476236d | |||
2d2cf621bd | |||
bb1acd5003 | |||
c00ddd410a | |||
35c51143bc | |||
16cd2f82be | |||
34e027525f | |||
331823e34f | |||
18daa170c5 | |||
7e8978044f | |||
2811396234 | |||
212ce2c71c | |||
f76ef718ed | |||
bd40d43686 | |||
2954a93b4d | |||
93d1d55cfa | |||
1c7951a530 | |||
84a53ea065 | |||
9dcf526b4c | |||
032cfc5bff | |||
5c9450d06d | |||
f811dd9a6c | |||
d13da3141d | |||
d4bab870a4 | |||
6ef7e74402 | |||
55ce001705 | |||
77c3fc291d | |||
ff849092ff | |||
d0cb074912 | |||
fdedde829e | |||
4364d87bf7 | |||
2232f1a92d | |||
251e11a7e8 | |||
d4e4290dc5 | |||
9fd57c5d7b | |||
873ce0a30c | |||
36297ca09d | |||
0ec3421a79 | |||
21d99ae643 | |||
4dc3335a86 | |||
c9627779ba | |||
3f1fae8906 | |||
2ae12fade0 | |||
c66c961297 | |||
8da5773dcd | |||
18106db367 | |||
5eb6f3cbfd | |||
614b07c01a | |||
2c84c326dd | |||
0ceb59c778 | |||
137ce0a3a7 | |||
59d2b999c2 | |||
d80ffbaff5 | |||
8c45e90ce1 | |||
ed7c1a4685 | |||
e32966bee2 | |||
5e779cc5f9 | |||
5e78f623f5 | |||
8e90a71dfc | |||
9ebc95885e | |||
e1136703a5 | |||
c446d1dc85 | |||
de49956aef | |||
efde43fadb | |||
af2a714b76 | |||
bfaa73b660 | |||
a7c818b88b | |||
e1fb450544 | |||
acb2910b70 | |||
0dd1a6fd69 | |||
5f3374a073 | |||
3049a0b554 | |||
fa7a411e34 | |||
6bf1c491a0 | |||
8e1f2c147e | |||
c4f4d24064 | |||
30578bdbb0 | |||
0e771a7db3 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.zip filter=lfs diff=lfs merge=lfs -text
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"Lua.diagnostics.globals": [
|
||||||
|
"UIParent"
|
||||||
|
]
|
||||||
|
}
|
330
Heimdall.lua
330
Heimdall.lua
@@ -1,39 +1,46 @@
|
|||||||
local addonname, data = ...
|
local addonname, shared = ...
|
||||||
---@cast data HeimdallData
|
---@cast shared HeimdallShared
|
||||||
---@cast addonname string
|
---@cast addonname string
|
||||||
|
|
||||||
-- TODO: Maybe make a configuration weakaura, make use of weakaura options...
|
|
||||||
-- TODO: Implement counting kills and display on whosniffer
|
-- TODO: Implement counting kills and display on whosniffer
|
||||||
-- Take last N seconds of combatlog into account ie. count who does damage to who
|
-- Take last N seconds of combatlog into account ie. count who does damage to who
|
||||||
-- Maybe even make an alert when someone does too much damage to someone else...
|
-- Maybe even make an alert when someone does too much damage to someone else...
|
||||||
-- But that would not be trivial as of now, I can't think of a way to do it sensibly
|
-- But that would not be trivial as of now, I can't think of a way to do it sensibly
|
||||||
-- TODO: Implement auto grouping via agent, maybe find "+" or something
|
|
||||||
|
|
||||||
local function init()
|
local function init()
|
||||||
---@class Heimdall_Data
|
---@class Heimdall_Data
|
||||||
---@field who { data: table<string, Player> }
|
---@field config HeimdallConfig
|
||||||
---@field stinkies table<string, boolean>
|
---@field stinkies table<string, boolean>
|
||||||
if not Heimdall_Data then Heimdall_Data = {} end
|
if not Heimdall_Data then Heimdall_Data = {} end
|
||||||
if not Heimdall_Data.config then Heimdall_Data.config = {} end
|
|
||||||
|
|
||||||
-- We don't care about these persisting
|
---@class InitTable
|
||||||
-- Actually we don't want some of them to persist
|
---@field Init fun(): nil
|
||||||
-- For those we DO we use (global) Heimdall_Data
|
|
||||||
|
|
||||||
---@class HeimdallData
|
---@class HeimdallShared
|
||||||
---@field config HeimdallConfig
|
|
||||||
---@field raceMap table<string, string>
|
---@field raceMap table<string, string>
|
||||||
---@field classColors table<string, string>
|
---@field classColors table<string, string>
|
||||||
---@field messenger HeimdallMessengerData
|
---@field messenger HeimdallMessengerData
|
||||||
---@field who HeimdallWhoData
|
---@field who HeimdallWhoData
|
||||||
|
---@field stinkyTracker HeimdallStinkyTrackerData
|
||||||
---@field dumpTable fun(table: any, depth?: number): nil
|
---@field dumpTable fun(table: any, depth?: number): nil
|
||||||
---@field utf8len fun(input: string): number
|
---@field utf8len fun(input: string): number
|
||||||
---@field padString fun(input: string, targetLength: number, left?: boolean): string
|
---@field padString fun(input: string, targetLength: number, left?: boolean): string
|
||||||
---@field GetOrDefault fun(table: table<any, any>, keys: string[], default: any): any
|
---@field GetOrDefault fun(table: table<any, any>, keys: string[], default: any): any
|
||||||
---@field Whoer { Init: fun() }
|
---@field Whoer InitTable
|
||||||
---@field Messenger { Init: fun() }
|
---@field Messenger InitTable
|
||||||
---@field Spotter { Init: fun() }
|
---@field Spotter InitTable
|
||||||
---@field DeathReporter { Init: fun() }
|
---@field DeathReporter InitTable
|
||||||
|
---@field Inviter InitTable
|
||||||
|
---@field Dueler InitTable
|
||||||
|
---@field Bully InitTable
|
||||||
|
---@field AgentTracker InitTable
|
||||||
|
---@field Emoter InitTable
|
||||||
|
---@field Echoer InitTable
|
||||||
|
---@field Macroer InitTable
|
||||||
|
---@field Commander InitTable
|
||||||
|
---@field StinkyTracker InitTable
|
||||||
|
---@field CombatAlerter InitTable
|
||||||
|
---@field Config InitTable
|
||||||
|
|
||||||
--- Config ---
|
--- Config ---
|
||||||
---@class HeimdallConfig
|
---@class HeimdallConfig
|
||||||
@@ -41,8 +48,19 @@ local function init()
|
|||||||
---@field who HeimdallWhoConfig
|
---@field who HeimdallWhoConfig
|
||||||
---@field messenger HeimdallMessengerConfig
|
---@field messenger HeimdallMessengerConfig
|
||||||
---@field deathReporter HeimdallDeathReporterConfig
|
---@field deathReporter HeimdallDeathReporterConfig
|
||||||
|
---@field inviter HeimdallInviterConfig
|
||||||
|
---@field dueler HeimdallDuelerConfig
|
||||||
|
---@field bully HeimdallBullyConfig
|
||||||
|
---@field agentTracker HeimdallAgentTrackerConfig
|
||||||
|
---@field emoter HeimdallEmoterConfig
|
||||||
|
---@field echoer HeimdallEchoerConfig
|
||||||
|
---@field macroer HeimdallMacroerConfig
|
||||||
|
---@field commander HeimdallCommanderConfig
|
||||||
|
---@field stinkyTracker HeimdallStinkyTrackerConfig
|
||||||
|
---@field combatAlerter HeimdallCombatAlerterConfig
|
||||||
---@field whisperNotify table<string, string>
|
---@field whisperNotify table<string, string>
|
||||||
---@field stinkies table<string, boolean>
|
---@field stinkies table<string, boolean>
|
||||||
|
---@field agents table<string, string>
|
||||||
|
|
||||||
---@class HeimdallSpotterConfig
|
---@class HeimdallSpotterConfig
|
||||||
---@field enabled boolean
|
---@field enabled boolean
|
||||||
@@ -64,6 +82,7 @@ local function init()
|
|||||||
|
|
||||||
---@class HeimdallMessengerConfig
|
---@class HeimdallMessengerConfig
|
||||||
---@field enabled boolean
|
---@field enabled boolean
|
||||||
|
---@field interval number
|
||||||
|
|
||||||
---@class HeimdallDeathReporterConfig
|
---@class HeimdallDeathReporterConfig
|
||||||
---@field enabled boolean
|
---@field enabled boolean
|
||||||
@@ -73,6 +92,56 @@ local function init()
|
|||||||
---@field zoneOverride string?
|
---@field zoneOverride string?
|
||||||
---@field duelThrottle number
|
---@field duelThrottle number
|
||||||
|
|
||||||
|
---@class HeimdallInviterConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field listeningChannel string
|
||||||
|
---@field keyword string
|
||||||
|
---@field allAssist boolean
|
||||||
|
---@field agentsAssist boolean
|
||||||
|
---@field throttle number
|
||||||
|
---@field kickOffline boolean
|
||||||
|
---@field cleanupInterval number
|
||||||
|
---@field afkThreshold number
|
||||||
|
|
||||||
|
---@class HeimdallDuelerConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field declineOther boolean
|
||||||
|
|
||||||
|
---@class HeimdallBullyConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
|
||||||
|
---@class HeimdallAgentTrackerConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field masterChannel string
|
||||||
|
|
||||||
|
---@class HeimdallEmoterConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field masterChannel string
|
||||||
|
---@field prefix string
|
||||||
|
|
||||||
|
---@class HeimdallEchoerConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field masterChannel string
|
||||||
|
---@field prefix string
|
||||||
|
|
||||||
|
---@class HeimdallMacroerConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field priority string[]
|
||||||
|
|
||||||
|
---@class HeimdallCommanderConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field masterChannel string
|
||||||
|
---@field commander string
|
||||||
|
---@field commands table<string, boolean>
|
||||||
|
|
||||||
|
---@class HeimdallStinkyTrackerConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field masterChannel string
|
||||||
|
|
||||||
|
---@class HeimdallCombatAlerterConfig
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field masterChannel string
|
||||||
|
|
||||||
--- Data ---
|
--- Data ---
|
||||||
---@class HeimdallMessengerData
|
---@class HeimdallMessengerData
|
||||||
---@field queue table<string, Message>
|
---@field queue table<string, Message>
|
||||||
@@ -83,7 +152,10 @@ local function init()
|
|||||||
---@field whoTicker number?
|
---@field whoTicker number?
|
||||||
---@field ignored table<string, boolean>
|
---@field ignored table<string, boolean>
|
||||||
|
|
||||||
data.GetOrDefault = function(table, keys, default)
|
---@class HeimdallStinkyTrackerData
|
||||||
|
---@field stinkies ReactiveValue
|
||||||
|
|
||||||
|
shared.GetOrDefault = function(table, keys, default)
|
||||||
local value = default
|
local value = default
|
||||||
if not table then return value end
|
if not table then return value end
|
||||||
if not keys then return value end
|
if not keys then return value end
|
||||||
@@ -104,34 +176,31 @@ local function init()
|
|||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
data.messenger = {
|
shared.messenger = {
|
||||||
queue = {}
|
queue = {}
|
||||||
}
|
}
|
||||||
data.who = {
|
shared.who = {
|
||||||
ignored = {},
|
ignored = {},
|
||||||
}
|
}
|
||||||
--/run Heimdall_Data.config = {who={enabled=true},deathReporter={enabled=true}}
|
|
||||||
--/run Heimdall_Data.config = {deathReporter={enabled=true}}
|
Heimdall_Data.config = {
|
||||||
--/run Heimdall_Data.config = {deathReporter={enabled=false},spotter={enabled=false}}
|
|
||||||
--/run Heimdall_Data.config = {deathReporter={enabled=false},spotter={enabled=true,everyone=true}}
|
|
||||||
data.config = {
|
|
||||||
spotter = {
|
spotter = {
|
||||||
enabled = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "enabled" }, true),
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "enabled" }, true),
|
||||||
everyone = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "everyone" }, false),
|
everyone = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "everyone" }, false),
|
||||||
hostile = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "hostile" }, true),
|
hostile = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "hostile" }, true),
|
||||||
alliance = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "alliance" }, true),
|
alliance = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "alliance" }, true),
|
||||||
stinky = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "stinky" }, true),
|
stinky = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "stinky" }, true),
|
||||||
notifyChannel = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "notifyChannel" }, "Agent"),
|
notifyChannel = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "notifyChannel" }, "Agent"),
|
||||||
zoneOverride = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "zoneOverride" }, nil),
|
zoneOverride = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "zoneOverride" }, nil),
|
||||||
throttleTime = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "throttleTime" }, 10)
|
throttleTime = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "throttleTime" }, 10)
|
||||||
},
|
},
|
||||||
who = {
|
who = {
|
||||||
enabled = data.GetOrDefault(Heimdall_Data, { "config", "who", "enabled" }, false),
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "who", "enabled" }, false),
|
||||||
ignored = data.GetOrDefault(Heimdall_Data, { "config", "who", "ignored" }, {}),
|
ignored = shared.GetOrDefault(Heimdall_Data, { "config", "who", "ignored" }, {}),
|
||||||
notifyChannel = data.GetOrDefault(Heimdall_Data, { "config", "who", "notifyChannel" }, "Agent"),
|
notifyChannel = shared.GetOrDefault(Heimdall_Data, { "config", "who", "notifyChannel" }, "Agent"),
|
||||||
ttl = data.GetOrDefault(Heimdall_Data, { "config", "who", "ttl" }, 10),
|
ttl = shared.GetOrDefault(Heimdall_Data, { "config", "who", "ttl" }, 20),
|
||||||
doWhisper = data.GetOrDefault(Heimdall_Data, { "config", "who", "doWhisper" }, true),
|
doWhisper = shared.GetOrDefault(Heimdall_Data, { "config", "who", "doWhisper" }, true),
|
||||||
zoneNotifyFor = data.GetOrDefault(Heimdall_Data, { "config", "who", "zoneNotifyFor" }, {
|
zoneNotifyFor = shared.GetOrDefault(Heimdall_Data, { "config", "who", "zoneNotifyFor" }, {
|
||||||
["Orgrimmar"] = true,
|
["Orgrimmar"] = true,
|
||||||
["Thunder Bluff"] = true,
|
["Thunder Bluff"] = true,
|
||||||
["Undercity"] = true,
|
["Undercity"] = true,
|
||||||
@@ -141,94 +210,73 @@ local function init()
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
messenger = {
|
messenger = {
|
||||||
enabled = data.GetOrDefault(Heimdall_Data, { "config", "messenger", "enabled" }, true),
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "messenger", "enabled" }, true),
|
||||||
|
interval = shared.GetOrDefault(Heimdall_Data, { "config", "messenger", "interval" }, 0.2),
|
||||||
},
|
},
|
||||||
deathReporter = {
|
deathReporter = {
|
||||||
enabled = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "enabled" }, false),
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "enabled" }, false),
|
||||||
throttle = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "throttle" }, 10),
|
throttle = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "throttle" }, 10),
|
||||||
doWhisper = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "doWhisper" }, true),
|
doWhisper = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "doWhisper" }, true),
|
||||||
notifyChannel = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "notifyChannel" }, "Agent"),
|
notifyChannel = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "notifyChannel" }, "Agent"),
|
||||||
zoneOverride = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "zoneOverride" }, nil),
|
zoneOverride = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "zoneOverride" }, nil),
|
||||||
duelThrottle = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "duelThrottle" }, 5),
|
duelThrottle = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "duelThrottle" }, 5),
|
||||||
|
},
|
||||||
|
whisperNotify = shared.GetOrDefault(Heimdall_Data, { "config", "whisperNotify" }, {}),
|
||||||
|
stinkies = shared.GetOrDefault(Heimdall_Data, { "config", "stinkies" }, {}),
|
||||||
|
inviter = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "enabled" }, false),
|
||||||
|
listeningChannel = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "listeningChannel" }, "Agent"),
|
||||||
|
keyword = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "keyword" }, "+"),
|
||||||
|
allAssist = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "allAssist" }, false),
|
||||||
|
agentsAssist = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "agentsAssist" }, false),
|
||||||
|
throttle = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "throttle" }, 1),
|
||||||
|
kickOffline = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "kickOffline" }, false),
|
||||||
|
cleanupInterval = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "cleanupInterval" }, 10),
|
||||||
|
afkThreshold = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "afkThreshold" }, 300),
|
||||||
|
},
|
||||||
|
dueler = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "dueler", "enabled" }, false),
|
||||||
|
declineOther = shared.GetOrDefault(Heimdall_Data, { "config", "dueler", "declineOther" }, false),
|
||||||
|
},
|
||||||
|
bully = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "bully", "enabled" }, false),
|
||||||
|
},
|
||||||
|
agentTracker = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "agentTracker", "enabled" }, false),
|
||||||
|
masterChannel = shared.GetOrDefault(Heimdall_Data, { "config", "agentTracker", "masterChannel" }, "Agent"),
|
||||||
|
},
|
||||||
|
emoter = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "enabled" }, false),
|
||||||
|
masterChannel = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "masterChannel" }, "Agent"),
|
||||||
|
prefix = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "prefix" }, ""),
|
||||||
|
},
|
||||||
|
echoer = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "enabled" }, false),
|
||||||
|
masterChannel = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "masterChannel" }, "Agent"),
|
||||||
|
prefix = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "prefix" }, ""),
|
||||||
|
},
|
||||||
|
macroer = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "macroer", "enabled" }, false),
|
||||||
|
priority = shared.GetOrDefault(Heimdall_Data, { "config", "macroer", "priority" }, {}),
|
||||||
|
},
|
||||||
|
agents = shared.GetOrDefault(Heimdall_Data, { "config", "agents" }, {}),
|
||||||
|
commander = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "commander", "enabled" }, false),
|
||||||
|
masterChannel = shared.GetOrDefault(Heimdall_Data, { "config", "commander", "masterChannel" }, "Agent"),
|
||||||
|
commander = shared.GetOrDefault(Heimdall_Data, { "config", "commander", "commander" }, "Heimdállr"),
|
||||||
|
commands = shared.GetOrDefault(Heimdall_Data, { "config", "commander", "commands" }, {}),
|
||||||
|
},
|
||||||
|
stinkyTracker = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "stinkyTracker", "enabled" }, false),
|
||||||
|
masterChannel = shared.GetOrDefault(Heimdall_Data, { "config", "stinkyTracker", "masterChannel" }, "Agent"),
|
||||||
|
},
|
||||||
|
combatAlerter = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "combatAlerter", "enabled" }, false),
|
||||||
|
masterChannel = shared.GetOrDefault(Heimdall_Data, { "config", "combatAlerter", "masterChannel" }, "Agent"),
|
||||||
},
|
},
|
||||||
whisperNotify = data.GetOrDefault(Heimdall_Data, { "config", "whisperNotify" }, {
|
|
||||||
"Extazyk",
|
|
||||||
"Smokefire",
|
|
||||||
"Smokemantra",
|
|
||||||
"Хихихантер",
|
|
||||||
"Муркот",
|
|
||||||
"Растафаркрай",
|
|
||||||
"Frosstmorn",
|
|
||||||
"Pulsjkee",
|
|
||||||
"Paskoo",
|
|
||||||
"Totleta",
|
|
||||||
"Healleta",
|
|
||||||
"Deathleta",
|
|
||||||
"Shootleta",
|
|
||||||
"Stableta"
|
|
||||||
}),
|
|
||||||
stinkies = data.GetOrDefault(Heimdall_Data, { "config", "stinkies" }, {
|
|
||||||
"Ahhahahh",
|
|
||||||
"Aye",
|
|
||||||
"Bbd",
|
|
||||||
"Blessly",
|
|
||||||
"Bunkkeer",
|
|
||||||
"Calmer",
|
|
||||||
"Chuvirloeban",
|
|
||||||
"Clairvoyant",
|
|
||||||
"Dewdew",
|
|
||||||
"Dwxrfshaman",
|
|
||||||
"Ebanirot",
|
|
||||||
"Heger",
|
|
||||||
"Hmor",
|
|
||||||
"Joule",
|
|
||||||
"Kaøs",
|
|
||||||
"Kromsaevmode",
|
|
||||||
"Kugisara",
|
|
||||||
"Lax",
|
|
||||||
"Negron",
|
|
||||||
"Oakskin",
|
|
||||||
"Pizdosorkam",
|
|
||||||
"Pussymism",
|
|
||||||
"Rattenfenger",
|
|
||||||
"Riener",
|
|
||||||
"Rollbot",
|
|
||||||
"Samuraqt",
|
|
||||||
"Sekiiro",
|
|
||||||
"Shadowmilf",
|
|
||||||
"Sonikblaster",
|
|
||||||
"Srakonyh",
|
|
||||||
"Stuffo",
|
|
||||||
"Subaruwrxsti",
|
|
||||||
"Sukunexd",
|
|
||||||
"Tomoki",
|
|
||||||
"Unwashed",
|
|
||||||
"Voitas",
|
|
||||||
"Wataru",
|
|
||||||
"Yooshima",
|
|
||||||
"Анджелос",
|
|
||||||
"Артейда",
|
|
||||||
"Асталабиста",
|
|
||||||
"Гебефрени",
|
|
||||||
"Курлык",
|
|
||||||
"Лжедмитресса",
|
|
||||||
"Ловилуну",
|
|
||||||
"Лопапа",
|
|
||||||
"Неонанируй",
|
|
||||||
"Паладийпал",
|
|
||||||
"Психопаточка",
|
|
||||||
"Сильверлейн",
|
|
||||||
"Сосканереалк",
|
|
||||||
"Счастьевам",
|
|
||||||
"Фоська",
|
|
||||||
"Фрил",
|
|
||||||
"Ххантуля",
|
|
||||||
"Чмодвенк",
|
|
||||||
"Шпек",
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.raceMap = {
|
shared.raceMap = {
|
||||||
["Orc"] = "Horde",
|
["Orc"] = "Horde",
|
||||||
["Undead"] = "Horde",
|
["Undead"] = "Horde",
|
||||||
["Tauren"] = "Horde",
|
["Tauren"] = "Horde",
|
||||||
@@ -252,7 +300,7 @@ local function init()
|
|||||||
["Mag'har Orc"] = "Horde"
|
["Mag'har Orc"] = "Horde"
|
||||||
}
|
}
|
||||||
|
|
||||||
data.classColors = {
|
shared.classColors = {
|
||||||
["Warrior"] = "C69B6D",
|
["Warrior"] = "C69B6D",
|
||||||
["Paladin"] = "F48CBA",
|
["Paladin"] = "F48CBA",
|
||||||
["Hunter"] = "AAD372",
|
["Hunter"] = "AAD372",
|
||||||
@@ -269,7 +317,7 @@ local function init()
|
|||||||
|
|
||||||
---@param input string
|
---@param input string
|
||||||
---@return number
|
---@return number
|
||||||
data.utf8len = function(input)
|
shared.utf8len = function(input)
|
||||||
if not input then
|
if not input then
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
@@ -297,9 +345,9 @@ local function init()
|
|||||||
---@param targetLength number
|
---@param targetLength number
|
||||||
---@param left boolean
|
---@param left boolean
|
||||||
---@return string
|
---@return string
|
||||||
data.padString = function(input, targetLength, left)
|
shared.padString = function(input, targetLength, left)
|
||||||
left = left or false
|
left = left or false
|
||||||
local len = data.utf8len(input)
|
local len = shared.utf8len(input)
|
||||||
if len < targetLength then
|
if len < targetLength then
|
||||||
if left then
|
if left then
|
||||||
input = input .. string.rep(" ", targetLength - len)
|
input = input .. string.rep(" ", targetLength - len)
|
||||||
@@ -310,10 +358,19 @@ local function init()
|
|||||||
return input
|
return input
|
||||||
end
|
end
|
||||||
|
|
||||||
data.Whoer.Init()
|
shared.Messenger.Init()
|
||||||
data.Messenger.Init()
|
shared.StinkyTracker.Init()
|
||||||
data.Spotter.Init()
|
shared.AgentTracker.Init()
|
||||||
data.DeathReporter.Init()
|
shared.Whoer.Init()
|
||||||
|
shared.Spotter.Init()
|
||||||
|
shared.DeathReporter.Init()
|
||||||
|
shared.Inviter.Init()
|
||||||
|
shared.Dueler.Init()
|
||||||
|
shared.Bully.Init()
|
||||||
|
shared.Macroer.Init()
|
||||||
|
shared.Commander.Init()
|
||||||
|
shared.CombatAlerter.Init()
|
||||||
|
shared.Config.Init()
|
||||||
print("Heimdall loaded!")
|
print("Heimdall loaded!")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -324,20 +381,3 @@ loadedFrame:SetScript("OnEvent", function(self, event, addonName)
|
|||||||
init()
|
init()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local logoutFrame = CreateFrame("Frame")
|
|
||||||
logoutFrame:RegisterEvent("PLAYER_LOGOUT")
|
|
||||||
logoutFrame:SetScript("OnEvent", function(self, event)
|
|
||||||
Heimdall_Data.config.stinkies = data.config.stinkies
|
|
||||||
end)
|
|
||||||
|
|
||||||
SlashCmdList["HEIMDALL_TOGGLE_STINKY"] = function(input)
|
|
||||||
print("Toggling stinky: " .. tostring(input))
|
|
||||||
if data.config.stinkies[input] then
|
|
||||||
data.config.stinkies[input] = nil
|
|
||||||
else
|
|
||||||
data.config.stinkies[input] = true
|
|
||||||
end
|
|
||||||
print(data.config.stinkies[input])
|
|
||||||
end
|
|
||||||
SLASH_HEIMDALL_TOGGLE_STINKY1 = "/has"
|
|
||||||
|
25
Heimdall.toc
25
Heimdall.toc
@@ -1,14 +1,27 @@
|
|||||||
## Interface: 70300
|
## Interface: 70300
|
||||||
## Title: Heimdall
|
## Title: Heimdall
|
||||||
|
## Version: 3.0.0
|
||||||
## Notes: Watches over areas and alerts when hostiles spotted
|
## Notes: Watches over areas and alerts when hostiles spotted
|
||||||
## Author: Cyka
|
## Author: Cyka
|
||||||
## SavedVariables: Heimdall_Data
|
## SavedVariables: Heimdall_Data
|
||||||
|
|
||||||
#core
|
#core
|
||||||
CLEUParser.lua
|
Modules/CLEUParser.lua
|
||||||
DumpTable.lua
|
Modules/ReactiveValue.lua
|
||||||
Spotter.lua
|
Modules/DumpTable.lua
|
||||||
Whoer.lua
|
Modules/Spotter.lua
|
||||||
Messenger.lua
|
Modules/Whoer.lua
|
||||||
DeathReporter.lua
|
Modules/Messenger.lua
|
||||||
|
Modules/DeathReporter.lua
|
||||||
|
Modules/Inviter.lua
|
||||||
|
Modules/Dueler.lua
|
||||||
|
Modules/Bully.lua
|
||||||
|
Modules/AgentTracker.lua
|
||||||
|
Modules/Emoter.lua
|
||||||
|
Modules/Echoer.lua
|
||||||
|
Modules/Macroer.lua
|
||||||
|
Modules/Commander.lua
|
||||||
|
Modules/StinkyTracker.lua
|
||||||
|
Modules/CombatAlerter.lua
|
||||||
|
Modules/Config.lua
|
||||||
Heimdall.lua
|
Heimdall.lua
|
BIN
Heimdall.zip
(Stored with Git LFS)
Normal file
BIN
Heimdall.zip
(Stored with Git LFS)
Normal file
Binary file not shown.
100
Messenger.lua
100
Messenger.lua
@@ -1,100 +0,0 @@
|
|||||||
local addonname, data = ...
|
|
||||||
---@cast data HeimdallData
|
|
||||||
---@cast addonname string
|
|
||||||
|
|
||||||
data.Messenger = {}
|
|
||||||
function data.Messenger.Init()
|
|
||||||
if not data.config.messenger.enabled then
|
|
||||||
print("Heimdall - Messenger disabled")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class Message
|
|
||||||
---@field message string
|
|
||||||
---@field channel string
|
|
||||||
---@field data string
|
|
||||||
|
|
||||||
---@type table<string, number>
|
|
||||||
local channelIdMap = {}
|
|
||||||
|
|
||||||
local FindOrJoinChannel = function(channelName, password)
|
|
||||||
local function GetChannelId(channelName)
|
|
||||||
local channels = { GetChannelList() }
|
|
||||||
for i = 1, #channels, 2 do
|
|
||||||
local id = channels[i]
|
|
||||||
local name = channels[i + 1]
|
|
||||||
if name == channelName then
|
|
||||||
return id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local channelId = GetChannelId(channelName)
|
|
||||||
if not channelId then
|
|
||||||
print("Channel", tostring(channelName), "not found, joining")
|
|
||||||
if password then
|
|
||||||
JoinPermanentChannel(channelName, password)
|
|
||||||
else
|
|
||||||
JoinPermanentChannel(channelName)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
channelId = GetChannelId(channelName)
|
|
||||||
channelIdMap[channelName] = channelId
|
|
||||||
return channelId
|
|
||||||
end
|
|
||||||
|
|
||||||
local ScanChannels = function()
|
|
||||||
local channels = { GetChannelList() }
|
|
||||||
for i = 1, #channels, 2 do
|
|
||||||
local id = channels[i]
|
|
||||||
local name = channels[i + 1]
|
|
||||||
channelIdMap[name] = id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not data.messenger then data.messenger = {} end
|
|
||||||
if not data.messenger.queue then data.messenger.queue = {} end
|
|
||||||
if not data.messenger.ticker then
|
|
||||||
data.messenger.ticker = C_Timer.NewTicker(0.2, function()
|
|
||||||
---@type Message
|
|
||||||
local message = data.messenger.queue[1]
|
|
||||||
if not message then return end
|
|
||||||
if not message.message or message.message == "" then return end
|
|
||||||
if not message.channel or message.channel == "" then return end
|
|
||||||
|
|
||||||
-- Map channel names to ids
|
|
||||||
if message.channel == "CHANNEL" and message.data and string.match(message.data, "%D") then
|
|
||||||
print("Channel presented as string:", message.data)
|
|
||||||
local channelId = channelIdMap[message.data]
|
|
||||||
if not channelId then
|
|
||||||
print("Channel not found, scanning")
|
|
||||||
ScanChannels()
|
|
||||||
channelId = channelIdMap[message.data]
|
|
||||||
end
|
|
||||||
if not channelId then
|
|
||||||
print("Channel not joined, joining")
|
|
||||||
channelId = FindOrJoinChannel(message.data)
|
|
||||||
end
|
|
||||||
print("Channel resolved to id", channelId)
|
|
||||||
message.data = channelId
|
|
||||||
end
|
|
||||||
|
|
||||||
table.remove(data.messenger.queue, 1)
|
|
||||||
if not message.message or message.message == "" then return end
|
|
||||||
if not message.channel or message.channel == "" then return end
|
|
||||||
if not message.data or message.data == "" then return end
|
|
||||||
SendChatMessage(message.message, message.channel, nil, message.data)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--C_Timer.NewTicker(2, function()
|
|
||||||
-- print("Q")
|
|
||||||
-- table.insert(data.messenger.queue, {
|
|
||||||
-- channel = "CHANNEL",
|
|
||||||
-- data = "Foobar",
|
|
||||||
-- message = "TEST"
|
|
||||||
-- })
|
|
||||||
--end)
|
|
||||||
|
|
||||||
print("Heimdall - Messenger loaded")
|
|
||||||
end
|
|
39
Modules/AgentTracker.lua
Normal file
39
Modules/AgentTracker.lua
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.AgentTracker = {}
|
||||||
|
function shared.AgentTracker.Init()
|
||||||
|
---@type table<string, boolean>
|
||||||
|
local channelRosterFrame = CreateFrame("Frame")
|
||||||
|
channelRosterFrame:RegisterEvent("CHANNEL_ROSTER_UPDATE")
|
||||||
|
channelRosterFrame:SetScript("OnEvent", function(self, event, index)
|
||||||
|
if not Heimdall_Data.config.agentTracker.enabled then return end
|
||||||
|
local name = GetChannelDisplayInfo(index)
|
||||||
|
if name ~= Heimdall_Data.config.agentTracker.masterChannel then return end
|
||||||
|
local count = select(5, GetChannelDisplayInfo(index))
|
||||||
|
for i = 1, count do
|
||||||
|
local name = GetChannelRosterInfo(index, i)
|
||||||
|
if name then
|
||||||
|
Heimdall_Data.config.agents[name] = date("%Y-%m-%dT%H:%M:%S")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--shared.dumpTable(Heimdall_Data.config.agents)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local agentTrackerChannelSniffer = CreateFrame("Frame")
|
||||||
|
agentTrackerChannelSniffer:RegisterEvent("CHAT_MSG_CHANNEL")
|
||||||
|
agentTrackerChannelSniffer:SetScript("OnEvent", function(self, event, msg, sender, ...)
|
||||||
|
if not Heimdall_Data.config.agentTracker.enabled then return end
|
||||||
|
local channelId = select(6, ...)
|
||||||
|
local channelname = GetChannelName(channelId)
|
||||||
|
if not channelname then return end
|
||||||
|
if channelname ~= Heimdall_Data.config.who.notifyChannel then return end
|
||||||
|
local agentName = sender
|
||||||
|
if not agentName then return end
|
||||||
|
Heimdall_Data.config.agents[agentName] = date("%Y-%m-%dT%H:%M:%S")
|
||||||
|
end)
|
||||||
|
|
||||||
|
print("Heimdall - AgentTracker loaded")
|
||||||
|
end
|
9
Modules/Bully.lua
Normal file
9
Modules/Bully.lua
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.Bully = {}
|
||||||
|
function shared.Bully.Init()
|
||||||
|
print("Heimdall - Bully loaded")
|
||||||
|
end
|
49
Modules/CombatAlerter.lua
Normal file
49
Modules/CombatAlerter.lua
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.CombatAlerter = {}
|
||||||
|
function shared.CombatAlerter.Init()
|
||||||
|
local alerted = {}
|
||||||
|
local combatAlerterFrame = CreateFrame("Frame")
|
||||||
|
combatAlerterFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
|
||||||
|
combatAlerterFrame:SetScript("OnEvent", function(self, event, ...)
|
||||||
|
if not Heimdall_Data.config.combatAlerter.enabled then return end
|
||||||
|
local destination, err = CLEUParser.GetDestName(...)
|
||||||
|
if err then return end
|
||||||
|
if destination ~= UnitName("player") then return end
|
||||||
|
local source, err = CLEUParser.GetSourceName(...)
|
||||||
|
if err then source = "unknown" end
|
||||||
|
|
||||||
|
if shared.stinkyTracker.stinkies and shared.stinkyTracker.stinkies[source] then
|
||||||
|
if alerted[source] then return end
|
||||||
|
alerted[source] = true
|
||||||
|
local x, y = GetPlayerMapPosition("player")
|
||||||
|
---@type Message
|
||||||
|
local msg = {
|
||||||
|
channel = "CHANNEL",
|
||||||
|
data = Heimdall_Data.config.combatAlerter.masterChannel,
|
||||||
|
message = string.format("%s is attacking me in %s(%s) at %2.2f,%2.2f ",
|
||||||
|
source,
|
||||||
|
GetZoneText(), GetSubZoneText(),
|
||||||
|
x * 100, y * 100
|
||||||
|
),
|
||||||
|
}
|
||||||
|
table.insert(shared.messenger.queue, msg)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
local combatTriggerFrame = CreateFrame("Frame")
|
||||||
|
combatTriggerFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
|
||||||
|
combatTriggerFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
|
||||||
|
-- We want to only alert once per target per combat encounter
|
||||||
|
-- Even a small throttle would probably spam too much here
|
||||||
|
-- ....but maybe we can call it a 120 second throttle or something?
|
||||||
|
-- We will see
|
||||||
|
combatTriggerFrame:SetScript("OnEvent", function(self, event, ...)
|
||||||
|
alerted = {}
|
||||||
|
end)
|
||||||
|
|
||||||
|
print("Heimdall - CombatAlerter loaded")
|
||||||
|
end
|
223
Modules/Commander.lua
Normal file
223
Modules/Commander.lua
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
local helpMessages = {
|
||||||
|
ru = {
|
||||||
|
"1) who - пишет вам никнеймы текущих врагов в отслеживаемых локациях по порядку и локацию.",
|
||||||
|
"2) classes = пишет какие классы. (например 1 паладин 1 прист 1 рога.",
|
||||||
|
"3) howmany - общее число врагов (например дуротар 4 . оргримар 2 ) ",
|
||||||
|
"4) + - автоматическое приглашение в группу с дуельным рогой для сброса кд и общего сбора. ",
|
||||||
|
"5) ++ - автоматическое приглашение в группу альянса. (если нужен рефрак)",
|
||||||
|
},
|
||||||
|
en = {
|
||||||
|
"1) who - reports currently tracked stinkies in orgrimmar and durotar",
|
||||||
|
"2) classes - reports stinkies grouped by class",
|
||||||
|
"3) howmany - reports stinkies grouped by zone",
|
||||||
|
"4) + - automatically invites to group with duel rogue for cd reset",
|
||||||
|
"5) ++ - automatically invites to alliance group",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.Commander = {}
|
||||||
|
function shared.Commander.Init()
|
||||||
|
---@param text string
|
||||||
|
---@param size number
|
||||||
|
---@return string[]
|
||||||
|
local function Partition(text, size)
|
||||||
|
local words = {}
|
||||||
|
for word in text:gmatch("[^,]+") do
|
||||||
|
words[#words + 1] = word
|
||||||
|
end
|
||||||
|
|
||||||
|
local ret = {}
|
||||||
|
local currentChunk = ""
|
||||||
|
|
||||||
|
for _, word in ipairs(words) do
|
||||||
|
if #currentChunk + #word + 1 <= size then
|
||||||
|
currentChunk = currentChunk .. (currentChunk == "" and word or " " .. word)
|
||||||
|
else
|
||||||
|
if #currentChunk > 0 then
|
||||||
|
ret[#ret + 1] = currentChunk
|
||||||
|
end
|
||||||
|
currentChunk = word
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #currentChunk > 0 then
|
||||||
|
ret[#ret + 1] = currentChunk
|
||||||
|
end
|
||||||
|
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
---@param arr table<string, Player>
|
||||||
|
---@return string[]
|
||||||
|
local function Count(arr)
|
||||||
|
local ret = {}
|
||||||
|
for _, player in pairs(arr) do
|
||||||
|
if Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
|
||||||
|
ret[player.zone] = (ret[player.zone] or 0) + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local text = {}
|
||||||
|
for zone, count in pairs(ret) do
|
||||||
|
text[#text + 1] = string.format("%s: %d", zone, count)
|
||||||
|
end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
---@param arr table<string, Player>
|
||||||
|
---@return string[]
|
||||||
|
local function CountPartitioned(arr)
|
||||||
|
local count = Count(arr)
|
||||||
|
local text = {}
|
||||||
|
for _, line in pairs(Partition(strjoin(", ", unpack(count)), 200)) do
|
||||||
|
text[#text + 1] = line
|
||||||
|
end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
---@param arr table<string, Player>
|
||||||
|
---@return string[]
|
||||||
|
local function Who(arr)
|
||||||
|
local ret = {}
|
||||||
|
for _, player in pairs(arr) do
|
||||||
|
if Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
|
||||||
|
ret[#ret + 1] = string.format("%s/%s (%s) %s", player.name, player.class, player.zone,
|
||||||
|
player.stinky and "(!!!!)" or "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
---@param arr table<string, Player>
|
||||||
|
---@return string[]
|
||||||
|
local function WhoPartitioned(arr)
|
||||||
|
local who = Who(arr)
|
||||||
|
local text = {}
|
||||||
|
for _, line in pairs(Partition(strjoin(", ", unpack(who)), 200)) do
|
||||||
|
text[#text + 1] = line
|
||||||
|
end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
---@param arr table<string, Player>
|
||||||
|
---@return string[]
|
||||||
|
local function CountClass(arr)
|
||||||
|
local ret = {}
|
||||||
|
for _, player in pairs(arr) do
|
||||||
|
if Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
|
||||||
|
ret[player.class] = (ret[player.class] or 0) + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local text = {}
|
||||||
|
for class, count in pairs(ret) do
|
||||||
|
text[#text + 1] = string.format("%s: %d", class, count)
|
||||||
|
end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
---@param arr table<string, Player>
|
||||||
|
---@return string[]
|
||||||
|
local function CountClassPartitioned(arr)
|
||||||
|
local countClass = CountClass(arr)
|
||||||
|
local text = {}
|
||||||
|
for _, line in pairs(Partition(strjoin(", ", unpack(countClass)), 200)) do
|
||||||
|
text[#text + 1] = line
|
||||||
|
end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
local function CountClassPartitionedStinkies()
|
||||||
|
local res = CountClassPartitioned(HeimdallStinkies)
|
||||||
|
if #res == 0 then
|
||||||
|
return { "No stinkies found" }
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
local function WhoPartitionedStinkies()
|
||||||
|
local res = WhoPartitioned(HeimdallStinkies)
|
||||||
|
if #res == 0 then
|
||||||
|
return { "No stinkies found" }
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
local function CountPartitionedStinkies()
|
||||||
|
local res = CountPartitioned(HeimdallStinkies)
|
||||||
|
if #res == 0 then
|
||||||
|
return { "No stinkies found" }
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
local function HelpRu() return helpMessages.ru end
|
||||||
|
local function HelpEn() return helpMessages.en end
|
||||||
|
local groupInviteFrame = CreateFrame("Frame")
|
||||||
|
groupInviteFrame:SetScript("OnEvent", function(self, event, ...)
|
||||||
|
AcceptGroup()
|
||||||
|
groupInviteFrame:UnregisterEvent("PARTY_INVITE_REQUEST")
|
||||||
|
C_Timer.NewTimer(0.1, function()
|
||||||
|
_G["StaticPopup1Button1"]:Click()
|
||||||
|
end, 1)
|
||||||
|
end)
|
||||||
|
local function JoinGroup()
|
||||||
|
groupInviteFrame:RegisterEvent("PARTY_INVITE_REQUEST")
|
||||||
|
C_Timer.NewTimer(10, function()
|
||||||
|
groupInviteFrame:UnregisterEvent("PARTY_INVITE_REQUEST")
|
||||||
|
end, 1)
|
||||||
|
return { "+" }
|
||||||
|
end
|
||||||
|
local function LeaveGroup()
|
||||||
|
LeaveParty()
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
---@param target string
|
||||||
|
local function FollowTarget(target)
|
||||||
|
if not target then return end
|
||||||
|
FollowUnit(target)
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class Command
|
||||||
|
---@field keywordRe string
|
||||||
|
---@field commanderOnly boolean
|
||||||
|
---@field callback fun(...: any): string[]
|
||||||
|
|
||||||
|
local commands = {
|
||||||
|
{ keywordRe = "^who", commanderOnly = false, callback = WhoPartitionedStinkies },
|
||||||
|
{ keywordRe = "^howmany", commanderOnly = false, callback = CountPartitionedStinkies },
|
||||||
|
{ keywordRe = "^classes", commanderOnly = false, callback = CountClassPartitionedStinkies },
|
||||||
|
{ keywordRe = "^help", commanderOnly = false, callback = HelpRu },
|
||||||
|
{ keywordRe = "^helpen", commanderOnly = false, callback = HelpEn },
|
||||||
|
{ keywordRe = "^joingroup", commanderOnly = false, callback = JoinGroup },
|
||||||
|
{ keywordRe = "^leavegroup", commanderOnly = false, callback = LeaveGroup },
|
||||||
|
{ keywordRe = "^follow", commanderOnly = false, callback = FollowTarget },
|
||||||
|
}
|
||||||
|
|
||||||
|
local commanderChannelFrame = CreateFrame("Frame")
|
||||||
|
commanderChannelFrame:RegisterEvent("CHAT_MSG_CHANNEL")
|
||||||
|
commanderChannelFrame:SetScript("OnEvent", function(self, event, msg, sender, ...)
|
||||||
|
if not Heimdall_Data.config.commander.enabled then return end
|
||||||
|
local channelId = select(6, ...)
|
||||||
|
local _, channelname = GetChannelName(channelId)
|
||||||
|
if channelname ~= Heimdall_Data.config.commander.masterChannel then return end
|
||||||
|
sender = string.match(sender, "^[^-]+")
|
||||||
|
|
||||||
|
for _, command in ipairs(commands) do
|
||||||
|
local enabled = Heimdall_Data.config.commander.commands[command.keywordRe] == true or false
|
||||||
|
if enabled and
|
||||||
|
(not command.commanderOnly
|
||||||
|
or (command.commanderOnly
|
||||||
|
and sender == Heimdall_Data.config.commander.commander)) then
|
||||||
|
if msg:match(command.keywordRe) then
|
||||||
|
local messages = command.callback({ strsplit(" ", msg) })
|
||||||
|
for _, message in ipairs(messages) do
|
||||||
|
---@type Message
|
||||||
|
local msg = {
|
||||||
|
channel = "CHANNEL",
|
||||||
|
data = channelname,
|
||||||
|
message = message
|
||||||
|
}
|
||||||
|
table.insert(shared.messenger.queue, msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
print("Heimdall - Commander loaded")
|
||||||
|
end
|
1321
Modules/Config.lua
Normal file
1321
Modules/Config.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,10 @@
|
|||||||
local addonname, data = ...
|
local addonname, shared = ...
|
||||||
---@cast data HeimdallData
|
---@cast shared HeimdallShared
|
||||||
---@cast addonname string
|
---@cast addonname string
|
||||||
|
|
||||||
data.DeathReporter = {}
|
---@diagnostic disable-next-line: missing-fields
|
||||||
function data.DeathReporter.Init()
|
shared.DeathReporter = {}
|
||||||
if not data.config.deathReporter.enabled then
|
function shared.DeathReporter.Init()
|
||||||
print("Heimdall - DeathReporter disabled")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
---@type table<string, number>
|
---@type table<string, number>
|
||||||
local recentDeaths = {}
|
local recentDeaths = {}
|
||||||
---@type table<string, number>
|
---@type table<string, number>
|
||||||
@@ -17,20 +13,20 @@ function data.DeathReporter.Init()
|
|||||||
---@param source string
|
---@param source string
|
||||||
---@param destination string
|
---@param destination string
|
||||||
---@param spellName string
|
---@param spellName string
|
||||||
---@param overkill number
|
local function RegisterDeath(source, destination, spellName)
|
||||||
local function RegisterDeath(source, destination, spellName, overkill)
|
if not Heimdall_Data.config.deathReporter.enabled then return end
|
||||||
if recentDeaths[destination]
|
if recentDeaths[destination]
|
||||||
and GetTime() - recentDeaths[destination] < data.config.deathReporter.throttle then
|
and GetTime() - recentDeaths[destination] < Heimdall_Data.config.deathReporter.throttle then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if recentDuels[destination]
|
if recentDuels[destination]
|
||||||
and GetTime() - recentDuels[destination] < data.config.deathReporter.duelThrottle then
|
and GetTime() - recentDuels[destination] < Heimdall_Data.config.deathReporter.duelThrottle then
|
||||||
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
|
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if recentDuels[source]
|
if recentDuels[source]
|
||||||
and GetTime() - recentDuels[source] < data.config.deathReporter.duelThrottle then
|
and GetTime() - recentDuels[source] < Heimdall_Data.config.deathReporter.duelThrottle then
|
||||||
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
|
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -38,18 +34,18 @@ function data.DeathReporter.Init()
|
|||||||
recentDeaths[destination] = GetTime()
|
recentDeaths[destination] = GetTime()
|
||||||
C_Timer.NewTimer(3, function()
|
C_Timer.NewTimer(3, function()
|
||||||
if recentDuels[destination]
|
if recentDuels[destination]
|
||||||
and GetTime() - recentDuels[destination] < data.config.deathReporter.duelThrottle then
|
and GetTime() - recentDuels[destination] < Heimdall_Data.config.deathReporter.duelThrottle then
|
||||||
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
|
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if recentDuels[source]
|
if recentDuels[source]
|
||||||
and GetTime() - recentDuels[source] < data.config.deathReporter.duelThrottle then
|
and GetTime() - recentDuels[source] < Heimdall_Data.config.deathReporter.duelThrottle then
|
||||||
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
|
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local zone = data.config.deathReporter.zoneOverride
|
local zone = Heimdall_Data.config.deathReporter.zoneOverride
|
||||||
if not zone then
|
if zone == nil or zone == "" then
|
||||||
zone = string.format("%s (%s)", GetZoneText(), GetSubZoneText())
|
zone = string.format("%s (%s)", GetZoneText(), GetSubZoneText())
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -62,19 +58,19 @@ function data.DeathReporter.Init()
|
|||||||
---@type Message
|
---@type Message
|
||||||
local msg = {
|
local msg = {
|
||||||
channel = "CHANNEL",
|
channel = "CHANNEL",
|
||||||
data = data.config.deathReporter.notifyChannel,
|
data = Heimdall_Data.config.deathReporter.notifyChannel,
|
||||||
message = text,
|
message = text,
|
||||||
}
|
}
|
||||||
table.insert(data.messenger.queue, msg)
|
table.insert(shared.messenger.queue, msg)
|
||||||
|
|
||||||
if data.config.deathReporter.doWhisper then
|
if Heimdall_Data.config.deathReporter.doWhisper then
|
||||||
for _, name in pairs(data.config.whisperNotify) do
|
for _, name in pairs(Heimdall_Data.config.whisperNotify) do
|
||||||
local msg = {
|
local msg = {
|
||||||
channel = "WHISPER",
|
channel = "WHISPER",
|
||||||
data = name,
|
data = name,
|
||||||
message = text,
|
message = text,
|
||||||
}
|
}
|
||||||
table.insert(data.messenger.queue, msg)
|
table.insert(shared.messenger.queue, msg)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -83,6 +79,7 @@ function data.DeathReporter.Init()
|
|||||||
local cleuFrame = CreateFrame("Frame")
|
local cleuFrame = CreateFrame("Frame")
|
||||||
cleuFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
|
cleuFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
|
||||||
cleuFrame:SetScript("OnEvent", function(self, event, ...)
|
cleuFrame:SetScript("OnEvent", function(self, event, ...)
|
||||||
|
if not Heimdall_Data.config.deathReporter.enabled then return end
|
||||||
local overkill, err = CLEUParser.GetOverkill(...)
|
local overkill, err = CLEUParser.GetOverkill(...)
|
||||||
if not err and overkill > 0 then
|
if not err and overkill > 0 then
|
||||||
local source, err = CLEUParser.GetSourceName(...)
|
local source, err = CLEUParser.GetSourceName(...)
|
||||||
@@ -95,14 +92,18 @@ function data.DeathReporter.Init()
|
|||||||
if err or not string.match(sourceGUID, "Player") then return end
|
if err or not string.match(sourceGUID, "Player") then return end
|
||||||
local destinationGUID, err = CLEUParser.GetDestGUID(...)
|
local destinationGUID, err = CLEUParser.GetDestGUID(...)
|
||||||
if err or not string.match(destinationGUID, "Player") then return end
|
if err or not string.match(destinationGUID, "Player") then return end
|
||||||
RegisterDeath(source, destination, spellName, overkill)
|
RegisterDeath(source, destination, spellName)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local systemMessageFrame = CreateFrame("Frame")
|
local systemMessageFrame = CreateFrame("Frame")
|
||||||
systemMessageFrame:RegisterEvent("CHAT_MSG_SYSTEM")
|
systemMessageFrame:RegisterEvent("CHAT_MSG_SYSTEM")
|
||||||
systemMessageFrame:SetScript("OnEvent", function(self, event, msg)
|
systemMessageFrame:SetScript("OnEvent", function(self, event, msg)
|
||||||
local source, destination = string.match(msg, "(.+) has defeated (.+) in a duel")
|
if not Heimdall_Data.config.deathReporter.enabled then return end
|
||||||
|
local source, destination = string.match(msg, "([^ ]+) has defeated ([^ ]+) in a duel")
|
||||||
|
if not source or not destination then return end
|
||||||
|
source = string.match(source, "([^-]+)")
|
||||||
|
destination = string.match(destination, "([^-]+)")
|
||||||
if source and destination then
|
if source and destination then
|
||||||
print(string.format("Detected duel between %s and %s", source, destination))
|
print(string.format("Detected duel between %s and %s", source, destination))
|
||||||
local now = GetTime()
|
local now = GetTime()
|
25
Modules/Dueler.lua
Normal file
25
Modules/Dueler.lua
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.Dueler = {}
|
||||||
|
function shared.Dueler.Init()
|
||||||
|
local frame = CreateFrame("Frame")
|
||||||
|
frame:RegisterEvent("DUEL_REQUESTED")
|
||||||
|
frame:SetScript("OnEvent", function(self, event, sender)
|
||||||
|
if not Heimdall_Data.config.dueler.enabled then return end
|
||||||
|
local allow = Heimdall_Data.config.agents[sender]
|
||||||
|
if allow then
|
||||||
|
print("Heimdall - Dueler - Accepting duel from " .. sender)
|
||||||
|
AcceptDuel()
|
||||||
|
else
|
||||||
|
if Heimdall_Data.config.dueler.autoDecline then
|
||||||
|
print("Heimdall - Dueler - Auto declining duel from " .. sender)
|
||||||
|
CancelDuel()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
print("Heimdall - Dueler loaded")
|
||||||
|
end
|
@@ -1,11 +1,11 @@
|
|||||||
local addonname, data = ...
|
local addonname, shared = ...
|
||||||
---@cast data HeimdallData
|
---@cast shared HeimdallShared
|
||||||
---@cast addonname string
|
---@cast addonname string
|
||||||
|
|
||||||
if not data.dumpTable then
|
if not shared.dumpTable then
|
||||||
---@param table table
|
---@param table table
|
||||||
---@param depth number?
|
---@param depth number?
|
||||||
data.dumpTable = function(table, depth)
|
shared.dumpTable = function(table, depth)
|
||||||
if not table then
|
if not table then
|
||||||
print(tostring(table))
|
print(tostring(table))
|
||||||
return
|
return
|
||||||
@@ -20,7 +20,7 @@ if not data.dumpTable then
|
|||||||
for k, v in pairs(table) do
|
for k, v in pairs(table) do
|
||||||
if (type(v) == "table") then
|
if (type(v) == "table") then
|
||||||
print(string.rep(" ", depth) .. k .. ":")
|
print(string.rep(" ", depth) .. k .. ":")
|
||||||
data.dumpTable(v, depth + 1)
|
shared.dumpTable(v, depth + 1)
|
||||||
else
|
else
|
||||||
print(string.rep(" ", depth) .. k .. ": ", v)
|
print(string.rep(" ", depth) .. k .. ": ", v)
|
||||||
end
|
end
|
36
Modules/Echoer.lua
Normal file
36
Modules/Echoer.lua
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.Echoer = {}
|
||||||
|
function shared.Echoer.Init()
|
||||||
|
local frame = CreateFrame("Frame")
|
||||||
|
frame:RegisterEvent("CHAT_MSG_CHANNEL")
|
||||||
|
frame:SetScript("OnEvent", function(self, event, msg, sender, ...)
|
||||||
|
if not Heimdall_Data.config.echoer.enabled then return end
|
||||||
|
|
||||||
|
local channelId = select(6, ...)
|
||||||
|
local channelname = ""
|
||||||
|
---@type any[]
|
||||||
|
local channels = { GetChannelList() }
|
||||||
|
for i = 1, #channels, 2 do
|
||||||
|
---@type number
|
||||||
|
local id = channels[i]
|
||||||
|
---@type string
|
||||||
|
local name = channels[i + 1]
|
||||||
|
if id == channelId then
|
||||||
|
channelname = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if channelname ~= Heimdall_Data.config.echoer.masterChannel then return end
|
||||||
|
|
||||||
|
if string.find(msg, "^" .. Heimdall_Data.config.echoer.prefix) then
|
||||||
|
local msg = string.sub(msg, string.len(Heimdall_Data.config.echoer.prefix) + 1)
|
||||||
|
SendChatMessage(msg, "SAY")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
print("Heimdall - Echoer loaded")
|
||||||
|
end
|
35
Modules/Emoter.lua
Normal file
35
Modules/Emoter.lua
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.Emoter = {}
|
||||||
|
function shared.Emoter.Init()
|
||||||
|
local frame = CreateFrame("Frame")
|
||||||
|
frame:RegisterEvent("CHAT_MSG_CHANNEL")
|
||||||
|
frame:SetScript("OnEvent", function(self, event, msg, sender, ...)
|
||||||
|
if not Heimdall_Data.config.emoter.enabled then return end
|
||||||
|
|
||||||
|
local channelId = select(6, ...)
|
||||||
|
local channelname = ""
|
||||||
|
---@type any[]
|
||||||
|
local channels = { GetChannelList() }
|
||||||
|
for i = 1, #channels, 2 do
|
||||||
|
---@type number
|
||||||
|
local id = channels[i]
|
||||||
|
---@type string
|
||||||
|
local name = channels[i + 1]
|
||||||
|
if id == channelId then
|
||||||
|
channelname = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if channelname ~= Heimdall_Data.config.emoter.masterChannel then return end
|
||||||
|
|
||||||
|
if string.find(msg, "^" .. Heimdall_Data.config.emoter.prefix) then
|
||||||
|
local emote = string.sub(msg, string.len(Heimdall_Data.config.emoter.prefix) + 1)
|
||||||
|
DoEmote(emote)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
print("Heimdall - Emoter loaded")
|
||||||
|
end
|
139
Modules/Inviter.lua
Normal file
139
Modules/Inviter.lua
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.Inviter = {}
|
||||||
|
function shared.Inviter.Init()
|
||||||
|
---@type Timer
|
||||||
|
local updateTimer = nil
|
||||||
|
|
||||||
|
local function FixGroup()
|
||||||
|
if not IsInRaid() then ConvertToRaid() end
|
||||||
|
if Heimdall_Data.config.inviter.allAssist then SetEveryoneIsAssistant() end
|
||||||
|
--shared.dumpTable(Heimdall_Data.config.inviter)
|
||||||
|
if Heimdall_Data.config.inviter.agentsAssist then
|
||||||
|
--shared.dumpTable(Heimdall_Data.config.agents)
|
||||||
|
for name, _ in pairs(Heimdall_Data.config.agents) do
|
||||||
|
if UnitInParty(name)
|
||||||
|
and not UnitIsGroupLeader(name)
|
||||||
|
and not UnitIsRaidOfficer(name) then
|
||||||
|
print("Promoting " .. name .. " to assistant")
|
||||||
|
PromoteToAssistant(name, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local framePool = {}
|
||||||
|
---@param name string
|
||||||
|
local function OverlayKickButtonElvUI(name)
|
||||||
|
for group = 1, 8 do
|
||||||
|
for player = 1, 5 do
|
||||||
|
local button = _G[string.format("ElvUF_RaidGroup%dUnitButton%d", group, player)]
|
||||||
|
|
||||||
|
local unitName = button and button.unit and UnitName(button.unit)
|
||||||
|
if unitName == name then
|
||||||
|
local overlayButton = framePool[button.unit] or
|
||||||
|
CreateFrame("Button",
|
||||||
|
string.format("HeimdallKickButton%s", button.unit, button, "SecureActionButtonTemplate"))
|
||||||
|
framePool[button.unit] = overlayButton
|
||||||
|
|
||||||
|
overlayButton:SetSize(button.UNIT_WIDTH/2, button.UNIT_HEIGHT/2)
|
||||||
|
overlayButton:SetPoint("CENTER", button, "CENTER", 0, 0)
|
||||||
|
overlayButton:SetNormalTexture("Interface\\Buttons\\UI-GroupLoot-KickIcon")
|
||||||
|
overlayButton:SetHighlightTexture("Interface\\Buttons\\UI-GroupLoot-KickIcon")
|
||||||
|
overlayButton:SetPushedTexture("Interface\\Buttons\\UI-GroupLoot-KickIcon")
|
||||||
|
overlayButton:SetDisabledTexture("Interface\\Buttons\\UI-GroupLoot-KickIcon")
|
||||||
|
overlayButton:SetAlpha(0.5)
|
||||||
|
overlayButton:Show()
|
||||||
|
overlayButton:SetScript("OnClick", function()
|
||||||
|
UninviteUnit(name)
|
||||||
|
overlayButton:Hide()
|
||||||
|
end)
|
||||||
|
-- button:SetAttribute("type", "macro")
|
||||||
|
-- button:SetAttribute("macrotext", "/kick " .. unit)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type table<string, number>
|
||||||
|
local groupMembers = {}
|
||||||
|
local function CleanGroups()
|
||||||
|
if not Heimdall_Data.config.inviter.kickOffline then return end
|
||||||
|
print("Cleaning groups")
|
||||||
|
local now = GetTime()
|
||||||
|
for i = 1, 40 do
|
||||||
|
local unit = "raid" .. i
|
||||||
|
if UnitExists(unit) then
|
||||||
|
local name = UnitName(unit)
|
||||||
|
if name then
|
||||||
|
-- When we load (into game) we want to consider everyone "online"
|
||||||
|
-- In other words everyone we haven't seen before is "online" the first time we see them
|
||||||
|
-- This is to avoid kicking people who might not be offline which we don't know because we just logged in
|
||||||
|
if not groupMembers[name] then
|
||||||
|
groupMembers[name] = now
|
||||||
|
else
|
||||||
|
local online = UnitIsConnected(unit)
|
||||||
|
if online then
|
||||||
|
groupMembers[name] = now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for name, time in pairs(groupMembers) do
|
||||||
|
if time < now - Heimdall_Data.config.inviter.afkThreshold then
|
||||||
|
print(string.format("Kicking %s for being offline", name))
|
||||||
|
-- Blyat this is protected...
|
||||||
|
-- UninviteUnit(name)
|
||||||
|
OverlayKickButtonElvUI(name)
|
||||||
|
end
|
||||||
|
if not UnitInParty(name) then
|
||||||
|
print(string.format("%s no longer in party", name))
|
||||||
|
groupMembers[name] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function Tick()
|
||||||
|
CleanGroups()
|
||||||
|
C_Timer.NewTimer(Heimdall_Data.config.inviter.cleanupInterval, Tick, 1)
|
||||||
|
end
|
||||||
|
Tick()
|
||||||
|
|
||||||
|
local inviterGroupFrame = CreateFrame("Frame")
|
||||||
|
inviterGroupFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
|
||||||
|
inviterGroupFrame:SetScript("OnEvent", function(self, event, ...)
|
||||||
|
if not Heimdall_Data.config.inviter.enabled then return end
|
||||||
|
if not UnitIsGroupLeader("player") then return end
|
||||||
|
|
||||||
|
if updateTimer then updateTimer:Cancel() end
|
||||||
|
updateTimer = C_Timer.NewTimer(Heimdall_Data.config.inviter.throttle, FixGroup)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local inviterChannelFrame = CreateFrame("Frame")
|
||||||
|
inviterChannelFrame:RegisterEvent("CHAT_MSG_CHANNEL")
|
||||||
|
inviterChannelFrame:SetScript("OnEvent", function(self, event, msg, sender, ...)
|
||||||
|
if not Heimdall_Data.config.inviter.enabled then return end
|
||||||
|
local channelId = select(6, ...)
|
||||||
|
local channelname = ""
|
||||||
|
---@type any[]
|
||||||
|
local channels = { GetChannelList() }
|
||||||
|
for i = 1, #channels, 2 do
|
||||||
|
---@type number
|
||||||
|
local id = channels[i]
|
||||||
|
---@type string
|
||||||
|
local name = channels[i + 1]
|
||||||
|
if id == channelId then
|
||||||
|
channelname = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if channelname ~= Heimdall_Data.config.inviter.listeningChannel then return end
|
||||||
|
if msg == Heimdall_Data.config.inviter.keyword then InviteUnit(sender) end
|
||||||
|
end)
|
||||||
|
|
||||||
|
print("Heimdall - Inviter loaded")
|
||||||
|
end
|
65
Modules/Macroer.lua
Normal file
65
Modules/Macroer.lua
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.Macroer = {}
|
||||||
|
function shared.Macroer.Init()
|
||||||
|
---@class stinky
|
||||||
|
---@field name string
|
||||||
|
---@field class string
|
||||||
|
---@field seenAt number
|
||||||
|
---@field hostile boolean
|
||||||
|
|
||||||
|
---@type table<string, stinky>
|
||||||
|
local recentStinkies = {}
|
||||||
|
|
||||||
|
local function FindOrCreateMacro(macroName)
|
||||||
|
local idx = GetMacroIndexByName(macroName)
|
||||||
|
if idx == 0 then
|
||||||
|
CreateMacro(macroName, "INV_Misc_QuestionMark", "")
|
||||||
|
end
|
||||||
|
idx = GetMacroIndexByName(macroName)
|
||||||
|
return idx
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param stinkies table<string, stinky>
|
||||||
|
local function FixMacro(stinkies)
|
||||||
|
if not Heimdall_Data.config.macroer.enabled then return end
|
||||||
|
if InCombatLockdown() then return end
|
||||||
|
local priorityMap = {}
|
||||||
|
for priority, className in ipairs(Heimdall_Data.config.macroer.priority) do
|
||||||
|
priorityMap[className] = priority
|
||||||
|
end
|
||||||
|
local minPriority = #Heimdall_Data.config.macroer.priority + 1
|
||||||
|
|
||||||
|
local sortedStinkies = {}
|
||||||
|
for _, stinky in pairs(stinkies) do
|
||||||
|
table.insert(sortedStinkies, stinky)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(sortedStinkies, function(a, b)
|
||||||
|
local aPriority = priorityMap[a.class] or minPriority
|
||||||
|
local bPriority = priorityMap[b.class] or minPriority
|
||||||
|
return aPriority > bPriority
|
||||||
|
end)
|
||||||
|
|
||||||
|
local lines = { "/targetenemy" }
|
||||||
|
for _, stinky in pairs(sortedStinkies) do
|
||||||
|
if stinky.seenAt > GetTime() - 600 then
|
||||||
|
print(string.format("Macroing %s", stinky.name))
|
||||||
|
lines[#lines + 1] = string.format("/tar %s", stinky.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local idx = FindOrCreateMacro("HeimdallTarget")
|
||||||
|
local body = strjoin("\n", unpack(lines))
|
||||||
|
EditMacro(idx, "HeimdallTarget", "INV_Misc_QuestionMark", body)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared.stinkyTracker.stinkies:onChange(function(value)
|
||||||
|
FixMacro(value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
print("Heimdall - Macroer loaded")
|
||||||
|
end
|
84
Modules/Messenger.lua
Normal file
84
Modules/Messenger.lua
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.Messenger = {}
|
||||||
|
function shared.Messenger.Init()
|
||||||
|
---@class Message
|
||||||
|
---@field message string
|
||||||
|
---@field channel string
|
||||||
|
---@field data string
|
||||||
|
|
||||||
|
local function GetChannelId(channelName)
|
||||||
|
local channels = { GetChannelList() }
|
||||||
|
for i = 1, #channels, 2 do
|
||||||
|
local id = channels[i]
|
||||||
|
local name = channels[i + 1]
|
||||||
|
if name == channelName then
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FindOrJoinChannel(channelName, password)
|
||||||
|
local channelId = GetChannelName(channelName)
|
||||||
|
if channelId == 0 then
|
||||||
|
print("Channel", tostring(channelName), "not found, joining")
|
||||||
|
if password then
|
||||||
|
JoinPermanentChannel(channelName, password)
|
||||||
|
else
|
||||||
|
JoinPermanentChannel(channelName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
channelId = GetChannelName(channelName)
|
||||||
|
return channelId
|
||||||
|
end
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
if not shared.messenger then shared.messenger = {} end
|
||||||
|
if not shared.messenger.queue then shared.messenger.queue = {} end
|
||||||
|
if not shared.messenger.ticker then
|
||||||
|
local function DoMessage()
|
||||||
|
if not Heimdall_Data.config.messenger.enabled then return end
|
||||||
|
---@type Message
|
||||||
|
local message = shared.messenger.queue[1]
|
||||||
|
if not message then return end
|
||||||
|
if not message.message or message.message == "" then return end
|
||||||
|
if not message.channel or message.channel == "" then return end
|
||||||
|
|
||||||
|
if message.channel == "CHANNEL" and message.data and string.match(message.data, "%D") then
|
||||||
|
print("Channel presented as string:", message.data)
|
||||||
|
local channelId = GetChannelName(message.data)
|
||||||
|
if channelId == 0 then
|
||||||
|
print("Channel not found, joining")
|
||||||
|
channelId = FindOrJoinChannel(message.data)
|
||||||
|
end
|
||||||
|
print("Channel resolved to id", channelId)
|
||||||
|
message.data = tostring(channelId)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.remove(shared.messenger.queue, 1)
|
||||||
|
if not message.message or message.message == "" then return end
|
||||||
|
if not message.channel or message.channel == "" then return end
|
||||||
|
if not message.data or message.data == "" then return end
|
||||||
|
SendChatMessage(message.message, message.channel, nil, message.data)
|
||||||
|
end
|
||||||
|
local function Tick()
|
||||||
|
DoMessage()
|
||||||
|
shared.messenger.ticker = C_Timer.NewTimer(Heimdall_Data.config.messenger.interval, Tick, 1)
|
||||||
|
end
|
||||||
|
Tick()
|
||||||
|
end
|
||||||
|
|
||||||
|
--C_Timer.NewTicker(2, function()
|
||||||
|
-- print("Q")
|
||||||
|
-- table.insert(data.messenger.queue, {
|
||||||
|
-- channel = "CHANNEL",
|
||||||
|
-- data = "Foobar",
|
||||||
|
-- message = "TEST"
|
||||||
|
-- })
|
||||||
|
--end)
|
||||||
|
|
||||||
|
print("Heimdall - Messenger loaded")
|
||||||
|
end
|
667
Modules/ReactiveValue.lua
Normal file
667
Modules/ReactiveValue.lua
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
local function Init()
|
||||||
|
local metadata = {
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return ReactiveValue|nil
|
||||||
|
__add = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value + other._value
|
||||||
|
end
|
||||||
|
if otherType == "string" and self._type == otherType then
|
||||||
|
return self._value .. other
|
||||||
|
end
|
||||||
|
if otherType == "number" and self._type == otherType then
|
||||||
|
return self._value + other
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return ReactiveValue|nil
|
||||||
|
__mul = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value * other._value
|
||||||
|
end
|
||||||
|
if otherType == "number" and self._type == otherType then
|
||||||
|
return self._value * other
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return ReactiveValue|nil
|
||||||
|
__sub = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value - other._value
|
||||||
|
end
|
||||||
|
if otherType == "number" and self._type == otherType then
|
||||||
|
return self._value - other
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return ReactiveValue|nil
|
||||||
|
__div = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value / other._value
|
||||||
|
end
|
||||||
|
if otherType == "number" and self._type == otherType then
|
||||||
|
return self._value / other
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return ReactiveValue|nil
|
||||||
|
__mod = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value % other._value
|
||||||
|
end
|
||||||
|
if otherType == "number" and self._type == otherType then
|
||||||
|
return self._value % other
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return ReactiveValue|nil
|
||||||
|
__pow = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value ^ other._value
|
||||||
|
end
|
||||||
|
if otherType == "number" and self._type == otherType then
|
||||||
|
return self._value ^ other
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return boolean
|
||||||
|
__eq = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value == other._value
|
||||||
|
end
|
||||||
|
return self._value == other
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return boolean
|
||||||
|
__lt = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value < other._value
|
||||||
|
end
|
||||||
|
return self._value < other
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return boolean
|
||||||
|
__le = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value <= other._value
|
||||||
|
end
|
||||||
|
return self._value <= other
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return boolean
|
||||||
|
__gt = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value > other._value
|
||||||
|
end
|
||||||
|
return self._value > other
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param other ReactiveValue
|
||||||
|
---@return boolean
|
||||||
|
__ge = function(self, other)
|
||||||
|
local otherType = type(other)
|
||||||
|
if otherType == "table" and other._type and other._type == self._type and other._value then
|
||||||
|
return self._value >= other._value
|
||||||
|
end
|
||||||
|
return self._value >= other
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@return number
|
||||||
|
__len = function(self)
|
||||||
|
if self._type == "table" then
|
||||||
|
return #self._value
|
||||||
|
end
|
||||||
|
if self._type == "string" then
|
||||||
|
return string.len(self._value)
|
||||||
|
end
|
||||||
|
return 0
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@return string
|
||||||
|
__tostring = function(self)
|
||||||
|
return tostring(self._value)
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param key string
|
||||||
|
---@param value any
|
||||||
|
---@return nil
|
||||||
|
__newindex = function(self, key, value)
|
||||||
|
local setupComplete = rawget(self, "_setupComplete")
|
||||||
|
if setupComplete == nil or setupComplete == false then
|
||||||
|
rawset(self, key, value)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self._type ~= "table" then
|
||||||
|
rawset(self, key, value)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self._value[key] = value
|
||||||
|
local ChangedKey = { key }
|
||||||
|
|
||||||
|
-- If the value being assigned is a ReactiveValue
|
||||||
|
-- Then listen to changes on it as well
|
||||||
|
-- And propagate those changes upwards
|
||||||
|
if self._recursive and getmetatable(value) == getmetatable(self) then
|
||||||
|
self:_setupListeners(key, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:_notify()
|
||||||
|
self:_notifyFieldChanged(ChangedKey)
|
||||||
|
self:_notifyAnyFieldChanged(ChangedKey)
|
||||||
|
end,
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param key string
|
||||||
|
---@return any|nil
|
||||||
|
__index = function(self, key)
|
||||||
|
local value = rawget(self, key)
|
||||||
|
if value ~= nil then
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
if rawget(self, "_type") ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local innerTable = rawget(self, "_value")
|
||||||
|
if innerTable ~= nil then
|
||||||
|
return rawget(innerTable, key)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
-- __index = ReactiveValue
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Sadly I could not get @generic to play nice with this class
|
||||||
|
--- I think it's not ready yet, there are issues on github describing similar problems and it is marked as WIP...
|
||||||
|
--- Guess I'll have to live without it for now and specify type of a RV in #type
|
||||||
|
|
||||||
|
---## A type safe value that can be listened to for changes
|
||||||
|
---### **Always use RV:set() for setting primitive values**
|
||||||
|
--- Supports primitive values and tables<br>
|
||||||
|
--- Tables can be listened to for changes on any field or a specific field<br>
|
||||||
|
---### Example usage (value):<br>
|
||||||
|
--- ```lua
|
||||||
|
--- local test = ReactiveValue.new(1)
|
||||||
|
--- test:onChange(function(value)
|
||||||
|
--- print("test changed to " .. value)
|
||||||
|
--- end)
|
||||||
|
--- test:set(2)
|
||||||
|
--- test:set(test + 3)
|
||||||
|
--- ```
|
||||||
|
---### Example usage (table):<br>
|
||||||
|
--- ```lua
|
||||||
|
--- local test = ReactiveValue.new({1, 2, 3})
|
||||||
|
--- test:onAnyFieldChange(function(field, value)
|
||||||
|
--- print(string.format("test.%s changed to %s", table.concat(field, "."), value))
|
||||||
|
--- end)
|
||||||
|
--- test[1] = 4 -- test.1 changed to 4
|
||||||
|
--- test[4] = {1, 2, 3} -- test.4 changed to <table>
|
||||||
|
--- test[4][1] = 14 -- No log(!!) because test[4] is a table and not a ReactiveValue
|
||||||
|
--- ```
|
||||||
|
---### To trigger a callback for `test[4][1]` in the previous example do:<br>
|
||||||
|
--- ```lua
|
||||||
|
--- local test = ReactiveValue.new({1, 2, 3}, true)
|
||||||
|
--- test:onAnyFieldChange(function(field, value)
|
||||||
|
--- print(string.format("test.%s changed to %s", table.concat(field, "."), value))
|
||||||
|
--- end)
|
||||||
|
--- test[1] = 4 -- test.1 changed to 4
|
||||||
|
--- test[4] = {1, 2, 3} -- test.4 changed to <table>
|
||||||
|
--- test[4][1] = 14 -- test.4.1 changed to 14
|
||||||
|
--- ```
|
||||||
|
---### To listen to a specific field of a table do:<br>
|
||||||
|
--- ```lua
|
||||||
|
--- local test = ReactiveValue.new({1, 2, 3}, true)
|
||||||
|
--- test:onFieldChange("1", function(value)
|
||||||
|
--- print("test.1 changed to " .. value)
|
||||||
|
--- end)
|
||||||
|
--- test[1] = 4 -- test.1 changed to 4
|
||||||
|
--- test[4] = {1, 2, 3} -- Does not trigger callback
|
||||||
|
-- ```
|
||||||
|
---@class ReactiveValue
|
||||||
|
---@field _listeners table<function, boolean>
|
||||||
|
---@field _fieldListeners table<string, table<function, boolean>>
|
||||||
|
---@field _anyFieldListeners table<number, table<function, boolean>>
|
||||||
|
---@field _oneTimeListeners table<function, boolean>
|
||||||
|
---@field _value any
|
||||||
|
---@field _type string
|
||||||
|
---@field _recursive boolean?
|
||||||
|
ReactiveValue = {
|
||||||
|
---#### Get the underlying value of a ReactiveValue
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@return any
|
||||||
|
get = function(self) end,
|
||||||
|
|
||||||
|
---### Set the underlying value of a ReactiveValue triggering listener callbacks
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param newValue any
|
||||||
|
set = function(self, newValue) end,
|
||||||
|
|
||||||
|
---## EVENT
|
||||||
|
---### Register a listener that is triggered whenever the underlying value changes
|
||||||
|
--- Returns a function that can be called to undo the callback
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param callback fun(value: any, type: string)
|
||||||
|
---@return fun(): nil
|
||||||
|
onChange = function(self, callback) end,
|
||||||
|
|
||||||
|
---## EVENT
|
||||||
|
---### Register a listener that is triggered whenever a specific field of a table changes
|
||||||
|
--- Returns a function that can be called to undo the callback
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param field string
|
||||||
|
---@param callback fun(field: string[], value: any, type: string)
|
||||||
|
---@return fun(): nil
|
||||||
|
onFieldChange = function(self, field, callback) end,
|
||||||
|
|
||||||
|
---## EVENT
|
||||||
|
---### Register a listener that is triggered whenever any field of a table changes
|
||||||
|
--- Returns a function that can be called to undo the callback
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param callback fun(field: string[], value: any, type: string)
|
||||||
|
---@param depth number? How deep to listen for changes
|
||||||
|
---@return fun(): nil
|
||||||
|
onAnyFieldChange = function(self, callback, depth) end,
|
||||||
|
|
||||||
|
---## EVENT
|
||||||
|
---### Register a listener that is triggered ONCE whenever the underlying value changes
|
||||||
|
--- Returns a function that can be called to undo the callback
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param callback fun(value: any, type: string)
|
||||||
|
---@return fun(): nil
|
||||||
|
once = function(self, callback) end,
|
||||||
|
---### Setup listeners for all fields of a table recursively
|
||||||
|
--- This is used to ensure that listeners are notified recursively
|
||||||
|
|
||||||
|
---@param self ReactiveValue
|
||||||
|
_setupAllListenersRecursively = function(self) end,
|
||||||
|
---### Setup listeners for a specific field of a table recursively
|
||||||
|
--- This is used to ensure that listeners are notified recursively
|
||||||
|
|
||||||
|
---@param self ReactiveValue
|
||||||
|
_setupListeners = function(self, key, value, recursive) end,
|
||||||
|
|
||||||
|
---### Notify listeners that the underlying value has changed
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@return nil
|
||||||
|
---#### Event contains:
|
||||||
|
--- 2. value: any - The new value of the changed field
|
||||||
|
--- 3. type: string - The type of the new value of the changed field
|
||||||
|
_notify = function(self) end,
|
||||||
|
|
||||||
|
---### Notify listeners that a specific field of the underlying value has changed
|
||||||
|
---#### Event contains:
|
||||||
|
--- 1. field: table<string> - A list of keys that lead to the changed field
|
||||||
|
--- 2. value: any - The new value of the changed field
|
||||||
|
--- 3. type: string - The type of the new value of the changed field
|
||||||
|
---@param self ReactiveValue
|
||||||
|
_notifyFieldChanged = function(self, field) end,
|
||||||
|
|
||||||
|
---### Notify listeners that any field of the underlying value has changed
|
||||||
|
---#### Event contains:
|
||||||
|
--- 1. field: table<string> - A list of keys that lead to the changed field
|
||||||
|
--- 2. value: any - The new value of the changed field
|
||||||
|
--- 3. type: string - The type of the new value of the changed field
|
||||||
|
_notifyAnyFieldChanged = function(self, field) end,
|
||||||
|
}
|
||||||
|
---### Constructor
|
||||||
|
---@param initialValue any
|
||||||
|
---@param recursive boolean?
|
||||||
|
---@return ReactiveValue
|
||||||
|
ReactiveValue.new = function(initialValue, recursive)
|
||||||
|
local self = setmetatable({}, metadata)
|
||||||
|
self._listeners = {}
|
||||||
|
self._fieldListeners = {}
|
||||||
|
self._anyFieldListeners = {}
|
||||||
|
self._oneTimeListeners = {}
|
||||||
|
self._value = initialValue
|
||||||
|
self._type = type(initialValue)
|
||||||
|
self._recursive = recursive or false
|
||||||
|
|
||||||
|
---@return any
|
||||||
|
self.get = function(self)
|
||||||
|
return self._value
|
||||||
|
end
|
||||||
|
---@param newValue any
|
||||||
|
self.set = function(self, newValue)
|
||||||
|
if self._value == newValue then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if type(newValue) ~= self._type then
|
||||||
|
error("Expected " .. self._type .. ", got " .. type(newValue))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self._value = newValue
|
||||||
|
self:_notify()
|
||||||
|
end
|
||||||
|
self.onChange = function(self, callback)
|
||||||
|
if type(callback) ~= "function" then
|
||||||
|
error("Expected function, got " .. type(callback))
|
||||||
|
return function() end
|
||||||
|
end
|
||||||
|
self._listeners[callback] = true
|
||||||
|
return function()
|
||||||
|
self._listeners[callback] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.onFieldChange = function(self, field, callback)
|
||||||
|
if type(callback) ~= "function" then
|
||||||
|
error("Expected function, got " .. type(callback))
|
||||||
|
return function() end
|
||||||
|
end
|
||||||
|
if self._fieldListeners[field] == nil then
|
||||||
|
self._fieldListeners[field] = {}
|
||||||
|
end
|
||||||
|
self._fieldListeners[field][callback] = true
|
||||||
|
return function()
|
||||||
|
self._fieldListeners[field][callback] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.onAnyFieldChange = function(self, callback, depth)
|
||||||
|
depth = depth or 99999
|
||||||
|
if type(callback) ~= "function" then
|
||||||
|
error("Expected function, got " .. type(callback))
|
||||||
|
return function() end
|
||||||
|
end
|
||||||
|
if self._anyFieldListeners[depth] == nil then
|
||||||
|
self._anyFieldListeners[depth] = {}
|
||||||
|
end
|
||||||
|
self._anyFieldListeners[depth][callback] = true
|
||||||
|
return function()
|
||||||
|
self._anyFieldListeners[depth][callback] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.once = function(self, callback)
|
||||||
|
if type(callback) ~= "function" then
|
||||||
|
error("Expected function, got " .. type(callback))
|
||||||
|
return function() end
|
||||||
|
end
|
||||||
|
self._oneTimeListeners[callback] = true
|
||||||
|
return function()
|
||||||
|
self._oneTimeListeners[callback] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self._setupAllListenersRecursively = function(self)
|
||||||
|
if self._type ~= "table" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for key, value in pairs(self._value) do
|
||||||
|
self:_setupListeners(key, value, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
---@param key string
|
||||||
|
---@param value any
|
||||||
|
---@param recursive boolean?
|
||||||
|
self._setupListeners = function(self, key, value, recursive)
|
||||||
|
recursive = recursive or false
|
||||||
|
if self._type ~= "table" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if getmetatable(value) ~= getmetatable(self) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
value._recursive = true
|
||||||
|
if value._type == "table" then
|
||||||
|
value:onAnyFieldChange(function(key2)
|
||||||
|
ChangedKey = { key, table.unpack(key2) }
|
||||||
|
self:_notifyFieldChanged(ChangedKey)
|
||||||
|
self:_notifyAnyFieldChanged(ChangedKey)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
value:onChange(function(newVal)
|
||||||
|
ChangedKey = { key }
|
||||||
|
self:_notifyFieldChanged(ChangedKey)
|
||||||
|
self:_notifyAnyFieldChanged(ChangedKey)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
if recursive then
|
||||||
|
value:_setupAllListenersRecursively()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if recursive then
|
||||||
|
self:_setupAllListenersRecursively()
|
||||||
|
end
|
||||||
|
|
||||||
|
self._notify = function(self)
|
||||||
|
for listener, _ in pairs(self._oneTimeListeners) do
|
||||||
|
-- task.spawn(listener, self._value, self._type)
|
||||||
|
listener(self._value, self._type)
|
||||||
|
self._oneTimeListeners[listener] = nil
|
||||||
|
end
|
||||||
|
for listener, _ in pairs(self._listeners) do
|
||||||
|
-- task.spawn(listener, self._value, self._type)
|
||||||
|
listener(self._value, self._type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- TODO: Maybe implement some sort of regex here or something...
|
||||||
|
-- Such as listening to *.field1 or something
|
||||||
|
-- But this (having to loop over listeners and evaluate some condition) would tank performance
|
||||||
|
-- Compared to a simple lookup
|
||||||
|
-- So I'm not going to do anything about it for now, until I figure out a better way
|
||||||
|
---@param field table<string, string> A list of keys that lead to the changed field
|
||||||
|
---@return nil
|
||||||
|
self._notifyFieldChanged = function(self, field)
|
||||||
|
local value = self._value
|
||||||
|
for _, key in ipairs(field) do
|
||||||
|
value = value[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
local strfield = table.concat(field, ".")
|
||||||
|
if self._fieldListeners[strfield] == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for listener, _ in pairs(self._fieldListeners[strfield]) do
|
||||||
|
-- task.spawn(listener, value, type(value))
|
||||||
|
listener(value, type(value))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
---@param self ReactiveValue
|
||||||
|
---@param field table<string, string> A list of keys that lead to the changed field
|
||||||
|
---@return nil
|
||||||
|
self._notifyAnyFieldChanged = function(self, field)
|
||||||
|
local value = self._value
|
||||||
|
for _, key in ipairs(field) do
|
||||||
|
value = value[key]
|
||||||
|
end
|
||||||
|
local keyDepth = #field
|
||||||
|
for listenerDepth, listeners in pairs(self._anyFieldListeners) do
|
||||||
|
if listenerDepth >= keyDepth then
|
||||||
|
for listener, _ in pairs(listeners) do
|
||||||
|
-- The reason this also returns type(value) is so that clients don't have to compute type(value)
|
||||||
|
-- I assume some of them might want to do it so computing it once is probably better than having every client compute it for themselves
|
||||||
|
-- task.spawn(listener, field, value, type(value))
|
||||||
|
listener(field, value, type(value))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self._setupComplete = true
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
-- S -- begintest
|
||||||
|
-- S local invocations = 0
|
||||||
|
-- S -- Integer example
|
||||||
|
-- S local test = ReactiveValue.new(1)
|
||||||
|
-- S test:onChange(function(value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test changed to " .. value)
|
||||||
|
-- S end)
|
||||||
|
-- S test:set(2)
|
||||||
|
-- S assert(invocations == 1)
|
||||||
|
-- S
|
||||||
|
-- S invocations = 0
|
||||||
|
-- String example
|
||||||
|
-- S test = ReactiveValue.new("test")
|
||||||
|
-- S test:onChange(function(value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test changed to " .. value)
|
||||||
|
-- S end)
|
||||||
|
-- S test:set("test2")
|
||||||
|
-- S assert(invocations == 1)
|
||||||
|
-- S
|
||||||
|
-- S -- Type safety example
|
||||||
|
-- S local res, err = pcall(test.set, test, 1)
|
||||||
|
-- S assert(res == false)
|
||||||
|
-- S assert(err:find("Expected string, got number"))
|
||||||
|
-- S
|
||||||
|
-- S -- Table example
|
||||||
|
-- S invocations = 0
|
||||||
|
-- S test = ReactiveValue.new({1, 2, 3})
|
||||||
|
-- S local clbk = test:onChange(function(value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test changed to")
|
||||||
|
-- S dumpTable(value, 0)
|
||||||
|
-- S end)
|
||||||
|
-- S test:set({1, 2, 3, 4})
|
||||||
|
-- S assert(invocations == 1)
|
||||||
|
-- S
|
||||||
|
-- S -- Callback removal example
|
||||||
|
-- S clbk()
|
||||||
|
-- S
|
||||||
|
-- S invocations = 0
|
||||||
|
-- S -- Any field change example
|
||||||
|
-- S clbk = test:onAnyFieldChange(function(field, value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value))
|
||||||
|
-- S end)
|
||||||
|
-- S test.Pero = 1
|
||||||
|
-- S test.Pero = nil
|
||||||
|
-- S assert(invocations == 2)
|
||||||
|
-- S clbk()
|
||||||
|
-- S
|
||||||
|
-- S invocations = 0
|
||||||
|
-- S -- Field change example
|
||||||
|
-- S test:onFieldChange("Pero", function(value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test.Pero changed to " .. value)
|
||||||
|
-- S end)
|
||||||
|
-- S test.Pero = 2
|
||||||
|
-- S assert(invocations == 1)
|
||||||
|
-- S
|
||||||
|
-- S invocations = 0
|
||||||
|
-- S -- One time listener example
|
||||||
|
-- S test:once(function(value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test changed to")
|
||||||
|
-- S dumpTable(value, 0)
|
||||||
|
-- S end)
|
||||||
|
-- S test:set({3, 2, 1})
|
||||||
|
-- S assert(invocations == 1)
|
||||||
|
-- S
|
||||||
|
-- S invocations = 0
|
||||||
|
-- S -- Table push example
|
||||||
|
-- S test = ReactiveValue.new({})
|
||||||
|
-- S test:onChange(function(value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test changed to")
|
||||||
|
-- S dumpTable(value, 0)
|
||||||
|
-- S end)
|
||||||
|
-- S test:onAnyFieldChange(function(field, value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test." .. table.concat(field, ".") .. " changed to " .. value)
|
||||||
|
-- S end)
|
||||||
|
-- S test[#test + 1] = 4
|
||||||
|
-- S assert(invocations == 2)
|
||||||
|
-- S
|
||||||
|
-- S invocations = 0
|
||||||
|
-- S test = ReactiveValue.new({
|
||||||
|
-- S name = "pero",
|
||||||
|
-- S coins = ReactiveValue.new(1)
|
||||||
|
-- S })
|
||||||
|
-- S test.coins:onChange(function(value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test.coins changed to " .. value)
|
||||||
|
-- S end)
|
||||||
|
-- S test.coins:set(2)
|
||||||
|
-- S assert(invocations == 1)
|
||||||
|
-- S
|
||||||
|
-- S invocations = 0
|
||||||
|
-- S test = ReactiveValue.new({
|
||||||
|
-- S name = "pero",
|
||||||
|
-- S coins = ReactiveValue.new(1)
|
||||||
|
-- S }, true)
|
||||||
|
-- S test:onAnyFieldChange(function(field, value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value))
|
||||||
|
-- S end)
|
||||||
|
-- S test.coins:set(2)
|
||||||
|
-- S test.pero2 = ReactiveValue.new({})
|
||||||
|
-- S test.pero2.coins = ReactiveValue.new(1)
|
||||||
|
-- S test.pero2.coins:set(2)
|
||||||
|
-- S assert(invocations == 4)
|
||||||
|
-- S
|
||||||
|
-- S invocations = 0
|
||||||
|
-- S test = ReactiveValue.new({
|
||||||
|
-- S name = "pero",
|
||||||
|
-- S coins = ReactiveValue.new({
|
||||||
|
-- S value = ReactiveValue.new(1)
|
||||||
|
-- S })
|
||||||
|
-- S }, true)
|
||||||
|
-- S test:onAnyFieldChange(function(field, value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value))
|
||||||
|
-- S end)
|
||||||
|
-- S test.coins.value:set(2)
|
||||||
|
-- S assert(invocations == 1)
|
||||||
|
-- S
|
||||||
|
-- S invocations = 0
|
||||||
|
-- S test = ReactiveValue.new({}, true)
|
||||||
|
-- S test.coins = ReactiveValue.new({})
|
||||||
|
-- S test.coins.value = ReactiveValue.new(1)
|
||||||
|
-- S test:onAnyFieldChange(function(field, value)
|
||||||
|
-- S invocations = invocations + 1
|
||||||
|
-- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value))
|
||||||
|
-- S end)
|
||||||
|
-- S test.coins.value:set(3)
|
||||||
|
-- S assert(invocations == 1)
|
||||||
|
--S
|
||||||
|
--S invocations = 0
|
||||||
|
--S test = ReactiveValue.new({}, true)
|
||||||
|
--S test:onAnyFieldChange(function(field, value)
|
||||||
|
--S invocations = invocations + 1
|
||||||
|
--S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value))
|
||||||
|
--S end, 1)
|
||||||
|
--S test.test2 = ReactiveValue.new({}, true)
|
||||||
|
--S test.test2.test3 = ReactiveValue.new(1)
|
||||||
|
--S assert(invocations == 1)
|
||||||
|
--S
|
||||||
|
-- S -- endtest
|
||||||
|
end
|
||||||
|
|
||||||
|
local frame = CreateFrame("Frame")
|
||||||
|
frame:RegisterEvent("PLAYER_LOGIN")
|
||||||
|
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||||
|
frame:RegisterEvent("GUILD_ROSTER_UPDATE")
|
||||||
|
frame:SetScript("OnEvent", function(self, event, ...)
|
||||||
|
Init()
|
||||||
|
end)
|
||||||
|
Init()
|
@@ -1,14 +1,10 @@
|
|||||||
local addonname, data = ...
|
local addonname, shared = ...
|
||||||
---@cast data HeimdallData
|
---@cast shared HeimdallShared
|
||||||
---@cast addonname string
|
---@cast addonname string
|
||||||
|
|
||||||
data.Spotter = {}
|
---@diagnostic disable-next-line: missing-fields
|
||||||
function data.Spotter.Init()
|
shared.Spotter = {}
|
||||||
if not data.config.spotter.enabled then
|
function shared.Spotter.Init()
|
||||||
print("Heimdall - Spotter disabled")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local function FormatHP(hp)
|
local function FormatHP(hp)
|
||||||
if hp > 1e9 then
|
if hp > 1e9 then
|
||||||
return string.format("%.1fB", hp / 1e9)
|
return string.format("%.1fB", hp / 1e9)
|
||||||
@@ -31,16 +27,17 @@ function data.Spotter.Init()
|
|||||||
---@return boolean
|
---@return boolean
|
||||||
---@return string? error
|
---@return string? error
|
||||||
local function ShouldNotify(unit, name, faction, hostile)
|
local function ShouldNotify(unit, name, faction, hostile)
|
||||||
if data.config.spotter.stinky then
|
if Heimdall_Data.config.agents[name] then return false end
|
||||||
if data.config.stinkies[name] then return true end
|
if Heimdall_Data.config.spotter.stinky then
|
||||||
|
if Heimdall_Data.config.stinkies[name] then return true end
|
||||||
end
|
end
|
||||||
if data.config.spotter.alliance then
|
if Heimdall_Data.config.spotter.alliance then
|
||||||
if faction == "Alliance" then return true end
|
if faction == "Alliance" then return true end
|
||||||
end
|
end
|
||||||
if data.config.spotter.hostile then
|
if Heimdall_Data.config.spotter.hostile then
|
||||||
if hostile then return true end
|
if hostile then return true end
|
||||||
end
|
end
|
||||||
return data.config.spotter.everyone
|
return Heimdall_Data.config.spotter.everyone
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param unit string
|
---@param unit string
|
||||||
@@ -53,14 +50,14 @@ function data.Spotter.Init()
|
|||||||
if not name then return string.format("Could not find name for unit %s", tostring(unit)) end
|
if not name then return string.format("Could not find name for unit %s", tostring(unit)) end
|
||||||
|
|
||||||
local time = GetTime()
|
local time = GetTime()
|
||||||
if throttleTable[name] and time - throttleTable[name] < data.config.spotter.throttleTime then
|
if throttleTable[name] and time - throttleTable[name] < Heimdall_Data.config.spotter.throttleTime then
|
||||||
return string.format("Throttled %s", tostring(name))
|
return string.format("Throttled %s", tostring(name))
|
||||||
end
|
end
|
||||||
throttleTable[name] = time
|
throttleTable[name] = time
|
||||||
|
|
||||||
local race = UnitRace(unit)
|
local race = UnitRace(unit)
|
||||||
if not race then return string.format("Could not find race for unit %s", tostring(unit)) end
|
if not race then return string.format("Could not find race for unit %s", tostring(unit)) end
|
||||||
local faction = data.raceMap[race]
|
local faction = shared.raceMap[race]
|
||||||
if not faction then return string.format("Could not find faction for race %s", tostring(race)) end
|
if not faction then return string.format("Could not find faction for race %s", tostring(race)) end
|
||||||
|
|
||||||
local hostile = UnitCanAttack("player", unit)
|
local hostile = UnitCanAttack("player", unit)
|
||||||
@@ -73,8 +70,11 @@ function data.Spotter.Init()
|
|||||||
local maxHp = UnitHealthMax(unit)
|
local maxHp = UnitHealthMax(unit)
|
||||||
if not maxHp then return string.format("Could not find maxHp for unit %s", tostring(unit)) end
|
if not maxHp then return string.format("Could not find maxHp for unit %s", tostring(unit)) end
|
||||||
|
|
||||||
local location = data.config.spotter.zoneOverride
|
local class = UnitClass(unit)
|
||||||
if not location then
|
if not class then return string.format("Could not find class for unit %s", tostring(unit)) end
|
||||||
|
|
||||||
|
local location = Heimdall_Data.config.spotter.zoneOverride
|
||||||
|
if not location or location == "" then
|
||||||
local zone = GetZoneText()
|
local zone = GetZoneText()
|
||||||
if not zone then return string.format("Could not find zone for unit %s", tostring(unit)) end
|
if not zone then return string.format("Could not find zone for unit %s", tostring(unit)) end
|
||||||
local subzone = GetSubZoneText()
|
local subzone = GetSubZoneText()
|
||||||
@@ -82,31 +82,38 @@ function data.Spotter.Init()
|
|||||||
location = string.format("%s (%s)", zone, subzone)
|
location = string.format("%s (%s)", zone, subzone)
|
||||||
end
|
end
|
||||||
|
|
||||||
local stinky = data.config.stinkies[name] or false
|
local x, y = GetPlayerMapPosition("player")
|
||||||
local text = string.format("I see (%s) %s %s of race %s (%s) with health %s/%s at %s",
|
local stinky = Heimdall_Data.config.stinkies[name] or false
|
||||||
|
local text = string.format("I see (%s) %s/%s %s of race %s (%s) with health %s/%s at %s (%2.2f, %2.2f)",
|
||||||
hostile and "Hostile" or "Friendly",
|
hostile and "Hostile" or "Friendly",
|
||||||
stinky and string.format("(%s)", "!!!!") or "",
|
|
||||||
name,
|
name,
|
||||||
|
class,
|
||||||
|
stinky and string.format("(%s)", "!!!!") or "",
|
||||||
race,
|
race,
|
||||||
faction,
|
faction,
|
||||||
FormatHP(hp),
|
FormatHP(hp),
|
||||||
FormatHP(maxHp),
|
FormatHP(maxHp),
|
||||||
location)
|
location,
|
||||||
|
x * 100, y * 100)
|
||||||
|
|
||||||
---@type Message
|
---@type Message
|
||||||
local msg = {
|
local msg = {
|
||||||
channel = "CHANNEL",
|
channel = "CHANNEL",
|
||||||
data = data.config.spotter.notifyChannel,
|
data = Heimdall_Data.config.spotter.notifyChannel,
|
||||||
message = text
|
message = text
|
||||||
}
|
}
|
||||||
data.dumpTable(msg)
|
--shared.dumpTable(msg)
|
||||||
table.insert(data.messenger.queue, msg)
|
table.insert(shared.messenger.queue, msg)
|
||||||
end
|
end
|
||||||
|
|
||||||
local frame = CreateFrame("Frame")
|
local frame = CreateFrame("Frame")
|
||||||
frame:RegisterEvent("NAME_PLATE_UNIT_ADDED")
|
frame:RegisterEvent("NAME_PLATE_UNIT_ADDED")
|
||||||
frame:RegisterEvent("TARGET_UNIT_CHANGED")
|
frame:RegisterEvent("UNIT_TARGET")
|
||||||
frame:SetScript("OnEvent", function(self, event, unit)
|
frame:SetScript("OnEvent", function(self, event, unit)
|
||||||
|
if not Heimdall_Data.config.spotter.enabled then return end
|
||||||
|
if event == "UNIT_TARGET" then
|
||||||
|
unit = "target"
|
||||||
|
end
|
||||||
local err = NotifySpotted(unit)
|
local err = NotifySpotted(unit)
|
||||||
if err then
|
if err then
|
||||||
print(string.format("Error notifying %s: %s", tostring(unit), tostring(err)))
|
print(string.format("Error notifying %s: %s", tostring(unit), tostring(err)))
|
109
Modules/StinkyTracker.lua
Normal file
109
Modules/StinkyTracker.lua
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
local addonname, shared = ...
|
||||||
|
---@cast shared HeimdallShared
|
||||||
|
---@cast addonname string
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
shared.StinkyTracker = {}
|
||||||
|
function shared.StinkyTracker.Init()
|
||||||
|
shared.stinkyTracker = {
|
||||||
|
stinkies = ReactiveValue.new({})
|
||||||
|
}
|
||||||
|
|
||||||
|
local whoRegex = "([^ -/]+)-?%w*/(%w+)"
|
||||||
|
---@param msg string
|
||||||
|
---@return table<string, stinky>
|
||||||
|
local function ParseWho(msg)
|
||||||
|
local stinkies = {}
|
||||||
|
for name, class in string.gmatch(msg, whoRegex) do
|
||||||
|
stinkies[name] = {
|
||||||
|
name = name,
|
||||||
|
class = class,
|
||||||
|
seenAt = GetTime(),
|
||||||
|
hostile = true
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return stinkies
|
||||||
|
end
|
||||||
|
local seeRegex = "I see %((%w+)%) ([^ -/]+)-?%w*/(%w+)"
|
||||||
|
---@param msg string
|
||||||
|
---@return table<string, stinky>
|
||||||
|
local function ParseSee(msg)
|
||||||
|
local stinkies = {}
|
||||||
|
local aggression, name, class = string.match(msg, seeRegex)
|
||||||
|
if not name or not class then
|
||||||
|
return stinkies
|
||||||
|
end
|
||||||
|
local stinky = {
|
||||||
|
name = name,
|
||||||
|
class = class,
|
||||||
|
seenAt = GetTime(),
|
||||||
|
hostile = aggression == "Hostile"
|
||||||
|
}
|
||||||
|
stinkies[name] = stinky
|
||||||
|
return stinkies
|
||||||
|
end
|
||||||
|
local arrivedRegex = "([^ -/]+)-?%w* of class (%w+)"
|
||||||
|
local arrivedRegexAlt = "([^ -/]+)-?%w* %(!!!!%) of class (%w+)"
|
||||||
|
---@param msg string
|
||||||
|
---@return table<string, stinky>
|
||||||
|
local function ParseArrived(msg)
|
||||||
|
local stinkies = {}
|
||||||
|
local name, class = string.match(msg, arrivedRegex)
|
||||||
|
if not name or not class then
|
||||||
|
name, class = string.match(msg, arrivedRegexAlt)
|
||||||
|
end
|
||||||
|
if not name or not class then
|
||||||
|
return stinkies
|
||||||
|
end
|
||||||
|
local stinky = {
|
||||||
|
name = name,
|
||||||
|
class = class,
|
||||||
|
seenAt = GetTime(),
|
||||||
|
hostile = true
|
||||||
|
}
|
||||||
|
stinkies[name] = stinky
|
||||||
|
return stinkies
|
||||||
|
end
|
||||||
|
|
||||||
|
local frame = CreateFrame("Frame")
|
||||||
|
frame:RegisterEvent("CHAT_MSG_CHANNEL")
|
||||||
|
frame:SetScript("OnEvent", function(self, event, msg, sender, ...)
|
||||||
|
if not Heimdall_Data.config.stinkyTracker.enabled then return end
|
||||||
|
local channelId = select(6, ...)
|
||||||
|
local _, channelname = GetChannelName(channelId)
|
||||||
|
if channelname ~= Heimdall_Data.config.stinkyTracker.masterChannel then return end
|
||||||
|
|
||||||
|
if string.find(msg, "^who:") then
|
||||||
|
local whoStinkies = ParseWho(msg)
|
||||||
|
for name, stinky in pairs(whoStinkies) do
|
||||||
|
if stinky.hostile then
|
||||||
|
shared.stinkyTracker.stinkies[name] = stinky
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if string.find(msg, "^I see") then
|
||||||
|
local seeStinkies = ParseSee(msg)
|
||||||
|
for name, stinky in pairs(seeStinkies) do
|
||||||
|
if stinky.hostile then
|
||||||
|
shared.stinkyTracker.stinkies[name] = stinky
|
||||||
|
end
|
||||||
|
if not stinky.hostile then
|
||||||
|
shared.stinkyTracker.stinkies[name] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if string.find(msg, " and guild ") then
|
||||||
|
local arrivedStinkies = ParseArrived(msg)
|
||||||
|
for name, stinky in pairs(arrivedStinkies) do
|
||||||
|
shared.stinkyTracker.stinkies[name] = stinky
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for name, stinky in pairs(shared.stinkyTracker.stinkies) do
|
||||||
|
if Heimdall_Data.config.agents[name] then
|
||||||
|
shared.stinkyTracker.stinkies[name] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
print("Heimdall - StinkyTracker loaded")
|
||||||
|
end
|
@@ -1,14 +1,10 @@
|
|||||||
local addonname, data = ...
|
local addonname, shared = ...
|
||||||
---@cast data HeimdallData
|
---@cast shared HeimdallShared
|
||||||
---@cast addonname string
|
---@cast addonname string
|
||||||
|
|
||||||
data.Whoer = {}
|
---@diagnostic disable-next-line: missing-fields
|
||||||
function data.Whoer.Init()
|
shared.Whoer = {}
|
||||||
if not data.config.who.enabled then
|
function shared.Whoer.Init()
|
||||||
print("Heimdall - Whoer disabled")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if not Heimdall_Data.who then Heimdall_Data.who = {} end
|
if not Heimdall_Data.who then Heimdall_Data.who = {} end
|
||||||
if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end
|
if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end
|
||||||
|
|
||||||
@@ -51,22 +47,23 @@ function data.Whoer.Init()
|
|||||||
---@return string
|
---@return string
|
||||||
ToString = function(self)
|
ToString = function(self)
|
||||||
local out = string.format("%s %s %s\nFirst: %s Last: %s Seen: %3d",
|
local out = string.format("%s %s %s\nFirst: %s Last: %s Seen: %3d",
|
||||||
data.padString(self.name, 16, true),
|
shared.padString(self.name, 16, true),
|
||||||
data.padString(self.guild, 26, false),
|
shared.padString(self.guild, 26, false),
|
||||||
data.padString(self.zone, 26, false),
|
shared.padString(self.zone, 26, false),
|
||||||
data.padString(self.firstSeen, 10, true),
|
shared.padString(self.firstSeen, 10, true),
|
||||||
data.padString(self.lastSeen, 10, true),
|
shared.padString(self.lastSeen, 10, true),
|
||||||
self.seenCount)
|
self.seenCount)
|
||||||
return string.format("|cFF%s%s|r", data.classColors[self.class], out)
|
return string.format("|cFF%s%s|r", shared.classColors[self.class], out)
|
||||||
end,
|
end,
|
||||||
---@return string
|
---@return string
|
||||||
NotifyMessage = function(self)
|
NotifyMessage = function(self)
|
||||||
local text = string.format(
|
local text = string.format(
|
||||||
"%s of class %s, race %s (%s) and guild %s in %s, first seen: %s, last seen: %s, times seen: %d",
|
"%s %s of class %s, race %s (%s) and guild %s in %s, first seen: %s, last seen: %s, times seen: %d",
|
||||||
self.name,
|
self.name,
|
||||||
|
self.stinky and "(!!!!)" or "",
|
||||||
self.class,
|
self.class,
|
||||||
self.race,
|
self.race,
|
||||||
tostring(data.raceMap[self.race]),
|
tostring(shared.raceMap[self.race]),
|
||||||
self.guild,
|
self.guild,
|
||||||
self.zone,
|
self.zone,
|
||||||
self.firstSeen,
|
self.firstSeen,
|
||||||
@@ -103,34 +100,30 @@ function data.Whoer.Init()
|
|||||||
end
|
end
|
||||||
---@type WHOFilter
|
---@type WHOFilter
|
||||||
local AllianceFilter = function(name, guild, level, race, class, zone)
|
local AllianceFilter = function(name, guild, level, race, class, zone)
|
||||||
if not race then
|
if not race then return false end
|
||||||
return false
|
if not shared.raceMap[race] then return false end
|
||||||
end
|
return shared.raceMap[race] == "Alliance"
|
||||||
if not data.raceMap[race] then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
return data.raceMap[race] == "Alliance"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local whoQueryIdx = 1
|
local whoQueryIdx = 1
|
||||||
---@type table<number, WHOQuery>
|
---@type WHOQuery[]
|
||||||
local whoQueries = {
|
local whoQueries = {
|
||||||
WHOQuery.new("g-\"БеспредеЛ\"", {}),
|
WHOQuery.new("g-\"БеспредеЛ\"", {}),
|
||||||
|
--WHOQuery.new("g-\"Dovahkin\"", {}),
|
||||||
WHOQuery.new(
|
WHOQuery.new(
|
||||||
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" z-\"Echo Isles\" r-\"Human\" r-\"Dwarf\" r-\"Night Elf\"",
|
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Human\" r-\"Dwarf\" r-\"Night Elf\"",
|
||||||
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }),
|
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }),
|
||||||
WHOQuery.new(
|
WHOQuery.new(
|
||||||
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" z-\"Echo Isles\" r-\"Gnome\" r-\"Draenei\" r-\"Worgen\"",
|
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Gnome\" r-\"Draenei\" r-\"Worgen\"",
|
||||||
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }),
|
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }),
|
||||||
WHOQuery.new(
|
WHOQuery.new(
|
||||||
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" z-\"Echo Isles\" r-\"Kul Tiran\" r-\"Dark Iron Dwarf\" r-\"Void Elf\"",
|
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Kul Tiran\" r-\"Dark Iron Dwarf\" r-\"Void Elf\"",
|
||||||
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }),
|
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }),
|
||||||
WHOQuery.new(
|
WHOQuery.new(
|
||||||
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" z-\"Echo Isles\" r-\"Lightforged Draenei\" r-\"Mechagnome\"",
|
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Lightforged Draenei\" r-\"Mechagnome\"",
|
||||||
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }),
|
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }),
|
||||||
WHOQuery.new("Kekv Demonboo Dotmada Firobot Verminal", {})
|
WHOQuery.new("Kekv Firobot Tomoki", {})
|
||||||
}
|
}
|
||||||
local queryPending = false
|
|
||||||
local ttl = #whoQueries * 2
|
local ttl = #whoQueries * 2
|
||||||
---@type WHOQuery?
|
---@type WHOQuery?
|
||||||
local lastQuery = nil
|
local lastQuery = nil
|
||||||
@@ -138,8 +131,9 @@ function data.Whoer.Init()
|
|||||||
---@param player Player
|
---@param player Player
|
||||||
---@return string?
|
---@return string?
|
||||||
local function Notify(player)
|
local function Notify(player)
|
||||||
|
if not Heimdall_Data.config.who.enabled then return end
|
||||||
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
|
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
|
||||||
if not data.config.who.zoneNotifyFor[player.zone] then
|
if not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
|
||||||
return string.format("Not notifying for zone %s",
|
return string.format("Not notifying for zone %s",
|
||||||
tostring(player.zone))
|
tostring(player.zone))
|
||||||
end
|
end
|
||||||
@@ -148,20 +142,20 @@ function data.Whoer.Init()
|
|||||||
---@type Message
|
---@type Message
|
||||||
local msg = {
|
local msg = {
|
||||||
channel = "CHANNEL",
|
channel = "CHANNEL",
|
||||||
data = data.config.who.notifyChannel,
|
data = Heimdall_Data.config.who.notifyChannel,
|
||||||
message = text
|
message = text
|
||||||
}
|
}
|
||||||
table.insert(data.messenger.queue, msg)
|
table.insert(shared.messenger.queue, msg)
|
||||||
|
|
||||||
if data.config.who.doWhisper then
|
if Heimdall_Data.config.who.doWhisper then
|
||||||
for _, name in pairs(data.config.whisperNotify) do
|
for _, name in pairs(Heimdall_Data.config.whisperNotify) do
|
||||||
---@type Message
|
---@type Message
|
||||||
local msg = {
|
local msg = {
|
||||||
channel = "WHISPER",
|
channel = "WHISPER",
|
||||||
data = name,
|
data = name,
|
||||||
message = text
|
message = text
|
||||||
}
|
}
|
||||||
table.insert(data.messenger.queue, msg)
|
table.insert(shared.messenger.queue, msg)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -171,34 +165,37 @@ function data.Whoer.Init()
|
|||||||
---@param zone string
|
---@param zone string
|
||||||
---@return string?
|
---@return string?
|
||||||
local function NotifyZoneChanged(player, zone)
|
local function NotifyZoneChanged(player, zone)
|
||||||
|
if not Heimdall_Data.config.who.enabled then return end
|
||||||
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
|
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
|
||||||
if not data.config.who.zoneNotifyFor[zone]
|
if not Heimdall_Data.config.who.zoneNotifyFor[zone]
|
||||||
and not data.config.who.zoneNotifyFor[player.zone] then
|
and not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
|
||||||
return string.format("Not notifying for zones %s and %s", tostring(zone), tostring(player.zone))
|
return string.format("Not notifying for zones %s and %s", tostring(zone), tostring(player.zone))
|
||||||
end
|
end
|
||||||
local text = string.format("%s of class %s and guild %s moved to %s",
|
local text = string.format("%s of class %s (%s - %s) and guild %s moved to %s",
|
||||||
player.name,
|
player.name,
|
||||||
player.class,
|
player.class,
|
||||||
|
player.race,
|
||||||
|
shared.raceMap[player.race] or "Unknown",
|
||||||
player.guild,
|
player.guild,
|
||||||
zone)
|
zone)
|
||||||
|
|
||||||
---@type Message
|
---@type Message
|
||||||
local msg = {
|
local msg = {
|
||||||
channel = "CHANNEL",
|
channel = "CHANNEL",
|
||||||
data = data.config.who.notifyChannel,
|
data = Heimdall_Data.config.who.notifyChannel,
|
||||||
message = text
|
message = text
|
||||||
}
|
}
|
||||||
table.insert(data.messenger.queue, msg)
|
table.insert(shared.messenger.queue, msg)
|
||||||
|
|
||||||
if data.config.who.doWhisper then
|
if Heimdall_Data.config.who.doWhisper then
|
||||||
for _, name in pairs(data.config.whisperNotify) do
|
for _, name in pairs(Heimdall_Data.config.whisperNotify) do
|
||||||
---@type Message
|
---@type Message
|
||||||
local msg = {
|
local msg = {
|
||||||
channel = "WHISPER",
|
channel = "WHISPER",
|
||||||
data = name,
|
data = name,
|
||||||
message = text
|
message = text
|
||||||
}
|
}
|
||||||
table.insert(data.messenger.queue, msg)
|
table.insert(shared.messenger.queue, msg)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -207,8 +204,9 @@ function data.Whoer.Init()
|
|||||||
---@param player Player
|
---@param player Player
|
||||||
---@return string?
|
---@return string?
|
||||||
local function NotifyGone(player)
|
local function NotifyGone(player)
|
||||||
|
if not Heimdall_Data.config.who.enabled then return end
|
||||||
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
|
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
|
||||||
if not data.config.who.zoneNotifyFor[player.zone] then
|
if not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
|
||||||
return string.format("Not notifying for zone %s",
|
return string.format("Not notifying for zone %s",
|
||||||
tostring(player.zone))
|
tostring(player.zone))
|
||||||
end
|
end
|
||||||
@@ -222,20 +220,20 @@ function data.Whoer.Init()
|
|||||||
---@type Message
|
---@type Message
|
||||||
local msg = {
|
local msg = {
|
||||||
channel = "CHANNEL",
|
channel = "CHANNEL",
|
||||||
data = data.config.who.notifyChannel,
|
data = Heimdall_Data.config.who.notifyChannel,
|
||||||
message = text
|
message = text
|
||||||
}
|
}
|
||||||
table.insert(data.messenger.queue, msg)
|
table.insert(shared.messenger.queue, msg)
|
||||||
|
|
||||||
if data.config.who.doWhisper then
|
if Heimdall_Data.config.who.doWhisper then
|
||||||
for _, name in pairs(data.config.whisperNotify) do
|
for _, name in pairs(Heimdall_Data.config.whisperNotify) do
|
||||||
---@type Message
|
---@type Message
|
||||||
local msg = {
|
local msg = {
|
||||||
channel = "WHISPER",
|
channel = "WHISPER",
|
||||||
data = name,
|
data = name,
|
||||||
message = text
|
message = text
|
||||||
}
|
}
|
||||||
table.insert(data.messenger.queue, msg)
|
table.insert(shared.messenger.queue, msg)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -245,6 +243,7 @@ function data.Whoer.Init()
|
|||||||
local frame = CreateFrame("Frame")
|
local frame = CreateFrame("Frame")
|
||||||
frame:RegisterEvent("WHO_LIST_UPDATE")
|
frame:RegisterEvent("WHO_LIST_UPDATE")
|
||||||
frame:SetScript("OnEvent", function(self, event, ...)
|
frame:SetScript("OnEvent", function(self, event, ...)
|
||||||
|
if not Heimdall_Data.config.who.enabled then return end
|
||||||
---@type WHOQuery?
|
---@type WHOQuery?
|
||||||
local query = lastQuery
|
local query = lastQuery
|
||||||
if not query then
|
if not query then
|
||||||
@@ -254,8 +253,8 @@ function data.Whoer.Init()
|
|||||||
|
|
||||||
for i = 1, GetNumWhoResults() do
|
for i = 1, GetNumWhoResults() do
|
||||||
local name, guild, level, race, class, zone = GetWhoInfo(i)
|
local name, guild, level, race, class, zone = GetWhoInfo(i)
|
||||||
if data.who.ignored[name] then return end
|
|
||||||
local continue = false
|
local continue = false
|
||||||
|
--print(name, guild, level, race, class, zone)
|
||||||
|
|
||||||
---@type WHOFilter[]
|
---@type WHOFilter[]
|
||||||
local filters = query.filters
|
local filters = query.filters
|
||||||
@@ -266,6 +265,8 @@ function data.Whoer.Init()
|
|||||||
continue = true
|
continue = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if Heimdall_Data.config.who.ignored[name] then continue = true end
|
||||||
|
--print(continue)
|
||||||
|
|
||||||
if not continue then
|
if not continue then
|
||||||
local timestamp = date("%Y-%m-%dT%H:%M:%S")
|
local timestamp = date("%Y-%m-%dT%H:%M:%S")
|
||||||
@@ -285,12 +286,12 @@ function data.Whoer.Init()
|
|||||||
player.firstSeen = timestamp
|
player.firstSeen = timestamp
|
||||||
end
|
end
|
||||||
|
|
||||||
local stinky = data.config.stinkies[name]
|
local stinky = Heimdall_Data.config.stinkies[name]
|
||||||
if stinky then
|
if stinky then
|
||||||
player.stinky = true
|
player.stinky = true
|
||||||
PlaySoundFile("Interface\\Sounds\\Domination.ogg", "Master")
|
--PlaySoundFile("Interface\\Sounds\\Domination.ogg", "Master")
|
||||||
else
|
else
|
||||||
PlaySoundFile("Interface\\Sounds\\Cloak.ogg", "Master")
|
--PlaySoundFile("Interface\\Sounds\\Cloak.ogg", "Master")
|
||||||
end
|
end
|
||||||
|
|
||||||
local err = Notify(player)
|
local err = Notify(player)
|
||||||
@@ -321,29 +322,28 @@ function data.Whoer.Init()
|
|||||||
-- Turns out WA cannot do this (
|
-- Turns out WA cannot do this (
|
||||||
-- aura_env.UpdateMacro()
|
-- aura_env.UpdateMacro()
|
||||||
_G["FriendsFrameCloseButton"]:Click()
|
_G["FriendsFrameCloseButton"]:Click()
|
||||||
queryPending = false
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if not data.who.updateTicker then
|
do
|
||||||
data.who.updateTicker = C_Timer.NewTicker(0.5, function()
|
local function UpdateStinkies()
|
||||||
for name, player in pairs(HeimdallStinkies) do
|
for name, player in pairs(HeimdallStinkies) do
|
||||||
if player.lastSeenInternal + data.config.who.ttl < GetTime() then
|
if player.lastSeenInternal + Heimdall_Data.config.who.ttl < GetTime() then
|
||||||
NotifyGone(player)
|
NotifyGone(player)
|
||||||
PlaySoundFile("Interface\\Sounds\\Uncloak.ogg", "Master")
|
--PlaySoundFile("Interface\\Sounds\\Uncloak.ogg", "Master")
|
||||||
HeimdallStinkies[name] = nil
|
HeimdallStinkies[name] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end
|
||||||
|
local function Tick()
|
||||||
|
UpdateStinkies()
|
||||||
|
C_Timer.NewTimer(0.5, Tick, 1)
|
||||||
|
end
|
||||||
|
Tick()
|
||||||
end
|
end
|
||||||
|
|
||||||
if not data.who.whoTicker then
|
do
|
||||||
data.who.whoTicker = C_Timer.NewTicker(1, function()
|
local function DoQuery()
|
||||||
if queryPending then
|
if not Heimdall_Data.config.who.enabled then return end
|
||||||
print("Tried running a who query while one is already pending, previous query:")
|
|
||||||
data.dumpTable(lastQuery)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
queryPending = true
|
|
||||||
|
|
||||||
local query = whoQueries[whoQueryIdx]
|
local query = whoQueries[whoQueryIdx]
|
||||||
whoQueryIdx = whoQueryIdx + 1
|
whoQueryIdx = whoQueryIdx + 1
|
||||||
@@ -352,62 +352,16 @@ function data.Whoer.Init()
|
|||||||
end
|
end
|
||||||
lastQuery = query
|
lastQuery = query
|
||||||
--print(string.format("Running who query: %s", tostring(query.query)))
|
--print(string.format("Running who query: %s", tostring(query.query)))
|
||||||
|
---@diagnostic disable-next-line: param-type-mismatch
|
||||||
SetWhoToUI(1)
|
SetWhoToUI(1)
|
||||||
SendWho(query.query)
|
SendWho(query.query)
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
local function Tick()
|
||||||
local whoQueryWhisperFrame = CreateFrame("Frame")
|
DoQuery()
|
||||||
whoQueryWhisperFrame:RegisterEvent("CHAT_MSG_WHISPER")
|
C_Timer.NewTimer(1, Tick, 1)
|
||||||
whoQueryWhisperFrame:SetScript("OnEvent", function(self, event, msg, sender)
|
|
||||||
if msg == "who" then
|
|
||||||
for _, player in pairs(HeimdallStinkies) do
|
|
||||||
local text = player:NotifyMessage()
|
|
||||||
---@type Message
|
|
||||||
local msg = {
|
|
||||||
channel = "WHISPER",
|
|
||||||
data = sender,
|
|
||||||
message = text
|
|
||||||
}
|
|
||||||
table.insert(data.messenger.queue, msg)
|
|
||||||
end
|
end
|
||||||
|
Tick()
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
|
|
||||||
local whoQueryChannelFrame = CreateFrame("Frame")
|
|
||||||
whoQueryChannelFrame:RegisterEvent("CHAT_MSG_CHANNEL")
|
|
||||||
whoQueryChannelFrame:SetScript("OnEvent", function(self, event, msg, sender, ...)
|
|
||||||
local channelId = select(6, ...)
|
|
||||||
local channelname = ""
|
|
||||||
---@type any[]
|
|
||||||
local channels = { GetChannelList() }
|
|
||||||
for i = 1, #channels, 2 do
|
|
||||||
---@type number
|
|
||||||
local id = channels[i]
|
|
||||||
---@type string
|
|
||||||
local name = channels[i + 1]
|
|
||||||
if id == channelId then
|
|
||||||
channelname = name
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if channelname ~= data.config.who.notifyChannel then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if msg == "who" then
|
|
||||||
for _, player in pairs(HeimdallStinkies) do
|
|
||||||
local text = player:NotifyMessage()
|
|
||||||
---@type Message
|
|
||||||
local msg = {
|
|
||||||
channel = "CHANNEL",
|
|
||||||
data = channelname,
|
|
||||||
message = text
|
|
||||||
}
|
|
||||||
table.insert(data.messenger.queue, msg)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
print("Heimdall - Whoer loaded")
|
print("Heimdall - Whoer loaded")
|
||||||
end
|
end
|
324
README.md
Normal file
324
README.md
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
# Heimdall WoW Addon
|
||||||
|
|
||||||
|
Heimdall is a comprehensive World of Warcraft addon designed to provide advanced player tracking, notification, and group management features.
|
||||||
|
|
||||||
|
## Report overview
|
||||||
|
|
||||||
|
- Player spotted:
|
||||||
|
- "I see (Hostile) Atomickitty of race Blood Elf (Horde) with health 6.8M/6.8M at Orgrimmar (Valley of Strength)"
|
||||||
|
- "I see (\<reaction\>) \<name\> of race \<race\> (\<faction\>) with health \<health\>/\<healthMax\> at \<location\>"
|
||||||
|
- Player appeared:
|
||||||
|
- "Любящаядева of class Mage, race Human (Alliance) and guild Анонимное сообщество in Valley of Trials, first seen: 2024-12-27T15:54:38, last seen: never, times seen: 0"
|
||||||
|
- "\<name\> of class \<class\>, race \<race\> (\<faction\>) and guild \<guild\> in \<zone\>, first seen: \<firstSeen\>, last seen: \<lastSeen\>, times seen: \<timesSeen\>"
|
||||||
|
- Player changed zone:
|
||||||
|
- "Thekinglord of class Paladin (Human - Alliance) and guild Caminantes Nocturnos R moved to Durotar"
|
||||||
|
- "\<name\> of class \<class\> (\<faction\>) and guild \<guild\> moved to \<zone\>"
|
||||||
|
- Player disappeared:
|
||||||
|
- "Thekinglord of class Paladin and guild Caminantes Nocturnos R left Valley of Trials"
|
||||||
|
- "\<name\> of class \<class\> and guild \<guild\> left \<zone\>"
|
||||||
|
- Player killed:
|
||||||
|
- "Euotuie killed Wild Mature Swine with Demon's Bite in Durotar (The Dranosh'ar Blockade)"
|
||||||
|
- "\<killer\> killed \<victim\> with \<spell\> in \<zone\> (\<subzone\>)"
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Heimdall is a multi-module addon that offers various functionalities to enhance player interaction and awareness in the game. It consists of several key modules, each with a specific purpose:
|
||||||
|
|
||||||
|
### 1. Spotter Module (`Spotter.lua`)
|
||||||
|
- Tracks and reports player sightings in real-time
|
||||||
|
- Configurable notification settings:
|
||||||
|
- Detect players by faction (Alliance, Horde)
|
||||||
|
- Identify hostile players
|
||||||
|
- Mark "stinky" players (predefined list)
|
||||||
|
- Sends notifications to a specified channel when players are spotted
|
||||||
|
- Provides detailed information about spotted players:
|
||||||
|
- Name
|
||||||
|
- Race
|
||||||
|
- Faction
|
||||||
|
- Health
|
||||||
|
- Location
|
||||||
|
- **Example report:**
|
||||||
|
- "I see (Hostile) Atomickitty of race Blood Elf (Horde) with health 6.8M/6.8M at Orgrimmar (Valley of Strength)"
|
||||||
|
- "I see (\<reaction\>) \<name\> of race \<race\> (\<faction\>) with health \<health\>/\<healthMax\> at \<location\>"
|
||||||
|
- Configuration:
|
||||||
|
- `enabled` - Whether the module is enabled
|
||||||
|
- `everyone` - Whether to report to everyone in the channel
|
||||||
|
- `hostile` - Whether to report hostile players (regardless of faction, ie. when a horde becomes alliance)
|
||||||
|
- `alliance` - Whether to report alliance players
|
||||||
|
- `stinky` - Whether to report only stinky players
|
||||||
|
- `notifyChannel` - The channel to report to (by name)
|
||||||
|
- `zoneOverride` - The zone to override the zone of the player to report (defaults to current zone/subzone)
|
||||||
|
- `throttleTime` - The time to throttle the reports to (in seconds)
|
||||||
|
- Configuration example:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.spotter = {enabled=true,everyone=false,hostile=true,alliance=true,stinky=true,notifyChannel="Agent",zoneOverride=nil,throttleTime=10}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Whoer Module (`Whoer.lua`)
|
||||||
|
- Advanced player tracking and logging system
|
||||||
|
- Periodically performs WHO queries in specific zones
|
||||||
|
- Maintains a persistent database of player information:
|
||||||
|
- First seen
|
||||||
|
- Last seen
|
||||||
|
- Seen count
|
||||||
|
- Zone history
|
||||||
|
- Sends notifications when:
|
||||||
|
- New players are detected
|
||||||
|
- Players change zones
|
||||||
|
- Players disappear from tracking
|
||||||
|
- Supports whisper notifications to predefined contacts
|
||||||
|
- Plays sound alerts for "stinky" players
|
||||||
|
- **Example report:**
|
||||||
|
- Player appeared:
|
||||||
|
- "Любящаядева of class Mage, race Human (Alliance) and guild Анонимное сообщество in Valley of Trials, first seen: 2024-12-27T15:54:38, last seen: never, times seen: 0"
|
||||||
|
- "\<name\> of class \<class\>, race \<race\> (\<faction\>) and guild \<guild\> in \<zone\>, first seen: \<firstSeen\>, last seen: \<lastSeen\>, times seen: \<timesSeen\>"
|
||||||
|
- Player changed zone:
|
||||||
|
- "Thekinglord of class Paladin (Human - Alliance) and guild Caminantes Nocturnos R moved to Durotar"
|
||||||
|
- "\<name\> of class \<class\> (\<faction\>) and guild \<guild\> moved to \<zone\>"
|
||||||
|
- Player disappeared:
|
||||||
|
- "Thekinglord of class Paladin and guild Caminantes Nocturnos R left Valley of Trials"
|
||||||
|
- "\<name\> of class \<class\> and guild \<guild\> left \<zone\>"
|
||||||
|
- Configuration:
|
||||||
|
- `enabled` - Whether the module is enabled
|
||||||
|
- `notifyChannel` - The channel to report to (by name)
|
||||||
|
- `ttl` - The time to live for a player (in seconds)
|
||||||
|
- `doWhisper` - Whether to whisper to predefined contacts
|
||||||
|
- `zoneNotifyFor` - Whether to notify for players in specific zones
|
||||||
|
- Configuration example:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.who = {enabled=true,notifyChannel="Agent",ttl=20,doWhisper=true,zoneNotifyFor={["Orgrimmar"]=true,["Thunder Bluff"]=true,["Undercity"]=true,["Durotar"]=true,["Echo Isles"]=true,["Valley of Trials"]=true}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Messenger Module (`Messenger.lua`)
|
||||||
|
- Centralized message queuing and sending system
|
||||||
|
- Manages message delivery across different chat channels
|
||||||
|
- Handles channel joining and message routing
|
||||||
|
- Provides a reliable messaging infrastructure for other modules
|
||||||
|
- Configuration:
|
||||||
|
- `enabled` - Whether the module is enabled
|
||||||
|
- Configuration example:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.messenger = {enabled=true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Inviter Module (`Inviter.lua`)
|
||||||
|
- Automated group invitation system
|
||||||
|
- Listens to a specific channel for invitation requests
|
||||||
|
- Supports a configurable keyword for invitations
|
||||||
|
- Automatically promotes channel members to assistants in raid groups
|
||||||
|
- Configuration:
|
||||||
|
- `enabled` - Whether the module is enabled
|
||||||
|
- `keyword` - The keyword to listen for
|
||||||
|
- `updateInterval` - The interval to update the list of channel members (in seconds)
|
||||||
|
- `listeningChannel` - The channel to listen for invitations
|
||||||
|
- Configuration example:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.inviter = {enabled=true,keyword="+",updateInterval=10,listeningChannel="Agent"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Death Reporter Module (`DeathReporter.lua`)
|
||||||
|
- Tracks and reports player deaths in combat
|
||||||
|
- Captures detailed death information:
|
||||||
|
- Killer
|
||||||
|
- Victim
|
||||||
|
- Killing spell
|
||||||
|
- Location
|
||||||
|
- Implements throttling to prevent spam
|
||||||
|
- Handles duel detection to avoid reporting duel-related deaths
|
||||||
|
- Sends notifications to a specified channel and optional whisper contacts
|
||||||
|
- **Example report:**
|
||||||
|
- "Euotuie killed Wild Mature Swine with Demon's Bite in Durotar (The Dranosh'ar Blockade)"
|
||||||
|
- "\<killer\> killed \<victim\> with \<spell\> in \<zone\> (\<subzone\>)"
|
||||||
|
- Configuration:
|
||||||
|
- `enabled` - Whether the module is enabled
|
||||||
|
- `notifyChannel` - The channel to report to (by name)
|
||||||
|
- `doWhisper` - Whether to whisper to predefined contacts
|
||||||
|
- Configuration example:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.deathReporter = {enabled=true,notifyChannel="Agent",doWhisper=true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Core Module (`Heimdall.lua`)
|
||||||
|
- Initializes and configures all other modules
|
||||||
|
- Manages global configuration and data persistence
|
||||||
|
- Provides utility functions for:
|
||||||
|
- UTF-8 string handling
|
||||||
|
- String padding
|
||||||
|
- Data retrieval with defaults
|
||||||
|
|
||||||
|
## Stinky Players
|
||||||
|
|
||||||
|
The addon maintains a list of "stinky" players - users of interest that trigger special notifications and tracking.
|
||||||
|
|
||||||
|
## Slash Commands
|
||||||
|
|
||||||
|
- `/has [PlayerName]`: Toggle a player's "stinky" status
|
||||||
|
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
1. Download the [addon](https://git.site.quack-lab.dev/dave/wow-Heimdall/media/branch/master/Heimdall.zip)
|
||||||
|
2. Extract the addon to your World of Warcraft `Interface/AddOns` directory
|
||||||
|
3. Ensure the addon is enabled in the character selection screen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Аддон Heimdall для WoW
|
||||||
|
|
||||||
|
Heimdall - это комплексный аддон для World of Warcraft, предназначенный для расширенного отслеживания игроков, уведомлений и управления группами.
|
||||||
|
|
||||||
|
## Обзор отчетов
|
||||||
|
|
||||||
|
- Обнаружен игрок:
|
||||||
|
- "I see (Hostile) Atomickitty of race Blood Elf (Horde) with health 6.8M/6.8M at Orgrimmar (Valley of Strength)"
|
||||||
|
- "Я вижу (\<reaction\>) \<name\> расы \<race\> (\<faction\>) со здоровьем \<health\>/\<healthMax\> в \<location\>"
|
||||||
|
- Появился игрок:
|
||||||
|
- "Любящаядева of class Mage, race Human (Alliance) and guild Анонимное сообщество in Valley of Trials, first seen: 2024-12-27T15:54:38, last seen: never, times seen: 0"
|
||||||
|
- "\<name\> класса \<class\>, расы \<race\> (\<faction\>) и гильдии \<guild\> в \<zone\>, первый раз замечен: \<firstSeen\>, последний раз замечен: \<lastSeen\>, встречен раз: \<timesSeen\>"
|
||||||
|
- Игрок сменил зону:
|
||||||
|
- "Thekinglord of class Paladin (Human - Alliance) and guild Caminantes Nocturnos R moved to Durotar"
|
||||||
|
- "\<name\> класса \<class\> (\<faction\>) и гильдии \<guild\> перешёл в \<zone\>"
|
||||||
|
- Игрок исчез:
|
||||||
|
- "Thekinglord of class Paladin and guild Caminantes Nocturnos R left Valley of Trials"
|
||||||
|
- "\<name\> класса \<class\> и гильдии \<guild\> покинул \<zone\>"
|
||||||
|
- Игрок убит:
|
||||||
|
- "Euotuie killed Wild Mature Swine with Demon's Bite in Durotar (The Dranosh'ar Blockade)"
|
||||||
|
- "\<killer\> убил \<victim\> с помощью \<spell\> в \<zone\> (\<subzone\>)"
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Heimdall - это многомодульный аддон, предоставляющий различные функции для улучшения взаимодействия между игроками и повышения осведомленности в игре. Он состоит из нескольких ключевых модулей:
|
||||||
|
|
||||||
|
### 1. Модуль Обнаружения (`Spotter.lua`)
|
||||||
|
- Отслеживает и сообщает об обнаружении игроков в реальном времени
|
||||||
|
- Настраиваемые параметры уведомлений:
|
||||||
|
- Обнаружение игроков по фракции (Альянс, Орда)
|
||||||
|
- Определение враждебных игроков
|
||||||
|
- Отметка "подозрительных" игроков (предопределенный список)
|
||||||
|
- Отправляет уведомления в указанный канал при обнаружении игроков
|
||||||
|
- Предоставляет подробную информацию об обнаруженных игроках:
|
||||||
|
- Имя
|
||||||
|
- Раса
|
||||||
|
- Фракция
|
||||||
|
- Здоровье
|
||||||
|
- Местоположение
|
||||||
|
- **Пример отчета:**
|
||||||
|
- "I see (Hostile) Atomickitty of race Blood Elf (Horde) with health 6.8M/6.8M at Orgrimmar (Valley of Strength)"
|
||||||
|
- "Обнаружен (\<реакция\>) \<имя\> расы \<раса\> (\<фракция\>) со здоровьем \<здоровье\>/\<макс_здоровье\> в \<локация\>"
|
||||||
|
- Конфигурация:
|
||||||
|
- `enabled` - Включен ли модуль
|
||||||
|
- `everyone` - Сообщать ли всем в канале
|
||||||
|
- `hostile` - Сообщать ли о враждебных игроках
|
||||||
|
- `alliance` - Сообщать ли об игроках Альянса
|
||||||
|
- `stinky` - Сообщать ли только о подозрительных игроках
|
||||||
|
- `notifyChannel` - Канал для отправки сообщений (по имени)
|
||||||
|
- `zoneOverride` - Зона для переопределения местоположения игрока
|
||||||
|
- `throttleTime` - Время задержки между сообщениями (в секундах)
|
||||||
|
- Пример конфигурации:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.spotter = {enabled=true,everyone=false,hostile=true,alliance=true,stinky=true,notifyChannel="Agent",zoneOverride=nil,throttleTime=10}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Модуль WHO (`Whoer.lua`)
|
||||||
|
- Продвинутая система отслеживания и логирования игроков
|
||||||
|
- Периодически выполняет WHO запросы в определенных зонах
|
||||||
|
- Поддерживает постоянную базу данных информации об игроках:
|
||||||
|
- Первое появление
|
||||||
|
- Последнее появление
|
||||||
|
- Количество появлений
|
||||||
|
- История зон
|
||||||
|
- Отправляет уведомления когда:
|
||||||
|
- Обнаружены новые игроки
|
||||||
|
- Игроки меняют зоны
|
||||||
|
- Игроки исчезают из отслеживания
|
||||||
|
- Поддерживает уведомления шепотом предопределенным контактам
|
||||||
|
- Проигрывает звуковые оповещения для "подозрительных" игроков
|
||||||
|
- **Пример отчета:**
|
||||||
|
- Появление игрока:
|
||||||
|
- "Любящаядева of class Mage, race Human (Alliance) and guild Анонимное сообщество in Valley of Trials, first seen: 2024-12-27T15:54:38, last seen: never, times seen: 0"
|
||||||
|
- "Имя класса <класс>, расы <раса> (<фракция>) из гильдии <гильдия> в <зона>, первое появление: <первое_появление>, последнее появление: <последнее_появление>, появлений: <количество>"
|
||||||
|
- Смена зоны:
|
||||||
|
- "Thekinglord of class Paladin (Human - Alliance) and guild Caminantes Nocturnos R moved to Durotar"
|
||||||
|
- "<имя> класса <класс> (<фракция>) из гильдии <гильдия> переместился в <зона>"
|
||||||
|
- Исчезновение:
|
||||||
|
- "Thekinglord of class Paladin and guild Caminantes Nocturnos R left Valley of Trials"
|
||||||
|
- "<имя> класса <класс> из гильдии <гильдия> покинул <зона>"
|
||||||
|
- Конфигурация:
|
||||||
|
- `enabled` - Включен ли модуль
|
||||||
|
- `notifyChannel` - Канал для отправки сообщений
|
||||||
|
- `ttl` - Время жизни записи об игроке (в секундах)
|
||||||
|
- `doWhisper` - Отправлять ли шепот контактам
|
||||||
|
- `zoneNotifyFor` - Уведомлять ли об игроках в определенных зонах
|
||||||
|
- Пример конфигурации:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.who = {enabled=true,notifyChannel="Agent",ttl=20,doWhisper=true,zoneNotifyFor={["Orgrimmar"]=true,["Thunder Bluff"]=true,["Undercity"]=true,["Durotar"]=true,["Echo Isles"]=true,["Valley of Trials"]=true}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Модуль Сообщений (`Messenger.lua`)
|
||||||
|
- Централизованная система очередей и отправки сообщений
|
||||||
|
- Управляет доставкой сообщений по разным чат-каналам
|
||||||
|
- Обрабатывает присоединение к каналам и маршрутизацию сообщений
|
||||||
|
- Предоставляет надежную инфраструктуру сообщений для других модулей
|
||||||
|
- Конфигурация:
|
||||||
|
- `enabled` - Включен ли модуль
|
||||||
|
- Пример конфигурации:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.messenger = {enabled=true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Модуль Приглашений (`Inviter.lua`)
|
||||||
|
- Автоматическая система приглашений в группу
|
||||||
|
- Прослушивает определенный канал на запросы приглашений
|
||||||
|
- Поддерживает настраиваемое ключевое слово для приглашений
|
||||||
|
- Автоматически повышает участников канала до помощников в рейдовых группах
|
||||||
|
- Конфигурация:
|
||||||
|
- `enabled` - Включен ли модуль
|
||||||
|
- `keyword` - Ключевое слово для прослушивания
|
||||||
|
- `updateInterval` - Интервал обновления списка участников канала (в секундах)
|
||||||
|
- `listeningChannel` - Канал для прослушивания приглашений
|
||||||
|
- Пример конфигурации:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.inviter = {enabled=true,keyword="+",updateInterval=10,listeningChannel="Agent"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Модуль Отчетов о Смертях (`DeathReporter.lua`)
|
||||||
|
- Отслеживает и сообщает о смертях игроков в бою
|
||||||
|
- Сохраняет подробную информацию о смерти:
|
||||||
|
- Убийца
|
||||||
|
- Жертва
|
||||||
|
- Убивающее заклинание
|
||||||
|
- Местоположение
|
||||||
|
- Реализует задержку для предотвращения спама
|
||||||
|
- Обрабатывает определение дуэлей во избежание сообщений о смертях в дуэлях
|
||||||
|
- Отправляет уведомления в указанный канал и опционально шепотом контактам
|
||||||
|
- **Пример отчета:**
|
||||||
|
- "Euotuie killed Wild Mature Swine with Demon's Bite in Durotar (The Dranosh'ar Blockade)"
|
||||||
|
- "<убийца> убил <жертва> с помощью <заклинание> в <зона> (<подзона>)"
|
||||||
|
- Конфигурация:
|
||||||
|
- `enabled` - Включен ли модуль
|
||||||
|
- `notifyChannel` - Канал для отправки сообщений
|
||||||
|
- `doWhisper` - Отправлять ли шепот контактам
|
||||||
|
- Пример конфигурации:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.deathReporter = {enabled=true,notifyChannel="Agent",doWhisper=true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Основной Модуль (`Heimdall.lua`)
|
||||||
|
- Инициализирует и настраивает все остальные модули
|
||||||
|
- Управляет глобальной конфигурацией и сохранением данных
|
||||||
|
- Предоставляет служебные функции для:
|
||||||
|
- Обработки UTF-8 строк
|
||||||
|
- Выравнивания строк
|
||||||
|
- Получения данных с значениями по умолчанию
|
||||||
|
|
||||||
|
## Подозрительные Игроки
|
||||||
|
|
||||||
|
Аддон поддерживает список "подозрительных" игроков - пользователей, представляющих интерес, которые вызывают специальные уведомления и отслеживание.
|
||||||
|
|
||||||
|
## Слэш-команды
|
||||||
|
|
||||||
|
- `/has [ИмяИгрока]`: Переключить статус "подозрительного" игрока
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
1. Скачайте [аддон](https://git.site.quack-lab.dev/dave/wow-Heimdall/media/branch/master/Heimdall.zip)
|
||||||
|
2. Распакуйте аддон в директорию World of Warcraft `Interface/AddOns`
|
||||||
|
3. Убедитесь, что аддон включен на экране выбора персонажа
|
1
Weakauras/Config/export
Normal file
1
Weakauras/Config/export
Normal file
File diff suppressed because one or more lines are too long
185
Weakauras/Config/init.lua
Normal file
185
Weakauras/Config/init.lua
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
---@param str string
|
||||||
|
---@return table<string, boolean>
|
||||||
|
local function StringToMap(str, deliminer)
|
||||||
|
if not str then return {} end
|
||||||
|
local map = {}
|
||||||
|
local parts = { strsplit(deliminer, str) }
|
||||||
|
for _, line in ipairs(parts) do
|
||||||
|
line = strtrim(line)
|
||||||
|
if line ~= "" then
|
||||||
|
map[line] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return map
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param str string
|
||||||
|
---@return string[]
|
||||||
|
local function StringToArray(str, deliminer)
|
||||||
|
if not str then return {} end
|
||||||
|
local ret = {}
|
||||||
|
local array = { strsplit(deliminer, str) }
|
||||||
|
for i, line in ipairs(array) do
|
||||||
|
line = strtrim(line)
|
||||||
|
if line ~= "" then
|
||||||
|
ret[i] = line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
if not Heimdall_Data then Heimdall_Data = {} end
|
||||||
|
|
||||||
|
local config = {
|
||||||
|
spotter = {
|
||||||
|
enabled = aura_env.config.spotter.enabled,
|
||||||
|
everyone = aura_env.config.spotter.everyone,
|
||||||
|
hostile = aura_env.config.spotter.hostile,
|
||||||
|
alliance = aura_env.config.spotter.alliance,
|
||||||
|
stinky = aura_env.config.spotter.stinky,
|
||||||
|
notifyChannel = aura_env.config.spotter.notifyChannel,
|
||||||
|
zoneOverride = aura_env.config.spotter.zoneOverride,
|
||||||
|
throttleTime = aura_env.config.spotter.throttleTime
|
||||||
|
},
|
||||||
|
who = {
|
||||||
|
enabled = aura_env.config.who.enabled,
|
||||||
|
ignored = StringToMap(aura_env.config.who.ignored, "\n"),
|
||||||
|
notifyChannel = aura_env.config.who.notifyChannel,
|
||||||
|
ttl = aura_env.config.who.ttl,
|
||||||
|
doWhisper = aura_env.config.who.doWhisper,
|
||||||
|
zoneNotifyFor = StringToMap(aura_env.config.who.zoneNotifyFor, "\n"),
|
||||||
|
},
|
||||||
|
messenger = {
|
||||||
|
enabled = aura_env.config.messenger.enabled,
|
||||||
|
interval = aura_env.config.messenger.interval,
|
||||||
|
},
|
||||||
|
deathReporter = {
|
||||||
|
enabled = aura_env.config.deathReporter.enabled,
|
||||||
|
throttle = aura_env.config.deathReporter.throttle,
|
||||||
|
doWhisper = aura_env.config.deathReporter.doWhisper,
|
||||||
|
notifyChannel = aura_env.config.deathReporter.notifyChannel,
|
||||||
|
zoneOverride = aura_env.config.deathReporter.zoneOverride,
|
||||||
|
duelThrottle = aura_env.config.deathReporter.duelThrottle,
|
||||||
|
},
|
||||||
|
whisperNotify = StringToArray(aura_env.config.whisperNotify, "\n"),
|
||||||
|
stinkies = StringToMap(aura_env.config.stinkies, "\n"),
|
||||||
|
inviter = {
|
||||||
|
enabled = aura_env.config.inviter.enabled,
|
||||||
|
listeningChannel = aura_env.config.inviter.listeningChannel,
|
||||||
|
keyword = aura_env.config.inviter.keyword,
|
||||||
|
allAssist = aura_env.config.inviter.allAssist,
|
||||||
|
agentsAssist = aura_env.config.inviter.agentsAssist,
|
||||||
|
throttle = aura_env.config.inviter.throttle,
|
||||||
|
kickOffline = aura_env.config.inviter.kickOffline,
|
||||||
|
cleanupInterval = aura_env.config.inviter.cleanupInterval,
|
||||||
|
afkThreshold = aura_env.config.inviter.afkThreshold,
|
||||||
|
},
|
||||||
|
dueler = {
|
||||||
|
enabled = aura_env.config.dueler.enabled,
|
||||||
|
declineOther = aura_env.config.dueler.declineOther,
|
||||||
|
},
|
||||||
|
bully = {
|
||||||
|
enabled = aura_env.config.bully.enabled,
|
||||||
|
},
|
||||||
|
agentTracker = {
|
||||||
|
enabled = aura_env.config.agentTracker.enabled,
|
||||||
|
masterChannel = aura_env.config.agentTracker.masterChannel,
|
||||||
|
},
|
||||||
|
emoter = {
|
||||||
|
enabled = aura_env.config.emoter.enabled,
|
||||||
|
masterChannel = aura_env.config.emoter.masterChannel,
|
||||||
|
prefix = aura_env.config.emoter.prefix,
|
||||||
|
},
|
||||||
|
echoer = {
|
||||||
|
enabled = aura_env.config.echoer.enabled,
|
||||||
|
masterChannel = aura_env.config.echoer.masterChannel,
|
||||||
|
prefix = aura_env.config.echoer.prefix,
|
||||||
|
},
|
||||||
|
macroer = {
|
||||||
|
enabled = aura_env.config.macroer.enabled,
|
||||||
|
priority = StringToArray(aura_env.config.macroer.priority, "\n"),
|
||||||
|
},
|
||||||
|
commander = {
|
||||||
|
enabled = aura_env.config.commander.enabled,
|
||||||
|
masterChannel = aura_env.config.commander.masterChannel,
|
||||||
|
commander = aura_env.config.commander.commander,
|
||||||
|
commands = StringToMap(aura_env.config.commander.commands, ","),
|
||||||
|
},
|
||||||
|
combatAlerter = {
|
||||||
|
enabled = aura_env.config.combatAlerter.enabled,
|
||||||
|
masterChannel = aura_env.config.combatAlerter.masterChannel,
|
||||||
|
},
|
||||||
|
stinkyTracker = {
|
||||||
|
enabled = aura_env.config.stinkyTracker.enabled,
|
||||||
|
masterChannel = aura_env.config.stinkyTracker.masterChannel,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Heimdall_Data.config.spotter.enabled = config.spotter.enabled
|
||||||
|
Heimdall_Data.config.spotter.everyone = config.spotter.everyone
|
||||||
|
Heimdall_Data.config.spotter.hostile = config.spotter.hostile
|
||||||
|
Heimdall_Data.config.spotter.alliance = config.spotter.alliance
|
||||||
|
Heimdall_Data.config.spotter.stinky = config.spotter.stinky
|
||||||
|
Heimdall_Data.config.spotter.notifyChannel = config.spotter.notifyChannel
|
||||||
|
Heimdall_Data.config.spotter.zoneOverride = config.spotter.zoneOverride
|
||||||
|
Heimdall_Data.config.spotter.throttleTime = config.spotter.throttleTime
|
||||||
|
|
||||||
|
Heimdall_Data.config.who.enabled = config.who.enabled
|
||||||
|
Heimdall_Data.config.who.ignored = config.who.ignored
|
||||||
|
Heimdall_Data.config.who.notifyChannel = config.who.notifyChannel
|
||||||
|
Heimdall_Data.config.who.ttl = config.who.ttl
|
||||||
|
Heimdall_Data.config.who.doWhisper = config.who.doWhisper
|
||||||
|
Heimdall_Data.config.who.zoneNotifyFor = config.who.zoneNotifyFor
|
||||||
|
|
||||||
|
Heimdall_Data.config.messenger.enabled = config.messenger.enabled
|
||||||
|
Heimdall_Data.config.messenger.interval = config.messenger.interval
|
||||||
|
|
||||||
|
Heimdall_Data.config.deathReporter.enabled = config.deathReporter.enabled
|
||||||
|
Heimdall_Data.config.deathReporter.throttle = config.deathReporter.throttle
|
||||||
|
Heimdall_Data.config.deathReporter.doWhisper = config.deathReporter.doWhisper
|
||||||
|
Heimdall_Data.config.deathReporter.notifyChannel = config.deathReporter.notifyChannel
|
||||||
|
Heimdall_Data.config.deathReporter.zoneOverride = config.deathReporter.zoneOverride
|
||||||
|
Heimdall_Data.config.deathReporter.duelThrottle = config.deathReporter.duelThrottle
|
||||||
|
|
||||||
|
Heimdall_Data.config.inviter.enabled = config.inviter.enabled
|
||||||
|
Heimdall_Data.config.inviter.listeningChannel = config.inviter.listeningChannel
|
||||||
|
Heimdall_Data.config.inviter.keyword = config.inviter.keyword
|
||||||
|
Heimdall_Data.config.inviter.allAssist = config.inviter.allAssist
|
||||||
|
Heimdall_Data.config.inviter.agentsAssist = config.inviter.agentsAssist
|
||||||
|
Heimdall_Data.config.inviter.throttle = config.inviter.throttle
|
||||||
|
Heimdall_Data.config.inviter.kickOffline = config.inviter.kickOffline
|
||||||
|
Heimdall_Data.config.inviter.cleanupInterval = config.inviter.cleanupInterval
|
||||||
|
Heimdall_Data.config.inviter.afkThreshold = config.inviter.afkThreshold
|
||||||
|
|
||||||
|
Heimdall_Data.config.dueler.enabled = config.dueler.enabled
|
||||||
|
Heimdall_Data.config.dueler.declineOther = config.dueler.declineOther
|
||||||
|
|
||||||
|
Heimdall_Data.config.bully.enabled = config.bully.enabled
|
||||||
|
|
||||||
|
Heimdall_Data.config.agentTracker.enabled = config.agentTracker.enabled
|
||||||
|
Heimdall_Data.config.agentTracker.masterChannel = config.agentTracker.masterChannel
|
||||||
|
|
||||||
|
Heimdall_Data.config.emoter.enabled = config.emoter.enabled
|
||||||
|
Heimdall_Data.config.emoter.masterChannel = config.emoter.masterChannel
|
||||||
|
Heimdall_Data.config.emoter.prefix = config.emoter.prefix
|
||||||
|
|
||||||
|
Heimdall_Data.config.echoer.enabled = config.echoer.enabled
|
||||||
|
Heimdall_Data.config.echoer.masterChannel = config.echoer.masterChannel
|
||||||
|
Heimdall_Data.config.echoer.prefix = config.echoer.prefix
|
||||||
|
|
||||||
|
Heimdall_Data.config.macroer.enabled = config.macroer.enabled
|
||||||
|
Heimdall_Data.config.macroer.priority = config.macroer.priority
|
||||||
|
|
||||||
|
Heimdall_Data.config.commander.enabled = config.commander.enabled
|
||||||
|
Heimdall_Data.config.commander.masterChannel = config.commander.masterChannel
|
||||||
|
Heimdall_Data.config.commander.commander = config.commander.commander
|
||||||
|
Heimdall_Data.config.commander.commands = config.commander.commands
|
||||||
|
|
||||||
|
Heimdall_Data.config.combatAlerter.enabled = config.combatAlerter.enabled
|
||||||
|
Heimdall_Data.config.combatAlerter.masterChannel = config.combatAlerter.masterChannel
|
||||||
|
|
||||||
|
Heimdall_Data.config.stinkyTracker.enabled = config.stinkyTracker.enabled
|
||||||
|
Heimdall_Data.config.stinkyTracker.masterChannel = config.stinkyTracker.masterChannel
|
||||||
|
|
||||||
|
Heimdall_Data.config.whisperNotify = config.whisperNotify
|
||||||
|
Heimdall_Data.config.stinkies = config.stinkies
|
Reference in New Issue
Block a user