141 Commits

Author SHA1 Message Date
a01dd5dace Release 2025-01-07 01:16:53 +01:00
b6f0fa9776 Add whispernotify and stinkies configs 2025-01-07 01:16:29 +01:00
cb03f8e23c UIPARENT EXISTS 2025-01-07 01:12:26 +01:00
fbff094227 Add every other config module 2025-01-07 01:12:26 +01:00
e08cf455ee Minor details 2025-01-07 00:46:43 +01:00
de57b4c8a8 Color the boxes so the modules are easily told apart 2025-01-07 00:44:32 +01:00
9fef1da261 Add MORE configuration 2025-01-07 00:34:27 +01:00
1f6a1a49a6 Tidy up row counts 2025-01-07 00:20:27 +01:00
fddd294a03 Unfuckup the button colors 2025-01-07 00:18:19 +01:00
00ffe6973e Add helper functions and assemble big boxes 2025-01-06 23:47:01 +01:00
f73096e439 Add BigTextFrame 2025-01-06 23:42:54 +01:00
e26ab02862 Make anyone be able to joingroup and leavegroup 2025-01-06 23:42:50 +01:00
c2b9c1267b Add a little text inset 2025-01-06 22:35:23 +01:00
a400a85dc9 Add whoer configuration 2025-01-06 22:33:28 +01:00
f5a4b49935 Add other text fields for spotter config 2025-01-06 22:30:07 +01:00
2dabe7959f Refactor spotter config into separate do block 2025-01-06 22:23:41 +01:00
dcfc406481 Refactor button colorization 2025-01-06 22:20:09 +01:00
f680d36d2b Add some basic config buttons for spotter 2025-01-06 22:17:38 +01:00
d44aa04e44 Make GridFrame automatically adjust to SetWidth and SetHeight 2025-01-06 21:26:52 +01:00
4594dff4a6 Make button more buttonlike 2025-01-06 20:14:00 +01:00
be1093d51f Close frame on escape 2025-01-06 20:11:40 +01:00
546aa27bb1 Add a basic ass enable spotter button 2025-01-06 20:10:02 +01:00
dfb2f687d0 Remove stinkied agents 2025-01-06 20:01:47 +01:00
9e344aed64 Make movable 2025-01-06 19:12:52 +01:00
ba77555cab Implement static frame too 2025-01-06 19:08:25 +01:00
e8e15444c9 Restrict frame size to follow grid (otherwise what's the point) 2025-01-06 19:06:37 +01:00
cdd859ba50 Stop tracking dovahkin 2025-01-06 18:00:44 +01:00
ab85b3712a Rework grid to have infinite rows 2025-01-06 17:35:59 +01:00
fc3732fd0c Begin reworking old approach to gridframe 2025-01-06 17:16:48 +01:00
4652c60e64 Implement a grid frame of sorts 2025-01-06 17:16:36 +01:00
e425fa9209 Implement offsetting to placement functions 2025-01-06 12:00:59 +01:00
f1e2f60836 Maybe generify UI a little 2025-01-06 12:00:59 +01:00
db7edaf802 Make little more better 2025-01-06 12:00:59 +01:00
58a7ecd723 Add more config options 2025-01-06 12:00:59 +01:00
bff1a4acf9 Make most basic config 2025-01-06 12:00:59 +01:00
b287fb41c4 Add config.lua 2025-01-06 12:00:58 +01:00
5e0f81ce53 Add more decimals to spotter coordinates 2025-01-06 11:56:38 +01:00
bd8b3fa00f Release 2025-01-06 02:03:37 +01:00
899fb888c3 Implement combatalerter 2025-01-06 02:03:23 +01:00
2d3820952f Refactor messenger to use GetChannelName 2025-01-06 02:02:37 +01:00
f6b043fa39 Prevent macroer from running in combat 2025-01-06 01:55:40 +01:00
8bce840497 Fix annoying deathReporter error 2025-01-06 01:48:10 +01:00
aff3e83bab Rework macroer to use the new stinkytracker 2025-01-06 01:42:46 +01:00
85a28bc7ca Rework the tracking functionality from macroer to stinkytracker 2025-01-06 01:40:29 +01:00
452aa2e3a0 Add reactive value 2025-01-06 01:39:07 +01:00
784cee6f04 Add config options for the 2 new modules added in previous commit 2025-01-06 01:39:07 +01:00
ca30998a5a Add basic structure for combatalerter and stinkytracker 2025-01-06 01:39:07 +01:00
476adcdc2e Actually disable inviter kicker 2025-01-06 01:09:34 +01:00
2c6142e6c4 Remove oopsie log 2025-01-06 01:04:28 +01:00
cae1eef659 Release 2025-01-06 01:03:03 +01:00
2dfb60e63d Implement some sort of semi automatic kicking from group
By overlaying a button over the existing button...
Not great but the best we can do
2025-01-06 01:02:03 +01:00
8524a1116a Implement follow command 2025-01-06 00:25:03 +01:00
60ccbc72bb Add basic structure and configuration for inviter kicker 2025-01-06 00:22:29 +01:00
8d3813f3ee Implement enabling/disabling commands 2025-01-06 00:00:16 +01:00
aa46000abf Implement joingroup and leavegroup for commander 2025-01-05 23:32:48 +01:00
6059c16a6e Implement commander only mode for commands 2025-01-05 23:27:04 +01:00
2e805abef7 Refactor commands to separate structure 2025-01-05 23:23:17 +01:00
06f143915c Add commander config to the weakaura 2025-01-05 23:23:17 +01:00
be20aa77b9 Migrate the commands from whoer to commander 2025-01-05 23:01:41 +01:00
7b2c67d130 Remove replying to whispers
Nobody is using it anyway, taking up space in the code...
2025-01-05 22:52:24 +01:00
9480c42181 Add player coordinates to spotter 2025-01-05 22:51:45 +01:00
17c163c71c Add help messages in english 2025-01-05 22:35:30 +01:00
3ee90fb767 Do not report spotter for agents 2025-01-05 22:30:09 +01:00
7f9476236d Add prefixes to commands even when no stinkies are found 2025-01-05 22:12:16 +01:00
2d2cf621bd Fix up macroer to work with the arrival and movement messages 2025-01-05 19:49:06 +01:00
bb1acd5003 Refactor macroer to include "I see" 2025-01-05 19:30:37 +01:00
c00ddd410a Remove the realm "-legionx5" from player names on dueler 2025-01-05 17:35:20 +01:00
35c51143bc Make agent tracker sniff channel messages for agents 2025-01-05 17:17:58 +01:00
16cd2f82be Fuck about with whoer 2025-01-05 00:28:55 +01:00
34e027525f Fix empty location 2025-01-03 23:42:53 +01:00
331823e34f Clean up the named who 2025-01-03 14:30:45 +01:00
18daa170c5 Add prefixes to commands/messages 2025-01-03 13:30:38 +01:00
7e8978044f Have partitioner split on "," instead of space 2025-01-03 13:22:56 +01:00
2811396234 Reverse macroer sorting, most important should be LAST not FIRST! 2025-01-02 23:07:33 +01:00
212ce2c71c Fix who messages to be partitioned 2025-01-02 23:07:01 +01:00
f76ef718ed Refactor GetChannelId out of FindOrJoinChannel
No reason for it to be nested
2025-01-02 14:11:12 +01:00
bd40d43686 Fix the weird function declaration in messenger 2025-01-02 14:10:17 +01:00
2954a93b4d Fix spotter oopsie 2025-01-02 12:14:18 +01:00
93d1d55cfa Add targetenemy as first line 2025-01-02 11:32:20 +01:00
1c7951a530 Release 2025-01-02 11:31:36 +01:00
84a53ea065 Remove debug everything 2025-01-02 11:31:19 +01:00
9dcf526b4c Update config weakaura 2025-01-02 11:23:26 +01:00
032cfc5bff Release 2025-01-02 11:19:37 +01:00
5c9450d06d Implement priority sorting of classes for macroer 2025-01-02 11:19:26 +01:00
f811dd9a6c Clean up some warnings 2025-01-02 11:06:45 +01:00
d13da3141d Implement macroer logic 2025-01-02 11:03:00 +01:00
d4bab870a4 Clean up the log messages a little 2025-01-02 11:02:56 +01:00
6ef7e74402 Implement the basic structure for macroer 2025-01-02 10:23:27 +01:00
55ce001705 Spice up the who messages a little 2025-01-02 10:23:11 +01:00
77c3fc291d Add help message to whoer 2025-01-02 00:09:25 +01:00
ff849092ff Remove inviter debug prints 2025-01-01 21:45:26 +01:00
d0cb074912 Add macroer (or the skeleton of) 2025-01-01 21:43:27 +01:00
fdedde829e Fix typo in inviter AAAAAAAAAAAAAAAA 2025-01-01 21:43:09 +01:00
4364d87bf7 Fix inviter infinite loop 2025-01-01 21:08:57 +01:00
2232f1a92d Release 2025-01-01 20:57:23 +01:00
251e11a7e8 Fix deathreporter not assigning zones (because it was "") 2025-01-01 20:57:11 +01:00
d4e4290dc5 Only promote units that aren't already
There's a big issue with this but I don't know how to fix it yet...
And that is that giving people assistant triggers a group update
Which triggers give people assistant
And so on...
2025-01-01 20:56:06 +01:00
9fd57c5d7b Fix whoer 2025-01-01 20:55:31 +01:00
873ce0a30c Minor fixes 2025-01-01 17:16:22 +01:00
36297ca09d Add emoter and ehcoer 2025-01-01 16:24:11 +01:00
0ec3421a79 Implement class listing in whoer 2025-01-01 16:14:10 +01:00
21d99ae643 Update config export 2025-01-01 16:02:35 +01:00
4dc3335a86 Add throttle to inviter so it doesn't have a stroke 2025-01-01 16:01:57 +01:00
c9627779ba Refactor agentTracker to its own module 2025-01-01 16:01:40 +01:00
3f1fae8906 Remove query pending mechanism from whoer 2025-01-01 15:42:58 +01:00
2ae12fade0 Fix target spotting for spotter 2025-01-01 15:40:42 +01:00
c66c961297 Rework the channel commands a little 2025-01-01 15:36:04 +01:00
8da5773dcd Implement "howmany" to whoer 2025-01-01 15:28:43 +01:00
18106db367 Fix issue with who ignored 2025-01-01 15:22:06 +01:00
5eb6f3cbfd Reset whoer regardless of enabled (it gets stuck otherwise) 2025-01-01 15:18:35 +01:00
614b07c01a Release 2025-01-01 15:05:11 +01:00
2c84c326dd Implement dueler 2025-01-01 15:04:20 +01:00
0ceb59c778 Remove vestigial code 2025-01-01 15:00:08 +01:00
137ce0a3a7 Refactor everything to modules 2025-01-01 14:58:05 +01:00
59d2b999c2 Update config 2025-01-01 14:56:56 +01:00
d80ffbaff5 Rework whoer 2025-01-01 14:47:40 +01:00
8c45e90ce1 Remove the fucking linter warnings 2025-01-01 14:38:02 +01:00
ed7c1a4685 Rework deathreporter 2025-01-01 14:34:12 +01:00
e32966bee2 Rework spotter 2025-01-01 14:24:58 +01:00
5e779cc5f9 Add interval config to messenger 2025-01-01 14:16:31 +01:00
5e78f623f5 Rework messenger config 2025-01-01 14:13:29 +01:00
8e90a71dfc Rework inviter config 2025-01-01 14:10:33 +01:00
9ebc95885e Rework main heimdall file to simplify config 2025-01-01 14:04:48 +01:00
e1136703a5 Fuck up some other shit just for fun 2025-01-01 14:00:07 +01:00
c446d1dc85 Add config weakaura 2025-01-01 13:59:58 +01:00
de49956aef Add report overview 2024-12-27 16:33:10 +01:00
efde43fadb Escape the lt and gt 2024-12-27 16:22:24 +01:00
af2a714b76 Bolden the reports, they are the highlights here 2024-12-27 16:21:41 +01:00
bfaa73b660 Release 2024-12-27 16:18:11 +01:00
a7c818b88b Machine translate russian 2024-12-27 16:18:02 +01:00
e1fb450544 Add readme 2024-12-27 16:16:17 +01:00
acb2910b70 Fix inviter channel scanning
It really was not so easy...
2024-12-27 14:15:34 +01:00
0dd1a6fd69 Add zip 2024-12-26 21:32:10 +01:00
5f3374a073 Rework inviter to grant assist only to members of listenChannel 2024-12-26 21:29:27 +01:00
3049a0b554 Update whoer lists 2024-12-26 21:29:16 +01:00
fa7a411e34 Remove notifications
I'm done with this shit
2024-12-17 14:34:50 +01:00
6bf1c491a0 Implement inviter 2024-12-16 01:04:28 +01:00
8e1f2c147e Fix stinky config 2024-12-15 21:01:49 +01:00
c4f4d24064 Longer who ttl 2024-12-15 21:00:33 +01:00
30578bdbb0 Add race and faction to "moved to" 2024-12-15 21:00:17 +01:00
0e771a7db3 Add stinki to whoer 2024-12-13 12:07:07 +01:00
28 changed files with 5456 additions and 2216 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.zip filter=lfs diff=lfs merge=lfs -text

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"Lua.diagnostics.globals": [
"UIParent"
]
}

