441 Commits

Author SHA1 Message Date
7eee3a13a6 Add Russian localization who (kto) for our russian friends 2025-05-25 11:09:43 +02:00
263cf8e2e4 Add chatSniffer localization and configuration UI 2025-05-25 10:58:08 +02:00
ccbf0f8dc2 Add chatSniffer configuration and integrate into Heimdall initialization 2025-05-25 10:54:01 +02:00
63027c2dcf Update module initialization messages to use consistent format 2025-05-25 10:51:41 +02:00
d46c874604 Add timestamp logging to ChatSniffer events 2025-05-25 10:48:11 +02:00
fd4f707b6c Add ChatSniffer module and update saved variables 2025-05-25 10:47:58 +02:00
1168876dcc Fix FriendsFrame_OnEvent to pass additional parameters so it actually works naturally 2025-05-25 10:45:17 +02:00
bdf5afe436 Comment out debug print statements in Messenger module and fix queue loading 2025-05-25 10:41:02 +02:00
85ff907f05 Reset whoQueryIdx to 1 when no WHO query is found 2025-05-25 10:39:45 +02:00
7ae9db030b Remove redundant event registration for ADDON_LOADED in Heimdall.lua 2025-05-25 10:34:48 +02:00
edf8a12865 Refactor message queuing to use NetworkMessenger and Messenger based on configuration 2025-05-21 01:42:41 +02:00
d081eedd47 Fix debug print statement to include message count 2025-05-20 20:45:42 +02:00
8532db5a25 Rename dumpTable to dump and make it work with any values 2025-05-20 20:40:25 +02:00
26e783ee2e Update subproject commit reference in Meta 2025-05-20 20:20:59 +02:00
4bd237abef Hook friends list show to NOT show while we're waiting for who results 2025-05-20 20:19:41 +02:00
0ab14de0e2 CCP 2025-05-20 20:04:41 +02:00
a25b6a20d5 Release 3.12.0 2025-05-20 20:02:47 +02:00
e85c14ea45 Update StinkyTracker to use ReactiveValue for accessing ignored and stinkies lists 2025-05-18 16:05:12 +02:00
a564178ca2 Fix dumpTable function to ensure keys and values are converted to strings before printing 2025-05-18 16:05:03 +02:00
b4a4011b18 Refactor NetworkMessenger initialization to use ReactiveValue for queue management 2025-05-18 16:00:39 +02:00
3f3d252104 Update subproject commit reference in Meta 2025-05-18 15:54:48 +02:00
287be2a31c Move data definitions into their separate modules 2025-05-18 15:54:38 +02:00
3ef0e4c935 Refactor Heimdall messenger module to improve structure and utilize ReactiveValue for queue management 2025-05-18 15:54:11 +02:00
ce92e8e12c Refactor Commander and StinkyTracker modules for improved clarity and consistency in type annotations 2025-05-18 12:48:26 +02:00
03597d1b5e Update subproject commit reference in Meta 2025-05-18 12:45:11 +02:00
36ad9783e5 Move all config definitions to their respective modules 2025-05-18 12:43:55 +02:00
565db30125 Refactor multiple Heimdall modules to use class-based structure for improved organization and clarity 2025-05-18 12:31:26 +02:00
017cbf01f8 Refactor Heimdall modules to use class-based structure for improved organization and clarity 2025-05-18 12:28:25 +02:00
b16cf762ac Refactor Heimdall modules to improve structure and clarity, including AchievementSniffer, BonkDetector, Bully, Commander, and Config. 2025-05-18 12:27:00 +02:00
0057ac3a5c Refactor CombatAlerter, Commander, Inviter, Macroer, Sniffer modules for improved structure and clarity 2025-05-18 12:12:57 +02:00
0edf0561d8 Refactor DeathReporter, CombatAlerter, Commander, Configurator, and AgentTracker modules 2025-05-18 11:48:50 +02:00
1129d787b5 Refactor BonkDetector and Bully modules for improved structure and clarity 2025-05-18 11:43:40 +02:00
8a24496801 Add scratch.lua to .gitignore 2025-05-18 11:37:05 +02:00
6cb918c13c Refactor Heimdall module fields for improved organization and clarity 2025-05-18 11:36:40 +02:00
e3eefadb75 Refactor AgentTracker and related modules to improve agent management and logging 2025-05-18 11:16:13 +02:00
eab562b36d Enhance dumpTable function to include optional message parameter for improved logging 2025-05-18 11:15:52 +02:00
20a7c0eead Refactor StinkyTracker to improve tracking and ignore functionality 2025-05-18 10:51:33 +02:00
f70c5adfcf Add debug logging for stinky changes and simplify stinky handling 2025-05-18 10:31:36 +02:00
52246e2e16 Update subproject commit reference in Meta 2025-05-18 10:18:45 +02:00
4897a5b1a9 Update ticker field type in HeimdallMessengerData and HeimdallNetworkMessengerData 2025-05-18 10:18:11 +02:00
2381f68912 Updater meta config 2025-05-18 10:10:45 +02:00
ad969e8604 Update subproject commit reference in Meta 2025-05-18 10:09:02 +02:00
31678f9a82 Fix config frame visibility in shared.Config.Init() 2025-05-18 10:08:23 +02:00
1785af4d9d Remove the premature exit 2025-05-05 01:16:57 +02:00
69d7efe6f0 Release 3.11.0 2025-05-05 01:16:31 +02:00
202c5d0f46 Update release script to automatically tag releases
Maybe it's a bit better who knows...........
2025-05-05 01:16:21 +02:00
476e907205 Release 3.10.4 2025-05-05 01:15:55 +02:00
724194abe2 Fix up the release to be a little more manual 2025-05-05 01:02:59 +02:00
d57b573683 Update
Some checks failed
Release Workflow / release (push) Failing after 18s
2025-05-05 00:58:16 +02:00
5d8afe8db7 Add the FUCKING REMOVED _
Some checks failed
Release Workflow / release (push) Failing after 18s
2025-05-05 00:30:58 +02:00
b14f1ff2f9 Update
Some checks failed
Release Workflow / release (push) Failing after 18s
2025-05-05 00:02:47 +02:00
a1301abdb2 Update 2025-05-05 00:02:38 +02:00
f897183920 Update 2025-05-04 23:56:58 +02:00
c1885ce76a Fix up config to comply with meta
Update
2025-05-04 23:39:35 +02:00
e676d53e97 Update meta 2025-05-04 23:23:48 +02:00
f05156b257 Update 2025-05-04 21:38:03 +02:00
c8f9d81b3e Remove meaningless vscode settings 2025-05-04 21:37:57 +02:00
3a1639ab27 Clean up the modules a little 2025-05-04 20:47:00 +02:00
1da1e7bf9f Code format
Some checks failed
Release Workflow / release (push) Failing after 23s
2025-05-04 15:09:47 +02:00
304fbcbaae Update meta
Some checks failed
Release Workflow / release (push) Failing after 20s
Update meta
2025-05-04 15:07:42 +02:00
80f8500f6e Add luacheckrcAdd debug logs
Some checks failed
Release Workflow / release (push) Failing after 18s
2025-04-30 20:18:15 +02:00
78cbcbde9d Add luacheckrc 2025-04-30 20:13:10 +02:00
Git Admin
8db1cb179c Release 3.10.1 2025-01-29 20:31:06 +00:00
a065e47545 Automagically tag shit
All checks were successful
Release Workflow / release (push) Successful in 7s
2025-01-29 21:30:56 +01:00
5271029b84 Release 3.10.0 2025-01-29 21:24:57 +01:00
7c4fb53baa Dummy
All checks were successful
Release Workflow / release (push) Successful in 6s
2025-01-29 21:18:16 +01:00
1c2fb471a6 Please trigger only on versions 2025-01-29 21:03:47 +01:00
85f72b14e0 Fix url?
All checks were successful
Release Workflow / release (push) Successful in 4s
2025-01-29 21:02:13 +01:00
99261beb08 Use http for runner...
Some checks failed
Release Workflow / release (push) Has been cancelled
2025-01-29 20:51:49 +01:00
d34be4f18d Add god damn 7z
Some checks failed
Release Workflow / release (push) Has been cancelled
2025-01-29 20:50:58 +01:00
3d26832e54 Remove debug workflow
Some checks failed
Release Workflow / release (push) Failing after 1m17s
2025-01-29 20:47:44 +01:00
cd80c9fc0e Add debug workflow
Some checks failed
Release Workflow / release (push) Has been cancelled
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 47s
2025-01-29 20:47:14 +01:00
672ca07782 Please run workflow 2025-01-29 20:43:54 +01:00
b06313c4eb Dummy 2025-01-29 20:40:42 +01:00
31be9b0ce8 Jebeni token 2025-01-29 20:39:25 +01:00
d69a53b4c8 Add release 2025-01-29 20:37:44 +01:00
7aa5d50a6c Fix zone notify for in commander 2025-01-27 16:22:32 +01:00
d047c632ed Fix counting in stinky tracker 2025-01-27 12:32:31 +01:00
9eb5c04a33 Release 3.9.3 2025-01-27 12:31:39 +01:00
498432d968 Fix stinky tracker parsing arrived messages 2025-01-27 12:31:36 +01:00
e04eb57202 Release 3.9.2 2025-01-27 01:03:05 +01:00
25fe8350a7 Fix noter 2025-01-27 01:03:00 +01:00
9a10386e65 Update whitelisted achievements 2025-01-27 00:53:11 +01:00
75ab8646e8 Release 3.9.1 2025-01-27 00:04:36 +01:00
37ef42e53d Spare memory a bit 2025-01-27 00:04:32 +01:00
b2aa1b75f3 Remove "echo to russian" 2025-01-27 00:02:37 +01:00
31bcd3481c Release 3.9.0 2025-01-27 00:01:35 +01:00
966078c339 Localize achievement sniffer 2025-01-27 00:01:29 +01:00
fdcd816235 Give up on the frames 2025-01-27 00:01:28 +01:00
054d8ab7ef Add more configs for achievement sniffers 2025-01-26 23:23:49 +01:00
0327359300 Implement inspect functionality 2025-01-26 23:20:00 +01:00
c61a4b0c04 Add structure for achievement sniffer 2025-01-26 22:44:59 +01:00
950f95cef1 More better logs 2025-01-26 22:31:06 +01:00
d40670fef6 Release 3.8.15 2025-01-26 22:20:07 +01:00
d6e5e7941f Fix inviter kick buttons 2025-01-26 22:20:04 +01:00
a06e9027c6 Ugh make more better regex 2025-01-26 20:03:59 +01:00
8fb0595ef8 Release 3.8.14 2025-01-26 20:00:08 +01:00
d3808a887e Fix up stinkytracker to play nice with the new messages 2025-01-26 19:59:23 +01:00
1ed379c23d Standardize whoer messages a little 2025-01-26 19:50:42 +01:00
0e66db1d8a Release 3.8.13 2025-01-26 18:44:25 +01:00
bac16d22a9 Fix whoer localization 2025-01-26 18:31:02 +01:00
8b4de82142 Fix L in stinky 2025-01-26 18:28:30 +01:00
35930c52a4 Remove the buggy cancer from _L 2025-01-26 18:20:49 +01:00
293e71e619 Fix naming in noter 2025-01-26 18:20:16 +01:00
7150189a0d Fix naming in bonkdetector 2025-01-26 18:20:16 +01:00
0e951d7089 Fix naming in minimaptagger 2025-01-26 18:20:16 +01:00
ef89c3001b Fix naming in sniffer 2025-01-26 18:20:15 +01:00
b538c7b5de Move every locale entry to root level 2025-01-26 18:20:15 +01:00
6c234e7fa4 Fix naming in combatalerter 2025-01-26 18:20:15 +01:00
407d8f2da2 Fix naming in stinkytracker 2025-01-26 18:20:15 +01:00
9b755cd0be Fix naming in commander 2025-01-26 18:20:14 +01:00
19f9d4bda9 Fix naming in echoer and emoter 2025-01-26 18:20:14 +01:00
de744337ad Fix naming in agenttracker 2025-01-26 18:20:14 +01:00
63ba6d2da1 Fix naming in inviter 2025-01-26 18:20:14 +01:00
79b77ee6c3 Rework localization in deathreporter 2025-01-26 18:20:13 +01:00
0e2935f844 Rework localization in spotter 2025-01-26 18:20:13 +01:00
0ad6a23daa Fix up mapfromstring 2025-01-26 18:20:13 +01:00
35398ebf38 Rework locale 2025-01-26 18:20:13 +01:00
62b028cf56 Add channel locale config options 2025-01-26 18:20:12 +01:00
ad676915bb Update locale 2025-01-26 18:20:12 +01:00
61ebb22a85 Fallback to empty arrays for config 2025-01-26 18:20:12 +01:00
8348b93b30 Update config with channels 2025-01-26 18:20:12 +01:00
550e11b488 Rework all "masterChannel" and so to "channels" (list) 2025-01-26 18:20:11 +01:00
ed10ea496d Release 3.8.12 2025-01-26 15:07:48 +01:00
6e5c7510d0 Remove printing to essence 2025-01-26 15:07:34 +01:00
68d0393915 Update weakaura display export 2025-01-26 15:05:06 +01:00
f1e07f0a3b Fallback for ru faction 2025-01-26 15:05:02 +01:00
90100f6f5b Release 3.8.11 2025-01-26 00:38:40 +01:00
995966e952 Make config bigger
Had to be done...
2025-01-26 00:03:55 +01:00
a90eb8248f Rework "notifyzonefor" to use regex 2025-01-25 22:51:43 +01:00
ffca28c67d Add locale 2025-01-25 22:29:25 +01:00
20a2a95ce6 More bigger query space 2025-01-25 22:00:46 +01:00
145fd02ba8 Rework query saving to only write strings instead of tables 2025-01-25 21:56:01 +01:00
196a5a8cfa Rework who queries out to config 2025-01-25 20:57:11 +01:00
439e9b29d1 Rework display to sort stinkies first 2025-01-25 19:48:04 +01:00
a3f1a0e96d Remove dates from who messages, they are way too long sadly 2025-01-25 10:50:03 +01:00
c81e349e90 Release 3.8.10 2025-01-24 20:35:55 +01:00
30068a5b11 Shorten every instance of "CHANNEL" to "C" and whisper too
To save a little space in messages, it's getting very cramped
2025-01-24 20:35:49 +01:00
036b6b23a8 Fix AI disaster 2025-01-22 16:05:41 +01:00
128eb44003 PRINTING NILS 2025-01-22 16:05:37 +01:00
0f72b2048e Shorten messages because they were getting too long for chat 2025-01-22 15:15:27 +01:00
1e4045ab7a Only print to essence for lawless 2025-01-22 15:15:17 +01:00
b871549087 Comment out SPAM 2025-01-22 15:15:17 +01:00
58760831dc Release 3.8.9 2025-01-21 09:23:08 +01:00
0a8ab00637 Shorten who new messages, too long make game die 2025-01-21 09:13:53 +01:00
8396801d80 Send RUSSIAN messages to essence please 2025-01-21 09:13:41 +01:00
19af9894e5 Fix nil in network messenger 2025-01-21 09:13:33 +01:00
7293f5a8fa Prevent messenger from sendingtoo long messages 2025-01-21 09:13:27 +01:00
9d12609147 Release 3.8.8 2025-01-21 08:57:04 +01:00
2c7089504f Make inviter whitelist multiple channels (ie. read) 2025-01-21 08:56:59 +01:00
8fbff23bee Release 3.8.7 2025-01-21 08:51:05 +01:00
0c5078e3f3 Add notifications for essence 2025-01-21 08:50:59 +01:00
d143a18838 Do fucking something who even fucking cares 2025-01-15 21:28:36 +01:00
ba889e442c Refactor stinky detection to heimdall 2025-01-15 20:58:41 +01:00
4511ecbf0a Add configurator and stinky cache 2025-01-15 20:58:40 +01:00
fe37bebd2c Make russian messages a little more better 2025-01-15 16:15:16 +01:00
d987436892 Release 3.8.6 2025-01-15 15:12:59 +01:00
9f4e19104f Make commander and whoer use the distributed messenger 2025-01-15 15:11:52 +01:00
c0568075e1 Release 3.8.5 2025-01-15 15:08:40 +01:00
13415fb065 Don't check for network leader on received message 2025-01-15 15:08:36 +01:00
ca13d1e364 Release 3.8.4 2025-01-15 14:50:49 +01:00
bf916dd8d5 Always tick 2025-01-15 14:50:46 +01:00
4aa168ebcc Now mark player as always online
Which means they will be added at the correct position in the network
chain
2025-01-15 14:42:40 +01:00
2cea01f367 Release 3.8.3 2025-01-15 14:38:25 +01:00
2ec0aea19c Forcefully trigger friend update
Because wow is held together by spit and cardboard
2025-01-15 14:37:59 +01:00
846584d6fe Verify name exists before adding them to network 2025-01-15 14:30:44 +01:00
1b5912a1bf Release 3.8.2 2025-01-15 14:27:06 +01:00
954dbfa425 Stop trying to add yourself to your own friend list 2025-01-15 14:27:03 +01:00
42ec90a5df Release 3.8.1 2025-01-15 14:25:01 +01:00
0b4350c8ae Implement distributed messenger 2025-01-15 14:24:57 +01:00
da28805882 Implement networkmessenger distributioning 2025-01-15 14:17:37 +01:00
6551e24069 Rework network to use timer instead of onupdate 2025-01-15 14:02:20 +01:00
28ef8cb33a Release 3.8.0 2025-01-15 13:49:14 +01:00
d4b0dee037 Implement message command in messenger 2025-01-15 13:48:56 +01:00
799d3e1ddd Update network when first running 2025-01-15 13:38:18 +01:00
e6f3bac946 Update Heimdall.toc to include NetworkMessenger module 2025-01-15 13:36:10 +01:00
05c7e71794 Rework network node tracking to be ordered 2025-01-15 13:31:17 +01:00
41b980d118 Release 3.7.1 2025-01-15 13:20:32 +01:00
3efd99cdc8 Run init for networkmessenger 2025-01-15 13:20:28 +01:00
119eb7965b Release 3.7.0 2025-01-15 13:19:50 +01:00
f4421f0334 Add network messenger config 2025-01-15 13:19:44 +01:00
688f2f4b30 Refactor addonprefix out of network 2025-01-15 13:15:18 +01:00
87300bf48a Update network nodes on network update 2025-01-15 13:08:31 +01:00
8cbad47acc Add updateinterval config to network 2025-01-15 12:56:56 +01:00
241615238c Implement basic structure for network 2025-01-15 12:54:22 +01:00
319e6cdd77 Release 3.6.2 2025-01-15 12:34:27 +01:00
d54e93ad85 Print index of note 2025-01-15 12:34:23 +01:00
efe0002e02 Implement compacting 2025-01-15 12:31:43 +01:00
e58d92c399 Fix adding notes 2025-01-15 12:26:50 +01:00
fa18138c3b Release 3.6.1 2025-01-15 12:20:18 +01:00
2a5d6e5157 Update help messages for noter 2025-01-15 12:20:12 +01:00
c32549fa87 Release 3.6.0 2025-01-15 12:16:14 +01:00
2689e39d70 Implement ACTUALLY sending notes 2025-01-15 12:16:07 +01:00
25f2310c25 Fix up adding and printing notes 2025-01-15 12:13:24 +01:00
0bed5ecf41 Implement adding notes 2025-01-15 11:55:02 +01:00
ec2f146095 Implement printing last N notes 2025-01-15 11:52:23 +01:00
75a84baa42 Implement addnote 2025-01-15 11:38:43 +01:00
1bc7ebc92a Refactor delete note and copy paste to print note 2025-01-15 11:35:12 +01:00
d620f577c1 Implement note delete
Weird start, I know
2025-01-15 11:30:26 +01:00
7af1b40222 Implement shared split 2025-01-15 11:30:20 +01:00
82f1539815 Implement basic structure for noter 2025-01-15 11:07:03 +01:00
e38ba012a8 Release 3.5.0 2025-01-15 08:44:42 +01:00
a0322718c1 Actually add sniffer... 2025-01-15 08:44:30 +01:00
01ca12f80e Release 3.4.7 2025-01-14 18:09:31 +01:00
3376b4fa7c Add config for the new "echotorussian" setting 2025-01-14 18:08:44 +01:00
308b65e2f6 Add option to echo to russian channel 2025-01-14 18:04:18 +01:00
fa5b73b5fe Rename deploy to release to make more sense 2025-01-13 16:46:55 +01:00
0fd088320d Release 3.4.6 2025-01-13 16:36:40 +01:00
a109c631cd Add version field to config 2025-01-13 16:36:37 +01:00
cf61a74fa8 Don't tag minimap if we aren't in the same zone 2025-01-13 16:25:30 +01:00
8fa4effb6b Add meta as submodule 2025-01-13 16:19:23 +01:00
5ca69bbe24 Add meta for locale 2025-01-13 16:16:51 +01:00
8816468ba0 Release 2025-01-13 15:33:26 +01:00
e9f17c585c Actually disable bonkdetector 2025-01-13 15:29:16 +01:00
373ca377a2 Ignore bonk events for source==destination (self harm) 2025-01-13 14:09:57 +01:00
770420a5b2 ...actually make it work, idiot 2025-01-13 10:30:55 +01:00
22b1b6bc73 Add macro command 2025-01-13 10:26:03 +01:00
18fd4bb9d2 Right, of course, UnitIsPLayer expects a UNIT how silly of me.....
I hate the wow api
2025-01-12 23:32:42 +01:00
ca333a93e3 Only trigger bonks for players 2025-01-12 23:22:33 +01:00
4c404225d2 Add config for bonkdetector 2025-01-12 23:19:38 +01:00
9f86a4e0f9 Add russian locale for bonkdetector 2025-01-12 23:11:29 +01:00
6273263c4e Implement bonkdetector 2025-01-12 23:07:31 +01:00
0b6b8df1a9 Clean up config a little 2025-01-12 22:58:19 +01:00
fe730a19df Add config for sniffer 2025-01-12 22:00:37 +01:00
636ef87cb1 Fix deathreporter 2025-01-12 01:16:06 +01:00
d333901576 Untrack (and track) stinkies on target that are or are not supposed to be tracked 2025-01-11 19:09:29 +01:00
d104bcc1fa Fix error when there's no battlefieldminimap
But still play alert
2025-01-11 13:14:31 +01:00
dbfbc2c347 Add localization for config panel 2025-01-11 13:10:32 +01:00
dd620c14d3 Hopefully fix nil in deathreporter 2025-01-11 12:33:55 +01:00
744098abc7 Fix scale number check 2025-01-10 21:42:47 +01:00
d41554271d Release 2025-01-10 21:35:27 +01:00
23bfbf9f4a PLEASE 2025-01-10 21:14:09 +01:00
f52ed8c791 Attempt #23458 2025-01-10 21:13:35 +01:00
604371a2e1 jebo ti token mater da ti jebo token mater dao bog crko 2025-01-10 21:11:39 +01:00
44cec2d2fb Try again 2025-01-10 21:10:39 +01:00
cbe9ef7303 Maybe fix workflow? 2025-01-10 21:05:25 +01:00
002970484d Dummy commit 2025-01-10 20:58:41 +01:00
f063ceb4e5 Add release workflow I hope 2025-01-10 20:55:54 +01:00
1eaefffe04 Add a scale config... such as it is 2025-01-10 20:44:53 +01:00
1291d21216 Fix oopsie 2025-01-10 19:01:20 +01:00
1c198f0133 Remove num of stinkies from log messages
Because it's not ipairs
2025-01-10 13:23:58 +01:00
23bf656f82 More logging to stinky tracker 2025-01-10 13:22:14 +01:00
30fae67f6c Fix who parsing in spotter 2025-01-10 12:16:21 +01:00
2726955034 Add more localizations
Add more localization..
2025-01-10 12:08:11 +01:00
e433bc3319 Update the FUCKIGN TOC 2025-01-09 21:16:33 +01:00
71df812170 Release 2025-01-09 20:59:13 +01:00
abb8540c12 Add russian locale
Poorly... But it'll do for now
2025-01-09 20:58:19 +01:00
12e0c23ea9 LFS sounds 2025-01-09 17:48:06 +01:00
b98ecdd0a4 Add all textures 2025-01-09 17:48:06 +01:00
7314c67357 Implement tomtom waypoint for help 2025-01-09 17:08:13 +01:00
6b74e01f0a Add configuration for textures 2025-01-09 16:51:03 +01:00
6becc08e18 Implement call for help 2025-01-09 16:38:57 +01:00
f66a990103 Add scale config 2025-01-09 16:37:36 +01:00
c3b9772512 Do a little formatting 2025-01-09 16:35:56 +01:00
6bb1cc683c Implement throttling sounds and niceify the code a bit 2025-01-09 16:21:59 +01:00
b5473e05e7 Shuffle around the config a little for fun 2025-01-09 15:50:32 +01:00
939ca47e3c Fix grid placement logic 2025-01-09 15:39:08 +01:00
c16e5f1e47 Fix up more logs 2025-01-09 15:32:57 +01:00
059f917acc Fix ttl = nil 2025-01-09 15:24:42 +01:00
43b08f22dd Add more config options 2025-01-09 14:34:06 +01:00
aa7e4a3d3e Add sound files 2025-01-09 14:19:17 +01:00
18f2b44941 Disable alerts that have ttl=0 2025-01-09 14:00:25 +01:00
b2925285a2 Add playing sound files (that don't yet exist) 2025-01-09 13:53:56 +01:00
c0a6a3c082 Add ttl per frame 2025-01-09 13:14:58 +01:00
7c7edcf959 Add location recognition for combat and death reporters 2025-01-09 12:56:23 +01:00
dee5053345 Standardize location reporting across modules 2025-01-09 12:56:11 +01:00
58e071e77b Implement icon scale to minimap tagger 2025-01-09 12:46:32 +01:00
a7e85acd67 Add battletags 2025-01-09 12:20:16 +01:00
176d184d91 Implement tag alerts 2025-01-09 12:18:04 +01:00
32543d04a0 Refactor frame creation a little 2025-01-09 12:11:03 +01:00
4b43ee86c0 Implement fading alerts 2025-01-09 12:06:34 +01:00
25959be98f Implement drawing alerts on map 2025-01-09 11:30:13 +01:00
cb6680304f Global frames ARE INDEED DEFINED 2025-01-09 11:28:38 +01:00
40646f16bc Add minimaptagger config 2025-01-09 11:28:37 +01:00
55e7ee2428 Add textures for displayings 2025-01-09 11:28:20 +01:00
a2930577d3 Add basic structure 2025-01-09 11:28:20 +01:00
e572f50de7 Fix whoer log 2025-01-09 11:27:25 +01:00
47b7f5d85a Fucking fix everything we fucked up 2025-01-09 11:24:27 +01:00
8ac29e4378 Rework death reporter a lil 2025-01-09 11:23:11 +01:00
be81a31302 Code format 2025-01-09 11:20:02 +01:00
2e44a1ef31 Rework the log messages to not spam on channel message 2025-01-09 11:15:44 +01:00
d182cc1418 Implement sniffer 2025-01-09 11:15:44 +01:00
d3004019c6 Fix up the log messages a lil
Unbutcher inviter
2025-01-09 11:15:44 +01:00
fca49c6302 Add debug options
Add debug buttons

Add combatalerter debug
2025-01-08 16:24:49 +01:00
8b085009a9 Add mld 2025-01-08 13:28:15 +01:00
2ba6d190f0 Properly display heimdall commands 2025-01-08 13:13:43 +01:00
8c5a94a12a Rework commands to be ^$ 2025-01-08 13:12:20 +01:00
bf9e1f0319 Make stinky frame larger 2025-01-07 14:52:08 +01:00
903baf7f38 Release 2025-01-07 13:36:43 +01:00
5b585ebba7 Fix deploy 2025-01-07 13:36:20 +01:00
34bae5dc7b Fix setting text to nil 2025-01-07 13:36:14 +01:00
4f97859533 Rework releases so that they actually work lol 2025-01-07 13:17:40 +01:00
4c55e65863 STOP MACROING AGENTS 2025-01-07 13:15:16 +01:00
016f0be480 Hide config frame on startup 2025-01-07 12:31:17 +01:00
fbf35d6d77 Properly render stinkies config 2025-01-07 10:30:02 +01:00
30ee1c717e Release 2025-01-07 01:28:53 +01:00
e3286571b1 Remove some old print statements 2025-01-07 01:28:20 +01:00
ece39790d2 Escape now closes config 2025-01-07 01:26:47 +01:00
eedadb0a3f Merge branch 'config' 2025-01-07 01:17:26 +01:00
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
00f7cba8fc Minor shit 2025-01-06 13:14:13 +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
200 changed files with 11302 additions and 2432 deletions

3
.gitattributes vendored Normal file
View File

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

2
.gitignore vendored
View File

@@ -1 +1 @@
Meta scratch.lua

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "Meta"]
path = Meta
url = https://git.site.quack-lab.dev/dave/wow_Meta

5
.luacheckrc Symbolic link
View File

@@ -0,0 +1,5 @@
globals = { "CykaPersistentData", "CreateFrame", "GetItemInfo", "aura_env" }
unused_args = false
max_line_length = 500
exclude_files = { "Meta/" }
global = false

14
.luarc.json Symbolic link
View File

@@ -0,0 +1,14 @@
{
"workspace": {
"library": [
"./Meta"
]
},
"diagnostics.disable": [
"unused-local",
"unused-vararg"
],
"diagnostics.globals": [
"aura_env"
]
}

View File

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

View File

@@ -1,29 +0,0 @@
local addonname, data = ...
---@cast data HeimdallData
---@cast addonname string
if not data.dumpTable then
---@param table table
---@param depth number?
data.dumpTable = function(table, depth)
if not table then
print(tostring(table))
return
end
if depth == nil then
depth = 0
end
if (depth > 200) then
print("Error: Depth > 200 in dumpTable()")
return
end
for k, v in pairs(table) do
if (type(v) == "table") then
print(string.rep(" ", depth) .. k .. ":")
data.dumpTable(v, depth + 1)
else
print(string.rep(" ", depth) .. k .. ": ", v)
end
end
end
end

View File