View File

@@ -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"

View File

@@ -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 LFS Normal file

Binary file not shown.

View File

@@ -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
View 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
View 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

File diff suppressed because it is too large Load Diff

49
Modules/CombatAlerter.lua Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,115 +1,116 @@
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") ---@type table<string, number>
return local recentDeaths = {}
end ---@type table<string, number>
local recentDuels = {}
---@type table<string, number>
local recentDeaths = {} ---@param source string
---@type table<string, number> ---@param destination string
local recentDuels = {} ---@param spellName string
local function RegisterDeath(source, destination, spellName)
---@param source string if not Heimdall_Data.config.deathReporter.enabled then return end
---@param destination string if recentDeaths[destination]
---@param spellName string and GetTime() - recentDeaths[destination] < Heimdall_Data.config.deathReporter.throttle then
---@param overkill number return
local function RegisterDeath(source, destination, spellName, overkill) end
if recentDeaths[destination]
and GetTime() - recentDeaths[destination] < data.config.deathReporter.throttle then if recentDuels[destination]
return and GetTime() - recentDuels[destination] < Heimdall_Data.config.deathReporter.duelThrottle then
end print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
return
if recentDuels[destination] end
and GetTime() - recentDuels[destination] < data.config.deathReporter.duelThrottle then if recentDuels[source]
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination)) and GetTime() - recentDuels[source] < Heimdall_Data.config.deathReporter.duelThrottle then
return print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
end return
if recentDuels[source] end
and GetTime() - recentDuels[source] < data.config.deathReporter.duelThrottle then
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination)) recentDeaths[destination] = GetTime()
return C_Timer.NewTimer(3, function()
end if recentDuels[destination]
and GetTime() - recentDuels[destination] < Heimdall_Data.config.deathReporter.duelThrottle then
recentDeaths[destination] = GetTime() print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
C_Timer.NewTimer(3, function() return
if recentDuels[destination] end
and GetTime() - recentDuels[destination] < data.config.deathReporter.duelThrottle then if recentDuels[source]
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination)) and GetTime() - recentDuels[source] < Heimdall_Data.config.deathReporter.duelThrottle then
return print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination))
end return
if recentDuels[source] end
and GetTime() - recentDuels[source] < data.config.deathReporter.duelThrottle then
print(string.format("Cancelling death reports for %s and %s because of recent duel", source, destination)) local zone = Heimdall_Data.config.deathReporter.zoneOverride
return if zone == nil or zone == "" then
end zone = string.format("%s (%s)", GetZoneText(), GetSubZoneText())
end
local zone = data.config.deathReporter.zoneOverride
if not zone then local text = string.format("%s killed %s with %s in %s",
zone = string.format("%s (%s)", GetZoneText(), GetSubZoneText()) tostring(source),
end tostring(destination),
tostring(spellName),
local text = string.format("%s killed %s with %s in %s", tostring(zone))
tostring(source),
tostring(destination), ---@type Message
tostring(spellName), local msg = {
tostring(zone)) channel = "CHANNEL",
data = Heimdall_Data.config.deathReporter.notifyChannel,
---@type Message message = text,
local msg = { }
channel = "CHANNEL", table.insert(shared.messenger.queue, msg)
data = data.config.deathReporter.notifyChannel,
message = text, if Heimdall_Data.config.deathReporter.doWhisper then
} for _, name in pairs(Heimdall_Data.config.whisperNotify) do
table.insert(data.messenger.queue, msg) local msg = {
channel = "WHISPER",
if data.config.deathReporter.doWhisper then data = name,
for _, name in pairs(data.config.whisperNotify) do message = text,
local msg = { }
channel = "WHISPER", table.insert(shared.messenger.queue, msg)
data = name, end
message = text, end
} end)
table.insert(data.messenger.queue, msg) end
end
end local cleuFrame = CreateFrame("Frame")
end) cleuFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end cleuFrame:SetScript("OnEvent", function(self, event, ...)
if not Heimdall_Data.config.deathReporter.enabled then return end
local cleuFrame = CreateFrame("Frame") local overkill, err = CLEUParser.GetOverkill(...)
cleuFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") if not err and overkill > 0 then
cleuFrame:SetScript("OnEvent", function(self, event, ...) local source, err = CLEUParser.GetSourceName(...)
local overkill, err = CLEUParser.GetOverkill(...) if err then source = "unknown" end
if not err and overkill > 0 then local destination, err = CLEUParser.GetDestName(...)
local source, err = CLEUParser.GetSourceName(...) if err then destination = "unknown" end
if err then source = "unknown" end local spellName, err = CLEUParser.GetSpellName(...)
local destination, err = CLEUParser.GetDestName(...) if err then spellName = "unknown" end
if err then destination = "unknown" end local sourceGUID, err = CLEUParser.GetSourceGUID(...)
local spellName, err = CLEUParser.GetSpellName(...) if err or not string.match(sourceGUID, "Player") then return end
if err then spellName = "unknown" end local destinationGUID, err = CLEUParser.GetDestGUID(...)
local sourceGUID, err = CLEUParser.GetSourceGUID(...) if err or not string.match(destinationGUID, "Player") then return end
if err or not string.match(sourceGUID, "Player") then return end RegisterDeath(source, destination, spellName)
local destinationGUID, err = CLEUParser.GetDestGUID(...) end
if err or not string.match(destinationGUID, "Player") then return end end)
RegisterDeath(source, destination, spellName, overkill)
end local systemMessageFrame = CreateFrame("Frame")
end) systemMessageFrame:RegisterEvent("CHAT_MSG_SYSTEM")
systemMessageFrame:SetScript("OnEvent", function(self, event, msg)
local systemMessageFrame = CreateFrame("Frame") if not Heimdall_Data.config.deathReporter.enabled then return end
systemMessageFrame:RegisterEvent("CHAT_MSG_SYSTEM") local source, destination = string.match(msg, "([^ ]+) has defeated ([^ ]+) in a duel")
systemMessageFrame:SetScript("OnEvent", function(self, event, msg) if not source or not destination then return end
local source, destination = string.match(msg, "(.+) has defeated (.+) in a duel") source = string.match(source, "([^-]+)")
if source and destination then destination = string.match(destination, "([^-]+)")
print(string.format("Detected duel between %s and %s", source, destination)) if source and destination then
local now = GetTime() print(string.format("Detected duel between %s and %s", source, destination))
recentDuels[source] = now local now = GetTime()
recentDuels[destination] = now recentDuels[source] = now
end recentDuels[destination] = now
end) end
end)
print("Heimdall - DeathReporter loaded")
end print("Heimdall - DeathReporter loaded")
end