@@ -1,343 +1,616 @@
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... local VERSION = "3.12.0"
-- TODO: Implement counting kills and display on whosniffer shared.VERSION = VERSION
-- 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...
-- 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> 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 StinkyTrackerData
---@field dumpTable fun(table: any, depth?: number): nil ---@field agentTracker AgentTrackerData
---@field utf8len fun(input: string): number ---@field networkNodes string[]
---@field padString fun(input: string, targetLength: number, left?: boolean): string ---@field network HeimdallNetworkData
---@field GetOrDefault fun(table: table<any, any>, keys: string[], default: any): any ---@field networkMessenger HeimdallNetworkMessengerData
---@field Whoer { Init: fun() } ---@field stinkyCache HeimdallStinkyCacheData
---@field Messenger { Init: fun() } ---@field _L fun(key: string, locale: string): string
---@field Spotter { Init: fun() } ---@field _Locale Localization
---@field DeathReporter { Init: fun() } ---@field VERSION string
---@field dump fun(table: any, msg?: string, depth?: number): nil
---@field utf8len fun(input: string): number
---@field padString fun(input: string, targetLength: number, left?: boolean): string
---@field GetOrDefault fun(table: table<any, any>, keys: string[], default: any): any
---@field Split fun(input: string, deliminer: string): string[]
---@field IsStinky fun(name: string): boolean
---@field Memoize fun(f: function): function
---@field GetLocaleForChannel fun(channel: string): string
---@field WhoQueryService WhoQueryService
---@field AchievementSniffer AchievementSniffer
---@field AgentTracker AgentTracker
---@field BonkDetector BonkDetector
---@field Bully Bully
---@field CombatAlerter CombatAlerter
---@field Commander Commander
---@field Config Config
---@field Configurator Configurator
---@field DeathReporter DeathReporter
---@field Dueler Dueler
---@field Echoer Echoer
---@field Emoter Emoter
---@field Inviter Inviter
---@field Macroer Macroer
---@field Messenger Messenger
---@field MinimapTagger MinimapTagger
---@field Network Network
---@field NetworkMessenger NetworkMessenger
---@field Noter Noter
---@field Sniffer Sniffer
---@field Spotter Spotter
---@field StinkyCache StinkyCache
---@field StinkyTracker StinkyTracker
---@field Whoer Whoer
---@field ChatSniffer ChatSniffer
--- Config --- --- Config ---
---@class HeimdallConfig ---@class HeimdallConfig
---@field spotter HeimdallSpotterConfig ---@field spotter HeimdallSpotterConfig
---@field who HeimdallWhoConfig ---@field who HeimdallWhoConfig
---@field messenger HeimdallMessengerConfig ---@field messenger HeimdallMessengerConfig
---@field deathReporter HeimdallDeathReporterConfig ---@field deathReporter HeimdallDeathReporterConfig
---@field whisperNotify table<string, string> ---@field inviter HeimdallInviterConfig
---@field stinkies table<string, boolean> ---@field dueler HeimdallDuelerConfig
---@field agentTracker HeimdallAgentTrackerConfig
---@field emoter HeimdallEmoterConfig
---@field echoer HeimdallEchoerConfig
---@field macroer HeimdallMacroerConfig
---@field commander HeimdallCommanderConfig
---@field stinkyTracker HeimdallStinkyTrackerConfig
---@field combatAlerter HeimdallCombatAlerterConfig
---@field sniffer HeimdallSnifferConfig
---@field noter HeimdallNoterConfig
---@field network HeimdallNetworkConfig
---@field networkMessenger HeimdallNetworkMessengerConfig
---@field configurator HeimdallConfiguratorConfig
---@field stinkyCache HeimdallStinkyCacheConfig
---@field achievementSniffer HeimdallAchievementSnifferConfig
---@field chatSniffer HeimdallChatSnifferConfig
---@field whisperNotify table<string, string>
---@field addonPrefix string
---@field stinkies table<string, boolean>
---@field agents table<string, string>
---@field scale number
---@field notes table<string, Note[]>
---@field channelLocale table<string, string>
---@field locale string
---@field debug boolean
---@class HeimdallSpotterConfig shared.GetOrDefault = function(table, keys, default)
---@field enabled boolean local value = default
---@field everyone boolean if not table then return value end
---@field hostile boolean if not keys then return value end
---@field alliance boolean
---@field stinky boolean
---@field notifyChannel string
---@field zoneOverride string?
---@field throttleTime number
---@class HeimdallWhoConfig local traverse = table
---@field enabled boolean for i = 1, #keys do
---@field ignored table<string, boolean> local key = keys[i]
---@field notifyChannel string if traverse[key] ~= nil then
---@field ttl number traverse = traverse[key]
---@field doWhisper boolean else
---@field zoneNotifyFor table<string, boolean> break
end
---@class HeimdallMessengerConfig if i == #keys then value = traverse end
---@field enabled boolean end
return value
end
---@class HeimdallDeathReporterConfig --/run Heimdall_Data.config.who.queries="g-\"БеспредеЛ\"|ally"
---@field enabled boolean Heimdall_Data.config = {
---@field throttle number debug = shared.GetOrDefault(Heimdall_Data, { "config", "debug" }, false),
---@field doWhisper boolean chatSniffer = {
---@field notifyChannel string enabled = shared.GetOrDefault(Heimdall_Data, { "config", "chatSniffer", "enabled" }, false),
---@field zoneOverride string? debug = shared.GetOrDefault(Heimdall_Data, { "config", "chatSniffer", "debug" }, false),
---@field duelThrottle number },
spotter = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "enabled" }, true),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "debug" }, false),
everyone = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "everyone" }, false),
hostile = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "hostile" }, true),
alliance = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "alliance" }, true),
stinky = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "stinky" }, true),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "channels" }, { "Agent" }),
zoneOverride = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "zoneOverride" }, nil),
throttleTime = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "throttleTime" }, 10),
},
who = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "who", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "who", "debug" }, false),
ignored = shared.GetOrDefault(Heimdall_Data, { "config", "who", "ignored" }, {}),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "who", "channels" }, { "Agent" }),
ttl = shared.GetOrDefault(Heimdall_Data, { "config", "who", "ttl" }, 20),
doWhisper = shared.GetOrDefault(Heimdall_Data, { "config", "who", "doWhisper" }, true),
zoneNotifyFor = shared.GetOrDefault(Heimdall_Data, { "config", "who", "zoneNotifyFor" }, {
["Orgrimmar"] = true,
["Thunder Bluff"] = true,
["Undercity"] = true,
["Durotar"] = true,
["Echo Isles"] = true,
["Valley of Trials"] = true,
}),
queries = shared.GetOrDefault(Heimdall_Data, { "config", "who", "queries" }, ""),
},
messenger = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "messenger", "enabled" }, true),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "messenger", "debug" }, false),
interval = shared.GetOrDefault(Heimdall_Data, { "config", "messenger", "interval" }, 0.2),
},
deathReporter = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "debug" }, false),
throttle = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "throttle" }, 10),
doWhisper = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "doWhisper" }, true),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "channels" }, { "Agent" }),
zoneOverride = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "zoneOverride" }, nil),
duelThrottle = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "duelThrottle" }, 5),
},
inviter = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "channels" }, { "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),
listeningChannel = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "listeningChannel" }, {}),
},
dueler = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "dueler", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "dueler", "debug" }, false),
declineOther = shared.GetOrDefault(Heimdall_Data, { "config", "dueler", "declineOther" }, false),
},
bully = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "bully", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "bully", "debug" }, false),
},
agentTracker = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "agentTracker", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "agentTracker", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "agentTracker", "channels" }, { "Agent" }),
},
emoter = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "channels" }, { "Agent" }),
prefix = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "prefix" }, ""),
},
echoer = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "channels" }, { "Agent" }),
prefix = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "prefix" }, ""),
},
macroer = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "macroer", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "macroer", "debug" }, 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),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "commander", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "commander", "channels" }, { "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),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "stinkyTracker", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "stinkyTracker", "channels" }, { "Agent" }),
ignoredTimeout = shared.GetOrDefault(Heimdall_Data, { "config", "stinkyTracker", "ignoredTimeout" }, 600),
},
combatAlerter = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "combatAlerter", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "combatAlerter", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "combatAlerter", "channels" }, { "Agent" }),
},
messageDelegator = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "messageDelegator", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "messageDelegator", "debug" }, false),
delegates = shared.GetOrDefault(Heimdall_Data, { "config", "messageDelegator", "delegates" }, {}),
masterChannel = shared.GetOrDefault(
Heimdall_Data,
{ "config", "messageDelegator", "masterChannel" },
"Agent"
),
},
sniffer = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "channels" }, { "Agent" }),
throttle = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "throttle" }, 10),
zoneOverride = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "zoneOverride" }, nil),
stinky = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "stinky" }, true),
},
minimapTagger = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "channels" }, { "Agent" }),
throttle = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "throttle" }, 10),
scale = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "scale" }, 3),
tagTTL = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "tagTTL" }, 1),
tagSound = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "tagSound" }, false),
tagSoundFile = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "tagSoundFile" },
"MGSSpot.ogg"
),
tagSoundThrottle = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "tagSoundThrottle" }, 0),
tagTextureFile = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "tagTextureFile" },
"Aura4.tga"
),
---
alertTTL = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "alertTTL" }, 1),
alertSound = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "alertSound" }, false),
alertSoundFile = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "alertSoundFile" },
"OOF.ogg"
),
alertSoundThrottle = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "alertSoundThrottle" },
0
),
alertTextureFile = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "alertTextureFile" },
"Aura27.tga"
),
---
combatTTL = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "combatTTL" }, 1),
combatSound = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "combatSound" }, false),
combatSoundFile = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "combatSoundFile" },
"StarScream.ogg"
),
combatSoundThrottle = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "combatSoundThrottle" },
2
),
combatTextureFile = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "combatTextureFile" },
"Aura58.tga"
),
---
helpTTL = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "helpTTL" }, 1),
helpSound = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "helpSound" }, false),
helpSoundFile = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "helpSoundFile" },
"MedicGangsterParadise.ogg"
),
helpSoundThrottle = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "helpSoundThrottle" },
2
),
helpTextureFile = shared.GetOrDefault(
Heimdall_Data,
{ "config", "minimapTagger", "helpTextureFile" },
"Aura68.tga"
),
},
whisperNotify = shared.GetOrDefault(Heimdall_Data, { "config", "whisperNotify" }, {}),
stinkies = shared.GetOrDefault(Heimdall_Data, { "config", "stinkies" }, {}),
notes = shared.GetOrDefault(Heimdall_Data, { "config", "notes" }, {}),
scale = shared.GetOrDefault(Heimdall_Data, { "config", "scale" }, 1),
locale = shared.GetOrDefault(Heimdall_Data, { "config", "locale" }, "en"),
bonkDetector = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "bonkDetector", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "bonkDetector", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "bonkDetector", "channels" }, { "Agent" }),
throttle = shared.GetOrDefault(Heimdall_Data, { "config", "bonkDetector", "throttle" }, 5),
},
noter = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "noter", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "noter", "debug" }, false),
channels = shared.GetOrDefault(Heimdall_Data, { "config", "noter", "channels" }, { "Agent" }),
lastNotes = shared.GetOrDefault(Heimdall_Data, { "config", "noter", "lastNotes" }, 5),
},
network = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "network", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "network", "debug" }, false),
members = shared.GetOrDefault(Heimdall_Data, { "config", "network", "members" }, {}),
updateInterval = shared.GetOrDefault(Heimdall_Data, { "config", "network", "updateInterval" }, 10),
},
networkMessenger = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "networkMessenger", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "networkMessenger", "debug" }, false),
interval = shared.GetOrDefault(Heimdall_Data, { "config", "networkMessenger", "interval" }, 0.01),
},
configurator = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "configurator", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "configurator", "debug" }, false),
},
stinkyCache = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "stinkyCache", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "stinkyCache", "debug" }, false),
commander = shared.GetOrDefault(Heimdall_Data, { "config", "stinkyCache", "commander" }, "Heimdállr"),
ttl = shared.GetOrDefault(Heimdall_Data, { "config", "stinkyCache", "ttl" }, 10),
},
achievementSniffer = {
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "achievementSniffer", "enabled" }, false),
debug = shared.GetOrDefault(Heimdall_Data, { "config", "achievementSniffer", "debug" }, false),
--texture = shared.GetOrDefault(Heimdall_Data, { "config", "achievementSniffer", "texture" }, "Aura53.tga"),
--offsetX = shared.GetOrDefault(Heimdall_Data, { "config", "achievementSniffer", "offsetX" }, 0),
--offsetY = shared.GetOrDefault(Heimdall_Data, { "config", "achievementSniffer", "offsetY" }, 0),
rescan = shared.GetOrDefault(Heimdall_Data, { "config", "achievementSniffer", "rescan" }, false),
scanInterval = shared.GetOrDefault(Heimdall_Data, { "config", "achievementSniffer", "scanInterval" }, 1),
--iconScale = shared.GetOrDefault(Heimdall_Data, { "config", "achievementSniffer", "iconScale" }, 1),
},
addonPrefix = shared.GetOrDefault(Heimdall_Data, { "config", "addonPrefix" }, "HEIMDALL"),
channelLocale = shared.GetOrDefault(Heimdall_Data, { "config", "channelLocale" }, {}),
}
--- Data --- shared.raceMap = {
---@class HeimdallMessengerData ["Orc"] = "Horde",
---@field queue table<string, Message> ["Undead"] = "Horde",
---@field ticker number? ["Tauren"] = "Horde",
["Troll"] = "Horde",
["Blood Elf"] = "Horde",
["Goblin"] = "Horde",
["Human"] = "Alliance",
["Dwarf"] = "Alliance",
["Night Elf"] = "Alliance",
["Gnome"] = "Alliance",
["Draenei"] = "Alliance",
["Worgen"] = "Alliance",
["Vulpera"] = "Horde",
["Nightborne"] = "Horde",
["Zandalari Troll"] = "Horde",
["Kul Tiran"] = "Alliance",
["Dark Iron Dwarf"] = "Alliance",
["Void Elf"] = "Alliance",
["Lightforged Draenei"] = "Alliance",
["Mechagnome"] = "Alliance",
["Mag'har Orc"] = "Horde",
}
---@class HeimdallWhoData shared.classColors = {
---@field updateTicker number? ["Warrior"] = "C69B6D",
---@field whoTicker number? ["Paladin"] = "F48CBA",
---@field ignored table<string, boolean> ["Hunter"] = "AAD372",
["Rogue"] = "FFF468",
["Priest"] = "FFFFFF",
["Death Knight"] = "C41E3A",
["Shaman"] = "0070DD",
["Mage"] = "3FC7EB",
["Warlock"] = "8788EE",
["Monk"] = "00FF98",
["Druid"] = "FF7C0A",
["Demon Hunter"] = "A330C9",
}
data.GetOrDefault = function(table, keys, default) ---@param input string
local value = default ---@return number
if not table then return value end shared.utf8len = function(input)
if not keys then return value end if not input then return 0 end
local len = 0
local i = 1
local n = #input
while i <= n do
local c = input:byte(i)
if c >= 0 and c <= 127 then
i = i + 1
elseif c >= 194 and c <= 223 then
i = i + 2
elseif c >= 224 and c <= 239 then
i = i + 3
elseif c >= 240 and c <= 244 then
i = i + 4
else
i = i + 1
end
len = len + 1
end
return len
end
---@param input string
---@param targetLength number
---@param left boolean
---@return string
shared.padString = function(input, targetLength, left)
left = left or false
local len = shared.utf8len(input)
if len < targetLength then
if left then
input = input .. string.rep(" ", targetLength - len)
else
input = string.rep(" ", targetLength - len) .. input
end
end
return input
end
local traverse = table ---@param input string
for i = 1, #keys do ---@param deliminer string
local key = keys[i] ---@return table<number, string>
if traverse[key] ~= nil then shared.Split = function(input, deliminer)
traverse = traverse[key] if deliminer == nil then deliminer = "%s" end
else local t = {}
break for str in string.gmatch(input, "([^" .. deliminer .. "]+)") do
end table.insert(t, str)
end
return t
end
---@param name string
---@return boolean
shared.IsStinky = function(name)
return Heimdall_Data.config.stinkies[name] ~= nil or shared.StinkyCache[name] ~= nil
end
if i == #keys then ---@param f function
value = traverse ---@return function
end shared.Memoize = function(f)
end local mem = {} -- memoizing table
return value setmetatable(mem, { __mode = "kv" }) -- make it weak
end return function(x) -- new version of 'f', with memoizing
if Heimdall_Data.config.debug then print(string.format("[Heimdall] Memoize %s", tostring(x))) end
local r = mem[x]
if r == nil then -- no previous result?
if Heimdall_Data.config.debug then
print(string.format("[Heimdall] Memoize %s is nil, calling original function", tostring(x)))
end
r = f(x) -- calls original function
if Heimdall_Data.config.debug then
print(string.format("[Heimdall] Memoized result for %s: %s", tostring(x), tostring(r)))
end
mem[x] = r -- store result for reuse
end
if Heimdall_Data.config.debug then
print(string.format("[Heimdall] Memoize %s is %s", tostring(x), tostring(r)))
end
return r
end
end
data.messenger = { ---@param channel string
queue = {} ---@return string
} shared.GetLocaleForChannel = function(channel) return Heimdall_Data.config.channelLocale[channel] or "en" end
data.who = {
ignored = {},
}
--/run Heimdall_Data.config = {who={enabled=true},deathReporter={enabled=true}}
--/run Heimdall_Data.config = {deathReporter={enabled=true}}
--/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 = {
enabled = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "enabled" }, true),
everyone = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "everyone" }, false),
hostile = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "hostile" }, true),
alliance = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "alliance" }, true),
stinky = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "stinky" }, true),
notifyChannel = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "notifyChannel" }, "Agent"),
zoneOverride = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "zoneOverride" }, nil),
throttleTime = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "throttleTime" }, 10)
},
who = {
enabled = data.GetOrDefault(Heimdall_Data, { "config", "who", "enabled" }, false),
ignored = data.GetOrDefault(Heimdall_Data, { "config", "who", "ignored" }, {}),
notifyChannel = data.GetOrDefault(Heimdall_Data, { "config", "who", "notifyChannel" }, "Agent"),
ttl = data.GetOrDefault(Heimdall_Data, { "config", "who", "ttl" }, 10),
doWhisper = data.GetOrDefault(Heimdall_Data, { "config", "who", "doWhisper" }, true),
zoneNotifyFor = data.GetOrDefault(Heimdall_Data, { "config", "who", "zoneNotifyFor" }, {
["Orgrimmar"] = true,
["Thunder Bluff"] = true,
["Undercity"] = true,
["Durotar"] = true,
["Echo Isles"] = true,
["Valley of Trials"] = true,
}),
},
messenger = {
enabled = data.GetOrDefault(Heimdall_Data, { "config", "messenger", "enabled" }, true),
},
deathReporter = {
enabled = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "enabled" }, false),
throttle = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "throttle" }, 10),
doWhisper = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "doWhisper" }, true),
notifyChannel = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "notifyChannel" }, "Agent"),
zoneOverride = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "zoneOverride" }, nil),
duelThrottle = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "duelThrottle" }, 5),
},
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 = { ---@param key string
["Orc"] = "Horde", ---@param locale string
["Undead"] = "Horde", ---@return string
["Tauren"] = "Horde", shared._L = function(key, locale)
["Troll"] = "Horde", local localeTable = shared._Locale[locale]
["Blood Elf"] = "Horde", if not localeTable then
["Goblin"] = "Horde", if Heimdall_Data.config.debug then
["Human"] = "Alliance", print(string.format("[Heimdall] Locale %s not found", tostring(locale)))
["Dwarf"] = "Alliance", end
["Night Elf"] = "Alliance", return key
["Gnome"] = "Alliance", end
["Draenei"] = "Alliance", local value = localeTable[key]
["Worgen"] = "Alliance", if not value then
["Vulpera"] = "Horde", if Heimdall_Data.config.debug then
["Nightborne"] = "Horde", print(string.format("[Heimdall] Key %s not found in locale %s", tostring(key), tostring(locale)))
["Zandalari Troll"] = "Horde", end
["Kul Tiran"] = "Alliance", return key
["Dark Iron Dwarf"] = "Alliance", end
["Void Elf"] = "Alliance", return value
["Lightforged Draenei"] = "Alliance", end
["Mechagnome"] = "Alliance",
["Mag'har Orc"] = "Horde"
}
data.classColors = { shared.Messenger.Init()
["Warrior"] = "C69B6D", shared.StinkyTracker.Init()
["Paladin"] = "F48CBA", shared.AgentTracker.Init()
["Hunter"] = "AAD372", shared.Whoer.Init()
["Rogue"] = "FFF468", shared.Spotter.Init()
["Priest"] = "FFFFFF", shared.DeathReporter.Init()
["Death Knight"] = "C41E3A", shared.Inviter.Init()
["Shaman"] = "0070DD", shared.Dueler.Init()
["Mage"] = "3FC7EB", shared.Bully.Init()
["Warlock"] = "8788EE", shared.Macroer.Init()
["Monk"] = "00FF98", shared.Commander.Init()
["Druid"] = "FF7C0A", shared.CombatAlerter.Init()
["Demon Hunter"] = "A330C9" shared.Config.Init()
} shared.MinimapTagger.Init()
shared.BonkDetector.Init()
---@param input string shared.Sniffer.Init()
---@return number shared.Noter.Init()
data.utf8len = function(input) shared.Network.Init()
if not input then shared.NetworkMessenger.Init()
return 0 shared.Configurator.Init()
end shared.StinkyCache.Init()
local len = 0 shared.AchievementSniffer.Init()
local i = 1 shared.ChatSniffer.Init()
local n = #input print("Heimdall loaded!")
while i <= n do
local c = input:byte(i)
if c >= 0 and c <= 127 then
i = i + 1
elseif c >= 194 and c <= 223 then
i = i + 2
elseif c >= 224 and c <= 239 then
i = i + 3
elseif c >= 240 and c <= 244 then
i = i + 4
else
i = i + 1
end
len = len + 1
end
return len
end
---@param input string
---@param targetLength number
---@param left boolean
---@return string
data.padString = function(input, targetLength, left)
left = left or false
local len = data.utf8len(input)
if len < targetLength then
if left then
input = input .. string.rep(" ", targetLength - len)
else
input = string.rep(" ", targetLength - len) .. input
end
end
return input
end
data.Whoer.Init()
data.Messenger.Init()
data.Spotter.Init()
data.DeathReporter.Init()
print("Heimdall loaded!")
end end
local loadedFrame = CreateFrame("Frame") local loadedFrame = CreateFrame("Frame")
loadedFrame:RegisterEvent("ADDON_LOADED") loadedFrame:RegisterEvent("ADDON_LOADED")
loadedFrame:SetScript("OnEvent", function(self, event, addonName) loadedFrame:SetScript("OnEvent", function(self, event, addonName)
if addonName == addonname then if addonName == addonname then init() end
init()
end
end) end)
local logoutFrame = CreateFrame("Frame") -- Create the import/export frame
logoutFrame:RegisterEvent("PLAYER_LOGOUT") local ccpFrame = CreateFrame("Frame", "CCPFrame", UIParent)
logoutFrame:SetScript("OnEvent", function(self, event) ccpFrame:SetSize(512 * 1.5, 512 * 1.5)
Heimdall_Data.config.stinkies = data.config.stinkies ccpFrame:SetPoint("CENTER")
end) ccpFrame:SetFrameStrata("HIGH")
ccpFrame:EnableMouse(true)
ccpFrame:SetMovable(true)
ccpFrame:SetResizable(false)
ccpFrame:SetBackdrop({
bgFile = "Interface/Tooltips/UI-Tooltip-Background",
edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
tile = true,
tileSize = 4,
edgeSize = 4,
insets = {
left = 4,
right = 4,
top = 4,
bottom = 4,
},
})
ccpFrame:SetBackdropColor(0, 0, 0, 0.8)
ccpFrame:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
SlashCmdList["HEIMDALL_TOGGLE_STINKY"] = function(input) ccpFrame:SetMovable(true)
print("Toggling stinky: " .. tostring(input)) ccpFrame:EnableMouse(true)
if data.config.stinkies[input] then ccpFrame:RegisterForDrag("LeftButton")
data.config.stinkies[input] = nil ccpFrame:SetScript("OnDragStart", function(self) self:StartMoving() end)
else ccpFrame:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
data.config.stinkies[input] = true ccpFrame:SetScript("OnShow", function(self) self:SetScale(1) end)
end ccpFrame:Hide()
print(data.config.stinkies[input])
-- Create scroll frame
local scrollFrame = CreateFrame("ScrollFrame", "CCPFrameScrollFrame", ccpFrame, "UIPanelScrollFrameTemplate")
scrollFrame:SetPoint("TOPLEFT", ccpFrame, "TOPLEFT", 10, -10)
scrollFrame:SetPoint("BOTTOMRIGHT", ccpFrame, "BOTTOMRIGHT", -30, 10)
-- Create the text box
local ccpFrameTextBox = CreateFrame("EditBox", "CCPFrameTextBox", scrollFrame)
ccpFrameTextBox:SetSize(512 * 1.5 - 40, 512 * 1.5 - 20)
ccpFrameTextBox:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 0, 0)
ccpFrameTextBox:SetFont("Fonts\\FRIZQT__.ttf", 12)
ccpFrameTextBox:SetTextColor(1, 1, 1, 1)
ccpFrameTextBox:SetTextInsets(10, 10, 10, 10)
ccpFrameTextBox:SetMultiLine(true)
ccpFrameTextBox:SetAutoFocus(true)
ccpFrameTextBox:SetMaxLetters(1000000)
ccpFrameTextBox:SetScript("OnEscapePressed", function(self) ccpFrame:Hide() end)
-- Set the scroll frame's scroll child
scrollFrame:SetScrollChild(ccpFrameTextBox)
CCP = function(window)
window = window or 1
local charFrame = _G["ChatFrame" .. window]
local maxLines = charFrame:GetNumMessages() or 0
local chat = {}
for i = 1, maxLines do
local currentMsg = charFrame:GetMessageInfo(i)
chat[#chat + 1] = currentMsg
end
ccpFrameTextBox:SetText(table.concat(chat, "\n"))
ccpFrame:Show()
ccpFrameTextBox:SetFocus()
end end
SLASH_HEIMDALL_TOGGLE_STINKY1 = "/has"

View File

@@ -1,14 +1,37 @@
## Interface: 70300 ## Interface: 70300
## Title: Heimdall ## Title: Heimdall
## Version: 3.12.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, Heimdall_Achievements, Heimdall_Chat
#core _L.lua
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/Network.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/MinimapTagger.lua
Modules/Config.lua
Modules/BonkDetector.lua
Modules/Sniffer.lua
Modules/Noter.lua
Modules/NetworkMessenger.lua
Modules/StinkyCache.lua
Modules/Configurator.lua
Modules/AchievementSniffer.lua
Modules/ChatSniffer.lua
Heimdall.lua Heimdall.lua

BIN
Heimdall.zip (Stored with Git 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

1
Meta Submodule

Submodule Meta added at eee043a846

View File

@@ -0,0 +1,304 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "AchievementSniffer"
---@class HeimdallAchievementSnifferConfig
---@field enabled boolean
---@field debug boolean
-----@field texture string
-----@field offsetX number
-----@field offsetY number
---@field rescan boolean
---@field scanInterval number
-----@field iconScale number
-- local HeimdallRoot = "Interface\\AddOns\\Heimdall\\"
-- local TextureRoot = HeimdallRoot .. "Texture\\"
local Achievements = {
15,
958,
1266,
2078,
2141,
2200,
4958,
5456,
5749,
6460,
6753,
7382,
7383,
7384,
8929,
8982,
9017,
9038,
9493,
10059,
10079,
10278,
10657,
10672,
10684,
10688,
10689,
10692,
10693,
10698,
10790,
10875,
11124,
11126,
11127,
11128,
11153,
11157,
11164,
11188,
11189,
11190,
11446,
11473,
11610,
11657,
11658,
11659,
11660,
11674,
11992,
11993,
11994,
11995,
11996,
11997,
11998,
11999,
12000,
12001,
12020,
12026,
12074,
12445,
12447,
12448,
}
---@class AchievementSniffer
shared.AchievementSniffer = {
Init = function()
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Module initialized", ModuleName))
end
local guidMap = {}
---@class AchievementData
---@field id number
---@field date string
---@field completed boolean
---@class Heimdall_Achievements
---@field players table<string, table<number, AchievementData>>
---@field alreadySeen table<string, boolean>
---@field rescan boolean
if not Heimdall_Achievements then Heimdall_Achievements = {} end
if not Heimdall_Achievements.players then Heimdall_Achievements.players = {} end
if not Heimdall_Achievements.alreadySeen then Heimdall_Achievements.alreadySeen = {} end
--local framePool = {}
--for i = 1, 40 do
-- local frame = CreateFrame("Frame", "HeimdallAchievementSnifferNameplate" .. i, UIParent)
-- local texture = frame:CreateTexture(nil, "ARTWORK")
-- texture:SetAllPoints(frame)
-- texture:SetTexture(TextureRoot .. Heimdall_Data.config.achievementSniffer.texture)
-- frame.texture = texture
-- frame:Hide()
-- table.insert(framePool, frame)
--end
---@param name string
---@return boolean
local function ShouldInspect(name)
local should = false
if not Heimdall_Achievements.players[name] then
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Player %s does not have prior achievement data", ModuleName, name))
end
should = true
end
if Heimdall_Achievements.alreadySeen[name] then
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Player %s has already been seen", ModuleName, name))
end
-- Save some memory
Heimdall_Achievements.players[name] = nil
should = false
end
if Heimdall_Data.config.achievementSniffer.rescan then
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Rescan is enabled", ModuleName))
end
should = true
end
return should
end
-- It's not working well AT ALL
-- I don't know how to do it better
-- It simply just does not work...
--local function UpdateFrames()
-- for i, frame in ipairs(framePool) do
-- local unit = "nameplate" .. i
-- if not UnitExists(unit) then
-- --if Heimdall_Data.config.achievementSniffer.debug then
-- -- print(string.format("[%s] Unit %s does not exist, hiding frame", ModuleName, unit))
-- --end
-- frame:Hide()
-- else
-- --local unitFrame = _G[string.format("ElvUI_NamePlate%dHealthBar", i)]
-- local unitFrame = _G[string.format("NamePlate%d", i)]
-- if unitFrame == nil then
-- if Heimdall_Data.config.achievementSniffer.debug then
-- print(string.format("[%s] Unit frame for %s not found", ModuleName, unit))
-- end
-- frame:Hide()
-- else
-- local unitName = UnitName(unit)
-- if Heimdall_Data.config.achievementSniffer.debug then
-- print(string.format("[%s] Unit frame found for %s (%s)", ModuleName, unit, unitName))
-- end
-- frame:Show()
-- frame:SetSize(32, 32)
-- frame:SetFrameStrata("HIGH")
-- frame:SetFrameLevel(100)
-- frame:SetScale(Heimdall_Data.config.achievementSniffer.iconScale)
-- frame.texture:SetTexture(TextureRoot .. Heimdall_Data.config.achievementSniffer.texture)
-- frame:SetPoint("CENTER", unitFrame, "CENTER",
-- Heimdall_Data.config.achievementSniffer.offsetX,
-- Heimdall_Data.config.achievementSniffer.offsetY)
-- frame:SetParent(unitFrame)
-- frame:SetAlpha(1)
-- local exists = ShouldInspect(unitName)
-- if exists then
-- frame.texture:SetVertexColor(1, 0, 0, 1)
-- else
-- frame.texture:SetVertexColor(0, 1, 0, 1)
-- end
-- if Heimdall_Data.config.achievementSniffer.debug then
-- print(string.format("[%s] Frame updated for %s", ModuleName, unitName))
-- end
-- end
-- end
-- end
--end
---@param unit string
local function TryInspect(unit)
local targetPlayer = UnitIsPlayer(unit)
if not targetPlayer then return end
local targetName = UnitName(unit)
local targetGuid = UnitGUID(unit)
guidMap[targetGuid] = targetName
if not ShouldInspect(targetName) then
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Not inspecting player: %s", ModuleName, targetName))
end
return
end
local canInspect = CheckInteractDistance(unit, 1)
if canInspect then
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Inspecting player: %s", ModuleName, targetName))
end
SetAchievementComparisonUnit(unit)
else
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Cannot inspect player (too far?): %s", ModuleName, targetName))
end
end
end
---@param name string
local function Scan(name)
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Scanning achievements for %s", ModuleName, name))
end
Heimdall_Achievements.players[name] = {}
for i, aid in ipairs(Achievements) do
local completed, month, day, year = GetAchievementComparisonInfo(aid)
if completed then
---@type string
local yearstr = "" .. year
if year < 100 then yearstr = "20" .. year end
local date = string.format("%04d-%02d-%02d", yearstr, month, day)
local data = {
id = aid,
date = date,
completed = completed,
}
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Achievement %d completed on %s", ModuleName, aid, date))
end
Heimdall_Achievements.players[name][aid] = data
end
end
--UpdateFrames()
end
local nameplateFrame = CreateFrame("Frame")
nameplateFrame:RegisterEvent("NAME_PLATE_UNIT_ADDED")
nameplateFrame:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
nameplateFrame:SetScript("OnEvent", function(self, event, unit)
if not Heimdall_Data.config.achievementSniffer.enabled then return end
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Event triggered: %s for unit: %s", ModuleName, event, unit))
end
if event == "NAME_PLATE_UNIT_ADDED" then TryInspect(unit) end
--UpdateFrames()
end)
local inspectFrame = CreateFrame("Frame")
inspectFrame:RegisterEvent("INSPECT_ACHIEVEMENT_READY")
inspectFrame:SetScript("OnEvent", function(self, event, guid)
if not Heimdall_Data.config.achievementSniffer.enabled then return end
local name = guidMap[guid]
if not name then
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] No name found for guid: %s", ModuleName, guid))
end
return
end
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Event triggered: %s for player: %s", ModuleName, event, name))
end
Scan(name)
end)
local function Tick()
C_Timer.NewTimer(Heimdall_Data.config.achievementSniffer.scanInterval, Tick)
if not Heimdall_Data.config.achievementSniffer.enabled then return end
if Heimdall_Data.config.achievementSniffer.debug then
print(string.format("[%s] Scanning achievements for everyone on screen", ModuleName))
end
for i = 1, 40 do
local unit = "nameplate" .. i
if UnitExists(unit) then
TryInspect(unit)
--else
-- if Heimdall_Data.config.achievementSniffer.debug then
-- print(string.format("[%s] Unit %s does not exist, nothing to inspect", ModuleName, unit))
-- end
end
end
--UpdateFrames()
end
Tick()
print(string.format("[%s] Module initialized", ModuleName))
end,
}

144
Modules/AgentTracker.lua Normal file
View File

@@ -0,0 +1,144 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "AgentTracker"
---@class AgentTrackerData
---@field agents ReactiveValue<table<string, string>>
---@class HeimdallAgentTrackerConfig
---@field enabled boolean
---@field debug boolean
---@field channels string[]
---@class AgentTracker
shared.AgentTracker = {
---@param name string
---@return boolean
Track = function(name)
if not name then return false end
local exists = shared.AgentTracker.IsAgent(name)
if exists then return false end
shared.agentTracker.agents[name] = date("%Y-%m-%dT%H:%M:%S")
-- Heimdall_Data.config.agents[name] = date("%Y-%m-%dT%H:%M:%S")
if Heimdall_Data.config.agentTracker.debug then
print(string.format("[%s] Tracking new agent: %s", ModuleName, name))
shared.dump(shared.agentTracker.agents)
end
return true
end,
---@param name string
---@return boolean
IsAgent = function(name)
if not name then return false end
return shared.agentTracker.agents[name] ~= nil
end,
---@param callback fun(agent: string)
OnChange = function(callback) shared.agentTracker.agents:onChange(callback) end,
---@param callback fun(agent: string)
ForEach = function(callback)
---@type table<string, string>
local agents = shared.agentTracker.agents:get()
for name, _ in pairs(agents) do
callback(name)
end
end,
---@return nil
Init = function()
shared.agentTracker = {
agents = ReactiveValue.new(Heimdall_Data.config.agents),
}
--/run Heimdall_Data.config.agents["Cyheuraeth"]=date("%Y-%m-%dT%H:%M:%S")
---@type table<string, boolean>
local channelRosterFrame = CreateFrame("Frame")
channelRosterFrame:RegisterEvent("CHANNEL_ROSTER_UPDATE")
channelRosterFrame:SetScript("OnEvent", function(self, event, index)
if Heimdall_Data.config.agentTracker.debug then
print(string.format("[%s] Channel roster update received", ModuleName))
end
if not Heimdall_Data.config.agentTracker.enabled then
if Heimdall_Data.config.agentTracker.debug then
print(string.format("[%s] Module disabled, ignoring roster update", ModuleName))
end
return
end
local name = GetChannelDisplayInfo(index)
if Heimdall_Data.config.agentTracker.debug then
print(string.format("[%s] Processing channel update: %s (index: %d)", ModuleName, name or "nil", index))
end
if name ~= Heimdall_Data.config.agentTracker.masterChannel then
if Heimdall_Data.config.agentTracker.debug then
print(string.format("[%s] Ignoring non-master channel: %s", ModuleName, name or "nil"))
end
return
end
local count = select(5, GetChannelDisplayInfo(index))
if Heimdall_Data.config.agentTracker.debug then
print(string.format("[%s] Processing %d members in channel", ModuleName, count))
end
local newAgents = 0
for i = 1, count do
name = GetChannelRosterInfo(index, i)
shared.AgentTracker.Track(name)
end
if Heimdall_Data.config.agentTracker.debug then
print(string.format("[%s] Roster update complete - Added %d new agents", ModuleName, newAgents))
end
end)
local agentTrackerChannelSniffer = CreateFrame("Frame")
agentTrackerChannelSniffer:RegisterEvent("CHAT_MSG_CHANNEL")
agentTrackerChannelSniffer:SetScript("OnEvent", function(self, event, msg, sender, ...)
-- if Heimdall_Data.config.agentTracker.debug then
-- print(string.format("[%s] Channel message received from: %s", ModuleName, sender))
-- end
if not Heimdall_Data.config.agentTracker.enabled then
-- if Heimdall_Data.config.agentTracker.debug then
-- print(string.format("[%s] Module disabled, ignoring channel message", ModuleName))
-- end
return
end
local channelId = select(6, ...)
local _, channelname = GetChannelName(channelId)
local ok = false
for _, channel in pairs(Heimdall_Data.config.agentTracker.channels) do
if channel == channelname then
ok = true
break
end
end
if not ok then
if Heimdall_Data.config.agentTracker.debug then
print(string.format("[%s] Channel name does not match any of the channels", ModuleName))
end
return
end
if Heimdall_Data.config.agentTracker.debug then
print(string.format("[%s] Processing message from master channel: %s", ModuleName, sender))
end
sender = string.match(sender, "^[^-]+")
local new = shared.AgentTracker.Track(sender)
if Heimdall_Data.config.agentTracker.debug then
print(
string.format(
"[%s] %s agent from message: %s",
ModuleName,
new and "Added new" or "Updated existing",
sender
)
)
end
end)
if Heimdall_Data.config.agentTracker.debug then
print(string.format("[%s] Module initialized", ModuleName))
shared.dump(shared.agentTracker.agents:get(), "Agents")
end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

142
Modules/BonkDetector.lua Normal file
View File

@@ -0,0 +1,142 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "BonkDetector"
---@class HeimdallBonkDetectorConfig
---@field enabled boolean
---@field debug boolean
---@field channels string[]
---@field throttle number
---@class BonkDetector
shared.BonkDetector = {
---@return nil
Init = function()
---@type table<string, number>
local lastReportTime = {}
local frame = CreateFrame("Frame")
frame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
frame:SetScript("OnEvent", function(self, event, ...)
-- if Heimdall_Data.config.bonkDetector.debug then
-- print(string.format("[%s] Combat log event received", ModuleName))
-- end
if not Heimdall_Data.config.bonkDetector.enabled then
-- if Heimdall_Data.config.bonkDetector.debug then
-- print(string.format("[%s] Module disabled, ignoring combat event", ModuleName))
-- end
return
end
local subevent = select(2, ...)
if not subevent:find("_DAMAGE") then
if Heimdall_Data.config.bonkDetector.debug then
print(string.format("[%s] Not a damage event, ignoring: %s", ModuleName, subevent))
end
return
end
---@type string|nil, string, string, string, string
local err, source, sourceGUID, destination, destinationGUID
source, err = CLEUParser.GetSourceName(...)
if err then
if Heimdall_Data.config.bonkDetector.debug then
print(string.format("[%s] Error getting source name: %s", ModuleName, err))
end
return
end
sourceGUID, err = CLEUParser.GetSourceGUID(...)
if err then
if Heimdall_Data.config.bonkDetector.debug then
print(string.format("[%s] Error getting source GUID: %s", ModuleName, err))
end
return
end
if not string.find(sourceGUID, "Player") then
if Heimdall_Data.config.bonkDetector.debug then
print(string.format("[%s] Source %s is not a player, nothing to do", ModuleName, source))
end
return
end
destination, err = CLEUParser.GetDestName(...)
if err then
if Heimdall_Data.config.bonkDetector.debug then
print(string.format("[%s] Error getting destination name: %s", ModuleName, err))
end
return
end
destinationGUID, err = CLEUParser.GetDestGUID(...)
if err then
if Heimdall_Data.config.bonkDetector.debug then
print(string.format("[%s] Error getting destination GUID: %s", ModuleName, err))
end
return
end
if not string.find(destinationGUID, "Player") then
if Heimdall_Data.config.bonkDetector.debug then
print(string.format("[%s] Destination %s is not a player, nothing to do", ModuleName, destination))
end
return
end
if source == destination then
if Heimdall_Data.config.bonkDetector.debug then
print(string.format("[%s] Source and destination are the same, ignoring event", ModuleName))
end
return
end
local currentTime = GetTime()
local throttle = Heimdall_Data.config.bonkDetector.throttle
if lastReportTime[source] and (currentTime - lastReportTime[source]) < throttle then
if Heimdall_Data.config.bonkDetector.debug then
local timeLeft = throttle - (currentTime - lastReportTime[source])
print(
string.format(
"[%s] Damage report throttled for %s (%.1f seconds remaining)",
ModuleName,
source,
timeLeft
)
)
end
return
end
lastReportTime[source] = currentTime
if Heimdall_Data.config.bonkDetector.debug then
print(
string.format(
"[%s] Processing damage event - Source: %s, Target: %s, Type: %s",
ModuleName,
source,
destination,
subevent
)
)
end
for _, channel in pairs(Heimdall_Data.config.bonkDetector.channels) do
local locale = shared.GetLocaleForChannel(channel)
local msg = string.format(shared._L("bonkDetected", locale), source, destination, subevent)
---@type Message
local message = {
channel = "C",
data = channel,
message = msg,
}
if Heimdall_Data.config.bonkDetector.debug then
print(string.format("[%s] Queuing bonk detector message", ModuleName))
shared.dump(message)
end
table.insert(shared.messenger.queue, message)
end
end)
print(string.format("[%s] Module initialized", ModuleName))
end,
}

16
Modules/Bully.lua Normal file
View File

@@ -0,0 +1,16 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Bully"
---@class HeimdallBullyConfig
---@field enabled boolean
---@field debug boolean
---@class Bully
shared.Bully = {
---@return nil
Init = function()
if Heimdall_Data.config.bully.debug then print(string.format("[%s] Module initialized", ModuleName)) end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

View File

@@ -12,12 +12,12 @@ local function Init()
["destGUID"] = 8, ["destGUID"] = 8,
["destName"] = 9, ["destName"] = 9,
["destFlags"] = 10, ["destFlags"] = 10,
["destRaidFlags"] = 11 ["destRaidFlags"] = 11,
}, },
["GENERIC_SPELL"] = { ["GENERIC_SPELL"] = {
["spellId"] = 12, ["spellId"] = 12,
["spellName"] = 13, ["spellName"] = 13,
["spellSchool"] = 14 ["spellSchool"] = 14,
}, },
["GENERIC_DAMAGE"] = { ["GENERIC_DAMAGE"] = {
["amount"] = 15, ["amount"] = 15,
@@ -29,19 +29,19 @@ local function Init()
["critical"] = 21, ["critical"] = 21,
["glancing"] = 22, ["glancing"] = 22,
["crushing"] = 23, ["crushing"] = 23,
["isOffHand"] = 24 ["isOffHand"] = 24,
}, },
["GENERIC_MISSED"] = { ["GENERIC_MISSED"] = {
["missType"] = 15, ["missType"] = 15,
["isOffHand"] = 16, ["isOffHand"] = 16,
["amountMissed"] = 17, ["amountMissed"] = 17,
["critical"] = 18 ["critical"] = 18,
}, },
["GENERIC_HEAL"] = { ["GENERIC_HEAL"] = {
["amount"] = 15, ["amount"] = 15,
["overhealing"] = 16, ["overhealing"] = 16,
["absorbed"] = 17, ["absorbed"] = 17,
["critical"] = 18 ["critical"] = 18,
}, },
["GENERIC_HEAL_ABSORBED"] = { ["GENERIC_HEAL_ABSORBED"] = {
["extraGUID"] = 15, ["extraGUID"] = 15,
@@ -52,44 +52,44 @@ local function Init()
["extraSpellName"] = 20, ["extraSpellName"] = 20,
["extraSchool"] = 21, ["extraSchool"] = 21,
["absorbedAmount"] = 22, ["absorbedAmount"] = 22,
["totalAmount"] = 23 ["totalAmount"] = 23,
}, },
["GENERIC_ENERGIZE"] = { ["GENERIC_ENERGIZE"] = {
["amount"] = 15, ["amount"] = 15,
["overEnergize"] = 16, ["overEnergize"] = 16,
["powerType"] = 17 ["powerType"] = 17,
}, },
["GENERIC_DRAIN"] = { ["GENERIC_DRAIN"] = {
["amount"] = 15, ["amount"] = 15,
["powerType"] = 16, ["powerType"] = 16,
["extraAmount"] = 17 ["extraAmount"] = 17,
}, },
["GENERIC_LEECH"] = { ["GENERIC_LEECH"] = {
["amount"] = 15, ["amount"] = 15,
["powerType"] = 16, ["powerType"] = 16,
["extraAmount"] = 17 ["extraAmount"] = 17,
}, },
["GENERIC_INTERRUPT"] = { ["GENERIC_INTERRUPT"] = {
["extraSpellId"] = 15, ["extraSpellId"] = 15,
["extraSpellName"] = 16, ["extraSpellName"] = 16,
["extraSchool"] = 17 ["extraSchool"] = 17,
}, },
["GENERIC_DISPEL"] = { ["GENERIC_DISPEL"] = {
["extraSpellId"] = 15, ["extraSpellId"] = 15,
["extraSpellName"] = 16, ["extraSpellName"] = 16,
["extraSchool"] = 17, ["extraSchool"] = 17,
["auraType"] = 18 ["auraType"] = 18,
}, },
["GENERIC_DISPEL_FAILED"] = { ["GENERIC_DISPEL_FAILED"] = {
["extraSpellId"] = 15, ["extraSpellId"] = 15,
["extraSpellName"] = 16, ["extraSpellName"] = 16,
["extraSchool"] = 17 ["extraSchool"] = 17,
}, },
["GENERIC_STOLEN"] = { ["GENERIC_STOLEN"] = {
["extraSpellId"] = 15, ["extraSpellId"] = 15,
["extraSpellName"] = 16, ["extraSpellName"] = 16,
["extraSchool"] = 17, ["extraSchool"] = 17,
["auraType"] = 18 ["auraType"] = 18,
}, },
["GENERIC_EXTRA_ATTACKS"] = { ["amount"] = 15 }, ["GENERIC_EXTRA_ATTACKS"] = { ["amount"] = 15 },
["GENERIC_AURA_APPLIED"] = { ["auraType"] = 15, ["amount"] = 16 }, ["GENERIC_AURA_APPLIED"] = { ["auraType"] = 15, ["amount"] = 16 },
@@ -102,38 +102,32 @@ local function Init()
["extraSpellId"] = 15, ["extraSpellId"] = 15,
["extraSpellName"] = 16, ["extraSpellName"] = 16,
["extraSchool"] = 17, ["extraSchool"] = 17,
["auraType"] = 18 ["auraType"] = 18,
}, },
["GENERIC_CAST_START"] = {}, ["GENERIC_CAST_START"] = {},
["GENERIC_CAST_SUCCESS"] = {}, ["GENERIC_CAST_SUCCESS"] = {},
["GENERIC_CAST_FAILED"] = {} ["GENERIC_CAST_FAILED"] = {},
} }
CLEUEventInfo["SWING_DAMAGE"] = CLEUEventInfo["GENERIC_DAMAGE"] CLEUEventInfo["SWING_DAMAGE"] = CLEUEventInfo["GENERIC_DAMAGE"]
CLEUEventInfo["SWING_MISSED"] = CLEUEventInfo["GENERIC_MISSED"] CLEUEventInfo["SWING_MISSED"] = CLEUEventInfo["GENERIC_MISSED"]
CLEUEventInfo["SWING_HEAL"] = CLEUEventInfo["GENERIC_HEAL"] CLEUEventInfo["SWING_HEAL"] = CLEUEventInfo["GENERIC_HEAL"]
CLEUEventInfo["SWING_HEAL_ABSORBED"] = CLEUEventInfo["SWING_HEAL_ABSORBED"] = CLEUEventInfo["GENERIC_HEAL_ABSORBED"]
CLEUEventInfo["GENERIC_HEAL_ABSORBED"]
CLEUEventInfo["SWING_ENERGIZE"] = CLEUEventInfo["GENERIC_ENERGIZE"] CLEUEventInfo["SWING_ENERGIZE"] = CLEUEventInfo["GENERIC_ENERGIZE"]
CLEUEventInfo["SWING_DRAIN"] = CLEUEventInfo["GENERIC_DRAIN"] CLEUEventInfo["SWING_DRAIN"] = CLEUEventInfo["GENERIC_DRAIN"]
CLEUEventInfo["SWING_LEECH"] = CLEUEventInfo["GENERIC_LEECH"] CLEUEventInfo["SWING_LEECH"] = CLEUEventInfo["GENERIC_LEECH"]
CLEUEventInfo["SWING_INTERRUPT"] = CLEUEventInfo["GENERIC_INTERRUPT"] CLEUEventInfo["SWING_INTERRUPT"] = CLEUEventInfo["GENERIC_INTERRUPT"]
CLEUEventInfo["SWING_DISPEL"] = CLEUEventInfo["GENERIC_DISPEL"] CLEUEventInfo["SWING_DISPEL"] = CLEUEventInfo["GENERIC_DISPEL"]
CLEUEventInfo["SWING_DISPEL_FAILED"] = CLEUEventInfo["SWING_DISPEL_FAILED"] = CLEUEventInfo["GENERIC_DISPEL_FAILED"]
CLEUEventInfo["GENERIC_DISPEL_FAILED"]
CLEUEventInfo["SWING_STOLEN"] = CLEUEventInfo["GENERIC_STOLEN"] CLEUEventInfo["SWING_STOLEN"] = CLEUEventInfo["GENERIC_STOLEN"]
CLEUEventInfo["SWING_EXTRA_ATTACKS"] = CLEUEventInfo["SWING_EXTRA_ATTACKS"] = CLEUEventInfo["GENERIC_EXTRA_ATTACKS"]
CLEUEventInfo["GENERIC_EXTRA_ATTACKS"]
CLEUEventInfo["SWING_AURA_APPLIED"] = CLEUEventInfo["GENERIC_AURA_APPLIED"] CLEUEventInfo["SWING_AURA_APPLIED"] = CLEUEventInfo["GENERIC_AURA_APPLIED"]
CLEUEventInfo["SWING_AURA_REMOVED"] = CLEUEventInfo["GENERIC_AURA_REMOVED"] CLEUEventInfo["SWING_AURA_REMOVED"] = CLEUEventInfo["GENERIC_AURA_REMOVED"]
CLEUEventInfo["SWING_AURA_APPLIED_DOSE"] = CLEUEventInfo["SWING_AURA_APPLIED_DOSE"] = CLEUEventInfo["GENERIC_AURA_APPLIED_DOSE"]
CLEUEventInfo["GENERIC_AURA_APPLIED_DOSE"] CLEUEventInfo["SWING_AURA_REMOVED_DOSE"] = CLEUEventInfo["GENERIC_AURA_REMOVED_DOSE"]
CLEUEventInfo["SWING_AURA_REMOVED_DOSE"] =
CLEUEventInfo["GENERIC_AURA_REMOVED_DOSE"]
CLEUEventInfo["SWING_AURA_REFRESH"] = CLEUEventInfo["GENERIC_AURA_REFRESH"] CLEUEventInfo["SWING_AURA_REFRESH"] = CLEUEventInfo["GENERIC_AURA_REFRESH"]
CLEUEventInfo["SWING_AURA_BROKEN"] = CLEUEventInfo["GENERIC_AURA_BROKEN"] CLEUEventInfo["SWING_AURA_BROKEN"] = CLEUEventInfo["GENERIC_AURA_BROKEN"]
CLEUEventInfo["SWING_AURA_BROKEN_SPELL"] = CLEUEventInfo["SWING_AURA_BROKEN_SPELL"] = CLEUEventInfo["GENERIC_AURA_BROKEN_SPELL"]
CLEUEventInfo["GENERIC_AURA_BROKEN_SPELL"]
CLEUEventInfo["SWING_CAST_START"] = CLEUEventInfo["GENERIC_CAST_START"] CLEUEventInfo["SWING_CAST_START"] = CLEUEventInfo["GENERIC_CAST_START"]
CLEUEventInfo["SWING_CAST_SUCCESS"] = CLEUEventInfo["GENERIC_CAST_SUCCESS"] CLEUEventInfo["SWING_CAST_SUCCESS"] = CLEUEventInfo["GENERIC_CAST_SUCCESS"]
CLEUEventInfo["SWING_CAST_FAILED"] = CLEUEventInfo["GENERIC_CAST_FAILED"] CLEUEventInfo["SWING_CAST_FAILED"] = CLEUEventInfo["GENERIC_CAST_FAILED"]
@@ -141,28 +135,22 @@ local function Init()
CLEUEventInfo["RANGE_DAMAGE"] = CLEUEventInfo["GENERIC_DAMAGE"] CLEUEventInfo["RANGE_DAMAGE"] = CLEUEventInfo["GENERIC_DAMAGE"]
CLEUEventInfo["RANGE_MISSED"] = CLEUEventInfo["GENERIC_MISSED"] CLEUEventInfo["RANGE_MISSED"] = CLEUEventInfo["GENERIC_MISSED"]
CLEUEventInfo["RANGE_HEAL"] = CLEUEventInfo["GENERIC_HEAL"] CLEUEventInfo["RANGE_HEAL"] = CLEUEventInfo["GENERIC_HEAL"]
CLEUEventInfo["RANGE_HEAL_ABSORBED"] = CLEUEventInfo["RANGE_HEAL_ABSORBED"] = CLEUEventInfo["GENERIC_HEAL_ABSORBED"]
CLEUEventInfo["GENERIC_HEAL_ABSORBED"]
CLEUEventInfo["RANGE_ENERGIZE"] = CLEUEventInfo["GENERIC_ENERGIZE"] CLEUEventInfo["RANGE_ENERGIZE"] = CLEUEventInfo["GENERIC_ENERGIZE"]
CLEUEventInfo["RANGE_DRAIN"] = CLEUEventInfo["GENERIC_DRAIN"] CLEUEventInfo["RANGE_DRAIN"] = CLEUEventInfo["GENERIC_DRAIN"]
CLEUEventInfo["RANGE_LEECH"] = CLEUEventInfo["GENERIC_LEECH"] CLEUEventInfo["RANGE_LEECH"] = CLEUEventInfo["GENERIC_LEECH"]
CLEUEventInfo["RANGE_INTERRUPT"] = CLEUEventInfo["GENERIC_INTERRUPT"] CLEUEventInfo["RANGE_INTERRUPT"] = CLEUEventInfo["GENERIC_INTERRUPT"]
CLEUEventInfo["RANGE_DISPEL"] = CLEUEventInfo["GENERIC_DISPEL"] CLEUEventInfo["RANGE_DISPEL"] = CLEUEventInfo["GENERIC_DISPEL"]
CLEUEventInfo["RANGE_DISPEL_FAILED"] = CLEUEventInfo["RANGE_DISPEL_FAILED"] = CLEUEventInfo["GENERIC_DISPEL_FAILED"]
CLEUEventInfo["GENERIC_DISPEL_FAILED"]
CLEUEventInfo["RANGE_STOLEN"] = CLEUEventInfo["GENERIC_STOLEN"] CLEUEventInfo["RANGE_STOLEN"] = CLEUEventInfo["GENERIC_STOLEN"]
CLEUEventInfo["RANGE_EXTRA_ATTACKS"] = CLEUEventInfo["RANGE_EXTRA_ATTACKS"] = CLEUEventInfo["GENERIC_EXTRA_ATTACKS"]
CLEUEventInfo["GENERIC_EXTRA_ATTACKS"]
CLEUEventInfo["RANGE_AURA_APPLIED"] = CLEUEventInfo["GENERIC_AURA_APPLIED"] CLEUEventInfo["RANGE_AURA_APPLIED"] = CLEUEventInfo["GENERIC_AURA_APPLIED"]
CLEUEventInfo["RANGE_AURA_REMOVED"] = CLEUEventInfo["GENERIC_AURA_REMOVED"] CLEUEventInfo["RANGE_AURA_REMOVED"] = CLEUEventInfo["GENERIC_AURA_REMOVED"]
CLEUEventInfo["RANGE_AURA_APPLIED_DOSE"] = CLEUEventInfo["RANGE_AURA_APPLIED_DOSE"] = CLEUEventInfo["GENERIC_AURA_APPLIED_DOSE"]
CLEUEventInfo["GENERIC_AURA_APPLIED_DOSE"] CLEUEventInfo["RANGE_AURA_REMOVED_DOSE"] = CLEUEventInfo["GENERIC_AURA_REMOVED_DOSE"]
CLEUEventInfo["RANGE_AURA_REMOVED_DOSE"] =
CLEUEventInfo["GENERIC_AURA_REMOVED_DOSE"]
CLEUEventInfo["RANGE_AURA_REFRESH"] = CLEUEventInfo["GENERIC_AURA_REFRESH"] CLEUEventInfo["RANGE_AURA_REFRESH"] = CLEUEventInfo["GENERIC_AURA_REFRESH"]
CLEUEventInfo["RANGE_AURA_BROKEN"] = CLEUEventInfo["GENERIC_AURA_BROKEN"] CLEUEventInfo["RANGE_AURA_BROKEN"] = CLEUEventInfo["GENERIC_AURA_BROKEN"]
CLEUEventInfo["RANGE_AURA_BROKEN_SPELL"] = CLEUEventInfo["RANGE_AURA_BROKEN_SPELL"] = CLEUEventInfo["GENERIC_AURA_BROKEN_SPELL"]
CLEUEventInfo["GENERIC_AURA_BROKEN_SPELL"]
CLEUEventInfo["RANGE_CAST_START"] = CLEUEventInfo["GENERIC_CAST_START"] CLEUEventInfo["RANGE_CAST_START"] = CLEUEventInfo["GENERIC_CAST_START"]
CLEUEventInfo["RANGE_CAST_SUCCESS"] = CLEUEventInfo["GENERIC_CAST_SUCCESS"] CLEUEventInfo["RANGE_CAST_SUCCESS"] = CLEUEventInfo["GENERIC_CAST_SUCCESS"]
CLEUEventInfo["RANGE_CAST_FAILED"] = CLEUEventInfo["GENERIC_CAST_FAILED"] CLEUEventInfo["RANGE_CAST_FAILED"] = CLEUEventInfo["GENERIC_CAST_FAILED"]
@@ -170,28 +158,22 @@ local function Init()
CLEUEventInfo["SPELL_DAMAGE"] = CLEUEventInfo["GENERIC_DAMAGE"] CLEUEventInfo["SPELL_DAMAGE"] = CLEUEventInfo["GENERIC_DAMAGE"]
CLEUEventInfo["SPELL_MISSED"] = CLEUEventInfo["GENERIC_MISSED"] CLEUEventInfo["SPELL_MISSED"] = CLEUEventInfo["GENERIC_MISSED"]
CLEUEventInfo["SPELL_HEAL"] = CLEUEventInfo["GENERIC_HEAL"] CLEUEventInfo["SPELL_HEAL"] = CLEUEventInfo["GENERIC_HEAL"]
CLEUEventInfo["SPELL_HEAL_ABSORBED"] = CLEUEventInfo["SPELL_HEAL_ABSORBED"] = CLEUEventInfo["GENERIC_HEAL_ABSORBED"]
CLEUEventInfo["GENERIC_HEAL_ABSORBED"]
CLEUEventInfo["SPELL_ENERGIZE"] = CLEUEventInfo["GENERIC_ENERGIZE"] CLEUEventInfo["SPELL_ENERGIZE"] = CLEUEventInfo["GENERIC_ENERGIZE"]
CLEUEventInfo["SPELL_DRAIN"] = CLEUEventInfo["GENERIC_DRAIN"] CLEUEventInfo["SPELL_DRAIN"] = CLEUEventInfo["GENERIC_DRAIN"]
CLEUEventInfo["SPELL_LEECH"] = CLEUEventInfo["GENERIC_LEECH"] CLEUEventInfo["SPELL_LEECH"] = CLEUEventInfo["GENERIC_LEECH"]
CLEUEventInfo["SPELL_INTERRUPT"] = CLEUEventInfo["GENERIC_INTERRUPT"] CLEUEventInfo["SPELL_INTERRUPT"] = CLEUEventInfo["GENERIC_INTERRUPT"]
CLEUEventInfo["SPELL_DISPEL"] = CLEUEventInfo["GENERIC_DISPEL"] CLEUEventInfo["SPELL_DISPEL"] = CLEUEventInfo["GENERIC_DISPEL"]
CLEUEventInfo["SPELL_DISPEL_FAILED"] = CLEUEventInfo["SPELL_DISPEL_FAILED"] = CLEUEventInfo["GENERIC_DISPEL_FAILED"]
CLEUEventInfo["GENERIC_DISPEL_FAILED"]
CLEUEventInfo["SPELL_STOLEN"] = CLEUEventInfo["GENERIC_STOLEN"] CLEUEventInfo["SPELL_STOLEN"] = CLEUEventInfo["GENERIC_STOLEN"]
CLEUEventInfo["SPELL_EXTRA_ATTACKS"] = CLEUEventInfo["SPELL_EXTRA_ATTACKS"] = CLEUEventInfo["GENERIC_EXTRA_ATTACKS"]
CLEUEventInfo["GENERIC_EXTRA_ATTACKS"]
CLEUEventInfo["SPELL_AURA_APPLIED"] = CLEUEventInfo["GENERIC_AURA_APPLIED"] CLEUEventInfo["SPELL_AURA_APPLIED"] = CLEUEventInfo["GENERIC_AURA_APPLIED"]
CLEUEventInfo["SPELL_AURA_REMOVED"] = CLEUEventInfo["GENERIC_AURA_REMOVED"] CLEUEventInfo["SPELL_AURA_REMOVED"] = CLEUEventInfo["GENERIC_AURA_REMOVED"]
CLEUEventInfo["SPELL_AURA_APPLIED_DOSE"] = CLEUEventInfo["SPELL_AURA_APPLIED_DOSE"] = CLEUEventInfo["GENERIC_AURA_APPLIED_DOSE"]
CLEUEventInfo["GENERIC_AURA_APPLIED_DOSE"] CLEUEventInfo["SPELL_AURA_REMOVED_DOSE"] = CLEUEventInfo["GENERIC_AURA_REMOVED_DOSE"]
CLEUEventInfo["SPELL_AURA_REMOVED_DOSE"] =
CLEUEventInfo["GENERIC_AURA_REMOVED_DOSE"]
CLEUEventInfo["SPELL_AURA_REFRESH"] = CLEUEventInfo["GENERIC_AURA_REFRESH"] CLEUEventInfo["SPELL_AURA_REFRESH"] = CLEUEventInfo["GENERIC_AURA_REFRESH"]
CLEUEventInfo["SPELL_AURA_BROKEN"] = CLEUEventInfo["GENERIC_AURA_BROKEN"] CLEUEventInfo["SPELL_AURA_BROKEN"] = CLEUEventInfo["GENERIC_AURA_BROKEN"]
CLEUEventInfo["SPELL_AURA_BROKEN_SPELL"] = CLEUEventInfo["SPELL_AURA_BROKEN_SPELL"] = CLEUEventInfo["GENERIC_AURA_BROKEN_SPELL"]
CLEUEventInfo["GENERIC_AURA_BROKEN_SPELL"]
CLEUEventInfo["SPELL_CAST_START"] = CLEUEventInfo["GENERIC_CAST_START"] CLEUEventInfo["SPELL_CAST_START"] = CLEUEventInfo["GENERIC_CAST_START"]
CLEUEventInfo["SPELL_CAST_SUCCESS"] = CLEUEventInfo["GENERIC_CAST_SUCCESS"] CLEUEventInfo["SPELL_CAST_SUCCESS"] = CLEUEventInfo["GENERIC_CAST_SUCCESS"]
CLEUEventInfo["SPELL_CAST_FAILED"] = CLEUEventInfo["GENERIC_CAST_FAILED"] CLEUEventInfo["SPELL_CAST_FAILED"] = CLEUEventInfo["GENERIC_CAST_FAILED"]
@@ -199,39 +181,25 @@ local function Init()
CLEUEventInfo["SPELL_PERIODIC_DAMAGE"] = CLEUEventInfo["GENERIC_DAMAGE"] CLEUEventInfo["SPELL_PERIODIC_DAMAGE"] = CLEUEventInfo["GENERIC_DAMAGE"]
CLEUEventInfo["SPELL_PERIODIC_MISSED"] = CLEUEventInfo["GENERIC_MISSED"] CLEUEventInfo["SPELL_PERIODIC_MISSED"] = CLEUEventInfo["GENERIC_MISSED"]
CLEUEventInfo["SPELL_PERIODIC_HEAL"] = CLEUEventInfo["GENERIC_HEAL"] CLEUEventInfo["SPELL_PERIODIC_HEAL"] = CLEUEventInfo["GENERIC_HEAL"]
CLEUEventInfo["SPELL_PERIODIC_HEAL_ABSORBED"] = CLEUEventInfo["SPELL_PERIODIC_HEAL_ABSORBED"] = CLEUEventInfo["GENERIC_HEAL_ABSORBED"]
CLEUEventInfo["GENERIC_HEAL_ABSORBED"]
CLEUEventInfo["SPELL_PERIODIC_ENERGIZE"] = CLEUEventInfo["GENERIC_ENERGIZE"] CLEUEventInfo["SPELL_PERIODIC_ENERGIZE"] = CLEUEventInfo["GENERIC_ENERGIZE"]
CLEUEventInfo["SPELL_PERIODIC_DRAIN"] = CLEUEventInfo["GENERIC_DRAIN"] CLEUEventInfo["SPELL_PERIODIC_DRAIN"] = CLEUEventInfo["GENERIC_DRAIN"]
CLEUEventInfo["SPELL_PERIODIC_LEECH"] = CLEUEventInfo["GENERIC_LEECH"] CLEUEventInfo["SPELL_PERIODIC_LEECH"] = CLEUEventInfo["GENERIC_LEECH"]
CLEUEventInfo["SPELL_PERIODIC_INTERRUPT"] = CLEUEventInfo["SPELL_PERIODIC_INTERRUPT"] = CLEUEventInfo["GENERIC_INTERRUPT"]
CLEUEventInfo["GENERIC_INTERRUPT"]
CLEUEventInfo["SPELL_PERIODIC_DISPEL"] = CLEUEventInfo["GENERIC_DISPEL"] CLEUEventInfo["SPELL_PERIODIC_DISPEL"] = CLEUEventInfo["GENERIC_DISPEL"]
CLEUEventInfo["SPELL_PERIODIC_DISPEL_FAILED"] = CLEUEventInfo["SPELL_PERIODIC_DISPEL_FAILED"] = CLEUEventInfo["GENERIC_DISPEL_FAILED"]
CLEUEventInfo["GENERIC_DISPEL_FAILED"]
CLEUEventInfo["SPELL_PERIODIC_STOLEN"] = CLEUEventInfo["GENERIC_STOLEN"] CLEUEventInfo["SPELL_PERIODIC_STOLEN"] = CLEUEventInfo["GENERIC_STOLEN"]
CLEUEventInfo["SPELL_PERIODIC_EXTRA_ATTACKS"] = CLEUEventInfo["SPELL_PERIODIC_EXTRA_ATTACKS"] = CLEUEventInfo["GENERIC_EXTRA_ATTACKS"]
CLEUEventInfo["GENERIC_EXTRA_ATTACKS"] CLEUEventInfo["SPELL_PERIODIC_AURA_APPLIED"] = CLEUEventInfo["GENERIC_AURA_APPLIED"]
CLEUEventInfo["SPELL_PERIODIC_AURA_APPLIED"] = CLEUEventInfo["SPELL_PERIODIC_AURA_REMOVED"] = CLEUEventInfo["GENERIC_AURA_REMOVED"]
CLEUEventInfo["GENERIC_AURA_APPLIED"] CLEUEventInfo["SPELL_PERIODIC_AURA_APPLIED_DOSE"] = CLEUEventInfo["GENERIC_AURA_APPLIED_DOSE"]
CLEUEventInfo["SPELL_PERIODIC_AURA_REMOVED"] = CLEUEventInfo["SPELL_PERIODIC_AURA_REMOVED_DOSE"] = CLEUEventInfo["GENERIC_AURA_REMOVED_DOSE"]
CLEUEventInfo["GENERIC_AURA_REMOVED"] CLEUEventInfo["SPELL_PERIODIC_AURA_REFRESH"] = CLEUEventInfo["GENERIC_AURA_REFRESH"]
CLEUEventInfo["SPELL_PERIODIC_AURA_APPLIED_DOSE"] = CLEUEventInfo["SPELL_PERIODIC_AURA_BROKEN"] = CLEUEventInfo["GENERIC_AURA_BROKEN"]
CLEUEventInfo["GENERIC_AURA_APPLIED_DOSE"] CLEUEventInfo["SPELL_PERIODIC_AURA_BROKEN_SPELL"] = CLEUEventInfo["GENERIC_AURA_BROKEN_SPELL"]
CLEUEventInfo["SPELL_PERIODIC_AURA_REMOVED_DOSE"] = CLEUEventInfo["SPELL_PERIODIC_CAST_START"] = CLEUEventInfo["GENERIC_CAST_START"]
CLEUEventInfo["GENERIC_AURA_REMOVED_DOSE"] CLEUEventInfo["SPELL_PERIODIC_CAST_SUCCESS"] = CLEUEventInfo["GENERIC_CAST_SUCCESS"]
CLEUEventInfo["SPELL_PERIODIC_AURA_REFRESH"] = CLEUEventInfo["SPELL_PERIODIC_CAST_FAILED"] = CLEUEventInfo["GENERIC_CAST_FAILED"]
CLEUEventInfo["GENERIC_AURA_REFRESH"]
CLEUEventInfo["SPELL_PERIODIC_AURA_BROKEN"] =
CLEUEventInfo["GENERIC_AURA_BROKEN"]
CLEUEventInfo["SPELL_PERIODIC_AURA_BROKEN_SPELL"] =
CLEUEventInfo["GENERIC_AURA_BROKEN_SPELL"]
CLEUEventInfo["SPELL_PERIODIC_CAST_START"] =
CLEUEventInfo["GENERIC_CAST_START"]
CLEUEventInfo["SPELL_PERIODIC_CAST_SUCCESS"] =
CLEUEventInfo["GENERIC_CAST_SUCCESS"]
CLEUEventInfo["SPELL_PERIODIC_CAST_FAILED"] =
CLEUEventInfo["GENERIC_CAST_FAILED"]
---@class CLEUParser ---@class CLEUParser
CLEUParser = { CLEUParser = {
@@ -239,132 +207,88 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetTimestamp = function(...) GetTimestamp = function(...)
local val = select(CLEUEventInfo["GENERIC"]["timestamp"], ...) local val = select(CLEUEventInfo["GENERIC"]["timestamp"], ...)
if val == nil then if val == nil then return 0, "Timestamp is nil or missing" end
return 0, "Timestamp is nil or missing" if type(val) ~= "number" then return 0, "Timestamp is not a number" end
end
if type(val) ~= "number" then
return 0, "Timestamp is not a number"
end
return val, nil return val, nil
end, end,
---@param ... any ---@param ... any
---@return string, nil|string ---@return string, nil|string
GetSubevent = function(...) GetSubevent = function(...)
local val = select(CLEUEventInfo["GENERIC"]["subevent"], ...) local val = select(CLEUEventInfo["GENERIC"]["subevent"], ...)
if val == nil then if val == nil then return "", "Subevent is nil or missing" end
return "", "Subevent is nil or missing" if type(val) ~= "string" then return "", "Subevent is not a string" end
end
if type(val) ~= "string" then
return "", "Subevent is not a string"
end
return val, nil return val, nil
end, end,
---@param ... any ---@param ... any
---@return boolean, nil|string ---@return boolean, nil|string
GetHideCaster = function(...) GetHideCaster = function(...)
local val = select(CLEUEventInfo["GENERIC"]["hideCaster"], ...) local val = select(CLEUEventInfo["GENERIC"]["hideCaster"], ...)
if val == nil then if val == nil then return false, "HideCaster is nil or missing" end
return false, "HideCaster is nil or missing" if type(val) ~= "boolean" then return false, "HideCaster is not a boolean" end
end
if type(val) ~= "boolean" then
return false, "HideCaster is not a boolean"
end
return val, nil return val, nil
end, end,
---@param ... any ---@param ... any
---@return string, nil|string ---@return string, nil|string
GetSourceGUID = function(...) GetSourceGUID = function(...)
local val = select(CLEUEventInfo["GENERIC"]["sourceGUID"], ...) local val = select(CLEUEventInfo["GENERIC"]["sourceGUID"], ...)
if val == nil then if val == nil then return "", "SourceGUID is nil or missing" end
return "", "SourceGUID is nil or missing" if type(val) ~= "string" then return "", "SourceGUID is not a string" end
end
if type(val) ~= "string" then
return "", "SourceGUID is not a string"
end
return val, nil return val, nil
end, end,
---@param ... any ---@param ... any
---@return string, nil|string ---@return string, nil|string
GetSourceName = function(...) GetSourceName = function(...)
local val = select(CLEUEventInfo["GENERIC"]["sourceName"], ...) local val = select(CLEUEventInfo["GENERIC"]["sourceName"], ...)
if val == nil then if val == nil then return "", "SourceName is nil or missing" end
return "", "SourceName is nil or missing" if type(val) ~= "string" then return "", "SourceName is not a string" end
end
if type(val) ~= "string" then
return "", "SourceName is not a string"
end
return val, nil return val, nil
end, end,
---@param ... any ---@param ... any
---@return number, nil|string ---@return number, nil|string
GetSourceFlags = function(...) GetSourceFlags = function(...)
local val = select(CLEUEventInfo["GENERIC"]["sourceFlags"], ...) local val = select(CLEUEventInfo["GENERIC"]["sourceFlags"], ...)
if val == nil then if val == nil then return 0, "SourceFlags is nil or missing" end
return 0, "SourceFlags is nil or missing" if type(val) ~= "number" then return 0, "SourceFlags is not a number" end
end
if type(val) ~= "number" then
return 0, "SourceFlags is not a number"
end
return val, nil return val, nil
end, end,
---@param ... any ---@param ... any
---@return number, nil|string ---@return number, nil|string
GetSourceRaidFlags = function(...) GetSourceRaidFlags = function(...)
local val = select(CLEUEventInfo["GENERIC"]["sourceRaidFlags"], ...) local val = select(CLEUEventInfo["GENERIC"]["sourceRaidFlags"], ...)
if val == nil then if val == nil then return 0, "SourceRaidFlags is nil or missing" end
return 0, "SourceRaidFlags is nil or missing" if type(val) ~= "number" then return 0, "SourceRaidFlags is not a number" end
end
if type(val) ~= "number" then
return 0, "SourceRaidFlags is not a number"
end
return val, nil return val, nil
end, end,
---@param ... any ---@param ... any
---@return string, nil|string ---@return string, nil|string
GetDestGUID = function(...) GetDestGUID = function(...)
local val = select(CLEUEventInfo["GENERIC"]["destGUID"], ...) local val = select(CLEUEventInfo["GENERIC"]["destGUID"], ...)
if val == nil then if val == nil then return "", "DestGUID is nil or missing" end
return "", "DestGUID is nil or missing" if type(val) ~= "string" then return "", "DestGUID is not a string" end
end
if type(val) ~= "string" then
return "", "DestGUID is not a string"
end
return val, nil return val, nil
end, end,
---@param ... any ---@param ... any
---@return string, nil|string ---@return string, nil|string
GetDestName = function(...) GetDestName = function(...)
local val = select(CLEUEventInfo["GENERIC"]["destName"], ...) local val = select(CLEUEventInfo["GENERIC"]["destName"], ...)
if val == nil then if val == nil then return "", "DestName is nil or missing" end
return "", "DestName is nil or missing" if type(val) ~= "string" then return "", "DestName is not a string" end
end
if type(val) ~= "string" then
return "", "DestName is not a string"
end
return val, nil return val, nil
end, end,
---@param ... any ---@param ... any
---@return number, nil|string ---@return number, nil|string
GetDestFlags = function(...) GetDestFlags = function(...)
local val = select(CLEUEventInfo["GENERIC"]["destFlags"], ...) local val = select(CLEUEventInfo["GENERIC"]["destFlags"], ...)
if val == nil then if val == nil then return 0, "DestFlags is nil or missing" end
return 0, "DestFlags is nil or missing" if type(val) ~= "number" then return 0, "DestFlags is not a number" end
end
if type(val) ~= "number" then
return 0, "DestFlags is not a number"
end
return val, nil return val, nil
end, end,
---@param ... any ---@param ... any
---@return number, nil|string ---@return number, nil|string
GetDestRaidFlags = function(...) GetDestRaidFlags = function(...)
local val = select(CLEUEventInfo["GENERIC"]["destRaidFlags"], ...) local val = select(CLEUEventInfo["GENERIC"]["destRaidFlags"], ...)
if val == nil then if val == nil then return 0, "DestRaidFlags is nil or missing" end
return 0, "DestRaidFlags is nil or missing" if type(val) ~= "number" then return 0, "DestRaidFlags is not a number" end
end
if type(val) ~= "number" then
return 0, "DestRaidFlags is not a number"
end
return val, nil return val, nil
end, end,
@@ -380,9 +304,7 @@ local function Init()
GetSpellId = function(...) GetSpellId = function(...)
local val = select(CLEUEventInfo["GENERIC_SPELL"]["spellId"], ...) local val = select(CLEUEventInfo["GENERIC_SPELL"]["spellId"], ...)
if val == nil then return 0, "SpellId is nil or missing" end if val == nil then return 0, "SpellId is nil or missing" end
if type(val) ~= "number" then if type(val) ~= "number" then return 0, "SpellId is not a number" end
return 0, "SpellId is not a number"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -396,12 +318,8 @@ local function Init()
---@return string, nil|string ---@return string, nil|string
GetSpellName = function(...) GetSpellName = function(...)
local val = select(CLEUEventInfo["GENERIC_SPELL"]["spellName"], ...) local val = select(CLEUEventInfo["GENERIC_SPELL"]["spellName"], ...)
if val == nil then if val == nil then return "", "SpellName is nil or missing" end
return "", "SpellName is nil or missing" if type(val) ~= "string" then return "", "SpellName is not a string" end
end
if type(val) ~= "string" then
return "", "SpellName is not a string"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -414,14 +332,9 @@ local function Init()
---@param ... any ---@param ... any
---@return number, nil|string ---@return number, nil|string
GetSpellSchool = function(...) GetSpellSchool = function(...)
local val = select(CLEUEventInfo["GENERIC_SPELL"]["spellSchool"], local val = select(CLEUEventInfo["GENERIC_SPELL"]["spellSchool"], ...)
...) if val == nil then return 0, "SpellSchool is nil or missing" end
if val == nil then if type(val) ~= "number" then return 0, "SpellSchool is not a number" end
return 0, "SpellSchool is nil or missing"
end
if type(val) ~= "number" then
return 0, "SpellSchool is not a number"
end
return val, nil return val, nil
end, end,
@@ -451,15 +364,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetAmount = function(...) GetAmount = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["amount"], ...) local val = select(CLEUEventInfo[subevent]["amount"], ...)
if val == nil then return 0, "Amount is nil or missing" end if val == nil then return 0, "Amount is nil or missing" end
if type(val) ~= "number" then if type(val) ~= "number" then return 0, "Amount is not a number" end
return 0, "Amount is not a number"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -479,21 +387,12 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetOverkill = function(...) GetOverkill = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0, if not CLEUEventInfo[subevent] then return 0, "Subevent is not a valid event" end
string.format("Failed getting subevent due to: %s", err) if not CLEUEventInfo[subevent]["overkill"] then return 0, "Overkill is nil or missing" end
end
if not CLEUEventInfo[subevent] then
return 0, "Subevent is not a valid event"
end
if not CLEUEventInfo[subevent]["overkill"] then
return 0, "Overkill is nil or missing"
end
local val = select(CLEUEventInfo[subevent]["overkill"], ...) local val = select(CLEUEventInfo[subevent]["overkill"], ...)
if val == nil then return 0, "Overkill is nil or missing" end if val == nil then return 0, "Overkill is nil or missing" end
if type(val) ~= "number" then if type(val) ~= "number" then return 0, "Overkill is not a number" end
return 0, "Overkill is not a number"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -513,15 +412,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetSchool = function(...) GetSchool = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["school"], ...) local val = select(CLEUEventInfo[subevent]["school"], ...)
if val == nil then return 0, "School is nil or missing" end if val == nil then return 0, "School is nil or missing" end
if type(val) ~= "number" then if type(val) ~= "number" then return 0, "School is not a number" end
return 0, "School is not a number"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -543,17 +437,10 @@ local function Init()
---@return boolean, nil|string ---@return boolean, nil|string
GetResisted = function(...) GetResisted = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return false, string.format("Failed getting subevent due to: %s", err) end
return false,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["resisted"], ...) local val = select(CLEUEventInfo[subevent]["resisted"], ...)
if val == nil then if val == nil then return false, "Resisted is nil or missing" end
return false, "Resisted is nil or missing" if type(val) ~= "boolean" then return false, "Resisted is not a boolean" end
end
if type(val) ~= "boolean" then
return false, "Resisted is not a boolean"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -575,17 +462,10 @@ local function Init()
---@return boolean, nil|string ---@return boolean, nil|string
GetBlocked = function(...) GetBlocked = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return false, string.format("Failed getting subevent due to: %s", err) end
return false,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["blocked"], ...) local val = select(CLEUEventInfo[subevent]["blocked"], ...)
if val == nil then if val == nil then return false, "Blocked is nil or missing" end
return false, "Blocked is nil or missing" if type(val) ~= "boolean" then return false, "Blocked is not a boolean" end
end
if type(val) ~= "boolean" then
return false, "Blocked is not a boolean"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -608,17 +488,10 @@ local function Init()
---@return boolean, nil|string ---@return boolean, nil|string
GetAbsorbed = function(...) GetAbsorbed = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return false, string.format("Failed getting subevent due to: %s", err) end
return false,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["absorbed"], ...) local val = select(CLEUEventInfo[subevent]["absorbed"], ...)
if val == nil then if val == nil then return false, "Absorbed is nil or missing" end
return false, "Absorbed is nil or missing" if type(val) ~= "boolean" then return false, "Absorbed is not a boolean" end
end
if type(val) ~= "boolean" then
return false, "Absorbed is not a boolean"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -640,17 +513,10 @@ local function Init()
---@return boolean, nil|string ---@return boolean, nil|string
GetCritical = function(...) GetCritical = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return false, string.format("Failed getting subevent due to: %s", err) end
return false,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["critical"], ...) local val = select(CLEUEventInfo[subevent]["critical"], ...)
if val == nil then if val == nil then return false, "Critical is nil or missing" end
return false, "Critical is nil or missing" if type(val) ~= "boolean" then return false, "Critical is not a boolean" end
end
if type(val) ~= "boolean" then
return false, "Critical is not a boolean"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -670,17 +536,10 @@ local function Init()
---@return boolean, nil|string ---@return boolean, nil|string
GetGlancing = function(...) GetGlancing = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return false, string.format("Failed getting subevent due to: %s", err) end
return false,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["glancing"], ...) local val = select(CLEUEventInfo[subevent]["glancing"], ...)
if val == nil then if val == nil then return false, "Glancing is nil or missing" end
return false, "Glancing is nil or missing" if type(val) ~= "boolean" then return false, "Glancing is not a boolean" end
end
if type(val) ~= "boolean" then
return false, "Glancing is not a boolean"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -700,17 +559,10 @@ local function Init()
---@return boolean, nil|string ---@return boolean, nil|string
GetCrushing = function(...) GetCrushing = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return false, string.format("Failed getting subevent due to: %s", err) end
return false,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["crushing"], ...) local val = select(CLEUEventInfo[subevent]["crushing"], ...)
if val == nil then if val == nil then return false, "Crushing is nil or missing" end
return false, "Crushing is nil or missing" if type(val) ~= "boolean" then return false, "Crushing is not a boolean" end
end
if type(val) ~= "boolean" then
return false, "Crushing is not a boolean"
end
return val, nil return val, nil
end, end,
--- Specific to subevents prefixed by: --- Specific to subevents prefixed by:
@@ -731,17 +583,10 @@ local function Init()
---@return boolean, nil|string ---@return boolean, nil|string
GetIsOffHand = function(...) GetIsOffHand = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return false, string.format("Failed getting subevent due to: %s", err) end
return false,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["isOffHand"], ...) local val = select(CLEUEventInfo[subevent]["isOffHand"], ...)
if val == nil then if val == nil then return false, "IsOffHand is nil or missing" end
return false, "IsOffHand is nil or missing" if type(val) ~= "boolean" then return false, "IsOffHand is not a boolean" end
end
if type(val) ~= "boolean" then
return false, "IsOffHand is not a boolean"
end
return val, nil return val, nil
end, end,
@@ -764,17 +609,10 @@ local function Init()
---@return string, nil|string ---@return string, nil|string
GetMissType = function(...) GetMissType = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return "", string.format("Failed getting subevent due to: %s", err) end
return "",
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["missType"], ...) local val = select(CLEUEventInfo[subevent]["missType"], ...)
if val == nil then if val == nil then return "", "MissType is nil or missing" end
return "", "MissType is nil or missing" if type(val) ~= "string" then return "", "MissType is not a string" end
end
if type(val) ~= "string" then
return "", "MissType is not a string"
end
return val, nil return val, nil
end, end,
@@ -797,17 +635,10 @@ local function Init()
--- return type is unconfirmed! --- return type is unconfirmed!
GetAmountMissed = function(...) GetAmountMissed = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["amountMissed"], ...) local val = select(CLEUEventInfo[subevent]["amountMissed"], ...)
if val == nil then if val == nil then return 0, "AmountMissed is nil or missing" end
return 0, "AmountMissed is nil or missing" if type(val) ~= "number" then return 0, "AmountMissed is not a number" end
end
if type(val) ~= "number" then
return 0, "AmountMissed is not a number"
end
return val, nil return val, nil
end, end,
@@ -830,17 +661,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetOverhealing = function(...) GetOverhealing = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["overhealing"], ...) local val = select(CLEUEventInfo[subevent]["overhealing"], ...)
if val == nil then if val == nil then return 0, "Overhealing is nil or missing" end
return 0, "Overhealing is nil or missing" if type(val) ~= "number" then return 0, "Overhealing is not a number" end
end
if type(val) ~= "number" then
return 0, "Overhealing is not a number"
end
return val, nil return val, nil
end, end,
@@ -861,17 +685,10 @@ local function Init()
---@return string, nil|string ---@return string, nil|string
GetExtraGUID = function(...) GetExtraGUID = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return "", string.format("Failed getting subevent due to: %s", err) end
return "",
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["extraGUID"], ...) local val = select(CLEUEventInfo[subevent]["extraGUID"], ...)
if val == nil then if val == nil then return "", "ExtraGUID is nil or missing" end
return "", "ExtraGUID is nil or missing" if type(val) ~= "string" then return "", "ExtraGUID is not a string" end
end
if type(val) ~= "string" then
return "", "ExtraGUID is not a string"
end
return val, nil return val, nil
end, end,
@@ -892,17 +709,10 @@ local function Init()
---@return string, nil|string ---@return string, nil|string
GetExtraName = function(...) GetExtraName = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return "", string.format("Failed getting subevent due to: %s", err) end
return "",
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["extraName"], ...) local val = select(CLEUEventInfo[subevent]["extraName"], ...)
if val == nil then if val == nil then return "", "ExtraName is nil or missing" end
return "", "ExtraName is nil or missing" if type(val) ~= "string" then return "", "ExtraName is not a string" end
end
if type(val) ~= "string" then
return "", "ExtraName is not a string"
end
return val, nil return val, nil
end, end,
@@ -923,17 +733,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetExtraFlags = function(...) GetExtraFlags = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["extraFlags"], ...) local val = select(CLEUEventInfo[subevent]["extraFlags"], ...)
if val == nil then if val == nil then return 0, "ExtraFlags is nil or missing" end
return 0, "ExtraFlags is nil or missing" if type(val) ~= "number" then return 0, "ExtraFlags is not a number" end
end
if type(val) ~= "number" then
return 0, "ExtraFlags is not a number"
end
return val, nil return val, nil
end, end,
@@ -954,17 +757,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetExtraRaidFlags = function(...) GetExtraRaidFlags = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["extraRaidFlags"], ...) local val = select(CLEUEventInfo[subevent]["extraRaidFlags"], ...)
if val == nil then if val == nil then return 0, "ExtraRaidFlags is nil or missing" end
return 0, "ExtraRaidFlags is nil or missing" if type(val) ~= "number" then return 0, "ExtraRaidFlags is not a number" end
end
if type(val) ~= "number" then
return 0, "ExtraRaidFlags is not a number"
end
return val, nil return val, nil
end, end,
@@ -989,17 +785,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetExtraSpellID = function(...) GetExtraSpellID = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["extraSpellID"], ...) local val = select(CLEUEventInfo[subevent]["extraSpellID"], ...)
if val == nil then if val == nil then return 0, "ExtraSpellID is nil or missing" end
return 0, "ExtraSpellID is nil or missing" if type(val) ~= "number" then return 0, "ExtraSpellID is not a number" end
end
if type(val) ~= "number" then
return 0, "ExtraSpellID is not a number"
end
return val, nil return val, nil
end, end,
@@ -1025,17 +814,10 @@ local function Init()
---@return string, nil|string ---@return string, nil|string
GetExtraSpellName = function(...) GetExtraSpellName = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return "", string.format("Failed getting subevent due to: %s", err) end
return "",
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["extraSpellName"], ...) local val = select(CLEUEventInfo[subevent]["extraSpellName"], ...)
if val == nil then if val == nil then return "", "extraSpellName is nil or missing" end
return "", "extraSpellName is nil or missing" if type(val) ~= "string" then return "", "extraSpellName is not a string" end
end
if type(val) ~= "string" then
return "", "extraSpellName is not a string"
end
return val, nil return val, nil
end, end,
@@ -1061,17 +843,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetExtraSchool = function(...) GetExtraSchool = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["extraSchool"], ...) local val = select(CLEUEventInfo[subevent]["extraSchool"], ...)
if val == nil then if val == nil then return 0, "ExtraSchool is nil or missing" end
return 0, "ExtraSchool is nil or missing" if type(val) ~= "number" then return 0, "ExtraSchool is not a number" end
end
if type(val) ~= "number" then
return 0, "ExtraSchool is not a number"
end
return val, nil return val, nil
end, end,
@@ -1092,17 +867,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetAbsorbedAmount = function(...) GetAbsorbedAmount = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["absorbedAmount"], ...) local val = select(CLEUEventInfo[subevent]["absorbedAmount"], ...)
if val == nil then if val == nil then return 0, "AbsorbedAmount is nil or missing" end
return 0, "AbsorbedAmount is nil or missing" if type(val) ~= "number" then return 0, "AbsorbedAmount is not a number" end
end
if type(val) ~= "number" then
return 0, "AbsorbedAmount is not a number"
end
return val, nil return val, nil
end, end,
@@ -1123,17 +891,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetOverEnergize = function(...) GetOverEnergize = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["overEnergize"], ...) local val = select(CLEUEventInfo[subevent]["overEnergize"], ...)
if val == nil then if val == nil then return 0, "OverEnergize is nil or missing" end
return 0, "OverEnergize is nil or missing" if type(val) ~= "number" then return 0, "OverEnergize is not a number" end
end
if type(val) ~= "number" then
return 0, "OverEnergize is not a number"
end
return val, nil return val, nil
end, end,
@@ -1158,17 +919,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetPowerType = function(...) GetPowerType = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["powerType"], ...) local val = select(CLEUEventInfo[subevent]["powerType"], ...)
if val == nil then if val == nil then return 0, "PowerType is nil or missing" end
return 0, "PowerType is nil or missing" if type(val) ~= "number" then return 0, "PowerType is not a number" end
end
if type(val) ~= "number" then
return 0, "PowerType is not a number"
end
return val, nil return val, nil
end, end,
@@ -1190,17 +944,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetExtraAmount = function(...) GetExtraAmount = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["extraAmount"], ...) local val = select(CLEUEventInfo[subevent]["extraAmount"], ...)
if val == nil then if val == nil then return 0, "ExtraAmount is nil or missing" end
return 0, "ExtraAmount is nil or missing" if type(val) ~= "number" then return 0, "ExtraAmount is not a number" end
end
if type(val) ~= "number" then
return 0, "ExtraAmount is not a number"
end
return val, nil return val, nil
end, end,
@@ -1229,17 +976,10 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetExtraSpellId = function(...) GetExtraSpellId = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["extraSpellId"], ...) local val = select(CLEUEventInfo[subevent]["extraSpellId"], ...)
if val == nil then if val == nil then return 0, "ExtraSpellId is nil or missing" end
return 0, "ExtraSpellId is nil or missing" if type(val) ~= "number" then return 0, "ExtraSpellId is not a number" end
end
if type(val) ~= "number" then
return 0, "ExtraSpellId is not a number"
end
return val, nil return val, nil
end, end,
@@ -1268,17 +1008,12 @@ local function Init()
---@return number, nil|string ---@return number, nil|string
GetExtraAuraType = function(...) GetExtraAuraType = function(...)
local subevent, err = CLEUParser.GetSubevent(...) local subevent, err = CLEUParser.GetSubevent(...)
if err then if err then return 0, string.format("Failed getting subevent due to: %s", err) end
return 0,
string.format("Failed getting subevent due to: %s", err)
end
local val = select(CLEUEventInfo[subevent]["auraType"], ...) local val = select(CLEUEventInfo[subevent]["auraType"], ...)
if val == nil then return 0, "AuraType is nil or missing" end if val == nil then return 0, "AuraType is nil or missing" end
if type(val) ~= "number" then if type(val) ~= "number" then return 0, "AuraType is not a number" end
return 0, "AuraType is not a number"
end
return val, nil return val, nil
end end,
} }
end end
@@ -1286,7 +1021,5 @@ local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_LOGIN") frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("PLAYER_ENTERING_WORLD") frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("GUILD_ROSTER_UPDATE") frame:RegisterEvent("GUILD_ROSTER_UPDATE")
frame:SetScript("OnEvent", function(self, event, ...) frame:SetScript("OnEvent", function(self, event, ...) Init() end)
Init()
end)
Init() Init()