25
Modules/Dueler.lua Normal file
View 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

View File

@@ -1,29 +1,29 @@
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
end end
if depth == nil then if depth == nil then
depth = 0 depth = 0
end end
if (depth > 200) then if (depth > 200) then
print("Error: Depth > 200 in dumpTable()") print("Error: Depth > 200 in dumpTable()")
return return
end end
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
end end
end end
end end

36
Modules/Echoer.lua Normal file
View 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
View 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
View 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
View 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
View 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
View 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()

View File

@@ -1,117 +1,124 @@
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") local function FormatHP(hp)
return if hp > 1e9 then
end return string.format("%.1fB", hp / 1e9)
elseif hp > 1e6 then
local function FormatHP(hp) return string.format("%.1fM", hp / 1e6)
if hp > 1e9 then elseif hp > 1e3 then
return string.format("%.1fB", hp / 1e9) return string.format("%.1fK", hp / 1e3)
elseif hp > 1e6 then else
return string.format("%.1fM", hp / 1e6) return hp
elseif hp > 1e3 then end
return string.format("%.1fK", hp / 1e3) end
else
return hp ---@type table<string, number>
end local throttleTable = {}
end
---@param unit string
---@type table<string, number> ---@param name string
local throttleTable = {} ---@param faction string
---@param hostile boolean
---@param unit string ---@return boolean
---@param name string ---@return string? error
---@param faction string local function ShouldNotify(unit, name, faction, hostile)
---@param hostile boolean if Heimdall_Data.config.agents[name] then return false end
---@return boolean if Heimdall_Data.config.spotter.stinky then
---@return string? error if Heimdall_Data.config.stinkies[name] then return true end
local function ShouldNotify(unit, name, faction, hostile) end
if data.config.spotter.stinky then if Heimdall_Data.config.spotter.alliance then
if data.config.stinkies[name] then return true end if faction == "Alliance" then return true end
end end
if data.config.spotter.alliance then if Heimdall_Data.config.spotter.hostile then
if faction == "Alliance" then return true end if hostile then return true end
end end
if data.config.spotter.hostile then return Heimdall_Data.config.spotter.everyone
if hostile then return true end end
end
return data.config.spotter.everyone ---@param unit string
end ---@return string?
local function NotifySpotted(unit)
---@param unit string if not unit then return string.format("Could not find unit %s", tostring(unit)) end
---@return string? if not UnitIsPlayer(unit) then return nil end
local function NotifySpotted(unit)
if not unit then return string.format("Could not find unit %s", tostring(unit)) end local name = UnitName(unit)
if not UnitIsPlayer(unit) then return nil end if not name then return string.format("Could not find name for unit %s", tostring(unit)) end
local name = UnitName(unit) local time = GetTime()
if not name then return string.format("Could not find name for unit %s", tostring(unit)) end if throttleTable[name] and time - throttleTable[name] < Heimdall_Data.config.spotter.throttleTime then
return string.format("Throttled %s", tostring(name))
local time = GetTime() end
if throttleTable[name] and time - throttleTable[name] < data.config.spotter.throttleTime then throttleTable[name] = time
return string.format("Throttled %s", tostring(name))
end local race = UnitRace(unit)
throttleTable[name] = time if not race then return string.format("Could not find race for unit %s", tostring(unit)) end
local faction = shared.raceMap[race]
local race = UnitRace(unit) if not faction then return string.format("Could not find faction for race %s", tostring(race)) end
if not race then return string.format("Could not find race for unit %s", tostring(unit)) end
local faction = data.raceMap[race] local hostile = UnitCanAttack("player", unit)
if not faction then return string.format("Could not find faction for race %s", tostring(race)) end local doNotify = ShouldNotify(unit, name, faction, hostile)
if not doNotify then return string.format("Not notifying for %s", tostring(name)) end
local hostile = UnitCanAttack("player", unit)
local doNotify = ShouldNotify(unit, name, faction, hostile) local hp = UnitHealth(unit)
if not doNotify then return string.format("Not notifying for %s", tostring(name)) end if not hp then return string.format("Could not find hp for unit %s", tostring(unit)) end
local hp = UnitHealth(unit) local maxHp = UnitHealthMax(unit)
if not hp then return string.format("Could not find hp for unit %s", tostring(unit)) end if not maxHp then return string.format("Could not find maxHp for unit %s", tostring(unit)) end
local maxHp = UnitHealthMax(unit) local class = UnitClass(unit)
if not maxHp then return string.format("Could not find maxHp for unit %s", tostring(unit)) end if not class then return string.format("Could not find class for unit %s", tostring(unit)) end
local location = data.config.spotter.zoneOverride local location = Heimdall_Data.config.spotter.zoneOverride
if not location then 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()
if not subzone then subzone = "" end if not subzone then subzone = "" end
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
hostile and "Hostile" or "Friendly", local text = string.format("I see (%s) %s/%s %s of race %s (%s) with health %s/%s at %s (%2.2f, %2.2f)",
stinky and string.format("(%s)", "!!!!") or "", hostile and "Hostile" or "Friendly",
name, name,
race, class,
faction, stinky and string.format("(%s)", "!!!!") or "",
FormatHP(hp), race,
FormatHP(maxHp), faction,
location) FormatHP(hp),
FormatHP(maxHp),
---@type Message location,
local msg = { x * 100, y * 100)
channel = "CHANNEL",
data = data.config.spotter.notifyChannel, ---@type Message
message = text local msg = {
} channel = "CHANNEL",
data.dumpTable(msg) data = Heimdall_Data.config.spotter.notifyChannel,
table.insert(data.messenger.queue, msg) message = text
end }
--shared.dumpTable(msg)
local frame = CreateFrame("Frame") table.insert(shared.messenger.queue, msg)
frame:RegisterEvent("NAME_PLATE_UNIT_ADDED") end
frame:RegisterEvent("TARGET_UNIT_CHANGED")
frame:SetScript("OnEvent", function(self, event, unit) local frame = CreateFrame("Frame")
local err = NotifySpotted(unit) frame:RegisterEvent("NAME_PLATE_UNIT_ADDED")
if err then frame:RegisterEvent("UNIT_TARGET")
print(string.format("Error notifying %s: %s", tostring(unit), tostring(err))) frame:SetScript("OnEvent", function(self, event, unit)
end if not Heimdall_Data.config.spotter.enabled then return end
end) if event == "UNIT_TARGET" then
unit = "target"
print("Heimdall - Spotter loaded") end
end local err = NotifySpotted(unit)
if err then
print(string.format("Error notifying %s: %s", tostring(unit), tostring(err)))
end
end)
print("Heimdall - Spotter loaded")
end