49
Modules/ChatSniffer.lua Normal file
View File

@@ -0,0 +1,49 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "ChatSniffer"
---@class HeimdallChatSnifferConfig
---@field enabled boolean
---@field debug boolean
---@class ChatSniffer
shared.ChatSniffer = {
Init = function()
Heimdall_Chat = Heimdall_Chat or {}
local frame = CreateFrame("Frame")
frame:RegisterEvent("CHAT_MSG_SAY")
frame:RegisterEvent("CHAT_MSG_YELL")
frame:RegisterEvent("CHAT_MSG_CHANNEL")
frame:RegisterEvent("CHAT_MSG_WHISPER")
frame:RegisterEvent("CHAT_MSG_CHANNEL_JOIN")
frame:RegisterEvent("CHAT_MSG_CHANNEL_LEAVE")
frame:RegisterEvent("CHAT_MSG_EMOTE")
frame:RegisterEvent("CHAT_MSG_PARTY")
frame:RegisterEvent("CHAT_MSG_PARTY_LEADER")
frame:RegisterEvent("CHAT_MSG_RAID")
frame:RegisterEvent("CHAT_MSG_RAID_LEADER")
frame:RegisterEvent("CHAT_MSG_RAID_WARNING")
frame:RegisterEvent("CHAT_MSG_SYSTEM")
frame:RegisterEvent("CHAT_MSG_TEXT_EMOTE")
frame:RegisterEvent("CHAT_MSG_YELL")
frame:SetScript("OnEvent", function(self, event, msg, sender, language, channel)
if not Heimdall_Data.config.chatSniffer.enabled then return end
if not Heimdall_Data.config.chatSniffer.debug then
shared.dump(string.format("[%s] got message", { event, msg, sender, language, channel }))
end
local timestamp = date("%Y-%m-%d %H:%M:%S")
local log = string.format(
"%s|%s|%s|%s|%s|%s",
tostring(timestamp),
tostring(event),
tostring(sender),
tostring(msg),
tostring(language),
tostring(channel)
)
Heimdall_Chat[#Heimdall_Chat + 1] = log
end)
print(string.format("[%s] Module initialized", ModuleName))
end,
}

141
Modules/CombatAlerter.lua Normal file
View File

@@ -0,0 +1,141 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "CombatAlerter"
---@class HeimdallCombatAlerterConfig
---@field enabled boolean
---@field debug boolean
---@field channels string[]
---@class CombatAlerter
shared.CombatAlerter = {
Init = function()
local alerted = {}
local combatAlerterFrame = CreateFrame("Frame")
combatAlerterFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
combatAlerterFrame:SetScript("OnEvent", function(self, event, ...)
if Heimdall_Data.config.combatAlerter.debug then
print(string.format("[%s] Combat log event received", ModuleName))
end
if not Heimdall_Data.config.combatAlerter.enabled then
if Heimdall_Data.config.combatAlerter.debug then
print(string.format("[%s] Module disabled, ignoring combat event", ModuleName))
end
return
end
---@type string|nil, string, string
local err, source, destination
destination, err = CLEUParser.GetDestName(...)
if err then
if Heimdall_Data.config.combatAlerter.debug then
print(string.format("[%s] Error getting destination: %s", ModuleName, err))
end
return
end
if Heimdall_Data.config.combatAlerter.debug then
print(string.format("[%s] Combat event destination: %s", ModuleName, destination))
end
if destination ~= UnitName("player") then
if Heimdall_Data.config.combatAlerter.debug then
print(string.format("[%s] Ignoring event - not targeted at player", ModuleName))
end
return
end
source, err = CLEUParser.GetSourceName(...)
if err then
if Heimdall_Data.config.combatAlerter.debug then
print(string.format("[%s] Error getting source, using 'unknown': %s", ModuleName, err))
end
source = "unknown"
end
if Heimdall_Data.config.combatAlerter.debug then
print(string.format("[%s] Combat event source: %s", ModuleName, source))
end
if shared.StinkyTracker.IsStinky(source) then
if Heimdall_Data.config.combatAlerter.debug then
print(
string.format(
"[%s] Source is tracked stinky: %s (Already alerted: %s)",
ModuleName,
source,
tostring(alerted[source] or false)
)
)
end
if alerted[source] then return end
alerted[source] = true
local x, y = GetPlayerMapPosition("player")
local zone, subZone = GetZoneText(), GetSubZoneText()
if Heimdall_Data.config.combatAlerter.debug then
print(
string.format(
"[%s] Player location: %s/%s at %.2f,%.2f",
ModuleName,
zone,
subZone,
x * 100,
y * 100
)
)
end
SetMapToCurrentZone()
SetMapByID(GetCurrentMapAreaID())
local areaId = GetCurrentMapAreaID()
for _, channel in pairs(Heimdall_Data.config.combatAlerter.channels) do
local locale = shared.GetLocaleForChannel(channel)
local text = string.format(
shared._L("combatAlerterInCombat", locale),
source,
shared._L("zone", locale),
shared._L("subZone", locale),
tostring(areaId),
x * 100,
y * 100
)
---@type Message
local msg = {
channel = "C",
data = channel,
message = text,
}
if Heimdall_Data.config.combatAlerter.debug then
print(string.format("[%s] Queuing alert message", ModuleName))
shared.dump(msg)
end
table.insert(shared.messenger.queue, msg)
end
elseif Heimdall_Data.config.combatAlerter.debug then
print(string.format("[%s] Source not in stinky list, ignoring: %s", ModuleName, source))
end
end)
local combatTriggerFrame = CreateFrame("Frame")
combatTriggerFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
combatTriggerFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
combatTriggerFrame:SetScript("OnEvent", function(self, event, ...)
if Heimdall_Data.config.combatAlerter.debug then
print(string.format("[%s] Combat state changed: %s", ModuleName, event))
if event == "PLAYER_REGEN_DISABLED" then
print(string.format("[%s] Entered combat - Resetting alerts", ModuleName))
else
print(string.format("[%s] Left combat - Resetting alerts", ModuleName))
end
end
alerted = {}
end)
if Heimdall_Data.config.combatAlerter.debug then print(string.format("[%s] Module initialized", ModuleName)) end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

438
Modules/Commander.lua Normal file
View File

@@ -0,0 +1,438 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Commander"
---@class HeimdallCommanderConfig
---@field enabled boolean
---@field debug boolean
---@field channels string[]
---@field commander string
---@field commands table<string, boolean>
local helpMessages = {
ru = {
"1) who - пишет вам никнеймы текущих врагов и локу.",
"2) classes - покажет классы врагов и число.",
"3) howmany - общее число врагов (дурик 4 . огри 2 ) ",
"4) + - атоинвайт в сбор пати и сброса кд.",
"5) ++ -автоинвайт в пати аликов (если нужен рефрак)",
"6 ) note Никнейм текст - добавление заметки.",
"7) note Никнейм - посмотреть последние заметки.",
"8) note Никнейм 5 - посмотреть конкретную заметку.",
"9) note Никнейм 1..5 - посмотреть заметки от 1 до 5",
"10) note Никнейм delete 1 - удалить заметку номер 1",
"11) note Никнейм delete 1..5 - удалить заметки 1 до 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",
"6) note <name> <note> - adds a note for the specified character.",
"7) note <name> - lists the last N notes for the character.",
"8) note <name> i - lists the i-th note for the character.",
"9) note <name> i..j - lists notes from i to j for the character.",
"10) note <name> delete i - deletes the i-th note for the character.",
"11) note <name> delete i..j - deletes notes from i to j for the character.",
},
}
---@class Commander
shared.Commander = {
Init = function()
---@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 shared.Whoer.ShouldNotifyForZone(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 = {}
---@diagnostic disable-next-line: param-type-mismatch something wrong with luals, it's picking up the "wrong" unpack
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 shared.Whoer.ShouldNotifyForZone(player.zone) then
ret[#ret + 1] = string.format(
"%s/%s (%s) %s",
player.name,
player.class,
player.zone,
player.stinky and "(!!!!)" or ""
)
end
end
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Command result: %s", ModuleName, strjoin(", ", unpack(ret))))
end
return ret
end
-- This is really ugly, duplicating methods like this
-- But I have no better idea
-- We would have to drag reference channel all the way here
-- And then in here do some kind of deciding based on the fucking channel locale
-- That's also a nasty solution... I guess adding "kto" is better
---@param arr table<string, Player>
---@return string[]
local function WhoRu(arr)
local ret = {}
for _, player in pairs(arr) do
if shared.Whoer.ShouldNotifyForZone(player.zone) then
shared.dump(player)
ret[#ret + 1] = string.format(
"%s/%s (%s) %s",
player.name,
shared._L(player.class, "ru"),
shared._L(player.zone, "ru"),
player.stinky and "(!!!!)" or ""
)
end
end
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Command result: %s", ModuleName, strjoin(", ", unpack(ret))))
end
return ret
end
---@param arr table<string, Player>
---@return string[]
local function WhoPartitioned(arr)
local who = Who(arr)
local text = {}
---@diagnostic disable-next-line: param-type-mismatch something wrong with luals, it's picking up the "wrong" unpack
for _, line in pairs(Partition(strjoin(", ", unpack(who)), 200)) do
text[#text + 1] = "who: " .. line
end
return text
end
---@param arr table<string, Player>
---@return string[]
local function WhoPartitionedRu(arr)
local who = WhoRu(arr)
local text = {}
---@diagnostic disable-next-line: param-type-mismatch something wrong with luals, it's picking up the "wrong" unpack
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 shared.Whoer.ShouldNotifyForZone(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
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Message text: %s", ModuleName, strjoin(", ", unpack(text))))
end
return text
end
---@param arr table<string, Player>
---@return string[]
local function CountClassPartitioned(arr)
local countClass = CountClass(arr)
local text = {}
---@diagnostic disable-next-line: param-type-mismatch something wrong with luals, it's picking up the "wrong" unpack
for _, line in pairs(Partition(strjoin(", ", unpack(countClass)), 200)) do
text[#text + 1] = line
end
return text
end
local function CountClassPartitionedStinkies()
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Executing: CountClassPartitionedStinkies", ModuleName))
end
local res = CountClassPartitioned(HeimdallStinkies)
if #res == 0 then return { "No stinkies found" } end
return res
end
local function WhoPartitionedStinkies()
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Executing: WhoPartitionedStinkies", ModuleName))
shared.dump(HeimdallStinkies)
end
local res = WhoPartitioned(HeimdallStinkies)
if #res == 0 then return { "No stinkies found" } end
return res
end
local function WhoPartitionedStinkiesRu()
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Executing: WhoPartitionedStinkies", ModuleName))
shared.dump(HeimdallStinkies)
end
local res = WhoPartitionedRu(HeimdallStinkies)
if #res == 0 then return { "No stinkies found" } end
return res
end
local function CountPartitionedStinkies()
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Executing: CountPartitionedStinkies", ModuleName))
end
local res = CountPartitioned(HeimdallStinkies)
if #res == 0 then return { "No stinkies found" } end
return res
end
local function HelpRu()
if Heimdall_Data.config.commander.debug then print(string.format("[%s] Executing: HelpRu", ModuleName)) end
return helpMessages.ru
end
local function HelpEn()
if Heimdall_Data.config.commander.debug then print(string.format("[%s] Executing: HelpEn", ModuleName)) end
return helpMessages.en
end
local groupInviteFrame = CreateFrame("Frame")
groupInviteFrame:SetScript("OnEvent", function(self, event, ...)
if Heimdall_Data.config.commander.debug then print(string.format("[%s] Event received", ModuleName)) end
AcceptGroup()
groupInviteFrame:UnregisterEvent("PARTY_INVITE_REQUEST")
C_Timer.NewTimer(0.1, function()
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Click event triggered", ModuleName))
end
_G["StaticPopup1Button1"]:Click()
end, 1)
end)
local function JoinGroup()
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] JoinGroup command received", ModuleName))
end
groupInviteFrame:RegisterEvent("PARTY_INVITE_REQUEST")
C_Timer.NewTimer(10, function() groupInviteFrame:UnregisterEvent("PARTY_INVITE_REQUEST") end, 1)
return { "+" }
end
local function LeaveGroup()
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] LeaveGroup command received", ModuleName))
end
LeaveParty()
return {}
end
---@param target string
local function FollowTarget(target)
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Following target: %s", ModuleName, target))
end
if not target then return end
FollowUnit(target)
return {}
end
---@param args string[]
local function MacroTarget(args)
if Heimdall_Data.config.commander.debug then
---@diagnostic disable-next-line: param-type-mismatch something wrong with luals, it's picking up the "wrong" unpack
print(string.format("[%s] Macroing: %s", ModuleName, strjoin(" ", unpack(args))))
end
if #args < 2 or #args % 2 ~= 0 then
if #args < 2 or #args % 2 ~= 0 then
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Invalid number of arguments for MacroTarget", ModuleName))
end
return {}
end
end
table.remove(args, 1)
for i = 1, #args do
local stinky = strtrim(args[i])
local name = stinky:match("([^/]+)")
local class = stinky:match("/([^ $]+)")
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Adding stinky: %s/%s", ModuleName, name, tostring(class)))
end
shared.StinkyTracker.Track({
name = name,
class = class or "unknown",
seenAt = GetTime(),
hostile = true,
})
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Added stinky: %s/%s", ModuleName, name, tostring(class)))
end
end
return {}
end
---@param args string[]
local function IgnoreMacroTarget(args)
if Heimdall_Data.config.commander.debug then
---@diagnostic disable-next-line: param-type-mismatch something wrong with luals, it's picking up the "wrong" unpack
print(string.format("[%s] Macroing: %s", ModuleName, strjoin(" ", unpack(args))))
end
if #args < 1 then
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Invalid number of arguments for IgnoreMacroTarget", ModuleName))
end
return {}
end
table.remove(args, 1)
for i = 1, #args do
local stinky = strtrim(args[i])
local name = stinky:match("([^/]+)")
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Ignoring stinky: %s", ModuleName, name))
end
shared.StinkyTracker.Ignore(name)
end
return {}
end
---@class Command
---@field keywordRe string
---@field commanderOnly boolean
---@field callback fun(...: any): string[]
local commands = {
{ keywordRe = "^who$", commanderOnly = false, callback = WhoPartitionedStinkies },
{ keywordRe = "^кто$", commanderOnly = false, callback = WhoPartitionedStinkiesRu },
{ 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 },
{ keywordRe = "^macro", commanderOnly = false, callback = MacroTarget },
{ keywordRe = "^ignore", commanderOnly = false, callback = IgnoreMacroTarget },
}
local commanderChannelFrame = CreateFrame("Frame")
commanderChannelFrame:RegisterEvent("CHAT_MSG_CHANNEL")
commanderChannelFrame:SetScript("OnEvent", function(self, event, msg, sender, ...)
--if Heimdall_Data.config.commander.debug then
-- print(string.format("[%s] Event received", ModuleName))
-- shared.dump(Heimdall_Data.config.commander)
--end
if not Heimdall_Data.config.commander.enabled then
--if Heimdall_Data.config.commander.debug then
-- print(string.format("[%s] Module disabled, ignoring event", ModuleName))
--end
return
end
local channelId = select(6, ...)
local _, channelname = GetChannelName(channelId)
local ok = false
for _, channel in pairs(Heimdall_Data.config.commander.channels) do
if channel == channelname then
ok = true
break
end
end
if not ok then
if Heimdall_Data.config.commander.debug then
print(
string.format(
"[%s] Channel name '%s' does not match any of the channels '%s'",
ModuleName,
channelname,
table.concat(Heimdall_Data.config.commander.channels, ", ")
)
)
end
return
end
sender = string.match(sender, "^[^-]+")
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Message from: %s", ModuleName, sender))
shared.dump(Heimdall_Data.config.commander)
end
for _, command in ipairs(commands) do
local enabled = Heimdall_Data.config.commander.commands[command.keywordRe] == true or false
if Heimdall_Data.config.commander.debug then
print(
string.format("[%s] Command match: %s = %s", ModuleName, command.keywordRe, tostring(enabled))
)
end
if
enabled
and (
not command.commanderOnly
-- if Heimdall_Data.config.commander.debug then print(string.format("[%s] Ignoring command, sender %s not commander %s", ModuleName, sender, Heimdall_Data.config.commander.commander)) end
or (command.commanderOnly and sender == Heimdall_Data.config.commander.commander)
)
then
if msg:match(command.keywordRe) then
---@diagnostic disable-next-line: redundant-parameter Currently luals does not support variadic functions as a @field
local messages = command.callback({ strsplit(",", msg) })
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Messages to send: %s", ModuleName, #messages))
shared.dump(messages)
end
for _, message in ipairs(messages) do
---@type Message
local returnmsg = {
channel = "C",
data = channelname,
message = message,
}
if Heimdall_Data.config.commander.debug then
print(string.format("[%s] Queuing message", ModuleName))
shared.dump(msg)
end
if Heimdall_Data.config.networkMessenger.enabled then
shared.NetworkMessenger.Enqueue(returnmsg)
elseif Heimdall_Data.config.messenger.enabled then
shared.Messenger.Enqueue(returnmsg)
end
end
end
end
end
end)
print(string.format("[%s] Module initialized", ModuleName))
end,
}

2710
Modules/Config.lua Normal file

File diff suppressed because it is too large Load Diff

12
Modules/Configurator.lua Normal file
View File

@@ -0,0 +1,12 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Configurator"
---@class HeimdallConfiguratorConfig
---@field enabled boolean
---@field debug boolean
---@class Configurator
shared.Configurator = {
Init = function() print(string.format("[%s] Module initialized", ModuleName)) end,
}

268
Modules/DeathReporter.lua Normal file
View File

@@ -0,0 +1,268 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "DeathReporter"
---@class HeimdallDeathReporterConfig
---@field enabled boolean
---@field debug boolean
---@field throttle number
---@field doWhisper boolean
---@field channels string[]
---@field zoneOverride string?
---@field duelThrottle number
---@class DeathReporter
shared.DeathReporter = {
Init = function()
---@type table<string, number>
local recentDeaths = {}
---@type table<string, number>
local recentDuels = {}
---@param source string
---@param destination string
---@param spellName string
local function RegisterDeath(source, destination, spellName)
if Heimdall_Data.config.deathReporter.debug then
print(
string.format(
"[%s] Processing death event - Source: %s, Target: %s, Spell: %s",
ModuleName,
source,
destination,
spellName
)
)
end
if not Heimdall_Data.config.deathReporter.enabled then
if Heimdall_Data.config.deathReporter.debug then
print(string.format("[%s] Module disabled, ignoring death event", ModuleName))
end
return
end
if
recentDeaths[destination]
and GetTime() - recentDeaths[destination] < Heimdall_Data.config.deathReporter.throttle
then
if Heimdall_Data.config.deathReporter.debug then
local timeLeft = Heimdall_Data.config.deathReporter.throttle
- (GetTime() - recentDeaths[destination])
print(
string.format(
"[%s] Death report throttled for %s (%.1f seconds remaining)",
ModuleName,
destination,
timeLeft
)
)
end
return
end
if
recentDuels[destination]
and GetTime() - recentDuels[destination] < Heimdall_Data.config.deathReporter.duelThrottle
then
if Heimdall_Data.config.deathReporter.debug then
print(
string.format(
"[%s] Ignoring death report - Recent duel detected for target: %s",
ModuleName,
destination
)
)
end
return
end
if
recentDuels[source]
and GetTime() - recentDuels[source] < Heimdall_Data.config.deathReporter.duelThrottle
then
if Heimdall_Data.config.deathReporter.debug then
print(
string.format(
"[%s] Ignoring death report - Recent duel detected for source: %s",
ModuleName,
source
)
)
end
return
end
if Heimdall_Data.config.deathReporter.debug then
print(string.format("[%s] Recording death for %s", ModuleName, destination))
end
recentDeaths[destination] = GetTime()
C_Timer.NewTimer(3, function()
if
recentDuels[destination]
and GetTime() - recentDuels[destination] < Heimdall_Data.config.deathReporter.duelThrottle
then
if Heimdall_Data.config.deathReporter.debug then
print(
string.format(
"[%s] Cancelling delayed death report - Recent duel detected for: %s",
ModuleName,
destination
)
)
end
return
end
if
recentDuels[source]
and GetTime() - recentDuels[source] < Heimdall_Data.config.deathReporter.duelThrottle
then
if Heimdall_Data.config.deathReporter.debug then
print(
string.format(
"[%s] Cancelling delayed death report - Recent duel detected for: %s",
ModuleName,
source
)
)
end
return
end
if Heimdall_Data.config.deathReporter.debug then
print(
string.format(
"[%s] Sending death report - %s killed %s with %s",
ModuleName,
source,
destination,
spellName
)
)
end
local zone, subzone = GetZoneText() or "Unknown", GetSubZoneText() or "Unknown"
if Heimdall_Data.config.spotter.zoneOverride then
zone = Heimdall_Data.config.spotter.zoneOverride or ""
subzone = ""
end
local x, y = GetPlayerMapPosition("player")
if Heimdall_Data.config.deathReporter.debug then
print(string.format("[%s] Player coordinates: %.2f, %.2f", ModuleName, x * 100, y * 100))
end
SetMapToCurrentZone()
SetMapByID(GetCurrentMapAreaID())
local zoneId = GetCurrentMapAreaID()
for _, channel in pairs(Heimdall_Data.config.deathReporter.channels) do
local locale = shared.GetLocaleForChannel(channel)
local text = string.format(
shared._L("killed", locale),
source,
destination,
shared._L(spellName, locale),
shared._L(zone, locale),
shared._L(subzone, locale),
zoneId,
x * 100,
y * 100
)
---@type Message
local msg = {
channel = "C",
data = channel,
message = text,
}
if Heimdall_Data.config.deathReporter.debug then
print(string.format("[%s] Queuing death report message", ModuleName))
shared.dump(msg)
end
table.insert(shared.messenger.queue, msg)
end
end)
end
local cleuFrame = CreateFrame("Frame")
cleuFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
cleuFrame:SetScript("OnEvent", function(self, event, ...)
-- if Heimdall_Data.config.deathReporter.debug then
-- print(string.format("[%s] Received combat log event", ModuleName))
-- end
if not Heimdall_Data.config.deathReporter.enabled then return end
local overkill, source, destination, spellName, sourceGUID, destinationGUID, err
overkill, err = CLEUParser.GetOverkill(...)
if not err and overkill > 0 then
source, err = CLEUParser.GetSourceName(...)
if err then
source = "unknown"
if Heimdall_Data.config.deathReporter.debug then
print(string.format("[%s] Error getting source name", ModuleName))
end
end
destination, err = CLEUParser.GetDestName(...)
if err then
destination = "unknown"
if Heimdall_Data.config.deathReporter.debug then
print(string.format("[%s] Error getting destination name", ModuleName))
end
end
spellName, err = CLEUParser.GetSpellName(...)
if err then
spellName = "unknown"
if Heimdall_Data.config.deathReporter.debug then
print(string.format("[%s] Error getting spell name", ModuleName))
end
end
sourceGUID, err = CLEUParser.GetSourceGUID(...)
if err or not string.match(sourceGUID, "Player") then return end
destinationGUID, err = CLEUParser.GetDestGUID(...)
if err or not string.match(destinationGUID, "Player") then return end
RegisterDeath(source, destination, spellName)
end
end)
local systemMessageFrame = CreateFrame("Frame")
systemMessageFrame:RegisterEvent("CHAT_MSG_SYSTEM")
systemMessageFrame:SetScript("OnEvent", function(self, event, msg)
if not Heimdall_Data.config.deathReporter.enabled then return end
local source, destination = string.match(msg, "([^ ]+) has defeated ([^ ]+) in a duel")
if Heimdall_Data.config.deathReporter.debug then
print(string.format("[%s] Received system message: %s", ModuleName, msg))
print(
string.format(
"[%s] Source: %s, Destination: %s",
ModuleName,
tostring(source),
tostring(destination)
)
)
end
if not source or not destination then return end
source = string.match(source, "([^-]+)")
destination = string.match(destination, "([^-]+)")
if source and destination then
if Heimdall_Data.config.deathReporter.debug then
print(string.format("[%s] Detected duel between %s and %s", ModuleName, source, destination))
end
local now = GetTime()
recentDuels[source] = now
recentDuels[destination] = now
end
end)
if Heimdall_Data.config.deathReporter.debug then
print(
string.format(
"[%s] Module initialized with throttle: %.1fs, duel throttle: %.1fs",
ModuleName,
Heimdall_Data.config.deathReporter.throttle,
Heimdall_Data.config.deathReporter.duelThrottle
)
)
end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

63
Modules/Dueler.lua Normal file
View File

@@ -0,0 +1,63 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Dueler"
---@class HeimdallDuelerConfig
---@field enabled boolean
---@field debug boolean
---@field declineOther boolean
---@class Dueler
shared.Dueler = {
Init = function()
local frame = CreateFrame("Frame")
frame:RegisterEvent("DUEL_REQUESTED")
frame:SetScript("OnEvent", function(self, event, sender)
if Heimdall_Data.config.dueler.debug then
print(string.format("[%s] Duel request received from: %s", ModuleName, sender))
end
if not Heimdall_Data.config.dueler.enabled then
if Heimdall_Data.config.dueler.debug then
print(string.format("[%s] Module disabled, ignoring duel request", ModuleName))
end
return
end
if Heimdall_Data.config.dueler.debug then
print(string.format("[%s] Checking if sender '%s' is in agents list", ModuleName, sender))
end
local allow = shared.AgentTracker.IsAgent(sender)
if allow then
if Heimdall_Data.config.dueler.debug then
print(string.format("[%s] Accepting duel from trusted agent: %s", ModuleName, sender))
end
AcceptDuel()
else
if Heimdall_Data.config.dueler.declineOther then
if Heimdall_Data.config.dueler.debug then
print(string.format("[%s] Auto-declining duel from untrusted sender: %s", ModuleName, sender))
end
CancelDuel()
else
if Heimdall_Data.config.dueler.debug then
print(
string.format("[%s] Leaving duel request from %s for manual response", ModuleName, sender)
)
end
end
end
end)
if Heimdall_Data.config.dueler.debug then
print(
string.format(
"[%s] Module initialized with auto-decline: %s",
ModuleName,
tostring(Heimdall_Data.config.dueler.declineOther)
)
)
end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

32
Modules/DumpTable.lua Normal file
View File

@@ -0,0 +1,32 @@
local _, shared = ...
---@cast shared HeimdallShared
if not shared.dump then
---@param value any
---@param msg string?
---@param depth number?
shared.dump = function(value, msg, depth)
if not value then
print(tostring(value))
return
end
if type(value) ~= "table" then
print(tostring(value))
return
end
if msg then print(msg) end
if depth == nil then depth = 0 end
if depth > 200 then
print("Error: Depth > 200 in dump()")
return
end
for k, v in pairs(value) do
if type(v) == "table" then
print(string.rep(" ", depth) .. tostring(k) .. ":")
shared.dump(v, msg, depth + 1)
else
print(string.rep(" ", depth) .. tostring(k) .. ": " .. tostring(v))
end
end
end
end

65
Modules/Echoer.lua Normal file
View File

@@ -0,0 +1,65 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Echoer"
---@class HeimdallEchoerConfig
---@field enabled boolean
---@field debug boolean
---@field channels string[]
---@field prefix string
---@class Echoer
shared.Echoer = {
Init = function()
local frame = CreateFrame("Frame")
frame:RegisterEvent("CHAT_MSG_CHANNEL")
frame:SetScript("OnEvent", function(self, event, msg, sender, ...)
--if Heimdall_Data.config.echoer.debug then
-- print(string.format("[%s] Channel message received from: %s", ModuleName, sender))
--end
if not Heimdall_Data.config.echoer.enabled then
--if Heimdall_Data.config.echoer.debug then
-- print(string.format("[%s] Module disabled, ignoring message", ModuleName))
--end
return
end
local channelId = select(6, ...)
local _, channelname = GetChannelName(channelId)
local ok = false
for _, channel in pairs(Heimdall_Data.config.echoer.channels) do
if channel == channelname then
ok = true
break
end
end
if not ok then
if Heimdall_Data.config.echoer.debug then
print(string.format("[%s] Channel name does not match any of the channels", ModuleName))
end
return
end
if Heimdall_Data.config.echoer.debug then
print(string.format("[%s] Processing message from master channel: %s", ModuleName, sender))
shared.dump(Heimdall_Data.config.echoer)
end
if string.find(msg, "^" .. Heimdall_Data.config.echoer.prefix) then
if Heimdall_Data.config.echoer.debug then
print(string.format("[%s] Found echo command in message: %s", ModuleName, msg))
end
local echomsg = string.sub(msg, string.len(Heimdall_Data.config.echoer.prefix) + 1)
if Heimdall_Data.config.echoer.debug then
print(string.format("[%s] Echoing message: %s", ModuleName, echomsg))
end
SendChatMessage(echomsg, "SAY")
elseif Heimdall_Data.config.echoer.debug then
print(string.format("[%s] Message does not start with echo prefix", ModuleName))
end
end)
if Heimdall_Data.config.echoer.debug then print(string.format("[%s] Module initialized", ModuleName)) end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

66
Modules/Emoter.lua Normal file
View File

@@ -0,0 +1,66 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Emoter"
---@class HeimdallEmoterConfig
---@field enabled boolean
---@field debug boolean
---@field channels string[]
---@field prefix string
---@class Emoter
shared.Emoter = {
Init = function()
local frame = CreateFrame("Frame")
frame:RegisterEvent("CHAT_MSG_CHANNEL")
frame:SetScript("OnEvent", function(self, event, msg, sender, ...)
--if Heimdall_Data.config.emoter.debug then
-- print(string.format("[%s] Channel message received from: %s", ModuleName, sender))
--end
if not Heimdall_Data.config.emoter.enabled then
--if Heimdall_Data.config.emoter.debug then
-- print(string.format("[%s] Module disabled, ignoring message", ModuleName))
--end
return
end
local channelId = select(6, ...)
local _, channelname = GetChannelName(channelId)
local ok = false
for _, channel in pairs(Heimdall_Data.config.emoter.channels) do
if channel == channelname then
ok = true
break
end
end
if not ok then
if Heimdall_Data.config.emoter.debug then
print(string.format("[%s] Channel name does not match any of the channels", ModuleName))
end
return
end
if Heimdall_Data.config.emoter.debug then
print(string.format("[%s] Processing message from master channel: %s", ModuleName, sender))
shared.dump(Heimdall_Data.config.emoter)
end
if string.find(msg, "^" .. Heimdall_Data.config.emoter.prefix) then
if Heimdall_Data.config.emoter.debug then
print(string.format("[%s] Found emote command in message: %s", ModuleName, msg))
end
local emote = string.sub(msg, string.len(Heimdall_Data.config.emoter.prefix) + 1)
if Heimdall_Data.config.emoter.debug then
print(string.format("[%s] Performing emote: %s", ModuleName, emote))
end
DoEmote(emote)
elseif Heimdall_Data.config.emoter.debug then
print(string.format("[%s] Message does not start with emote prefix", ModuleName))
end
end)
if Heimdall_Data.config.emoter.debug then print(string.format("[%s] Module initialized", ModuleName)) end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

275
Modules/Inviter.lua Normal file
View File

@@ -0,0 +1,275 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Inviter"
---@class HeimdallInviterConfig
---@field enabled boolean
---@field debug boolean
---@field channels string[]
---@field keyword string
---@field allAssist boolean
---@field agentsAssist boolean
---@field throttle number
---@field kickOffline boolean
---@field cleanupInterval number
---@field afkThreshold number
---@field listeningChannel table<string, boolean>
---@class Inviter
shared.Inviter = {
Init = function()
-- Fallback for old config
if type(Heimdall_Data.config.inviter.listeningChannel) == "string" then
Heimdall_Data.config.inviter.listeningChannel = {
[Heimdall_Data.config.inviter.listeningChannel] = true,
}
end
---@type Timer
local updateTimer = nil
local function FixGroup()
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Checking and fixing group configuration", ModuleName))
end
if not IsInRaid() then
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Converting party to raid", ModuleName))
end
ConvertToRaid()
end
if Heimdall_Data.config.inviter.allAssist then
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Setting all members to assistant", ModuleName))
end
SetEveryoneIsAssistant()
end
if Heimdall_Data.config.inviter.agentsAssist then
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Processing agents for assistant promotion", ModuleName))
end
shared.AgentTracker.ForEach(function(agent)
if UnitInParty(agent) and not UnitIsGroupLeader(agent) and not UnitIsRaidOfficer(agent) then
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Promoting agent to assistant: %s", ModuleName, agent))
end
PromoteToAssistant(agent, true)
elseif Heimdall_Data.config.inviter.debug then
if not UnitInParty(agent) then
print(string.format("[%s] Agent not in party: %s", ModuleName, agent))
elseif UnitIsGroupLeader(agent) then
print(string.format("[%s] Agent is already leader: %s", ModuleName, agent))
elseif UnitIsRaidOfficer(agent) then
print(string.format("[%s] Agent is already assistant: %s", ModuleName, agent))
end
end
end)
end
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Group configuration update complete", ModuleName))
end
end
---@param name string
---@return Frame?
local function FindPlayerRaidFrame(name)
for group = 1, 8 do
for player = 1, 5 do
local button = _G[string.format("ElvUF_RaidGroup%dUnitButton%d", group, player)]
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] button = %s", ModuleName, tostring(button)))
end
local unitName = button and button.unit and UnitName(button.unit)
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] unitName = %s", ModuleName, tostring(unitName)))
end
if unitName == name then
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] unitName == name", ModuleName))
end
return button
end
end
end
return nil
end
local framePool = {}
local playerButtons = {}
setmetatable(playerButtons, { __mode = "kv" })
---@param players string[]
local function OverlayKickButtons(players)
for _, frame in pairs(framePool) do
frame:Hide()
end
for _, name in pairs(players) do
local frame = FindPlayerRaidFrame(name)
if frame then
playerButtons[name] = frame
-- All of these are ELVUI specific so they won't be in our meta...
---@diagnostic disable-next-line: undefined-field
local button = framePool[frame.unit]
or CreateFrame(
"Button",
---@diagnostic disable-next-line: undefined-field
string.format("HeimdallKickButton%s", frame.unit, frame, "SecureActionButtonTemplate")
)
---@diagnostic disable-next-line: undefined-field
framePool[frame.unit] = button
---@diagnostic disable-next-line: undefined-field
button:SetSize(frame.UNIT_WIDTH / 2, frame.UNIT_HEIGHT / 2)
button:SetPoint("CENTER", frame, "CENTER", 0, 0)
button:SetNormalTexture("Interface\\Buttons\\UI-GroupLoot-KickIcon")
button:SetHighlightTexture("Interface\\Buttons\\UI-GroupLoot-KickIcon")
button:SetPushedTexture("Interface\\Buttons\\UI-GroupLoot-KickIcon")
button:SetDisabledTexture("Interface\\Buttons\\UI-GroupLoot-KickIcon")
button:SetAlpha(0.5)
button:Show()
button:SetScript("OnClick", function()
UninviteUnit(name)
button:Hide()
end)
else
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Frame for player %s not found", ModuleName, name))
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
local afkPlayers = {}
for name, time in pairs(groupMembers) do
if not UnitInParty(name) then
print(string.format("%s no longer in party", name))
groupMembers[name] = nil
else
if time < now - Heimdall_Data.config.inviter.afkThreshold then
print(string.format("Kicking %s for being offline", name))
afkPlayers[#afkPlayers + 1] = name
-- Blyat this is protected...
-- UninviteUnit(name)
end
end
end
OverlayKickButtons(afkPlayers)
end
local function Tick()
CleanGroups()
C_Timer.NewTimer(Heimdall_Data.config.inviter.cleanupInterval, Tick, 1)
end
Tick()
local groupRosterUpdateFrame = CreateFrame("Frame")
groupRosterUpdateFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
groupRosterUpdateFrame:SetScript("OnEvent", function(self, event, ...)
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Event received: %s", ModuleName, event))
end
if not Heimdall_Data.config.inviter.enabled then
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Module disabled, ignoring event", ModuleName))
end
return
end
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Group roster changed - Checking configuration", ModuleName))
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 Heimdall_Data.config.inviter.debug then
-- print(string.format("[%s] Chat message received: %s", ModuleName, msg))
-- shared.dump(Heimdall_Data.config.inviter)
--end
if not Heimdall_Data.config.inviter.enabled then return end
local channelId = select(6, ...)
local _, channelname = GetChannelName(channelId)
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Channel name: %s", ModuleName, channelname))
end
local ok = false
for _, channel in pairs(Heimdall_Data.config.inviter.channels) do
if channel == channelname then
ok = true
break
end
end
if not ok then
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Channel name does not match any of the channels", ModuleName))
end
return
end
if msg == Heimdall_Data.config.inviter.keyword then
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Inviting %s", ModuleName, sender))
end
InviteUnit(sender)
else
if Heimdall_Data.config.inviter.debug then
print(string.format("[%s] Message does not match keyword", ModuleName))
end
end
end)
if Heimdall_Data.config.inviter.debug then
print(
string.format(
"[%s] Module initialized - All assist: %s, Agents assist: %s",
ModuleName,
tostring(Heimdall_Data.config.inviter.allAssist),
tostring(Heimdall_Data.config.inviter.agentsAssist)
)
)
end
if Heimdall_Data.config.inviter.debug then
print(
string.format(
"[%s] Module initialized - All assist: %s, Agents assist: %s",
ModuleName,
tostring(Heimdall_Data.config.inviter.allAssist),
tostring(Heimdall_Data.config.inviter.agentsAssist)
)
)
end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

101
Modules/Macroer.lua Normal file
View File

@@ -0,0 +1,101 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Macroer"
---@class HeimdallMacroerConfig
---@field enabled boolean
---@field debug boolean
---@field priority string[]
---@class Macroer
shared.Macroer = {
Init = function()
local function FindOrCreateMacro(macroName)
if Heimdall_Data.config.macroer.debug then
print(string.format("[%s] Finding or creating macro: %s", ModuleName, macroName))
end
local idx = GetMacroIndexByName(macroName)
if idx == 0 then
if Heimdall_Data.config.macroer.debug then
print(string.format("[%s] Creating new macro: %s", ModuleName, macroName))
end
CreateMacro(macroName, "INV_Misc_QuestionMark", "")
end
idx = GetMacroIndexByName(macroName)
if Heimdall_Data.config.macroer.debug then print(string.format("[%s] Macro index: %d", ModuleName, idx)) end
return idx
end
---@param stinkies table<string, Stinky>
local function FixMacro(stinkies)
if Heimdall_Data.config.macroer.debug then
print(string.format("[%s] Fixing macro with %d stinkies", ModuleName, #stinkies))
end
if not Heimdall_Data.config.macroer.enabled then
if Heimdall_Data.config.macroer.debug then
print(string.format("[%s] Module disabled, skipping macro update", ModuleName))
end
return
end
if InCombatLockdown() then
if Heimdall_Data.config.macroer.debug then
print(string.format("[%s] In combat, skipping macro update", ModuleName))
end
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
if not shared.AgentTracker.IsAgent(stinky.name) then sortedStinkies[#sortedStinkies + 1] = stinky end
end
if Heimdall_Data.config.macroer.debug then
print(string.format("[%s] Processing %d non-agent stinkies", ModuleName, #sortedStinkies))
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)
if Heimdall_Data.config.macroer.debug then
print(string.format("[%s] Sorted stinkies: %d", ModuleName, #sortedStinkies))
shared.dump(sortedStinkies)
end
local lines = { "/targetenemy" }
for _, stinky in pairs(sortedStinkies) do
if stinky.seenAt > GetTime() - 600 then
if Heimdall_Data.config.macroer.debug then
print(string.format("[%s] Adding target macro for: %s", ModuleName, stinky.name))
end
lines[#lines + 1] = string.format("/tar %s", stinky.name)
end
end
local idx = FindOrCreateMacro("HeimdallTarget")
---@diagnostic disable-next-line: param-type-mismatch
local body = strjoin("\n", unpack(lines))
if Heimdall_Data.config.macroer.debug then
print(string.format("[%s] Updating macro with %d lines", ModuleName, #lines))
end
EditMacro(idx, "HeimdallTarget", "INV_Misc_QuestionMark", body)
end
shared.StinkyTracker.OnChange(function(stinkies)
if Heimdall_Data.config.macroer.debug then
print(string.format("[%s] Stinkies changed, updating macro", ModuleName))
shared.dump(stinkies)
end
FixMacro(stinkies)
end)
if Heimdall_Data.config.macroer.debug then print(string.format("[%s] Module initialized", ModuleName)) end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

176
Modules/Messenger.lua Normal file
View File

@@ -0,0 +1,176 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Messenger"
---@class HeimdallMessengerConfig
---@field enabled boolean
---@field debug boolean
---@field interval number
---@class HeimdallMessengerData
---@field queue ReactiveValue<table<string, Message>>
---@field ticker Timer?
---@class Message
---@field message string
---@field channel string
---@field data string
---@class Messenger
shared.Messenger = {
---@param message Message
Enqueue = function(message) table.insert(shared.messenger.queue, message) end,
Init = function()
shared.messenger = {
queue = ReactiveValue.new({}),
}
local function FindOrJoinChannel(channelName, password)
local channelId = GetChannelName(channelName)
if channelId == 0 then
if Heimdall_Data.config.messenger.debug then
print(string.format("[%s] Channel not found, joining: %s", ModuleName, channelName))
end
if password then
JoinPermanentChannel(channelName, password)
else
JoinPermanentChannel(channelName)
end
end
channelId = GetChannelName(channelName)
if Heimdall_Data.config.messenger.debug then
print(string.format("[%s] Channel found with ID: %s (%s)", ModuleName, channelId, channelName))
end
return channelId
end
if not shared.messenger.ticker then
local function DoMessage()
-- if Heimdall_Data.config.messenger.debug then
-- print(
-- string.format(
-- "[%s] Processing message queue - Size: %d",
-- ModuleName,
-- #shared.messenger.queue:get()
-- )
-- )
-- end
if not Heimdall_Data.config.messenger.enabled then
-- if Heimdall_Data.config.messenger.debug then
-- print(string.format("[%s] Module disabled, skipping message processing", ModuleName))
-- end
return
end
---@type Message
local message = shared.messenger.queue[1]
if not message then
-- if Heimdall_Data.config.messenger.debug then
-- print(string.format("[%s] Message queue empty", ModuleName))
-- end
return
end
if Heimdall_Data.config.messenger.debug then shared.dump(message, "[%s] Processing message:") end
if not message.message or message.message == "" then
if Heimdall_Data.config.messenger.debug then
shared.dump(message, string.format("[%s] Invalid message: empty content", ModuleName))
end
return
end
if not message.channel or message.channel == "" then
if Heimdall_Data.config.messenger.debug then
shared.dump(message, string.format("[%s] Invalid message: no channel specified", ModuleName))
end
return
end
if string.find(message.channel, "^C") then
if Heimdall_Data.config.messenger.debug then
shared.dump(
message,
string.format("[%s] Converting channel type from C to CHANNEL", ModuleName)
)
end
message.channel = "CHANNEL"
elseif string.find(message.channel, "^W") then
if Heimdall_Data.config.messenger.debug then
shared.dump(
message,
string.format("[%s] Converting channel type from W to WHISPER", ModuleName)
)
end
message.channel = "WHISPER"
end
if message.channel == "CHANNEL" and message.data and string.match(message.data, "%D") then
if Heimdall_Data.config.messenger.debug then
shared.dump(message, string.format("[%s] Processing channel message:", ModuleName))
end
local channelId = GetChannelName(message.data)
if channelId == 0 then
if Heimdall_Data.config.messenger.debug then
shared.dump(message, string.format("[%s] Channel not found, joining:", ModuleName))
end
channelId = FindOrJoinChannel(message.data)
if Heimdall_Data.config.messenger.debug then
print(string.format("[%s] Channel join result - ID: %s", ModuleName, channelId))
end
end
message.data = tostring(channelId)
end
table.remove(shared.messenger.queue, 1)
if not message.message or message.message == "" then
if Heimdall_Data.config.messenger.debug then
shared.dump(message, string.format("[%s] Skipping empty message", ModuleName))
end
return
end
if not message.channel or message.channel == "" then
if Heimdall_Data.config.messenger.debug then
shared.dump(message, string.format("[%s] Skipping message with no channel", ModuleName))
end
return
end
if not message.data or message.data == "" then
if Heimdall_Data.config.messenger.debug then
shared.dump(message, string.format("[%s] Skipping message with no data", ModuleName))
end
return
end
if Heimdall_Data.config.messenger.debug then
shared.dump(message, string.format("[%s] Sending message:", ModuleName))
end
if string.len(message.message) > 255 then
shared.dump(message, string.format("[%s] Message too long!!!!: %s", ModuleName, message.message))
return
end
SendChatMessage(message.message, message.channel, nil, message.data)
end
local function Tick()
-- if Heimdall_Data.config.messenger.debug then
-- print(string.format("[%s] Tick - Queue size: %d", ModuleName, #shared.messenger.queue:get()))
-- end
DoMessage()
shared.messenger.ticker = C_Timer.NewTimer(Heimdall_Data.config.messenger.interval, Tick, 1)
end
Tick()
end
if Heimdall_Data.config.messenger.debug then
print(
string.format(
"[%s] Module initialized with interval: %s",
ModuleName,
Heimdall_Data.config.messenger.interval
)
)
end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

597
Modules/MinimapTagger.lua Normal file
View File

@@ -0,0 +1,597 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "MinimapTagger"
---@class HeimdallMinimapTaggerConfig
---@field enabled boolean
---@field debug boolean
---@field channels string[]
---@field throttle number
---@field scale number
---@field tagTTL number
---@field tagSound boolean
---@field tagSoundFile string
---@field tagSoundThrottle number
---@field tagTextureFile string
---@field alertTTL number
---@field alertSound boolean
---@field alertSoundFile string
---@field alertSoundThrottle number
---@field alertTextureFile string
---@field combatTTL number
---@field combatSound boolean
---@field combatSoundFile string
---@field combatSoundThrottle number
---@field combatTextureFile string
---@field helpTTL number
---@field helpSound boolean
---@field helpSoundFile string
---@field helpSoundThrottle number
---@field helpTextureFile string
local HeimdallRoot = "Interface\\AddOns\\Heimdall\\"
local SoundRoot = HeimdallRoot .. "Sounds\\"
local TextureRoot = HeimdallRoot .. "Texture\\"
--/run local a=GetChannelName("Agent")local b,c=GetPlayerMapPosition("player")b,c=b*100,c*100;local d=string.format("I need help at %s (%s) [%s](%2.2f, %2.2f)",GetZoneText(),GetSubZoneText(),GetCurrentMapAreaID(),b,c)SendChatMessage(d,"CHANNEL",nil,a)
---@class MinimapTagger
shared.MinimapTagger = {
Init = function()
---@param x number
---@param y number
---@param frame Frame
---@param scale number?
---@param ttl number?
local function PlantFrame(x, y, frame, scale, ttl)
if not BattlefieldMinimap then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] BattlefieldMinimap not found", ModuleName))
end
return
end
scale = scale or 1
ttl = ttl or 1
local w, h = BattlefieldMinimap:GetSize()
w, h = w * BattlefieldMinimap:GetEffectiveScale(), h * BattlefieldMinimap:GetEffectiveScale()
local maxSize = w > h and w or h
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Minimap size: %d", ModuleName, maxSize))
print(string.format("[%s] Scale: %d", ModuleName, scale))
print(string.format("[%s] TTL: %d", ModuleName, ttl))
end
local iconSize = maxSize * 0.05
iconSize = iconSize * scale
x, y = x / 100, y / 100
-- Could do with how... I have no idea, but this seems more accurate than without
--x, y = x - 0.01, y - 0.01
local offsetx, offsety = w * x, h * y
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Alert position: %d, %d", ModuleName, x, y))
print(string.format("[%s] Alert offset: %d, %d", ModuleName, offsetx, offsety))
end
frame:Hide()
frame:SetSize(iconSize, iconSize)
frame:SetFrameStrata("HIGH")
frame:SetFrameLevel(100)
frame:SetPoint("CENTER", BattlefieldMinimap, "TOPLEFT", offsetx, -offsety)
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Alert frame created, OnUpdate hooked", ModuleName))
end
frame:SetScript("OnShow", function(self)
self:SetAlpha(1)
self.custom.busy = true
self.custom.progress = 0
self:SetScript("OnUpdate", function(selff, elapsed)
self.custom.progress = self.custom.progress + elapsed
local progress = self.custom.progress / ttl
-- if Heimdall_Data.config.minimapTagger.debug then
-- print(string.format("[%s] Alert progress%%: %f", ModuleName, progress))
-- print(string.format("[%s] Alert progress: %f", ModuleName, self.custom.progress))
-- print(string.format("[%s] Alert ttl: %d", ModuleName, Heimdall_Data.config.minimapTagger.ttl))
-- end
self:SetAlpha(1 - progress)
if progress >= 1 then
self:Hide()
self.custom.busy = false
self:SetScript("OnUpdate", nil)
end
end)
end)
frame:Show()
end
--region Alert
---@type Frame[]
local alertFramePool = {}
local alertFramePoolMaxSize = 20
for i = 1, alertFramePoolMaxSize do
local frame = CreateFrame("Frame")
frame.custom = { busy = false }
local texture = frame:CreateTexture(nil, "ARTWORK")
texture:SetAllPoints(frame)
texture:SetTexture(TextureRoot .. Heimdall_Data.config.minimapTagger.alertTextureFile)
table.insert(alertFramePool, frame)
end
local muteAlertUntil = 0
---@param x number|nil
---@param y number|nil
---@param scale number?
---@param doTag boolean?
local function PlantAlert(x, y, scale, doTag)
if x == nil or y == nil then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Alert position is nil, ignoring", ModuleName))
end
return
end
if doTag == nil then doTag = true end
local frame = nil
for _, alertFrame in ipairs(alertFramePool) do
---@diagnostic disable-next-line: undefined-field
if not alertFrame.custom.busy then
frame = alertFrame
break
end
end
if not frame then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Alert frame pool is full and could not get frame", ModuleName))
end
return
end
if Heimdall_Data.config.minimapTagger.alertSound then
if Heimdall_Data.config.minimapTagger.debug then
print(
string.format(
"[%s] Playing alert sound: %s",
ModuleName,
Heimdall_Data.config.minimapTagger.alertSoundFile
)
)
end
if muteAlertUntil > GetTime() then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Alert sound is muted until %d", ModuleName, muteAlertUntil))
end
else
muteAlertUntil = GetTime() + Heimdall_Data.config.minimapTagger.alertSoundThrottle
local ok = PlaySoundFile(SoundRoot .. Heimdall_Data.config.minimapTagger.alertSoundFile, "Master")
if not ok and Heimdall_Data.config.minimapTagger.debug then
print(
string.format(
"[%s] Failed to play alert sound: %s",
ModuleName,
Heimdall_Data.config.minimapTagger.alertSoundFile
)
)
end
end
end
if doTag then PlantFrame(x, y, frame, scale, Heimdall_Data.config.minimapTagger.alertTTL) end
end
--endregion
--region Tag
---@type Frame[]
local tagFramePool = {}
local tagFramePoolMaxSize = 20
for i = 1, tagFramePoolMaxSize do
local frame = CreateFrame("Frame")
frame.custom = { busy = false }
local texture = frame:CreateTexture(nil, "ARTWORK")
texture:SetAllPoints(frame)
texture:SetTexture(TextureRoot .. Heimdall_Data.config.minimapTagger.tagTextureFile)
table.insert(tagFramePool, frame)
end
local muteTagUntil = 0
---@param x number|nil
---@param y number|nil
---@param scale number?
---@param doTag boolean?
local function PlantTag(x, y, scale, doTag)
if x == nil or y == nil then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Tag position is nil, ignoring", ModuleName))
end
return
end
if doTag == nil then doTag = true end
local frame = nil
for _, tagFrame in ipairs(tagFramePool) do
---@diagnostic disable-next-line: undefined-field
if not tagFrame.custom.busy then
frame = tagFrame
break
end
end
if not frame then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Tag frame pool is full and could not get frame", ModuleName))
end
return
end
if Heimdall_Data.config.minimapTagger.tagSound then
if Heimdall_Data.config.minimapTagger.debug then
print(
string.format(
"[%s] Playing tag sound: %s",
ModuleName,
Heimdall_Data.config.minimapTagger.tagSoundFile
)
)
end
if muteTagUntil > GetTime() then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Tag sound is muted until %d", ModuleName, muteTagUntil))
end
else
muteTagUntil = GetTime() + Heimdall_Data.config.minimapTagger.tagSoundThrottle
local ok = PlaySoundFile(SoundRoot .. Heimdall_Data.config.minimapTagger.tagSoundFile, "Master")
if not ok and Heimdall_Data.config.minimapTagger.debug then
print(
string.format(
"[%s] Failed to play tag sound: %s",
ModuleName,
Heimdall_Data.config.minimapTagger.tagSoundFile
)
)
end
end
end
if doTag then PlantFrame(x, y, frame, scale, Heimdall_Data.config.minimapTagger.tagTTL) end
end
--endregion
--region Combat
---@type Frame[]
local combatFramePool = {}
local combatFramePoolMaxSize = 20
for i = 1, combatFramePoolMaxSize do
local frame = CreateFrame("Frame")
frame.custom = { busy = false }
local texture = frame:CreateTexture(nil, "ARTWORK")
texture:SetAllPoints(frame)
texture:SetTexture(TextureRoot .. Heimdall_Data.config.minimapTagger.combatTextureFile)
table.insert(combatFramePool, frame)
end
local muteCombatUntil = 0
---@param x number|nil
---@param y number|nil
---@param scale number?
---@param doTag boolean?
local function PlantCombat(x, y, scale, doTag)
if x == nil or y == nil then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Combat position is nil, ignoring", ModuleName))
end
return
end
if doTag == nil then doTag = true end
local frame = nil
for _, combatFrame in ipairs(combatFramePool) do
---@diagnostic disable-next-line: undefined-field
if not combatFrame.custom.busy then
frame = combatFrame
break
end
end
if not frame then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Battle frame pool is full and could not get frame", ModuleName))
end
return
end
if Heimdall_Data.config.minimapTagger.combatSound then
if Heimdall_Data.config.minimapTagger.debug then
print(
string.format(
"[%s] Playing combat sound: %s",
ModuleName,
Heimdall_Data.config.minimapTagger.combatSoundFile
)
)
end
if muteCombatUntil > GetTime() then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Combat sound is muted until %d", ModuleName, muteCombatUntil))
end
else
muteCombatUntil = GetTime() + Heimdall_Data.config.minimapTagger.combatSoundThrottle
local ok = PlaySoundFile(SoundRoot .. Heimdall_Data.config.minimapTagger.combatSoundFile, "Master")
if not ok and Heimdall_Data.config.minimapTagger.debug then
print(
string.format(
"[%s] Failed to play combat sound: %s",
ModuleName,
Heimdall_Data.config.minimapTagger.combatSoundFile
)
)
end
end
end
if doTag then PlantFrame(x, y, frame, scale, Heimdall_Data.config.minimapTagger.combatTTL) end
end
--endregion
--region Help
---@type Frame[]
local helpFramePool = {}
local helpFramePoolMaxSize = 20
for i = 1, helpFramePoolMaxSize do
local frame = CreateFrame("Frame")
frame.custom = { busy = false }
local texture = frame:CreateTexture(nil, "ARTWORK")
texture:SetAllPoints(frame)
texture:SetTexture(TextureRoot .. Heimdall_Data.config.minimapTagger.helpTextureFile)
table.insert(helpFramePool, frame)
end
local muteHelpUntil = 0
---@param x number|nil
---@param y number|nil
---@param scale number?
---@param doTag boolean?
local function PlantHelp(x, y, scale, doTag)
if x == nil or y == nil then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Help position is nil, ignoring", ModuleName))
end
return
end
if doTag == nil then doTag = true end
local frame = nil
for _, helpFrame in ipairs(helpFramePool) do
---@diagnostic disable-next-line: undefined-field
if not helpFrame.custom.busy then
frame = helpFrame
break
end
end
if not frame then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Help frame pool is full and could not get frame", ModuleName))
end
return
end
if Heimdall_Data.config.minimapTagger.helpSound then
if Heimdall_Data.config.minimapTagger.debug then
print(
string.format(
"[%s] Playing help sound: %s",
ModuleName,
Heimdall_Data.config.minimapTagger.helpSoundFile
)
)
end
if muteHelpUntil > GetTime() then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Help sound is muted until %d", ModuleName, muteHelpUntil))
end
else
muteHelpUntil = GetTime() + Heimdall_Data.config.minimapTagger.helpSoundThrottle
local ok = PlaySoundFile(SoundRoot .. Heimdall_Data.config.minimapTagger.helpSoundFile, "Master")
if not ok and Heimdall_Data.config.minimapTagger.debug then
print(
string.format(
"[%s] Failed to play help sound: %s",
ModuleName,
Heimdall_Data.config.minimapTagger.helpSoundFile
)
)
end
end
end
if doTag then PlantFrame(x, y, frame, scale, Heimdall_Data.config.minimapTagger.helpTTL) end
end
--endregion
local pauseUntil = 0
local frame = CreateFrame("Frame")
frame:RegisterEvent("WORLD_MAP_UPDATE")
frame:SetScript("OnEvent", function(self, event, addon)
if pauseUntil > GetTime() then return end
pauseUntil = GetTime() + 1
if not BattlefieldMinimap then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] BattlefieldMinimap not found", ModuleName))
end
return
end
if not Heimdall_Data.config.minimapTagger.enabled then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] MinimapTagger is disabled", ModuleName))
end
return
end
local scale = Heimdall_Data.config.minimapTagger.scale
BattlefieldMinimap:SetScale(scale)
BattlefieldMinimap:SetMovable(true)
BattlefieldMinimap:EnableMouse(true)
BattlefieldMinimap:RegisterForDrag("LeftButton")
BattlefieldMinimap:SetScript("OnDragStart", function(selff) selff:StartMoving() end)
BattlefieldMinimap:SetScript("OnDragStop", function(selff) selff:StopMovingOrSizing() end)
BattlefieldMinimapBackground:Hide()
BattlefieldMinimapCloseButton:Hide()
BattlefieldMinimapCorner:Hide()
BattlefieldMinimap:HookScript("OnHide", function(selff)
for _, alertFrame in ipairs(alertFramePool) do
alertFrame:Hide()
---@diagnostic disable-next-line: undefined-field
alertFrame.custom.busy = false
end
for _, tagFrame in ipairs(tagFramePool) do
tagFrame:Hide()
---@diagnostic disable-next-line: undefined-field
tagFrame.custom.busy = false
end
-- What the fuck is this global?
for _, battleFrame in ipairs(battleFramePool) do
battleFrame:Hide()
battleFrame.custom.busy = false
end
end)
end)
local chatFrame = CreateFrame("Frame")
chatFrame:RegisterEvent("CHAT_MSG_CHANNEL")
chatFrame:SetScript("OnEvent", function(self, event, msg, sender, ...)
--if Heimdall_Data.config.echoer.debug then
-- print(string.format("[%s] Channel message received from: %s", ModuleName, sender))
--end
if not Heimdall_Data.config.minimapTagger.enabled then
--if Heimdall_Data.config.echoer.debug then
-- print(string.format("[%s] Module disabled, ignoring message", ModuleName))
--end
return
end
local channelId = select(6, ...)
local _, channelname = GetChannelName(channelId)
local ok = false
for _, channel in pairs(Heimdall_Data.config.minimapTagger.channels) do
if channelname == channel then
ok = true
break
end
end
if not ok then
if Heimdall_Data.config.minimapTagger.debug then
print(
string.format(
"[%s] Ignoring message from non-master channel: %s, need %s",
ModuleName,
channelname,
Heimdall_Data.config.minimapTagger.masterChannel
)
)
end
return
end
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Processing message from master channel: %s", ModuleName, sender))
shared.dump(Heimdall_Data.config.minimapTagger)
end
local doTag = true
local messageMapId = string.match(msg, "%[(%d+)%]") or 0
if messageMapId then messageMapId = tonumber(messageMapId) end
local currentMapId = GetCurrentMapAreaID()
if currentMapId ~= messageMapId then
if Heimdall_Data.config.minimapTagger.debug then
print(
string.format(
"[%s] Current map ID (%d) does not match message map ID (%d), ignoring message",
ModuleName,
currentMapId,
messageMapId
)
)
end
doTag = false
end
--region Tag
if string.find(msg, "^I see") then
if Heimdall_Data.config.minimapTagger.tagTTL == 0 then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Tag TTL is 0, ignoring message: %s", ModuleName, msg))
end
return
end
local x, y = string.match(msg, "%((%d+%.%d+)%s*,%s*(%d+%.%d+)%)")
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Found alert position: %s, %s", ModuleName, tostring(x), tostring(y)))
end
if x and y then PlantTag(tonumber(x), tonumber(y), 2, doTag) end
end
--endregion
--region Combat
if string.find(msg, "^I am in combat with") then
if Heimdall_Data.config.minimapTagger.combatTTL == 0 then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Combat TTL is 0, ignoring message: %s", ModuleName, msg))
end
return
end
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Found combat alert in message: %s", ModuleName, msg))
end
local x, y = string.match(msg, "%((%d+%.%d+)%s*,%s*(%d+%.%d+)%)")
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Found combat position: %s, %s", ModuleName, tostring(x), tostring(y)))
end
if x and y then PlantCombat(tonumber(x), tonumber(y), 2, doTag) end
end
--endregion
--region Death
if string.find(msg, " killed ") then
if Heimdall_Data.config.minimapTagger.alertTTL == 0 then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Alert TTL is 0, ignoring message: %s", ModuleName, msg))
end
return
end
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Found death alert in message: %s", ModuleName, msg))
end
local x, y = string.match(msg, "%((%d+%.%d+)%s*,%s*(%d+%.%d+)%)")
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Found death position: %s, %s", ModuleName, tostring(x), tostring(y)))
end
if x and y then PlantAlert(tonumber(x), tonumber(y), 2, doTag) end
end
--endregion
--region Help
if string.find(msg, "I need help") then
if Heimdall_Data.config.minimapTagger.helpTTL == 0 then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Help TTL is 0, ignoring message: %s", ModuleName, msg))
end
return
end
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Found help alert in message: %s", ModuleName, msg))
end
local x, y = string.match(msg, "%((%d+%.%d+)%s*,%s*(%d+%.%d+)%)")
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Found help position: %s, %s", ModuleName, tostring(x), tostring(y)))
end
if x and y then
x, y = tonumber(x), tonumber(y)
PlantHelp(x, y, 1, doTag)
---@diagnostic disable-next-line: undefined-global
if TomTom then
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Adding help waypoint to TomTom", ModuleName))
end
local areaId = string.match(msg, "%[(%d+)%]") or 0
if areaId then areaId = tonumber(areaId) end
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] Area ID: %s", ModuleName, tostring(areaId)))
end
---@diagnostic disable-next-line: undefined-global
TomTom:AddMFWaypoint(areaId, nil, x / 100, y / 100, {
title = "Help " .. sender,
world = true,
from = "Heimdall",
crazy = true,
})
else
if Heimdall_Data.config.minimapTagger.debug then
print(string.format("[%s] No tomtom no waypoint", ModuleName))
end
end
end
end
--endregion
end)
print(string.format("[%s] Module initialized", ModuleName))
end,
}

85
Modules/Network.lua Normal file
View File

@@ -0,0 +1,85 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Network"
---@class HeimdallNetworkConfig
---@field enabled boolean
---@field debug boolean
---@field members string[]
---@field updateInterval number
---@class HeimdallNetworkData
---@field ticker Timer?
---@class Network
shared.Network = {
Init = function()
if not shared.network then shared.network = {} end
local updatePending = false
local function FriendListUpdate()
updatePending = false
if not Heimdall_Data.config.network.enabled then return end
---@type table<string, boolean>
local friends = {}
for i = 1, GetNumFriends() do
local name, _, _, _, connected, _, _, _ = GetFriendInfo(i)
if name then
friends[name] = connected
if Heimdall_Data.config.network.debug then
print(
string.format("[%s] Friend %s is %s", ModuleName, name, connected and "online" or "offline")
)
end
else
if Heimdall_Data.config.network.debug then
print(string.format("[%s] Friend %s is nil", ModuleName, i))
end
end
end
for _, member in ipairs(Heimdall_Data.config.network.members) do
if friends[member] == nil and member ~= UnitName("player") then
if Heimdall_Data.config.network.debug then
print(string.format("[%s] Adding friend %s", ModuleName, member))
end
AddFriend(member)
end
end
friends[UnitName("player")] = true
shared.networkNodes = {}
-- Why are we skipping this again...?
-- if false then shared.networkNodes[#shared.networkNodes + 1] = UnitName("player") end
for _, player in ipairs(Heimdall_Data.config.network.members) do
if friends[player] then
shared.networkNodes[#shared.networkNodes + 1] = player
if Heimdall_Data.config.network.debug then
print(string.format("[%s] Adding network node %s", ModuleName, player))
end
end
end
if Heimdall_Data.config.network.debug then
print(string.format("[%s] Network nodes:", ModuleName))
shared.dump(shared.networkNodes)
end
end
local friendsFrame = CreateFrame("Frame")
friendsFrame:RegisterEvent("FRIENDLIST_UPDATE")
friendsFrame:SetScript("OnEvent", function(self, event, ...) end)
local function NetworkTick()
if Heimdall_Data.config.network.debug then print("Network module is updating.") end
ShowFriends()
updatePending = true
C_Timer.After(1, function()
if updatePending then FriendListUpdate() end
end)
shared.network.ticker = C_Timer.NewTimer(Heimdall_Data.config.network.updateInterval, NetworkTick, 1)
end
NetworkTick()
print(string.format("[%s] Module initialized", ModuleName))
end,
}

View File

@@ -0,0 +1,204 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "NetworkMessenger"
---@class HeimdallNetworkMessengerConfig
---@field enabled boolean
---@field debug boolean
---@field interval number
---@class HeimdallNetworkMessengerData
---@field queue table<string, Message>
---@field ticker Timer?
---@class NetworkMessenger
shared.NetworkMessenger = {
---@param message Message
Enqueue = function(message) table.insert(shared.networkMessenger.queue, message) end,
Init = function()
RegisterAddonMessagePrefix(Heimdall_Data.config.addonPrefix)
shared.networkMessenger = {
queue = ReactiveValue.new({}),
}
if not shared.networkMessenger.ticker then
local function DoMessage()
--if Heimdall_Data.config.networkMessenger.debug then
-- print(string.format("[%s] Processing network message queue", ModuleName))
--end
if not Heimdall_Data.config.networkMessenger.enabled then
--if Heimdall_Data.config.networkMessenger.debug then
-- print(string.format("[%s] Module disabled, skipping network message processing", ModuleName))
--end
return
end
---@type Message
local message = shared.networkMessenger.queue[1]
if not message then
--if Heimdall_Data.config.networkMessenger.debug then
-- print(string.format("[%s] Network message queue empty", ModuleName))
--end
return
end
if not message.message or message.message == "" then
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] Invalid network message: empty content", ModuleName))
end
return
end
if not message.channel or message.channel == "" then
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] Invalid network message: no channel specified", ModuleName))
end
return
end
table.remove(shared.networkMessenger.queue, 1)
if not message.message or message.message == "" then
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] Skipping empty network message", ModuleName))
end
return
end
if not message.channel or message.channel == "" then
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] Skipping network message with no channel", ModuleName))
end
return
end
if not message.data or message.data == "" then
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] Skipping network message with no data", ModuleName))
end
return
end
if Heimdall_Data.config.networkMessenger.debug then
print(
string.format(
"[%s] Sending network message: '%s' to %s:%s",
ModuleName,
message.message,
message.channel,
message.data
)
)
end
local payload = string.format("dmessage|%s|%s|%s", message.message, message.channel, message.data)
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] Payload: %s", ModuleName, payload))
end
if not shared.networkNodes or #shared.networkNodes == 0 then
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] No network nodes found, wtf????", ModuleName))
end
return
end
local target = shared.networkNodes[1]
SendAddonMessage(Heimdall_Data.config.addonPrefix, payload, "WHISPER", target)
end
local function Tick()
--if Heimdall_Data.config.networkMessenger.debug then
-- local queueSize = #shared.networkMessenger.queue
-- print(string.format("[%s] Queue check - Network messages pending: %d", ModuleName, queueSize))
--end
DoMessage()
shared.networkMessenger.ticker =
C_Timer.NewTimer(Heimdall_Data.config.networkMessenger.interval, Tick, 1)
end
Tick()
end
-- If we are the leader then we delegate messages (dmessage)
-- If we get a "message" command from leader then we send the message
local nextIdx = 1
local addonMsgFrame = CreateFrame("Frame")
addonMsgFrame:RegisterEvent("CHAT_MSG_ADDON")
addonMsgFrame:SetScript("OnEvent", function(self, event, prefix, message, channel, source)
if not Heimdall_Data.config.networkMessenger.enabled then return end
if prefix ~= Heimdall_Data.config.addonPrefix then return end
source = string.match(source, "[^%-]+")
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] Received message from %s: %s", ModuleName, source, message))
end
if #shared.networkNodes == 0 then
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] No network nodes found, wtf????", ModuleName))
end
return
end
-- There should always be at least one network node ergo should always exist a leader
-- Because the us, the player, is also a node
--local networkLeader = shared.networkNodes[1]
--if source ~= networkLeader then
-- if Heimdall_Data.config.networkMessenger.debug then
-- print(string.format("[%s] Message from %s is not from the network leader (%s)", ModuleName, source,
-- networkLeader))
-- end
-- return
--end
local parts = shared.Split(message, "|")
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] Received message parts:", ModuleName))
shared.dump(parts)
end
local command = strtrim(parts[1])
if command == "message" then
local content = strtrim(tostring(parts[2]))
local targetchannel = strtrim(tostring(parts[3]))
local target = strtrim(tostring(parts[4]))
if Heimdall_Data.config.networkMessenger.debug then
print(
string.format(
"[%s] Received message command: %s %s %s",
ModuleName,
content,
targetchannel,
target
)
)
end
---@type Message
local msg = {
channel = targetchannel,
message = content,
data = target,
}
table.insert(shared.messenger.queue, msg)
elseif command == "dmessage" then
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] Received dmessage command", ModuleName))
end
parts[1] = "message"
local content = table.concat(parts, "|")
if nextIdx > #shared.networkNodes then nextIdx = 1 end
local recipient = shared.networkNodes[nextIdx]
nextIdx = nextIdx + 1
if Heimdall_Data.config.networkMessenger.debug then
print(string.format("[%s] Sending message %s to %s", ModuleName, content, recipient))
end
SendAddonMessage(Heimdall_Data.config.addonPrefix, content, "WHISPER", recipient)
end
end)
--/run Heimdall_Data.Test()
Heimdall_Data.Test = function()
local testmsg = {
channel = "W",
message = "Hi, mom!",
data = "Secundus",
}
for i = 1, 36 do
table.insert(shared.networkMessenger.queue, testmsg)
end
end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

307
Modules/Noter.lua Normal file
View File

@@ -0,0 +1,307 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Noter"
---@class HeimdallNoterConfig
---@field enabled boolean
---@field debug boolean
---@field channels string[]
---@field lastNotes number
---@class Note
---@field source string
---@field for string
---@field date string
---@field note string
---@class Noter
shared.Noter = {
Init = function()
-- ---Hopefully this will not be necessary
-- ---@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 array any[]
---@return any[]
local function Compact(array)
local compacted = {}
for _, v in pairs(array) do
compacted[#compacted + 1] = v
end
return compacted
end
---@param name string
---@param args string[]
local function DeleteNotes(name, args)
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Delete note command received for: %s", ModuleName, name))
end
local range = args[4]
if range then
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Range received for delete note: %s", ModuleName, range))
end
local indices = shared.Split(range, "..")
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Indices for range deletion: %s", ModuleName, table.concat(indices, ", ")))
shared.dump(indices)
end
local start = tonumber(indices[1])
local finish = tonumber(indices[2])
if not start then
if Heimdall_Data.config.noter.debug then
print(
string.format("[%s] Invalid start range for delete note: %s", ModuleName, tostring(start))
)
end
return
end
if not finish then finish = start end
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Deleting note range %s to %s for: %s", ModuleName, start, finish, name))
end
-- Here, because we are deleting random notes, we lose the "iterative" index property
-- Ie it's not longer 1..100, it might be 1..47, 50, 68..100
-- Which means that we cannot use ipairs, bad!
for i = start, finish do
if not Heimdall_Data.config.notes[name] then Heimdall_Data.config.notes[name] = {} end
if not Heimdall_Data.config.notes[name][i] then
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Note at index %s does not exist", ModuleName, i))
end
else
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Deleting note %s at index %s", ModuleName, name, i))
shared.dump(Heimdall_Data.config.notes[name][i])
end
Heimdall_Data.config.notes[name][i] = nil
end
end
Heimdall_Data.config.notes[name] = Compact(Heimdall_Data.config.notes[name])
end
end
---@param channel string
---@param index number
---@param note Note
local function PrintNote(channel, index, note)
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Printing note at index %d for: %s", ModuleName, index, note.source))
print(string.format("[%s] [%s][%d] %s: %s", ModuleName, note.source, index, note.date, note.note))
end
---@type Message
local msg = {
channel = "C",
data = channel,
message = string.format("[%s][%d] %s: %s", note.source, index, note.date, note.note),
}
if Heimdall_Data.config.networkMessenger.enabled then
shared.NetworkMessenger.Enqueue(msg)
elseif Heimdall_Data.config.messenger.enabled then
shared.Messenger.Enqueue(msg)
end
end
---@param name string
---@param args string[]
local function PrintNotes(channel, name, args)
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Print note command received for: %s", ModuleName, name))
end
local range = args[3]
if not range then
if Heimdall_Data.config.noter.debug then
print(
string.format(
"[%s] No range specified for print note, defaulting to last %d notes",
ModuleName,
Heimdall_Data.config.noter.lastNotes
)
)
end
local notes = Heimdall_Data.config.notes[name] or {}
local start = math.max(1, #notes - Heimdall_Data.config.noter.lastNotes + 1)
local finish = #notes
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Printing notes from %d to %d for: %s", ModuleName, start, finish, name))
end
for i = start, finish do
PrintNote(channel, i, notes[i])
end
return
end
if range then
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Range received for print note: %s", ModuleName, range))
end
local indices = shared.Split(range, "..")
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Indices for range printing: %s", ModuleName, table.concat(indices, ", ")))
shared.dump(indices)
end
local start = tonumber(indices[1])
local finish = tonumber(indices[2])
if not start then
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Invalid start range for print note: %s", ModuleName, tostring(start)))
end
return
end
if not finish then finish = start end
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Printing note range %s to %s for: %s", ModuleName, start, finish, name))
end
for i = start, finish do
if not Heimdall_Data.config.notes[name] then Heimdall_Data.config.notes[name] = {} end
if not Heimdall_Data.config.notes[name][i] then
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Note at index %s does not exist", ModuleName, i))
end
else
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Printing note %s at index %s", ModuleName, name, i))
shared.dump(Heimdall_Data.config.notes[name][i])
end
PrintNote(channel, i, Heimdall_Data.config.notes[name][i])
end
end
end
end
---@param name string
---@param sender string
---@param args string[]
local function AddNote(name, sender, args)
if not Heimdall_Data.config.notes[name] then Heimdall_Data.config.notes[name] = {} end
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Adding note for: %s from: %s", ModuleName, name, sender))
shared.dump(args)
end
local msgparts = {}
for i = 3, #args do
msgparts[#msgparts + 1] = args[i]
end
local msg = table.concat(msgparts, " ")
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Adding note for: %s from: %s", ModuleName, name, sender))
print(string.format("[%s] Note: %s", ModuleName, msg))
end
local note = {
source = sender,
date = date("%Y-%m-%dT%H:%M:%S"),
note = msg,
}
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Adding note", ModuleName))
shared.dump(note)
end
table.insert(Heimdall_Data.config.notes[name], note)
end
-- Here's the plan:
-- Implement a "note" command, that will do everything
-- Saying "note <name> <note>" will add a note to the list for the character
-- Saying "note <name>" will list last N notes
-- Saying "note <name> i" will list the i-th note
-- Saying "note <name> i..j" will list notes from i to j
-- Saying "note <name> delete i" will delete the i-th note
-- Saying "note <name> delete i..j" will delete notes from i to j
local noterChannelFrame = CreateFrame("Frame")
noterChannelFrame:RegisterEvent("CHAT_MSG_CHANNEL")
noterChannelFrame:SetScript("OnEvent", function(self, event, msg, sender, ...)
--if Heimdall_Data.config.noter.debug then
-- print(string.format("[%s] Event received", ModuleName))
-- shared.dump(Heimdall_Data.config.noter)
--end
if not Heimdall_Data.config.noter.enabled then
--if Heimdall_Data.config.noter.debug then
-- print(string.format("[%s] Module disabled, ignoring event", ModuleName))
--end
return
end
local channelId = select(6, ...)
local _, channelname = GetChannelName(channelId)
local ok = false
for _, channel in pairs(Heimdall_Data.config.noter.channels) do
if channelname == channel then
ok = true
break
end
end
if not ok then
--if Heimdall_Data.config.noter.debug then
-- print(string.format("[%s] Channel %s does not match the master channel %s", ModuleName, channelname, Heimdall_Data.config.noter.masterChannel))
--end
return
end
sender = string.match(sender, "^[^-]+")
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Message from: %s", ModuleName, sender))
shared.dump(Heimdall_Data.config.noter)
end
if not msg or msg == "" then
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Empty message, ignoring", ModuleName))
end
return
end
local args = { strsplit(" ", msg) }
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Arguments received: %s", ModuleName, table.concat(args, ", ")))
shared.dump(args)
end
local command = args[1]
if command == "note" then
local name = strtrim(string.lower(args[2] or ""))
if Heimdall_Data.config.noter.debug then
print(string.format("[%s] Note command received for: %s", ModuleName, name))
end
local note = strtrim(args[3] or "")
if Heimdall_Data.config.noter.debug then print(string.format("[%s] Note: %s", ModuleName, note)) end
if note == "delete" then
DeleteNotes(name, args)
elseif string.find(note, "^[%d%.]*$") then
PrintNotes(channelname, name, args)
else
AddNote(name, sender, args)
end
end
end)
print(string.format("[%s] Module initialized", ModuleName))
end,
}

609
Modules/ReactiveValue.lua Normal file
View File

@@ -0,0 +1,609 @@
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 dump(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 dump(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 dump(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()

92
Modules/Sniffer.lua Normal file
View File

@@ -0,0 +1,92 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Sniffer"
---@class HeimdallSnifferConfig
---@field enabled boolean
---@field debug boolean
---@field channels string[]
---@field throttle number -- throttleTime in the original code, matching config name now
---@field zoneOverride string?
---@field stinky boolean
---@class Sniffer
shared.Sniffer = {
Init = function()
if Heimdall_Data.config.sniffer.debug then print(string.format("[%s] Module initializing", ModuleName)) end
local smellThrottle = {}
local SmellStinky = function(stinky)
if Heimdall_Data.config.sniffer.debug then
print(string.format("%s: SmellStinky", ModuleName))
shared.dump(Heimdall_Data.config.sniffer)
end
if not Heimdall_Data.config.sniffer.enabled then return end
if Heimdall_Data.config.sniffer.stinky and not shared.IsStinky(stinky) then
if Heimdall_Data.config.sniffer.debug then
print(string.format("%s: Stinky not found in config", ModuleName))
end
return
end
if smellThrottle[stinky] and GetTime() - smellThrottle[stinky] < Heimdall_Data.config.sniffer.throttle then
if Heimdall_Data.config.sniffer.debug then print(string.format("%s: Throttled", ModuleName)) end
return
end
smellThrottle[stinky] = GetTime()
for _, channel in pairs(Heimdall_Data.config.sniffer.channels) do
local locale = shared.GetLocaleForChannel(channel)
local text = string.format(shared._L("snifferStinky", locale), stinky)
---@type Message
local msg = {
channel = "C",
data = channel,
message = text,
}
if Heimdall_Data.config.sniffer.debug then
print(string.format("[%s] Queuing sniffer message", ModuleName))
shared.dump(msg)
end
table.insert(shared.messenger.queue, msg)
end
end
local cleuFrame = CreateFrame("Frame")
cleuFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
cleuFrame:SetScript("OnEvent", function(self, event, ...)
if Heimdall_Data.config.sniffer.debug then
print(string.format("[%s] Received event: %s", ModuleName, event))
end
if not Heimdall_Data.config.sniffer.enabled then
if Heimdall_Data.config.sniffer.debug then
print(string.format("[%s] Module disabled, ignoring event", ModuleName))
end
return
end
local source, destination, err
source, err = CLEUParser.GetSourceName(...)
if Heimdall_Data.config.sniffer.debug then
print(string.format("[%s] Processing source: %s", ModuleName, source))
end
if err then
if Heimdall_Data.config.sniffer.debug then
print(string.format("[%s] Error parsing source: %s", ModuleName, err))
end
return
end
SmellStinky(source)
destination, err = CLEUParser.GetDestName(...)
if Heimdall_Data.config.sniffer.debug then
print(string.format("[%s] Processing destination: %s", ModuleName, destination))
end
if err then
if Heimdall_Data.config.sniffer.debug then
print(string.format("[%s] Error parsing destination: %s", ModuleName, err))
end
return
end
SmellStinky(destination)
end)
if Heimdall_Data.config.sniffer.debug then print(string.format("[%s] Module initialized", ModuleName)) end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

238
Modules/Spotter.lua Normal file
View File

@@ -0,0 +1,238 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Spotter"
---@class HeimdallSpotterConfig
---@field enabled boolean
---@field debug boolean
---@field everyone boolean
---@field hostile boolean
---@field alliance boolean
---@field stinky boolean
---@field channels string[]
---@field zoneOverride string?
---@field throttleTime number
---@class Spotter
shared.Spotter = {
Init = function()
local function FormatHP(hp)
if hp > 1e9 then
return string.format("%.1fB", hp / 1e9)
elseif hp > 1e6 then
return string.format("%.1fM", hp / 1e6)
elseif hp > 1e3 then
return string.format("%.1fK", hp / 1e3)
else
return hp
end
end
---@type table<string, number>
local throttleTable = {}
---@param unit string
---@param name string
---@param faction string
---@param hostile boolean
---@return boolean
---@return string? error
local function ShouldNotify(unit, name, faction, hostile)
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Checking notification criteria for %s (%s)", ModuleName, name, faction))
end
if shared.AgentTracker.IsAgent(name) then
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Skipping agent: %s", ModuleName, name))
end
return false
end
if Heimdall_Data.config.spotter.stinky then
if shared.IsStinky(name) then
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Notifying - Found stinky: %s", ModuleName, name))
end
return true
end
end
if Heimdall_Data.config.spotter.alliance then
if faction == "Alliance" then
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Notifying - Found Alliance player: %s", ModuleName, name))
end
return true
end
end
if Heimdall_Data.config.spotter.hostile then
if hostile then
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Notifying - Found hostile player: %s", ModuleName, name))
end
return true
end
end
if Heimdall_Data.config.spotter.debug then
print(
string.format(
"[%s] Using everyone setting: %s",
ModuleName,
tostring(Heimdall_Data.config.spotter.everyone)
)
)
end
return Heimdall_Data.config.spotter.everyone
end
---@param unit string
---@return string?
local function NotifySpotted(unit)
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Processing spotted unit: %s", ModuleName, unit))
end
if not unit then return string.format("Could not find unit %s", tostring(unit)) end
if not UnitIsPlayer(unit) then
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Ignoring non-player unit: %s", ModuleName, unit))
end
return nil
end
local name = UnitName(unit)
if not name then return string.format("Could not find name for unit %s", tostring(unit)) end
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Processing player: %s", ModuleName, name))
end
local time = GetTime()
if throttleTable[name] and time - throttleTable[name] < Heimdall_Data.config.spotter.throttleTime then
if Heimdall_Data.config.spotter.debug then
local remainingTime = Heimdall_Data.config.spotter.throttleTime - (time - throttleTable[name])
print(
string.format("[%s] Player %s throttled for %.1f more seconds", ModuleName, name, remainingTime)
)
end
return string.format("Throttled %s", tostring(name))
end
throttleTable[name] = time
local race = UnitRace(unit)
if not race then return string.format("Could not find race for unit %s", tostring(unit)) end
local faction = shared.raceMap[race]
if not faction then return string.format("Could not find faction for race %s", tostring(race)) end
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Player %s is %s (%s)", ModuleName, name, race, faction))
end
local hostile = UnitCanAttack("player", unit)
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Player %s is %s", ModuleName, name, hostile and "hostile" or "friendly"))
end
local doNotify = ShouldNotify(unit, name, faction, hostile)
if not doNotify then
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Skipping notification for %s", ModuleName, name))
end
return string.format("Not notifying for %s", tostring(name))
end
local hp = UnitHealth(unit)
if not hp then return string.format("Could not find hp for unit %s", tostring(unit)) end
local maxHp = UnitHealthMax(unit)
if not maxHp then return string.format("Could not find maxHp for unit %s", tostring(unit)) end
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Player %s health: %s/%s", ModuleName, name, FormatHP(hp), FormatHP(maxHp)))
end
local class = UnitClass(unit)
if not class then return string.format("Could not find class for unit %s", tostring(unit)) end
local zone, subzone = GetZoneText() or "Unknown", GetSubZoneText() or "Unknown"
if Heimdall_Data.config.spotter.zoneOverride then
zone = Heimdall_Data.config.spotter.zoneOverride or ""
subzone = ""
end
local x, y = GetPlayerMapPosition("player")
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Player %s coordinates: %.2f, %.2f", ModuleName, name, x * 100, y * 100))
end
local pvpOn = UnitIsPVP(unit)
local stinky = shared.IsStinky(name) or false
SetMapToCurrentZone()
SetMapByID(GetCurrentMapAreaID())
local areaId = tostring(GetCurrentMapAreaID())
for _, channel in pairs(Heimdall_Data.config.spotter.channels) do
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Processing channel: %s", ModuleName, channel))
end
local locale = shared.GetLocaleForChannel(channel)
local text = string.format(
shared._L("spotterSpotted", locale),
hostile and shared._L("hostile", locale) or shared._L("friendly", locale),
name,
shared._L(class, locale),
stinky and string.format("(%s)", "!!!!") or "",
shared._L(race, locale),
shared._L(faction, locale),
pvpOn and shared._L("pvpOn", locale) or shared._L("pvpOff", locale),
string.gsub(FormatHP(hp), "M", "kk"),
string.gsub(FormatHP(maxHp), "M", "kk"),
shared._L(zone, locale),
shared._L(subzone, locale),
areaId,
x * 100,
y * 100
)
---@type Message
local msg = {
channel = "C",
data = channel,
message = text,
}
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Queuing spotter message", ModuleName))
shared.dump(msg)
end
table.insert(shared.messenger.queue, msg)
end
end
local frame = CreateFrame("Frame")
frame:RegisterEvent("NAME_PLATE_UNIT_ADDED")
frame:RegisterEvent("UNIT_TARGET")
frame:SetScript("OnEvent", function(self, event, unit)
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Event received: %s for unit: %s", ModuleName, event, unit or "target"))
end
if not Heimdall_Data.config.spotter.enabled then
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Module disabled, ignoring event", ModuleName))
end
return
end
if event == "UNIT_TARGET" then unit = "target" end
local err = NotifySpotted(unit)
if err then
if Heimdall_Data.config.spotter.debug then
print(string.format("[%s] Error processing unit %s: %s", ModuleName, unit, err))
end
end
end)
if Heimdall_Data.config.spotter.debug then print(string.format("[%s] Module initialized", ModuleName)) end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

83
Modules/StinkyCache.lua Normal file
View File

@@ -0,0 +1,83 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "StinkyCache"
---@class HeimdallStinkyCacheConfig
---@field enabled boolean
---@field debug boolean
---@field commander string
---@field ttl number
---@class HeimdallStinkyCacheData
---@field stinkies table<string, {value: number, timestamp: number}>
---@class StinkyCache
shared.StinkyCache = {
Init = function()
shared.stinkyCache = {
stinkies = {},
}
---@param name string
local function AskCommander(name)
if Heimdall_Data.config.stinkyCache.debug then
print(
string.format(
"[%s] Asking commander %s about %s",
ModuleName,
Heimdall_Data.config.stinkyCache.commander,
name
)
)
end
local messageParts = { "isstinky", name }
local message = table.concat(messageParts, "|")
SendAddonMessage(
Heimdall_Data.config.addonPrefix,
message,
"WHISPER",
Heimdall_Data.config.stinkyCache.commander
)
return
end
local addonMessageFrame = CreateFrame("Frame")
addonMessageFrame:RegisterEvent("CHAT_MSG_ADDON")
addonMessageFrame:SetScript("OnEvent", function(self, event, msg, sender, ...)
if sender == Heimdall_Data.config.stinkyCache.commander then
if Heimdall_Data.config.stinkyCache.debug then
print(
string.format(
"[%s] Received stinky from commander %s: %s",
ModuleName,
Heimdall_Data.config.stinkyCache.commander,
msg
)
)
end
local parts = { strsplit("|", msg) }
local name, value = parts[1], parts[2]
shared.stinkyCache.stinkies[name] = { value = value, timestamp = time() }
else
if Heimdall_Data.config.stinkyCache.debug then
print(string.format("[%s] Received stinky from non-commander %s: %s", ModuleName, sender, msg))
end
local parts = { strsplit("|", msg) }
local command, name = parts[1], parts[2]
if parts[1] == "isstinky" then local res = Heimdall_Data.config.stinkies[parts[2]] end
end
end)
setmetatable(shared.stinkyCache.stinkies, {
__index = function(self, key)
local value = rawget(self, key)
local now = GetTime()
if value == nil or now - value.timestamp > Heimdall_Data.config.stinkyCache.ttl then
AskCommander(key)
end
return rawget(self, key)
end,
})
print(string.format("[%s] Module initialized", ModuleName))
end,
}

364
Modules/StinkyTracker.lua Normal file
View File

@@ -0,0 +1,364 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "StinkyTracker"
---@class Stinky
---@field name string
---@field class string
---@field seenAt number
---@field hostile boolean
---@class HeimdallStinkyTrackerConfig
---@field enabled boolean
---@field debug boolean
---@field ignoredTimeout number
---@field channels string[]
---@class StinkyTrackerData
---@field stinkies ReactiveValue<table<string, Stinky>>
---@field ignored ReactiveValue<table<string, number>>
---@class StinkyTracker
shared.StinkyTracker = {
---@param stinky Stinky
---@return boolean
Track = function(stinky)
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Request to track stinky: %s (%s)", ModuleName, stinky.name, stinky.class))
end
local ignored = shared.stinkyTracker.ignored[stinky.name]
-- TODO: Add a config option for the ignored timeout
if ignored and ignored > GetTime() - 60 then
if Heimdall_Data.config.stinkyTracker.debug then
print(
string.format(
"[%s] Stinky is ignored, not tracking: %s (%s)",
ModuleName,
stinky.name,
stinky.class
)
)
shared.dump(shared.stinkyTracker.ignored:get())
shared.dump(shared.stinkyTracker.stinkies:get())
end
return false
else
-- Timed out or was never ignored
shared.stinkyTracker.stinkies[stinky.name] = nil
end
shared.stinkyTracker.stinkies[stinky.name] = stinky
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Stinky is now tracked: %s (%s)", ModuleName, stinky.name, stinky.class))
shared.dump(shared.stinkyTracker.stinkies:get())
shared.dump(shared.stinkyTracker.ignored:get())
end
return true
end,
---@param name string
---@return nil
Ignore = function(name)
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Request to ignore stinky: %s", ModuleName, name))
end
shared.stinkyTracker.ignored[name] = GetTime()
shared.stinkyTracker.stinkies[name] = nil
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Stinky is now ignored: %s", ModuleName, name))
shared.dump(shared.stinkyTracker.ignored:get())
shared.dump(shared.stinkyTracker.stinkies:get())
end
end,
---@param name string
---@return boolean
IsStinky = function(name)
if not shared.stinkyTracker.stinkies then return false end
if not shared.stinkyTracker.stinkies[name] then return false end
if shared.stinkyTracker.ignored[name] then return false end
return true
end,
---@param callback fun(stinkies: table<string, Stinky>)
---@return nil
OnChange = function(callback) shared.stinkyTracker.stinkies:onChange(callback) end,
---@param callback fun(name: string, stinky: Stinky)
---@return nil
ForEach = function(callback)
---@type table<string, Stinky>
local stinkies = shared.stinkyTracker.stinkies:get()
for name, stinky in pairs(stinkies) do
callback(name, stinky)
end
end,
Init = function()
shared.stinkyTracker = {
stinkies = ReactiveValue.new({}),
ignored = ReactiveValue.new({}),
}
local whoRegex = "([^ -/]+)-?%w*/(%w+)"
---@param msg string
---@return table<string, Stinky>
local function ParseWho(msg)
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Parsing WHO message: '%s'", ModuleName, msg))
end
local stinkies = {}
for name, class in string.gmatch(msg, whoRegex) do
stinkies[name] = {
name = name,
class = class,
seenAt = GetTime(),
hostile = true,
}
if Heimdall_Data.config.stinkyTracker.debug then
print(
string.format(
"[%s] Found hostile player: %s (%s) at %s",
ModuleName,
name,
class,
date("%H:%M:%S", time())
)
)
shared.dump(stinkies)
end
end
return stinkies
end
local seeRegex = "I see %((%w+)%) ([^ -/]+)-?%w*/(%w+)"
---@param msg string
---@return table<string, Stinky>
local function ParseSee(msg)
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Parsing SEE message: '%s'", ModuleName, msg))
end
local stinkies = {}
local aggression, name, class = string.match(msg, seeRegex)
if not name or not class then
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Error: Invalid SEE message format", ModuleName))
end
return stinkies
end
local stinky = {
name = name,
class = class,
seenAt = GetTime(),
hostile = aggression == "hostile",
}
stinkies[name] = stinky
if Heimdall_Data.config.stinkyTracker.debug then
print(
string.format(
"[%s] Found stinky in SEE: %s (%s) - %s at %s",
ModuleName,
name,
class,
aggression,
date("%H:%M:%S", time())
)
)
shared.dump(stinkies)
end
return stinkies
end
local arrivedRegex = "([^ -/]+)-?%w*; c:([^;]+)"
local arrivedRegexAlt = "([^ -/]+)-?%w*%(!!!!%); c:([^;]+)"
---@param msg string
---@return table<string, Stinky>
local function ParseArrived(msg)
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("%s: Parsing arrived message: %s", ModuleName, msg))
end
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
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("%s: No valid stinky found in arrived message", ModuleName))
end
return stinkies
end
local stinky = {
name = name,
class = class,
seenAt = GetTime(),
hostile = true,
}
stinkies[name] = stinky
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("%s: Found stinky in arrived: %s/%s", ModuleName, name, class))
shared.dump(stinkies)
end
return stinkies
end
local frame = CreateFrame("Frame")
frame:RegisterEvent("CHAT_MSG_CHANNEL")
frame:SetScript("OnEvent", function(self, event, msg, sender, ...)
--if Heimdall_Data.config.stinkyTracker.debug then
-- print(string.format("[%s] Event received: %s from %s", ModuleName, event, sender))
--end
if not Heimdall_Data.config.stinkyTracker.enabled then
--if Heimdall_Data.config.stinkyTracker.debug then
-- print(string.format("[%s] Module disabled, ignoring event", ModuleName))
--end
return
end
local channelId = select(6, ...)
local _, channelname = GetChannelName(channelId)
local ok = false
for _, channel in pairs(Heimdall_Data.config.stinkyTracker.channels) do
if channel == channelname then
ok = true
break
end
end
if not ok then
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Ignoring message from non-master channel: %s", ModuleName, channelname))
end
return
end
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Processing message from master channel: %s", ModuleName, sender))
shared.dump(Heimdall_Data.config.stinkyTracker)
end
local stinkies = {}
if string.find(msg, "^who:") then
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Processing WHO message from %s", ModuleName, sender))
end
local whoStinkies = ParseWho(msg)
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Found stinkies in WHO message", ModuleName))
shared.dump(whoStinkies)
end
for name, stinky in pairs(whoStinkies) do
stinkies[name] = stinky
end
end
if string.find(msg, "^I see") then
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Processing SEE message from %s", ModuleName, sender))
end
local seeStinkies = ParseSee(msg)
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Found stinkies in SEE message", ModuleName))
shared.dump(seeStinkies)
end
for name, stinky in pairs(seeStinkies) do
stinkies[name] = stinky
end
end
if string.find(msg, "arrived to") or string.find(msg, "moved to") then
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Processing ARRIVED message from %s", ModuleName, sender))
end
local arrivedStinkies = ParseArrived(msg)
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Found stinkies in ARRIVED message", ModuleName))
shared.dump(arrivedStinkies)
end
for name, stinky in pairs(arrivedStinkies) do
stinkies[name] = stinky
end
end
for name, stinky in pairs(stinkies) do
if shared.stinkyTracker.ignored[name] then
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Ignoring stinky: %s (%s)", ModuleName, name, stinky.class))
end
shared.stinkyTracker.ignored[name] = nil
else
shared.stinkyTracker.stinkies[name] = stinky
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Added stinky: %s (%s)", ModuleName, name, stinky.class))
end
end
end
-- Log total stinky count after processing
if Heimdall_Data.config.stinkyTracker.debug then
local count = 0
for _ in pairs(shared.stinkyTracker.stinkies:get()) do
count = count + 1
end
print(string.format("[%s] Current total stinkies tracked: %d", ModuleName, count))
end
shared.StinkyTracker.ForEach(function(name, stinky)
if shared.AgentTracker.IsAgent(name) then
shared.stinkyTracker.stinkies[name] = nil
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Removed agent from stinkies: %s", ModuleName, name))
end
end
end)
end)
local targetFrame = CreateFrame("Frame")
targetFrame:RegisterEvent("UNIT_TARGET")
targetFrame:SetScript("OnEvent", function(self, event, unit)
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Event received: %s for unit: %s", ModuleName, event, unit or "target"))
end
unit = "target"
if not Heimdall_Data.config.stinkyTracker.enabled then
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Module disabled, ignoring event", ModuleName))
end
return
end
local name = UnitName(unit)
if not UnitIsPlayer(unit) then
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Target %s is not a player, nothing to do", ModuleName, name))
end
return
end
local enemy = UnitCanAttack("player", unit)
if enemy then
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Target %s is enemy - tracking as stinky", ModuleName, name))
end
shared.stinkyTracker.stinkies[name] = {
name = name,
class = UnitClass(unit),
seenAt = GetTime(),
hostile = true,
}
return
end
if not shared.stinkyTracker.stinkies[name] then
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Target %s is friendly and not stinky, nothing to do", ModuleName, name))
end
return
end
if Heimdall_Data.config.stinkyTracker.debug then
print(string.format("[%s] Target %s is friendly and stinky - removing from stinkies", ModuleName, name))
end
shared.stinkyTracker.stinkies[name] = nil
end)
if Heimdall_Data.config.stinkyTracker.debug then print(string.format("[%s] Module initialized", ModuleName)) end
print(string.format("[%s] Module initialized", ModuleName))
end,
}

735
Modules/Whoer.lua Normal file
View File

@@ -0,0 +1,735 @@
local _, shared = ...
---@cast shared HeimdallShared
local ModuleName = "Whoer"
---@class HeimdallWhoConfig
---@field enabled boolean
---@field debug boolean
---@field ignored table<string, boolean>
---@field channels string[]
---@field ttl number
---@field doWhisper boolean
---@field zoneNotifyFor table<string, boolean>
---@field queries string
---@class HeimdallWhoData
---@field updateTicker Timer?
---@field whoTicker Timer?
local whoWaiting = false
---@class Whoer
shared.Whoer = {
Init = function()
if not Heimdall_Data.who then Heimdall_Data.who = {} end
if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end
---@type table<string, Player>
HeimdallStinkies = {}
---@class Player
---@field name string
---@field guild string
---@field race string
---@field class string
---@field zone string
---@field lastSeenInternal number
---@field lastSeen string
---@field firstSeen string
---@field seenCount number
---@field stinky boolean?
Player = {
---@param name string
---@param guild string
---@param race string
---@param class string
---@param zone string
---@return Player
new = function(name, guild, race, class, zone)
local self = setmetatable({}, {
__index = Player,
})
self.name = name
self.guild = guild
self.race = race
self.class = class
self.zone = zone
self.lastSeenInternal = GetTime()
self.lastSeen = "never"
self.firstSeen = "never"
self.seenCount = 0
return self
end,
---@return string
ToString = function(self)
local out = string.format(
"%s %s %s\nFirst: %s Last: %s Seen: %3d",
shared.padString(self.name, 16, true),
shared.padString(self.guild, 26, false),
shared.padString(self.zone, 26, false),
shared.padString(self.firstSeen, 10, true),
shared.padString(self.lastSeen, 10, true),
self.seenCount
)
return string.format("|cFF%s%s|r", shared.classColors[self.class], out)
end,
}
---@class WHOQuery
---@field query string
---@field filters WHOFilter[]
WHOQuery = {
---@param query string
---@param filters WHOFilter[]
---@return WHOQuery
new = function(query, filters)
local self = setmetatable({}, {
__index = WHOQuery,
})
self.query = query
self.filters = filters
return self
end,
}
---@class WHOFilter
---@field Run fun(name: string, guild: string, level: number, race: string, class: string, zone: string): boolean
---@field key string
---@type WHOFilter
local NotSiegeOfOrgrimmarFilter = {
Run = function(name, guild, level, race, class, zone)
if not zone then return false end
return zone ~= "Siege of Orgrimmar"
end,
key = "notsoo",
}
---@type WHOFilter
local AllianceFilter = {
Run = function(name, guild, level, race, class, zone)
if not race then return false end
if not shared.raceMap[race] then return false end
return shared.raceMap[race] == "Alliance"
end,
key = "ally",
}
---@class WhoQueryService
---@field queries WHOQuery[]
---@field filters WHOFilter[]
---@field getFilter fun(key: string): WHOFilter?
---@field WhoQueryToString fun(query: WHOQuery): string
---@field WhoQueryFromString fun(query: string): WHOQuery
---@field WhoQueriesToString fun(queries: WHOQuery[]): string
---@field WhoQueriesFromString fun(queries: string): WHOQuery[]
shared.WhoQueryService = {
queries = {},
filters = {
NotSiegeOfOrgrimmarFilter,
AllianceFilter,
},
---@param key string
---@return WHOFilter?
getFilter = function(key)
for _, filter in pairs(shared.WhoQueryService.filters) do
if filter.key == key then return filter end
end
return nil
end,
---@param query WHOQuery
---@return string
WhoQueryToString = function(query)
local ret = ""
ret = ret .. query.query
ret = ret .. ";"
for _, filter in pairs(query.filters) do
ret = ret .. filter.key .. ";"
end
return ret
end,
---@param queries WHOQuery[]
---@return string
WhoQueriesToString = function(queries)
local ret = ""
for _, query in pairs(queries) do
ret = ret .. shared.WhoQueryService.WhoQueryToString(query) .. "\n"
end
return ret
end,
---@param query string
---@return WHOQuery
WhoQueryFromString = function(query)
local queryParts = shared.Split(query, ";")
local filters = {}
for _, filterKey in pairs(queryParts) do
local filter = shared.WhoQueryService.getFilter(filterKey)
if not filter then
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Filter %s not found", ModuleName, filterKey))
end
else
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Filter %s found", ModuleName, filterKey))
end
table.insert(filters, filter)
end
end
if Heimdall_Data.config.who.debug then
print(string.format("[%s] WHO query: %s with %d filters", ModuleName, queryParts[1], #filters))
end
shared.dump(filters)
return WHOQuery.new(queryParts[1], filters)
end,
---@param queryStr string
---@return WHOQuery[]
WhoQueriesFromString = function(queryStr)
local queries = shared.Split(queryStr, "\n")
local ret = {}
for _, query in pairs(queries) do
table.insert(ret, shared.WhoQueryService.WhoQueryFromString(query))
end
return ret
end,
}
shared.WhoQueryService.queries = shared.WhoQueryService.WhoQueriesFromString(Heimdall_Data.config.who.queries)
---@param inputZone string
---@return boolean
shared.Whoer.ShouldNotifyForZone = shared.Memoize(function(inputZone)
if not Heimdall_Data.config.who.debug then
print(string.format("[%s] ShouldNotifyForZone %s", ModuleName, inputZone))
end
for zone, _ in pairs(Heimdall_Data.config.who.zoneNotifyFor) do
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Checking zone %s", ModuleName, zone))
end
if zone == "*" then return true end
if string.find(inputZone, zone) then
if not Heimdall_Data.config.who.debug then
print(
string.format(
"[%s] ShouldNotifyForZone %s is true thanks to %s",
ModuleName,
inputZone,
zone
)
)
end
return true
end
end
if not Heimdall_Data.config.who.debug then
print(string.format("[%s] ShouldNotifyForZone %s is false", ModuleName, inputZone))
end
return false
end)
-----@type WHOQuery[]
--local whoQueries = {
-- WHOQuery.new("g-\"БеспредеЛ\"", {}),
-- WHOQuery.new("g-\"ЗАО бещёки\"", {}),
-- WHOQuery.new("g-\"КОНИЛИНГУСЫ\"", {}),
-- --WHOQuery.new("g-\"Dovahkin\"", {}),
-- WHOQuery.new(
-- "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Human\" r-\"Dwarf\" r-\"Night Elf\"",
-- { NotSiegeOfOrgrimmarFilter, AllianceFilter }),
-- WHOQuery.new(
-- "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Gnome\" r-\"Draenei\" r-\"Worgen\"",
-- { NotSiegeOfOrgrimmarFilter, AllianceFilter }),
-- WHOQuery.new(
-- "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Kul Tiran\" r-\"Dark Iron Dwarf\" r-\"Void Elf\"",
-- { NotSiegeOfOrgrimmarFilter, AllianceFilter }),
-- WHOQuery.new(
-- "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Lightforged Draenei\" r-\"Mechagnome\"",
-- { NotSiegeOfOrgrimmarFilter, AllianceFilter }),
-- WHOQuery.new("Kekv Firobot Tomoki Mld Alltros", {})
--}
local whoQueryIdx = 1
---@type WHOQuery?
local lastQuery = nil
---@param player Player
---@return string?
local function Notify(player)
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Processing notification for player: %s", ModuleName, player.name))
print(
string.format(
"[%s] Player details - Guild: %s, Race: %s, Class: %s, Zone: %s",
ModuleName,
player.guild,
player.race,
player.class,
player.zone
)
)
print(
string.format(
"[%s] Player history - First seen: %s, Last seen: %s, Seen count: %d",
ModuleName,
player.firstSeen,
player.lastSeen,
player.seenCount
)
)
end
if not Heimdall_Data.config.who.enabled then
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Module disabled, skipping notification", ModuleName))
end
return
end
if not player then
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Error: Cannot notify for nil player", ModuleName))
end
return string.format("Cannot notify for nil player %s", tostring(player))
end
if not shared.Whoer.ShouldNotifyForZone(player.zone) then
--if not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
if Heimdall_Data.config.who.debug then
print(
string.format(
"[%s] Skipping notification - Zone '%s' not in notify list",
ModuleName,
player.zone
)
)
end
return string.format("Not notifying for zone %s", tostring(player.zone))
end
for _, channel in pairs(Heimdall_Data.config.who.channels) do
local locale = shared.GetLocaleForChannel(channel)
local text = string.format(
shared._L("whoerNew", locale),
player.name,
player.stinky and "(!!!!)" or "",
shared._L(player.class, locale),
--shared._L(player.race, locale),
shared._L(shared.raceMap[player.race] or "unknown", locale),
player.guild,
shared._L(player.zone, locale)
)
---@type Message
local msg = {
channel = "C",
data = channel,
message = text,
}
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Queuing channel notification", ModuleName))
shared.dump(msg)
end
if Heimdall_Data.config.networkMessenger.enabled then
shared.NetworkMessenger.Enqueue(msg)
elseif Heimdall_Data.config.messenger.enabled then
shared.Messenger.Enqueue(msg)
end
end
--if Heimdall_Data.config.who.doWhisper then
-- if Heimdall_Data.config.who.debug then
-- print(string.format("[%s] Processing whisper notifications for %d recipients", ModuleName,
-- #Heimdall_Data.config.whisperNotify))
-- end
-- for _, name in pairs(Heimdall_Data.config.whisperNotify) do
-- ---@type Message
-- local msg = {
-- channel = "W",
-- data = name,
-- message = text
-- }
-- if Heimdall_Data.config.who.debug then
-- print(string.format("[%s] Queuing whisper to %s", ModuleName, name))
-- end
-- --table.insert(shared.messenger.queue, msg)
-- table.insert(shared.networkMessenger.queue, msg)
-- end
--end
return nil
end
---@param player Player
---@param zone string
---@return string?
local function NotifyZoneChanged(player, zone)
if not Heimdall_Data.config.who.enabled then return end
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
--if not Heimdall_Data.config.who.zoneNotifyFor[zone]
-- and not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
if not shared.Whoer.ShouldNotifyForZone(zone) and not shared.Whoer.ShouldNotifyForZone(player.zone) then
return string.format("Not notifying for zones %s and %s", tostring(zone), tostring(player.zone))
end
for _, channel in pairs(Heimdall_Data.config.who.channels) do
local locale = shared.GetLocaleForChannel(channel)
local text = string.format(
shared._L("whoerMoved", locale),
player.name,
player.stinky and "(!!!!)" or "",
shared._L(player.class, locale),
--shared._L(player.race, locale),
shared._L(shared.raceMap[player.race] or "unknown", locale),
player.guild,
shared._L(zone, locale)
)
---@type Message
local msg = {
channel = "C",
data = channel,
message = text,
}
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Queuing channel notification", ModuleName))
shared.dump(msg)
end
if Heimdall_Data.config.networkMessenger.enabled then
shared.NetworkMessenger.Enqueue(msg)
elseif Heimdall_Data.config.messenger.enabled then
shared.Messenger.Enqueue(msg)
end
end
--if Heimdall_Data.config.who.doWhisper then
-- for _, name in pairs(Heimdall_Data.config.whisperNotify) do
-- ---@type Message
-- local msg = {
-- channel = "W",
-- data = name,
-- message = text
-- }
-- --table.insert(shared.messenger.queue, msg)
-- table.insert(shared.networkMessenger.queue, msg)
-- end
--end
return nil
end
---@param player Player
---@return string?
local function NotifyGone(player)
if not Heimdall_Data.config.who.enabled then return end
if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end
--if not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then
if not shared.Whoer.ShouldNotifyForZone(player.zone) then
return string.format("Not notifying for zone %s", tostring(player.zone))
end
for _, channel in pairs(Heimdall_Data.config.who.channels) do
local locale = shared.GetLocaleForChannel(channel)
local text = string.format(
shared._L("whoerGone", locale),
player.name,
player.stinky and "(!!!!)" or "",
shared._L(player.class, locale),
--shared._L(player.race, locale),
shared._L(shared.raceMap[player.race] or "unknown", locale),
player.guild,
shared._L(player.zone, locale)
)
---@type Message
local msg = {
channel = "C",
data = channel,
message = text,
}
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Queuing channel notification", ModuleName))
shared.dump(msg)
end
if Heimdall_Data.config.networkMessenger.enabled then
shared.NetworkMessenger.Enqueue(msg)
elseif Heimdall_Data.config.messenger.enabled then
shared.Messenger.Enqueue(msg)
end
end
--if Heimdall_Data.config.who.doWhisper then
-- for _, name in pairs(Heimdall_Data.config.whisperNotify) do
-- ---@type Message
-- local msg = {
-- channel = "W",
-- data = name,
-- message = text
-- }
-- --table.insert(shared.messenger.queue, msg)
-- table.insert(shared.networkMessenger.queue, msg)
-- end
--end
return nil
end
local frame = CreateFrame("Frame")
frame:RegisterEvent("WHO_LIST_UPDATE")
frame:SetScript("OnEvent", function(self, event, ...)
if Heimdall_Data.config.who.debug then
print(string.format("[%s] WHO list update received", ModuleName))
print(
string.format("[%s] Query index: %d/%d", ModuleName, whoQueryIdx, #shared.WhoQueryService.queries)
)
end
if not Heimdall_Data.config.who.enabled then
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Module disabled, ignoring WHO update", ModuleName))
end
return
end
---@type WHOQuery?
local query = lastQuery
if not query then
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Error: No active WHO query found", ModuleName))
end
return
end
local results = GetNumWhoResults()
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Processing %d WHO results for query: %s", ModuleName, results, query.query))
end
for i = 1, results do
local name, guild, level, race, class, zone = GetWhoInfo(i)
if Heimdall_Data.config.who.debug then
print(
string.format(
"[%s] Processing result %d/%d: %s/%s/%s",
ModuleName,
i,
results,
name,
class,
zone
)
)
end
local continue = false
---@type WHOFilter[]
local filters = query.filters
for _, filter in pairs(filters) do
if Heimdall_Data.config.who.debug then
print(
string.format(
"[%s] Running filter %s on %s/%s/%s",
ModuleName,
filter.key,
name,
class,
zone
)
)
end
if not filter.Run(name, guild, level, race, class, zone) then
if Heimdall_Data.config.who.debug then
print(
string.format(
"[%s] Player %s filtered out by WHO filter %s",
ModuleName,
name,
filter.key
)
)
end
continue = true
break
end
end
if Heimdall_Data.config.who.ignored[name] then
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Ignoring blacklisted player: %s", ModuleName, name))
end
continue = true
end
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Player %s is not blacklisted", ModuleName, name))
end
if not continue then
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Player %s is not filtered out", ModuleName, name))
end
local timestamp = date("%Y-%m-%dT%H:%M:%S")
local player = HeimdallStinkies[name]
if not player then
if Heimdall_Data.config.who.debug then
print(
string.format("[%s] New player detected: %s (%s) in %s", ModuleName, name, class, zone)
)
end
player = Player.new(name, guild, race, class, zone)
if not Heimdall_Data.who then Heimdall_Data.who = {} end
if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end
local existing = Heimdall_Data.who.data[name]
if existing then
if Heimdall_Data.config.who.debug then
print(
string.format(
"[%s] Found existing data for %s - Last seen: %s, Count: %d",
ModuleName,
name,
existing.lastSeen or "never",
existing.seenCount or 0
)
)
end
player.lastSeen = existing.lastSeen or "never"
player.firstSeen = existing.firstSeen or "never"
player.seenCount = existing.seenCount or 0
end
if player.firstSeen == "never" then
player.firstSeen = timestamp
if Heimdall_Data.config.who.debug then
print(
string.format(
"[%s] First time seeing player: %s at %s",
ModuleName,
name,
timestamp
)
)
end
end
local stinky = shared.IsStinky(name)
if stinky then
if Heimdall_Data.config.who.debug then
print(string.format("[%s] Player %s marked as stinky!", ModuleName, name))
end
player.stinky = true
--PlaySoundFile("Interface\\Sounds\\Domination.ogg", "Master")
-- else
-- PlaySoundFile("Interface\\Sounds\\Cloak.ogg", "Master")
end
local err = Notify(player)
if err then
print(
string.format(
"[%s] Error notifying for %s: %s",
ModuleName,
tostring(name),
tostring(err)
)
)
end
player.lastSeen = timestamp
player.seenCount = player.seenCount + 1
HeimdallStinkies[name] = player
end
player.lastSeenInternal = GetTime()
if player.zone ~= zone then
if Heimdall_Data.config.who.debug then
print(
string.format(
"[%s] Player %s zone changed from %s to %s",
ModuleName,
name,
player.zone,
zone
)
)
end
local err = NotifyZoneChanged(player, zone)
if err then
print(string.format("Error notifying for %s: %s", tostring(name), tostring(err)))
end
end
player.zone = zone
player.lastSeen = timestamp
HeimdallStinkies[name] = player
if not Heimdall_Data.who then Heimdall_Data.who = {} end
if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end
Heimdall_Data.who.data[name] = player
end
end
-- Turns out WA cannot do this (
-- aura_env.UpdateMacro()
-- No longer needed with the hook to friends frame show
-- _G["FriendsFrameCloseButton"]:Click()
end)
do
local function UpdateStinkies()
for name, player in pairs(HeimdallStinkies) do
if player.lastSeenInternal + Heimdall_Data.config.who.ttl < GetTime() then
NotifyGone(player)
--PlaySoundFile("Interface\\Sounds\\Uncloak.ogg", "Master")
HeimdallStinkies[name] = nil
end
end
end
local function Tick()
UpdateStinkies()
C_Timer.NewTimer(0.5, Tick, 1)
end
Tick()
end
do
local function DoQuery()
if not Heimdall_Data.config.who.enabled then return end
local query = shared.WhoQueryService.queries[whoQueryIdx]
if not query then
if Heimdall_Data.config.who.debug then
print(
string.format("[%s] Error: No WHO query found to run at index %d", ModuleName, whoQueryIdx)
)
whoQueryIdx = 1
end
return
end
if Heimdall_Data.config.who.debug then
print(
string.format(
"[%s] Running WHO query %d/%d: %s",
ModuleName,
whoQueryIdx,
#shared.WhoQueryService.queries,
query.query
)
)
print(string.format("[%s] Query has %d filters", ModuleName, #query.filters))
for i, filter in ipairs(query.filters) do
print(string.format("[%s] Filter %d: %s", ModuleName, i, filter.key))
end
end
whoQueryIdx = whoQueryIdx + 1
if whoQueryIdx > #shared.WhoQueryService.queries then whoQueryIdx = 1 end
lastQuery = query
whoWaiting = true
---@diagnostic disable-next-line: param-type-mismatch
SetWhoToUI(1)
SetWhoToUI(1)
SendWho(query.query)
end
local function Tick()
DoQuery()
C_Timer.NewTimer(1, Tick, 1)
end
Tick()
end
local original_FriendsFrame_OnEvent = FriendsFrame_OnEvent
local function my_FriendsFrame_OnEvent(self, event, ...)
if not (event == "WHO_LIST_UPDATE" and whoWaiting) then original_FriendsFrame_OnEvent(self, event, ...) end
end
FriendsFrame_OnEvent = my_FriendsFrame_OnEvent
print(string.format("[%s] Module initialized", ModuleName))
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. Убедитесь, что аддон включен на экране выбора персонажа

BIN
Sounds/MGSSpot.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Sounds/MedicGangsterParadise.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Sounds/OOF.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Sounds/StarScream.ogg (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
Texture/Aura1.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura10.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura100.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura101.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura102.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura103.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura104.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura105.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura106.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura107.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura108.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura109.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura11.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura110.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura111.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura112.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura113.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura114.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura115.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura116.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura117.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura118.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura119.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura12.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura120.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura121.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura122.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura123.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura124.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura125.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura126.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura127.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura128.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura129.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura13.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura130.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura131.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura132.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura133.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura134.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura135.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura136.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura137.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura138.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura139.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura14.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura140.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura141.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura142.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura143.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura144.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura145.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura15.tga (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Texture/Aura16.tga (Stored with Git LFS) Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More