109
Modules/StinkyTracker.lua Normal file
View 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

View File

@@ -1,413 +1,367 @@
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") if not Heimdall_Data.who then Heimdall_Data.who = {} end
return if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end
end
---@type table<string, Player>
if not Heimdall_Data.who then Heimdall_Data.who = {} end HeimdallStinkies = {}
if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end
---@class Player
---@type table<string, Player> ---@field name string
HeimdallStinkies = {} ---@field guild string
---@field race string
---@class Player ---@field class string
---@field name string ---@field zone string
---@field guild string ---@field lastSeenInternal number
---@field race string ---@field lastSeen string
---@field class string ---@field firstSeen string
---@field zone string ---@field seenCount number
---@field lastSeenInternal number ---@field stinky boolean?
---@field lastSeen string Player = {
---@field firstSeen string ---@param name string
---@field seenCount number ---@param guild string
---@field stinky boolean? ---@param race string
Player = { ---@param class string
---@param name string ---@param zone string
---@param guild string ---@return Player
---@param race string new = function(name, guild, race, class, zone)
---@param class string local self = setmetatable({}, {
---@param zone string __index = Player
---@return Player })
new = function(name, guild, race, class, zone) self.name = name
local self = setmetatable({}, { self.guild = guild
__index = Player self.race = race
}) self.class = class
self.name = name self.zone = zone
self.guild = guild self.lastSeenInternal = GetTime()
self.race = race self.lastSeen = "never"
self.class = class self.firstSeen = "never"
self.zone = zone self.seenCount = 0
self.lastSeenInternal = GetTime() return self
self.lastSeen = "never" end,
self.firstSeen = "never" ---@return string
self.seenCount = 0 ToString = function(self)
return self local out = string.format("%s %s %s\nFirst: %s Last: %s Seen: %3d",
end, shared.padString(self.name, 16, true),
---@return string shared.padString(self.guild, 26, false),
ToString = function(self) shared.padString(self.zone, 26, false),
local out = string.format("%s %s %s\nFirst: %s Last: %s Seen: %3d", shared.padString(self.firstSeen, 10, true),
data.padString(self.name, 16, true), shared.padString(self.lastSeen, 10, true),
data.padString(self.guild, 26, false), self.seenCount)
data.padString(self.zone, 26, false), return string.format("|cFF%s%s|r", shared.classColors[self.class], out)
data.padString(self.firstSeen, 10, true), end,
data.padString(self.lastSeen, 10, true), ---@return string
self.seenCount) NotifyMessage = function(self)
return string.format("|cFF%s%s|r", data.classColors[self.class], out) local text = string.format(
end, "%s %s of class %s, race %s (%s) and guild %s in %s, first seen: %s, last seen: %s, times seen: %d",
---@return string self.name,
NotifyMessage = function(self) self.stinky and "(!!!!)" or "",
local text = string.format( self.class,
"%s of class %s, race %s (%s) and guild %s in %s, first seen: %s, last seen: %s, times seen: %d", self.race,
self.name, tostring(shared.raceMap[self.race]),
self.class, self.guild,
self.race, self.zone,
tostring(data.raceMap[self.race]), self.firstSeen,
self.guild, self.lastSeen,
self.zone, self.seenCount)
self.firstSeen, return text
self.lastSeen, end
self.seenCount) }
return text
end ---@class WHOQuery
} ---@field query string
---@field filters WHOFilter[]
---@class WHOQuery WHOQuery = {
---@field query string ---@param query string
---@field filters WHOFilter[] ---@param filters WHOFilter[]
WHOQuery = { ---@return WHOQuery
---@param query string new = function(query, filters)
---@param filters WHOFilter[] local self = setmetatable({}, {
---@return WHOQuery __index = WHOQuery
new = function(query, filters) })
local self = setmetatable({}, { self.query = query
__index = WHOQuery self.filters = filters
}) return self
self.query = query end
self.filters = filters }
return self
end ---@alias WHOFilter fun(name: string, guild: string, level: number, race: string, class: string, zone: string): boolean
} ---@type WHOFilter
local NotSiegeOfOrgrimmarFilter = function(name, guild, level, race, class, zone)
---@alias WHOFilter fun(name: string, guild: string, level: number, race: string, class: string, zone: string): boolean if not zone then
---@type WHOFilter return false
local NotSiegeOfOrgrimmarFilter = function(name, guild, level, race, class, zone) end
if not zone then return zone ~= "Siege of Orgrimmar"
return false end
end ---@type WHOFilter
return zone ~= "Siege of Orgrimmar" local AllianceFilter = function(name, guild, level, race, class, zone)
end if not race then return false end
---@type WHOFilter if not shared.raceMap[race] then return false end
local AllianceFilter = function(name, guild, level, race, class, zone) return shared.raceMap[race] == "Alliance"
if not race then end
return false
end local whoQueryIdx = 1
if not data.raceMap[race] then ---@type WHOQuery[]
return false local whoQueries = {
end WHOQuery.new("g-\"БеспредеЛ\"", {}),
return data.raceMap[race] == "Alliance" --WHOQuery.new("g-\"Dovahkin\"", {}),
end WHOQuery.new(
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Human\" r-\"Dwarf\" r-\"Night Elf\"",
local whoQueryIdx = 1 { NotSiegeOfOrgrimmarFilter, AllianceFilter }),
---@type table<number, WHOQuery> WHOQuery.new(
local whoQueries = { "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Gnome\" r-\"Draenei\" r-\"Worgen\"",
WHOQuery.new("g-\"БеспредеЛ\"", {}), { NotSiegeOfOrgrimmarFilter, AllianceFilter }),
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-\"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-\"Gnome\" r-\"Draenei\" r-\"Worgen\"", "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Lightforged Draenei\" r-\"Mechagnome\"",
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }), { NotSiegeOfOrgrimmarFilter, AllianceFilter }),
WHOQuery.new( WHOQuery.new("Kekv Firobot Tomoki", {})
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" z-\"Echo Isles\" r-\"Kul Tiran\" r-\"Dark Iron Dwarf\" r-\"Void Elf\"", }
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }), local ttl = #whoQueries * 2
WHOQuery.new( ---@type WHOQuery?
"z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" z-\"Echo Isles\" r-\"Lightforged Draenei\" r-\"Mechagnome\"", local lastQuery = nil
{ NotSiegeOfOrgrimmarFilter, AllianceFilter }),
WHOQuery.new("Kekv Demonboo Dotmada Firobot Verminal", {}) ---@param player Player
} ---@return string?
local queryPending = false local function Notify(player)
local ttl = #whoQueries * 2 if not Heimdall_Data.config.who.enabled then return end
---@type WHOQuery? if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
local lastQuery = nil if not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
return string.format("Not notifying for zone %s",
---@param player Player tostring(player.zone))
---@return string? end
local function Notify(player)
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end local text = player:NotifyMessage()
if not data.config.who.zoneNotifyFor[player.zone] then ---@type Message
return string.format("Not notifying for zone %s", local msg = {
tostring(player.zone)) channel = "CHANNEL",
end data = Heimdall_Data.config.who.notifyChannel,
message = text
local text = player:NotifyMessage() }
---@type Message table.insert(shared.messenger.queue, msg)
local msg = {
channel = "CHANNEL", if Heimdall_Data.config.who.doWhisper then
data = data.config.who.notifyChannel, for _, name in pairs(Heimdall_Data.config.whisperNotify) do
message = text ---@type Message
} local msg = {
table.insert(data.messenger.queue, msg) channel = "WHISPER",
data = name,
if data.config.who.doWhisper then message = text
for _, name in pairs(data.config.whisperNotify) do }
---@type Message table.insert(shared.messenger.queue, msg)
local msg = { end
channel = "WHISPER", end
data = name,
message = text return nil
} end
table.insert(data.messenger.queue, msg) ---@param player Player
end ---@param zone string
end ---@return string?
local function NotifyZoneChanged(player, zone)
return nil if not Heimdall_Data.config.who.enabled then return end
end if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
---@param player Player if not Heimdall_Data.config.who.zoneNotifyFor[zone]
---@param zone string and not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
---@return string? return string.format("Not notifying for zones %s and %s", tostring(zone), tostring(player.zone))
local function NotifyZoneChanged(player, zone) end
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end local text = string.format("%s of class %s (%s - %s) and guild %s moved to %s",
if not data.config.who.zoneNotifyFor[zone] player.name,
and not data.config.who.zoneNotifyFor[player.zone] then player.class,
return string.format("Not notifying for zones %s and %s", tostring(zone), tostring(player.zone)) player.race,
end shared.raceMap[player.race] or "Unknown",
local text = string.format("%s of class %s and guild %s moved to %s", player.guild,
player.name, zone)
player.class,
player.guild, ---@type Message
zone) local msg = {
channel = "CHANNEL",
---@type Message data = Heimdall_Data.config.who.notifyChannel,
local msg = { message = text
channel = "CHANNEL", }
data = data.config.who.notifyChannel, table.insert(shared.messenger.queue, msg)
message = text
} if Heimdall_Data.config.who.doWhisper then
table.insert(data.messenger.queue, msg) for _, name in pairs(Heimdall_Data.config.whisperNotify) do
---@type Message
if data.config.who.doWhisper then local msg = {
for _, name in pairs(data.config.whisperNotify) do channel = "WHISPER",
---@type Message data = name,
local msg = { message = text
channel = "WHISPER", }
data = name, table.insert(shared.messenger.queue, msg)
message = text end
} end
table.insert(data.messenger.queue, msg)
end return nil
end end
---@param player Player
return nil ---@return string?
end local function NotifyGone(player)
---@param player Player if not Heimdall_Data.config.who.enabled then return end
---@return string? if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
local function NotifyGone(player) if not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end return string.format("Not notifying for zone %s",
if not data.config.who.zoneNotifyFor[player.zone] then tostring(player.zone))
return string.format("Not notifying for zone %s", end
tostring(player.zone))
end local text = string.format("%s of class %s and guild %s left %s",
player.name,
local text = string.format("%s of class %s and guild %s left %s", player.class,
player.name, player.guild,
player.class, player.zone)
player.guild,
player.zone) ---@type Message
local msg = {
---@type Message channel = "CHANNEL",
local msg = { data = Heimdall_Data.config.who.notifyChannel,
channel = "CHANNEL", message = text
data = data.config.who.notifyChannel, }
message = text table.insert(shared.messenger.queue, msg)
}
table.insert(data.messenger.queue, msg) if Heimdall_Data.config.who.doWhisper then
for _, name in pairs(Heimdall_Data.config.whisperNotify) do
if data.config.who.doWhisper then ---@type Message
for _, name in pairs(data.config.whisperNotify) do local msg = {
---@type Message channel = "WHISPER",
local msg = { data = name,
channel = "WHISPER", message = text
data = name, }
message = text table.insert(shared.messenger.queue, msg)
} end
table.insert(data.messenger.queue, msg) end
end
end return nil
end
return nil
end local frame = CreateFrame("Frame")
frame:RegisterEvent("WHO_LIST_UPDATE")
local frame = CreateFrame("Frame") frame:SetScript("OnEvent", function(self, event, ...)
frame:RegisterEvent("WHO_LIST_UPDATE") if not Heimdall_Data.config.who.enabled then return end
frame:SetScript("OnEvent", function(self, event, ...) ---@type WHOQuery?
---@type WHOQuery? local query = lastQuery
local query = lastQuery if not query then
if not query then print("No query wtf?")
print("No query wtf?") return
return end
end
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) local continue = false
if data.who.ignored[name] then return end --print(name, guild, level, race, class, zone)
local continue = false
---@type WHOFilter[]
---@type WHOFilter[] local filters = query.filters
local filters = query.filters for _, filter in pairs(filters) do
for _, filter in pairs(filters) do if not filter(name, guild, level, race, class, zone) then
if not filter(name, guild, level, race, class, zone) then -- Mega scuffed, yes...
-- Mega scuffed, yes... -- But wow does not have gotos
-- But wow does not have gotos 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
local timestamp = date("%Y-%m-%dT%H:%M:%S") if not continue then
local player = HeimdallStinkies[name] local timestamp = date("%Y-%m-%dT%H:%M:%S")
if not player then local player = HeimdallStinkies[name]
player = Player.new(name, guild, race, class, zone) if not player then
if not Heimdall_Data.who then Heimdall_Data.who = {} end player = Player.new(name, guild, race, class, zone)
if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end if not Heimdall_Data.who then Heimdall_Data.who = {} end
local existing = Heimdall_Data.who.data[name] if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end
local existing = Heimdall_Data.who.data[name]
if existing then
player.lastSeen = existing.lastSeen or "never" if existing then
player.firstSeen = existing.firstSeen or "never" player.lastSeen = existing.lastSeen or "never"
player.seenCount = existing.seenCount or 0 player.firstSeen = existing.firstSeen or "never"
end player.seenCount = existing.seenCount or 0
if player.firstSeen == "never" then end
player.firstSeen = timestamp if player.firstSeen == "never" then
end player.firstSeen = timestamp
end
local stinky = data.config.stinkies[name]
if stinky then local stinky = Heimdall_Data.config.stinkies[name]
player.stinky = true if stinky then
PlaySoundFile("Interface\\Sounds\\Domination.ogg", "Master") player.stinky = true
else --PlaySoundFile("Interface\\Sounds\\Domination.ogg", "Master")
PlaySoundFile("Interface\\Sounds\\Cloak.ogg", "Master") else
end --PlaySoundFile("Interface\\Sounds\\Cloak.ogg", "Master")
end
local err = Notify(player)
if err then local err = Notify(player)
print(string.format("Error notifying for %s: %s", tostring(name), tostring(err))) if err then
end print(string.format("Error notifying for %s: %s", tostring(name), tostring(err)))
end
player.lastSeen = timestamp
player.seenCount = player.seenCount + 1 player.lastSeen = timestamp
HeimdallStinkies[name] = player player.seenCount = player.seenCount + 1
end HeimdallStinkies[name] = player
end
player.lastSeenInternal = GetTime()
if player.zone ~= zone then player.lastSeenInternal = GetTime()
local err = NotifyZoneChanged(player, zone) if player.zone ~= zone then
if err then local err = NotifyZoneChanged(player, zone)
print(string.format("Error notifying for %s: %s", tostring(name), tostring(err))) if err then
end print(string.format("Error notifying for %s: %s", tostring(name), tostring(err)))
end end
player.zone = zone end
player.lastSeen = timestamp player.zone = zone
HeimdallStinkies[name] = player player.lastSeen = timestamp
if not Heimdall_Data.who then Heimdall_Data.who = {} end HeimdallStinkies[name] = player
if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end if not Heimdall_Data.who then Heimdall_Data.who = {} end
Heimdall_Data.who.data[name] = player if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end
end Heimdall_Data.who.data[name] = player
end end
-- Turns out WA cannot do this ( end
-- aura_env.UpdateMacro() -- Turns out WA cannot do this (
_G["FriendsFrameCloseButton"]:Click() -- aura_env.UpdateMacro()
queryPending = false _G["FriendsFrameCloseButton"]:Click()
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
end local function Tick()
UpdateStinkies()
if not data.who.whoTicker then C_Timer.NewTimer(0.5, Tick, 1)
data.who.whoTicker = C_Timer.NewTicker(1, function() end
if queryPending then Tick()
print("Tried running a who query while one is already pending, previous query:") end
data.dumpTable(lastQuery)
return do
end local function DoQuery()
queryPending = true if not Heimdall_Data.config.who.enabled then return end
local query = whoQueries[whoQueryIdx] local query = whoQueries[whoQueryIdx]
whoQueryIdx = whoQueryIdx + 1 whoQueryIdx = whoQueryIdx + 1
if whoQueryIdx > #whoQueries then if whoQueryIdx > #whoQueries then
whoQueryIdx = 1 whoQueryIdx = 1
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)))
SetWhoToUI(1) ---@diagnostic disable-next-line: param-type-mismatch
SendWho(query.query) SetWhoToUI(1)
end) SendWho(query.query)
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) end
if msg == "who" then Tick()
for _, player in pairs(HeimdallStinkies) do end
local text = player:NotifyMessage()
---@type Message print("Heimdall - Whoer loaded")
local msg = { end
channel = "WHISPER",
data = sender,
message = text
}
table.insert(data.messenger.queue, msg)
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")
end

324
README.md Normal file
View 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

File diff suppressed because one or more lines are too long

185
Weakauras/Config/init.lua Normal file
View 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

5
deploy.sh Normal file
View File

@@ -0,0 +1,5 @@
rm Heimdall.zip
mkdir Heimdall
cp *.lua *.toc Modules/*.lua Heimdall
7z a Heimdall.zip Heimdall
rm -rf Heimdall